feat(codex-app): 支持运行中 steer,并完善重启恢复机制 - #588
Conversation
9081efe to
38b0692
Compare
Resolve upstream lifecycle conflicts while preserving Codex App steer correlation, runner freshness, durable restart settlement, and working-directory updates.
Claude 首次 Review — 无阻塞,代码质量高,零回归对本 PR(head 改动逻辑(白话)让 Codex App(app-server 模式)在正在执行一个 Turn 的过程中也能接住中途新消息——通过
验证
重点核对的正确性
非阻塞 nit(P3)
已 @codex 复审。未经 @申晗 确认不合码。 |
|
To use Codex here, create a Codex account and connect to github. |
复审更新 — 确认一个 blocker(codex 发现,Claude 独立复核 CONFIRMED)复审阶段 @codex 抓到一个我首审漏掉的实质回归,我已独立把整条链走通,确认成立。 问题:
|
|
To use Codex here, create a Codex account and connect to github. |
Codex 二次复审结论 — 2 个 blocker,暂不批准复审基线:HEAD 🔴 Blocker 1:无 live worker 的
|
复审更新 — 第二条 blocker(P2,codex 发现,Claude 独立复核 CONFIRMED)@codex 又抓到一条 Riff generation 混淆,我独立把每一环复核过,确认成立,并补一层范围界定。 问题:重启期间 stale Riff
|
|
To use Codex here, create a Codex account and connect to github. |
修复已就绪 — 两处 blocker 已修 + 回归测试(Claude 代修,交 codex-dev2 apply)申晗授权直接代作者修复。两处 blocker 已修复并全量验证,fix 提交在 P1|无 live worker 的假重启(tmux/herdr/zellij)
P2|Riff stale
|
|
To use Codex here, create a Codex account and connect to github. |
复审(codex 发现 / Claude 复核 CONFIRMED)抓到 PR deepcoldy#588 重启链路两处回归,本 commit 修复并补回归测试。 ## P1|无 live worker 的假重启(tmux/herdr/zellij) requestSessionRestart 的无 worker 分支直接 forkWorker,不再像旧 /restart 那样先 killWorker。对「worker 已死但持久 pane 仍存活」的会话(daemon 重启后 restoreActiveSessions 恢复的常见状态),spawnCli 会 reattach 旧 CLI(TmuxBackend.spawn 忽略 bin/args 只 attach-session,物理 CLI 没重启),却仍走 markPromptReady → 发 restart_result:succeeded。 用户看到「已恢复就绪」,但 CLI 根本没重启;卡死 pane 只能等 40s timeout。 修法:requestSessionRestart 无 worker 分支在 forkWorker 前,对**非 adopt** 会话先销毁 存活的持久 pane(killPersistentBackendTarget),强制 spawnCli 走物理 fresh spawn,让成功 回执变真实。 - 仅限持久 pane:getSessionPersistentBackendType/persistentBackendTargetForSession 天然 排除 riff(riff 从不 reattach——总是新建 RiffBackend;其远端任务须跨重启存活以保 follow-up 血缘)。 - adopt 会话跳过:botmux 从不拥有用户的 pane,销毁会破坏 bridge 不变量。 - 抽出纯判定 shouldDestroyPaneBeforeRestart 便于单测 adopt-skip 决策。 附带 defense-in-depth:markPromptReady 里 restart_result:succeeded 只在 backend 真装好 (非 null)时才发,否则保留 attemptId 交给真 replacement / coordinator timeout。 ## P2|Riff stale taskDoneCb 抢占 restart 终态(Riff-only + 时序窗口) RiffBackend 的 fetchAndEmitOutput(taskId).finally(() => taskDoneCb?.()) 可在 destroySession()/kill() 之后 resolve(两者都不清 taskDoneCb、也不 await 在飞的 fetch)。 worker 的 onTaskDone 钩子是三个异步 backend ready/exit 回调里**唯一没做 generation identity check** 的——旁边 onAgentStatus/onExit 都有 `backend !== observedBackend` 围栏。 restart 中 replacementSpawnInProgress=true 期间,stale 回调的 markPromptReady() 会穿过全局 `cliRestartInProgress && !replacementSpawnInProgress` 布尔闸,提前发 restart_result:succeeded 并清 activeRestartAttemptId,吞掉 replacement 的真实失败终态。 修法:onTaskDone 加 `if (backend !== observedBackend) return;`,与隔壁两个回调一致。 ## 测试(test/restart-worker-null-reattach.test.ts,9 用例) - P1:shouldDestroyPaneBeforeRestart 纯判定(owned 销毁 / adopt 跳过)+ requestSessionRestart kill-before-fork 接线 + destroyLivePaneBeforeRestart 只杀已解析目标、无目标 no-op。 - P2:真 RiffBackend 证 taskDoneCb 在 kill() 后仍触发(证明 fence 必要)+ 三个回调 fence 接线 + markPromptReady defense 断言。 - 每处修复均做变异测试验证判别力(回退修复 → 对应测试转红)。 ## 影响面 - 跨后端:P1 仅影响持久 pane(tmux/herdr/zellij),riff/pty 不变;P2 仅 Riff。 - 跨 CLI:P1 的 markPromptReady defense 对所有 CLI 生效但 backend 非空是既有不变量,零行为变化; P2 的 fence 只在 riff 的 onTaskDone。 - 跨会话类型:adopt 会话在 P1 显式豁免(不销毁用户 pane)。 ## 验证 - pnpm build 绿、pnpm exec tsc --noEmit 绿。 - 13 个相关测试文件 392 用例全绿;全量套件对比干净 master 零新增失败。 Co-Authored-By: Riff <noreply@riff.dev>
复审(codex 发现 / Claude 复核 CONFIRMED)抓到 PR deepcoldy#588 重启链路两处回归,本 commit 修复并补回归测试。 ## P1|无 live worker 的假重启(tmux/herdr/zellij) requestSessionRestart 的无 worker 分支直接 forkWorker,不再像旧 /restart 那样先 killWorker。对「worker 已死但持久 pane 仍存活」的会话(daemon 重启后 restoreActiveSessions 恢复的常见状态),spawnCli 会 reattach 旧 CLI(TmuxBackend.spawn 忽略 bin/args 只 attach-session,物理 CLI 没重启),却仍走 markPromptReady → 发 restart_result:succeeded。 用户看到「已恢复就绪」,但 CLI 根本没重启;卡死 pane 只能等 40s timeout。 修法:requestSessionRestart 无 worker 分支在 forkWorker 前,对**非 adopt** 会话先销毁 存活的持久 pane,强制 spawnCli 走物理 fresh spawn,让成功回执变真实。 - 仅限持久 pane:getSessionPersistentBackendType/persistentBackendTargetForSession 天然 排除 riff(riff 从不 reattach;其远端任务须跨重启存活以保 follow-up 血缘)。 - adopt 会话跳过(纯判定 shouldDestroyPaneBeforeRestart):botmux 从不拥有用户 pane。 Fail-safe 硬化(codex 复审观察):kill 原语会吞掉自身失败(TmuxBackend.killSession `catch{}`、Herdr runHerdr 返 false),裸 try/catch 探不到失败的 kill。故 kill 后 PROBE, 仍 'exists' 则重试一次并再 probe;单调推进单个 probe 变量,'unknown' 首探不误判为重试后 存活。三态诊断日志不谎报:missing→info「will relaunch」、unknown→warn「indeterminate, 可能 reattach」、exists→error。存活仍继续 fork(拒 fork 会让会话彻底无法重启,比原 bug 更糟),但留可 grep 的响亮痕迹。彻底防 reattach(forceFresh 信号入 spawnCli)是更大的独立 改动,列 P3 follow-up。 附带 defense-in-depth:markPromptReady 里 restart_result:succeeded 只在 backend 真装好 (非 null)时才发,否则保留 attemptId 交给真 replacement / coordinator timeout。 ## P2|Riff stale taskDoneCb 抢占 restart 终态(Riff-only + 时序窗口) RiffBackend 的 fetchAndEmitOutput(taskId).finally(() => taskDoneCb?.()) 可在 destroySession()/kill() 之后 resolve(两者都不清 taskDoneCb、也不 await 在飞的 fetch)。 worker 的 onTaskDone 钩子是三个异步 backend ready/exit 回调里唯一没做 generation identity check 的——旁边 onAgentStatus/onExit 都有 `backend !== observedBackend` 围栏。 restart 中 replacementSpawnInProgress=true 期间,stale 回调的 markPromptReady() 会穿过 全局布尔闸,提前发 restart_result:succeeded 并清 activeRestartAttemptId,吞掉 replacement 的真实失败终态。 修法:onTaskDone 加 `if (backend !== observedBackend) return;`,与隔壁两个回调一致。 ## 测试(test/restart-worker-null-reattach.test.ts,10 用例) - P1:shouldDestroyPaneBeforeRestart 纯判定(owned 销毁 / adopt 跳过)+ requestSessionRestart kill-before-fork 接线 + destroyLivePaneBeforeRestart 的 probe-retry + 单调单 probe + 三态诊断 + probe 存活不阻断 fork。 - P2:真 RiffBackend 证 taskDoneCb 在 kill() 后仍触发(证明 fence 必要)+ 三个回调 fence 接线 + markPromptReady defense 断言。 - 每处修复均做变异测试验证判别力(回退修复 → 对应测试转红)。 ## 影响面 - 跨后端:P1 仅影响持久 pane(tmux/herdr/zellij),riff/pty 不变;P2 仅 Riff。 - 跨 CLI:markPromptReady defense 对所有 CLI 生效但 backend 非空是既有不变量,零行为变化; P2 fence 只在 riff 的 onTaskDone。 - 跨会话类型:adopt 会话在 P1 显式豁免(不销毁用户 pane)。 ## 验证 - pnpm build 绿、pnpm exec tsc --noEmit 绿。 - 11 个相关测试文件 340 用例全绿;全量套件对比干净 master 零新增失败。 Co-Authored-By: Riff <noreply@riff.dev>
37b9fbf to
2eb4a7a
Compare
Codex 接力落地 — 两个 blocker 修复已推到 PR 分支已将 Claude 提供、双方共同 harden 后的修复 commit 落地内容
独立核对与验证
修复已在 PR 分支,但本次仍未批准、未合并、未部署 daemon;等待申晗最终确认。 |
修复终态(harden 版) — PR head 已更新至
|
|
To use Codex here, create a Codex account and connect to github. |
本次优化重点
本 PR 重点完善 botmux 的 Codex App(app-server)接入体验:当 Codex App 正在执行一个 Turn 时,飞书中途发送的新消息不再只能等待当前 Turn 完成后再开启下一轮,而是通过 app-server 的
turn/steer注入当前 Turn,让 Codex 能在本轮执行过程中及时接收并遵循新的引导。收到,引导成功只是这套能力的用户可见反馈:botmux 仅在 app-server 明确确认 steer 已被接受后才回复该提示,并非无条件发送一条提示消息。核心改动
1. Codex App 支持真实的运行中 steer
turn/steer将中途消息注入当前正在执行的 Turn。expectedTurnId精确绑定目标 Turn,避免消息被引导到错误的执行轮次。steer_accepted后,向对应的飞书消息回复收到,引导成功。2. 完善 steer 的异常边界和降级策略
input_queued、steer_attempt、steer_accepted、steer_rejected_fallback等生命周期事件,方便定位排队、接受、拒绝和异常状态。3. 配套完善 Codex App 重启与运行时新鲜度
/restart与重启卡片统一使用同一套协调流程。4. 修复静默恢复后的截图模式丢失
用户可见变化
收到,引导成功。范围说明
codex-app路径;codexCLI 已使用其原生的运行中输入能力,本 PR 不改变该路径的交互语义。收到,引导成功是真实 steer 被 app-server 接受后的确认反馈,核心改动是 steer 协议接入、消息保序、关联路由和异常降级。上游同步与冲突处理
本分支已同步至
upstream/master的b30e8949。合并时处理了command-handler、worker IPC 类型及 worker 生命周期三处冲突,并同时保留两侧语义:attemptId关联、Runner freshness、输入暂存和 steer 恢复链路。attemptId。验证结果
pnpm exec tsc --noEmit通过。pnpm build通过,生成的 Runtime Build ID 为3b43d3371dad。git diff --cached --check upstream/master通过。飞书实测
input_queued -> steer_attempt -> steer_accepted,随后自动回复收到,引导成功。/restart时,飞书先显示重启中,约 4 秒后新 Runner 达到 Prompt Ready 并显示成功;用户已确认两条状态消息均可见。/restart复用同一协调器,其路由、就绪终态和竞态行为均有自动化测试覆盖。