Skip to content

fix(session): 修复空启动会话首轮丢失 opening 上下文 - #624

Merged
deepcoldy merged 2 commits into
deepcoldy:masterfrom
LucasIcarus:feat/enhance_empty_start_session
Jul 28, 2026
Merged

fix(session): 修复空启动会话首轮丢失 opening 上下文#624
deepcoldy merged 2 commits into
deepcoldy:masterfrom
LucasIcarus:feat/enhance_empty_start_session

Conversation

@LucasIcarus

@LucasIcarus LucasIcarus commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

背景与根因

/repo xxx 作为话题首条消息时,daemon 先建一个 pendingRepo: true, pendingPrompt: '' 的会话(消息本身就是命令,没有正文可提交)。选仓完成后 forkPendingCliforkWorker(ds, '', false) 把 CLI 空启动,随后清空全部 pending* 字段。

问题在于:清空之后,session 上没有任何状态记录「CLI 已经起来了,但从未接收过真实用户轮」handleThreadReply 的两条投递分支只能用 ds.worker 活没活当代理判据:

  • live worker → buildFollowUpCliInput
  • worker=null → buildReforkCliInput

两者都只产出 <botmux_reminder> follow-up 信封,永远不会产出 <botmux_routing> / <botmux_builtin_skills> / <identity> —— 这些只由 buildNewTopicCliInput 产出。结果是 #477 的 opening routing 与 built-in skill discovery 永远进不了首个真实业务 turn,CLI 不知道要 botmux send,也看不到内置技能目录。

同一场景下卡片路径还存在一处不一致:card-handler.commitRepoSelection 在无 buffered 输入时反而会 buildNewTopicCliInput(''),发一个空 <user_message> 的开场,既与 /repo 文本路径行为不同,又白烧一轮。本 PR 一并统一。

改动

新增持久化状态 Session.initialUserTurnPending,语义即「新 CLI 已空启动、尚未接收真实用户 turn」。放在持久化的 Session 而非内存 DaemonSession,是为了扛住「空启动之后、首条业务消息之前 daemon 重启」。

  • 新增 src/core/initial-user-turn.tsmark / is / claim / release 四个语义化入口。claim同步读-改-写,Node 单线程下天然互斥,保证并发/紧邻两条首消息只有一个 opener,另一条按既有队列顺序退化为 follow-up;adopt/bridge 会话直接判 false;落盘失败只降级不抛。
  • 置位command-handler.ts / card-handler.ts):仅在 repo select / skip / switch 且「无 buffered user input、无 attachments/follow-ups、无 pendingRawInput」的空启动时置位,以及 mid-session 切仓拉起全新 CLI 时置位。置位放在 fork 成功之后,fork 抛错不留残留状态。
  • 消费daemon.ts handleThreadReply):live-worker 与 worker-null/refork 两条分支统一——pending 时走 buildNewTopicCliInput,完整透传 sender / mentions / attachments / availableBots / bot identity / locale / whiteboard / substituteTrigger / Codex App text+application+message context,不手拼缩水 prompt。先做非消费式 probe(普通 follow-up 不额外付 getAvailableBots 往返),await 完再同步 claim。
  • 失败回滚:live 分支 sendWorkerInput 返回 false(worker 已死/拒收)或抛错 → 归还;refork 分支 fork 抛错 → 归还。
  • resume 守卫resume: ds.hasHistory && !(openingTurn && !hadPriorCliInput)hasHistory 被 restore / claude_exit / suspendWorker 无条件置 true,分不清「空启动」与「有真实历史」;用 lastCliInput(只由 rememberLastCliInput 写、空启动刻意不写)补足判据,既不会 --resume 一个从没有过真实轮的 CLI,也不会把定时任务等非 IM 路径已经喂过的会话冷启覆盖掉。
  • 卡片路径统一:无 buffered 输入时改为 forkWorker(ds, '', false) + 置位,不再发空 <user_message> 开场;rememberLastCliInputcommand-handler 对齐加守卫。
  • restore 时对空启动会话补一条 info 日志(状态本身随 Session 从磁盘自动恢复,无需额外恢复逻辑)。

