第 40 讲:Rollout 录制压缩与 Trace 回放

Codex 源码解析 · 第 40 讲

Codex 源码解析系列 第 40 讲:Rollout 录制压缩与 Trace 回放 基于 OpenAI Codex 源码 · 2026-11-03 💡 本讲一句话: Codex 把每场对话当成"黑匣子"来录——热路径只往队列里塞事件、后台任务负责落盘、7 天没动过的冷文件自动 zstd 压缩、trace bundle 还能确定性重放成结构化图。读完你能带走三样东西:一个"通道 + 单写者 + 恢复模式"的可靠写入范式、一套"三重状态校验 + 原子发布"的冷数据压缩协议,以及 raw event log / reduced graph 分离的可回放设计。 一、Rollout:Codex 的"黑匣子" 前面几讲我们反复提到 session 的历史、resume、fork,但一直没回答一个底层问题: 这些对话到底存在哪、怎么存 。答案是 rollout——每个会话对应一个 JSONL 文件( rollout-2026-11-03T08-12-33.456Z-<conversation_id>.jsonl ),第一行是 session meta(cwd、git 信息、模型 provider、agent 角色……),之后每发生一件事就追加一行。它既是 resume 的数据源,也是 thread store / state DB 的"真相之源"。 本讲按写入路径 → 压缩 worker → 读取路径 → trace bundle 四条线,把 codex-rs/rollout (recorder.rs 2227 行、compression.rs 1121 行)和 codex-rs/rollout-trace (约 9K LOC)的关键代码逐行拆开。 二、RolloutRecorder:通道 + 单写者任务 先看核心数据结构。注意 RolloutRecorder 本身 不持有任何文件句柄 ——它只是一个 channel sender: 📄 codex-rs/rollout/src/recorder.rs (第 86-90、126-141 行) pub struct RolloutRecorder { // recorder = 通道 sender + 写任务句柄 + 文件路径,三者都可廉价克隆 tx: Sender<RolloutCmd>, // 异步命令通道:热路径代码只"入队",从不直接碰磁盘 writer_task: Arc<RolloutWriterTask>, // 后台写任务的共享可观测状态(任务句柄 + 终态失败) pub(crate) rollout_path: PathBuf, // 本会话 JSONL 文件的规范路径 } enum RolloutCmd { // 可以发给写者的全部命令,每条都带一次性应答通道 AddItems(Vec<RolloutItem>), // 追加一批对话条目(热路径最高频的命令) Persist { ack: oneshot::Sender<std::io::Result<()>> }, // 强制物化文件 + 写盘,用一次性通道回结果 Flush { ack: oneshot::Sender<std::io::Result<()>> }, // 冲刷所有缓冲写入并等待提交完成 Shutdown { ack: oneshot::Sender<std::io::Result<()>> }, // 优雅关闭:写完剩余数据后退出循环 Discard { ack: oneshot::Sender<()> }, // 丢弃:不写盘直接终止(如会话被取消) } 为什么这样设计: 把"产生事件"和"落盘"彻底解耦。agent loop 每轮都会产生大量 item(消息、工具调用、审批结果),如果同步写盘,一次磁盘抖动就会卡住整个推理循环;而通过 channel 入队后,热路径的开销只是内存拷贝 + 一次 send。同时所有写入收敛到 单个后台任务 串行处理——天然保序,不需要任何锁来协调"谁先写哪一行"。 五条命令的语义对照: 命令 行为 应答语义 AddItems 入内存缓冲,文件已存在则顺手冲刷 无 ack(fire-and-forget) Persist 强制创建文件 + 写全部缓冲(幂等) io::Res…

阅读全文:Codex 源码解析 第 40 讲