Replies: 1 comment 1 reply
|
The diagnosis holds. Two things I can add: where it stands upstream (your version still has the advance loop; master does not), and one cross-line correction that changes how option 3 has to be written. The loop is gone from master, and it is gone from
|
| line | how the Agent builds its inbox | a seeded child's inbox |
|---|---|---|
0.1.2-rc.1, 0.1.3-alpha.2 |
new Inbox(session, …) folding session.ownEvents() — the suffix after the inherited prefix |
empty; the phantom never appears |
0.1.5-*, 0.1.6-* |
the inbox projection over the session's events |
holds the inherited insert |
I measured that with the real registry, the real agent factory and the real projections on all eight published lines, creating the child exactly as fork() does (seed + inheritedEventCount + meta.isSeeded) and reading the child's inbox before any plugin runs. On 0.1.2/0.1.3 the same copied prefix that produces your seq 191 shape leaves the child's inbox empty — the same prefix, two different children, decided by the inbox implementation rather than by the cut.
So a compensating removal has to be "the identities the child's inbox actually holds and the copied prefix explains": intersect the prefix fold with the public Inbox.nextTurn / Inbox.nextStep reads, and cancel only the intersection. Cancelling from the prefix fold alone would append a durable removal and announce a drop on a line where nothing was ever queued.
Option 3 is implementable with public API
The vantage is a documented invariant rather than a race: agent/created is dispatched inside AgentRegistry.create() before session/fork returns the child id, and AgentFactory.createAgent awaits serial listeners before releasing queued work. One caveat for anyone writing it — on 0.1.2–0.1.5 that event is an un-awaited emit dispatch carrying only { agent } (from 0.1.6 it is an awaited serial dispatch carrying source too), so the listener has to finish its work inline. Inbox.remove(messageId) then durably records one agent/inbox/spliced { outcome: "canceled" } per item, which is exactly your third bullet.
I packaged that as @argszero/cordis-plugin-fork-inbox-guard@0.1.0 — npm, MIT:
npm install @argszero/cordis-plugin-fork-inbox-guard- insert:
- id: fork-inbox-guard
name: '@argszero/cordis-plugin-fork-inbox-guard'It emits one durable notice at the child's first admitted step naming what it dropped (mode: observe reports without cancelling; announce: false cancels silently).
Source and the full evidence trail: https://github.com/argszero/cordis-plugin-fork-inbox-guard
To be clear about what it is: a compensating control, not the fix — it does not change what a fork copies. On 0.1.5 it covers the case you report; on 0.1.6 and later it covers only the carve-out above. Its evidence is 32 behaviour tests against a real @deepseek-ai/cordis context plus 14 packaging tests, the behaviour suite re-run against each published line from the registry with the installed versions asserted to equal the probed ones, and a real-harness probe per line answering control / subset / treatment / inertness — the per-line table is in the README.
Uh oh!
There was an error while loading. Please reload this page.
Summary
session/forkwith anatSeqinside a non-final turn produces a child whose inbox contains a phantom queued input: the child inherits the parent'sagent/inbox/splicedinsert for the next turn's user message, but not theturn/start/ inbox remove that consumed it. The child is idle (no turn starts on seed), so the queued item never runs and cannot be explained from the child's own transcript.Version:
@deepseek-ai/dsh0.1.5-alpha.2 (dsh-api-session-controller).Reproduction
session/fork { sessionId, atSeq: <seq of turn N's turn/end> }where turn N+1 exists.inheritedEventCountcovers seq up to just before turn N+1'sturn/start; the last inherited event isagent/inbox/spliced { target: "next-turn", inserted: [<turn N+1 user message>] }. The child shows one pending queued input and never runs it.Concrete log tail from a parent (turn 10 → 11):
Forking at
atSeq: 188yields a child withinheritedEventCount: 192(seq 0–191), thensession/end-seed,session/title.Cause
packages/api/session-controller(lib/index.js,fork()):Advancing
cutup to the nextturn/startis presumably meant to carry over inter-turn housekeeping (session/title,feedback/*,session/end-seed,command/*), but it also swallows theagent/inbox/splicedinsert that precedes every user-initiatedturn/start. Since the matching removal is emitted afterturn/start, the child's inbox fold ends with an unconsumed item.The same applies to a steer queued mid-turn N that is later promoted to turn N+1 (
next-turninsert inside turn N, removal afterturn/startN+1).Expected
A fork through turn N should have an empty inbox. Either:
boundary.seq + 1and let housekeeping events be re-derived, oragent/inbox/spliced(allow-list only the housekeeping types), oragent/inbox/spliced removedCountfor any items the seed leaves pending.Workaround (client side)
After fork, inspect the child's inbox projection and cancel inherited items, or send a new message (the phantom item runs first).
All reactions