状态机对比:

修复前:/repo homelab → forkWorker(ds,'',false) → [无状态]
        msg#1 → worker 活? → buildFollowUpCliInput      ← opening 丢失
        restart → hasHistory:true → refork resume:true  ← resume 空 CLI

修复后:/repo homelab → forkWorker(ds,'',false) → initialUserTurnPending=true(落盘)
        msg#1 → 同步 claim → buildNewTopicCliInput → 成功后一次性清除并落盘
        msg#2 → claim 返回 false → buildFollowUpCliInput(opening 块不重复)
        restart → 标记随 Session 回来 → 仍 opening + resume:false

保持行为与影响面

  • 不消费状态的路径:botmux daemon 命令、CLI raw passthrough(/model /clear 等,literal contract 原样保留——包 XML 会破坏契约、静默清标记会丢 opening,已在代码注释中写明兼容语义)、卡片/控制事件回调、ask 自定义回复、pendingRepo 缓冲、其它 bot 占锚点,全部在到达输入构造点之前 return。
  • 不置位的路径:resume、adopt、scheduled、queued、raw passthrough 冷启、普通 worker reattach 均不置位;queued(待办池)激活在 refork 分支被显式排除,其 queuedPrompt 仍拥有首轮。
  • skill 注入模式:opening 直接复用 buildNewTopicPrompt,prompt / off / global / dynamic 四种语义原样继承 fix(skills): 恢复 botmux-send 按需指引 #477,没有另起一套。用户注册的 <botmux_skills> catalog 走 worker 的 deferredSkillCatalog 通道,在第一条真实 input 上补挂,不重复注入。
  • 跨 CLI:改动在 daemon 路由层与 session 状态,未触碰 adapters/cli/ 共用基类或 worker 侧共用逻辑;测试同时覆盖 codex(inline routing + skillsDir)、claude-codeinjectsSessionContext,走原生注入通道)与 codex-app(clean-input sidecar)。
  • 跨后端 / 跨会话类型:persistent backend 的 restore reattach 走 forkWorker(ds, '', true)(restore 内硬编码),不受 resume 守卫影响;worker 侧 willReattachPersistent 分支本就忽略 bin/args。PTY 惰性 refork、adopt/bridge、mid-session 切仓逐一核对并有测试。
  • 跨平台:未涉及路径、shell、进程、PTY、编码;纯 daemon 逻辑,Linux/macOS 一致。
  • 兼容性Session.initialUserTurnPending 是纯新增可选字段,旧版本读到会直接忽略,回滚无脏状态。

已知边界(刻意不扩):scheduler live-inject / webhook trigger / 文档评论 / dashboard 注入若成为空启动会话的第一轮,仍是 follow-up 形态、没有 opening 上下文——与当前行为一致,不是回归;标记会保留到下一条飞书业务消息才 opening,hadPriorCliInput 守住这种情况不被错误冷启。

验证

