From 79cf16c4347d99a7d6dea4a076793440aa440d03 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Mon, 16 Mar 2026 13:29:12 -0700 Subject: [PATCH] core: collapse reference turn context state Co-authored-by: Codex --- codex-rs/core/src/codex.rs | 15 +++-- .../core/src/codex/rollout_reconstruction.rs | 43 +++++-------- .../src/codex/rollout_reconstruction_tests.rs | 4 +- codex-rs/core/src/codex_tests.rs | 10 +-- codex-rs/core/src/context_manager/history.rs | 61 ++++++++----------- 5 files changed, 55 insertions(+), 78 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index bca21d0a25..53c2507726 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -187,12 +187,13 @@ pub enum SteerInputError { EmptyInput, } -/// Notes from the previous real user turn. +/// Notes from the last stored turn-context snapshot that future prompt construction +/// should diff against. /// -/// Conceptually this is the same role that `previous_model` used to fill, but -/// it can carry other prior-turn settings that matter when constructing -/// sensible state-change diffs or full-context reinjection, such as model -/// switches or detecting a prior `realtime_active -> false` transition. +/// Conceptually this is the same role that `previous_model` used to fill, but it +/// can carry other prior-turn settings that matter when constructing sensible +/// state-change diffs or full-context reinjection, such as model switches or +/// detecting a prior `realtime_active -> false` transition. #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct PreviousTurnSettings { pub(crate) model: String, @@ -2147,6 +2148,8 @@ impl Session { .await; { let mut state = self.state.lock().await; + // The injected initial context becomes the new baseline for future diffs, + // so advance both the stored settings snapshot and the reference baseline. state.set_reference_context_item(Some(turn_context.to_turn_context_item())); } @@ -2174,7 +2177,7 @@ impl Session { .previous_turn_settings(); let surviving_turn_context_item = reconstructed_rollout .reference_turn_context_state - .latest_turn_context_item(); + .turn_context_item(); let mut state = self.state.lock().await; state.replace_history_with_reference_turn_context_state( reconstructed_rollout.history, diff --git a/codex-rs/core/src/codex/rollout_reconstruction.rs b/codex-rs/core/src/codex/rollout_reconstruction.rs index f769e3ced6..bdce454bc6 100644 --- a/codex-rs/core/src/codex/rollout_reconstruction.rs +++ b/codex-rs/core/src/codex/rollout_reconstruction.rs @@ -64,39 +64,26 @@ fn merge_surviving_segment_turn_context_state( segment_turn_context_state: ReferenceTurnContextState, counts_as_user_turn: bool, ) { - // Only real user turns should backfill "previous turn settings". Standalone task turns may - // carry lifecycle events, but they must not become the latest real turn context. + let segment_is_newest_stored_turn_context = + reference_turn_context_state.turn_context_item().is_none(); + + // Only real user turns may establish the surviving stored turn-context snapshot. + // Standalone task turns can carry lifecycle events, but they must not become the source of + // future settings diffs or rollback metadata. if counts_as_user_turn - && reference_turn_context_state - .latest_turn_context_item() - .is_none() - && let Some(turn_context_item) = segment_turn_context_state.latest_turn_context_item() + && segment_is_newest_stored_turn_context + && let Some(turn_context_item) = segment_turn_context_state.turn_context_item() { - reference_turn_context_state.set_latest_turn_context_item(Some(turn_context_item)); + reference_turn_context_state.set_turn_context_item_during_reverse_replay(turn_context_item); } - // A compaction seen in this segment hides older reference baselines, but it must not erase a - // newer stored reference baseline we already captured from a later surviving user turn. - if segment_turn_context_state.compacted_since_model_saw_reference_turn_context() - && reference_turn_context_state - .stored_reference_turn_context_item() - .is_none() + // A compaction seen in this segment shadows any older baseline, but it must not hide a newer + // stored turn context we already captured from a later surviving user turn. + if segment_is_newest_stored_turn_context + && segment_turn_context_state.compacted_since_model_saw_reference_turn_context() { reference_turn_context_state.note_compaction(); } - - // The model-visible reference baseline comes from the newest surviving user turn that both - // carries a stored baseline and has not been hidden by a later surviving compaction. - if counts_as_user_turn - && !reference_turn_context_state.compacted_since_model_saw_reference_turn_context() - && reference_turn_context_state - .stored_reference_turn_context_item() - .is_none() - && let Some(turn_context_item) = - segment_turn_context_state.stored_reference_turn_context_item() - { - reference_turn_context_state.set_reference_context_item(Some(turn_context_item)); - } } impl Session { @@ -212,9 +199,7 @@ impl Session { if base_replacement_history.is_some() && pending_rollback_turns == 0 - && reference_turn_context_state - .latest_turn_context_item() - .is_some() + && reference_turn_context_state.turn_context_item().is_some() { // At this point the replay-derived metadata and replacement-history base for the // surviving tail are both fixed, so older rollout items cannot affect this result. diff --git a/codex-rs/core/src/codex/rollout_reconstruction_tests.rs b/codex-rs/core/src/codex/rollout_reconstruction_tests.rs index 189d8cf17c..8fe7e1f508 100644 --- a/codex-rs/core/src/codex/rollout_reconstruction_tests.rs +++ b/codex-rs/core/src/codex/rollout_reconstruction_tests.rs @@ -257,7 +257,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_com serde_json::to_value( reconstructed .reference_turn_context_state - .latest_turn_context_item(), + .turn_context_item(), ) .expect("serialize surviving turn context item"), serde_json::to_value(Some(first_context_item)) @@ -491,7 +491,7 @@ async fn reconstruct_history_rollback_backfills_surviving_turn_context_from_olde serde_json::to_value( reconstructed .reference_turn_context_state - .latest_turn_context_item(), + .turn_context_item(), ) .expect("serialize surviving turn context item"), serde_json::to_value(Some(first_context_item)) diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 36a742b00b..5df86f60fa 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -1033,7 +1033,7 @@ async fn record_initial_history_reconstructs_forked_transcript() { } #[tokio::test] -async fn record_initial_history_forked_hydrates_previous_turn_settings() { +async fn record_initial_history_forked_advances_previous_turn_settings_to_injected_baseline() { let (session, turn_context) = make_session_and_context().await; let previous_model = "forked-rollout-model"; let previous_context_item = TurnContextItem { @@ -1094,7 +1094,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() { assert_eq!( session.previous_turn_settings().await, Some(PreviousTurnSettings { - model: previous_model.to_string(), + model: turn_context.model_info.slug.clone(), realtime_active: Some(turn_context.realtime_active), }) ); @@ -3914,7 +3914,7 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co } #[tokio::test] -async fn record_context_updates_and_set_reference_context_item_persists_baseline_without_emitting_diffs() +async fn record_context_updates_and_set_reference_context_item_emits_model_diff_from_stored_baseline() { let (session, previous_context) = make_session_and_context().await; let next_model = if previous_context.model_info.slug == "gpt-5.1" { @@ -3955,7 +3955,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline let update_items = session .build_settings_update_items(Some(&previous_context_item), &turn_context) .await; - assert_eq!(update_items, Vec::new()); + assert!(!update_items.is_empty()); session .record_context_updates_and_set_reference_context_item(&turn_context) @@ -3963,7 +3963,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline assert_eq!( session.clone_history().await.raw_items().to_vec(), - Vec::new() + update_items ); assert_eq!( serde_json::to_value(session.reference_context_item().await) diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index 643c596ebd..a906529c02 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -36,30 +36,24 @@ pub(crate) struct ContextManager { reference_turn_context_state: ReferenceTurnContextState, } -/// Session-owned bookkeeping for turn-context state that survives history replay, -/// rollback, and compaction. +/// Session-owned bookkeeping for the last stored turn-context snapshot that survives +/// history replay, rollback, and compaction. /// -/// This intentionally tracks both the latest real turn context we know about and the -/// model-visible reference baseline, because those diverge when compaction hides the -/// baseline without erasing the last real turn's settings. +/// The stored `TurnContextItem` drives future settings diffs and rollback metadata. +/// Compaction may shadow it for reference-baseline purposes without erasing the +/// stored snapshot itself. #[derive(Debug, Clone, Default)] pub(crate) struct ReferenceTurnContextState { - /// The most recent real turn context we reconstructed or recorded, even if a later - /// compaction means the model can no longer rely on it as the active baseline. + /// The last stored turn-context snapshot we reconstructed or recorded. /// - /// This drives `previous_turn_settings()` and other rollback bookkeeping, which - /// intentionally survive compaction until a newer real turn replaces them. - latest_turn_context_item: Option, - /// The last turn context item that established the model's reference baseline. - /// - /// Unlike `latest_turn_context_item`, this is only model-visible when - /// `compacted_since_model_saw_reference_turn_context` is false. - reference_turn_context_item: Option, + /// This drives `previous_turn_settings()` and rollback bookkeeping even when + /// compaction has shadowed it for reference-baseline purposes. + turn_context_item: Option, /// Whether compaction has crossed the current reference baseline without a later - /// reinjection or real turn context re-establishing it. + /// reinjection or newer stored turn context re-establishing it. /// /// When this is true, `reference_context_item()` must return `None` even if - /// `reference_turn_context_item` still retains the last stored baseline for replay or + /// `turn_context_item` still retains the last stored snapshot for replay or /// rollback bookkeeping. compacted_since_model_saw_reference_turn_context: bool, } @@ -74,7 +68,7 @@ impl ReferenceTurnContextState { } pub(crate) fn note_compaction_during_reverse_replay(&mut self) { - if self.reference_turn_context_item.is_none() { + if self.turn_context_item.is_none() { self.compacted_since_model_saw_reference_turn_context = true; } } @@ -83,39 +77,34 @@ impl ReferenceTurnContextState { &mut self, turn_context_item: &TurnContextItem, ) { - if self.latest_turn_context_item.is_none() { - self.latest_turn_context_item = Some(turn_context_item.clone()); - } - if self.reference_turn_context_item.is_none() { - self.reference_turn_context_item = Some(turn_context_item.clone()); + if self.turn_context_item.is_none() { + self.turn_context_item = Some(turn_context_item.clone()); } } - pub(crate) fn set_latest_turn_context_item(&mut self, item: Option) { - self.latest_turn_context_item = item; + pub(crate) fn set_turn_context_item_during_reverse_replay( + &mut self, + turn_context_item: TurnContextItem, + ) { + self.turn_context_item = Some(turn_context_item); } pub(crate) fn record_regular_turn_context(&mut self, turn_context_item: TurnContextItem) { - self.latest_turn_context_item = Some(turn_context_item.clone()); - self.reference_turn_context_item = Some(turn_context_item); + self.turn_context_item = Some(turn_context_item); self.compacted_since_model_saw_reference_turn_context = false; } pub(crate) fn set_reference_context_item(&mut self, item: Option) { if let Some(item) = item { - self.reference_turn_context_item = Some(item); + self.turn_context_item = Some(item); self.compacted_since_model_saw_reference_turn_context = false; } else { self.note_compaction(); } } - pub(crate) fn latest_turn_context_item(&self) -> Option { - self.latest_turn_context_item.clone() - } - - pub(crate) fn stored_reference_turn_context_item(&self) -> Option { - self.reference_turn_context_item.clone() + pub(crate) fn turn_context_item(&self) -> Option { + self.turn_context_item.clone() } pub(crate) fn compacted_since_model_saw_reference_turn_context(&self) -> bool { @@ -123,7 +112,7 @@ impl ReferenceTurnContextState { } pub(crate) fn previous_turn_settings(&self) -> Option { - self.latest_turn_context_item + self.turn_context_item .as_ref() .map(|turn_context_item| PreviousTurnSettings { model: turn_context_item.model.clone(), @@ -135,7 +124,7 @@ impl ReferenceTurnContextState { if self.compacted_since_model_saw_reference_turn_context { None } else { - self.reference_turn_context_item.clone() + self.turn_context_item.clone() } } }