雨天小六

读懂 Codex(8.21):Rollout Reconstruction 的反向分段和前向重放

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

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

恢复算法先从尾部反向寻找最小充分基座,再把保留下来的后缀按原顺序前向重放;反向阶段决定“从哪里开始”,前向阶段决定“最终是什么”。

本节解决的不是“把对象存一下”这种抽象问题,而是把问题限定在:拆解 segment、用户边界、Compacted 与 Rollback 的两阶段算法。输入:按物理顺序排列的 canonical RolloutItem;状态所有者:session::rollout_reconstruction;成功结果:ContextManager history、previous settings、reference context、WorldState 与窗口元数据。先把这些边界钉住,后面的顺序、失败和恢复才不会混成一句“持久化失败后重试”。

状态边界

问题本节答案
输入按物理顺序排列的 canonical RolloutItem
状态所有者session::rollout_reconstruction
成功产物ContextManager history、previous settings、reference context、WorldState 与窗口元数据
研究范围拆解 segment、用户边界、Compacted 与 Rollback 的两阶段算法

正常路径:先看顺序点

Rollout Reconstruction 的反向分段和前向重放的正常路径时序图,展示从尾到头按 TurnStarted/用户边界分段、应用 pending rollback skip、选最新 surviving compact replacement、截取需重放的后缀并恢复元数据、从旧到新 apply Response/Compact/Rollback
图 8.21-1:从尾到头按 TurnStarted/用户边界分段 → 应用 pending rollback skip → 选最新 surviving compact replacement → 截取需重放的后缀并恢复元数据 → 从旧到新 apply Response/Compact/Rollback。这张图标出本节的实际顺序点与状态所有者。

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

  1. 从尾到头按 TurnStarted/用户边界分段。
  2. 应用 pending rollback skip。
  3. 选最新 surviving compact replacement。
  4. 截取需重放的后缀并恢复元数据。
  5. 从旧到新 apply Response/Compact/Rollback。

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

源码机制拆解

分段不是按任意 role=user

真实用户边界可以来自 EventMsg::UserMessage、ResponseItem 的 user-turn boundary 或 InterAgentCommunication;TurnStarted 用来封闭活动 segment。上下文型 user 消息不能误算成一个可回滚 Turn。

反向 Rollback 维护待跳过计数

遇到 ThreadRolledBack(N) 就累加 pending_rollback_turns;之后只有含真实用户边界的 segment 才消耗一个计数。后台维护 Turn 不会让回滚少跳一次用户请求。

最新 surviving checkpoint 成为基座

反向扫描在未被 rollback 排除的 segment 中选择第一个有 replacement_history 的 CompactedItem。它之前的 ResponseItem 无需进入 replay suffix。

早停需要多项事实同时就绪

仅找到 compact 不足以停止;previous settings 与 reference TurnContext 也必须能从完成的真实用户 Turn 确定。否则继续扫描更旧 segment,防止使用不完整 Turn 的设置。

前向重放处理兼容历史

先用 replacement_history 建 ContextManager,再顺序 apply 后缀。新的 Compacted 再替换;Legacy compact 缺 replacement 时走旧式摘要重建;Rollback marker 调用 drop_last_n_user_turns。

Python 风格伪代码

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

def reconstruct(items):
    segments = reverse_segment(items)
    pending_rollback = 0
    base = None
    suffix_start = 0
    settings = UNKNOWN
    reference_context = NEVER_SET

    for segment in segments:                       # newest -> oldest
        pending_rollback += segment.rollback_count
        if pending_rollback and segment.has_real_user_boundary:
            pending_rollback -= 1
            continue
        if base is None and segment.compact_has_replacement:
            base = segment.compact.replacement_history
            suffix_start = segment.after_checkpoint_index
        settings = settings.or_from_completed_user_turn(segment)
        reference_context = reference_context.observe(segment)
        if base is not None and settings.known and reference_context.known:
            break

    history = ContextHistory(base or [])
    for item in items[suffix_start:]:              # oldest -> newest
        match item:
            case ResponseItem(value): history.append(value)
            case Compacted(replacement=Some(value)): history.replace(value)
            case Compacted(replacement=None): history.legacy_rebuild()
            case ThreadRolledBack(n): history.drop_last_n_user_turns(n)
    return assemble_state(history, settings, reference_context, items)

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

失败、取消与恢复

Rollout Reconstruction 的反向分段和前向重放的失败路径图,区分把 contextual user 当 Turn、在不完整 Turn 取 settings、找到 compact 立即早停、Legacy compact 无 replacement
图 8.21-2:同一操作在不同故障点留下的状态不同,恢复责任必须回到拥有该状态的组件。
故障点已留下的状态可观察结果恢复责任
把 contextual user 当 TurnRollback 计数提前消耗错误用户历史存活只认真实 user boundary
在不完整 Turn 取 settings该 Turn 没有可靠终态Resume 使用半更新配置只从 surviving completed user turn 取
找到 compact 立即早停reference context 可能仍未知下次 diff 基线错误满足全部 early-stop 条件
Legacy compact 无 replacement不能直接 replace需要兼容摘要重建强制扫描/旧算法并清理不可靠基线

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

必须保持的不变量

  • Rollback 只跳过真实用户指令 Turn
  • 选择的 base checkpoint 必须在 surviving history 中
  • 后缀永远按原物理顺序重放
  • history 与 settings/reference/world-state 来自同一存活边界

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

设计取舍

两阶段算法比从头全重放复杂,却让大日志可以跳过已被新 checkpoint 取代的前缀,同时保留 Legacy 与回滚语义。

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

Mini Codex 复刻

先写纯函数 segmenter 和 reducer;golden tests 覆盖 completed/incomplete/non-user/inter-agent Turn、多个 rollback、多个 compact 和越界回滚。

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

源码导航

相邻测试也很重要:

本节结论

恢复算法先从尾部反向寻找最小充分基座,再把保留下来的后缀按原顺序前向重放;反向阶段决定“从哪里开始”,前向阶段决定“最终是什么”。

阅读导航

上一节:8.20 · 下一节:8.22

评论


← 返回文章列表