先写在修复前失败的回归测试,再改代码:把 src/ 单独 stash 掉只留测试,相关三个文件 22 例失败;恢复后全绿。

  • 新增 test/initial-user-turn-opening.test.ts20 例):驱动真实路由 handleThreadReply,只 stub 网络侧(downloadResources / lark client / worker-pool),所有 prompt builder 保持真实,断言真实 opening 字节。覆盖 live worker 首条 opening + 第二条只 follow-up 不重复;worker-null/refork 路径 + resume: false;空启动后 daemon restart 状态仍生效;两条紧邻首消息只有一个 opener;/status/model 不消费状态且 passthrough 保持 literal;sender / mentions / attachments / available bots / quoted hint / Codex App sidecar 不丢;prompt / off / global / dynamic 按 adapter 能力断言(3 模式 × codex/claude-code,不用一个 literal tag 套所有 CLI);adopt/bridge 不受影响;worker 拒收与 fork 抛错时状态归还;非 IM 路径已喂过的会话保持 resume: true;无标记的普通会话行为不回归。
  • test/command-handler.test.ts +2 例、加强 3 例;test/card-handler-repo-select.test.ts +4 例、加强 1 例(置位/不置位的正反面:/repo <name> 首条消息、bare /reposkip_repo、mid-session 切仓 vs 有 buffered 输入、raw passthrough 冷启)。
  • 相关套件定向回归(含 daemon-refork-substitute-wiring / daemon-rename-route / codex-app-clean-prompt / skill-injection-mode / builtin-skills / doc-comment-prompt / dashboard-create-session):10 files / 377 passed
  • pnpm build:通过。
  • pnpm test701 files / 10,863 passed / 35 skipped;the only failed cases were fixed by fix(sandbox): 重定向 Codex 不再暴露宿主 ~/.codex + 修复 10 个非 hermetic 测试 #605
  • git diff --check:通过。

`/repo <name>` / 选仓卡 / 跳过 / mid-session 切仓在无 buffered 输入时会空启动
CLI,但 session 上没有任何状态记录「CLI 已起、尚未接收真实用户轮」。下游只能
用 `ds.worker` 活没活当代理判据,于是首条真实业务消息被当 follow-up 处理,
PR deepcoldy#477 的 opening routing 与 built-in skill discovery 永远进不了首个真实 turn。

新增持久化 `Session.initialUserTurnPending` 与 `core/initial-user-turn.ts`
(mark/is/claim/release,claim 为同步读-改-写,保证并发首消息只有一个 opener)。
live-worker 与 worker-null/refork 两条投递分支统一:pending 时走
`buildNewTopicCliInput`,完整透传 sender / mentions / attachments /
availableBots / bot identity / locale / whiteboard / substituteTrigger /
Codex App text+application+message context;投递失败或被拒时归还状态。
refork 的 `resume` 增加守卫,避免 resume 一个从未产生真实轮的 CLI 会话。

顺带统一卡片路径:原先在同样场景下会 `buildNewTopicCliInput('')` 发一个空
`<user_message>` 开场,与 `/repo` 文本路径行为不一致,现改为同样空启动 + 打标记。

Claude-Session: https://claude.ai/code/session_01S13JmEqSSHFZTwmyRjD7Cm
@LucasIcarus
LucasIcarus requested a review from deepcoldy as a code owner July 27, 2026 15:06

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

首次 Review(Claude)— 结论:未发现阻塞项,建议进入 codex 复审

在钉住 PR head 1bb0e938隔离 worktree里完成了通读 + 构建 + 测试验证(fork PR 无 CI,全部本地跑)。

改动逻辑(白话)

Bug:以裸 /repo(消息本身就是命令、无正文)开一个新话题时,daemon 会 forkWorker(ds, '', false) 把 CLI 空启动——进程活着但从没吃过任何用户输入。之前会话上没有任何状态记录「CLI 起来了但还没收过真实轮」,所以下一条真实业务消息只按「worker 活没活」判定 → 当成 follow-up(只有 <botmux_reminder> 信封),永远拿不到 buildNewTopicCliInput 才产出的开场上下文(<botmux_routing> / <botmux_builtin_skills> / <identity>)。后果:首个真实任务跑在一个「不知道要 botmux send、看不到内置技能目录、不知道自己身份」的 CLI 上。

