diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 441277d20f..e0e83140fd 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -3664,9 +3664,9 @@ impl Session { self.persist_rollout_items(&[RolloutItem::TurnContext(turn_context_item.clone())]) .await; - // Advance the in-memory turn-context tracker even when this turn emitted no model-visible - // context items. Regular turns become both the latest turn-settings source and the active - // model-visible reference baseline for subsequent diffing. + // Advance the stored turn-context snapshot even when this turn emitted no model-visible + // context items. Regular turns become both the `previous_turn_settings()` source and the + // active model-visible reference baseline for subsequent diffing. self.record_regular_turn_context(turn_context_item).await; } diff --git a/codex-rs/core/src/codex/rollout_reconstruction.rs b/codex-rs/core/src/codex/rollout_reconstruction.rs index 663a877701..7e73a514b5 100644 --- a/codex-rs/core/src/codex/rollout_reconstruction.rs +++ b/codex-rs/core/src/codex/rollout_reconstruction.rs @@ -65,38 +65,29 @@ 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. - 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() - { - reference_turn_context_state.set_latest_turn_context_item(Some(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. + // A compaction seen in this segment shadows any older stored turn context, but it must not + // erase a newer stored turn context 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() + .stored_turn_context_item() .is_none() { 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. + // Only real user turns should establish the stored turn context. Standalone task turns may + // carry lifecycle events, but they must not become the source of `previous_turn_settings()` + // or the reference baseline. + // + // This stores the newest surviving real turn context, while preserving any later compaction + // shadow we already learned about from newer surviving segments. 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() + .stored_turn_context_item() .is_none() - && let Some(turn_context_item) = - segment_turn_context_state.stored_reference_turn_context_item() + && let Some(turn_context_item) = segment_turn_context_state.stored_turn_context_item() { - reference_turn_context_state.set_reference_context_item(Some(turn_context_item)); + reference_turn_context_state.note_turn_context_during_reverse_replay(&turn_context_item); } } @@ -217,7 +208,7 @@ impl Session { if base_replacement_history.is_some() && pending_rollback_turns == 0 && reference_turn_context_state - .latest_turn_context_item() + .stored_turn_context_item() .is_some() { // At this point the replay-derived metadata and replacement-history base for the diff --git a/codex-rs/core/src/codex/rollout_reconstruction_tests.rs b/codex-rs/core/src/codex/rollout_reconstruction_tests.rs index c9ae48feb9..76373dbca7 100644 --- a/codex-rs/core/src/codex/rollout_reconstruction_tests.rs +++ b/codex-rs/core/src/codex/rollout_reconstruction_tests.rs @@ -271,7 +271,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(), + .stored_turn_context_item(), ) .expect("serialize surviving turn context item"), serde_json::to_value(Some(first_context_item)) @@ -499,7 +499,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(), + .stored_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 7795598431..c7840e8b93 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -1299,7 +1299,7 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result< } #[tokio::test] -async fn record_initial_history_forked_hydrates_previous_turn_settings() { +async fn record_initial_history_forked_advances_previous_turn_settings_to_current_baseline() { let (session, turn_context) = make_session_and_context().await; let previous_model = "forked-rollout-model"; let previous_context_item = TurnContextItem { @@ -1358,7 +1358,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), }) ); @@ -4119,7 +4119,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_switch_for_stored_baseline() { let (session, previous_context) = make_session_and_context().await; let next_model = if previous_context.model_info.slug == "gpt-5.1" { @@ -4160,7 +4160,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) @@ -4168,7 +4168,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 ad8195b003..19fd8e0420 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -39,30 +39,26 @@ 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 stored turn context that drives both +/// `previous_turn_settings()` and the model-visible reference baseline. /// -/// 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 turn context survives compaction so future turns can still diff +/// against the last known settings, while +/// `compacted_since_model_saw_reference_turn_context` controls whether the model +/// may still treat that stored item as the active reference baseline. #[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 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 is the single source of truth for both `previous_turn_settings()` and + /// the reference baseline. Compaction can shadow it for model visibility + /// without erasing it. + turn_context_item: Option, /// Whether compaction has crossed the current reference baseline without a later /// reinjection or real 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 baseline for replay or /// rollback bookkeeping. compacted_since_model_saw_reference_turn_context: bool, } @@ -77,7 +73,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; } } @@ -86,39 +82,27 @@ 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.turn_context_item.is_none() { + self.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()); - } - } - - pub(crate) fn set_latest_turn_context_item(&mut self, item: Option) { - self.latest_turn_context_item = 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 stored_turn_context_item(&self) -> Option { + self.turn_context_item.clone() } pub(crate) fn compacted_since_model_saw_reference_turn_context(&self) -> bool { @@ -126,7 +110,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(), @@ -138,7 +122,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() } } }