【BUG】fork 出的子会话继承父会话未认领的收件箱消息并重复执行 #6555
Replies: 4 comments
|
+1,被这个bug气了一整天,还以为是我的代码有问题…… |
|
你那条补充特别有价值:第 3–4 步之间不需要抢时机 —— 只要锚点 turn 不是最后一个 turn,下一条排队消息必然落在 已验证的修复:
|
| 运行 | 子会话 pendingNextTurn |
子会话自有事件 | 父会话队列 |
|---|---|---|---|
| 未挂插件 | 含父会话排队消息 id | [] |
未受影响 |
| 挂上插件 | [] |
一条持久 agent/inbox/spliced { removedCount: 1, outcome: "canceled" } |
未受影响 |
关键代码路径(与各位的定位一致)
packages/api/session-controller/src/commands.ts:243-246— cut 从boundary.seq + 1一直推进到下一个turn/start,把区间内的agent/inbox/spliced一起扫进子会话 seedpackages/api/session-controller/src/commands.ts:263-268— 该前缀作为seed与inheritedEventCount,并打上meta.isSeeded: truepackages/core/agent-loop/src/inbox.ts:27-56—inboxProjectionDefinition折叠日志里全部 splice,完全不看inheritedEventCount;而packages/core/session/src/index.ts:607其实已经在切点写了session/end-seed { inherited: true }标记,只是投影没读它
插件怎么修
在 agent/created 时,对 header.isSeeded 的会话,只重放 session.inheritedEventCount 之前的事件(按 inbox splice 规则折叠),得到「分叉那一刻正在排队」的精确 id 集合,再用 agent.inbox.remove(id) 持久地移除它们 —— 写入的是 agent/inbox/spliced,不是只改内存。子会话在切点之后为自己排队的新输入不受影响;非 seeded 会话一律不碰,所以 resume 回来的会话保留自己的队列。仓库里另有 12 个契约测试。
边界说明:这只是把解决方案打包成可挂载守护插件,不是上游补丁 —— bundle 补丁够不到 commands.ts 与 inbox 投影内部。真正的 upstream 修复仍应落在那处 cut 条件(或给 inboxProjectionDefinition 加 inheritedEventCount 判断)。在官方合入前,插件让现有部署立刻受益。
Verified fix: dsh-plugin-fork-inbox-guard
- npm: https://www.npmjs.com/package/dsh-plugin-fork-inbox-guard
- Source / raw reproduction transcripts: https://github.com/Robin1987China/dsh-plugin-fork-inbox-guard (
docs/verification.md)
Reproduced end to end on master aa8262ec091 through the real sessionController.fork() (no mock), then re-run with the fix mounted:
| Run | Child pendingNextTurn |
Child own events | Parent queue |
|---|---|---|---|
| No guard | parent's queued id present | [] |
intact |
| Guard mounted | [] |
one durable agent/inbox/spliced { removedCount: 1, outcome: "canceled" } |
intact |
The relevant code paths are packages/api/session-controller/src/commands.ts:243-246 (the cut walks to the next turn/start, sweeping the agent/inbox/spliced into the seed), commands.ts:263-268, and packages/core/agent-loop/src/inbox.ts:27-56 (the projection folds every splice and never consults inheritedEventCount, even though packages/core/session/src/index.ts:607 already wrote a session/end-seed { inherited: true } marker at the cut).
The guard, on agent/created for a header.isSeeded session, replays only the events before session.inheritedEventCount, then durably removes exactly the identities that were pending at the cut. Input the child queued for itself after the cut is untouched, and non-seeded sessions are never touched.
Scope note: this is a mountable guard, not an upstream patch — a bundle patch cannot reach commands.ts or the inbox projection. The real fix still belongs in the cut condition above.
|
补充:安装方式有更新。 这个修复现在也被 umbrella bundle dsh plugin --profile <你的 profile> add dsh-community-fixes再把 |
|
这个和 #6262 是同一个根因,我在那边验证过并推了一个直接改 commands.ts 里那处 cut 条件的修复,不是运行时守护插件: https://github.com/Mide69/deepseek-harness/tree/fix/session-fork-seed-boundary 思路和 Robin1987China 说的"真正的 upstream 修复应该落在那处 cut 条件"一致:不是完全去掉那个 while 循环(仓库里已经有一个测试依赖它把 turn/end 之后的 request/header 模型选择也带进子会话种子,直接删掉循环会破坏那个测试),而是把带非空 inserted 的 agent/inbox/spliced 当成扫描的硬边界,其余事件类型照常继续扫到下一个 turn/start。加了回归测试复现这个确切场景,跑过完整 typecheck 和 lint,都是干净的。 dsh-plugin-fork-inbox-guard 这个守护插件方案也很扎实,两者可以并存,插件在官方合入前给现有部署止血,分支是给愿意合并的维护者的直接补丁。 |
Uh oh!
There was an error while loading. Please reload this page.
fork 子会话会继承父会话尚未认领的收件箱消息,在自己的首轮重新执行它,新输入被排在队尾。
复现、预期与验收
复现步骤
查询截图广州天气,100字以内告诉我,等 turn 1 完成。是今日的广州天气,等父会话把 B 回答完(父会话 turn 2 完成)。不查了查深圳天气。补充:第 3-4 步之间不需要抢时机。host 的切点是「锚点 turn 的 turn/end 之后一直推进到下一个 turn/start 之前」,只要锚点 turn 的下一个 turn 已存在于日志中,该 turn 的 prompt 插入事件就必然落进 seed(实测 cut=32,被继承项在 seq 31)。
实际结果
子会话 session-6054df05-c33b-4e80-9e55-10706e596d9c:
父会话 session-600371be-f8fb-4060-a696-9bdf75731a15 里 B 已经执行过:seq 32 turn/start turn=2、seq 35 user/message id=5b6b79f4、seq 53 turn/end turn=2 completed。即同一条消息在父、子两个会话里各执行了一次。
预期结果
fork 出的子会话不应把 seq 小于 inheritedEventCount 的 agent/inbox/spliced 插入重建为自己的待执行输入:打开子会话时队列应为空;输入 C 后由 C 开启首个 turn,不重复执行 B。
环境
根因定位
fork 切点把未认领的收件箱插入卷进了 seed:
投影重建时 registry 折叠的是全量内存日志,inheritedEventCount 只作为参数传给 init()。缺陷在收件箱投影的定义:init 不接收 inheritedEventCount,apply 也没有 event.seq 守卫,于是从 seq 0 起的全部 agent/inbox/spliced 都被折叠:
对照:同一版本里 schedule 与 subagentCatalog 两个投影都写了 if (event.seq < state.inheritedEventCount) return state;session/end-seed 的文档也写明边界之前的事件 this lifecycle produced none of them。因此这是收件箱投影漏掉边界守卫的回归,而不是设计意图。
验收条件
All reactions