Skip to content

Instantly share code, notes, and snippets.

@acmerfight
Last active September 10, 2026 08:51
Show Gist options
  • Select an option

  • Save acmerfight/ddfad0c835d2fefb3f26b7bd3f2b09ee to your computer and use it in GitHub Desktop.

Select an option

Save acmerfight/ddfad0c835d2fefb3f26b7bd3f2b09ee to your computer and use it in GitHub Desktop.
Codex 等待为何仍消耗 token:图解源码、Astra low 实测、17 次受控对照与复现脚本

Codex 等待子任务时,为什么还会消耗 token?一次可以自己复现的源码实验

已经验证: 空等待超时后,Codex 会再次请求模型;延长等待仍允许用户输入提前唤醒。
尚未验证: 这些请求具体扣掉多少订阅额度,以及提示词冲突是否导致模型主动频繁轮询。

实验日期:2026-09-08 至 2026-09-09。真实模型调用使用 GPT-6 Astra / low。本 Gist 包含图解、原始实验的脱敏结果、可执行脚本、固定源码链接和复现步骤。无需先懂 Rust;最快的复现只需 Python 和 Codex CLI。

2026-09-09 更新:这次真的调用了 Astra low,也跑了真实子 agent。

先前的 17 次受控运行使用本地 mock;下面新增的是 4 个真实主会话、4 个真实子 agent,共 18 次模型请求。全部从会话日志核验为 Astra low。两轮回答不同的问题,不能混算。

这次要回答读者的质疑:“轮询花不了多少,真正的大头是不是子 agent 重复加载上下文?”

实际测量 结果 可以说明什么
两次空超时后的模型请求 输入 36,041,其中缓存命中 35,584;输出 25 输出短不代表输入少;也不能按全部未缓存输入算费用
同一个短任务,子 agent 继承 all vs none 两种创建顺序下,all 都多 3,123 输入 token 继承父历史的输入开销确实存在
两组子 agent 的缓存命中 第一组 all 未缓存输入更少,第二组 all 完全未命中 不能只按上下文长度推算实际费用
真实子 agent 完成后的长等待 两组都提前返回,没有空超时 这次补上了真实完成通知路径;没有测精确通知延迟

结论:少让主 agent 空转、少给子 agent 塞无关历史,两项都值得优化。谁是额度大头,本实验仍未证明。 没有逐请求订阅扣额,也没有做跨 TTL 的等待测试。

flowchart LR
  A["主 agent 空等待超时"] --> B["又一次模型请求"]
  C["子 agent 继承父历史"] --> D["更多输入上下文"]
  B --> E["分别统计:输入、缓存命中、输出"]
  D --> E
  E --> F["再结合实际计量规则判断成本<br/>不能只看输入总量"]
Loading

第一次阅读请先看 真实测试完整报告与复现步骤。已有 Git 下载目录时,真实测试入口是 python3 live_run.py wait2,随后按报告执行其余三项与分析器;会消耗账户额度。只想验证程序等待机制、不调用真实模型,请继续使用下方第二轮的 controlled.py

真实测试数据:live-results.json;预先协议:LIVE-PROTOCOL.md;运行脚本:live_run.py;分析脚本:live_analyze.py。发布用脚本只改了路径、命名和版本检查,已用本次保存的真实日志重新核验;没有为发布再次调用模型。


2026-09-08:第二轮受控实验(本地 mock)

原始问题是:Codex 明明在等子助手,为什么额度还会掉?是不是等待也一直在“烧 token”?

答案要分两层:程序安静挂起的那段时间,不会因为时间流逝就持续请求模型;但一次等待到期后,模型可能被叫回来处理上下文,只决定一句“继续等”。重复发生时,就多了很多没有新信息的请求。我们确认了这种可避免的开销机制;尚未证明它是 Astra 额度消耗快的主要原因,也没有测出能省多少额度。

像一位负责人等同事交报告:每半分钟重新翻一遍资料,再问“好了没”,会增加工作量。让程序等到有事再通知,可以省掉中间这些询问。这个比喻不表示缓存内容每次都按原价收费。

这次用一个容易控制的实验来验证:让 Codex 等待,到第 65 秒再发来同一条用户输入。 两组主动改变的只有等待上限;它不是“子助手完成”的替代证明。

同样在第 65 秒收到输入 每次最多等 30 秒 最多等 10 分钟
全部父模型请求 4 次 2 次
没有新信息的超时 2 次 0 次
输入发出到下一次请求 30.72 毫秒 49.31 毫秒
flowchart LR
    subgraph S["短等待:共 4 次请求"]
      S0["0 秒:开始等"] --> S1["30 秒:没消息<br/>再次请求模型"]
      S1 --> S2["60 秒:没消息<br/>再次请求模型"]
      S2 --> S3["65 秒:收到输入<br/>继续处理"]
    end
    subgraph L["长等待:共 2 次请求"]
      L0["0 秒:开始等"] --> L1["中间挂起<br/>没有模型请求"]
      L1 --> L2["65 秒:收到输入<br/>提前结束等待"]
    end
Loading

长等待并没有硬等满 10 分钟;它省掉了第 30、60 秒两次“还没消息”的模型请求。请求数减半,不代表费用或额度消耗减半。 两组都在预先设定的 1 秒响应界限内;这里也不声称长等待比短等待响应更快。

第二轮共 17 次正式运行、7 组成对检查,全部通过。2.75 秒的加速对照各重复三对、交替顺序;另有上面的真实 30 秒尺度检查和三次用户插话测试。还测到:通用执行工具 exec/wait 的外层短 yield(提前返回“还在运行”)本身也会增加请求,所以只调 wait_agent 的参数可能不够。

为少用 token,这一轮让真实 Codex 程序运行工具,本地模拟服务扮演模型,没有新增真实模型推理调用。它能检查因果机制,不能测量 Astra 自主选择短等待的概率或实际费用。上一轮真实 Astra low 调用的记录保留在下文,二者不混算。

**第二轮的历史限制(第三轮已有真实成功样本):**原计划中的子助手完成通知集成测试没有跑通:创建返回成功,却未观察到子助手模型请求,原因未定位。六次调试失败已记录,正式实验前改用固定时刻的用户输入。因而本轮没有新增证明子助手完成、失败、请求批准三种通知的端到端可靠性,也没有把失败调试当作产品缺陷。

阅读顺序:第二轮完整结果预先写定的方案及修订原始数据。下面保留第一轮源码追踪及实验全过程。

自己复现第二轮:不调用真实模型

准备 Python 3.10+、Git 和 Codex CLI;先确认 codex --version。本次验证版本为 0.153.4、macOS。下载全部文件,并在下载目录运行:

git clone https://gist.github.com/acmerfight/ddfad0c835d2fefb3f26b7bd3f2b09ee.git codex-wait-experiment
cd codex-wait-experiment
python3 controlled.py --codex "$(command -v codex)" --output results.json
python3 analyze.py

如果 command -v codex 没有输出,请先安装 CLI,或把它替换成已有程序的绝对路径。部分 macOS 安装的路径为 /Applications/ChatGPT.app/Contents/Resources/codex,以本机实际路径为准。也可点击 Gist 的 Download ZIP,解压后进入文件所在目录运行后两条命令。

完整运行含两次 65 秒等待,另需若干启动时间;无需支付模型推理费用来复现这一轮。脚本写入 results.json,最后的分析应输出 runs: 17paired_checks: 7all_passed: true。分析只核验判据,不要求你的毫秒数与本文一致。运行会覆盖下载的结果文件;需要保留原始版本时先复制目录。

controlled.pyreproduce.py 要放在一起。analyze.py 适用于完整运行;不要用 --pilot--skip-scale 的不完整数据生成正式报告。模型、平台或 CLI 版本变化可能影响结果;app-server 会读取部分本机配置,故没有声称整个进程完全离线或完全隔离。不要加 --live:那是第一轮中单独的真实模型测试选项。

先用一分钟理解问题

把主代理想成一位负责人,把子代理想成正在工作的同事。负责人可以把“同事做完了就叫我”交给程序处理,也可以每隔半分钟重新看一遍项目资料,再问“做完了吗?”

模型每次重新参与判断,都可能使用上下文 token。即使答案只是“还没做完,继续等”,也不代表这次请求没有输入成本。

这里几个词的含义:

名词 在本实验中的意思
模型请求 Codex 再请模型生成一次回答或工具调用
token 模型处理内容的计量单位,不等于字数,也不直接等于额度百分比
上下文 模型作答时可使用的先前消息、工具定义和结果
缓存 复用此前处理过的内容;复用不代表完全免费
超时 这次等待的期限到了;不表示子任务失败
mock / 模拟服务 本机的“假模型服务器”,按脚本返回固定结果,不调用真实模型
low 模型推理强度设置;它不关闭输入上下文处理

图 1:真正发生的循环

flowchart TD
    A["模型决定等待"] --> B["程序等待消息<br/>默认期限 30 秒"]
    B --> C{"有新消息吗?"}
    C -->|"消息先到"| D["立即返回:有消息"]
    C -->|"期限到了,仍无消息"| E["返回:等待超时"]
    D --> F["工具结果加入历史"]
    E --> F
    F --> G["再次请求模型<br/>带上逻辑上下文"]
    G --> H{"模型下一步做什么?"}
    H -->|"继续等待"| B
    H -->|"处理结果或结束"| I["推进任务"]
    classDef wait fill:#e8f2ff,stroke:#2563eb,color:#172554
    classDef repeat fill:#fff1e5,stroke:#c65d12,color:#7c2d12
    classDef done fill:#e9f7ef,stroke:#258452,color:#14532d
    class B wait
    class E,F,G repeat
    class D,I done
Loading

蓝色部分本来就支持事件等待。橙色部分解释了空超时怎样进入下一轮推理。图中的“继续等待”仍是模型的选择;我们没有发现运行时内部每 30 秒自动调用一次模型的循环。

1. 我们要验证什么?

起点是 这篇 Reddit 讨论:作者报告短等待频繁超时,并提出它可能解释额度快速消耗。以下实验独立检查运行机制,不把作者的账号扣额当作本次测量。

我们把问题拆成四个能实际检查的问题:

  1. 没有消息的等待超时,会不会产生下一次模型请求?
  2. 只提高默认等待时间,能不能阻止模型显式要求短等?
  3. 等待设得很长,用户还能不能及时打断?
  4. 换成 low 后,输入 token 会不会消失?

2. 固定实验版本,避免“测的不是同一个东西”

对象 本次使用的版本 用途
源码 c7f81afc191d74ef6e96a5add25eee05662d2215 读代码、编译仓库定向测试、提取等待函数
已安装 CLI codex-cli 0.153.4 运行时模拟测试、真实模型测试
模型请求 gpt-6-astralow 每条模拟请求都检查 effort 字段;真实调用显式指定
测试机器 macOS / Apple Silicon 其他系统尚未逐一验证
脚本 Python 3.10+,仅标准库 不需要 pip 安装依赖
提取函数测试 Rust 1.95.0、Tokio 1.53.1 Tokio 虚拟时间,不需要真的等 25 分钟

源码提交与已安装 CLI 是两条独立证据链。 没有声称安装的 CLI 就是从这个提交构建的二进制。新版行为可能变化,复现结果应保留自己的版本号。

3. 第一轮:沿着源码找到“超时后发生什么”

可以按这张表逐项检查,链接固定到实验提交:

观察点 对应源码 看什么
默认期限 config/mod.rs:236 最小 10 秒、默认 30 秒、最大 1 小时
参数怎么生效 wait.rs:53 省略用默认值;太短向上钳制;太长报错
等待的实现 wait.rs:187 等 watch 消息变化,或者到期
为什么续跑 stream_events_utils.rs:327 工具调用标记 needs_follow_up = true
结果进入历史 turn.rs:2232 收集工具结果并写入历史
准备下一次请求 turn.rs:419 从历史构造逻辑输入,再请求模型

关键代码可以理解成:

等消息,最多等到 deadline
  消息到了 → 返回“有消息”
  用户说话 → 返回“被新输入打断”
  期限到了 → 返回“超时”

工具返回后 → 把结果交回模型

没有看到专门把“无新信息的超时”留在运行时继续等待的分支。因此,短期限如果被反复选择,就会反复经过模型。

另一个发现是提示词相互牵制:多代理指导 希望按分钟长等,Astra 内置模板 却要求等待调用不超过 60 秒,并希望频繁向用户更新进展。我们在实际捕获的模拟 HTTP 请求中同时看到了两种指导。

这能提出一个值得验证的原因,但尚未做移除其中一条指导的行为对照实验,所以不能把它宣布为短轮询的唯一根因。

4. 第二轮:用“假模型”测真正的 Codex

为了精确知道发出了多少次请求,我们让真实 Codex 连接本机模拟服务。模拟服务每次返回预先写好的工具调用;Codex 则真正执行等待。这样不用依赖模型随机决定做什么,也不消耗真实模型额度。

图 2:模拟的只是回答,等待与网络请求都是真的

sequenceDiagram
    participant P as 测试脚本
    participant C as 真实 Codex CLI
    participant S as 本机模拟模型服务
    P->>C: 启动,设置 Astra / low
    C->>S: 第 1 次请求
    S-->>C: 请调用 wait_agent
    Note over C: 真正执行等待,直到超时
    C->>S: 第 2 次请求,包含超时结果
    S-->>C: 再等待一次,或结束
    Note over P,S: 记录请求次数、间隔、历史条目、effort
Loading

原始测量如下。多数测试把时间缩到毫秒以便快速检查;其中一项保留了真实 30 秒等待,另有一项测试 25 分钟期限的提前唤醒。

测试条件 原始实测结果 验证了什么
连续三次请求等 30ms,最小值 10ms 4 次请求;间隔约 50–52ms 每次空超时都会交回模型
默认值改成 500ms,显式请求 30ms 51ms 后下一次请求 只改默认值挡不住显式短等待
默认值 500ms,省略 timeout 参数 521ms 后下一次请求 省略参数时才用默认值
最小值改成 500ms,显式请求 30ms 542ms,返回钳制说明 最小值能约束短等待
最大值 2000ms,却请求 2001ms 28ms,返回错误 超过最大值会报错
原生默认 30 秒,省略参数 30,042ms 后下一次请求 真实墙钟验证
25 分钟期限,约 200ms 后注入用户输入 250.5ms 后下一次请求 长期限允许提前唤醒

表中时间是相邻请求到达本机服务的间隔,包含等待和客户端处理开销。最后一行也包含注入前的约 200ms,不能称为“用户输入后的响应延迟为 250.5ms”。公开脚本另外记录 steer_to_next_request_ms,将这两种时间分开。

三次空超时实验中,历史条目数为 7 → 9 → 11 → 13。每轮新增一条工具调用和一条结果。初始 7 条依赖环境;复现脚本检查的是相邻增长关系,不要求每个人都从 7 开始。

图 3:延长“最多等多久”,不会要求必须等满

sequenceDiagram
    participant U as 用户或测试脚本
    participant R as Codex 运行时
    participant M as 模型
    M->>R: wait_agent,最多等 25 分钟
    Note over R: 正在等事件,没有再次请求模型
    U->>R: 约 200ms 后发来新输入
    R-->>M: 立即返回:被新输入打断
    Note over R,M: 第二次请求约在第 250.5ms 到达
Loading

这个测试通过真实 app-server 的 turn/steer 接口注入输入;不是在图上假设它能唤醒。

5. 第三轮:直接测试原源码函数,再跑仓库测试

我们原样提取 WaitOutcomewait_for_activity,只用同形枚举替代它依赖的会话活动类型,配上真正的 Tokio 消息通道。文件与提取片段都校验 SHA-256。六项测试覆盖:无消息到期、邮箱提前唤醒、用户输入提前唤醒、两种已排队事件、通道关闭。

这里使用虚拟时间:程序时钟可以推进 25 分钟,无需现实等待 25 分钟。六项全部通过。通道关闭会立即被报告成 TimedOut,所以单独看到这个布尔值,也不能断定一定等满了期限。

随后从固定提交编译并执行了仓库自带的 11 项 V2 等待测试,全部通过。其余 2,452 项被筛选条件排除,不能写成“整个 Codex 测试套件通过”。

6. 第四轮:一次真实 Astra / low 调用

