Skip to content

Rollout重建与恢复

从反向 rollout 扫描追踪 rollback、replacement history、WorldState replay、旧格式压缩和恢复结果。

基于rust-v0.150.0
CodexRustRuntime

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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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_verbatimCompacted item + replacement historyhistory 与窗口字段逐项保留不证明未知 item 均可重放
reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_completed_turnsrollback + 多个 user turnhistory、previous settings、reference baseline 同步减少不覆盖网络恢复
reconstruct_history_replays_world_state_from_latest_compaction_windowfull snapshot + merge patch生成最新 WorldState baseline不恢复实时环境
reconstruct_history_legacy_compaction_without_replacement_history_clears_later_reference_context_itemlegacy compaction + context item不错误注入当前 baseline不证明旧历史完全可重建

8. 重建边界验证 ​

bash
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

读者应能回答:

  1. 为什么 replacement history 找到后只需要重放 suffix?
  2. rollback 为什么按 user boundary 而不是按 rollout item 数量?
  3. legacy compaction 为什么不能直接注入当前 TurnContext?
  4. WorldState baseline 为什么和 model history 分开恢复?
  5. 恢复结果为什么不等于原 Tokio task、channel 和 provider connection 已复活?

下一步阅读 WorldState环境快照 对照 live snapshot 与 rollout replay,再阅读 ThreadManager恢复 观察恢复投影如何装配回 Session。