雨天小六

读懂 Codex(8.5):延迟物化、SessionMeta 和第一批写入

· 更新于 2026-08-03 · 专栏:读懂 Codex

#Codex#Agent Runtime#持久化#Rollout#软件架构

新 Thread 可以先存在于内存而没有 Rollout 文件,但一旦物化,第一条规范记录必须是预先构造好的 SessionMeta,后续读取以它确定身份和 History Mode。

本节解决的不是“把对象存一下”这种抽象问题,而是把问题限定在:解释 Create 路径从无文件到首批记录,不讨论 Resume 打开既有文件。输入:RolloutRecorderParams::Create、SessionMeta 和首批业务项;状态所有者:RolloutWriterState 的 deferred writer 状态;成功结果:以 canonical SessionMeta 开头的 JSONL Rollout。先把这些边界钉住,后面的顺序、失败和恢复才不会混成一句“持久化失败后重试”。

状态边界

问题本节答案
输入RolloutRecorderParams::Create、SessionMeta 和首批业务项
状态所有者RolloutWriterState 的 deferred writer 状态
成功产物以 canonical SessionMeta 开头的 JSONL Rollout
研究范围解释 Create 路径从无文件到首批记录,不讨论 Resume 打开既有文件

正常路径:先看顺序点

延迟物化、SessionMeta 和第一批写入的正常路径时序图,展示创建时计算路径与 SessionMeta、不立即创建空文件、首批 AddItems 进入 pending、Persist/Flush 触发 materialize、先写 SessionMeta 再写业务项
图 8.5-1:创建时计算路径与 SessionMeta → 不立即创建空文件 → 首批 AddItems 进入 pending → Persist/Flush 触发 materialize → 先写 SessionMeta 再写业务项。这张图标出本节的实际顺序点与状态所有者。

这条路径可以压缩成五步:

  1. 创建时计算路径与 SessionMeta。
  2. 不立即创建空文件。
  3. 首批 AddItems 进入 pending。
  4. Persist/Flush 触发 materialize。
  5. 先写 SessionMeta 再写业务项。

图中的箭头不是“可能调用”的依赖图,而是源码中决定可见性和所有权转移的先后关系。前一步没有确认时,后一步不能替它作出更强的成功承诺。

源码机制拆解

创建与物化分离

Recorder::new(Create) 保存目标路径和元数据,却让 writer 为 None。这样一次很快失败、没有产生任何规范内容的 Thread 不必留下空文件。

SessionMeta 是物理日志头

它携带 thread/session identity、cwd、source、provider、base instructions、dynamic tools、history mode、history_base 与多 Agent 字段。Loader 把第一条 SessionMeta 当作 canonical 元数据。

Persist 可以主动制造存在性

即使 pending 没有业务记录,persist 也要求创建文件并写入 SessionMeta。需要让 Thread 立即可被列表、Fork 或外部引用发现时,调用方必须显式跨过这条边界。

首批业务项不能抢在 Meta 前面

Writer 在第一次打开 Create 状态时把预存 SessionMeta 放到序列最前。Paginated ordinal 的起点也以该头记录和 history_base 为依据。

后续 SessionMeta 不改写头身份

复制 Fork 可能带入旧 SessionMeta 行,Loader 仍只把第一条作为当前物理 Rollout 的 canonical meta;后续行作为历史内容保留,不能覆盖当前 Thread 身份。

Python 风格伪代码

下面的伪代码只保留设计职责、状态和失败顺序;它不逐行翻译 Rust,也不借 Python 语法虚构源码中不存在的事务:

class DeferredWriter:
    def __init__(self, path, session_meta):
        self.path = path
        self.session_meta = session_meta
        self.file = None
        self.pending = []

    async def add(self, items):
        self.pending.extend(items)
        if self.file is not None:
            await self.write_pending()

    async def materialize(self):
        if self.file is None:
            self.file = await create_new_jsonl(self.path)
            await self.write_line(self.session_meta)
        await self.write_pending()

    async def persist(self):
        await self.materialize()
        await self.file.flush()

阅读时要特别看三处:哪个对象拥有可变状态,哪一个 await 是可观察屏障,以及失败后保留的是已提交前缀、未提交后缀,还是完全独立的外部副作用。

失败、取消与恢复

延迟物化、SessionMeta 和第一批写入的失败路径图,区分创建后从未物化、首行不是 SessionMeta、SessionMeta 写成一半、复制来的旧 Meta 覆盖当前身份
图 8.5-2:同一操作在不同故障点留下的状态不同,恢复责任必须回到拥有该状态的组件。
故障点已留下的状态可观察结果恢复责任
创建后从未物化只有内存 Thread进程退出后不可恢复需要耐久身份时显式 persist
首行不是 SessionMeta缺少 canonical identity/modeLoader 拒绝或无法解释后续行物化逻辑独占首行写入
SessionMeta 写成一半尾部 JSON 不完整加载跳过坏行后找不到 meta创建失败,不把文件当有效 Thread
复制来的旧 Meta 覆盖当前身份一个文件出现多条 meta路径和 ThreadId 错配只认首条 canonical meta

这里没有统一的“回滚一切”。内存状态、日志行、SQLite 投影、父子拓扑和工具造成的文件/网络变化分别有自己的提交点。恢复代码只能根据已经存在的权威证据继续,不能用较弱的投影替较强的事实背书。

必须保持的不变量

  • 有效 Rollout 的第一条可解析记录是当前 SessionMeta
  • 未物化 Thread 不能声称已经耐久
  • 业务项的 ordinal 必须接在当前历史基准之后
  • Create 不应留下无意义空日志

这些不变量比“最终能 Resume”更严格:正常路径要成立,Writer 竞争、任务取消、坏尾行、投影落后和旧格式兼容时也必须成立。

设计取舍

延迟物化减少短命 Thread 和探测操作的磁盘噪声,但上层必须理解“对象已创建”与“可从磁盘发现”是两个阶段。

源码可以直接证明字段、分支、调用顺序和测试期望;“为什么这样设计”的表述是基于这些事实作出的工程归纳,不把它包装成未公开的产品承诺。

Mini Codex 复刻

创建 Thread 时只分配 ID 和 Meta;第一次 append 或显式 persist 用 create-new 打开文件,原子地先写 Meta,再写 pending。

复刻时先验证协议不变量,再补性能优化。一个能在故障注入下说明“留下了什么”的小实现,比一个只在正常路径调用 save() 的演示更接近真实 Runtime。

源码导航

相邻测试也很重要:

本节结论

新 Thread 可以先存在于内存而没有 Rollout 文件,但一旦物化,第一条规范记录必须是预先构造好的 SessionMeta,后续读取以它确定身份和 History Mode。

阅读导航

上一节:8.4 · 下一节:8.6

评论


← 返回文章列表