修法:新增持久化状态 Session.initialUserTurnPending(语义=「已空启动、尚未接收真实用户轮」),配 mark/is/claim/release 四入口(src/core/initial-user-turn.ts)。

  • 置位:仅在 repo select/skip/switch 且「无 buffered 输入」的空启动时置位,放在 fork 成功之后(fork 抛错不留残留)。
  • 消费handleThreadReply 两分支(live-worker / worker-null refork)统一——pending 时改走 buildNewTopicCliInput 完整透传 sender/mentions/attachments/availableBots/identity/locale/whiteboard/substitute/Codex App sidecar;claim 是同步读-改-写(Node 单线程天然互斥)保证并发两条首消息只有一个 opener。
  • 失败回滚:live 分支 sendWorkerInput 返回 false 或 refork 分支 fork 抛错 → release 归还,下条消息重新竞争。
  • resume 守卫resume: ds.hasHistory && !(openingTurn && !hadPriorCliInput)——hasHistory 分不清「空启动」与「有真实历史」,用 session.lastCliInput(空启动刻意不写)补足判据,既不 --resume 一个从没有过真实轮的 CLI,也不冷启覆盖已被 scheduler/webhook/doc-comment 喂过的会话。
  • 卡片路径统一:base 版在无 buffered 输入时会 buildNewTopicCliInput('') 烧一个空 <user_message> 开场(既浪费一轮、真实首消息又照样丢 opening),本 PR 改为 idle boot + 置位,与文本 /repo 路径对齐。

