Compaction 的耐久结果不是一条“已经总结”的提示,而是一份可直接接管 Context History 的 replacement_history,加上窗口身份、WorldState full 和可选 TurnContext 基线。
本节解决的不是“把对象存一下”这种抽象问题,而是把问题限定在:研究 checkpoint 安装的原子语义与恢复起点,不比较本地/远端生成方法。输入:压缩后的 ResponseItem、reference TurnContext、WorldState 和新窗口元数据;状态所有者:Session::replace_compacted_history;成功结果:热 Session 与未来 Resume 共享的同一 replacement history。先把这些边界钉住,后面的顺序、失败和恢复才不会混成一句“持久化失败后重试”。
状态边界
| 问题 | 本节答案 |
|---|---|
| 输入 | 压缩后的 ResponseItem、reference TurnContext、WorldState 和新窗口元数据 |
| 状态所有者 | Session::replace_compacted_history |
| 成功产物 | 热 Session 与未来 Resume 共享的同一 replacement history |
| 研究范围 | 研究 checkpoint 安装的原子语义与恢复起点,不比较本地/远端生成方法 |
正常路径:先看顺序点
这条路径可以压缩成五步:
- 为缺失 ResponseItem 分配稳定 ID。
- 替换内存 Context History。
- 追加携带相同数组的 CompactedItem。
- 追加 WorldState full。
- 追加 reference TurnContext 并开启新窗口。
图中的箭头不是“可能调用”的依赖图,而是源码中决定可见性和所有权转移的先后关系。前一步没有确认时,后一步不能替它作出更强的成功承诺。
源码机制拆解
Replacement 是完整基座而非增量
CompactedItem.replacement_history=Some(items) 表示恢复器可以直接 history.replace(items),无需读取 checkpoint 之前的 ResponseItem 才能得到当前上下文。
内存与落盘使用同一 item 副本
replace_compacted_history 先为缺 ID 的 ResponseItem 分配 ID,再用 items.clone 同时更新 ContextManager 和构造 CompactedItem。避免热 Session 与冷 Resume 在 item identity 上分叉。
窗口元数据属于 checkpoint
message 之外还保存 window_number、first_window_id、previous_window_id 与 current window_id。Checkpoint 同时是上下文内容边界和请求缓存/追踪边界。
WorldState 在新窗口写 full
旧 patch chain 已被 replacement 截断,不能继续引用不可见 baseline。Session 在 CompactedItem 后持久化 WorldStateItem::full,并同步 ContextManager baseline。
TurnContext 决定是否继续 diff
Mid-turn compact 注入完整初始上下文时,checkpoint 后可写 reference TurnContext,下一步继续 diff;standalone/pre-turn compact 不注入时清除 reference,让下一普通 Turn 全量重建。
Python 风格伪代码
下面的伪代码只保留设计职责、状态和失败顺序;它不逐行翻译 Rust,也不借 Python 语法虚构源码中不存在的事务:
async def install_checkpoint(session, items, reference_context,
world_state, window):
items = assign_missing_stable_item_ids(items)
checkpoint = CompactedItem(
replacement_history=copy(items),
window_number=window.number,
first_window_id=window.first,
previous_window_id=window.previous,
window_id=window.current,
)
session.history.replace(items)
session.history.reference_context = reference_context
if world_state is not None:
session.history.world_state_baseline = world_state.snapshot()
await session.persist(checkpoint)
if world_state is not None:
await session.persist(WorldState.full(world_state.snapshot()))
if reference_context is not None:
await session.persist(reference_context)
session.queue_session_start_source(COMPACT)
阅读时要特别看三处:哪个对象拥有可变状态,哪一个 await 是可观察屏障,以及失败后保留的是已提交前缀、未提交后缀,还是完全独立的外部副作用。
失败、取消与恢复
| 故障点 | 已留下的状态 | 可观察结果 | 恢复责任 |
|---|---|---|---|
| 只替换内存不写 checkpoint | 热 Session 已变短 | 重启又看到旧历史 | 持久化相同 replacement |
| CompactedItem 在 ID 分配前构造 | 磁盘/内存 item ID 不同 | 后续更新和缓存相关性破裂 | 先规范 identity 再 clone |
| 新窗口只写 patch | 旧 baseline 不在 replacement | 恢复 WorldState 无法解释 | checkpoint 后写 full |
| 错误保留 reference context | replacement 没有对应初始上下文 | 下一 Turn 只发 diff | DoNotInject 模式清除 reference |
这里没有统一的“回滚一切”。内存状态、日志行、SQLite 投影、父子拓扑和工具造成的文件/网络变化分别有自己的提交点。恢复代码只能根据已经存在的权威证据继续,不能用较弱的投影替较强的事实背书。
必须保持的不变量
- 热 Context History 与 checkpoint replacement 逐项相同
- checkpoint 之后建立独立 WorldState baseline
- 窗口 ID 与 replacement 在同一语义边界生成
- 旧日志保留但不再是有效历史基座
这些不变量比“最终能 Resume”更严格:正常路径要成立,Writer 竞争、任务取消、坏尾行、投影落后和旧格式兼容时也必须成立。
设计取舍
把 replacement 整体写入日志会重复一部分文本、增加磁盘占用,却显著缩短恢复扫描并保持旧记录可审计。
源码可以直接证明字段、分支、调用顺序和测试期望;“为什么这样设计”的表述是基于这些事实作出的工程归纳,不把它包装成未公开的产品承诺。
Mini Codex 复刻
Checkpoint 结构直接存 replacement list 和 window tuple;安装函数是唯一入口,禁止调用方分别改内存与写日志。
复刻时先验证协议不变量,再补性能优化。一个能在故障注入下说明“留下了什么”的小实现,比一个只在正常路径调用 save() 的演示更接近真实 Runtime。
源码导航
- codex-rs/core/src/session/mod.rs:replace_compacted_history 的 ID、内存替换、落盘与基线顺序
- codex-rs/protocol/src/protocol.rs:CompactedItem 的 replacement/window 字段
- codex-rs/core/src/session/rollout_reconstruction.rs:恢复时选择和应用 replacement
相邻测试也很重要:
- codex-rs/core/src/session/rollout_reconstruction_tests.rs:多个 checkpoint、窗口、WorldState 与 reference context
- codex-rs/rollout/src/model_context.rs:反向选择可用 checkpoint 的模块内测试
本节结论
Compaction 的耐久结果不是一条“已经总结”的提示,而是一份可直接接管 Context History 的 replacement_history,加上窗口身份、WorldState full 和可选 TurnContext 基线。
评论
登录后即可评论