创建时间:2026-06-06 最后更新:2026-06-06 状态:🔄 P1+P2+P3 已实现并通过单测(typecheck 干净、全量单测 3261 通过);真实 Codex Runtime smoke 待跑;P5/P6 用户决定本轮不做 协作边界:Codex 负责调研、计划、测试审查;Claude Code 负责产品代码修复。Codex 不直接改
src/产品代码。
用户反馈:Codex 正在执行任务时,一旦点击“终止 / Stop”,后续无法发送新的指令,像是整个进程挂住了。这个问题与此前 #578 “中断后发送无响应”症状相似,但当前调研显示 #578 的修复只覆盖了前端 force-abort 兜底,没有覆盖 Codex Runtime 的真实后端中断链路。
本计划要解决的用户结果:
- 用户点击 Stop 后,当前 turn 应进入明确的 interrupted / stopped 状态。
- 同一会话的输入框应恢复可发送。
- 下一条用户指令应能被同一会话接受,不需要新建会话、重启应用或手动 kill 后台进程。
- 如果 Codex 后端没有成功中断,CodePilot 必须暴露可诊断状态,不能让 UI 看起来已停止但 session lock 仍在续租。
| Phase | 内容 | 状态 | 备注 |
|---|---|---|---|
| Phase 0 | 事实核查与边界确认 | ✅ 已完成 | Codex 本轮只读调研 + 相关单测运行 |
| Phase 1 | Interrupt route fan-out 修复 | ✅ 已实现(单测) | interrupt/route.ts 三路 best-effort fan-out(native + codex_runtime + SDK),各自独立 try/catch;pin:interrupt-route-runtime-fanout.test.ts |
| Phase 2 | Codex Runtime 接住已传入的 abort signal | ✅ 已实现(单测) | codex/runtime.ts stream() 读 options.abortController.signal:预启动 bail + active 中断 + abort-before-turnId race(含 listener 后 re-check 闭合窄窗);抽共享 helper issueCodexTurnInterrupt 与 interrupt() 同实现;pin:codex-interrupt-contract.test.ts |
| Phase 3 | session lock 必需 watchdog / bounded cleanup | ✅ 已实现(单测) | 新 session-lock-settle.ts(幂等 / 按 lockId 释放 / 仅持锁时改状态)+ chat/route.ts !autoTrigger abort watchdog(grace 8s 后强制 settle interrupted);行为单测 session-lock-settle.test.ts |
| Phase 4 | P1-P3 Guardrail 与 smoke | 🔄 单测完成、真实 smoke 待跑 | 3 个单测文件已落(fan-out / abort 契约 / lock settler);Codex Runtime 真实 login → long turn → Stop → 同会话下一条 的 smoke 本环境跑不了,待真实凭据补 |
| Phase 5 | terminal 状态同步审计 | ⏸ 本轮不做(用户决定) | P1-P3 smoke 后若仍见状态分裂再展开 |
| Phase 6 | 长 turn / post-tool / no-output 诊断兜底 | ⏸ 本轮不做(用户决定) | P1-P3 smoke 后若仍见 no-output / post-tool 卡死再展开 |
- 用户直接反馈:“Codex 进行中的任务一旦点终止,就无法发送新的指令,整个进程会挂掉。”
- 本仓库历史:
docs/exec-plans/active/issue-tracker.md里 #578 记录为“中断后发送无响应已修”,修复点是stream-session-manager.ts先无条件调度force-abort。 - 新调研发现:前端停止兜底和后端 Codex
turn/interrupt是两条不同链路。前端能本地退出,不代表 Codex app-server turn 已被取消。 - 外部旁证:OpenAI Codex Desktop 有多条 stuck / Stop 失效 /
markedStreaming=true的 issue,但这些 issue 的作者推测不能直接采信,只能作为症状聚类参考。
本地代码证据优先级高于外部 issue 推测。当前最强的本地证据:
src/lib/stream-session-manager.ts的stopStream()会请求/api/chat/interrupt,并在 2 秒后本地 abort 前端 stream。这说明 UI 层已有 #578 兜底。src/app/api/chat/interrupt/route.ts只尝试getRuntime('native')和 SDKconversation.interrupt(),没有调用getRuntime('codex_runtime')。src/lib/codex/runtime.ts已经有interrupt(sessionId),并会调用 Codex app-server 的turn/interrupt。也就是说,Codex Runtime 有中断能力,但 HTTP interrupt route 没接它。src/app/api/chat/route.ts在 send 入口获取 session lock,并把模型流tee()成客户端流和后台collectStreamResponse。lock 释放发生在后台收集流结束回调里。如果 Codex 后端 turn 没结束,后台流可能不结束,lock 会继续续租,下一条指令会被阻塞。
当前结论:根因不是单一缺口,而是三段中断链同时没有闭环:
- Interrupt route 漏接 Codex Runtime:
/api/chat/interrupt没有 fan-out 到getRuntime('codex_runtime')?.interrupt(sessionId)。 - force-abort 信号已传到 Codex Runtime 门口但被丢弃:
chat/route.ts把request.signal转给abortController,并经streamClaude()传入codexRuntime.stream(options);但 Codex runtime 当前不读取options.abortController?.signal,所以前端 2 秒 force-abort 同样无法中断 Codex turn。 - session lock 不是 TTL 自愈,而是可无限期续租:
chat/route.ts每 60 秒 renew 一次 600 秒 lock,clearInterval只在collectStreamResponse完成回调里发生。turn 不结束 → collect 不结束 → interval 不清 → lock 永远续租,用户必须重启/kill 才能恢复。
因此 P1+P2+P3 都是用户这次问题的热修闭环。P1 只是最小代码 diff,不是完整修复;真正需要重点审查的是 P3 的精确 lockId cleanup。
Claude Code 应按 P1+P2+P3 一起闭环推进:
- 在
/api/chat/interrupt中把codex_runtime纳入 interrupt fan-out。 - 在 Codex Runtime 内接住已经传入的
options.abortController?.signal,覆盖turn/start返回前后的 abort race:如果用户早于activeCodexTurns.set()点 Stop,拿到 turnId 后仍应立即turn/interrupt。 - 给
chat/route.ts/collectStreamResponse增加精确 lockId 的 bounded cleanup / watchdog,覆盖turn/interrupt返回但上游没有 terminal event 的最坏情况。 - 对失败路径写清楚诊断日志:Stop 请求是否到达 route、route 是否调用 Codex Runtime、Codex Runtime 是否找到 active turn、
turn/interrupt是否返回、turn/completed(status=interrupted)是否到达。
必须至少覆盖:
- 单元测试:
/api/chat/interrupt对native、codex_runtime和 SDK conversation 都做 best-effort interrupt。 - 单元测试:Codex Runtime 在 abort signal 早于
turn/start返回时,不会永久丢失 interrupt。 - 回归测试:点击 Stop 后,stream snapshot 离开 active;session runtime status 回到 idle;同一 session 下一次 POST 不返回
SESSION_BUSY。 - Smoke:在 Codex Runtime 下启动一个可控长任务,点击 Stop,然后立刻发送第二条消息;第二条要被接受并有可见反馈。
interrupt/route.ts需要 source-pin 或 mock 行为测试,防止未来新增 Runtime 后 route fan-out 再漏。- Codex Runtime interrupt race 需要专门测试:不能只测试“已有 active turn 时能
turn/interrupt”。 - session lock cleanup 需要测试“只释放当前 lockId,不误清后续请求的新 lockId”。
issue-tracker.md中 #578 不应继续被理解为完全关闭;它只修了前端 force-abort,Codex Runtime 路径是新缺口。
MessageInput/ChatView的 Stop 最终调用stopStream(sessionId)。src/lib/stream-session-manager.ts中stopStream()会:- 请求
POST /api/chat/interrupt; - 同时调度 2 秒后的
abortController.abort(); - 这项 #578 修复保证 interrupt endpoint 挂住时,前端不再永久卡在 active。
- 请求
这个修复必要但不充分:它只能保证前端 stream 不被一个挂住的 /api/chat/interrupt 绑死,不能保证后端 Codex turn 已被取消。
当前 src/app/api/chat/interrupt/route.ts 的注释说 “Tries both runtimes”,但实际只有:
- Native Runtime:
getRuntime('native')?.interrupt(sessionId) - Claude SDK:
getConversation(sessionId)?.interrupt()
缺失:
getRuntime('codex_runtime')?.interrupt(sessionId)
这与 streamClaude() 的路由不一致。src/lib/claude-client.ts 在 Codex Account 或 sessionRuntimePin === 'codex_runtime' 时会路由到 getRuntime('codex_runtime'),但停止路径没有对称跟上。
src/lib/codex/runtime.ts 里已有:
activeCodexTurns = new Map<string, { threadId; turnId }>()turn/start返回后activeCodexTurns.set(sessionId, { threadId, turnId })interrupt(sessionId)中调用client.request('turn/interrupt', { threadId, turnId })run_completed/run_failed时activeCodexTurns.delete(sessionId)并closeStream()
官方 Codex app-server Turn API 也说明:turn/interrupt 需要 threadId 和 turnId,成功后返回 {},并应发出 turn/completed 且状态为 interrupted。参考:https://www.mintlify.com/openai/codex/api/turns
所以当前最小修复不是重新设计 Codex interrupt,而是把 HTTP route 接到已存在的 Runtime interrupt。
src/app/api/chat/route.ts 已创建 abortController,并监听 request.signal:
- 客户端断开 / 前端
force-abort会触发 request abort; - route 把它转成
abortController.abort(); streamClaude()再把同一个 controller 传给具体 Runtime;- Codex Runtime 的
stream(options)已收到options.abortController。
缺口在 Codex Runtime 内部:当前 src/lib/codex/runtime.ts 的 stream() 启动 app-server、watch、subscribe、turn/start,但没有读取 options.abortController?.signal。因此 #578 的 2 秒 force-abort 对 Native / SDK 前端流有兜底意义,但在 Codex Runtime 下并不会自动触发 turn/interrupt。
这意味着 Phase 2 不是“新铺一条 abort 信号链”,而是接住一条已经传到 runtime options 的信号。
src/app/api/chat/route.ts 的 send 入口会:
acquireSessionLock(session_id, lockId, ..., 600)- 开始 stream 后每 60 秒
renewSessionLock(session_id, lockId, 600) stream.tee()后把streamForCollect交给collectStreamResponse- 只有
collectStreamResponse完成回调里才releaseSessionLock(session_id, lockId)和setSessionRuntimeStatus(session_id, 'idle')
如果前端被 force-abort 关闭,但后台 collect reader 仍在等 Codex app-server 的 turn 结束,则 lock 不释放。更严重的是:这个 lock 会被每 60 秒续租一次 600 秒,续租 interval 也只在 collect 完成回调里清掉。换句话说,turn 不结束时它不是“等 10 分钟自动恢复”,而是无限期阻塞。这解释了用户说的“整个进程挂住”,也对上外部 issue 中必须 restart / force-kill 才恢复的症状族。
P1+P2 的 load-bearing 假设是:
- Stop 发出
turn/interrupt; - Codex app-server 返回
{}; - 上游随后发
turn/completed status=interrupted; event-mapper.ts转成run_completed;codex/runtime.ts在run_completed | run_failed分支closeStream();collectStreamResponse读到done后释放 lock。
但用户这次反馈和外部 #14251 / #24467 的共同风险,恰恰是“中断/完成后没有可靠 terminal event 或 UI 状态没收口”。所以 P3 必须覆盖 turn/interrupt 发出但 stream 不 close 的情况。设计重点:
- watchdog / bounded cleanup 必须绑定当前
lockId; - 只能释放当前请求持有的 lock,不能清掉后续窗口或后续发送的新 lock;
stream.tee()后取消一个分支不等于源流结束,必须审查 collect 侧的 reader;- cleanup 状态应标为
idle/interrupted/ diagnostic error,不能伪装成正常 completed。
这些 issue 已逐个打开核对。结论分三层:
- Issue 本身真实存在:GitHub 页面、标题、状态、日期、环境和用户描述都能核到。
- 用户描述的现象可采信为“有人报告过”:例如 stuck Thinking、Stop 无效、completed/interrupted 后 UI 仍 active。
- 作者推测的根因不能直接采信:是否是 app-server、trace stream、WSL、image latency、goal、v0.55 regression,都不能无验证地套到 CodePilot。
本地适用性判断:
- 已确认映射到本仓库代码缺口:Stop 后无法恢复发送。证据是
/api/chat/interrupt漏掉codex_runtimefan-out,而 Codex Runtime 已有turn/interrupt。 - 存在相似风险,需审计/补 guardrail:terminal 状态同步、completed/interrupted 后 UI / lock / runtime status 是否一致。
- 上游 core 或环境问题,CodePilot 只能做诊断/兜底:Codex core 长 turn 不产出、post-tool continuation 不恢复、Windows v0.55 silent no output。
| 来源 | 可采信的事实 | 不直接采信的部分 | 对本计划的作用 |
|---|---|---|---|
| openai/codex#24467 | macOS Desktop 线程可长期 spinner;日志出现 latestTurnStatus=interrupted 但 markedStreaming=true;用户提到 interrupted / completed 后 UI 状态未清 |
issue 作者关于具体 Desktop lifecycle 的推测 | 作为“terminal turn 后 UI 状态可能不一致”的旁证;本仓库仍以 session lock + route fan-out 代码证据为主 |
| openai/codex#24287 | Prompt accepted 后 UI stuck Thinking;Stop 失败;后端可能仍在工作;重启后状态可见性不一致 | 多窗口、goal、trace-stream 的归因推测 | 说明“UI 显示停止/无进度”和“后端仍活跃”可能分离;提醒修复时不要只看 composer |
| openai/codex#12852 | Stop 无效;同线程继续发 prompt 仍无限 Thinking;新线程正常;kill 后台进程后输入恢复 | “必须 kill 进程”的处理不等于根因 | 与用户反馈高度相似,支持增加“同线程下一条指令可发送”的 smoke |
| openai/codex#21360 | 长会话、tool-heavy、compaction 后可能留下未完成 core turn;manual abort / interrupt 出现在日志中 | hooks、analytics、image latency 等推测不能套用 CodePilot | 提醒验证非图像长任务和工具返回后的 continuation,不要只做 happy path |
| openai/codex#6279 | v0.55.0 有用户报告 prompt 后无输出 / 静默失败 | 版本回退即根因的推断不足 | 作为 0.55 附近泛化稳定性信号,不作为本计划根因 |
| openai/codex#20754 | Windows Desktop 可在底层任务已完成后仍显示 thinking/running;重启后能看到已完成结果 | WSL/git-root 解析错误是否为根因未证实 | 加入 Phase 5:terminal 状态同步审计,验证 CodePilot completed 后 UI / runtime status / lock 三者一致 |
| openai/codex#14251 | interrupted/disconnected turn 后 thread 可永久 generating;composer disabled;手工补 terminal 事件可恢复 | 手工改 session log 是 workaround,不是修复方案 | 加入 Phase 5:Stop / disconnect 后必须有 terminal 状态或 bounded recovery |
| openai/codex#19980 | Windows 有“thinking but no output”报告,但 issue 内容只有 session id,细节不足 | 缺少复现步骤和日志,不能据此定根因 | 不进入直接修复;仅作为 Phase 6 no-output diagnostics 的弱信号 |
| 问题族 | 是否在 CodePilot 中已确认存在 | 本地依据 | 处理 |
|---|---|---|---|
| Stop 后同会话无法继续发送 | ✅ 已确认有代码缺口 | /api/chat/interrupt 没调用 codex_runtime;session lock 释放依赖后台 collect 结束 |
Phase 1-3 直接修 |
| completed/interrupted 后 UI 仍 active / composer disabled | CodePilot 有 stream snapshot、runtime status、session lock 三套状态;若 terminal SSE 丢失会不同步 | Phase 5 审计 + guardrail | |
| 后端继续跑但前端 trace/progress 消失 | 前端 force-abort 可先关闭 UI;Codex turn 未 interrupt 时后端仍可能活跃 | Phase 1-3 修主要路径;Phase 6 补诊断 | |
| tool outputs 返回后 assistant continuation 不恢复 | ❌ 未确认是 CodePilot bug | 这是上游 Codex core / app-server 报告;CodePilot mapper 对 tool_started/tool_completed 已有映射 | Phase 6 只做 bounded recovery / 日志,不承诺修 core |
| v0.55.0 Windows silent no output | ❌ 未确认是 CodePilot bug | 外部 issue 是旧 Windows Codex 版本报告;缺少本仓库可验证链路 | 不列直接修复;作为版本/平台 smoke 观察项 |
用户可见变化:点 Stop 后,Codex Runtime 下也会真正请求 Codex app-server 中断当前 turn。
不做什么:不改变 Stop 按钮 UI,不改变 Native / SDK Runtime 的既有中断语义,不新增队列功能。
实现路径:
- 修改
src/app/api/chat/interrupt/route.ts:- 保留 native best-effort;
- 增加
getRuntime('codex_runtime')?.interrupt(sessionId); - 保留 SDK
conversation.interrupt(); - 所有分支继续 best-effort,单个 Runtime 报错不能阻止其他 Runtime 尝试。
- 建议日志:
sessionId- attempted runtimes
codex_runtime是否可用- conversation 是否存在
- 不记录 prompt / 文件路径 / credential。
验收:
POST /api/chat/interrupt源码或 mock test 能证明codex_runtime被调用。- 注意:
codexRuntime.interrupt()是 fire-and-forget,route 返回{ interrupted: true }只能证明“发出了中断尝试”,不能证明 Codex turn 已经进入 interrupted terminal 状态。真正的状态收口要靠 Phase 2/3/4 验证。 - Native / SDK 既有测试不回归。
用户可见变化:前端 2 秒 force-abort 不只关闭 UI,也会被 Codex Runtime 读到并转成 turn/interrupt;用户在任务刚开始、Codex app-server 还没返回 turnId 时点 Stop,也不会留下一个后端继续跑的 turn。
不做什么:不把 Codex app-server 协议封装重写,不引入新 SDK。
实现路径:
- 在
src/lib/codex/runtime.ts中监听已经传入的options.abortController?.signal。 - 如果 signal 已经 aborted:
- 在
turn/start前能退出就退出; - 如果
turn/start已发出但还没拿到 turnId,则拿到 turnId 后立即turn/interrupt。
- 在
- 如果 signal 在 active turn 期间 aborted:
- 调用同一条
interrupt(sessionId)或内部 helper; - 避免重复 interrupt 造成噪音。
- 调用同一条
- 需要保证
activeCodexTurns.delete(sessionId)仍只在 terminal 事件或明确 close 路径清理,不留下 stale turnId。
验收:
- 单测模拟 abort before
turn/startresolves。 - 单测模拟 abort after
activeCodexTurns.set()。 - 两种情况下都不应出现 stream 永久 open。
用户可见变化:Stop 后输入框恢复,下一条 prompt 能直接发,不需要重启或新建线程。
不做什么:不在本阶段实现“streaming 中追加消息排队”;那属于 B-022 队列行为,和本计划“Stop 后恢复”分开验收。
实现路径:
- 核查
collectStreamResponse的 reader 在客户端 abort 后是否仍可能永久等待。 - 必须在 request abort / explicit stop 路径上增加 bounded cleanup / watchdog,覆盖
turn/interrupt成功发出但上游不发 terminal event 的情况:- 只释放当前 lockId;
- 不覆盖其他窗口/后续请求的新 lock;
- 清理时写 runtime status
idle或interrupted,不要误报completed。
- 重点审查
stream.tee():客户端分支被 abort 不保证 collect 分支结束;watchdog 应围绕 collect 分支和当前 lockId 设计。 - 如果 release lock 提前于后台 persistence,需确认不会丢 assistant content;Stop 场景可以接受不保存完整 assistant 内容,但不能破坏 DB。
验收:
- Stop 后立即发第二条,chat API 不返回
SESSION_BUSY。 setSessionRuntimeStatus从 running 回到 idle/interrupted。- 如果 Codex app-server 没有发
turn/completed,watchdog 仍能释放当前 lockId。 - 如果 Stop 后用户已经发起新请求,watchdog 不能释放新请求的 lockId。
- 中断的 assistant message 若有部分内容,保存策略符合现有 stopped 行为;无内容时不写假消息。
用户可见变化:这类“Stop 后 UI 恢复但后端没停”的问题以后更难静默回归。
不做什么:不依赖公开 GitHub issue 的复现步骤作为唯一验证。
测试建议:
src/__tests__/unit/interrupt-route-runtime-fanout.test.ts- mock
getRuntime('native') - mock
getRuntime('codex_runtime') - mock
getConversation - 调
POST(),断言三条 best-effort 都尝试。
- mock
- 扩
src/__tests__/unit/codex-interrupt-contract.test.ts- 保留已有
turn/interruptcontract; - 增加 abort race source-pin / behavior pin。
- 保留已有
- 增加 chat route lock recovery 测试或 smoke:
- Codex Runtime 长任务 active;
- Stop;
- 等待 interrupted / stopped;
- 立即发送第二条;
- 断言没有
SESSION_BUSY,UI 能收到第二条状态。
- 增加 lockId 精确性测试:
- old lockId 被 watchdog 清理;
- new lockId 已存在时不被 old watchdog 清理。
Smoke Ledger 初始要求:
| Date | Runtime | Provider | Model | 凭据形态 | 场景 | Result | Evidence |
|---|---|---|---|---|---|---|---|
| 待跑 | codex_runtime | codex_account 或可用 CodePilot provider | 当前默认 Codex 模型 | real login/API key | long turn → Stop → same-session next prompt | 📋 | dev server log + session id + screenshot |
| 待跑 | native | 任意可用 provider | 快速模型 | API key | long turn → Stop → same-session next prompt | 📋 | 确认 fan-out 未回归 native |
| 待跑 | claude_code | Claude Code SDK | 当前默认 Claude 模型 | CLI 登录/env | long turn → Stop → same-session next prompt | 📋 | 确认 SDK conversation interrupt 未回归 |
用户可见变化:即使 Codex turn 是 completed / interrupted / failed,CodePilot 也不会留下“UI 还在跑、输入框不能发、DB lock 还被占”的分裂状态。
触发条件:P1-P3 落地并通过 smoke 后,如果仍出现 completed/interrupted 后 UI、runtime status、lock 不一致,再展开本阶段。不要让 Phase 5 阻塞 P1-P3 hotfix。
不做什么:不照搬 Codex Desktop 的 markedStreaming / local session log 机制;CodePilot 没有同名字段,不能把外部修法机械搬过来。
实现路径:
- 审计并测试三套状态是否一致:
- frontend stream snapshot:
phase应离开 active; - DB/runtime status:
setSessionRuntimeStatus(..., 'idle' | 'interrupted' | error); - session lock:当前 lockId 必须释放,不能影响后续请求。
- frontend stream snapshot:
- 核查
turn/completed status=interrupted:event-mapper.ts已把非 failed 的 status 作为run_completed.finishReason传下去;codex/runtime.ts在run_completed | run_failed上关闭 stream;- 需要补测试覆盖 interrupted status 的端到端状态收口,而不只 source-pin mapper。
- 如果终端 SSE 丢失,增加 bounded recovery:
- 可基于 frontend AbortError / Stop 请求 / route interrupt 结果设置一个短时 watchdog;
- watchdog 只能释放当前 lockId,不能抢后续新请求。
验收:
- completed 后不会残留 running / active。
- interrupted 后不会残留 running / active。
- failed 后不会残留 running / active。
- disconnect / stop 后同会话下一条能发送。
用户可见变化:如果 Codex core 长时间没有 assistant 输出、tool 后不继续、或没有 terminal 事件,CodePilot 应给出可诊断状态和恢复路径,而不是无限 Thinking。
触发条件:P1-P3 落地并通过 smoke 后,如果仍见 no-output / post-tool continuation 卡死,再展开本阶段。不要把上游 core 风险拖成当前 Stop hotfix 的前置条件。
不做什么:不承诺修复上游 Codex core 的 post-tool continuation 或 Windows v0.55 silent no-output;这些需要上游修。CodePilot 只负责不让本应用永久无反馈、无恢复。
实现路径:
- 利用现有
STREAM_IDLE_TIMEOUT_MS和 Codex retry event,补 Codex Runtime 特有诊断:- 最近一次 Codex event type;
- 是否已发
turn/interrupt; - 是否收到
turn/completed; - 是否有 tool_completed 后长时间没有 assistant_delta / run_completed。
- 对 no-output path 做可见 error / status:
- 不把空响应保存成正常 assistant message;
- 明确提示“Codex turn timed out / no terminal event”,并允许下一条继续。
- 如需新增 watchdog,必须加测试证明:
- 正常长工具不会被误杀;
- willRetry=true 不会被当 terminal;
- terminal event 到达后 watchdog 清理。
验收:
- 模拟 tool_completed 后无后续 terminal,最终有 bounded diagnostic,不无限 active。
- 模拟 no-output idle timeout,最终可继续发送。
willRetry=true路径仍等待真实 terminal,不被提前关闭。
- Run Checkpoint 截图保留:此前 Codex 已改
MessageInputblocked path 为 reject,避免PromptInput清空附件;Claude Code review 指出还应补prompt-input.tsxreject 分支 source-pin。这是测试防线缺口,建议另行补测试,不属于本计划的产品代码主线。 - B-022 streaming 期间排队:本计划只保证 Stop 后能恢复发送;“不中断当前任务而排队下一条”是另一个交互能力,不能混在本计划里验收。
- 上游 Codex Desktop stuck issues:可以参考症状和日志字段,但不能把上游 issue 作者的原因推测直接写进 CodePilot 修复方案。CodePilot 先修本地已确认的 route / lock / abort 链路。
- 2026-06-06: Codex 调研确认
/api/chat/interrupt漏掉codex_runtime,而 Codex Runtime 自身已有turn/interrupt实现。将其列为 Phase 1 最小修复。 - 2026-06-06: 不把 OpenAI Codex GitHub Issues 的原因推测直接采信;只把 #24467 / #24287 / #12852 / #21360 / #6279 作为症状聚类和 smoke 设计参考。
- 2026-06-06: #578 历史修复不视为完全覆盖本问题;它解决前端 force-abort 不被 hung interrupt endpoint 绑死,未验证 Codex app-server turn 真正 interrupted。
- 2026-06-06: 明确区分 Stop 后恢复发送和 streaming 期间排队。前者是本计划 P1,后者留在 B-022。
- 2026-06-06: 二次核实外部 issue 后新增 #20754 / #14251 / #19980 到同一文档。结论:外部问题真实存在,但只有 Stop 后无法继续发送已在 CodePilot 中确认有代码缺口;completed/interrupted 状态分裂和 no-output 属于需审计/兜底的风险项,不直接宣称已复现。
- 2026-06-06: Claude Code 复核指出根因应收紧为三段:interrupt route 漏 Codex;abort signal 已传到 Codex Runtime 但被忽略;session lock 由 60s interval 无限续租,非 600s TTL 自愈。接受该审查,调整计划权重:P1+P2+P3 必须共同交付,P5/P6 后置为 smoke 后仍有状态分裂时再展开。
- 2026-06-06: Claude Code 实现 P1+P2+P3(产品代码)。P1
interrupt/route.ts加codex_runtime分支并改掉误导性 "Tries both runtimes" 注释。P2codex/runtime.tsstream()监听已传入的options.abortController.signal,并抽issueCodexTurnInterrupt共享 helper 让interrupt()(route 路径)与 abort handler 同实现(复用 contract,不重写);覆盖预启动 bail / active 中断 / abort-before-turnId race(含 addEventListener 后 re-check 闭合窄窗)。P3 新增 DI 纯函数session-lock-settle.ts(参照stopStreamWith可测试模式),chat/route.ts把 collect 回调与!autoTriggerabort watchdog 都走同一个幂等 settler;释放按 lockId、仅在仍持锁时写 runtime status(避免覆盖新请求的running)。验证:typecheck 干净,全量单测 3261 通过(首跑 4 个已知 SQLite flaky,复跑两次稳定全绿),新增/改写 3 个测试文件。未做:真实 Codex Runtime 凭据 smoke(本环境无 live app-server)。
| Date | Runtime | Provider | Model | 凭据形态 | 场景 | Result | Evidence |
|---|---|---|---|---|---|---|---|
| 待跑 | codex_runtime | codex_account 或可用 CodePilot provider | 当前默认 Codex 模型 | real login/API key | long turn → Stop → same-session next prompt | 📋 |