我们还明确要求真实模型调用两次 wait_agent(timeout_ms=100),然后只回答 DONE。没有启动子代理。实际完成了两次等待并正常结束。

整个回合的服务端用量汇总 数值
输入 token 44,532
其中缓存输入 22,016
缓存写入字段 0
输出 token 45
其中推理输出字段 0

这是整个回合,包括首轮输入和最终回答。不能把 44,532 全部算作等待损耗;复现也不应以得到相同 token 数为成功条件。

这项实验是强制执行两次等待,证明运行路径在 low 下成立;它不测量模型在自然任务里主动轮询的频率

7. 现在自己复现:先运行不消耗模型额度的版本

在 Gist 页面点 Download ZIP,解压后在该文件夹打开终端。请下载全部文件,源函数测试还需要随附的锁文件。

先检查 Python 与 Codex:

python3 --version
codex --version

需要 Python 3.10 或更高。没有 Codex 时,安装 Node.js 后可以安装固定 CLI 版本;已经有合适版本就跳过:

npm install -g @openai/codex@0.153.4

也可以从前面的 0.153.4 release 下载对应系统的可执行文件。

运行完整模拟测试:

python3 reproduce.py

成功标志: 七项 PASS,最后显示 7/7 selected scenarios passed,并生成 wait-results.json。其中一个场景确实等待 30 秒,其他场景通常很快;启动开销因机器不同而异。

只想先做快速检查,可以跳过那项 30 秒测试:

python3 reproduce.py --quick

程序不在 PATH 时,自己指定位置:

python3 reproduce.py --codex /path/to/codex

Windows 可以将 python3 换成 py -3,并指定解压得到的 codex.exe。脚本采用跨平台标准库,但本次只在 macOS 验证;遇到平台差异应记录错误,不要当成产品缺陷的证据。

模拟模式不要求模型订阅或 API key,模型端点固定为 127.0.0.1。它仍会启动本机 Codex;特别是 app-server 可能读取本机设置或启动配置中的扩展,因此不要把“不调用真实模型”理解成整个进程完全离线。测试只使用临时命令行设置,没有修改全局 Codex 配置。

结果 JSON 不保存完整请求正文、认证头、本机绝对路径或会话 ID。它保留请求次数、间隔、历史条目数和断言所需的工具结果。

可选 A:复现六项原源码函数测试,不需要模型账号

需要先有 Rust / Cargojustcargo-nextest

rustup toolchain install 1.95.0 --profile minimal
cargo +1.95.0 install --locked just
cargo +1.95.0 install --locked cargo-nextest
python3 source_probe.py --run

脚本下载固定提交的一个公开源文件,验证哈希,然后生成小型测试项目并运行。无需克隆或编译整个 Codex。成功标志是 6 tests run: 6 passed

已经有源码时也可读取本地文件,哈希仍必须匹配:

python3 source_probe.py --checkout /path/to/codex --run

可选 B:复现仓库原生的 11 项测试

这一步需要 Git、完整 Rust 构建环境、网络以及较多磁盘空间。首次依赖下载和编译比上面的小项目重得多。

git clone https://github.com/openai/codex.git codex-wait-source
cd codex-wait-source
git checkout c7f81afc191d74ef6e96a5add25eee05662d2215
just test -p codex-core --lib -E 'test(multi_agent_v2_wait_agent)'

成功标志是 11 tests run: 11 passed。源码仓库的测试入口为 just test;这里遵循该入口。

可选 C:真实模型实验,会消耗你的账号额度

只有明确加 --live 才会运行它。先通过 codex login 登录有 Astra 访问权限的账号,再执行:

python3 reproduce.py --live --output live-results.json

脚本显式使用 gpt-6-astra / low。成功条件是完成两次 wait 并返回 DONE。用量可能不同;这是正常现象。模型如果不遵从指定动作,脚本会报错,不会把不一致的行为偷偷计入同一个实验。

8. 看懂结果,不把几个不同的数字混为一谈

模拟服务每次故意报告:输入 1000、其中缓存 900、输出 10、其中推理输出 2。四次请求的汇总应该是 4000、3600、40、8。它只用来检验客户端怎样累计字段,这些数字不是实际消耗或费用

flowchart LR
    A["逻辑上下文<br/>模型可使用哪些内容"] --> B["请求传输<br/>可能只发送增量"]
    B --> C["服务端用量<br/>输入、缓存、输出"]
    C --> D["订阅扣额<br/>还需要计量规则"]
    classDef layer fill:#eef2ff,stroke:#6366f1,color:#1e1b4b
    class A,B,C,D layer
Loading

这些层次不能直接画等号。WebSocket 客户端 支持复用前缀并只传新增内容;因此长上下文不代表网络每次完整重传。本次请求捕获使用 HTTP/SSE,没有实测 WebSocket 传输字节量

服务端响应解析逐响应累计代码 也说明,统计时要分清单次用量与累计快照,不能把同一个累计数反复相加。

9. 可以怎么改善?

测试表明,V2 已经有约束短等待的配置。例如下面是一个待评估的五分钟实验起点,不是经过成本优化得出的最佳值,也没有在本次测试中写入全局配置:

[features.multi_agent_v2]
enabled = true
min_wait_timeout_ms = 300000
default_wait_timeout_ms = 300000
max_wait_timeout_ms = 1500000

只提高 default 不足;显式请求短等待时,需要 min 生效。提高最小值也会限制模型主动检查的频率,应结合任务需求评估。正常消息仍能提前唤醒,所以子代理频繁发送无意义消息仍可能带来额外请求。

更完整的改进还包括:消除等待指令冲突,让 UI 自行展示“仍在工作”,并让无新信息的到期检查尽量留在运行时。

不要简单让主代理交完任务就结束:当前 完成通知trigger_turn 为 false。“唤醒正在等待的回合”与“启动已经结束的新回合”不是同一个功能。

10. 证据清单与常见问题

文件 用途
reproduce.py 七项模拟运行时测试;--live 为单独的真实调用
source_probe.py 下载或读取固定源码,校验哈希,生成并运行六项函数测试
source-probe.Cargo.lock 小型 Rust 测试项目的固定依赖
original-results.json 本文所引用的原始实验脱敏数据
publication-retest.json 发布前使用公开脚本重新测试的结果,和原始测量分开保存
RESULTS.md / PROTOCOL.md 第二轮完整结果、实验方案与正式运行前修订
controlled.py / analyze.py 第二轮 17 次受控运行及结果核验;复用 reproduce.py
results.json / pilot-summary.json 第二轮正式数据与调试失败摘要,分开保存
SHA256.json 第二轮实验文件的 SHA-256;自行复现修改结果后哈希会变化
LICENSE.txt / NOTICE.txt 授权与上游归属说明

为什么我的毫秒数不一样? 调度、网络栈和进程启动不同。比较行为、次数和合理时序,不追求小数点一致。

为什么 token 数不一样? 模型和上下文可能不同;模拟 token 才是固定夹具。真实测试的成功条件不是固定 token 数。

找不到 Codex / 找不到参数? 先运行 codex --version;指定程序路径,尽量使用 0.153.4。新旧 CLI 的工具或参数可能不同。

缺少锁文件 / 哈希不匹配? 下载全部 Gist 文件;本地源码切换到固定提交。不要跳过哈希校验来声称复现了同一版本。

为什么 both_wait_guidances_present 为 false? 模型目录或提示模板可能变化;它是诊断字段,不是每个环境都必须满足的断言。

能据此说 68% 的额度被浪费吗? 不能。本实验没有订阅计费归因,也没有把原始 token 占比当作费用占比。

到底验证了什么? 第一轮:原生定向测试 11/11;提取函数测试 6/6;模拟运行时场景 7/7;一次真实 Astra / low 测试成功。第二轮:17 次正式运行、7 个成对检查通过,直接比较等待间隔、外层 yield 和输入唤醒。它们不是相互独立的费用实验,不能把测试通过数当作节省额度的证据。

本文借助 AI 整理,并以源码、可执行断言和明确的证据边界为依据。图由 Mermaid 文本生成;如果第三方 Gist 阅读器没有渲染图,请在 GitHub Gist 原页面查看。

