乐于分享
好东西不私藏

OpenAI Codex 源码研究(五):rollout——会话如何以 JSONL 事件流持久化

OpenAI Codex 源码研究(五):rollout——会话如何以 JSONL 事件流持久化

编码代理迟早要回答三个问题:进程重启后如何恢复会话?出现问题后如何检查过程?子代理从哪一份历史开始?

Codex 的基础答案是 rollout:将规范化的会话项目持续写入 JSONL。它采用事件流式持久化,但本文不把它等同于完整的通用事件溯源框架。

磁盘上的结构

RolloutRecorder 的源码注释说明,rollout 用于回放或检查会话。文件位于日期分层目录中:

~/.codex/sessions/YYYY/MM/DD/rollout-<时间戳>-<thread_id>.jsonl 

每行记录一个 JSON 项目。写入侧使用容量为 256 的 Tokio mpsc 通道和后台任务:调用方可能在通道满时异步等待,但不会在调用线程执行阻塞式磁盘 I/O。

记录器还暴露 persistflush 和 shutdown 屏障。写入失败时,未写入的后缀会保留在内存队列中,后续屏障可以重新打开文件并重试。这比简单的“异步写完就不管”更可靠。

从 rollout 派生的三项能力

恢复

resume_thread_from_rollout 从 rollout 路径读取初始历史,再创建恢复后的线程。另一个入口 resume_thread_with_history 接受已经构造好的历史。两者职责不同,不宜都概括为“直接从文件恢复”。

子代理分叉

spawn_subagent 在读取分叉快照前依次执行:

fork_source.ensure_rollout_materialized().await; fork_source.flush_rollout().await?; 

随后通过 read_thread(..., include_history = true) 读取已持久化历史,并用 ForkSnapshot::Interrupted 构造子线程的初始历史。这里的“Interrupted”描述分叉历史的边界语义,不表示父线程一定被永久标记或停止。

关键价值在于:子代理拿到的是一个明确、可重现的持久化分叉点,而不是仍在变化的内存引用。

回滚留痕

普通文件名以 thread ID 结尾;thread/revert 可以为同一 thread 创建带独立 rollout ID 的新文件。原 rollout 保留,新状态写入新的不可变文件,因此回滚不需要覆盖旧记录。

为什么不能只存消息数组

消息数组通常缺少三类信息:

  1. 工具执行相关的环境与上下文更新;
  2. 审批、工具调用和中断等过程事件;
  3. 分叉、回滚和恢复所需的明确边界。

rollout 把这些内容放进同一套持久化通道,为恢复、分析和审计提供共同输入。不过,“记录更多”也意味着更高的数据治理要求:提示词、工具输出和路径信息可能包含敏感内容,接入审计系统前应配置访问控制、脱敏和保留期限。

对自建团队的启示

  1. 持久化的不应只有消息,还应覆盖影响模型判断的关键环境和控制事件;
  2. 后台写入必须配屏障,在分叉、退出等关键点确认数据已落盘;
  3. 回滚和分叉应保留原历史,新状态通过新分支或新流表达;
  4. 把 rollout 当敏感数据管理,可审计不等于可以无限期明文保存。

本文基于 OpenAI Codex commit 343074d 的静态源码分析。文中的架构判断不等同于对安全性、性能或生产成熟度的背书。