第 7 章:Resume、Rollback、Fork 与 Replay
阅读契约: 把本章当作 reconstruction 问题来看 resume、rollback、fork 和 replay。阅读时找出哪些持久事实让 Codex 能在时间流逝或分支之后重建 prompt state。
第 6 章把 compaction 解释为 checkpoint 协议。这套协议只有在系统之后能重建有效上下文时才划算。Codex 必须能 resume 旧 thread、回滚最近用户 turn、为子 agent 派生工作、并向客户端 replay 足够多的历史。进程退出或分叉之后,runtime 不能信任内存里的 vector,必须从 rollout 证据重建。
重建代码是 Codex 上下文纪律最清楚的例子之一。它从新到旧扫描 rollout items,找到最近仍有效的 replacement-history checkpoint 与 resume 元信息,然后把存活的后缀向前重放。Rollback 标记在扫描时被解释,重建出的历史反映的是有效状态,而不是 raw 事件顺序。
读完本章,你应该明白持久上下文与持久 transcript 不是一回事。

重建的三个输出
重建返回三件东西:
| 输出 | 为什么重要 |
|---|---|
| 重建的历史 | 后续 turn 用的模型可见账本。 |
| 上一次 turn 设置 | resume 时决定模型/realtime diff 所需的元信息。 |
| Reference context item | 设置 diff 的 baseline,或显式的”已清空”状态。 |
后两个输出容易被忽视。Resume 不只是”加载消息”,它还必须恢复第 4 章 diff 系统使用的上下文 baseline。如果 compaction 清空了那个 baseline,resume 必须保持已清空的状态。 逆向扫描高效,因为一旦找到更新的存活 replacement-history checkpoint 与所需元信息,旧 rollout items 就与重建无关。
阅读 Rollout 布局
Rollout 是一份 append-only 的结构化 item 日志,典型片段会混合 initial context、user turns、assistant turns、tool observations、checkpoint、rollback marker 和 fork boundary。
扫描从右往左走,碰到最近的存活 checkpoint 就短路。例子里 u3 与 u4 之间的 CP 就是这个点,于是 i_c、u1、a1、t1、u2、a2 对重建已无意义。Rollback 标记 RB 在扫描中被解释。
逆向扫描算法
算法不长但谨慎。下面的伪代码忠实地反映其形状:
// 伪代码 -- 逆向重建。
rebuiltSuffix = []
referenceCleared = false
sawCheckpoint = false
for item in reversed(rollout):
if not sawCheckpoint and item.isReplacementCheckpoint():
rebuiltSuffix = item.replacementBase + rebuiltSuffix
sawCheckpoint = true
continue
if item.isRollbackMarker():
applyRollback(rebuiltSuffix, item.scope)
continue
if item.isPreviousSettings():
previousSettings = previousSettings or item.value
continue
if item.isReferenceContext():
referenceContext = referenceContext or item.value
if item.cleared: referenceCleared = true
continue
if not sawCheckpoint:
rebuiltSuffix.prepend(item)
if referenceCleared:
referenceContext = None
return rebuiltSuffix, previousSettings, referenceContext
注意三条性质。其一,循环在拿到所需信息后立刻终止;其二,“已清空”标志即使较早出现(逆序中较晚扫到),也会胜过更早的 baseline;其三,rollback 标记在构建过程中应用,避免对 suffix 编辑两次。
Rollback 改变过去的含义
Rollback 标记不会删除 raw rollout 记录,它改变哪些 user-turn 段落计入有效历史。逆向扫描时,Codex 把”丢弃最近 N 个用户 turn”理解为”跳过接下来 N 个完成的 user-turn 段落”。这让重建在保留 raw 证据的同时,重建出用户要求的状态。
同样的逻辑出现在 rollout truncation helpers:在应用 rollback 标记的同时跟踪 user-message 位置。Fork-turn 位置同时包含真实用户消息与触发 turn 的 assistant inter-agent envelope;rollback 按 instruction-turn 边界移除过期后缀。
这是一个严肃设计:rollback 是账本里的事件,而不是对日志的破坏性编辑。
| 朴素做法 | Codex 做法 |
|---|---|
| 修改日志:删除被回滚的 items。 | 追加一个 marker, 在重建时应用。 |
| Resume 直接读磁盘上的内容。 | Resume 读取 rollout 并做投影。 |
| 审计只能看到存活状态, 看不到为什么存活。 | 审计可以重放回滚决定。 |
| 删除前后读到的客户端会出现分歧。 | 所有客户端看到同一份 append-only 日志。 |
Codex 方式付出少量重放代价,换来完整可审计性。
Fork 边界不止是人类消息
多 agent 工作让上下文边界更复杂。子 agent 可能从 assistant inter-agent envelope 而不是普通用户消息开始。Codex 的 fork turn 逻辑把某些 assistant envelope 在它们触发 turn 时视为边界,保住了被委托工作的语义单元。
// 伪代码 -- 说明有效 fork 截断。
for item in rollout:
if item.isRollbackMarker():
removeRolledBackInstructionTurns()
if item.isRealUserMessage()
or item.isTriggeringAgentEnvelope():
rememberForkBoundary(item.position)
return suffixStartingAtNthBoundaryFromEnd()
这个模式不限于多 agent 场景。如果你的 runtime 启动工作的方式不止一种,上下文截断必须理解所有方式。
Fork 边界规则很简洁:
AGENT_ENVELOPE 是触发了子 agent turn 的 assistant 消息。它被视为边界,因为从它开始的后缀本身是一个工作单元。用户消息也是边界,但不是唯一的种类。
遗留 Compaction
重建代码仍然处理没有 replacement history 的遗留 compaction 记录:从用户消息和已存的 compaction message 重建 compacted history、清空 reference baseline、接受相对不那么理想的 prompt 形状。这条向后兼容路径有教育意义:新 checkpoint 协议存在,正是因为单纯摘要不够。
这也是源码区分持久 rollout 证据与 live history 的原因。Live history 可以随时间改进,rollout replay 则要兼容旧记录。
遗留判断规则虽小但值得命名: 分支从不”无声”产生劣化 prompt;形状差异是显式的、被记录的、telemetry 可见的。
应用模式
- Replay From Evidence: 从 append-only rollout 事实重建上下文;迁移时把 live 内存当作缓存;避免 resume 路径信任过期内存状态。
- Reverse Checkpoint Search: 反向扫描找最新存活起点;迁移到事件溯源系统;有 checkpoint 能界定工作量时,不要重放整段日志。
- Rollback Marker: 把 rollback 记录为事件;迁移时在重建时应用 marker;避免破坏性日志编辑抹掉可审计性。
- Semantic Boundaries: 显式定义 user、agent 与 fork turn 边界;迁移到每一种工作来源;拒绝只懂人类消息的截断。
- Legacy Bridge: 保留兼容路径但清空不安全 baseline;迁移时把正确性放在 prompt 完美形状之前;不要把旧记录当作新 checkpoint。