Skip to content

Codex 长时间后台任务会不会把 Token 轮询光?我翻了源码

Codex 长时间后台任务会不会把 Token 轮询光?我翻了源码

最近在用 Codex 跑一些很长的任务时,我突然想到一个问题:

如果一个后台 terminal 要跑 10 个小时,Codex 为了等它结束,每隔几十秒甚至每分钟都去检查一次,那模型请求和 Token 岂不是很快就被轮询吃光了?

翻了当前 openai/codex 的实现以后,发现这里其实有两层完全不同的机制:本地进程监听模型主动轮询。前者基本不消耗模型 Token,真正需要注意的是后者。

本文基于 2026-08-19 的 openai/codex main 分支(commit d1d51f6)记录。Codex 迭代很快,后续实现可能变化。

结论先说

最重要的结论有五条:

  1. Codex 底层不是靠 LLM 每隔一段时间检查进程是否结束。 进程输出和退出状态由本地 Tokio task 事件驱动地监听。
  2. exec_command 返回一个仍在运行的 session_id 后,如果模型决定“继续等”,它会调用空的 write_stdin 做 poll。这种 poll 会回到 agent loop,因此会增加模型请求次数和 Token / usage 消耗。
  3. write_stdin 的参数默认 yield_time_ms250 ms,但空 poll 在底层至少会等待 5 秒。如果模型一直不显式指定更长的时间,理论上可能退化成 5 秒一轮。
  4. Codex 专门为后台 terminal 提供了 long poll:默认单次空 poll 最多可在本地等待 5 分钟;这个上限还可以通过 background_terminal_max_timeout 调大。
  5. 对真正的 10 小时级任务,最省 Token 的方案不是“让主 agent 一直等”,而是让任务继续在后台运行并结束当前 turn;如果必须同步等,则显式使用很长的 yield_time_ms

下面逐层看。

1. exec_command 首先只等 10 秒

当前 unified exec 的 exec_command 默认:

1
yield_time_ms = 10000

也就是说,Codex 启动一条命令后,默认先在本地等待 10 秒。如果命令在这段时间内结束,直接把结果返回给模型;如果还没有结束,就返回一个 session_id,后续可以通过 write_stdin 继续与该进程交互。

相关源码:

因此一个长任务的典型生命周期是:

1
2
3
4
5
6
7
8
9
LLM

├─ exec_command(...)
│ ↓
│ 启动本地进程
│ ↓
│ 默认等待 10 秒

└─ 仍未结束 → 返回 session_id

问题从这里才真正开始:拿到 session_id 后,到底是谁负责“等它结束”?

2. 进程退出检测本身并不需要 LLM 轮询

这是最容易误解的地方。

Codex 的 unified exec 会在本地启动独立的异步任务:

  • 一个 task 持续读取 PTY / stdout,并把输出写进共享 transcript;
  • 另一个 exit watcher 直接等待进程的退出信号;
  • 进程真正结束后,再发送最终的 command completion event。

也就是说:

1
2
3
4
5
后台进程

├── stdout ──→ 本地 streaming task

└── exit ────→ 本地 exit watcher

这些都是 Rust / Tokio runtime 内部的事件等待,不需要让模型每分钟问一句“结束了吗?”。

相关源码:

所以,“Codex 能不能知道进程什么时候结束”并不是 Token 问题。

真正烧 Token 的场景是:模型所在的当前 turn 想要同步等到这个结果,于是不断发工具调用去取状态。

3. 真正需要警惕的是空 write_stdin poll

write_stdin 既可以真的给进程写 stdin,也可以什么都不写:

1
write_stdin(session_id=123, chars="")

空写入在 Codex 中就是 background poll。

当前 handler 给 write_stdin 的默认 yield_time_ms 是:

1
250 ms

但底层对空 poll 有一个最小等待时间:

1
MIN_EMPTY_YIELD_TIME_MS = 5000

所以模型如果只写:

1
write_stdin(session_id=123)

实际不会 250 ms 就返回,而是至少在本地等 5 秒

这个下限本身是合理的,否则轮询会更加疯狂。但对于一个要跑 10 小时的任务来说,5 秒仍然非常短。

如果模型真的一直以 5 秒为周期等待:

1
10 × 3600 / 5 = 7200

理论上就是 7200 次 poll

这里要强调:具体 Token 花费取决于模型、provider、上下文缓存等因素,不能简单认为“一个 poll 等于固定多少 Token”。但如此高的模型续跑次数显然没有必要。

相关源码:

4. Codex 已经提供了 5 分钟 long poll

好消息是,Codex 显然考虑过这个问题。

当前默认配置里:

1
background_terminal_max_timeout = 300000

也就是 300000 ms = 5 分钟

这个值不是“后台进程最多只能运行 5 分钟”,而是:

一次空 write_stdin poll 最多可以在本地等待 5 分钟。

进程本身并不会因为这个 timeout 被杀掉。

相关源码:

这个机制是在 2026-02-19 的改动中专门加入的。当时默认后台 poll 上限还从 30 秒提高到了 5 分钟:

因此模型完全可以这样等待:

1
2
3
4
5
write_stdin(
session_id=123,
chars="",
yield_time_ms=300000
)

