Codex <turn_aborted> 注入链与 TUI 中断路径排查记录(2026-04-28)
结论
这次看到的:
- rollout 里出现
<turn_aborted> ... </turn_aborted> - TUI 显示
Conversation interrupted - tell the model what to do differently. - 回答像“说到一半突然闭嘴”
结论不是 Token Pool 上游 SSE 在注入这段文本,而是 Codex Core 在 Interrupted abort 路径里主动把 marker 写进 history/rollout,然后 TUI 再把 turn_aborted 事件渲染成中断提示。
同时确认:
- 普通“新输入提交”默认 不会 自动 interrupt 上一轮。
- 正常路径是:如果当前 turn 还在跑,新输入会走
turn/steer,不是turn/interrupt。 - 只有显式 interrupt 路径,例如
Esc/Ctrl+C/ 某些主动取消动作,才会进入这条<turn_aborted>注入链。
这次补的本地观测
为避免后续只能靠 rollout 反推,这次给本地 wrapper C:\Users\ASUS-KL\bin\codex.ps1 又补了一层观测:
- 继续保留
turn-abort-trace.jsonl - 新增自动开启 Codex TUI 自带 session log
- 每次启动为本轮生成独立 session log 路径
- 结束时回收并写入 session log 摘要,后续复现时可以直接看有没有真正发出
Op::Interrupt
关键位置:
C:\Users\ASUS-KL\bin\codex.ps1:13- 新增
tui-session-logs根目录
- 新增
C:\Users\ASUS-KL\bin\codex.ps1:189New-TuiSessionLogPath
C:\Users\ASUS-KL\bin\codex.ps1:197Get-TuiSessionLogSummary
C:\Users\ASUS-KL\bin\codex.ps1:417- wrapper 自动设置
CODEX_TUI_RECORD_SESSION=1
- wrapper 自动设置
C:\Users\ASUS-KL\bin\codex.ps1:418- wrapper 自动设置
CODEX_TUI_SESSION_LOG_PATH
- wrapper 自动设置
C:\Users\ASUS-KL\bin\codex.ps1:427- 退出后把 session log 摘要写回
turn-abort-trace.jsonl
- 退出后把 session log 摘要写回
这样下一次再复现时,不只知道“发生了 turn_aborted”,还可以知道:
- 本轮有没有真正发出
Interrupt - 在
Interrupt前有没有先发UserTurn - 是不是先 steer,后 interrupt
当前已确认的本地证据
1. 当前会话里确实存在 Codex 自己注入的 marker
会话文件:
C:\Users\ASUS-KL\.codex\sessions\2026\04\28\rollout-2026-04-28T13-57-29-019dd2a9-fe20-74e0-b612-b291ca0ea34f.jsonl:20- 记录到 synthetic user message:
<turn_aborted> ... </turn_aborted>
- 记录到 synthetic user message:
C:\Users\ASUS-KL\.codex\sessions\2026\04\28\rollout-2026-04-28T13-57-29-019dd2a9-fe20-74e0-b612-b291ca0ea34f.jsonl:21- 紧接着记录:
type = "turn_aborted"reason = "interrupted"
- 紧接着记录:
同一会话后面又重复了一次:
C:\Users\ASUS-KL\.codex\sessions\2026\04\28\rollout-2026-04-28T13-57-29-019dd2a9-fe20-74e0-b612-b291ca0ea34f.jsonl:1008C:\Users\ASUS-KL\.codex\sessions\2026\04\28\rollout-2026-04-28T13-57-29-019dd2a9-fe20-74e0-b612-b291ca0ea34f.jsonl:1014
2. wrapper 观测已经能稳定抓到 abort 事件
当前 trace 文件:
C:\Users\ASUS-KL\.codex\log\turn-abort-trace.jsonl
已经能记录:
- 启动参数
- 父进程信息
- gateway 摘要
- child exit code
- 最近
turn_aborted事件 - 后续复现时的 TUI session log 摘要
源码链路确认
A. <turn_aborted> 是谁注入的
官方 Codex 源码里,真正写 marker 的是 Core 的 interrupt abort 路径:
core/src/tasks/mod.rs- 核心逻辑:
handle_task_abort(...)- 当
reason == TurnAbortReason::Interrupted - 调用
interrupted_turn_history_marker(...) - 再
record_into_history(...) - 再
persist_rollout_items(...) - 然后
flush_rollout() - 最后才发
EventMsg::TurnAborted(...)
也就是说,顺序是:
- Core 先把
<turn_aborted>marker 写进 history / rollout - Core flush rollout
- Core 再发
turn_abortedevent
这和当前 session 文件里的现象完全一致:marker 总是在 turn_aborted event 之前。
官方源码来源:
B. TUI 的“Conversation interrupted...”是谁渲染的
官方 TUI 源码里:
tui/src/chatwidget.rsEventMsg::TurnAborted(ev)收到TurnAbortReason::Interrupted后- 调用
on_interrupted_turn(...)
- 调用
on_interrupted_turn(...)- 默认会往 history 里插入一条 interruption notice
- 文案来自:
interrupted_turn_message(...)- 返回:
Conversation interrupted - tell the model what to do differently. Something went wrong? Hit /feedback to report the issue.
所以这句英文提示不是网关写的,不是 Token Pool 写的,是 Codex TUI 本地 UI 层自己渲染。
官方源码来源:
submit / steer / continue 路径结论
1. 普通新输入提交不会默认 interrupt 上一轮
官方 TUI 路由里,当用户提交新消息时:
chatwidget.rssubmit_user_message_with_history_and_shell_escape_policy(...)- 如果当前
agent_turn_running == true - 它不会先 interrupt
- 而是把这次输入记为
pending_steer - 然后仍然发
AppCommand::user_turn(...)
接着在线程路由层:
tui/src/app/thread_routing.rstry_submit_active_thread_op_via_app_server(...)- 如果当前 thread 有 active turn
AppCommandView::UserTurn会优先走:app_server.turn_steer(...)
- 只有没有 active turn 时才会走:
app_server.turn_start(...)
这意味着:
- 正常“继续说一句/补一句/新输入提交” = steer
- 不等于 interrupt
官方源码来源:
- https://github.com/openai/codex/blob/main/codex-rs/tui/src/chatwidget.rs
- https://github.com/openai/codex/blob/main/codex-rs/tui/src/app/thread_routing.rs
2. 哪种路径会主动 interrupt
已确认至少有一条显式路径:
- 当存在
pending_steers - 当前 task 正在运行
- 用户按
Esc - TUI 会设置
submit_pending_steers_after_interrupt = true - 然后显式发
AppCommand::interrupt()
随后:
- Core 触发
TurnAbortReason::Interrupted - 注入
<turn_aborted> - TUI 收到
TurnAborted on_interrupted_turn(...)再把 pending steers 合并后重新提交
这说明:
- “steer 导致 interrupt”是存在的
- 但它是 显式 interrupt 后再重提 steer
- 不是“所有新输入默认都会 interrupt”
同理,Ctrl+C 对可取消任务也会走 interrupt 路径。
对这次复现的判断
这次 session 里,两个 turn 都是在启动后很短时间内被打断:
- turn
019dd2ce-bd4e-7f73-8096-e30a2598c7d3task_started:2026-04-28T06:37:37.490Zturn_aborted:2026-04-28T06:37:39.344Z- 持续约
1853ms
- turn
019dd2ce-c7d2-7a73-bbed-27d904be2a27task_started:2026-04-28T06:37:40.183Zturn_aborted:2026-04-28T06:37:41.289Z- 持续约
1106ms
从源码与时间线综合判断:
- 这更像是某个本地 interrupt/cancel 动作在 turn 刚开始时就被触发
- 不是 Token Pool 中途把上游流截断后“伪造成
turn_aborted” - 也不能仅凭“我又输入了一句 continue”就断言是 submit 自动打断上一轮
当前仍未 100% 锁死的一点是:
- 这两次具体是
Esc、Ctrl+C、launcher 侧 interrupt,还是别的本地取消入口触发
原因不是源码没线索,而是 当时那几次复现没有开启 TUI session log,所以看不到 from_tui -> op -> Interrupt 的原始证据。
下一次复现时怎么一把锁死
现在 wrapper 已经自动开启:
CODEX_TUI_RECORD_SESSION=1CODEX_TUI_SESSION_LOG_PATH=<per-launch file>
所以下次只要再出现一次“怎么又断流 / turn_aborted”,直接看:
C:\Users\ASUS-KL\.codex\log\turn-abort-trace.jsonl- 找本轮
launch_exit - 看
tui_session_log.path
- 找本轮
- 打开对应
session-*.jsonl - 搜:
"kind":"op""Interrupt""UserTurn"
如果看到:
UserTurn之后没有Interrupt- 那说明不是 submit 自动打断
Interrupt明确存在- 就能继续追是哪个按键/入口触发
验证
- wrapper smoke check:
C:\Users\ASUS-KL\bin\codex.ps1 --version- 已通过
- wrapper trace:
C:\Users\ASUS-KL\.codex\log\turn-abort-trace.jsonl- 已正常写入
launch_init / gateway_result / launch_exit
- 当前结论状态:
- who injected
<turn_aborted>:verified - new submit auto-interrupt previous turn:
rejected as default behavior - exact local interrupt source for the already-happened repro:
not yet captured at op level
- who injected
一句话结论
这次 <turn_aborted> 不是 Token Pool 注入的,也不是上游 SSE 自己吐出来的,而是 Codex Core 在 Interrupted abort 路径里主动写进 rollout 的 marker;TUI 那句 “Conversation interrupted...” 也是本地 UI 自己渲染的。普通 submit 默认走 turn/steer,不是自动 interrupt;下一次复现时,wrapper 新补的 TUI session log 就能直接抓到到底是谁发了 Interrupt。
2026-04-28 第二轮修复:收口双启动链
新结论
这轮继续排本地链路后,确认还有一个确定性的本地问题:
C:\Users\ASUS-KL\bin\codex.ps1- 是现在带有 env 清理、gateway、
turn-abort-trace.jsonl、TUI session log 采集的 canonical wrapper
- 是现在带有 env 清理、gateway、
C:\Users\ASUS-KL\bin\codex.cmd- 却一直没有走上面这条 wrapper,而是直接跳到另一条并行 launcher:
C:\ProgramData\npm-global\codex-launcher.cjs
也就是说,机器上实际同时存在两条活跃启动链:
codex.ps1->C:\Users\ASUS-KL\.codex\gateway.ps1->C:\ProgramData\npm-global\codex.ps1codex.cmd->C:\ProgramData\npm-global\codex-launcher.cjs->C:\ProgramData\npm-global\node_modules\@openai\codex\bin\codex.js
这会造成两个直接问题:
.cmd路径不会经过我们已经加好的 transient env 清理.cmd路径也不会写turn-abort-trace.jsonl/CODEX_TUI_SESSION_LOG_PATH
所以一旦某些启动场景命中 codex.cmd,就会回到“老链路”:
- 观测缺失
- 启动语义不一致
- 可能保留旧 session/control 状态
这就是本机当前最明确、可验证、且能直接修掉的 launcher 层根因。
直接证据
修复前做了一个最小对照:
- 读取
C:\Users\ASUS-KL\.codex\log\turn-abort-trace.jsonl行数- 当时是
14
- 当时是
- 运行:
cmd /c "C:\Users\ASUS-KL\bin\codex.cmd --version"
- 再读 trace 行数
- 仍然是
14
- 仍然是
这说明修复前 codex.cmd 确实完全绕开 C:\Users\ASUS-KL\bin\codex.ps1。
已实施修复
已把:
C:\Users\ASUS-KL\bin\codex.cmd
改成只做两件事:
- 先清空这些 sticky env:
CODEX_INTERACTIVE_CONTROLCODEX_INTERACTIVE_CONTROL_PROMPTCODEX_INTERACTIVE_POLICYCODEX_INTERACTIVE_SESSION_IDCODEX_THREAD_ID
- 然后统一委托到:
C:\Users\ASUS-KL\bin\codex.ps1
也就是把 Windows 的 .cmd 入口也强制收口到同一条 canonical wrapper 真相链。
修复后验证
修复后再次运行:
cmd /c "C:\Users\ASUS-KL\bin\codex.cmd --version"
验证结果:
- 命令正常返回:
codex-cli 0.125.0
turn-abort-trace.jsonl行数从14增加到17- 最新 trace 明确记录到:
parent_name = "cmd.exe"parent_command_line = "\"C:\\Windows\\system32\\cmd.exe\" /c \"C:\\Users\\ASUS-KL\\bin\\codex.cmd --version\""wrapper_path = "C:\\Users\\ASUS-KL\\bin\\codex.ps1"
这证明:
.cmd路径已经不再绕开 wrapper- 后续无论从
PowerShell还是cmd打codex,都会进入同一条清理 + gateway + trace 链
这次修复真正解决了什么
它解决的是 本地 launcher split-brain:
- 不再有一个带 trace/清理的
ps1路径 - 另一个不带 trace/清理的
cmd路径
对这类 turn_aborted 问题,这很关键,因为之后:
- 所有入口都会先清 sticky interactive env
- 所有入口都会产出统一 trace
- 所有入口都能自动尝试采集 TUI session log
也就是说,就算还要继续追某一次具体 Interrupt 的键盘/提交来源,也不会再因为“走错启动链”而失去证据。