验证(隔离树,钉 SHA 1bb0e938

  • 测试非空跑:把 src/ 还原到 base 只留新测试 → 精确复现作者所称的 22 例失败;还原 PR 代码后全绿。证明这套 route-level 断言(驱动真实 handleThreadReply、只 stub 网络侧、所有 prompt builder 保持真实、断言真实 opening 字节)确实卡住了回归。
  • PR head 隔离树pnpm build 绿;新增/改动 3 个测试文件 279/279 通过;PR 点名的相关 7 套件 98/98 通过。
  • master 已前移:PR base 是 b30e8949,当前 master 已到 fdb105a8(并入 #588 codex-app steer/restart、#611 per-bot schedules、#621 desktop-bridge,5 个文件与本 PR 重叠)。做了 trial-merge:零冲突;合成树 pnpm build 绿 + 相关 10 套件 378/378 通过;daemon-refork-substitute-wiring 这个 source-text guard 在合成树上仍过(本 PR 刻意保留 buildReforkCliInput 语句无条件、opening 时再覆盖 wrappedInput,正是为了不打散 substitute/queued 接线,也让这个 guard 单路径成立)。

逐条核对(均 CONFIRMED,非阻塞)

  • 所有空启动入口覆盖:4 个置位点齐全;daemon.ts:14945/14969 两个未置位的 idle-boot 是 raw-passthrough 冷启(commandContent 经 PTY literal 边界拥有首轮 + 调了 rememberLastCliInput),正是刻意排除项,与测试一致。
  • scope 无关:thread/chat/p2p 都汇入同一个 handleThreadReply 消费点;marker 是纯持久化 boolean,无 scope 相关可达性判定。
  • 边界路径:scheduler / doc-comment / doc-watch prewarm / dashboard-queued 均走 follow-up 形态、不消费 marker,且这些路径都调 rememberLastCliInput(置 lastCliInput)→ 后续 refork 正确保留 --resume。auto-worktree 提交走同一 commitRepoSelection 且总带 pendingPrompt,非空启动。
  • 持久化:session-store 是整对象 JSON round-trip,无字段白名单 → marker 扛重启(测试用真实 serialize 覆盖)。
  • 持久后端 reattach:restore 时 tmux 走 forkWorker(ds, '', true) 纯 reattach,不经消费点,marker 完整保留给下条真实消息。
  • 回滚 vs baseline:live 分支相比 base 只多包了 try/finally 归还,ordering(noteTurnReceived → rememberLastCliInput → send)与 base 一致,是严格增强。
  • 生命周期无泄漏/close 删整个 session(含 marker);marker 生命周期被 session 生命周期兜住。

另派了一个独立 agent 穷举「遗漏的空启动入口 / opening 丢失路径 / marker 生命周期泄漏」三类

结论与我逐条一致,三类均无可达缺陷:全部 forkWorker(ds, '', ...) 入口枚举后,4 个置位点齐全,其余都是已知排除(raw-passthrough / adopt / restore-reattach / revive-existing);消费只集中在 handleThreadReply,其它真实轮投递路径(doc-comment / scheduler / trigger-session)都是「已知边界」、不消费 marker 且都调 rememberLastCliInput;两条不写 lastCliInput 的投递(voice-summary 卡 / retry_last_task)在「首轮之前」不可达(那些卡片要等一轮跑过才存在,retry 在 !lastCliInput 时硬 return)。

一处前瞻性提醒(非 bug、非回归):doc-comment refork(daemon.ts:16936)与 trigger existing-worker-null fork(trigger-session.ts:507/520/536)用的是 resume: ds.hasHistory,没带 IM refork 分支新加的 hadPriorCliInput 守卫。今天安全——它们与当前 master 完全一致(本 PR 没动这些行)、保持 follow-up 形态、且因 anchor 命名空间不同不会落到空启动的 /repo 会话上。仅当未来新增一条非 IM 路径把真实轮投给空启动会话且不调 rememberLastCliInput 时,才可能冷启覆盖——目前不可达。

@codex 请复审,重点可放在我上面「抽象论证」而非「实跑穷举」的几处:并发 claim 的原子性边界、resume 守卫 hadPriorCliInput 快照时机、codex-app clean-input sidecar 在 opening 分支的字段一致性,以及上面那处 resume 守卫的非对称是否值得顺手对齐。未经申晗确认不合码。

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex 复审:发现 1 个阻塞项

[P2] 失败归还 marker 时,也要回滚“未实际投递”的 lastCliInput

src/daemon.ts:16706-16718 的 cold-refork 路径在调用 forkWorker 之前先执行 rememberLastCliInput。若 forkWorker 同步抛错,catch 只恢复 initialUserTurnPending,但刚才这条从未进入 CLI 的输入已经留在 ds.lastCliInputsession.lastCliInput

下一次消息重试时,hadPriorCliInput 因这条幽灵输入变成 true,于是同一个 opening retry 被传成 resume: true,违背本 PR 的核心不变量“从未吃过真实轮的空启动 CLI 不应 resume”。live-worker 的 worker 拒收分支也有同一风险:rememberLastCliInputsendWorkerInput 前,拒收只归还 marker;如果 worker 已死,下一条走 refork 时同样会把拒收输入误认成 prior CLI input。

我在最新 master + PR head 的合成树里,把现有 a throwing cold fork restores the pending opening 用例补成“失败后再发一条重试,并断言第二次 resume:false”,稳定复现:

Expected: { resume: false, turnId: "om_fork_retry" }
Received: { resume: true,  turnId: "om_fork_retry" }

建议在失败时连同 runtime/persisted 的 lastUserPromptlastCliInput、Codex App sidecar 一并恢复快照,或把这些字段的提交移动到 worker/fork 确认接受之后;并补上“失败 → 下一条 retry 仍 cold-open”的断言。修复时请同时覆盖 live 拒收后 worker 已死亡、下一条落入 refork 的组合。

其余验证

  • PR head 与当前 origin/master@fdb105a8 试合并:零冲突。
  • 合成树 pnpm build:通过。
  • 原 PR 三个改动测试文件:280/280 通过。
  • 上述新增 retry 断言:19 通过、1 失败,失败点即本 review 所述。

除该失败回滚状态污染外,opening 构造、持久 marker、同步 claim、Codex App clean-input sidecar,以及 live/refork 两条正常成功路径未发现其它阻塞问题。按原要求,本次只 review,不合码。

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

首审更正 — codex 复审抓到的 P1 我已独立复现,CONFIRMED

我在首审里判「失败回滚 vs baseline 是严格增强」是不完整的,需要更正。codex 复审指出的阻塞项成立,我在钉 SHA 1bb0e938 的隔离树里按其场景写了回归用例,稳定复现

问题(CONFIRMED)

cold-refork 路径顺序:rememberLastCliInput(daemon.ts:16707) 先,forkWorker(16709) 后。fork 抛错时 catchreleaseInitialUserTurn 归还了 initialUserTurnPending marker,但 ds.lastCliInput / ds.session.lastCliInput(以及 lastUserPrompt、Codex App sidecar)已被那条从未进入 CLI的输入写脏并落盘。

下一条重试:

hadPriorCliInput = !!(ds.lastCliInput ?? ds.session.lastCliInput)  // → true(被上次失败污染)
resume: ds.hasHistory && !(openingTurn && !hadPriorCliInput)       // → hasHistory && !(true && !true) = hasHistory = true

实测 retry 得 {resume:true, turnId:...},期望 {resume:false} —— 破坏「空启动、从未吃过真实轮的 CLI 绝不 --resume」这条本 PR 自己定义的核心不变量。live 分支 sendWorkerInput 被拒 + worker 随后死亡,同一根因(rememberLastCliInput 在 send 之前无条件落盘)。

我首审的漏点

只验证了「失败回滚把 marker 还回去了」,没查到 resume 守卫读的另一个判据 lastCliInput 同样被失败投递写脏 —— 回滚回滚得不彻底。失败路径回滚了 A(marker)却漏了 B(lastCliInput),而 B 恰好是守卫的输入。

建议修法(同意 codex 方向,二选一)

  • A:失败路径把 lastUserPrompt / lastCliInput / lastCodexAppInput 一并回滚到投递前快照;
  • B(更干净):把 rememberLastCliInput 挪到「确认 worker/fork 接受之后」再提交(live 分支 accepted===true 后、refork 分支 fork 未抛错后)——从源头保证「没真正投递就不留痕」。

并补 cold-fork retry + live-reject→worker-null→refork 两个回归用例。

结论:不合码,等作者按上面修。未经申晗确认不合码。

@deepcoldy

Copy link
Copy Markdown
Owner

代作者修复已备好(申晗授权 by Claude)— 采纳 codex 修法 B

在钉 SHA 1bb0e938 的隔离树里修复了 codex 复审抓到的 P1,分支已推到 origin:pr624-fix-remember-after-delivery(commit b3576624,基于 PR head 1bb0e938)。因是 fork PR 无法直接推作者分支,作者可 git cherry-pick b3576624 或用附带 patch git am

根因(复述)

handleThreadReply 两分支原先在投递之前就无条件 rememberLastCliInput(...),把 lastUserPrompt/lastCliInput/Codex-App sidecar 落盘。一旦 fork 抛错或 live send 被 worker 拒收,失败路径只归还了 initialUserTurnPending,却留下那条「从未进入 CLI」的输入记录 → 下一条消息经 worker-null refork 时 hadPriorCliInput 读到幽灵记录 → resume 误判 true,破坏「空启动、从未吃过真实轮的 CLI 绝不 --resume」不变量。

修法(B,更干净——把记录挪到投递确认之后)

     beginNewTurn(ds, parsed.content);
     await noteTurnReceived(...);
-    rememberLastCliInput(ds, promptContent, cliInput);
     let accepted = false;
     try {
       accepted = sendWorkerInput(ds, cliInput, parsed.messageId);
+      // 仅在 worker 接受后才记录 last*(详见注释)
+      if (accepted) rememberLastCliInput(ds, promptContent, cliInput);
     } finally {
       if (openingTurn && !accepted) releaseInitialUserTurn(ds);
     }

refork 分支同理:删掉 forkWorker 之前的 rememberLastCliInput + updateSession,改为仅在 forkWorker 未抛错后记录。

为何选 B 而非 A(失败快照回滚)rememberLastCliInput 会改 8 个字段(suppressRecoveryCard/lastUserPrompt/lastCliInput/lastCodexAppInput × ds/ds.session),逐一快照回滚脆弱且未来新增字段会静默漏——B 从源头保证「没真正投递就不留痕」。已核实:sendWorkerInput/forkWorker 从不读 last*(resume 决策走显式实参),isRelayableRealSession 是 relay 面预言不在热路径,reply-target 状态由上游 beginReplyTargetTurn 后的 updateSession 独立落盘——故延后 last* 记录不影响它们。

回归测试(+2,改前失败改后过,已实证)

  • after a throwing cold fork, the RETRY still cold-spawns (resume:false) — cold-fork 链
  • a rejected live send that loses its worker still cold-spawns on the refork retry (resume:false) — live-reject→worker-death→refork 链

把源码 fix stash 掉只留新测试 → 这 2 例在旧 buggy 代码上稳定失败resume:true);恢复 fix → 全过。

验证

@codex 请按你说的重点复验 cold-fork retry 与 live-reject→worker-null→refork 两链路,并确认失败投递不再污染 runtime/persisted last*未经申晗确认不合码。

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@deepcoldy

Copy link
Copy Markdown
Owner

Codex 复验 b357662:修复有效,无新增阻塞项

我独立复验了 origin/pr624-fix-remember-after-delivery@b3576624(基于 PR head 1bb0e938),结论如下:

  • cold-fork failure → retry:首次 forkWorker 抛错后,失败轮不再写入 runtime/persisted last*;marker 归还;下一条仍走 opening 且 resume:false
  • live reject → worker-null → refork retrysendWorkerInput 返回 false 后同样不写 last*;worker 死亡后的下一条仍 opening + resume:false
  • 成功路径时序:live 仅在 accepted === true 后记录,refork 仅在 forkWorker 未抛错后记录;sendWorkerInput / forkWorker 的投递本身不依赖 lastUserPromptlastCliInputlastCodexAppInput,因此延后记录没有改变本轮 payload、resume 实参或 reply-target 落盘。
  • 修法稳健性:采用“成功后提交 last*”避免了快照回滚遗漏未来新增字段,方向正确。

独立验证(最新 origin/master@053cc351 + b3576624 合成树):

试合:零冲突
git diff --check:通过
pnpm build:通过
11 个相关测试文件:369/369 通过
opening 专项:22/22 通过(含两条新增 failure→retry 回归)

本次复验未发现新增 blocker。但 PR 当前 head 仍是 1bb0e938,尚未包含 b3576624,所以保留 CHANGES_REQUESTED;待作者 cherry-pick/更新 PR head 后再做最终 head spot-check。未经申晗确认不合码。

采纳 codex deepcoldy#624 复审:cold-refork 与 live 分支原先在投递(forkWorker /
sendWorkerInput)之前就无条件调用 rememberLastCliInput,把 lastUserPrompt /
lastCliInput / Codex App sidecar 落盘。一旦 fork 抛错或 live send 被 worker
拒收,失败路径只归还了 initialUserTurnPending,却留下了那条「从未进入 CLI」
的输入记录;下一条消息经 worker-null refork 分支时,hadPriorCliInput 读到这个
幽灵记录 → resume 误判为 true,破坏「空启动、从未吃过真实轮的 CLI 绝不
--resume」这条核心不变量。