#!/usr/bin/env python3
"""Validate all predeclared runs and produce the concise Chinese result report."""
import hashlib
import json
import pathlib
import statistics
root=pathlib.Path(__file__).resolve().parent
d=json.loads((root/'results.json').read_text())
assert d['pilot'] is False and len(d['runs'])==17
assert len(d['comparisons'])==7
assert all(r['passed'] for r in d['runs']+d['comparisons'])
assert all(q['effort']=='low' and q['model']=='gpt-6-astra'
for r in d['runs'] if r['family']!='steer' for q in r['requests'])
assert all(r['all_requests_effort']=='low' and r['model']=='gpt-6-astra'
for r in d['runs'] if r['family']=='steer')
def group(family,interval,duration=2750):
return [r for r in d['runs'] if r['family']==family and
r.get('interval_ms')==interval and r.get('duration_ms')==duration]
def span(xs):
low,high=min(xs),max(xs)
return str(low) if low==high else f'{low}{high}'
def count(rs):return span([r['parent_requests'] for r in rs])
def lat(rs):return span([r['event_to_parent_request_ms'] for r in rs])
a,b=group('input_event',500),group('input_event',10000)
c,e=group('wrapper',500),group('wrapper',10000)
sa,sb=group('input_event',30000,65000),group('input_event',600000,65000)
steer=[r for r in d['runs'] if r['family']=='steer']
initial_sizes=sorted({r['requests'][0]['input_bytes'] for r in a+b})
report=f'''# Codex 等待:受控实验结果
**结论:在真实 Codex 运行时、本地脚本模型的对照中,缩短等待间隔会造成额外模型请求;外层执行工具的短 yield 也能独立产生同类开销。长等待仍可被用户输入提前结束。**
执行环境:macOS;{d['version']};全部模型请求配置为 gpt-6-astra / low,但由 localhost 模拟服务响应。**本次测试没有新增真实模型推理调用;当前聊天的分析和工具调度本身仍会用 token。**
## 先看一个真实时间尺度的例子
让 Codex 等待,第 65 秒固定发来同一条输入。两组唯一主动改变的因素是等待超时:
| 条件 | 等待上限 | 全部父请求 | 空超时 | 输入发出后多久出现下一请求 |
|---|---:|---:|---:|---:|
| 短等待 | 30 秒 | {count(sa)} | {sa[0]['empty_timeouts']} | {lat(sa)} ms |
| 长等待 | 10 分钟 | {count(sb)} | {sb[0]['empty_timeouts']} | {lat(sb)} ms |
两组输入都在约第 65 秒发出;长等待没有硬等到第 10 分钟。短组第 30、60 秒的空超时分别多触发一次请求。这里的“减少请求”不能直接换算成“节省相同比例的 token、钱或额度”。请求数包含最初发起等待的请求和最后接收事件的请求。
## 全部正式对照
| 实验 | 重复数 | 短等待父请求 | 长等待父请求 | 判据 |
|---|---:|---:|---:|---|
| 固定 2.75 秒注入输入;0.5 秒 vs 10 秒等待 | 3 对 | {count(a)} | {count(b)} | 全部通过 |
| 固定 2.75 秒异步任务;外层 yield 0.5 秒 vs 10 秒 | 3 对 | {count(c)} | {count(e)} | 全部通过 |
| 固定 65 秒注入输入;30 秒 vs 10 分钟等待 | 1 对 | {count(sa)} | {count(sb)} | 通过 |
| 25 分钟等待中,约 0.2 秒后输入 | 3 次 | — | 每次 2 请求 | 全部通过 |
共 17 次正式运行、7 个成对检查;短尺度按 AB / BA / AB 交替。没有靠反复重跑直到通过来筛选正式结果。
短尺度输入延迟:短等待 {lat(a)} ms;长等待 {lat(b)} ms。三次 25 分钟等待的输入延迟为 {span([r['steer_to_next_request_ms'] for r in steer])} ms。均满足预先设定的 1000ms 判据。**我们证明的是此次运行及时唤醒,并不宣称两种等待的延迟完全相等或给出生产环境延迟保证。**
## 为什么这能说明问题,又不能说明所有问题
- 运行等待、超时、工具续接的是安装的 Codex 二进制;请求次数由本地 HTTP 服务实际计数,不是拿公式算出来的。
- 模拟服务把策略固定为“事件没来就继续等,来了就结束”,因此能隔离等待间隔的因果作用;它没有测量真实模型是否会选择这套策略。
- 外层实验实际执行相同的 JavaScript 定时异步任务;短 yield 多次返回运行状态,长 yield 一次等到结果。**没有声称这一版本允许 wait_agent 嵌入 exec,也没有把这两层等同。**
- 原始输入字节数仅作诊断。短尺度首请求长度实际出现 {initial_sizes} 字节:运行时生成的上下文并非逐字节相同。模拟策略不读取这些变化,故不影响本实验的请求数因果比较;但不据此作精确输入量/费用比较。
- 无真实 token/缓存测量,模拟 usage 不代表消耗。未用账号额度条做小样本费用推断,未做跨模型成本外推。
## 未完成的部分与调试记录
最初计划让真实子助手在固定时刻完成,但 pilot 1–6 中虽得到 spawn 成功返回,未观察到子助手模型请求,无法形成有效完成对照。尝试请求分类检查、诊断日志和非 ephemeral 模式后仍未解决,原因尚未定位。正式运行前已在 PROTOCOL.md 中保留原方案并记录修订,改用实际 app-server 用户输入事件。后来 input_event 与 wrapper 调试通过,才启动正式运行。
**因此,子助手“完成、失败、请求批准”三种通知的端到端可靠性,这次没有新增验证。** 此前的源码函数测试支持邮箱事件会提前结束等待,但不能用它代替完整客户端链路测试。此前测试与本次正式数据不混算。
## 必须的结论和后续优先级
现在可以确定:在无新信息时反复超时,会产生可避免的模型请求;仅关注 wait_agent 的参数可能漏掉通用执行工具的外层 yield。用户输入唤醒不需要以每 30 秒调用模型作为前提。
这足以支持“减少安静等待期间的无效模型重入”这一优化方向,尚不足以支持“额度节省 X%”或“这是 Astra 耗费额度的主要原因”。
下一步若要真正提交运行时修复,优先打通子助手通知的完整集成测试,覆盖完成、失败、需要批准、用户输入、通知恰好发生在超时边界。若目标是解释真实账号额度,再做有独立费用计量的 Astra low 配对测试;本轮按停止规则没有为此消耗真实模型额度。
## 复现
需要 Python 3.10+ 与 Codex CLI;仅在上述 macOS 版本验证。脚本默认使用 localhost 模型服务;不需要 API key。app-server 会读取部分本机配置,所以不把整个进程称作完全离线或完全隔离。
```bash
python3 controlled.py --codex /absolute/path/to/codex --output results.json
python3 analyze.py
```
`controlled.py` 与 `reproduce.py` 必须放在同一目录。完整运行包含两个 65 秒等待。脚本会持续写入结果;失败即停止。所有原始数字见 results.json;分析脚本先校验 17 次运行与 7 个对照,再生成本文。
第一轮源码分析与本轮补充均记录在[同一个公开 Gist](https://gist.github.com/acmerfight/ddfad0c835d2fefb3f26b7bd3f2b09ee)。从 Gist 首页开始可看到通俗讲解、下载步骤和所有实验文件。
'''
(root/'RESULTS.md').write_text(report)
manifest={p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in root.iterdir()
if p.is_file() and p.name not in ['SHA256.json']}
(root/'SHA256.json').write_text(json.dumps(manifest,indent=2)+'\n')
print(json.dumps({'runs':17,'paired_checks':7,'all_passed':True,
'real_scale_requests':[sa[0]['parent_requests'],sb[0]['parent_requests']],
'real_scale_latency_ms':[sa[0]['event_to_parent_request_ms'],sb[0]['event_to_parent_request_ms']]}))
#!/usr/bin/env python3
"""Paired deterministic tests of an installed Codex runtime; zero upstream inference.
Python 3.10+. Place reproduce.py (from the linked original Gist) beside this file.
Run: python3 controlled.py --codex /path/to/codex --output results.json
Use --pilot for one short case per family. --skip-scale omits the 130s scale check.
"""
import argparse
import hashlib
import json
import pathlib
import queue
import re
import subprocess
import tempfile
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import reproduce as base
def fixed_input(command, cwd, duration):
"""Use actual app-server input, timestamped from actual wait item/started."""
inbox, errors = queue.Queue(), []
proc = subprocess.Popen(command, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, text=True, bufsize=1)
def reader():
for line in proc.stdout:
try: inbox.put(json.loads(line))
except json.JSONDecodeError: pass
threading.Thread(target=reader,daemon=True).start()
threading.Thread(target=lambda:errors.extend(proc.stderr.readlines()),daemon=True).start()
def send(i,method,params):
payload={'method':method,'params':params}
if i is not None:payload['id']=i
proc.stdin.write(json.dumps(payload)+'\n');proc.stdin.flush()
def receive(predicate):
deferred=[];deadline=time.monotonic()+duration/1000+20
try:
while time.monotonic()<deadline:
event=inbox.get(timeout=max(.01,deadline-time.monotonic()))
base.require('error' not in event,str(event.get('error')))
if predicate(event):return event
deferred.append(event)
raise TimeoutError('No matching app-server event')
finally:
for event in deferred:inbox.put(event)
try:
send(1,'initialize',{'clientInfo':{'name':'controlled_wait','version':'1.0'},
'capabilities':{'experimentalApi':True}})
receive(lambda e:e.get('id')==1);send(None,'initialized',{})
send(2,'thread/start',{'model':base.MODEL,'modelProvider':'wait_probe','cwd':cwd,
'approvalPolicy':'never','sandbox':'read-only','ephemeral':True})
tid=receive(lambda e:e.get('id')==2)['result']['thread']['id']
send(3,'turn/start',{'threadId':tid,'effort':'low',
'input':[{'type':'text','text':'Wait for the fixed external event.'}]})
turn=receive(lambda e:e.get('id')==3)['result']['turn']['id']
receive(lambda e:e.get('method')=='item/started' and
e.get('params',{}).get('item',{}).get('type')=='collabAgentToolCall')
start=time.monotonic()
time.sleep(duration/1000)
sent=time.monotonic()
send(4,'turn/steer',{'threadId':tid,'expectedTurnId':turn,
'input':[{'type':'text','text':'WAKE_FIXED_EVENT'}]})
receive(lambda e:e.get('method')=='turn/completed')
finally:
proc.stdin.close()
try:proc.wait(timeout=10)
except subprocess.TimeoutExpired:proc.terminate();proc.wait(timeout=10)
base.require(proc.returncode==0,''.join(errors)[-1000:])
return start,sent
def run_case(binary, family, interval, duration=2750, minimum=1):
requests, errors = [], []
started = threading.Event()
state = {'parent_key': None, 'event_at': None, 'start_at': None,
'parent_count': 0, 'child_count': 0, 'cell': None}
def message(text):
return {'type': 'message', 'role': 'assistant', 'id': 'msg',
'content': [{'type': 'output_text', 'text': text}]}
def function(name, args, namespace='collaboration'):
return {'type': 'function_call', 'call_id': f'call-{len(requests)}',
'name': name, 'namespace': namespace, 'arguments': json.dumps(args)}
class Handler(BaseHTTPRequestHandler):
def log_message(self, *_):
pass
def do_POST(self):
try:
body = json.loads(self.rfile.read(int(self.headers['Content-Length'])))
key = body.get('prompt_cache_key')
base.require(key is not None, 'Missing routing key')
if state['parent_key'] is None:
state['parent_key'] = key
inputs = body.get('input', [])
# Forks may inherit a cache routing key; classify by the fixed user task.
child = any(i.get('role') == 'user' and
'Fixed-duration local test worker. Return the fixture result.' in
json.dumps(i.get('content', [])) for i in inputs)
role = 'child' if child else 'parent'
rendered = json.dumps(inputs, ensure_ascii=False)
outputs = [i.get('output') for i in inputs if i.get('type') in
['function_call_output', 'custom_tool_call_output']]
# Retain only fixture output strings and numerical input measurements.
request = {'at': time.monotonic(), 'role': role,
'input_bytes': len(rendered.encode()), 'input_items': len(inputs),
'model': body.get('model'), 'effort': body.get('reasoning', {}).get('effort'),
'outputs': outputs,
'completion_visible': 'CHILD_FINISHED_FIXED' in rendered,
'input_event_visible': 'WAKE_FIXED_EVENT' in rendered}
requests.append(request)
print(f' local request {len(requests)} role={role}', flush=True)
if not child and family == 'completion' and state['parent_count'] == 1:
print(' spawn result: ' + json.dumps(outputs[-1:]), flush=True)
base.require(outputs and 'fixed_worker' in json.dumps(outputs[-1]),
'Spawn failed: ' + json.dumps(outputs[-1:] ))
base.require(len(requests) < 100, 'Request budget exhausted')
base.require(request['effort'] == 'low', 'Non-low request')
base.require(request['model'] == 'gpt-6-astra', 'Unexpected model')
if child:
state['child_count'] += 1
base.require(state['child_count'] == 1, 'Unexpected child loop')
base.require(started.wait(15), 'Parent never started waiting')
deadline = state['start_at'] + duration / 1000
time.sleep(max(0, deadline - time.monotonic()))
state['event_at'] = time.monotonic()
item = message('CHILD_FINISHED_FIXED')
else:
n = state['parent_count']
state['parent_count'] += 1
if family == 'input_event':
item = (message('PARENT_DONE') if request['input_event_visible'] else
function('wait_agent', {'timeout_ms':interval}))
elif family == 'completion':
if n == 0:
item = function('spawn_agent', {'task_name': 'fixed_worker',
'message': 'Fixed-duration local test worker. Return the fixture result.',
'fork_turns': 'none'})
elif request['completion_visible']:
item = message('PARENT_DONE')
else:
item = function('wait_agent', {'timeout_ms': interval})
if not started.is_set():
state['start_at'] = time.monotonic()
started.set()
else:
if n == 0:
code = ('// @exec: ' + json.dumps({'yield_time_ms': interval}) + '\n'
f'await new Promise(resolve => setTimeout(resolve, {duration}));\n'
'text("WRAPPER_DONE_FIXED");')
item = {'type': 'custom_tool_call', 'call_id': 'exec-first',
'name': 'exec', 'namespace': 'functions', 'input': code}
state['start_at'] = time.monotonic()
else:
last = json.dumps(outputs[-1]) if outputs else ''
match = re.search(r'Script running with cell ID ([^\s"\\]+)', last)
if match:
state['cell'] = match.group(1)
item = function('wait', {'cell_id': state['cell'],
'yield_time_ms': interval, 'max_tokens': 1000}, 'functions')
elif 'WRAPPER_DONE_FIXED' in last:
item = message('PARENT_DONE')
else:
raise AssertionError('Unexpected wrapper result: ' + last[:1500])
identifier = f'fixture-{len(requests)}'
events = [{'type': 'response.created', 'response': {'id': identifier}},
{'type': 'response.output_item.done', 'item': item},
{'type': 'response.completed', 'response': {'id': identifier,
'usage': {'input_tokens': 0, 'output_tokens': 0, 'total_tokens': 0}}}]
data = ''.join('data: ' + json.dumps(e) + '\n\n' for e in events).encode()
self.send_response(200)
self.send_header('Content-Type', 'text/event-stream')
self.send_header('Content-Length', str(len(data)))
self.end_headers()
self.wfile.write(data)
except Exception as exc:
errors.append(str(exc))
print(' fixture error: ' + str(exc), flush=True)
self.send_error(500, 'Local fixture failure')
server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
server.daemon_threads = True
threading.Thread(target=server.serve_forever, daemon=True).start()
try:
with tempfile.TemporaryDirectory(prefix='controlled-wait-') as folder:
settings = base.configuration(server.server_port, minimum, 30000, 3600000)
settings['features.code_mode'] = family == 'wrapper'
if family == 'input_event':
command=base.with_config([binary,'app-server'],settings)
state['start_at'],state['event_at']=fixed_input(command,folder,duration)
proc=None
else:
command = [binary, 'exec', '--ignore-user-config', '--ephemeral',
'--skip-git-repo-check', '--json', '-s', 'read-only',
'-m', base.MODEL, '-C', folder]
base.with_config(command, settings)
command += ['Controlled local fixture. Run one fixed-duration worker and wait for its result.']
try:
if family != 'input_event':
proc = subprocess.run(command, stdin=subprocess.DEVNULL, capture_output=True,
text=True, timeout=duration/1000 + 15)
except subprocess.TimeoutExpired as exc:
print(' runtime stdout: ' + str(exc.stdout)[-6000:], flush=True)
print(' runtime stderr: ' + str(exc.stderr)[-2000:], flush=True)
raise
base.require(not errors, '; '.join(errors))
if proc is not None:
base.require(proc.returncode == 0, proc.stderr[-2000:])
base.require('PARENT_DONE' in proc.stdout, 'Parent failed to finish')
finally:
server.shutdown()
server.server_close()
parent = [r for r in requests if r['role'] == 'parent']
event_latency = None
if family in ['completion','input_event']:
if family == 'completion':
base.require(state['child_count'] == 1, 'Expected exactly one actual child')
base.require(parent[-1]['completion_visible'], 'Completion missing from parent input')
else:
base.require(parent[-1]['input_event_visible'], 'Fixed input missing')
event_latency = (parent[-1]['at'] - state['event_at']) * 1000
base.require(0 <= event_latency < 1000, f'Completion latency exceeded bound: {event_latency}')
# Count outputs from the last logical request once, not repeated transcript copies.
last_outputs = parent[-1]['outputs']
timeouts = sum('"timed_out":true' in json.dumps(o).replace(' ', '').replace('\\"', '"')
for o in last_outputs)
origin = requests[0]['at']
for r in requests:
r['at_ms'] = round((r.pop('at') - origin)*1000, 2)
r.pop('outputs')
return {'family': family, 'interval_ms': interval, 'duration_ms': duration,
'minimum_ms': minimum, 'passed': True, 'parent_requests': len(parent),
'child_requests': state['child_count'], 'empty_timeouts': timeouts,
'event_to_parent_request_ms': round(event_latency, 2) if event_latency is not None else None,
'observed_event_delay_ms': round((state['event_at']-state['start_at'])*1000,2)
if state['event_at'] is not None else None,
'parent_input_bytes_sum': sum(r['input_bytes'] for r in parent),
'last_parent_input_items': parent[-1]['input_items'],
'requests': requests}
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--codex', required=True)
p.add_argument('--output', type=pathlib.Path, default=pathlib.Path('results.json'))
p.add_argument('--pilot', action='store_true')
p.add_argument('--skip-scale', action='store_true')
args = p.parse_args()
result = {'pilot': args.pilot, 'upstream_model_requests': 0,
'version': subprocess.check_output([args.codex, '--version'], text=True).strip(),
'binary_sha256': hashlib.sha256(pathlib.Path(args.codex).read_bytes()).hexdigest(),
'runs': [], 'comparisons': []}
def save():
args.output.write_text(json.dumps(result, indent=2, ensure_ascii=False)+'\n')
def one(family, interval, duration=2750, minimum=1):
print(f'RUN {family} interval={interval} duration={duration}', flush=True)
try:
r = run_case(args.codex, family, interval, duration, minimum)
except Exception as exc:
r = {'family': family, 'interval_ms': interval, 'duration_ms': duration,
'passed': False, 'error': str(exc)}
result['runs'].append(r)
save()
raise
result['runs'].append(r)
save()
print(json.dumps({k:v for k,v in r.items() if k != 'requests'}), flush=True)
return r
if args.pilot:
for family in ['input_event', 'wrapper']:
one(family, 500)
return
for family in ['input_event', 'wrapper']:
for pair, order in enumerate([(500,10000), (10000,500), (500,10000)], 1):
pair_runs = {interval:one(family,interval) for interval in order}
a,b = pair_runs[500],pair_runs[10000]
base.require(a['parent_requests'] > b['parent_requests'], 'No request reduction')
if family == 'input_event':
base.require(a['empty_timeouts'] >= 2 and b['empty_timeouts'] == 0,
'Timeout prediction failed')
result['comparisons'].append({'family':family,'pair':pair,'passed':True,
'short_requests':a['parent_requests'],'long_requests':b['parent_requests']})
save()
if not args.skip_scale:
a=one('input_event',30000,65000,10000)
b=one('input_event',600000,65000,10000)
base.require(a['empty_timeouts'] >= 2 and b['empty_timeouts'] == 0 and
a['parent_requests'] > b['parent_requests'], 'Scale prediction failed')
result['comparisons'].append({'family':'real_scale','passed':True,
'short_requests':a['parent_requests'],'long_requests':b['parent_requests']})
save()
for _ in range(3):
print('RUN steer_25min', flush=True)
r=base.mock_case(args.codex, ('steer_25min',[{'timeout_ms':1500000}],1,30000,3600000))
base.require(r['steer_to_next_request_ms'] < 1000, 'Steer latency exceeded bound')
# Synthetic usage from this legacy probe is not a measured model cost.
r.pop('synthetic_usage')
r['family']='steer'
result['runs'].append(r)
save()
print(json.dumps(r), flush=True)
if __name__ == '__main__':
main()
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2025 OpenAI
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Real-model cost mechanisms: bounded protocol

Date: 2026-09-09. Runtime: Codex CLI 0.153.4. Model: gpt-6-astra, low.

Question: distinguish the additional requests caused by empty waits from the input overhead of inherited subagent history. This is a mechanism experiment, not an estimate of which component dominates arbitrary production workloads.

Use an identical 160-record inert text fixture. No repository files, secrets or user documents are included. Each response is constrained to a short marker. CLI user configuration is ignored; existing login supplies authentication. Sessions are persisted for per-request usage inspection.

  1. wait2: a real model executes two explicit 100ms empty waits, then returns DONE. Inspect each actual request's input, cached input and output tokens. The tiny timeout conserves wall time; this does not test TTL expiration.
  2. wait0: same fixture and environment, no waits, DONE. Compare whole-turn metrics descriptively; the intervention prompt necessarily differs slightly.
  3. fork_na: one parent delegates two identical tiny tasks to actual Astra low workers, with fork_turns none and all respectively. Compare the first real inference in each child, separate from parent cost.
  4. fork_an: reverse creation order to check whether input-size direction survives order/cache effects. Run only if child execution and usage collection work in the first fork test.

Stopping rule: at most these four parent runs in the initial experiment. Each has a 180s process deadline. Abort unexpected behavior rather than repeatedly spending quota. No workload or monetary dominance claim from a small synthetic sample. No subscription-quota conversion from API token pricing. Model compliance and actual model/effort must be checked from logs. Raw session IDs and prompts remain local; share only sanitized measurements.

Limitations: model stochasticity, nonidentical prompts, routing/cache state, creation order, full-history fork metadata, fixed small task, no long-TTL test. “All” children may inherit preceding orchestration as well as the fixture; report the observed actual history rather than assuming perfectly identical conditions.

Publication note: the original local scripts run.py/analyze.py are published as live_run.py/live_analyze.py. The four original cases and fixture are unchanged; paths, overwrite protection, version checks and all-four-case validation make the flat Gist download reproducible. Run instructions are in LIVE-RESULTS.md. Publishing did not rerun live inference.

