Files
codex/codex-rs/rollout/src/model_context.rs
T
Owen Lin e7bbc79f48 Resume model context from the latest compaction boundary (#47365)
## Why

Resuming a compacted thread should use the compaction and its newer records without depending on older turn contexts or restoring stale state from before the compaction.

## What changed

- Stop paginated model-context scans at the newest compaction with `replacement_history` and `window_number`. Fall back to full replay if the newest compaction lacks either field.
- Align reconstruction with that boundary and restore `last_started_turn_id`, previous turn settings, and the multi-agent version from compaction resume metadata. Allow newer completed turns to override the saved settings.
- Keep canonical session metadata alongside the bounded replay and use the same scan boundary for subagent rollout migration.

## Testing

Add coverage for cold resume after mid-turn compaction, bounded/full replay equivalence, missing companion state, settings precedence, and incomplete-compaction fallback.

GitOrigin-RevId: f836464bb5cadd9be4a4e05b5d1679223713fc87
2026-09-22 21:17:08 +00:00

60 lines
2.5 KiB
Rust

//! Collects persisted rollout items needed to reconstruct the most recent context window.
//!
//! Storage readers feed items newest-to-oldest. The scan stops at the newest compaction that has
//! both replacement history and a window number, or at the beginning of the rollout when no such
//! compaction exists. Items are returned in chronological order.
use crate::RolloutItem;
/// Whether a reverse model-context scan needs more rollout items.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ModelContextScanProgress {
/// The reader should provide the next older rollout item.
Continue,
/// The scan has collected a safe bounded suffix.
Complete,
}
/// Finds a bounded suffix for reconstructing the most recent context window.
///
/// A compaction with replacement history and a window number is a complete conversation-history
/// boundary. Records after it provide any companion state that was persisted. Older compactions
/// missing either field require the complete rollout so reconstruction can rebuild their history
/// or window number.
#[derive(Debug, Default)]
pub struct ModelContextScan {
items_newest_first: Vec<RolloutItem>,
requires_full_replay: bool,
}
impl ModelContextScan {
/// Adds the next newest-to-oldest rollout item and reports whether the reader can stop.
pub fn push(&mut self, item: RolloutItem) -> ModelContextScanProgress {
let progress = if self.requires_full_replay {
ModelContextScanProgress::Continue
} else if let RolloutItem::Compacted(compacted) = &item {
if compacted.replacement_history.is_some() && compacted.window_number.is_some() {
ModelContextScanProgress::Complete
} else {
// This compaction cannot be reconstructed from a bounded suffix. Do not stop at
// an older compaction because this newer one still affects the surviving history.
self.requires_full_replay = true;
ModelContextScanProgress::Continue
}
} else {
ModelContextScanProgress::Continue
};
self.items_newest_first.push(item);
progress
}
/// Returns the collected items in chronological order.
///
/// Call this after the reader reaches the beginning of its source or after [`Self::push`]
/// returns [`ModelContextScanProgress::Complete`].
pub fn finish(mut self) -> Vec<RolloutItem> {
self.items_newest_first.reverse();
self.items_newest_first
}
}