这 5 分钟是在 Codex 本地异步等待,并不是模型每隔几秒重新推理一次。

而且即使进程在这期间持续输出日志,collector 也会在本地持续收集,不需要每来一行 stdout 就重新唤醒一次模型。如果进程提前结束,则可以通过退出信号提前完成等待。

5. 10 小时任务:5 秒、5 分钟、1 小时差多少?

假设任务整整运行 10 小时,并且当前 turn 一定要一直等到它完成:

单次 poll 等待10 小时内约需 poll 次数
5 秒7200 次
5 分钟120 次
1 小时10 次

差距非常大。

这也是为什么我现在更倾向于把长任务的后台 poll 上限直接提高到 1 小时:

1
2
# ~/.codex/config.toml
background_terminal_max_timeout = 3600000

但是这里有一个很重要的坑:

这个配置只是提高“允许等待多久”的上限,并不会自动把每一次 write_stdin 都变成 1 小时。

因为模型调用 write_stdin 时仍然可以传更小的 yield_time_ms,甚至完全不传。

所以如果真想稳定避免高频轮询,还需要给 Codex 一条明确指令。

6. 我现在给 Codex 加的规则

我准备把下面这段加入自己的全局 / 项目指令:

1
2
3
4
5
对于长时间运行的后台终端命令,不要频繁轮询。

使用空的 write_stdin 进行轮询时,请将 yield_time_ms 设置为 3600000(1 小时)。

如果当前轮次并不需要获得该任务的最终结果,请让进程继续在后台运行,不要同步等待其完成。

配合:

1
background_terminal_max_timeout = 3600000

这样,如果一个任务预计运行 10 小时,而当前 turn 又确实必须等到它完成,理想情况下只需要大约 10 次 long poll。

更重要的是:如果当前 turn 根本不需要最终结果,那就完全没有必要等。

7. 最省 Token 的方式:结束当前 turn,让进程自己跑

当前 Codex 的 unified exec 支持一个很有用的状态:

当前 turn 已经结束,但 background terminal 仍然处于运行状态。

Codex Core 甚至会在 turn 完成时统计还有多少个 unified exec process 正在后台运行,并提供 background terminal 的 list / terminate API。

因此,对于:

1
python train.py

这种预计要跑 10 小时的任务,最合理的流程其实是:

1
2
3
4
5
6
7
8
9
10
11
12
13
启动 train.py

10 秒后仍未完成,得到 session_id

告诉用户任务正在后台运行

结束当前 turn

========= 中间 10 小时不需要持续模型 poll =========

用户之后回来要求检查结果

再读取后台 terminal 状态

这比把一个主 agent 挂在那里同步等 10 小时合理得多。

相关源码:

8. 但不要把 Codex background terminal 当成真正的作业调度器

还有一个边界必须注意。

Codex session 整体 shutdown 时,会清理 unified exec 进程并调用 terminate_all_processes()。所以如果需求是:

我退出 Codex、关掉 TUI、SSH 断开以后,这个训练还必须继续跑 10 小时。

那么就不应该依赖 Codex-managed background terminal。

这种任务应该交给真正的任务托管工具,例如:

1
2
3
tmux
systemd-run --user
Slurm

尤其在服务器 / GPU 集群上,Slurm 本来就是更合适的生命周期管理者。

相关源码:

9. 一个很有意思的旁证:Codex 曾经设计了专门的 awaiter

源码里还有一个非常有意思的文件:

1
core/src/agent/builtins/awaiter.toml

它为专门等待长任务的 agent 设置了:

1
2
background_terminal_max_timeout = 3600000
model_reasoning_effort = "low"

而它的行为规则明确要求:

  • 长任务继续使用 tool call 等待;
  • 使用 long timeout;
  • 如果需要多次等待,则指数增加 timeout / yield time。

这其实正好验证了前面的思路:等待长任务不应该由主 agent 高频轮询,而应该使用低 reasoning、超长 long poll 的专门等待策略。

不过在当前 main 中,这个 awaiter role 被标记为:

1
Awaiter is temp removed

也就是说,目前还不能指望 Codex 自动把所有长任务委托给一个专门的 awaiter。

相关源码:

结论

这次翻源码以后,我对 Codex 的 background terminal 行为有了一个比较清晰的认识:

1
2
3
4
5
6
7
8
9
10
11
12
13
进程生命周期检测:本地事件驱动,不需要 LLM 高频检查



模型如果同步等待:通过 write_stdin poll



poll 太短:模型续跑次数可能非常夸张



正确做法:long poll,或者直接结束当前 turn

所以对于长达数小时甚至十几个小时的训练、benchmark、编译、测试任务,我现在会遵循两个原则:

  1. 结果不需要立刻拿到,就让它留在后台,不要让主 agent 同步守着。
  2. 确实需要等,就把空 write_stdinyield_time_ms 拉长到小时级,并同步提高 background_terminal_max_timeout

这比单纯担心“Codex 会不会每分钟查一次”更准确:Codex 底层其实有事件驱动的 watcher,真正决定 Token 消耗的是上层 agent 选择怎样等待。

About this Post

This post is written by KaranocaVe.

#Codex #CLI #OpenAI #Terminal #Token