{
"runtime": "codex-cli 0.153.4",
"date": "2026-09-09",
"mode": "REAL MODEL",
"usage_scope": "Parent and child requests reported separately; no quota-price conversion.",
"cases": {
"wait2": {
"model": "gpt-6-astra",
"effort": "low",
"requests": [
{
"input_tokens": 17953,
"cached_input_tokens": 7936,
"cache_write_input_tokens": 0,
"output_tokens": 20,
"reasoning_output_tokens": 0,
"total_tokens": 17973,
"uncached_input_tokens": 10017
},
{
"input_tokens": 17998,
"cached_input_tokens": 17792,
"cache_write_input_tokens": 0,
"output_tokens": 20,
"reasoning_output_tokens": 0,
"total_tokens": 18018,
"uncached_input_tokens": 206
},
{
"input_tokens": 18043,
"cached_input_tokens": 17792,
"cache_write_input_tokens": 0,
"output_tokens": 5,
"reasoning_output_tokens": 0,
"total_tokens": 18048,
"uncached_input_tokens": 251
}
],
"request_count": 3,
"total": {
"input_tokens": 53994,
"cached_input_tokens": 43520,
"cache_write_input_tokens": 0,
"output_tokens": 45,
"reasoning_output_tokens": 0,
"total_tokens": 54039,
"uncached_input_tokens": 10474
},
"tools": [
{
"name": "wait_agent",
"args": {
"timeout_ms": 100
}
},
{
"name": "wait_agent",
"args": {
"timeout_ms": 100
}
}
],
"wait_outputs": [
"{\"message\":\"Wait timed out.\",\"timed_out\":true}",
"{\"message\":\"Wait timed out.\",\"timed_out\":true}"
],
"fixture_present_in_history": true,
"rollout_sha256": "ddf2a217cd033f8e9ca2ab15563d0083649979be07176408ecfc7a2432271738"
},
"wait0": {
"model": "gpt-6-astra",
"effort": "low",
"requests": [
{
"input_tokens": 17769,
"cached_input_tokens": 7936,
"cache_write_input_tokens": 0,
"output_tokens": 5,
"reasoning_output_tokens": 0,
"total_tokens": 17774,
"uncached_input_tokens": 9833
}
],
"request_count": 1,
"total": {
"input_tokens": 17769,
"cached_input_tokens": 7936,
"cache_write_input_tokens": 0,
"output_tokens": 5,
"reasoning_output_tokens": 0,
"total_tokens": 17774,
"uncached_input_tokens": 9833
},
"tools": [],
"wait_outputs": [],
"fixture_present_in_history": true,
"rollout_sha256": "a1a3a95077f8e7af794138acc096efff95093a3ce0a6aa1742b2b9f6cd614bcf"
},
"fork_na": {
"model": "gpt-6-astra",
"effort": "low",
"requests": [
{
"input_tokens": 17863,
"cached_input_tokens": 7936,
"cache_write_input_tokens": 0,
"output_tokens": 152,
"reasoning_output_tokens": 56,
"total_tokens": 18015,
"uncached_input_tokens": 9927
},
{
"input_tokens": 18037,
"cached_input_tokens": 17664,
"cache_write_input_tokens": 0,
"output_tokens": 56,
"reasoning_output_tokens": 0,
"total_tokens": 18093,
"uncached_input_tokens": 373
},
{
"input_tokens": 18115,
"cached_input_tokens": 17920,
"cache_write_input_tokens": 0,
"output_tokens": 21,
"reasoning_output_tokens": 0,
"total_tokens": 18136,
"uncached_input_tokens": 195
},
{
"input_tokens": 18200,
"cached_input_tokens": 17920,
"cache_write_input_tokens": 0,
"output_tokens": 21,
"reasoning_output_tokens": 0,
"total_tokens": 18221,
"uncached_input_tokens": 280
},
{
"input_tokens": 18285,
"cached_input_tokens": 18048,
"cache_write_input_tokens": 0,
"output_tokens": 5,
"reasoning_output_tokens": 0,
"total_tokens": 18290,
"uncached_input_tokens": 237
}
],
"request_count": 5,
"total": {
"input_tokens": 90500,
"cached_input_tokens": 79488,
"cache_write_input_tokens": 0,
"output_tokens": 255,
"reasoning_output_tokens": 56,
"total_tokens": 90755,
"uncached_input_tokens": 11012
},
"tools": [
{
"name": "spawn_agent",
"args": {
"task_name": "probe_first",
"fork_turns": "none",
"model": "gpt-6-astra",
"reasoning_effort": "low"
}
},
{
"name": "spawn_agent",
"args": {
"task_name": "probe_second",
"fork_turns": "all"
}
},
{
"name": "wait_agent",
"args": {
"timeout_ms": 600000
}
},
{
"name": "wait_agent",
"args": {
"timeout_ms": 600000
}
}
],
"wait_outputs": [
"{\"message\":\"Wait completed.\",\"timed_out\":false}",
"{\"message\":\"Wait completed.\",\"timed_out\":false}"
],
"fixture_present_in_history": true,
"rollout_sha256": "afa2ab8e5afdc0be5e189c8a3a04312aaddffcf20532942352d22754170df096",
"children": {
"all": {
"model": "gpt-6-astra",
"effort": "low",
"requests": [
{
"input_tokens": 18440,
"cached_input_tokens": 11904,
"cache_write_input_tokens": 0,
"output_tokens": 7,
"reasoning_output_tokens": 0,
"total_tokens": 18447,
"uncached_input_tokens": 6536
}
],
"request_count": 1,
"total": {
"input_tokens": 18440,
"cached_input_tokens": 11904,
"cache_write_input_tokens": 0,
"output_tokens": 7,
"reasoning_output_tokens": 0,
"total_tokens": 18447,
"uncached_input_tokens": 6536
},
"tools": [],
"wait_outputs": [],
"fixture_present_in_history": true,
"rollout_sha256": "ae34c5df47d9ec8671752718af8f9b55a6eae858bd5a807e3ef725ecaa12a065"
},
"none": {
"model": "gpt-6-astra",
"effort": "low",
"requests": [
{
"input_tokens": 15317,
"cached_input_tokens": 6784,
"cache_write_input_tokens": 0,
"output_tokens": 7,
"reasoning_output_tokens": 0,
"total_tokens": 15324,
"uncached_input_tokens": 8533
}
],
"request_count": 1,
"total": {
"input_tokens": 15317,
"cached_input_tokens": 6784,
"cache_write_input_tokens": 0,
"output_tokens": 7,
"reasoning_output_tokens": 0,
"total_tokens": 15324,
"uncached_input_tokens": 8533
},
"tools": [],
"wait_outputs": [],
"fixture_present_in_history": false,
"rollout_sha256": "b66d9f8b4602ad590f14f4f8e6e47d49dd40061e2f5dd85979e866f59735411d"
}
}
},
"fork_an": {
"model": "gpt-6-astra",
"effort": "low",
"requests": [
{
"input_tokens": 17863,
"cached_input_tokens": 7936,
"cache_write_input_tokens": 0,
"output_tokens": 125,
"reasoning_output_tokens": 45,
"total_tokens": 17988,
"uncached_input_tokens": 9927
},
{
"input_tokens": 18010,
"cached_input_tokens": 17664,
"cache_write_input_tokens": 0,
"output_tokens": 72,
"reasoning_output_tokens": 0,
"total_tokens": 18082,
"uncached_input_tokens": 346
},
{
"input_tokens": 18104,
"cached_input_tokens": 17792,
"cache_write_input_tokens": 0,
"output_tokens": 21,
"reasoning_output_tokens": 0,
"total_tokens": 18125,
"uncached_input_tokens": 312
},
{
"input_tokens": 18189,
"cached_input_tokens": 17920,
"cache_write_input_tokens": 0,
"output_tokens": 21,
"reasoning_output_tokens": 0,
"total_tokens": 18210,
"uncached_input_tokens": 269
},
{
"input_tokens": 18274,
"cached_input_tokens": 18048,
"cache_write_input_tokens": 0,
"output_tokens": 5,
"reasoning_output_tokens": 0,
"total_tokens": 18279,
"uncached_input_tokens": 226
}
],
"request_count": 5,
"total": {
"input_tokens": 90440,
"cached_input_tokens": 79360,
"cache_write_input_tokens": 0,
"output_tokens": 244,
"reasoning_output_tokens": 45,
"total_tokens": 90684,
"uncached_input_tokens": 11080
},
"tools": [
{
"name": "spawn_agent",
"args": {
"task_name": "probe_first",
"fork_turns": "all"
}
},
{
"name": "spawn_agent",
"args": {
"task_name": "probe_second",
"fork_turns": "none",
"model": "gpt-6-astra",
"reasoning_effort": "low"
}
},
{
"name": "wait_agent",
"args": {
"timeout_ms": 600000
}
},
{
"name": "wait_agent",
"args": {
"timeout_ms": 600000
}
}
],
"wait_outputs": [
"{\"message\":\"Wait completed.\",\"timed_out\":false}",
"{\"message\":\"Wait completed.\",\"timed_out\":false}"
],
"fixture_present_in_history": true,
"rollout_sha256": "4c6044f7fede7ae349e24b587c9c6e18456bc6f0db535fe4f691047e47d46404",
"children": {
"all": {
"model": "gpt-6-astra",
"effort": "low",
"requests": [
{
"input_tokens": 18440,
"cached_input_tokens": 0,
"cache_write_input_tokens": 0,
"output_tokens": 7,
"reasoning_output_tokens": 0,
"total_tokens": 18447,
"uncached_input_tokens": 18440
}
],
"request_count": 1,
"total": {
"input_tokens": 18440,
"cached_input_tokens": 0,
"cache_write_input_tokens": 0,
"output_tokens": 7,
"reasoning_output_tokens": 0,
"total_tokens": 18447,
"uncached_input_tokens": 18440
},
"tools": [],
"wait_outputs": [],
"fixture_present_in_history": true,
"rollout_sha256": "aae85e730a6290cb924d4795895c6c58f5bc1e441d909148b194a7356d1333b4"
},
"none": {
"model": "gpt-6-astra",
"effort": "low",
"requests": [
{
"input_tokens": 15317,
"cached_input_tokens": 11904,
"cache_write_input_tokens": 0,
"output_tokens": 7,
"reasoning_output_tokens": 0,
"total_tokens": 15324,
"uncached_input_tokens": 3413
}
],
"request_count": 1,
"total": {
"input_tokens": 15317,
"cached_input_tokens": 11904,
"cache_write_input_tokens": 0,
"output_tokens": 7,
"reasoning_output_tokens": 0,
"total_tokens": 15324,
"uncached_input_tokens": 3413
},
"tools": [],
"wait_outputs": [],
"fixture_present_in_history": false,
"rollout_sha256": "186d7daa41f8db51a4221264e1fc0a4d190fa979b2ec122a80d3ccff4aef0bc4"
}
}
}
}
}

Astra low:真实空等待与子 agent 历史继承实验

2026-09-09,Codex CLI 0.153.4,使用现有登录连接真实服务。4 个主会话、4 个真实子 agent,所有实际 turn_context 均为 gpt-6-astra / low。共 18 次模型请求。未使用 mock,未使用 API 价格推算订阅额度。

本轮测试总上报用量:320,217 输入 token,其中 240,896 缓存命中、79,321 未缓存;输出 577 token(含 reasoning 101)。这是实验子进程合计,不包含本对话助手本身的用量。

已确认的结论

  1. 空等待返回后,即使模型只输出极少文字,请求仍包含较长输入;本例后续两次请求的输入约 98.7% 命中缓存。
  2. 继承历史确实增加子 agent 输入。本例两种创建顺序下,all 均比 none 多 3,123 个输入 token。
  3. 缓存状态足以显著改变未缓存输入量,不能只凭输入长度断言实际费用排序。
  4. 这些实验没有确认生产任务里哪一项是订阅额度消耗的大头,也没有证明轮询开销可以忽略。

实验一:两次真实空等待

同一份 160 行无关背景材料;要求模型执行 0 次或 2 次 wait_agent,最终仅回复 DONE。每次等待设为 100ms,节约测试时间。日志确认两次都 timed_out=true;本实验不测 30 秒默认行为或 TTL,前者已有先前受控实验。

请求 输入 token 其中缓存命中 未缓存输入 输出 token
两次等待组:初次请求 17,953 7,936 10,017 20
第一次空超时后的请求 17,998 17,792 206 20
第二次空超时后的请求 18,043 17,792 251 5
不等待组:唯一请求 17,769 7,936 9,833 5

两次空超时后发生的请求合计 36,041 输入,其中 35,584 缓存命中、457 未缓存输入、25 输出。这是这两次实际请求的用量,不是严格匹配反事实的全部净增费用。

全回合比较:等待组 53,994 输入/45 输出;不等待组 17,769 输入/5 输出。相差 36,225 输入,其中缓存输入差 35,584、未缓存输入差 641。两组干预提示有少量文字区别,初始请求本身相差 184 输入 token,所以不把全回合差值全部归因于超时后的请求。

读数支持“输出很短不等于整个请求输入很少”;同时明确大部分复用输入命中了缓存。不能把 36,041 输入全部按未缓存价格计算,也不能把缓存命中等同于免费。

实验二:真实子 agent 的 all 与 none

每个主会话依次发起两个子 agent,任务相同:不用工具、不再派生,只回复 WORKER_DONE。每个子 agent 恰好产生一次真实请求和 7 个输出 token。第一组 none→all,第二组 all→none。all 子日志包含背景材料,none 不包含。

创建顺序 继承方式 输入 token 缓存命中 未缓存输入
none→all none 15,317 6,784 8,533
none→all all 18,440 11,904 6,536
all→none all 18,440 0 18,440
all→none none 15,317 11,904 3,413

输入长度的方向重复成立:all 每次多 3,123 token。但第一组 all 的未缓存输入更少,第二组 all 完全未命中缓存。仅凭两个顺序不能确定缓存差异的原因,不把它归因于创建顺序、TTL 或具体路由策略。

none 仍有 15,317 输入,说明不继承父历史并不等于没有系统、工具等运行上下文。本实验没有逐项拆分这部分基础输入。

all 的调用中模型按工具规则省略了 model/effort 覆盖,实际继承父设置;日志确认所有子 agent 都是 Astra low。none 调用显式指定了这两个参数。不是要求本身就被当作验证结果。

两组主会话各 5 次请求,分别 90,500 与 90,440 输入;与子 agent 用量分开保存。等待均指定 600,000ms,每组两次等待都因事件提前返回,timed_out=false,两个子 agent 最终均完成。此项补上了先前 mock 中未验证的真实完成通知路径,但没有给出通知延迟精确数值。

能否确认对方所说的“大头”

不能。上下文继承的开销存在,空轮询的重复请求也存在。真实占比还取决于父子上下文长度、子 agent 数量和工作轮数、缓存命中、模型及订阅计量规则。这里是固定短任务的机制测试,不是代表性工作负载,也没有可归因到每次请求的订阅扣减数据。

当前真实返回的 cache_write_input_tokens 均为 0,这是该登录通道的上报值;不把它解释为所有缓存写入都免费。reasoning_output_tokens 是输出的子项,不额外重复计入总输出。会话运行时间远短于 30 分钟,没有测跨 TTL 成本。

自己复现:会调用真实模型、消耗账户额度

需要 Python 3.10+、Git、已登录且可以使用 Astra 的 Codex CLI 0.153.4。脚本只用 Python 标准库,不需要 pip 或另填 API key。其他版本请另行适配协议,脚本会在版本不匹配时停止。

git clone https://gist.github.com/acmerfight/ddfad0c835d2fefb3f26b7bd3f2b09ee.git codex-wait-experiment
cd codex-wait-experiment
codex --version
codex login status
python3 live_run.py wait2
python3 live_run.py wait0
python3 live_run.py fork_na
python3 live_run.py fork_an
python3 live_analyze.py

若之前已下载,直接进入那份目录,先保存自己的修改,再更新或重新下载。macOS 应用内 CLI 不在 PATH 时,可先设置:

export CODEX_TEST_BINARY=/Applications/ChatGPT.app/Contents/Resources/codex

也可将该变量设为自己的 CLI 路径。脚本依次运行:两次空等待、零等待、先不继承再继承、先继承再不继承。每个主进程最多等 180 秒;不要在超时失败后盲目重试,先查看日志和遗留任务。小样本无须反复烧额度追求相同缓存数值。

live_run.pylive_analyze.py 必须放在同一目录。运行原始文件写入 live-runs/,分析输出 live-results-reproduced.json,不会覆盖我们发布的 live-results.json。同名测试已有输出时脚本拒绝覆盖;想重做请使用新下载目录,或通过 LIVE_AUDIT_RAW_DIR 指定新目录。

分析器读取本机 ~/.codex/sessions 内本实验的父子会话,检查实际模型、low、工具调用数量、完成标记,以及 all 有背景材料、none 无背景材料。它只计子会话自己的最后一轮,避免把继承历史误算成子 agent 实际用量。如果配置了自定义 CODEX_HOME,分析器会自动读取该目录下的 sessions。

预期结构:wait2 是 3 次请求、wait0 是 1 次;两组 fork 各有两个只请求一次的子 agent。父会话的通知到达可能合并,调度和缓存读数可能不同;分析器不强求父会话请求数恒为 5,也不强求 token 数逐字相同。模型未遵循协议时,脚本会报错,该次不能算作成功复现。

