Skip to content

fix(codex): 修复提交重试和告警锚点#443

Draft
deepcoldy wants to merge 5 commits into
masterfrom
wt/397-head-9d4cec17-2-commits-938-22
Draft

fix(codex): 修复提交重试和告警锚点#443
deepcoldy wants to merge 5 commits into
masterfrom
wt/397-head-9d4cec17-2-commits-938-22

Conversation

@deepcoldy

Copy link
Copy Markdown
Owner

变更

  • Codex 的 writeInput 增加一次硬失败自愈:首次提交未被 history.jsonl 确认时,先清空 composer,再重新粘贴并再次提交。
  • 重新提交的节奏对 reattach 场景更保守:初次 Enter 延后,避免静默重连窗口里过早按下提交。
  • user_notify 的 deferred warning 现在冻结当次 submit 的 turnId,避免后来的消息把失败卡漂到别的 turn 上。

根因

  • Codex 在 daemon 静默重连后存在“粘进 composer 但 Enter 没真正提交”的窗口,原逻辑只会继续报错,不会自动重驱。
  • worker 之前使用全局 currentBotmuxTurnId 发送 deferred 告警,turn 在队列继续推进后会漂移。

验证

  • pnpm build
  • pnpm vitest run test/write-input.test.ts
  • pnpm vitest run test/session-lifecycle-hooks.test.ts

@deepcoldy

Copy link
Copy Markdown
Owner Author

Codex + Claude 二次 review 补充

已在原修复上补齐重连自愈、告警路由与回归测试,并合入最新 origin/master

修复补强

  • Codex reattach 路径通过 captureViewport() 确认本次 paste 已进入 composer(或出现 pasted-content 占位)后再按 Enter;无 viewport 能力的后端保留旧 settle fallback。
  • 首次提交未确认时执行 C-u 清 composer、重新 paste + submit;tmux paste/send 的 dropped-write false 会向 adapter 透传。
  • 第二次 paste 前再次执行首次 attempt 的 recheck,命中恢复间隙内迟到的 history.jsonl 记录则直接返回,收窄重复提交窗口。
  • turnIdsenderOpenId 跟随 PendingCliInput,并在 await writeInput 前冻结局部值,避免并发消息导致 deferred warning 漂移。
  • user_notify 直接回复失败的原消息 ID,并 @原人类发送者;bot 发送方不 @,失败时回退旧 scoped reply。

影响面

  • CLI 特有行为封装在 codex.ts;其它 CLI 不进入 composer-evidence 分支。
  • 公共 PtyHandle/IPC/queue 字段均为可选或向后兼容;Trae/CoCo/Pi 继续忽略 pasteText 返回值。
  • tmux-pipe dropped paste 现在返回 false;PTY/Tmux/Zellij/Herdr 原有 void 实现仍兼容。
  • fresh / reattach、普通会话 / adopt Codex 路径和告警 direct-reply / fallback 均已核对。

实际验证

  • 修复前核心自愈用例连续 3 次稳定失败submitted:false,只 paste 1 次)。
  • 合入原 PR 自愈后同一用例连续 3 次通过
  • 最终 5 个核心回归连续 5 轮全绿
  • 本地定向:write-input / session-lifecycle-hooks / worker-queue-merge / tmux-pipe-backend 全绿;合 master 后 Claude 独立重跑相关 188 tests 全绿
  • pnpm build:通过(Codex 与 Claude 各自独立执行)。
  • pnpm test7752 passed / 12 failed;12 项均为当前机器 central-platform URL 注入、host-local timezone、既有 dashboard CSS 断言,与本次 diff 无关;定向重跑确认。

二次 review

Claude 完整 review 后给出 P0 无、P1 无、P2 1 项(恢复间隙重复提交窗口);P2 已通过 second-attempt 前 recheck + 真门槛测试处理。Claude delta 复核结论:通过,无阻塞项,可合并

@deepcoldy

Copy link
Copy Markdown
Owner Author

实测重复提交问题:根因与修复

申晗 live 验证发现首次消息会重复进入 Codex。真实 session 证据显示:第一次 Enter 已被 Codex 接受,但 history.jsonl 延迟约 4.5 秒才追加;原实现已在追加前执行第二次 paste,随后又把第一条迟到的 history 误判成第二次提交成功,第二份内容因此进入 Queued follow-up input

修复提交:f6b4e0fb

  • 删除 C-u + 二次 paste,同一 prompt 在所有恢复路径中最多只 paste 一次。
  • 首次提交未确认时,返回延迟 recover;worker 20 秒后先 recheck,只有无 history、无 usage-limit、无 PTY/structured/send 活动的 stuck 状态才尝试恢复。
  • 恢复前再次检查 history,并只检查 viewport 最后一个 composer 区域;能证明原文仍在 composer 时才补按一次 Enter,绝不重粘。
  • stale/dead pane、无 viewport 证据、composer 已清空或显示其他内容时均不恢复,走原告警路径。

验证:

  • 新的生产时序回归在修前连续 3 次稳定得到两份 accepted prompt;修后连续 5 次仅一份,pasteText 始终 1 次。
  • 增加 reattach 晚到 paste 自愈测试:只补 1 次 Enter、仍只 paste 1 次。
  • 增加 viewport 上方历史含原文、底部 composer 已空/占位符的反误判测试:不补 Enter。
  • pnpm vitest run test/write-input.test.ts test/session-lifecycle-hooks.test.ts test/worker-queue-merge.test.ts test/tmux-pipe-backend.test.ts test/terminal-write-auth.test.ts test/submit-confirmation.test.ts:6 files / 195 tests 全绿。
  • pnpm build:通过。
  • Claude 独立 delta review:通过、无阻塞项。

影响面:paste/viewport/Enter 逻辑仍封装在 Codex adapter;公共层只新增可选 recover callback 并在 worker 的 deferred submit-confirmation 路径透传,其他 CLI 不提供该 callback,行为不变。fresh / reattach / adopt 三个 Codex 调用点均覆盖。

deepcoldy added a commit that referenced this pull request Jul 13, 2026
P1-1 fresh 首轮双执行窗口(已接受但 ack 丢/延迟 → 自动 paste 重发,同 #443)
→ 严格三态,exactly-once 优先,不靠 schema 幂等:
- engine `sendFirstTurn()` → 'accepted' | 'not-sent' | 'ambiguous'。首轮 timeout
  non-fatal(engine 保活);`onDispatch` 在 ws.send 成功后置位区分帧是否发出。
- 'not-sent'(帧未 dispatch,ws 未开/send 抛)→ 唯一允许 paste fallback 恰好一次。
- 'accepted'(ack 到 **或** rollout 内匹配本轮 user_message)→ engaged,绝不重发。
  正证据用 `drainCodexRollout().events` 的 `kind==='user'` + prompt 内容匹配
  (`rolloutUserTurnMatches`),排除 thread/start 只建 session_meta 的空 rollout。
- 'ambiguous'(帧已发出后 timeout/断连且无正证据)→ engaged、0 auto-paste、0 queue、
  1 notify;viewer resume 该 thread 自然恢复(真落地用户就能看到)。**绝不靠超时把
  ambiguous 降级 not-landed**(negative evidence 不安全,stop 防不了已发生副作用)。
- 控制流锁:ambiguous 走正常成功 init(不 throw/不 restart),daemon 不拿原 prompt
  重新 init;worker restart IPC 两处均 prompt:''(已核)。原 prompt 绝不进
  pendingMessages/inflightInputs。

P1-2 marker ABA(旧 engine 迟到 child-exit 删掉新 engine marker → 孤儿不可 reap)
→ `removeMarkerIfOwned`:删前校验 marker 的 pid+wsUrl 仍等于本 engine,不匹配不删。

P2 update watcher:命中 update menu → **user_notify**(仍绝不自动 Enter,默认可能是
Update now);抽纯 `decideStartupDialogAction`(update 优先级高于 press-enter)。

worker 编排门槛可测:`orchestrateCodexRpcInit` engage 改三态 enum、`shouldQueueInitialPrompt`
纯函数锁入队 wiring(ambiguous engine-active+queuePrompt=false → 不入队)。

测试:engine 三态(not-sent/accepted-via-rollout/ambiguous)+ marker ABA(旧 exit 不删他人
marker);lifecycle orchestrator 三态分支 + shouldQueueInitialPrompt 入队 wiring +
rolloutUserTurnMatches + decideStartupDialogAction。build 绿,468 定向测试绿。
deepcoldy added a commit that referenced this pull request Jul 20, 2026
P1-1 fresh 首轮双执行窗口(已接受但 ack 丢/延迟 → 自动 paste 重发,同 #443)
→ 严格三态,exactly-once 优先,不靠 schema 幂等:
- engine `sendFirstTurn()` → 'accepted' | 'not-sent' | 'ambiguous'。首轮 timeout
  non-fatal(engine 保活);`onDispatch` 在 ws.send 成功后置位区分帧是否发出。
- 'not-sent'(帧未 dispatch,ws 未开/send 抛)→ 唯一允许 paste fallback 恰好一次。
- 'accepted'(ack 到 **或** rollout 内匹配本轮 user_message)→ engaged,绝不重发。
  正证据用 `drainCodexRollout().events` 的 `kind==='user'` + prompt 内容匹配
  (`rolloutUserTurnMatches`),排除 thread/start 只建 session_meta 的空 rollout。
- 'ambiguous'(帧已发出后 timeout/断连且无正证据)→ engaged、0 auto-paste、0 queue、
  1 notify;viewer resume 该 thread 自然恢复(真落地用户就能看到)。**绝不靠超时把
  ambiguous 降级 not-landed**(negative evidence 不安全,stop 防不了已发生副作用)。
- 控制流锁:ambiguous 走正常成功 init(不 throw/不 restart),daemon 不拿原 prompt
  重新 init;worker restart IPC 两处均 prompt:''(已核)。原 prompt 绝不进
  pendingMessages/inflightInputs。

P1-2 marker ABA(旧 engine 迟到 child-exit 删掉新 engine marker → 孤儿不可 reap)
→ `removeMarkerIfOwned`:删前校验 marker 的 pid+wsUrl 仍等于本 engine,不匹配不删。

P2 update watcher:命中 update menu → **user_notify**(仍绝不自动 Enter,默认可能是
Update now);抽纯 `decideStartupDialogAction`(update 优先级高于 press-enter)。

worker 编排门槛可测:`orchestrateCodexRpcInit` engage 改三态 enum、`shouldQueueInitialPrompt`
纯函数锁入队 wiring(ambiguous engine-active+queuePrompt=false → 不入队)。

测试:engine 三态(not-sent/accepted-via-rollout/ambiguous)+ marker ABA(旧 exit 不删他人
marker);lifecycle orchestrator 三态分支 + shouldQueueInitialPrompt 入队 wiring +
rolloutUserTurnMatches + decideStartupDialogAction。build 绿,468 定向测试绿。
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.

1 participant