Replies: 3 comments
|
补充一份独立复现:Windows / PowerShell 7 / Node.js 24.19.0,DSH 0.1.5-alpha.1(5dda764ed3aa172535a7967b06ff95d9cbfe536a)也存在继承 pending inbox 的行为。 用真实 Session 和 inboxProjectionDefinition 做了不调用模型、不加载第三方插件的最小测试:父 Session 中分别保留 next-turn、next-step 的 agent/inbox/spliced,取 snapshotEvents() 作为 isSeeded: true、origin: subagent 的新 Session 种子。对子会话重放 inbox projection 后,两类父级待办均仍存在。这一测试直接证明队列继承;并不把它冒充为完整 GUI 端到端执行测试。 本地候选采用你提到的第二种方向:保留继承事件,在首次构造新 seeded 会话的 ReactLoopAgent 时通过正常 inbox.clear() 追加持久取消 splice。目前候选条件为: if (session.header.isSeeded && session.firstLiveSeq === session.inheritedEventCount) {
this.inbox.clear()
}这样不修改父会话,也不重排已有 splice 坐标;已有自身 live events 的子会话冷恢复时不会再次清空其待办。该方案是本地验证候选,尚需维护者审查不同 fork/activation 模式是否都应采用此策略。 10 项专项回归通过,覆盖:原版队列继承复现、next-turn/next-step 同时清理、父会话不变、新子任务和 steering 保留、冷恢复、普通会话恢复、嵌套分叉、旧日志 splice 坐标、取消事件持久化,以及真实 ReactLoopAgent + projection registry 构造。没有附带用户对话内容或密钥。 独立委派子会话应继承上下文但不自动认领父任务;若有 fork 模式有意接续待办,建议明确区分该语义。 |
|
Your four-step reproduction is exactly right, including the detail that the user's new input ends up queued behind the inherited one. I reproduced this on current master and shipped a mountable guard. Verified fix:
|
| 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.
已验证的修复:dsh-plugin-fork-inbox-guard
- npm:https://www.npmjs.com/package/dsh-plugin-fork-inbox-guard
- 源码 / 复现记录:https://github.com/Robin1987China/dsh-plugin-fork-inbox-guard (
docs/verification.md)
我在 master aa8262ec091 上用真实的 sessionController.fork()(不是模拟)做了端到端复现,再挂上修复重跑:
| 运行 | 子会话 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 判断)。在官方合入前,插件让现有部署立刻受益。
|
Addendum — install path changed. This fix is now also carried by the umbrella bundle dsh plugin --profile <your-profile> add dsh-community-fixesAdd |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
环境
origin/master5dda764ed3)现象
从某轮对话的结果上「分支」出新会话后,子会话并不是全新的:如果分支时收件箱里还有排队提示词(即上一轮运行期间用户提交了下一条消息),子会话首次提交输入时,父会话排队中的提示词会被当作待办直接执行,用户刚输入的内容反而进入排队队列。
复现步骤
agent/inbox/spliced事件(收件箱待办输入),尚未被认领。根因
agent/inbox/spliced { target: "next-turn", inserted: [...] },其在日志中的物理位置是上一轮turn/end之后、下一轮turn/start之前——恰好是 fork 切割时要扫过的区域。packages/api/session-controller/src/commands.ts的fork()在定位边界turn/end后,用while (events[cut].type !== 'turn/start') cut++向后补齐边界轮的尾事件——这条循环把 dangling 的收件箱 splice 记录一并装进了子会话种子。packages/core/agent-loop/src/inbox.ts的收件箱投影(inboxProjectionDefinition.apply)折叠种子里的全部 splice 事件,父会话的排队提示词因此被恢复为子会话 agent 的 pending 输入。claim()取走next-turn队首——即被复活的父会话排队提示词;用户的新输入进入队列排在后面。证据(本地真实会话日志)
fork 时刻的父会话:
fork 后的子会话:
相同的消息 UUID
4e88b854…在父、子两个会话里各执行了一次,证明这是种子导致的收件箱复活,而不是用户重新输入。(从该子会话再次 fork 又复现了一次,连续第三代。)修复建议
任选其一:
fork()计算出cut后,剔除种子中边界turn/end之后的agent/inbox/spliced记录——它们是父会话的活队列状态,不属于对话历史;agent/inbox/spliced { target: "next-turn", start: 0, removedCount: N, inserted: [] }),把继承的待办输入清空。All reactions