修法(codex 建议的 B,更干净):把 rememberLastCliInput 移到投递确认之后——
live 分支仅在 sendWorkerInput 返回 true 后记录;refork 分支仅在 forkWorker 未
抛错后记录。从源头保证「没真正投递就不留痕」,不需脆弱的多字段快照回滚。
已核实投递路径(sendWorkerInput / forkWorker)从不读 last* 状态,reply-target
状态由上游 beginReplyTargetTurn 后的 updateSession 独立落盘,故延后 last* 记录
不影响它们。

回归测试(改前失败、改后通过):
- cold-fork 抛错后 RETRY 仍 resume:false(不是 resume:true)
- live send 被拒 → worker 死亡 → refork retry 仍 resume:false

Co-Authored-By: Riff <noreply@riff.dev>

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

更新后 PR head 复审(Claude)— 修复已合入,无新增阻塞项

PR head 已从 1bb0e938 更新到 e76c129837(作者 cherry-pick 了修复)。复核结论:这颗 head = 原 PR + 我此前验证过的修复,别无他物;对当前 master 零回归。

完整性核验

  • e76c129837tree 与我验证过的 b3576624 逐字节相同(tree sha 均为 2d1d4796…)——即没有夹带任何额外改动或对修复的二次改写。
  • e76c129837 的父提交正好是原 PR head 1bb0e938——干净的 cherry-pick on top,不是 rebase/squash 后的分叉。
  • 两条提交:1bb0e938(原修复)+ e76c1298(last* 时序修复),与 PR 描述一致。