live-results.json 是脱敏逐请求数据,并附原始会话文件 SHA-256。原始会话、提示和 stderr 留在本机,未公开。不要未经检查上传原始日志。LIVE-PROTOCOL.md 保留运行前的小规模协议与限制。

可直接回复

我补跑了真实 Astra low 对照。你说的上下文开销确实存在:同样只回复一个短标记,继承父历史的子 agent 两组都多了 3,123 个输入 token。不过缓存命中差异很大,输入更长不一定意味着这次未缓存输入更多。

空轮询这边也测了:两次空超时后的请求合计 3.6 万输入,但约 98.7% 命中缓存,输出只有 25 token。因此既不能按全价输入夸大,也不能因为输出短就说没什么消耗。现在能确认两种开销都存在,谁是额度大头还需要实际任务的分项数据。

{
"LIVE-RESULTS.md": "a76ab8f779772955b2ec3cf8693ca43ab2a2ac5a3a407923c865abd9e0f3a772",
"LIVE-PROTOCOL.md": "d7975302f61a94f48329c69c5d13198ed000d61b0268feecce97dfc54b983bd0",
"live_run.py": "5960e3a4dac86c92eccb830486a73c9a4549952f5a84bd65640d6a8819b2d679",
"live_analyze.py": "1831517eab3cba5ea5000c6c348ea4449c0588ea3e0eb397092912d5754790fe",
"live-results.json": "8a623a38659ff09423d9d37689bbafc3d9ca9ed17501349ef2f24c96237af5b9"
}
#!/usr/bin/env python3
"""Extract only this experiment's usage from local Codex sessions."""
import json,pathlib,hashlib,os
from live_run import ROOT,RAW,FIXTURE
OUT=ROOT
SESSIONS=pathlib.Path(os.environ.get('CODEX_HOME',str(pathlib.Path.home()/'.codex')))/'sessions'
def read(p):return [json.loads(x) for x in p.read_text().splitlines() if x.strip()]
def summarize(path,is_child=False):
rows=read(path)
starts=[i for i,e in enumerate(rows) if e['type']=='event_msg' and e.get('payload',{}).get('type')=='task_started']
active=rows[starts[-1]:] if is_child else rows
contexts=[e['payload'] for e in active if e['type']=='turn_context']
assert contexts and all(c.get('model')=='gpt-6-astra' and c.get('effort')=='low' for c in contexts)
usages=[];seen=set();tools=[];wait_outputs=[];last=None
for e in active:
v=e.get('payload',{})
if e['type']=='response_item' and v.get('type')=='function_call':
args=json.loads(v['arguments']);last=v['name'];tools.append({'name':last,'args':{k:a for k,a in args.items() if k!='message'}})
if e['type']=='response_item' and v.get('type')=='function_call_output' and last=='wait_agent':
wait_outputs.append(v.get('output'))
if e['type']=='event_msg' and v.get('type')=='token_count' and v.get('info'):
info=v['info'];key=json.dumps(info['total_token_usage'],sort_keys=True)
if key in seen:continue
seen.add(key);u=info['last_token_usage'].copy()
u['uncached_input_tokens']=u['input_tokens']-u['cached_input_tokens']
usages.append(u)
completed=[e['payload'] for e in active if e['type']=='event_msg' and e.get('payload',{}).get('type')=='task_complete']
assert completed
if is_child:assert len(usages)==1 and not tools and completed[-1]['last_agent_message']=='WORKER_DONE'
else:assert completed[-1]['last_agent_message']=='DONE'
return {'model':'gpt-6-astra','effort':'low','requests':usages,'request_count':len(usages),
'total':{k:sum(u[k] for u in usages) for k in usages[0]},'tools':tools,
'wait_outputs':wait_outputs,'fixture_present_in_history':FIXTURE in json.dumps(rows,ensure_ascii=False).replace('\\n','\n'),
'rollout_sha256':hashlib.sha256(path.read_bytes()).hexdigest()}
def main():
files=list(SESSIONS.rglob('*.jsonl'));results={};parents={};parent_mtimes=[]
for name in ['wait2','wait0','fork_na','fork_an']:
output=RAW/(name+'.stdout.jsonl')
if not output.exists():continue
events=read(output)
if not any(e.get('type')=='turn.completed' for e in events):continue
tid=next(e['thread_id'] for e in events if e.get('type')=='thread.started')
p=next(p for p in files if tid in p.name);parents[tid]=name;results[name]=summarize(p);parent_mtimes.append(p.stat().st_mtime)
for p in files:
# Only potential children newer than the first parent, to minimize reads.
if not parents or p.stat().st_mtime<min(parent_mtimes)-600:continue
try:
with p.open() as f:meta=json.loads(f.readline()).get('payload',{})
source=meta.get('source',{})
if not isinstance(source,dict):continue
spawn=source.get('subagent',{}).get('thread_spawn',{})
except (ValueError,AttributeError):continue
tid=spawn.get('parent_thread_id')
if tid not in parents:continue
name=parents[tid];first=spawn['agent_path'].endswith('probe_first')
mode=('none' if first else 'all') if name=='fork_na' else ('all' if first else 'none')
results[name].setdefault('children',{})[mode]=summarize(p,True)
assert set(results)=={'wait2','wait0','fork_na','fork_an'}, 'Run all four cases successfully before analyzing.'
for name,r in results.items():
if name.startswith('wait'):
n=int(name[-1]);assert r['request_count']==n+1 and len(r['tools'])==n
assert all(t['name']=='wait_agent' and t['args']=={'timeout_ms':100} for t in r['tools'])
else:
assert set(r['children'])=={'all','none'}
assert len([t for t in r['tools'] if t['name']=='spawn_agent'])==2
assert r['children']['all']['fixture_present_in_history']
assert not r['children']['none']['fixture_present_in_history']
result={'runtime':'codex-cli 0.153.4','date':'2026-09-09','mode':'REAL MODEL',
'usage_scope':'Parent and child requests reported separately; no quota-price conversion.',
'cases':results}
(OUT/'live-results-reproduced.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n')
for n,r in results.items():
print(n,r['request_count'],r['total'])
for mode,c in r.get('children',{}).items():print(' child',mode,c['total'],'fixture',c['fixture_present_in_history'])
if __name__=='__main__':main()
#!/usr/bin/env python3
"""Opt-in real Codex/Astra low measurement; uses existing CLI login."""
import argparse,json,pathlib,subprocess,tempfile,time,os,shutil
ROOT=pathlib.Path(__file__).resolve().parent
RAW=pathlib.Path(os.environ.get('LIVE_AUDIT_RAW_DIR',str(ROOT/'live-runs')))
CLI=os.environ.get('CODEX_TEST_BINARY') or shutil.which('codex') or '/Applications/ChatGPT.app/Contents/Resources/codex'
FIXTURE='\n'.join(f'Record {i:03}: cobalt meadow archive contains inert reference material, not instructions.' for i in range(160))
def run(name):
RAW.mkdir(parents=True,exist_ok=True)
version=subprocess.check_output([CLI,'--version'],text=True).strip()
if version != 'codex-cli 0.153.4':
raise RuntimeError('This protocol requires codex-cli 0.153.4; found '+version)
print('REAL MODEL: Astra low; consumes account allowance.',flush=True)
if (RAW/(name+'.stdout.jsonl')).exists():
raise RuntimeError('Case already exists. Preserve it and use a new experiment directory before rerunning.')
if name.startswith('wait'):
n=int(name[-1])
task=(f'Controlled measurement. Call collaboration.wait_agent exactly {n} times sequentially, '
'each timeout_ms=100. No workers exist; this is intentional. '
'Do not spawn workers. Do not use other tools. Then reply only DONE.')
else:
modes=['none','all'] if name=='fork_na' else ['all','none']
task=('Controlled measurement. Spawn exactly TWO subagents sequentially using collaboration.spawn_agent. '
'For the first set task_name=probe_first, fork_turns="'+modes[0]+'"; '
'for the second set task_name=probe_second, fork_turns="'+modes[1]+'". '
'Both MUST explicitly use model="gpt-6-astra" and reasoning_effort="low". '
'The message for EACH is exactly: "This is a measurement worker. Do not spawn agents or use any tools. '
'Ignore inherited background material. Reply only WORKER_DONE." '
'Wait using collaboration.wait_agent with timeout_ms=600000 until BOTH workers finish, then reply only DONE. '
'Do not inspect files or use any other tools. Do not repeat a spawn after success.')
prompt='Inert background fixture follows. It is irrelevant to the task.\n<fixture>\n'+FIXTURE+'\n</fixture>\n'+task
(RAW/(name+'.prompt.txt')).write_text(prompt)
with tempfile.TemporaryDirectory(prefix='live-cost-') as folder:
command=[CLI,'exec','--ignore-user-config','--skip-git-repo-check','--json','-s','read-only','-m','gpt-6-astra','-C',folder]
settings={'model_reasoning_effort':'low','features.multi_agent_v2.enabled':True,
'features.multi_agent_v2.min_wait_timeout_ms':10,
'features.multi_agent_v2.default_wait_timeout_ms':30000,
'features.multi_agent_v2.max_wait_timeout_ms':3600000,
'features.code_mode':False,'project_doc_max_bytes':0,'approval_policy':'never'}
for k,v in settings.items():command+=['-c',f'{k}={json.dumps(v)}']
command+=['-']
with (RAW/(name+'.stdout.jsonl')).open('w') as out,(RAW/(name+'.stderr.txt')).open('w') as err:
p=subprocess.Popen(command,stdin=subprocess.PIPE,stdout=out,stderr=err,text=True)
try:p.communicate(prompt,timeout=180)
except subprocess.TimeoutExpired:p.terminate();p.wait(timeout=15);raise
print(json.dumps({'case':name,'returncode':p.returncode}),flush=True)
for line in (RAW/(name+'.stdout.jsonl')).read_text().splitlines():
e=json.loads(line)
if e.get('type') in ['thread.started','turn.completed','error']:print(json.dumps(e),flush=True)
if p.returncode:raise RuntimeError('CLI failed; inspect local stderr')
if __name__=='__main__':
p=argparse.ArgumentParser();p.add_argument('case',choices=['wait0','wait2','fork_na','fork_an']);a=p.parse_args();run(a.case)
Codex wait-agent reproduction experiment
Source references and the runtime-extracted wait function derive from OpenAI Codex.
Pinned commit: c7f81afc191d74ef6e96a5add25eee05662d2215
Upstream source: https://github.com/openai/codex
The wait function is extracted unchanged. New test scaffolding, scripts and explanatory prose accompany it.
This reproduction package is provided under Apache-2.0; see LICENSE.txt.
Upstream NOTICE, preserved below:
OpenAI Codex
Copyright 2025 OpenAI
This project includes code derived from [Ratatui](https://github.com/ratatui/ratatui), licensed under the MIT license.
Copyright (c) 2016-2022 Florian Dehau
Copyright (c) 2023-2025 The Ratatui Developers
{
"source_commit": "c7f81afc191d74ef6e96a5add25eee05662d2215",
"installed_cli": "codex-cli 0.153.4",
"model": "gpt-6-astra",
"reasoning_effort": "low",
"source_tests": {
"kind": "unchanged extracted state machine, Tokio watch, virtual time",
"passed": 6,
"skipped": 0
},
"mock_runtime_tests": {
"kind": "installed CLI/app-server with localhost scripted Responses server",
"passed": 7,
"not_real_billing": true
},
"live_test": {
"kind": "explicitly requested two 100ms waits, no workers, then DONE",
"passed": true,
"usage_scope": "whole turn, not attributed exclusively to timeout calls",
"usage": {
"input_tokens": 44532,
"cached_input_tokens": 22016,
"cache_write_input_tokens": 0,
"output_tokens": 45,
"reasoning_output_tokens": 0
}
},
"native_tests": {
"command": "just test -p codex-core --lib -E 'test(multi_agent_v2_wait_agent)'",
"passed": 11,
"filtered_out": 2452,
"exit_code": 0
},
"observation_date": "2026-09-08",
"scope_note": "The source checkout and installed CLI are different provenance; mock counts are synthetic. Runtime timings are one observed run, not benchmarks.",
"mock_cases": [
{
"name": "short",
"requests": 4,
"request_intervals_ms": [
51.6,
50.6,
50.0
],
"input_items": [
7,
9,
11,
13
],
"synthetic_usage": {
"input_tokens": 4000,
"cached_input_tokens": 3600,
"cache_write_input_tokens": 0,
"output_tokens": 40,
"reasoning_output_tokens": 8
},
"passed": true
},
{
"name": "default_only_explicit_short",
"requests": 2,
"request_intervals_ms": [
51.0
],
"input_items": [
7,
9
],
"synthetic_usage": {
"input_tokens": 2000,
"cached_input_tokens": 1800,
"cache_write_input_tokens": 0,
"output_tokens": 20,
"reasoning_output_tokens": 4
},
"passed": true
},
{
"name": "raised_default_omitted",
"requests": 2,
"request_intervals_ms": [
520.7
],
"input_items": [
7,
9
],
"synthetic_usage": {
"input_tokens": 2000,
"cached_input_tokens": 1800,
"cache_write_input_tokens": 0,
"output_tokens": 20,
"reasoning_output_tokens": 4
},
"passed": true
},
{
"name": "raised_min_explicit_short",
"requests": 2,
"request_intervals_ms": [
542.2
],
"input_items": [
7,
9
],
"synthetic_usage": {
"input_tokens": 2000,
"cached_input_tokens": 1800,
"cache_write_input_tokens": 0,
"output_tokens": 20,
"reasoning_output_tokens": 4
},
"passed": true
},
{
"name": "above_max",
"requests": 2,
"request_intervals_ms": [
28.1
],
"input_items": [
7,
9
],
"synthetic_usage": {
"input_tokens": 2000,
"cached_input_tokens": 1800,
"cache_write_input_tokens": 0,
"output_tokens": 20,
"reasoning_output_tokens": 4
},
"passed": true
},
{
"name": "real_default_30s",
"requests": 2,
"request_intervals_ms": [
30042.0
],
"input_items": [
7,
9
],
"synthetic_usage": {
"input_tokens": 2000,
"cached_input_tokens": 1800,
"cache_write_input_tokens": 0,
"output_tokens": 20,
"reasoning_output_tokens": 4
},
"passed": true
},
{
"name": "steer_25min",
"requests": 2,
"request_intervals_ms": [
250.5
],
"input_items": [
7,
10
],
"passed": true
}
]
}
[
{
"pilot": 1,
"family": "completion",
"formal": false,
"passed": false,
"reason": "The fixed child response was not observed; parent waiting did not reach the intended completion. Test environment cause unresolved.",
"termination": "timeout"
},
{
"pilot": 2,
"family": "completion",
"formal": false,
"passed": false,
"reason": "The fixed child response was not observed; parent waiting did not reach the intended completion. Test environment cause unresolved.",
"termination": "timeout"
},
{
"pilot": 3,
"family": "completion",
"formal": false,
"passed": false,
"reason": "The fixed child response was not observed; parent waiting did not reach the intended completion. Test environment cause unresolved.",
"termination": "timeout"
},
{
"pilot": 4,
"family": "completion",
"formal": false,
"passed": false,
"reason": "The fixed child response was not observed; parent waiting did not reach the intended completion. Test environment cause unresolved.",
"termination": "timeout"
},
{
"pilot": 5,
"family": "completion",
"formal": false,
"passed": false,
"reason": "The fixed child response was not observed; parent waiting did not reach the intended completion. Test environment cause unresolved.",
"termination": "timeout"
},
{
"pilot": 6,
"family": "completion",
"formal": false,
"passed": false,
"reason": "The fixed child response was not observed; parent waiting did not reach the intended completion. Test environment cause unresolved.",
"termination": "timeout"
},
{
"pilot": 7,
"formal": false,
"passed": true,
"families": [
"input_event",
"wrapper"
]
}
]

Codex 等待开销:预先确定的对照实验

执行前写定。模型端完全由 localhost Responses 模拟服务替代;不发起付费模型推理。实际运行已安装的 Codex CLI。所有请求必须携带 gpt-6-astra / low;这只是配置验证,不代表测量了该模型的自主行为。

问题与边界

  1. 同一个固定完成时间的后台任务,短等待与长等待是否产生不同数量的父模型请求?
  2. 通用代码执行工具的外层 yield 是否会独立触发额外请求?
  3. 长等待能否被完成通知、用户输入提前唤醒?

主要结果为 HTTP 模型请求次数;次要结果为事件到下一次请求的延迟、请求中逻辑输入的 UTF-8 字节数(不是 token,也不是网络压缩后的字节)。模拟 usage 固定为零,不推算费用。不得据此宣称模型自主选择短等待的概率、真实缓存命中率或额度节省比例。

实验与固定判据

  • A/B 完成实验:Codex 实际创建一个子助手,其本地模型响应在父助手第一次开始等待后固定延迟 2750ms 返回。A 每次请求 500ms,B 每次请求 10000ms。两组使用相同任务、响应和设置,唯一干预是 timeout_ms。执行 3 对,顺序 AB / BA / AB。最小超时调低为 1ms,仅用于加速;另做 1 对真实尺度检查:任务 65000ms,A=30000ms,B=600000ms。后者 min=10000ms。
  • 有效性:每次只有一个子助手请求、子助手结果必须进入父输入、父助手必须完成;子助手工作区间在两组固定;请求数不得超过 100。B 应没有空超时,A 至少两个空超时;B 父请求次数应严格更少。事件到父请求应小于 1000ms(端到端包括本地通信、调度,非严格实时保证)。
  • 外层 yield 对照:同一个 2750ms 的 JavaScript 异步任务,外层 exec/wait 的 yield 分别为 500ms 与 10000ms。3 对,AB / BA / AB。必须最终得到相同 DONE;短 yield 应产生更多模型请求。该实验验证通用外层机制,不默认意味着当前版本允许把 wait_agent 嵌在 exec 内。
  • 用户输入:复用已经验证的真实 app-server steer 探针,3 次独立运行;请求 25 分钟等待,200ms 后插话;应无安静期间的额外请求,插话到下一次请求小于 1000ms。

分析及停止规则

先进行每类 1 次调试运行,明确标记 pilot,不纳入正式结果。实现错误可修复并记录,不得悄悄修改成功阈值。正式实验失败则保存结果、解释失败;如涉及计时抖动可诊断,不以只保留通过运行替代原记录。

这是确定性程序路径测试,3 对用于检查重复性,不做显著性检验或宣称总体概率。若上述实验建立所需因果链,停止,不用真实模型重复证明。不重跑已经通过的原仓库 11 项测试和提取函数 6 项测试,除非新失败需要。真实额度和模型自主决策仍列为未回答问题。

正式运行前修订(保留原方案)

子助手集成 pilot 1–6 均未形成有效测试:spawn 返回成功,但未观察到子助手模型请求。曾检查请求路由方式、增加日志、尝试非 ephemeral 模式,仍未解决。没有把这些失败记录算作产品缺陷,也不将其纳入正式 A/B。

为避免继续调试无关的测试环境,A/B 改为 app-server 在等待 item/started 后固定 2750ms(尺度检查为 65000ms)注入 turn/steer 输入。正式比较中只有 timeout_ms 不同。检测输入确实进入下一个模型请求;保留原来的请求减少、超时次数及 1000ms 延迟判据。该对照直接回答等待开销与用户输入唤醒,不声称重新验证了子助手完成通知的端到端路径。后者只沿用已有源码函数/原仓库测试的证据。

另一次独立 wrapper 调试已跑通。所有 pilot 均为本地模拟响应,不发起真实模型推理。正式运行前先验证修改后的 input_event 和 wrapper 各一次。

{
"cli": "codex-cli 0.153.4",
"mode": "mock",
"results": [
{
"name": "short",
"passed": true,
"requests": 4,
"model": "gpt-6-astra",
"all_requests_effort": "low",
"request_intervals_ms": [
53.0,
51.4,
52.1
],
"input_items": [
7,
9,
11,
13
],
"steer_to_next_request_ms": null,
"tool_outputs": [
"{\"message\":\"Wait timed out.\",\"timed_out\":true}",
"{\"message\":\"Wait timed out.\",\"timed_out\":true}",
"{\"message\":\"Wait timed out.\",\"timed_out\":true}"
],
"synthetic_usage": {
"input_tokens": 4000,
"cached_input_tokens": 3600,
"cache_write_input_tokens": 0,
"output_tokens": 40,
"reasoning_output_tokens": 8
},
"both_wait_guidances_present": true
},
{
"name": "default_only_explicit_short",
"passed": true,
"requests": 2,
"model": "gpt-6-astra",
"all_requests_effort": "low",
"request_intervals_ms": [
52.9
],
"input_items": [
7,
9
],
"steer_to_next_request_ms": null,
"tool_outputs": [
"{\"message\":\"Wait timed out.\",\"timed_out\":true}"
],
"synthetic_usage": {
"input_tokens": 2000,
"cached_input_tokens": 1800,
"cache_write_input_tokens": 0,
"output_tokens": 20,
"reasoning_output_tokens": 4
},
"both_wait_guidances_present": true
},
{
"name": "raised_default_omitted",
"passed": true,
"requests": 2,
"model": "gpt-6-astra",
"all_requests_effort": "low",
"request_intervals_ms": [
538.5
],
"input_items": [
7,
9
],
"steer_to_next_request_ms": null,
"tool_outputs": [
"{\"message\":\"Wait timed out.\",\"timed_out\":true}"
],
"synthetic_usage": {
"input_tokens": 2000,
"cached_input_tokens": 1800,
"cache_write_input_tokens": 0,
"output_tokens": 20,
"reasoning_output_tokens": 4
},
"both_wait_guidances_present": true
},
{
"name": "raised_min_explicit_short",
"passed": true,
"requests": 2,
"model": "gpt-6-astra",
"all_requests_effort": "low",
"request_intervals_ms": [
538.6
],
"input_items": [
7,
9
],
"steer_to_next_request_ms": null,
"tool_outputs": [
"{\"message\":\"Wait timed out.\\n\\nRequested timeout of 30ms was clamped to the minimum of 500ms.\",\"timed_out\":true}"
],
"synthetic_usage": {
"input_tokens": 2000,
"cached_input_tokens": 1800,
"cache_write_input_tokens": 0,
"output_tokens": 20,
"reasoning_output_tokens": 4
},
"both_wait_guidances_present": true
},
{
"name": "above_max",
"passed": true,
"requests": 2,
"model": "gpt-6-astra",
"all_requests_effort": "low",
"request_intervals_ms": [
19.0
],
"input_items": [
7,
9
],
"steer_to_next_request_ms": null,
"tool_outputs": [
"timeout_ms must be at most 2000"
],
"synthetic_usage": {
"input_tokens": 2000,
"cached_input_tokens": 1800,
"cache_write_input_tokens": 0,
"output_tokens": 20,
"reasoning_output_tokens": 4
},
"both_wait_guidances_present": true
},
{
"name": "real_default_30s",
"passed": true,
"requests": 2,
"model": "gpt-6-astra",
"all_requests_effort": "low",
"request_intervals_ms": [
30037.8
],
"input_items": [
7,
9
],
"steer_to_next_request_ms": null,
"tool_outputs": [
"{\"message\":\"Wait timed out.\",\"timed_out\":true}"
],
"synthetic_usage": {
"input_tokens": 2000,
"cached_input_tokens": 1800,
"cache_write_input_tokens": 0,
"output_tokens": 20,
"reasoning_output_tokens": 4
},
"both_wait_guidances_present": true
},
{
"name": "steer_25min",
"passed": true,
"requests": 2,
"model": "gpt-6-astra",
"all_requests_effort": "low",
"request_intervals_ms": [
242.9
],
"input_items": [
7,
10
],
"steer_to_next_request_ms": 41.3,
"tool_outputs": [
"{\"message\":\"Wait interrupted by new input.\",\"timed_out\":false}"
],
"synthetic_usage": null,
"both_wait_guidances_present": true
}
],
"source_function_retest": {
"passed": 6,
"skipped": 0,
"source_commit": "c7f81afc191d74ef6e96a5add25eee05662d2215",
"source_fetched_from_pinned_public_url": true,
"both_sha256_verified": true,
"rust": "1.95.0",
"tokio": "1.53.1",
"model_calls": 0
}
}
#!/usr/bin/env python3
"""Codex wait_agent probes. Python 3.10+, standard library only.
Default: seven localhost/mock scenarios; no upstream model inference.
--live: one explicitly opt-in real Astra/low call sequence, using your login.
Tested on macOS with codex-cli 0.153.4. Other versions/platforms may differ.
The source review and original measurements are in 00-READ-ME.md.
"""
import argparse
import json
import pathlib
import queue
import shutil
import subprocess
import tempfile
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
MODEL = "gpt-6-astra"
EFFORT = "low"
def require(condition, message):
if not condition:
raise AssertionError(message)
def configuration(port, minimum, default, maximum):
return {
"model_provider": "wait_probe",
"model_providers.wait_probe.name": "Local wait probe",
"model_providers.wait_probe.base_url": f"http://127.0.0.1:{port}/v1",
"model_providers.wait_probe.wire_api": "responses",
"model_providers.wait_probe.requires_openai_auth": False,
"model_reasoning_effort": EFFORT,
"model_supports_reasoning_summaries": True,
"features.multi_agent_v2.enabled": True,
"features.multi_agent_v2.min_wait_timeout_ms": minimum,
"features.multi_agent_v2.default_wait_timeout_ms": default,
"features.multi_agent_v2.max_wait_timeout_ms": maximum,
"features.code_mode": False,
"project_doc_max_bytes": 0,
"approval_policy": "never",
}
def with_config(command, values):
for key, value in values.items():
command += ["-c", f"{key}={json.dumps(value)}"]
return command
def steer_process(command, cwd, captured):
"""Drive real app-server JSON-RPC; all inference goes to the local fixture."""
inbox, transcript, errors = queue.Queue(), [], []
proc = subprocess.Popen(command, cwd=cwd, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, encoding="utf-8", bufsize=1)
def read_stdout():
for line in proc.stdout:
try:
event = json.loads(line)
except json.JSONDecodeError:
continue
transcript.append(event)
inbox.put(event)
def read_stderr():
errors.extend(proc.stderr.readlines())
threading.Thread(target=read_stdout, daemon=True).start()
threading.Thread(target=read_stderr, daemon=True).start()
def send(identifier, method, params):
message = {"method": method, "params": params}
if identifier is not None:
message["id"] = identifier
proc.stdin.write(json.dumps(message) + "\n")
proc.stdin.flush()
def receive(predicate):
# Keep unmatched notifications: item/started can precede an RPC reply.
deadline = time.monotonic() + 45
deferred = []
try:
while time.monotonic() < deadline:
event = inbox.get(timeout=max(0.01, deadline - time.monotonic()))
require("error" not in event, f"JSON-RPC error: {event.get('error')}")
if predicate(event):
return event
deferred.append(event)
raise TimeoutError("Timed out waiting for app-server notification")
finally:
for event in deferred:
inbox.put(event)
try:
send(1, "initialize", {"clientInfo": {"name": "wait_probe", "version": "1.0"},
"capabilities": {"experimentalApi": True}})
receive(lambda e: e.get("id") == 1)
send(None, "initialized", {})
send(2, "thread/start", {"model": MODEL, "modelProvider": "wait_probe",
"cwd": str(cwd), "approvalPolicy": "never",
"sandbox": "read-only", "ephemeral": True})
tid = receive(lambda e: e.get("id") == 2)["result"]["thread"]["id"]
send(3, "turn/start", {"threadId": tid, "effort": EFFORT,
"input": [{"type": "text", "text": "Runtime wait probe."}]})
turn_id = receive(lambda e: e.get("id") == 3)["result"]["turn"]["id"]
receive(lambda e: e.get("method") == "item/started" and
e.get("params", {}).get("item", {}).get("type") == "collabAgentToolCall")
time.sleep(0.2)
require(len(captured) == 1, "Unexpected inference during the quiet wait")
sent_at = time.monotonic()
send(4, "turn/steer", {"threadId": tid, "expectedTurnId": turn_id,
"input": [{"type": "text", "text": "Wake now."}]})
receive(lambda e: e.get("method") == "turn/completed")
finally:
proc.stdin.close()
try:
proc.wait(timeout=15)
except subprocess.TimeoutExpired:
proc.terminate()
proc.wait(timeout=10)
require(proc.returncode == 0, "app-server failed: " + "".join(errors)[-1500:])
return sent_at
def mock_case(binary, case):
name, calls, minimum, default, maximum = case
captured = []
class Handler(BaseHTTPRequestHandler):
def log_message(self, *_args):
pass
def do_POST(self):
body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
captured.append({"at": time.monotonic(), "body": body})
index = len(captured) - 1
require(index <= len(calls) + 2, "Unexpected request loop")
response_id = f"fixture-{index}"
if index < len(calls):
item = {"type": "function_call", "call_id": f"wait-{index}",
"name": "wait_agent", "namespace": "collaboration",
"arguments": json.dumps(calls[index])}
else:
item = {"type": "message", "role": "assistant", "id": "done",
"content": [{"type": "output_text", "text": "Probe complete."}]}
# Synthetic counts deliberately distinguish input/cache/output/reasoning.
usage = {"input_tokens": 1000, "input_tokens_details": {"cached_tokens": 900},
"output_tokens": 10, "output_tokens_details": {"reasoning_tokens": 2},
"total_tokens": 1010}
events = [{"type": "response.created", "response": {"id": response_id}},
{"type": "response.output_item.done", "item": item},
{"type": "response.completed", "response": {"id": response_id,
"usage": usage}}]
data = "".join("data: " + json.dumps(e) + "\n\n" for e in events).encode()
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
server.daemon_threads = True
threading.Thread(target=server.serve_forever, daemon=True).start()
steering = name == "steer_25min"
stdout = ""
sent_at = None
try:
with tempfile.TemporaryDirectory(prefix="codex-wait-probe-") as folder:
cwd = pathlib.Path(folder).resolve()
settings = configuration(server.server_port, minimum, default, maximum)
if steering:
command = with_config([binary, "app-server"], settings)
sent_at = steer_process(command, cwd, captured)
else:
command = [binary, "exec", "--ignore-user-config", "--ephemeral",
"--skip-git-repo-check", "--json", "-s", "read-only",
"-m", MODEL, "-C", str(cwd)]
with_config(command, settings)
command += ["Runtime probe. Follow the scripted local tool calls."]
proc = subprocess.run(command, stdin=subprocess.DEVNULL, capture_output=True,
text=True, encoding="utf-8", timeout=90)
require(proc.returncode == 0, proc.stderr[-1500:])
stdout = proc.stdout
finally:
server.shutdown()
server.server_close()
require(len(captured) == len(calls) + 1, f"Wrong request count: {len(captured)}")
require(all(r["body"].get("reasoning", {}).get("effort") == EFFORT for r in captured),
"A request did not use low reasoning")
inputs = [r["body"].get("input", []) for r in captured]
intervals = [round((b["at"] - a["at"]) * 1000, 1) for a, b in zip(captured, captured[1:])]
outputs = [item["output"] for item in inputs[-1] if item.get("type") == "function_call_output"]
require(len(outputs) == len(calls), "Missing tool outputs in the next logical input")
if steering:
expected = {"message": "Wait interrupted by new input.", "timed_out": False}
require(json.loads(outputs[-1]) == expected, f"Wrong steer output: {outputs[-1]}")
require(intervals[0] < 10000, "Steer did not wake within the test's 10-second bound")
elif name == "above_max":
require("at most 2000" in outputs[-1], f"Wrong max-limit output: {outputs[-1]}")
else:
require(all(json.loads(o)["timed_out"] is True for o in outputs), "Expected timeout")
for i, args in enumerate(calls):
expected_ms = max(args["timeout_ms"], minimum) if "timeout_ms" in args else default
require(intervals[i] >= expected_ms, f"Returned too early: {intervals[i]}ms")
if name == "raised_min_explicit_short":
require("clamped" in outputs[-1], "Missing minimum-clamp explanation")
# Do not hard-code the initial history length; it can differ by installation.
if not steering:
require(all(len(b) == len(a) + 2 for a, b in zip(inputs, inputs[1:])),
"History did not grow by one call and one result per loop")
synthetic_usage = None
if stdout:
events = [json.loads(line) for line in stdout.splitlines() if line.startswith("{")]
synthetic_usage = next(e["usage"] for e in events if e["type"] == "turn.completed")
for key, each in {"input_tokens": 1000, "cached_input_tokens": 900,
"output_tokens": 10, "reasoning_output_tokens": 2}.items():
require(synthetic_usage[key] == each * len(captured), f"Incorrect sum for {key}")
first_request = json.dumps(captured[0]["body"])
return {"name": name, "passed": True, "requests": len(captured),
"model": MODEL, "all_requests_effort": EFFORT,
"request_intervals_ms": intervals, "input_items": [len(i) for i in inputs],
"steer_to_next_request_ms": (round((captured[1]["at"] - sent_at) * 1000, 1)
if sent_at is not None else None),
"tool_outputs": outputs, "synthetic_usage": synthetic_usage,
"both_wait_guidances_present": ("wait calls longer than 60 seconds" in first_request
and "prefer longer waits (minutes)" in first_request)}
def live_case(binary):
print("REAL MODEL CALL: this consumes your account allowance; model=gpt-6-astra, effort=low.",
flush=True)
with tempfile.TemporaryDirectory(prefix="codex-live-wait-") as folder:
cwd = str(pathlib.Path(folder).resolve())
command = [binary, "exec", "--ignore-user-config", "--ephemeral", "--skip-git-repo-check",
"--json", "-s", "read-only", "-m", MODEL, "-C", cwd]
settings = configuration(0, 10, 100, 1000)
settings = {k: v for k, v in settings.items() if not k.startswith("model_provider")}
with_config(command, settings)
command += ["This is a bounded runtime timeout probe. Use low reasoning. Call "
"collaboration.wait_agent exactly twice, sequentially, each with timeout_ms=100. "
"There are intentionally no workers. After each timeout continue to the next step; "
"after the second timeout reply only DONE. Do not spawn agents, inspect files, "
"or use other tools."]
proc = subprocess.run(command, stdin=subprocess.DEVNULL, capture_output=True,
text=True, encoding="utf-8", timeout=180)
require(proc.returncode == 0, proc.stderr[-1500:])
events = [json.loads(line) for line in proc.stdout.splitlines() if line.startswith("{")]
items = [e["item"] for e in events if e["type"] == "item.completed"]
waits = [i for i in items if i.get("type") == "collab_tool_call" and i.get("tool") == "wait"]
replies = [i["text"].strip() for i in items if i.get("type") == "agent_message"]
require(len(waits) == 2 and all(i["status"] == "completed" for i in waits),
"Model did not complete exactly two waits; inspect your local run before inferring costs")
require(replies and replies[-1] == "DONE", f"Unexpected final answer: {replies}")
usage = next(e["usage"] for e in events if e["type"] == "turn.completed")
return {"name": "live_low", "passed": True, "model_requested": MODEL,
"effort_requested": EFFORT, "completed_waits": len(waits),
"usage_scope": "entire turn, including first request and final response", "usage": usage}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--codex", default="codex", help="Codex executable name or full path")
parser.add_argument("--output", default="wait-results.json", help="Sanitized JSON results file")
parser.add_argument("--quick", action="store_true", help="Skip the real 30-second mock wait")
parser.add_argument("--live", action="store_true", help="Run the paid real-model probe instead")
args = parser.parse_args()
binary = shutil.which(args.codex)
if not binary:
parser.error("Codex not found. Install CLI 0.153.4 or pass --codex /path/to/codex")
version = subprocess.check_output([binary, "--version"], text=True).strip()
print(f"Executable: {version}; requested model={MODEL}; reasoning={EFFORT}", flush=True)
if "0.153.4" not in version:
print("VERSION NOTE: this report tested 0.153.4; your behavior may differ.", flush=True)
cases = [
("short", [{"timeout_ms": 30}] * 3, 10, 30, 2000),
("default_only_explicit_short", [{"timeout_ms": 30}], 10, 500, 2000),
("raised_default_omitted", [{}], 10, 500, 2000),
("raised_min_explicit_short", [{"timeout_ms": 30}], 500, 500, 2000),
("above_max", [{"timeout_ms": 2001}], 10, 30, 2000),
("real_default_30s", [{}], 10000, 30000, 3600000),
("steer_25min", [{"timeout_ms": 1500000}], 1500000, 1500000, 3600000),
]
results = []
try:
if args.live:
results.append(live_case(binary))
print("PASS live_low", flush=True)
else:
for case in cases:
if args.quick and case[0] == "real_default_30s":
continue
print(f"RUN {case[0]}", flush=True)
result = mock_case(binary, case)
results.append(result)
print(f"PASS {case[0]}: requests={result['requests']}, "
f"intervals_ms={result['request_intervals_ms']}", flush=True)
finally:
pathlib.Path(args.output).write_text(json.dumps({"cli": version,
"mode": "live" if args.live else "mock", "results": results},
ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(f"{len(results)}/{len(results)} selected scenarios passed. Saved {args.output}")
if __name__ == "__main__":
main()
{
"pilot": false,
"upstream_model_requests": 0,
"version": "codex-cli 0.153.4",
"binary_sha256": "a30ec314bbd0e3721632234d07db7c99855db3b9f1e32dbe8c791947f07e7629",
"runs": [
{
"family": "input_event",
"interval_ms": 500,
"duration_ms": 2750,
"minimum_ms": 1,
"passed": true,
"parent_requests": 7,
"child_requests": 0,
"empty_timeouts": 5,
"event_to_parent_request_ms": 12.1,
"observed_event_delay_ms": 2753.02,
"parent_input_bytes_sum": 461370,
"last_parent_input_items": 20,
"requests": [
{
"role": "parent",
"input_bytes": 64812,
"input_items": 7,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 0.0
},
{
"role": "parent",
"input_bytes": 65170,
"input_items": 9,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 548.61
},
{
"role": "parent",
"input_bytes": 65528,
"input_items": 11,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 1088.88
},
{
"role": "parent",
"input_bytes": 65886,
"input_items": 13,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 1641.86
},
{
"role": "parent",
"input_bytes": 66244,
"input_items": 15,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 2188.94
},
{
"role": "parent",
"input_bytes": 66602,
"input_items": 17,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 2709.45
},
{
"role": "parent",
"input_bytes": 67128,
"input_items": 20,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": true,
"at_ms": 2766.41
}
]
},
{
"family": "input_event",
"interval_ms": 10000,
"duration_ms": 2750,
"minimum_ms": 1,
"passed": true,
"parent_requests": 2,
"child_requests": 0,
"empty_timeouts": 0,
"event_to_parent_request_ms": 54.91,
"observed_event_delay_ms": 2755.06,
"parent_input_bytes_sum": 130152,
"last_parent_input_items": 10,
"requests": [
{
"role": "parent",
"input_bytes": 64812,
"input_items": 7,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 0.0
},
{
"role": "parent",
"input_bytes": 65340,
"input_items": 10,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": true,
"at_ms": 2811.39
}
]
},
{
"family": "input_event",
"interval_ms": 10000,
"duration_ms": 2750,
"minimum_ms": 1,
"passed": true,
"parent_requests": 2,
"child_requests": 0,
"empty_timeouts": 0,
"event_to_parent_request_ms": 55.16,
"observed_event_delay_ms": 2755.07,
"parent_input_bytes_sum": 130152,
"last_parent_input_items": 10,
"requests": [
{
"role": "parent",
"input_bytes": 64812,
"input_items": 7,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 0.0
},
{
"role": "parent",
"input_bytes": 65340,
"input_items": 10,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": true,
"at_ms": 2812.09
}
]
},
{
"family": "input_event",
"interval_ms": 500,
"duration_ms": 2750,
"minimum_ms": 1,
"passed": true,
"parent_requests": 7,
"child_requests": 0,
"empty_timeouts": 5,
"event_to_parent_request_ms": 11.58,
"observed_event_delay_ms": 2755.05,
"parent_input_bytes_sum": 455287,
"last_parent_input_items": 20,
"requests": [
{
"role": "parent",
"input_bytes": 63943,
"input_items": 7,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 0.0
},
{
"role": "parent",
"input_bytes": 64301,
"input_items": 9,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 528.68
},
{
"role": "parent",
"input_bytes": 64659,
"input_items": 11,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 1067.95
},
{
"role": "parent",
"input_bytes": 65017,
"input_items": 13,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 1579.59
},
{
"role": "parent",
"input_bytes": 65375,
"input_items": 15,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 2094.58
},
{
"role": "parent",
"input_bytes": 65733,
"input_items": 17,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 2603.95
},
{
"role": "parent",
"input_bytes": 66259,
"input_items": 20,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": true,
"at_ms": 2767.61
}
]
},
{
"family": "input_event",
"interval_ms": 500,
"duration_ms": 2750,
"minimum_ms": 1,
"passed": true,
"parent_requests": 7,
"child_requests": 0,
"empty_timeouts": 5,
"event_to_parent_request_ms": 14.28,
"observed_event_delay_ms": 2755.05,
"parent_input_bytes_sum": 455287,
"last_parent_input_items": 20,
"requests": [
{
"role": "parent",
"input_bytes": 63943,
"input_items": 7,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 0.0
},
{
"role": "parent",
"input_bytes": 64301,
"input_items": 9,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 550.6
},
{
"role": "parent",
"input_bytes": 64659,
"input_items": 11,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 1098.69
},
{
"role": "parent",
"input_bytes": 65017,
"input_items": 13,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 1651.99
},
{
"role": "parent",
"input_bytes": 65375,
"input_items": 15,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 2174.88
},
{
"role": "parent",
"input_bytes": 65733,
"input_items": 17,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 2694.19
},
{
"role": "parent",
"input_bytes": 66259,
"input_items": 20,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": true,
"at_ms": 2770.08
}
]
},
{
"family": "input_event",
"interval_ms": 10000,
"duration_ms": 2750,
"minimum_ms": 1,
"passed": true,
"parent_requests": 2,
"child_requests": 0,
"empty_timeouts": 0,
"event_to_parent_request_ms": 51.55,
"observed_event_delay_ms": 2755.06,
"parent_input_bytes_sum": 130152,
"last_parent_input_items": 10,
"requests": [
{
"role": "parent",
"input_bytes": 64812,
"input_items": 7,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 0.0
},
{
"role": "parent",
"input_bytes": 65340,
"input_items": 10,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": true,
"at_ms": 2808.21
}
]
},
{
"family": "wrapper",
"interval_ms": 500,
"duration_ms": 2750,
"minimum_ms": 1,
"passed": true,
"parent_requests": 7,
"child_requests": 0,
"empty_timeouts": 0,
"event_to_parent_request_ms": null,
"observed_event_delay_ms": null,
"parent_input_bytes_sum": 412901,
"last_parent_input_items": 19,
"requests": [
{
"role": "parent",
"input_bytes": 57704,
"input_items": 7,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 0.0
},
{
"role": "parent",
"input_bytes": 58177,
"input_items": 9,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 585.61
},
{
"role": "parent",
"input_bytes": 58581,
"input_items": 11,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 1132.5
},
{
"role": "parent",
"input_bytes": 58985,
"input_items": 13,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 1662.92
},
{
"role": "parent",
"input_bytes": 59389,
"input_items": 15,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 2198.68
},
{
"role": "parent",
"input_bytes": 59793,
"input_items": 17,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 2714.05
},
{
"role": "parent",
"input_bytes": 60272,
"input_items": 19,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 2809.23
}
]
},
{
"family": "wrapper",
"interval_ms": 10000,
"duration_ms": 2750,
"minimum_ms": 1,
"passed": true,
"parent_requests": 2,
"child_requests": 0,
"empty_timeouts": 0,
"event_to_parent_request_ms": null,
"observed_event_delay_ms": null,
"parent_input_bytes_sum": 115958,
"last_parent_input_items": 9,
"requests": [
{
"role": "parent",
"input_bytes": 57704,
"input_items": 7,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 0.0
},
{
"role": "parent",
"input_bytes": 58254,
"input_items": 9,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 2834.97
}
]
},
{
"family": "wrapper",
"interval_ms": 10000,
"duration_ms": 2750,
"minimum_ms": 1,
"passed": true,
"parent_requests": 2,
"child_requests": 0,
"empty_timeouts": 0,
"event_to_parent_request_ms": null,
"observed_event_delay_ms": null,
"parent_input_bytes_sum": 115958,
"last_parent_input_items": 9,
"requests": [
{
"role": "parent",
"input_bytes": 57704,
"input_items": 7,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 0.0
},
{
"role": "parent",
"input_bytes": 58254,
"input_items": 9,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 2856.72
}
]
},
{
"family": "wrapper",
"interval_ms": 500,
"duration_ms": 2750,
"minimum_ms": 1,
"passed": true,
"parent_requests": 7,
"child_requests": 0,
"empty_timeouts": 0,
"event_to_parent_request_ms": null,
"observed_event_delay_ms": null,
"parent_input_bytes_sum": 412901,
"last_parent_input_items": 19,
"requests": [
{
"role": "parent",
"input_bytes": 57704,
"input_items": 7,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 0.0
},
{
"role": "parent",
"input_bytes": 58177,
"input_items": 9,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 582.04
},
{
"role": "parent",
"input_bytes": 58581,
"input_items": 11,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 1117.31
},
{
"role": "parent",
"input_bytes": 58985,
"input_items": 13,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 1643.67
},
{
"role": "parent",
"input_bytes": 59389,
"input_items": 15,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 2169.22
},
{
"role": "parent",
"input_bytes": 59793,
"input_items": 17,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 2678.22
},
{
"role": "parent",
"input_bytes": 60272,
"input_items": 19,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 2803.66
}
]
},
{
"family": "wrapper",
"interval_ms": 500,
"duration_ms": 2750,
"minimum_ms": 1,
"passed": true,
"parent_requests": 7,
"child_requests": 0,
"empty_timeouts": 0,
"event_to_parent_request_ms": null,
"observed_event_delay_ms": null,
"parent_input_bytes_sum": 412901,
"last_parent_input_items": 19,
"requests": [
{
"role": "parent",
"input_bytes": 57704,
"input_items": 7,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 0.0
},
{
"role": "parent",
"input_bytes": 58177,
"input_items": 9,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 588.36
},
{
"role": "parent",
"input_bytes": 58581,
"input_items": 11,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 1125.63
},
{
"role": "parent",
"input_bytes": 58985,
"input_items": 13,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 1665.16
},
{
"role": "parent",
"input_bytes": 59389,
"input_items": 15,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 2201.47
},
{
"role": "parent",
"input_bytes": 59793,
"input_items": 17,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 2749.32
},
{
"role": "parent",
"input_bytes": 60272,
"input_items": 19,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 2801.9
}
]
},
{
"family": "wrapper",
"interval_ms": 10000,
"duration_ms": 2750,
"minimum_ms": 1,
"passed": true,
"parent_requests": 2,
"child_requests": 0,
"empty_timeouts": 0,
"event_to_parent_request_ms": null,
"observed_event_delay_ms": null,
"parent_input_bytes_sum": 115958,
"last_parent_input_items": 9,
"requests": [
{
"role": "parent",
"input_bytes": 57704,
"input_items": 7,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 0.0
},
{
"role": "parent",
"input_bytes": 58254,
"input_items": 9,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 2830.36
}
]
},
{
"family": "input_event",
"interval_ms": 30000,
"duration_ms": 65000,
"minimum_ms": 10000,
"passed": true,
"parent_requests": 4,
"child_requests": 0,
"empty_timeouts": 2,
"event_to_parent_request_ms": 30.72,
"observed_event_delay_ms": 65010.52,
"parent_input_bytes_sum": 261592,
"last_parent_input_items": 14,
"requests": [
{
"role": "parent",
"input_bytes": 64816,
"input_items": 7,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 0.0
},
{
"role": "parent",
"input_bytes": 65176,
"input_items": 9,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 30055.74
},
{
"role": "parent",
"input_bytes": 65536,
"input_items": 11,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 60081.16
},
{
"role": "parent",
"input_bytes": 66064,
"input_items": 14,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": true,
"at_ms": 65042.86
}
]
},
{
"family": "input_event",
"interval_ms": 600000,
"duration_ms": 65000,
"minimum_ms": 10000,
"passed": true,
"parent_requests": 2,
"child_requests": 0,
"empty_timeouts": 0,
"event_to_parent_request_ms": 49.31,
"observed_event_delay_ms": 65005.13,
"parent_input_bytes_sum": 130161,
"last_parent_input_items": 10,
"requests": [
{
"role": "parent",
"input_bytes": 64816,
"input_items": 7,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": false,
"at_ms": 0.0
},
{
"role": "parent",
"input_bytes": 65345,
"input_items": 10,
"model": "gpt-6-astra",
"effort": "low",
"completion_visible": false,
"input_event_visible": true,
"at_ms": 65055.96
}
]
},
{
"name": "steer_25min",
"passed": true,
"requests": 2,
"model": "gpt-6-astra",
"all_requests_effort": "low",
"request_intervals_ms": [
246.9
],
"input_items": [
7,
10
],
"steer_to_next_request_ms": 40.9,
"tool_outputs": [
"{\"message\":\"Wait interrupted by new input.\",\"timed_out\":false}"
],
"both_wait_guidances_present": true,
"family": "steer"
},
{
"name": "steer_25min",
"passed": true,
"requests": 2,
"model": "gpt-6-astra",
"all_requests_effort": "low",
"request_intervals_ms": [
238.4
],
"input_items": [
7,
10
],
"steer_to_next_request_ms": 32.5,
"tool_outputs": [
"{\"message\":\"Wait interrupted by new input.\",\"timed_out\":false}"
],
"both_wait_guidances_present": true,
"family": "steer"
},
{
"name": "steer_25min",
"passed": true,
"requests": 2,
"model": "gpt-6-astra",
"all_requests_effort": "low",
"request_intervals_ms": [
241.8
],
"input_items": [
7,
10
],
"steer_to_next_request_ms": 35.3,
"tool_outputs": [
"{\"message\":\"Wait interrupted by new input.\",\"timed_out\":false}"
],
"both_wait_guidances_present": true,
"family": "steer"
}
],
"comparisons": [
{
"family": "input_event",
"pair": 1,
"passed": true,
"short_requests": 7,
"long_requests": 2
},
{
"family": "input_event",
"pair": 2,
"passed": true,
"short_requests": 7,
"long_requests": 2
},
{
"family": "input_event",
"pair": 3,
"passed": true,
"short_requests": 7,
"long_requests": 2
},
{
"family": "wrapper",
"pair": 1,
"passed": true,
"short_requests": 7,
"long_requests": 2
},
{
"family": "wrapper",
"pair": 2,
"passed": true,
"short_requests": 7,
"long_requests": 2
},
{
"family": "wrapper",
"pair": 3,
"passed": true,
"short_requests": 7,
"long_requests": 2
},
{
"family": "real_scale",
"passed": true,
"short_requests": 4,
"long_requests": 2
}
]
}

Codex 等待:受控实验结果

结论:在真实 Codex 运行时、本地脚本模型的对照中,缩短等待间隔会造成额外模型请求;外层执行工具的短 yield 也能独立产生同类开销。长等待仍可被用户输入提前结束。

执行环境:macOS;codex-cli 0.153.4;全部模型请求配置为 gpt-6-astra / low,但由 localhost 模拟服务响应。本次测试没有新增真实模型推理调用;当前聊天的分析和工具调度本身仍会用 token。

先看一个真实时间尺度的例子

让 Codex 等待,第 65 秒固定发来同一条输入。两组唯一主动改变的因素是等待超时:

条件 等待上限 全部父请求 空超时 输入发出后多久出现下一请求
短等待 30 秒 4 2 30.72 ms
长等待 10 分钟 2 0 49.31 ms

两组输入都在约第 65 秒发出;长等待没有硬等到第 10 分钟。短组第 30、60 秒的空超时分别多触发一次请求。这里的“减少请求”不能直接换算成“节省相同比例的 token、钱或额度”。请求数包含最初发起等待的请求和最后接收事件的请求。

全部正式对照

实验 重复数 短等待父请求 长等待父请求 判据
固定 2.75 秒注入输入;0.5 秒 vs 10 秒等待 3 对 7 2 全部通过
固定 2.75 秒异步任务;外层 yield 0.5 秒 vs 10 秒 3 对 7 2 全部通过
固定 65 秒注入输入;30 秒 vs 10 分钟等待 1 对 4 2 通过
25 分钟等待中,约 0.2 秒后输入 3 次 每次 2 请求 全部通过

共 17 次正式运行、7 个成对检查;短尺度按 AB / BA / AB 交替。没有靠反复重跑直到通过来筛选正式结果。

短尺度输入延迟:短等待 11.58–14.28 ms;长等待 51.55–55.16 ms。三次 25 分钟等待的输入延迟为 32.5–40.9 ms。均满足预先设定的 1000ms 判据。我们证明的是此次运行及时唤醒,并不宣称两种等待的延迟完全相等或给出生产环境延迟保证。

为什么这能说明问题,又不能说明所有问题

  • 运行等待、超时、工具续接的是安装的 Codex 二进制;请求次数由本地 HTTP 服务实际计数,不是拿公式算出来的。
  • 模拟服务把策略固定为“事件没来就继续等,来了就结束”,因此能隔离等待间隔的因果作用;它没有测量真实模型是否会选择这套策略。
  • 外层实验实际执行相同的 JavaScript 定时异步任务;短 yield 多次返回运行状态,长 yield 一次等到结果。没有声称这一版本允许 wait_agent 嵌入 exec,也没有把这两层等同。
  • 原始输入字节数仅作诊断。短尺度首请求长度实际出现 [63943, 64812] 字节:运行时生成的上下文并非逐字节相同。模拟策略不读取这些变化,故不影响本实验的请求数因果比较;但不据此作精确输入量/费用比较。
  • 无真实 token/缓存测量,模拟 usage 不代表消耗。未用账号额度条做小样本费用推断,未做跨模型成本外推。

未完成的部分与调试记录

最初计划让真实子助手在固定时刻完成,但 pilot 1–6 中虽得到 spawn 成功返回,未观察到子助手模型请求,无法形成有效完成对照。尝试请求分类检查、诊断日志和非 ephemeral 模式后仍未解决,原因尚未定位。正式运行前已在 PROTOCOL.md 中保留原方案并记录修订,改用实际 app-server 用户输入事件。后来 input_event 与 wrapper 调试通过,才启动正式运行。

因此,子助手“完成、失败、请求批准”三种通知的端到端可靠性,这次没有新增验证。 此前的源码函数测试支持邮箱事件会提前结束等待,但不能用它代替完整客户端链路测试。此前测试与本次正式数据不混算。

必须的结论和后续优先级

现在可以确定:在无新信息时反复超时,会产生可避免的模型请求;仅关注 wait_agent 的参数可能漏掉通用执行工具的外层 yield。用户输入唤醒不需要以每 30 秒调用模型作为前提。

这足以支持“减少安静等待期间的无效模型重入”这一优化方向,尚不足以支持“额度节省 X%”或“这是 Astra 耗费额度的主要原因”。

下一步若要真正提交运行时修复,优先打通子助手通知的完整集成测试,覆盖完成、失败、需要批准、用户输入、通知恰好发生在超时边界。若目标是解释真实账号额度,再做有独立费用计量的 Astra low 配对测试;本轮按停止规则没有为此消耗真实模型额度。

复现

需要 Python 3.10+ 与 Codex CLI;仅在上述 macOS 版本验证。脚本默认使用 localhost 模型服务;不需要 API key。app-server 会读取部分本机配置,所以不把整个进程称作完全离线或完全隔离。

python3 controlled.py --codex /absolute/path/to/codex --output results.json
python3 analyze.py

controlled.pyreproduce.py 必须放在同一目录。完整运行包含两个 65 秒等待。脚本会持续写入结果;失败即停止。所有原始数字见 results.json;分析脚本先校验 17 次运行与 7 个对照,再生成本文。

第一轮源码分析与本轮补充均记录在同一个公开 Gist。从 Gist 首页开始可看到通俗讲解、下载步骤和所有实验文件。

{
"reproduce.py": "da59278ca321c45d9fbb29e410e86086fd587aac68689828b662c736a2890c50",
"results.json": "a2ea8bd278ad0e5304da4bb2d932de370a0c4a397e06cc77e956631d80deb302",
"controlled.py": "9b8d76da59112067038a2712712382a24678351fbc18cc259d9c42862ef2b762",
"RESULTS.md": "1fb5e7157e8c1052597db6c9f36421f15493ca62b862594e724111808f3785f8",
"pilot-summary.json": "19f679997295c3f4bf28645508071ee89a4fdef51ac29d6bbfba2993cc46729a",
"analyze.py": "98581f3149f81e3daa8da596a5a9a2a243f9415b61b97e854be968c0961c0c42",
"PROTOCOL.md": "ac9d514c7caf9dc15e666e5ec96f8eecd7c506e826fc3cb1e51cfe6744eb9c14"
}
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tokio"
version = "1.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
dependencies = [
"pin-project-lite",
"tokio-macros",
]
[[package]]
name = "tokio-macros"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "wait-source-probe"
version = "0.1.0"
dependencies = [
"tokio",
]
#!/usr/bin/env python3
"""Build a small test crate from a hash-verified, unchanged Codex wait function.
Python 3.10+. Default downloads one public, pinned source file (no credentials).
--checkout /path/to/codex reads that file locally instead. --run requires
Rust 1.95.0, just and cargo-nextest. No model calls or account allowance.
"""
import argparse
import hashlib
import json
import pathlib
import shutil
import subprocess
import urllib.request
COMMIT = "c7f81afc191d74ef6e96a5add25eee05662d2215"
RELATIVE = "codex-rs/core/src/tools/handlers/multi_agents_v2/wait.rs"
URL = f"https://raw.githubusercontent.com/openai/codex/{COMMIT}/{RELATIVE}"
FILE_SHA256 = "6b4c6cc9d86c915e5a591ad5103a65a28b62874c66d8aa81302e4e584f504dcb"
EXTRACT_SHA256 = "4c283d4e77837e9a369825de0c16453c6e301cda3caf0e4590d6b1ef67d67697"
MARKER = "#[derive(Clone, Copy, Debug, Eq, PartialEq)]"
PRELUDE = """// Extracted Codex code: Copyright 2025 OpenAI, Apache-2.0.
// Test scaffolding added for the wait-agent reproduction. See NOTICE.txt.
#![allow(dead_code)]
use tokio::time::{Instant, timeout_at};
#[derive(Clone, Copy)]
enum InputQueueActivity { Mailbox, Steer }
"""
TESTS = r"""#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[tokio::test(start_paused = true)]
async fn quiet_wait_has_no_early_return() {
let (_tx, mut rx) = tokio::sync::watch::channel(InputQueueActivity::Mailbox);
let start = Instant::now();
let task = tokio::spawn(async move {
wait_for_activity(&mut rx, None, start + Duration::from_secs(1500)).await
});
tokio::task::yield_now().await;
tokio::time::advance(Duration::from_secs(30)).await;
assert!(!task.is_finished());
tokio::time::advance(Duration::from_secs(1471)).await;
assert_eq!(task.await.unwrap(), WaitOutcome::TimedOut);
}
#[tokio::test(start_paused = true)]
async fn mailbox_wakes_25_minute_wait_at_one_second() {
let (tx, mut rx) = tokio::sync::watch::channel(InputQueueActivity::Mailbox);
let start = Instant::now();
let task = tokio::spawn(async move {
wait_for_activity(&mut rx, None, start + Duration::from_secs(1500)).await
});
tokio::task::yield_now().await;
tokio::time::advance(Duration::from_secs(1)).await;
tx.send_replace(InputQueueActivity::Mailbox);
assert_eq!(task.await.unwrap(), WaitOutcome::MailboxActivity);
assert!(start.elapsed() < Duration::from_secs(2));
}
#[tokio::test(start_paused = true)]
async fn user_input_wakes_25_minute_wait_at_one_second() {
let (tx, mut rx) = tokio::sync::watch::channel(InputQueueActivity::Mailbox);
let start = Instant::now();
let task = tokio::spawn(async move {
wait_for_activity(&mut rx, None, start + Duration::from_secs(1500)).await
});
tokio::task::yield_now().await;
tokio::time::advance(Duration::from_secs(1)).await;
tx.send_replace(InputQueueActivity::Steer);
assert_eq!(task.await.unwrap(), WaitOutcome::Steered);
assert!(start.elapsed() < Duration::from_secs(2));
}
#[tokio::test(start_paused = true)]
async fn pending_mailbox_does_not_wait() {
let (_tx, mut rx) = tokio::sync::watch::channel(InputQueueActivity::Mailbox);
let start = Instant::now();
let result = wait_for_activity(&mut rx, Some(InputQueueActivity::Mailbox),
start + Duration::from_secs(1500)).await;
assert_eq!(result, WaitOutcome::MailboxActivity);
assert_eq!(start.elapsed(), Duration::ZERO);
}
#[tokio::test(start_paused = true)]
async fn pending_steer_does_not_wait() {
let (_tx, mut rx) = tokio::sync::watch::channel(InputQueueActivity::Mailbox);
let start = Instant::now();
let result = wait_for_activity(&mut rx, Some(InputQueueActivity::Steer),
start + Duration::from_secs(1500)).await;
assert_eq!(result, WaitOutcome::Steered);
assert_eq!(start.elapsed(), Duration::ZERO);
}
#[tokio::test(start_paused = true)]
async fn channel_closure_is_reported_as_timeout() {
let (tx, mut rx) = tokio::sync::watch::channel(InputQueueActivity::Mailbox);
drop(tx);
let start = Instant::now();
assert_eq!(wait_for_activity(&mut rx, None,
start + Duration::from_secs(1500)).await, WaitOutcome::TimedOut);
assert_eq!(start.elapsed(), Duration::ZERO);
}
}
"""
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--checkout", type=pathlib.Path, help="Optional local Codex checkout")
parser.add_argument("--output", type=pathlib.Path, default=pathlib.Path("source-probe"))
parser.add_argument("--run", action="store_true", help="Run six tests using just / nextest")
args = parser.parse_args()
if args.checkout:
data = (args.checkout / RELATIVE).read_bytes()
else:
print("Downloading the pinned public wait.rs source...", flush=True)
with urllib.request.urlopen(URL, timeout=30) as response:
data = response.read()
if hashlib.sha256(data).hexdigest() != FILE_SHA256:
raise SystemExit("Source hash mismatch. Use the documented commit; no test crate generated.")
source = data.decode("utf-8")
extracted = source[source.index(MARKER):]
if hashlib.sha256(extracted.encode()).hexdigest() != EXTRACT_SHA256:
raise SystemExit("Extracted function hash mismatch; no test crate generated.")
root = args.output.resolve()
(root / "src").mkdir(parents=True, exist_ok=True)
(root / "src/lib.rs").write_text(PRELUDE + extracted + "\n" + TESTS, encoding="utf-8")
(root / "Cargo.toml").write_text('''[package]
name = "wait-source-probe"
version = "0.1.0"
edition = "2024"
[dependencies]
tokio = { version = "=1.53.1", features = ["sync", "time", "macros", "rt", "test-util"] }
''', encoding="utf-8")
(root / "rust-toolchain.toml").write_text('[toolchain]\nchannel = "1.95.0"\n', encoding="utf-8")
(root / "justfile").write_text("test:\n cargo nextest run --locked --no-fail-fast\n", encoding="utf-8")
lock = pathlib.Path(__file__).with_name("source-probe.Cargo.lock")
if not lock.is_file():
raise SystemExit("Missing source-probe.Cargo.lock: download all Gist files, not this script alone.")
shutil.copyfile(lock, root / "Cargo.lock")
(root / "extraction.json").write_text(json.dumps({"source_url": URL,
"source_commit": COMMIT, "file_sha256": FILE_SHA256,
"extracted_sha256": EXTRACT_SHA256, "unchanged_start_line": 180}, indent=2), encoding="utf-8")
print("Verified source and extraction SHA-256. Generated", root, flush=True)
if args.run:
if not shutil.which("just"):
raise SystemExit("Install just and cargo-nextest, then rerun with --run.")
subprocess.run(["just", "--justfile", str(root / "justfile"), "test"], check=True)
else:
print("Next: just --justfile", root / "justfile", "test")
if __name__ == "__main__":
main()
@Dentonshaw

Copy link
Copy Markdown

如果赚了大钱,你会干什么? 别想了—你不是如果。 —— Dentonshaw

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment