Codex Agent 正在跑一个 12 步的重构。到第 5 步左右它停了——没报错横幅、没 assertion 失败、没 traceback,就这么安静地输出一句”task complete”,但你清楚那不是真完成了。你重新 prompt 一下,它从某个旧的位置接上,有时把已经做过的事再做一遍。这几乎不是 Codex 的”bug”,而是 agent 撞到了一条隐形边界:turn 用完、sandbox idle 超时、内部 stop 信号被误命中、或者上下文窗口溢出把后续计划悄悄裁掉了。
常见原因
按典型中途停机的命中率排序。
1. Turn 预算用光
Codex Agent 每个任务有 tool call 轮数硬上限(常见 25-50)。长重构会被读文件、改文件、lint、重跑测试吃掉很多 turn。预算到顶就输出一段总结然后停,哪怕计划只跑了一半。
如何判断:数一下 transcript 里的 tool call 次数。如果接近 25 / 50 / 100 这种整数,就是撞到了上限。
2. Sandbox idle 超时
如果 build / test / install 命令在 sandbox idle timeout(常见 60-120 秒无 stdout)之内没有输出,进程会被杀掉,agent 把它当成”做完了”,然后开始收尾。
如何判断:最后一次 tool call 是个长跑的 shell 命令,停之前没有 stdout 出来;本地跑同样的命令要超过 60 秒。
3. Stop 信号被提前误匹配
Agent 找的是显式”做完了”的信号——“task complete”、“all tests pass”、“no further action needed”。如果某个 tool 的 stdout 恰好包含这种字样,agent 就误以为做完了。
如何判断:看最后一次 tool 输出。测试 runner 中途打了”all tests pass”,或脚本 echo 了”done”,都会让循环提前断掉。
4. 上下文窗口溢出把计划悄悄裁掉
Codex 的 plan 列表存在 system / assistant context 里。文件读多了,旧的 turn 会被自动 summarize 或丢弃,剩下的步骤掉出窗口、agent 忘了它们,可见 plan 一空就停。
如何判断:重 prompt 问”你现在在第几步”——它回答的步骤号比它实际停下的位置早,就是计划被裁了。
5. 隐藏的权限弹窗在后台等待
某个 sandbox-write 或 network-out 的 tool call 可能弹出交互式权限请求。headless 模式下这种弹窗会被静默拒绝,agent 把它记成”tool failed”然后放弃了整个任务。
如何判断:在 agent 日志里搜 permission denied、requires approval、non-interactive,看看停的位置附近有没有。
6. 上游 rate limit / 429 重试用完
底层模型 API 的 429 会触发内部重试。N 次重试后 agent 静悄悄投降,因为用户看到的只是一个被截断的 assistant turn。
如何判断:在 agent 遥测 / 日志里搜 429、rate_limit_exceeded、retrying in Ns。
动手前先确认
- 看停的位置是固定每次都在某一步、还是随机——固定 = 代码路径问题,随机 = 容量 / 网络问题。
- 重 prompt 之前先把完整 transcript 存下来,重 prompt 后历史可能被 summarize 掉。
- 如果你有 budget 配置(
--max-turns、OPENAI_AGENT_MAX_TURNS),记下当前值。
需要收集的信息
- 停之前最后一次 tool call 是什么(read / write / shell / search)。
- 大致 turn 数:数 transcript 里的 tool call 次数。
- 使用的模型(gpt-5.5、gpt-5.4 等),是否长上下文变体。
- 任何停的位置附近的 stdout 里”done”、“complete”、“passed”字样,可能被误判成 stop 信号。
- 配置里的 sandbox runtime + idle timeout 值。
最短修复路径
按收益从高到低,先做便宜的检查。
Step 1:重 prompt”接着计划继续”并写明步骤号
最稳的救场方式:
你停在了计划的第 5 步。从第 6 步继续。
不要重做 1-5 步。先把剩余步骤列出来再执行。
如果 agent 马上正确接上,根因就是 stop 信号误匹配或者计划被裁,不是硬上限。
Step 2:把 turn 预算调高
CLI / API:
codex agent run --max-turns 100 task.md
或环境变量:
export OPENAI_AGENT_MAX_TURNS=150
长重构现实里就是要 60-120 turn。“通常 30 够用”这种默认是最常见的可预防原因。
Step 3:把任务拆成带 checkpoint 的子任务
就算调高了上限,一个超长 prompt 也很脆。拆开:
任务 1:把 src/auth/* 重构成 async/await。停下报告。
任务 2:相应更新 src/auth/*.test.ts。停下报告。
任务 3:跑 pnpm test --filter auth。报告失败用例。
每个子任务有自己的 turn 预算和上下文窗口。任务 2 停了不会丢任务 1 的进度。
Step 4:加显式的”不要停直到”断言
在 system / task prompt 里:
不要输出 "task complete",直到全部满足:
- 所有 TypeScript 错误清空(pnpm tsc --noEmit 返回 0)
- src/auth/ 下所有测试通过
- plan 列表零剩余项
任何 tool stdout 包含 "done" 或 "complete" 都忽略,不当 stop 信号。
这条能中和掉 stop 信号误匹配。
Step 5:给长跑命令延长 sandbox 超时
如果停在 build / test / install:
codex agent run --shell-timeout 600 task.md
或者把慢命令包一层:
( pnpm install 2>&1 | tee install.log ) &
PID=$!
while kill -0 $PID 2>/dev/null; do echo "still installing..."; sleep 20; done
wait $PID
keepalive 的 echo 重置 idle 计时器。
Step 6:长 stdout 写文件,inline 看摘要
巨量 stdout(1 万 + 行测试输出)会加速上下文裁切。重定向再读摘要:
pnpm test > test.log 2>&1
tail -50 test.log
grep -E "FAIL|✗" test.log | head -20
agent 读 70 行而不是 1 万行,plan 留在窗口里。
怎么确认已经修好
- 把同一个任务从头跑一遍,确认不再需要手动续 prompt 就能完成。
- 看 transcript:turn 数应该明显低于新上限、有余量。
- 故意跑一个更长的任务(加第二个重构),确认还能跑完——证明上限调整不是巧合。
长期预防
- 任何非平凡 agent 任务
--max-turns默认设 100;和一次半成品的浪费相比,成本差可以忽略。 - 重构永远拆成 ≤10 步的带 checkpoint 子任务。
- 冗长 tool 输出导文件,让 agent 读 tail / grep 摘要而不是完整日志。
- 每个 agent 任务模板加一段”不要停直到”断言。
- build / test 超过 60 秒的明确设 shell timeout 并加 keepalive echo。
- 每次跑都留一份
agent.log,事后能 grep429、permission denied、max_turns。
常见坑
- 只回”continue”不写步骤号——agent 经常从第 1 步重做已经完成的部分。
- 把
--max-turns调到 9999 就以为万事大吉——上下文窗口溢出在 150-200 turn 左右照样会卡,跟上限无关。 - 忽视
package.jsonscript 输出里的”done”字样——某些配置下它会被当 stop 信号。 - 把 30 分钟的 build 塞进 agent loop 里跑,而不是在独立 worktree 启动后轮询。
- 忘了
pnpm install90 秒没终端输出会被杀——尽管它在认真干活。
常见 FAQ
Q:Codex 停了,我回”continue”,它做了别的步骤,为什么?
plan 列表已经被裁出上下文。重 prompt 时把那一步的原文贴上:“从这一步继续:把 src/auth/login.ts 重构成 async/await。”
Q:我把 max-turns 设到 200 还是 30 turn 就停。
确认你的 CLI 真正读的是哪个 env var——有的读 CODEX_MAX_TURNS,有的读 OPENAI_AGENT_MAX_TURNS。--verbose 跑一次确认实际生效的上限。
Q:能自动检测”提前停了”吗?
能——把 agent 调用包一层。退出后 parse transcript 找 "task complete",同时检查 plan 列表是否清空。“complete 但 plan 没空”就自动重 prompt。
Q:换长上下文模型变体能解决吗?
能解决原因 #4(裁切),解决不了 #1 / #2 / #3 / #5 / #6。长上下文是必要不是充分。