恢复算法先从尾部反向寻找最小充分基座,再把保留下来的后缀按原顺序前向重放;反向阶段决定“从哪里开始”,前向阶段决定“最终是什么”。
本节解决的不是“把对象存一下”这种抽象问题,而是把问题限定在:拆解 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 的两阶段算法 |
正常路径:先看顺序点
这条路径可以压缩成五步:
- 从尾到头按 TurnStarted/用户边界分段。
- 应用 pending rollback skip。
- 选最新 surviving compact replacement。
- 截取需重放的后缀并恢复元数据。
- 从旧到新 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 是可观察屏障,以及失败后保留的是已提交前缀、未提交后缀,还是完全独立的外部副作用。
失败、取消与恢复
| 故障点 | 已留下的状态 | 可观察结果 | 恢复责任 |
|---|---|---|---|
| 把 contextual user 当 Turn | Rollback 计数提前消耗 | 错误用户历史存活 | 只认真实 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。
源码导航
- codex-rs/core/src/session/rollout_reconstruction.rs:反向 segment、Rollback skip、base 选择与前向 replay
- codex-rs/core/src/context_manager/history.rs:drop_last_n_user_turns 与 replace
相邻测试也很重要:
- codex-rs/core/src/session/rollout_reconstruction_tests.rs:完成/未完成 Turn、非用户段、inter-agent、compact、window 与 rollback 组合
- codex-rs/core/src/thread_rollout_truncation_tests.rs:Fork 截断时应用 Rollback marker 的一致性
本节结论
恢复算法先从尾部反向寻找最小充分基座,再把保留下来的后缀按原顺序前向重放;反向阶段决定“从哪里开始”,前向阶段决定“最终是什么”。
评论
登录后即可评论