AGENT

codex-turn-aborted-trace-2026-04-28

2026/04/28 25 min read AGENT AUTH CODEX TURN ABORTED TRACE

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 又补了一层观测:

  1. 继续保留 turn-abort-trace.jsonl
  2. 新增自动开启 Codex TUI 自带 session log
  3. 每次启动为本轮生成独立 session log 路径
  4. 结束时回收并写入 session log 摘要,后续复现时可以直接看有没有真正发出 Op::Interrupt

关键位置:

  • C:\Users\ASUS-KL\bin\codex.ps1:13
    • 新增 tui-session-logs 根目录
  • C:\Users\ASUS-KL\bin\codex.ps1:189
    • New-TuiSessionLogPath
  • C:\Users\ASUS-KL\bin\codex.ps1:197
    • Get-TuiSessionLogSummary
  • C:\Users\ASUS-KL\bin\codex.ps1:417
    • wrapper 自动设置 CODEX_TUI_RECORD_SESSION=1
  • C:\Users\ASUS-KL\bin\codex.ps1:418
    • wrapper 自动设置 CODEX_TUI_SESSION_LOG_PATH
  • C:\Users\ASUS-KL\bin\codex.ps1:427
    • 退出后把 session log 摘要写回 turn-abort-trace.jsonl

这样下一次再复现时,不只知道“发生了 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>
  • 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:1008
  • C:\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(...)

也就是说,顺序是:

  1. Core 先把 <turn_aborted> marker 写进 history / rollout
  2. Core flush rollout
  3. Core 再发 turn_aborted event

这和当前 session 文件里的现象完全一致:marker 总是在 turn_aborted event 之前。

官方源码来源:

B. TUI 的“Conversation interrupted...”是谁渲染的

官方 TUI 源码里:

  • tui/src/chatwidget.rs
  • EventMsg::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.rs
    • submit_user_message_with_history_and_shell_escape_policy(...)
    • 如果当前 agent_turn_running == true
    • 它不会先 interrupt
    • 而是把这次输入记为 pending_steer
    • 然后仍然发 AppCommand::user_turn(...)

接着在线程路由层:

  • tui/src/app/thread_routing.rs
    • try_submit_active_thread_op_via_app_server(...)
    • 如果当前 thread 有 active turn
    • AppCommandView::UserTurn 会优先走:
      • app_server.turn_steer(...)
    • 只有没有 active turn 时才会走:
      • app_server.turn_start(...)

这意味着:

  • 正常“继续说一句/补一句/新输入提交” = steer
  • 不等于 interrupt

官方源码来源:

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-e30a2598c7d3
    • task_started2026-04-28T06:37:37.490Z
    • turn_aborted2026-04-28T06:37:39.344Z
    • 持续约 1853ms
  • turn 019dd2ce-c7d2-7a73-bbed-27d904be2a27
    • task_started2026-04-28T06:37:40.183Z
    • turn_aborted2026-04-28T06:37:41.289Z
    • 持续约 1106ms

从源码与时间线综合判断:

  • 这更像是某个本地 interrupt/cancel 动作在 turn 刚开始时就被触发
  • 不是 Token Pool 中途把上游流截断后“伪造成 turn_aborted
  • 也不能仅凭“我又输入了一句 continue”就断言是 submit 自动打断上一轮

当前仍未 100% 锁死的一点是:

  • 这两次具体是 EscCtrl+C、launcher 侧 interrupt,还是别的本地取消入口触发

原因不是源码没线索,而是 当时那几次复现没有开启 TUI session log,所以看不到 from_tui -> op -> Interrupt 的原始证据。

下一次复现时怎么一把锁死

现在 wrapper 已经自动开启:

  • CODEX_TUI_RECORD_SESSION=1
  • CODEX_TUI_SESSION_LOG_PATH=<per-launch file>

所以下次只要再出现一次“怎么又断流 / turn_aborted”,直接看:

  1. C:\Users\ASUS-KL\.codex\log\turn-abort-trace.jsonl
    • 找本轮 launch_exit
    • tui_session_log.path
  2. 打开对应 session-*.jsonl
  3. 搜:
    • "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

一句话结论

这次 <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
  • C:\Users\ASUS-KL\bin\codex.cmd
    • 却一直没有走上面这条 wrapper,而是直接跳到另一条并行 launcher:
    • C:\ProgramData\npm-global\codex-launcher.cjs

也就是说,机器上实际同时存在两条活跃启动链:

  1. codex.ps1 -> C:\Users\ASUS-KL\.codex\gateway.ps1 -> C:\ProgramData\npm-global\codex.ps1
  2. codex.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 层根因。

直接证据

修复前做了一个最小对照:

  1. 读取 C:\Users\ASUS-KL\.codex\log\turn-abort-trace.jsonl 行数
    • 当时是 14
  2. 运行:
    • cmd /c "C:\Users\ASUS-KL\bin\codex.cmd --version"
  3. 再读 trace 行数
    • 仍然是 14

这说明修复前 codex.cmd 确实完全绕开 C:\Users\ASUS-KL\bin\codex.ps1

已实施修复

已把:

  • C:\Users\ASUS-KL\bin\codex.cmd

改成只做两件事:

  1. 先清空这些 sticky env:
    • CODEX_INTERACTIVE_CONTROL
    • CODEX_INTERACTIVE_CONTROL_PROMPT
    • CODEX_INTERACTIVE_POLICY
    • CODEX_INTERACTIVE_SESSION_ID
    • CODEX_THREAD_ID
  2. 然后统一委托到:
    • 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 还是 cmdcodex,都会进入同一条清理 + gateway + trace 链

这次修复真正解决了什么

它解决的是 本地 launcher split-brain

  • 不再有一个带 trace/清理的 ps1 路径
  • 另一个不带 trace/清理的 cmd 路径

对这类 turn_aborted 问题,这很关键,因为之后:

  • 所有入口都会先清 sticky interactive env
  • 所有入口都会产出统一 trace
  • 所有入口都能自动尝试采集 TUI session log

也就是说,就算还要继续追某一次具体 Interrupt 的键盘/提交来源,也不会再因为“走错启动链”而失去证据。