雨天小六

读懂 Codex(8.10):`.jsonl.zst` 压缩和重新物化

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

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

冷 Rollout 压缩是后台 best-effort 表示变换,不改变逻辑历史;任何需要追加或引用物理前缀的路径都会先把 .jsonl.zst 安全物化回普通 JSONL。

本节解决的不是“把对象存一下”这种抽象问题,而是把问题限定在:研究冷压缩筛选、原子发布和追加前解压,不讨论 JSONL 内容语义。输入:达到冷却阈值且未被引用的 plain JSONL;状态所有者:后台 compression worker 与 materialize_rollout_for_reference;成功结果:可透明读取的 zstd 表示,或可继续追加的 plain 表示。先把这些边界钉住,后面的顺序、失败和恢复才不会混成一句“持久化失败后重试”。

状态边界

问题本节答案
输入达到冷却阈值且未被引用的 plain JSONL
状态所有者后台 compression worker 与 materialize_rollout_for_reference
成功产物可透明读取的 zstd 表示,或可继续追加的 plain 表示
研究范围研究冷压缩筛选、原子发布和追加前解压,不讨论 JSONL 内容语义

正常路径:先看顺序点

`.jsonl.zst` 压缩和重新物化的正常路径时序图,展示扫描 active/archived 冷文件、排除引用和变化中的对象、压缩到临时文件并回读验证、发布 zst 后删除 plain、追加时解压临时文件并恢复 plain
图 8.10-1:扫描 active/archived 冷文件 → 排除引用和变化中的对象 → 压缩到临时文件并回读验证 → 发布 zst 后删除 plain → 追加时解压临时文件并恢复 plain。这张图标出本节的实际顺序点与状态所有者。

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

  1. 扫描 active/archived 冷文件。
  2. 排除引用和变化中的对象。
  3. 压缩到临时文件并回读验证。
  4. 发布 zst 后删除 plain。
  5. 追加时解压临时文件并恢复 plain。

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

源码机制拆解

Worker 有时间和并发预算

默认只考虑至少 7 天未修改的文件;run marker 防止 6 小时内重复启动,单轮最长 5 小时,并发压缩 job 上限 2,zstd level 为 3。它不阻塞前台启动结果。

引用中的物理文件不能压缩切换

扫描会构建 RolloutReferenceIndex,跳过被 history_base 指向的源与 Fork pointer。引用读取依赖稳定 byte offset,不能在无协调时把表示换掉。

发布前后都重新检查

Worker 先压缩临时文件并验证能够解码,再检查源文件身份/mtime/size 没变化;发布后再次确认,最后才删除 plain,降低并发追加造成的数据丢失风险。

读取允许短暂表示竞态

打开 plain 或 .zst 时,若恰好撞到另一进程切换表示导致 NotFound,会以 50ms 间隔重试最多 3 次。读取接口对上层隐藏具体扩展名。

追加必须物化 plain

解压先写临时文件、sync、保留权限和 mtime,再以 no-clobber 方式发布 plain。确认 plain 存在以后才删除压缩文件,然后 Recorder 才执行尾部换行检查与追加。

Python 风格伪代码

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

async def compress_if_cold(path, refs):
    snapshot = await stat_identity(path)
    if snapshot.age_days < 7 or refs.contains(path):
        return SKIP
    temp = await zstd_encode_to_temp(path, level=3)
    await verify_can_decode(temp)
    if await stat_identity(path) != snapshot:
        return RETRY_LATER
    await publish_noclobber(temp, path + '.zst')
    if await stat_identity(path) == snapshot:
        await remove_plain(path)

async def materialize_for_append(zst_path):
    temp = await decode_to_temp(zst_path)
    await temp.sync()
    plain = await publish_noclobber(temp, remove_suffix(zst_path, '.zst'))
    if await exists(plain):
        await remove(zst_path)
    return plain

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

失败、取消与恢复

`.jsonl.zst` 压缩和重新物化的失败路径图,区分压缩期间源文件变化、zst 验证失败、读取撞上表示切换、plain 未发布先删 zst
图 8.10-2:同一操作在不同故障点留下的状态不同,恢复责任必须回到拥有该状态的组件。
故障点已留下的状态可观察结果恢复责任
压缩期间源文件变化临时 zst 基于旧快照不能发布为当前历史身份重检失败后放弃
zst 验证失败临时产物不可读plain 必须保留删除 temp 并记录警告
读取撞上表示切换plain/zst 名字短暂变化首次 open NotFound有限次数延迟重试
plain 未发布先删 zst两种表示都不存在历史丢失确认 plain 后才移除 zst

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

必须保持的不变量

  • 任一时刻至少有一种完整可读表示
  • 压缩不改变 JSONL 解码后的 RolloutLine 序列
  • 被引用祖先不在后台无协调切换表示
  • 追加只发生在 plain JSONL 上

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

设计取舍

透明压缩节省冷历史空间,却引入表示竞态、临时文件和引用感知;因此它被设计成可放弃的维护任务,而非写入成功条件。

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

Mini Codex 复刻

后台任务只压缩 immutable-by-check 的冷文件;读端尝试 plain/zst,写端强制 materialize,故障注入确保发布顺序不会出现双缺失。

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

源码导航

相邻测试也很重要:

本节结论

冷 Rollout 压缩是后台 best-effort 表示变换,不改变逻辑历史;任何需要追加或引用物理前缀的路径都会先把 .jsonl.zst 安全物化回普通 JSONL。

阅读导航

上一节:8.9 · 下一节:8.11

评论


← 返回文章列表