Rollout重建与恢复
本文回答一个具体问题:Session 从 rollout 恢复时,如何在不重新执行 Turn 的情况下重建 history、上一 Turn 设置、reference context、context window 和 WorldState baseline?重点是 ActiveReplaySegment 的反向扫描、 replacement history checkpoint、rollback 计数和 legacy compaction 边界。
本文不把 rollout 当作单纯的消息数组,也不把“恢复成功”解释成所有运行时资源都被复活。恢复函数返回的是 后续 Session 装配所需的投影;task、channel 和 provider connection 需要另行创建。
1. 返回对象与所有权
源码位置:codex-rs/core/src/session/rollout_reconstruction.rs
pub(super) struct RolloutReconstruction {
pub(super) history: Vec<ResponseItemEnvelope>,
pub(super) previous_turn_settings: Option<PreviousTurnSettings>,
pub(super) reference_context_item: Option<TurnContextItem>,
pub(super) world_state_baseline: Option<WorldStateSnapshot>,
pub(super) window_number: u64,
pub(super) first_window_id: Option<Uuid>,
pub(super) previous_window_id: Option<Uuid>,
pub(super) window_id: Option<Uuid>,
}这些结果由 Session::reconstruct_history_from_rollout 产生,随后用于 resume/fork hydration。history 是 模型输入历史,其他字段是恢复配置和窗口基线;它们不是同一个持久化层。
2. 从新到旧扫描
实现从 rollout_items.iter().enumerate().rev() 开始。每个活动段收集 Turn id、是否包含真实 user boundary、 最新 TurnContext、WorldState item、window 和 replacement history;遇到匹配的 TurnStarted 才 finalize。 这样可以优先找到最新仍然有效的 compaction checkpoint,而不让更旧的 rollout 覆盖恢复结果。
源码位置:codex-rs/core/src/session/rollout_reconstruction.rs
for (index, item) in rollout_items.iter().enumerate().rev() {
match item {
RolloutItem::Compacted(compacted) => {
let active = active_segment.get_or_insert_with(Default::default);
if active.base_replacement_history.is_none()
&& let Some(history) = &compacted.replacement_history
{
active.base_replacement_history = Some(history);
rollout_suffix = &rollout_items[index + 1..];
}
}
RolloutItem::EventMsg(EventMsg::ThreadRolledBack(rollback)) => {
pending_rollback_turns += rollback.num_turns as usize;
}
RolloutItem::EventMsg(EventMsg::TurnStarted(event)) => { /* finalize matching segment */ }
_ => { /* collect context, world state and response boundaries */ }
}
}3. replacement
CompactedItem::replacement_history 是完整 history base;找到最新存活 checkpoint 后,代码只把其后的 rollout suffix 正向物化。ThreadRolledBack 的语义是删除最新的 N 个真实 user turns,而不是删除 N 个 任意 RolloutItem,所以反向扫描用 pending_rollback_turns 跳过包含 user boundary 的 segment。
源码位置:codex-rs/core/src/session/rollout_reconstruction.rs :: finalize_active_segment
if pending_rollback_turns > 0 {
if active_segment.counts_as_user_turn {
*pending_rollback_turns -= 1;
}
return;
}
world_state_replay.extend(active_segment.world_state_replay);
if base_replacement_history.is_none() {
*base_replacement_history = active_segment.base_replacement_history;
}
if previous_turn_settings.is_none() && active_segment.counts_as_user_turn {
*previous_turn_settings = active_segment.previous_turn_settings;
}测试 reconstruct_history_rollback_skips_non_user_turns_for_history_and_metadata 证明 standalone Turn 不会被 误当作普通 user turn;另一个 reconstruct_history_rollback_counts_inter_agent_assistant_turns 明确证明 inter-agent assistant Turn 会计入 rollback 的 Turn 边界。reconstruct_history_rollback_clears_history_and_metadata_when_exceeding_user_turns 证明 rollback 数超过存活边界时 history 和 resume metadata 都会被清空。
恢复还需要区分 reference context 的三种状态:从未建立 baseline、曾建立但被 compaction 清除、以及当前 仍有效的最新 baseline。只有第二种状态需要显式写入清除结果,避免把“没有记录”误当成“已清除”。
源码位置:codex-rs/core/src/session/rollout_reconstruction.rs :: TurnReferenceContextItem
enum TurnReferenceContextItem {
NeverSet,
Cleared,
Latest(Box<TurnContextItem>),
}4. 上下文压缩兼容
有 window number 的 compaction 优先于 SessionMeta 中的初始 window。旧格式 compaction 没有 window number 时,代码不盲目注入当前 initial context,并使用 fallback count;没有 replacement history 的 legacy item 还可能清除后续 reference context baseline。
对应测试包括 reconstruct_history_restores_initial_window_from_session_meta、 reconstruct_history_prefers_compacted_window_over_session_meta 和两个 legacy compaction 测试。它们只证明 恢复投影,不证明 provider 会接受恢复后的 window。
实现会先检查是否存在缺少 window_number 的 legacy compaction;存在时放弃使用 SessionMeta 的初始 window 作为可靠编号,防止旧记录被错误套用新窗口身份。
5. WorldState重放
WorldState item 不直接进入 model history。代码从最新有效 checkpoint 开始重放 full snapshot 和 patch,生成 world_state_baseline;compaction 创建新窗口时必须写入 full baseline,后续 patch 才能安全应用。
源码位置:codex-rs/core/src/session/rollout_reconstruction.rs
let mut world_state_baseline: Option<WorldStateSnapshot> = None;
for item in world_state_replay {
match item {
RolloutItem::Compacted(_) => world_state_baseline = None,
RolloutItem::WorldState(world_state) if world_state.full => {
world_state_baseline = serde_json::from_value(world_state.state.clone()).ok();
}
RolloutItem::WorldState(world_state) => {
if let Some(baseline) = world_state_baseline.as_mut() {
let _ = baseline.apply_merge_patch(&world_state.state);
}
}
_ => unreachable!("only world-state replay items are collected"),
}
}reconstruct_history_replays_world_state_from_latest_compaction_window 断言最新 full baseline 和 patch 合并 后的 JSON;它不证明当前文件系统或环境变量已经恢复。
RolloutReconstruction 随后由 record_initial_history 安装到新 Session。它只返回 history、窗口和上下文 投影,不复活原来的 Tokio task、CancellationToken、MCP worker 或 provider connection。
源码位置:codex-rs/core/src/session/rollout_reconstruction.rs :: history materialization
if let Some(base_replacement_history) = base_replacement_history {
history.replace(base_replacement_history.to_vec());
}
for item in rollout_suffix {
match item {
RolloutItem::ResponseItem(response_item) => history.record_items(/* ... */),
RolloutItem::Compacted(compacted) => {
if let Some(replacement) = &compacted.replacement_history {
history.replace(replacement.clone());
}
}
_ => {}
}
}6. 失败与损坏
- rollout 为空或没有有效 checkpoint:history 由可重放的 response item 物化,window 使用 fallback。
- rollback 超过 user boundary:丢弃 history、previous settings 和 reference context。
- legacy compaction 缺少 replacement history:保留可解释的 summary 语义,不注入当前初始上下文。
- incomplete Turn 仍可按
TurnStarted/TurnAborted的 segment 边界恢复;它不会复活原来的 Tokio task。 - WorldState 缺少 full baseline 时,patch 不能凭空构造完整环境,必须保留缺口而不是猜测。
7. 测试矩阵
| 测试 | 输入 | 关键断言 | 边界 |
|---|---|---|---|
reconstruct_history_uses_replacement_history_verbatim | Compacted item + replacement history | history 与窗口字段逐项保留 | 不证明未知 item 均可重放 |
reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_completed_turns | rollback + 多个 user turn | history、previous settings、reference baseline 同步减少 | 不覆盖网络恢复 |
reconstruct_history_replays_world_state_from_latest_compaction_window | full snapshot + merge patch | 生成最新 WorldState baseline | 不恢复实时环境 |
reconstruct_history_legacy_compaction_without_replacement_history_clears_later_reference_context_item | legacy compaction + context item | 不错误注入当前 baseline | 不证明旧历史完全可重建 |
8. 重建边界验证
rg -n "reconstruct_history_from_rollout|ActiveReplaySegment|replacement_history|pending_rollback_turns|WorldState" \
codex-rs/core/src/session/rollout_reconstruction.rs
cargo test -p codex-core reconstruct_history读者应能回答:
- 为什么 replacement history 找到后只需要重放 suffix?
- rollback 为什么按 user boundary 而不是按 rollout item 数量?
- legacy compaction 为什么不能直接注入当前 TurnContext?
- WorldState baseline 为什么和 model history 分开恢复?
- 恢复结果为什么不等于原 Tokio task、channel 和 provider connection 已复活?
下一步阅读 WorldState环境快照 对照 live snapshot 与 rollout replay,再阅读 ThreadManager恢复 观察恢复投影如何装配回 Session。