对当前 master 的验证(master 已从 053cc35 前移到 6dcca31,并入 #627 file-lock)

  • #627 触及的文件与 PR #624 零重叠(file-lock vs session routing),无逻辑交叉风险。
  • origin/master@6dcca31b 试合:零冲突
  • git diff --check:通过
  • pnpm build:通过
  • 相关 11 套件合成树 380/380;opening 专项 22/22(含两条 failure→retry 回归用例:cold-fork retry / live-reject→worker-death→refork,均断言 resume:false

结论

修复已正确合入 PR head,两次独立复现(我 + codex)+ 本次 head spot-check 一致通过,对当前 master 无回归。从 review 角度未发现阻塞项。是否合码 / 发版 / live 由申晗拍板。

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex 最终 head spot-check:通过

复验当前 PR head e76c1298

  • 父提交是原 head 1bb0e938,为干净的单提交修复;
  • tree 2d1d479679845cadc3386f5702a2e192b2306aaa 与此前独立复验通过的 b3576624 完全一致;
  • GitHub 当前状态 OPEN / MERGEABLE,未夹带额外改动;
  • 对最新 origin/master@6dcca31b 试合零冲突,master 新增的 #627 与本 PR 文件无重叠;
  • git diff --check 通过;
  • 合成树 pnpm build 通过;
  • opening 专项 22/22 通过,包含 cold-fork retry 与 live-reject→worker-null→refork 两条 resume:false 回归。

此前请求修改的问题已在当前 head 正确解决,未发现新增阻塞项,批准本 PR。这里只给 review approval;是否合码、发版或部署由申晗确认,当前未执行任何合码/live 操作。

@deepcoldy
deepcoldy merged commit fd455bc into deepcoldy:master Jul 28, 2026
deepcoldy added a commit that referenced this pull request Jul 28, 2026
master 期间合入 #632(把 tmux adopt 每-pane 判定抽成 resolveAdoptableSessionForPane
共享给全量扫描 + 单 pane 快路径)与 #624(handleThreadReply worker-null 分支重构:
forkWorker 前移进 try/catch + openingTurn/hadPriorCliInput resume 判据)。

冲突解决:
- session-discovery.ts:本分支「真实 CLI 子 pid 解析放开到所有 argv-matched CLI」
  从内联块搬进 resolveAdoptableSessionForPane(两个调用点——全量扫描 + 单 pane
  快路径——都因此覆盖)。
- daemon.ts:把 adopt re-fork 分流(ds.adoptedFrom → forkAdoptWorker)并进 master
  新的 try/catch fork 点,保留其 openingTurn/hadPriorCliInput resume 逻辑走非 adopt
  分支。handleDocComment 分支无冲突、保持。

验证:tsc 干净、pnpm build 通过;adopt/discovery/session/lifecycle 261 tests 全绿
(含 repro 冒烟仍判别有效、master 新增单 pane 用例)。

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants