Repository navigation
Replies: 5 comments
|
Source-level confirmation, one correction, and where the seam actually is. Traced on One correction: the in-tree backstop you cite is no longer on master. The two sites, precisely. On append, On your "worth deciding deliberately" note: the append path does already contain one stateful cross-event validator ( Where a plugin can hold this today. The gap is plugin-mountable on the producer side of the log, and the seam is Interim answer to your last line: with the invariant runtime gone and the format validator reachable only on read, your option one (the append path refusing a repeated id) is the only thing that can keep the session usable at the moment of the mistake. Until something enforces the invariant before the write, producer-side uniqueness — or the same enforcement applied at the append boundary from outside the tree — is the whole distance between a re-issued |
|
A guard in plugin form, published and installable — Following the analysis above, the seam that is actually reachable from a plugin is the What it does with a colliding batch:
Nothing is dropped — the batch stays complete and the log stays loadable. The collision is logged instead of being written down as a session that will not open again. Against the real backend and the shipped validator, the unguarded shipment reproduces your failure ( npm install --save @argszero/dsh-ptc-subcall-guard- insert:
- id: ptc-subcall-guard
name: '@argszero/dsh-ptc-subcall-guard'
inject:
- sessionPersistenceThe package ships its own bundle patch, so a profile that installs it by name gets that row without writing it by hand. Configuration: Limits, stated plainly: it prevents the write and repairs nothing already on disk, so a session that already carries a duplicate stays unloadable; it cannot wrap a write handle obtained before it mounted; and the repair is a rename, so anything that reads a Source, tests and a mutation harness: https://github.com/argszero/dsh-ptc-subcall-guard — probed against the |
|
两条实测补充,一条可能对你们的修法有用。 1)这道校验只有关系折叠在做 —— 我把你贴的那六种形状各变异一条到本机一份真实 v4 会话上(它正好带一对
所以:只跑 顺带:本机 13 份会话里只有一份真的带 PTC dispatch,六种变异它全部命中而健康的那对报 2)你报的这个具体形状,看起来是能修的 —— 我们还没做,先把判据说出来。
我们目前只做到「报告」这一步( 3)给写入侧的一点观察。 你们已经把 |
|
Both halves of your split check out at source, and the renumbering you describe is already implemented and published — with one difference in the scheme. 1. The fold is the only rejecting reader — confirmed, and it is reached through exactly one call site.
2. The second-pair rename exists as a published plugin.
One scheme difference worth stating, because it affects your predicate:
End-to-end witness, against the real 3. On your write-side suggestion — agreed, and I would add where the id comes from: the parent half is One correction to my own earlier reply in this thread in light of your point 1: I framed the write path as "unvalidated" — the sharper statement is that the write path validates JSON shape and sequence, and the relationship invariants are enforced only by the decode fold, so the asymmetry is between two different validators, not between validation and its absence. |
|
谢谢,两点都接受,而且你的方案比我的好 —— 我按它实现了。 1)我原来的方案有个你没提但确实存在的洞。 我提的是「把第二次那一对重编号成该 parent 下下一个未占用的 2)一个实现细节,我们这边踩到了,供你核对。 改名不能只改 3)你的守卫和文件级修复解决的是两个不同的面,所以我们两个都做了一遍。 你在 seed 上修,进程内就打开了;但磁盘上那份文件仍然是拒读的 ——
实测(真实 4376 事件会话,带一对 dispatch):造出 #9677 形状后折叠报 我把它作为子集实现在 |
Uh oh!
There was an error while loading. Please reload this page.
Impact: a
run_codesub-call id can repeat inside one Session. The Session v4 formatrequires
subCallIdto be unique, so the collision is written happily and only caught whenthe log is next read — and reading is all-or-nothing, so one repeat costs the whole
session. A real 3,569-event session became permanently unopenable:
No error at write time, no recovery in the product. Reproduced on the released
0.2.0-rc.2; the same code is present onmaster(d74326738).Root cause
packages/core/tools/src/ptc.tsnumbers each sub-call from a counter scoped to oneexecute():Both halves of that id may repeat within one Session, and each for its own reason:
exec.callId— a provider tool-call id. It is not guaranteed to be unique persession; the provider re-issues it when the assistant repeats a call. In the corrupted
session, every collision had its
callIdre-advertised in a later step (4 ids, eachadvertised once per step).
n— restarts at 1 for every newexecute(), which is intended and is pinned bythe existing test at
tests/ptc.spec.ts:625, where three sub-calls of one run readcall-1:ptc:1..3.A call that has to be re-issued therefore restarts the numbering while reusing the parent
id, and emits a second
tool/ptc-dispatch-startcarrying an id whose first dispatch isalready settled.
How it happened in practice
The trigger is a sub-call that outlives its step — polling a background job is the natural
one:
Every colliding id had exactly this shape: one dispatch per step, each immediately
following its own re-advertisement of the same
callId.subCallIdcall_0_ca5abc1b57133bfb:ptc:1call_0_970ffbdb73359cd:ptc:1call_0_91366f045c2e7d01:ptc:1call_0_b844fbaca8e0ee8:ptc:14 colliding ids across 357 dispatches — enough to lose the session. Note it needs a
single dispatch per step: ordinary multi-call usage takes
:ptc:1and:ptc:2insideone run and never reaches this.
Why it is not caught earlier
packages/core/tools/src/invariant.tsalready walks every session event, but checks onlynon-emptiness, root consistency, and turn enclosure — a repeated
subCallIdwith anunchanged
rootCallIdpasses both of its checks.packages/session/session-format-v3-to-v4/src/relationships.ts:190:So the invariant the producer must satisfy is enforced by a different package at read time.
Worth deciding deliberately: either the append path should reject a repeated id (fail fast,
session stays usable) or the producer should make repeats impossible. The patch below does
the second; the first seems worth having anyway, as a separate change.
Proposed fix
Scope the counter to the Session rather than one execution.
executealready holds thesession (
exec.agent?.session, used by both append sites at lines 590 and 617), so noexecution-model change and no new persisted state is needed.
Sessionneeds a type import for theWeakMapkey. The package has no runtime dependency on@deepseek-ai/dsh-session(it is a dev-dependency, andtests/ptc.spec.tsalready importsSession), so this must be a type-only import;invariant.tstypes aWeakMapkeyed bysession without any explicit import, which suggests the ambient path may suffice — worth
checking against the repo's conventions before applying.
Constraints that shape it
log, so any counter that resumes with the log can reach a restored id and corrupt it
again. A
WeakMapstarts empty each process and can only move forward.<parent>:ptc:<n>keeps its shape.README.md:129states consumers treat these idsas opaque and correlate by exact equality, and nothing parses the suffix — so re-scoping
nis invisible downstream.:ptc:1..3, so the assertion attests/ptc.spec.ts:625keeps passing. The counter issession-wide, so a second
run_codecall in the same session continues the numberingrather than restarting — which is the point.
ToolExecution. Anchoring onsessionavoids widening the frozenexecution type.
Worth one sentence in
README.md(andREADME.zh.md) beside the existing id contract:that
nis unique per Session, not perrun_codeexecution. That is the fact the currentwording leaves open, and the one this bug fell through.
Two things to confirm at apply time
exec.agentis always present for arun_codeexecution. Both append sitesreach through
exec.agent?.session, so it should be — but the fallback branch aboveshould be dropped if a session-less dispatch-start cannot reach the log at all.
Sessiontype import path/syntax per the repo's conventions.Regression test
packages/core/tools/tests/ptc.spec.tsalready has everything needed (fakeAgent,runCode). TworunCodecalls shareToolCallId('call-1')deliberately, matching thereal failure:
Today that fails with
starts === ['call-1:ptc:1', 'call-1:ptc:1']— the exact state thevalidator rejects on load. A second case could assert that the single-run numbering still
reads
:ptc:1..3.Verification done here
(
JsonlSessionPersistence.requireStoredLog), and the error above is the verbatim result.:ptc:1..4forone call re-issued over four steps, where the current code yields
:ptc:1four times.released build and from
master.Not claimed: the diff has not been compiled or run inside the repository — it is written
against the sources above, so treat the hunk as a proposal to adapt, not a tested patch.
Context, if useful: I hit this on a session that had been polling a background job, and
repaired the stored log with a script that renumbers the colliding dispatch pairs
(nothing else in the log is touched — those two event types are the only ones carrying a
PTC id, and a start is always followed by its own settle). That is a workaround for
already-damaged logs, not a substitute for the fix.
All reactions