From 9e6d1c5278b6e65059f7e5f610561a3f4be0aa0d Mon Sep 17 00:00:00 2001 From: Ruslan Nigmatullin Date: Wed, 11 Nov 2026 11:52:49 +0000 Subject: [PATCH] Preserve retained assistant revisions across rollout replay (#60839) ## Why Replaying delivered assistant messages allocates new retained-source revisions instead of preserving those captured in live history. ## What changed - Capture retained sources when recording assistant deliveries, persist them in rollout metadata, and reuse their revisions during replay. - Add optional `next_item_ordinal` compaction metadata and a `next_model_id_ordinals` helper to recover item and window allocation positions from checkpoints and subsequent records. Ignore allocation metadata that refers to other threads. ## Testing Extend tests to compare live and replayed delivery sources, preserve revisions through an older reader's rewrite, and verify ordinal recovery from checkpoints and subsequent local items. GitOrigin-RevId: cfdeebe8e13b1700b34b9c98696880a90a99ace5 --- codex-rs/Cargo.lock | 1 + .../app-server-exports-stable.json.zst | Bin 169045 -> 169130 bytes codex-rs/core/src/agent/control_tests.rs | 3 + codex-rs/core/src/context_manager/history.rs | 11 ++- .../core/src/context_manager/history_tests.rs | 1 + codex-rs/core/src/session/mod.rs | 1 + codex-rs/core/src/session/retained_context.rs | 9 ++- .../session/rollout_reconstruction_tests.rs | 1 + codex-rs/core/src/session/tests.rs | 20 +++++ codex-rs/ext/guardian-v3/tests/evidence.rs | 1 + codex-rs/history/Cargo.toml | 1 + .../history/src/compaction_resume_metadata.rs | 3 + codex-rs/history/src/lib.rs | 2 + codex-rs/history/src/model_id_ordinals.rs | 70 ++++++++++++++++++ .../src/retained_assistant_messages.rs | 18 +++-- codex-rs/history/src/retained_context.rs | 42 ++++++++--- .../history/src/retained_context_tests.rs | 1 + codex-rs/history/src/rollout_payload.rs | 3 + codex-rs/history/src/tests.rs | 61 ++++++++++++++- .../src/local/model_context_tests.rs | 13 +++- .../local/rollout_migration/rollback_plan.rs | 1 + 21 files changed, 238 insertions(+), 25 deletions(-) create mode 100644 codex-rs/history/src/model_id_ordinals.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 80ab9016a6..70cb064d0f 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3850,6 +3850,7 @@ dependencies = [ "codex-protocol", "codex-protocol-test-support", "codex-utils-absolute-path", + "codex-utils-model-facing-id", "pretty_assertions", "schemars 0.8.22", "serde", diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index 29094221fa7dc75ded9b1534188d335ae51b97cc..6bffcd663e579419927e354381b98cc3fdaf7346 100644 GIT binary patch delta 23781 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 bool { - if !Arc::make_mut(&mut self.retained_context).record(event, &self.item_id_generator) { + self.record_retained_context_for_persistence(&mut event.clone()) + } + + pub(crate) fn record_retained_context_for_persistence( + &mut self, + event: &mut RetainedContextEvent, + ) -> bool { + if !Arc::make_mut(&mut self.retained_context) + .record_and_capture(event, &self.item_id_generator) + { return false; } match event { diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index 5aff78bb7f..62bd597a8e 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -203,6 +203,7 @@ fn delivered_assistant_context_invalidates_reviews_without_changing_authorizatio let before_delivery = history.conversation_history_snapshot(); let authorization = before_delivery.user_message_revision(); let event = RetainedContextEvent::DeliveredAssistantMessage { + retained_source: None, message: codex_history::RetainedUserMessage { origin: codex_history::UserInputOrigin::User, turn_id: "turn".to_owned(), diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index ce93060cd4..ea95f784bd 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -4959,6 +4959,7 @@ impl Session { compaction_response_id: metadata.compaction_response_id, latest_token_usage_record: state.latest_token_usage_record.clone(), resume_metadata: Some(CompactionResumeMetadata { + next_item_ordinal: None, rejected_audio_file_ids: state.history.rejected_audio_file_ids(), dynamic_tools: Some(Box::new(DynamicToolsSnapshot { thread_id: self.thread_id(), diff --git a/codex-rs/core/src/session/retained_context.rs b/codex-rs/core/src/session/retained_context.rs index 6572cbb811..eb344f50e6 100644 --- a/codex-rs/core/src/session/retained_context.rs +++ b/codex-rs/core/src/session/retained_context.rs @@ -156,7 +156,6 @@ impl Session { } pub(crate) async fn record_retained_context(&self, mut event: RetainedContextEvent) { - event.bound(); // Share the checkpoint persistence lock so a fact cannot land on the wrong side // of the checkpoint/suffix boundary. Ephemeral threads use the same live state. let _guard = thread_settings::acquire_persistence_lock(self).await; @@ -165,7 +164,7 @@ impl Session { .lock() .await .history - .record_retained_context(&event) + .record_retained_context_for_persistence(&mut event) { self.persist_rollout_items(&[RolloutItem::RetainedContext(event)]) .await; @@ -211,10 +210,12 @@ impl Session { let mut state = session.state.lock().await; let mut event = RetainedContextEvent::DeliveredAssistantMessage { message, + retained_source: None, acceptance_order: state.history.reserve_input_order(), }; - event.bound(); - state.history.record_retained_context(&event); + state + .history + .record_retained_context_for_persistence(&mut event); let mut pending = session .code_mode_message_tasks .pending_persistence diff --git a/codex-rs/core/src/session/rollout_reconstruction_tests.rs b/codex-rs/core/src/session/rollout_reconstruction_tests.rs index 37683e4a08..96181f9f9a 100644 --- a/codex-rs/core/src/session/rollout_reconstruction_tests.rs +++ b/codex-rs/core/src/session/rollout_reconstruction_tests.rs @@ -1642,6 +1642,7 @@ async fn bounded_replay_matches_full_replay_after_empty_turn_compactions( compaction_response_id: None, latest_token_usage_record: None, resume_metadata: current.then(|| codex_history::CompactionResumeMetadata { + next_item_ordinal: None, rejected_audio_file_ids: Default::default(), dynamic_tools: None, multi_agent_version: None, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 39dcd3cd54..08131fc76d 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -2631,6 +2631,25 @@ async fn inter_agent_communication_waits_for_confirmed_delivery_persistence() { ] ); let live = session.clone_history().await; + let replayed = session + .reconstruct_history_from_rollout(&replay_turn, &resumed.history) + .await; + let delivery_sources = |context: &codex_history::RetainedContext| { + context + .ordered_entries() + .filter_map(|(_, entry)| match entry { + codex_history::RetainedContextEntry::AssistantMessage(_) => Some( + context + .source(entry) + .expect("confirmed delivery has a revision"), + ), + _ => None, + }) + .collect::>() + }; + let expected = delivery_sources(live.retained_context()); + assert_eq!(expected.len(), 2); + assert_eq!(delivery_sources(&replayed.retained_context), expected); let communication = live .annotated_items() .iter() @@ -6219,6 +6238,7 @@ async fn compaction_persists_resume_metadata_and_companion_records() { session.multi_agent_version = std::sync::OnceLock::from(MultiAgentVersion::V2); let expected = CompactionResumeMetadata { + next_item_ordinal: None, rejected_audio_file_ids: Default::default(), dynamic_tools: Some(Box::new(DynamicToolsSnapshot { thread_id: session.thread_id(), diff --git a/codex-rs/ext/guardian-v3/tests/evidence.rs b/codex-rs/ext/guardian-v3/tests/evidence.rs index 0ca967a661..e39d331299 100644 --- a/codex-rs/ext/guardian-v3/tests/evidence.rs +++ b/codex-rs/ext/guardian-v3/tests/evidence.rs @@ -130,6 +130,7 @@ Approved action: {"action":{"command":"write /workspace/report.txt"},"outcome":" acceptance_order: Some(2), }); let delivered = RolloutItem::RetainedContext(RetainedContextEvent::DeliveredAssistantMessage { + retained_source: None, message: RetainedUserMessage { turn_id: "turn-1".into(), message_id: Some("delivered-1".into()), diff --git a/codex-rs/history/Cargo.toml b/codex-rs/history/Cargo.toml index b43dc52698..f090e69cc3 100644 --- a/codex-rs/history/Cargo.toml +++ b/codex-rs/history/Cargo.toml @@ -15,6 +15,7 @@ workspace = true [dependencies] codex-protocol = { workspace = true } codex-utils-absolute-path = { workspace = true } +codex-utils-model-facing-id = { workspace = true } schemars = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/codex-rs/history/src/compaction_resume_metadata.rs b/codex-rs/history/src/compaction_resume_metadata.rs index 6368889632..94768545af 100644 --- a/codex-rs/history/src/compaction_resume_metadata.rs +++ b/codex-rs/history/src/compaction_resume_metadata.rs @@ -18,6 +18,9 @@ use std::sync::Arc; /// compactions that did not. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct CompactionResumeMetadata { + /// Next ordinal in the producing thread's seeded item ID sequence at this checkpoint. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub next_item_ordinal: Option, /// Immutable file references rejected before this checkpoint. #[serde(default, skip_serializing_if = "BTreeSet::is_empty")] pub rejected_audio_file_ids: Arc>, diff --git a/codex-rs/history/src/lib.rs b/codex-rs/history/src/lib.rs index 28ad371cde..54792bf89b 100644 --- a/codex-rs/history/src/lib.rs +++ b/codex-rs/history/src/lib.rs @@ -290,6 +290,8 @@ impl JsonSchema for RolloutItem { } mod guardian_history; +mod model_id_ordinals; +pub use model_id_ordinals::next_model_id_ordinals; mod reconciled_retained_context; mod retained_context; mod sender_user_messages; diff --git a/codex-rs/history/src/model_id_ordinals.rs b/codex-rs/history/src/model_id_ordinals.rs new file mode 100644 index 0000000000..41e397e9d1 --- /dev/null +++ b/codex-rs/history/src/model_id_ordinals.rs @@ -0,0 +1,70 @@ +//! Recovers seeded allocation positions from a compaction checkpoint and subsequent thread items. + +use codex_protocol::ResponseItemId; +use codex_utils_model_facing_id::ModelFacingId; + +use crate::RetainedContextEvent; +use crate::RolloutItem; + +/// Next item and window ordinals. Legacy strings have no allocation provenance. +pub fn next_model_id_ordinals(records: &[RolloutItem]) -> (u64, u64) { + let mut item = None; + let mut window = None; + for record in records { + match record { + RolloutItem::SessionMeta(meta) => { + if let Some(initial) = &meta.meta.context_window { + window = window.max(initial.window_id.allocation().map(ModelFacingId::ordinal)); + } + } + RolloutItem::Compacted(checkpoint) => { + if let Some(next) = checkpoint + .resume_metadata + .as_ref() + .and_then(|m| m.next_item_ordinal) + { + item = next.checked_sub(1); + } + if let Some(id) = &checkpoint.window_id { + window = window.max(id.allocation().map(ModelFacingId::ordinal)); + } + } + RolloutItem::ResponseItem(envelope) => { + item = item.max(envelope.item.id().and_then(item_ordinal)); + // The retained revision is local. Guardian delivery metadata quotes other threads. + if let Some(source) = envelope + .metadata + .as_ref() + .and_then(|m| m.retained_source.as_ref()) + { + item = item.max(item_ordinal(&source.revision)); + } + } + RolloutItem::InterAgentCommunication(message) => { + item = item.max(message.id.as_ref().and_then(item_ordinal)); + } + RolloutItem::RetainedContext(RetainedContextEvent::DeliveredAssistantMessage { + retained_source: Some(source), + .. + }) => { + item = item.max(item_ordinal(&source.revision)); + } + RolloutItem::DynamicToolsUpdated { .. } + | RolloutItem::InterAgentCommunicationMetadata { .. } + | RolloutItem::TurnContext(_) + | RolloutItem::TokenUsageRecord(_) + | RolloutItem::WorldState(_) + | RolloutItem::SecurityRiskScore(_) + | RolloutItem::RetainedContext(_) + | RolloutItem::EventMsg(_) + | RolloutItem::RealtimeItem(_) => {} + } + } + let next = |last: Option| last.map_or(0, |ordinal| ordinal.wrapping_add(1)); + (next(item), next(window)) +} + +fn item_ordinal(id: &ResponseItemId) -> Option { + id.allocation() + .map(|allocation| allocation.model_id.ordinal()) +} diff --git a/codex-rs/history/src/retained_assistant_messages.rs b/codex-rs/history/src/retained_assistant_messages.rs index b942f1decf..b4e75363ab 100644 --- a/codex-rs/history/src/retained_assistant_messages.rs +++ b/codex-rs/history/src/retained_assistant_messages.rs @@ -37,9 +37,20 @@ impl RetainedContext { /// Returns the captured source for the original envelope, independently of buffer eviction. pub fn record_assistant_message( &mut self, - mut message: RetainedUserMessage, + message: RetainedUserMessage, source: RetainedInputSource, item_id_generator: &ItemIdGenerator, + ) -> Option { + self.record_assistant_message_with_revision(message, source, || { + item_id_generator("retained") + }) + } + + pub(super) fn record_assistant_message_with_revision( + &mut self, + mut message: RetainedUserMessage, + source: RetainedInputSource, + revision: impl FnOnce() -> codex_protocol::ResponseItemId, ) -> Option { if source == RetainedInputSource::Local(None) { // Leave unsequenced sources to legacy transcript selection. Missing @@ -72,10 +83,7 @@ impl RetainedContext { if empty { return None; } - let revision = message - .message_id - .as_ref() - .map(|_| item_id_generator("retained")); + let revision = message.message_id.as_ref().map(|_| revision()); let entry = Ordered { revision, inherited, diff --git a/codex-rs/history/src/retained_context.rs b/codex-rs/history/src/retained_context.rs index f883348845..50782c183c 100644 --- a/codex-rs/history/src/retained_context.rs +++ b/codex-rs/history/src/retained_context.rs @@ -179,6 +179,9 @@ pub enum RetainedContextEvent { #[serde(flatten)] message: RetainedUserMessage, acceptance_order: u64, + /// The host-captured evidence revision. Older deliveries only persisted the call ID. + #[serde(default, skip_serializing_if = "Option::is_none")] + retained_source: Option, }, } @@ -477,16 +480,24 @@ impl RetainedContext { event: &RetainedContextEvent, item_id_generator: &ItemIdGenerator, ) -> bool { - let mut event = event.clone(); + self.record_and_capture(&mut event.clone(), item_id_generator) + } + + /// Records a host event and fills in its retained source for rollout persistence. + pub fn record_and_capture( + &mut self, + event: &mut RetainedContextEvent, + item_id_generator: &ItemIdGenerator, + ) -> bool { event.bound(); match event { RetainedContextEvent::AcceptedFileAudio { evidence } => { if !evidence.fits_record_budget() - || self.accepted_file_audio.as_ref() == Some(&evidence) + || self.accepted_file_audio.as_ref() == Some(evidence) { return false; } - self.accepted_file_audio = Some(evidence); + self.accepted_file_audio = Some(Arc::clone(evidence)); true } RetainedContextEvent::VerifiedAnswer { @@ -497,17 +508,17 @@ impl RetainedContext { existing.value.turn_id == answer.turn_id && existing.value.call_id == answer.call_id }) { - if self.verified_answers[index].value == answer { + if self.verified_answers[index].value == *answer { return false; } self.verified_answers.remove(index); } - let order = self.record_order(acceptance_order); + let order = self.record_order(*acceptance_order); self.verified_answers.push_back(Ordered { revision: None, inherited: false, order, - value: answer, + value: answer.clone(), }); bound_family( &mut self.verified_answers, @@ -518,19 +529,26 @@ impl RetainedContext { RetainedContextEvent::DeliveredAssistantMessage { message, acceptance_order, + retained_source, } => { - if self.assistant_messages.iter().any(|entry| { + if let Some(existing) = self.assistant_messages.iter().find(|entry| { message.message_id.is_some() && entry.value.message_id == message.message_id - && entry.value == message + && entry.value == *message && !entry.inherited }) { + *retained_source = existing.source(RetainedSourceRole::Assistant); return false; } - self.record_assistant_message( - message, - RetainedInputSource::Local(Some(acceptance_order)), - item_id_generator, + *retained_source = self.record_assistant_message_with_revision( + message.clone(), + RetainedInputSource::Local(Some(*acceptance_order)), + || { + retained_source.as_ref().map_or_else( + || item_id_generator("retained"), + |source| source.revision.clone(), + ) + }, ); true } diff --git a/codex-rs/history/src/retained_context_tests.rs b/codex-rs/history/src/retained_context_tests.rs index 8d71cf59fa..aac0179ebc 100644 --- a/codex-rs/history/src/retained_context_tests.rs +++ b/codex-rs/history/src/retained_context_tests.rs @@ -51,6 +51,7 @@ fn publish_answer() -> RetainedContextEvent { fn delivered_message(id: &str, text: &str, acceptance_order: u64) -> RetainedContextEvent { RetainedContextEvent::DeliveredAssistantMessage { + retained_source: None, message: RetainedUserMessage { origin: crate::UserInputOrigin::User, turn_id: "turn-1".to_owned(), diff --git a/codex-rs/history/src/rollout_payload.rs b/codex-rs/history/src/rollout_payload.rs index 869195968b..bebb049358 100644 --- a/codex-rs/history/src/rollout_payload.rs +++ b/codex-rs/history/src/rollout_payload.rs @@ -218,6 +218,7 @@ impl<'a> From<&'a RolloutItem> for RolloutItemWire<'a> { RolloutItem::RetainedContext(RetainedContextEvent::DeliveredAssistantMessage { message, acceptance_order, + retained_source, }) => { // Earlier clients preserve this as genuine assistant context, including across compaction. let text = if message.complete { @@ -256,6 +257,7 @@ impl<'a> From<&'a RolloutItem> for RolloutItemWire<'a> { id_allocation: None, }, metadata: Some(Cow::Owned(CodexHarnessMetadata { + retained_source: retained_source.clone(), delivered_assistant_message: Some(marker), user_input_order: Some(*acceptance_order), ..Default::default() @@ -323,6 +325,7 @@ impl From> for RolloutItem { phase: None, }, acceptance_order, + retained_source: metadata.retained_source.clone(), }, ); } diff --git a/codex-rs/history/src/tests.rs b/codex-rs/history/src/tests.rs index 670388dc68..a8965d8e2e 100644 --- a/codex-rs/history/src/tests.rs +++ b/codex-rs/history/src/tests.rs @@ -1,5 +1,6 @@ use anyhow::Result; use codex_protocol::ContextWindowId; +use codex_protocol::ResponseItemId; use codex_protocol::models::AudioReference; use codex_protocol::models::ConfigurationReasoning; use codex_protocol::models::PermissionProfile; @@ -366,7 +367,8 @@ fn delivered_assistant_rollout_survives_an_earlier_read_and_rewrite() -> Result< } for complete in [true, false] { - let original = RetainedContextEvent::DeliveredAssistantMessage { + let mut original = RetainedContextEvent::DeliveredAssistantMessage { + retained_source: None, message: RetainedUserMessage { origin: crate::UserInputOrigin::User, turn_id: "root-turn".to_owned(), @@ -377,6 +379,14 @@ fn delivered_assistant_rollout_survives_an_earlier_read_and_rewrite() -> Result< }, acceptance_order: 7, }; + let sequence = std::sync::Mutex::new( + codex_utils_model_facing_id::ModelFacingIdGenerator::new(/*seed*/ 42, "item"), + ); + let generator: codex_protocol::ItemIdGenerator = Arc::new(move |kind| { + ResponseItemId::generated(kind, sequence.lock().unwrap().fetch_next()) + }); + let mut live = RetainedContext::default(); + assert!(live.record_and_capture(&mut original, &generator)); let wire = serde_json::to_value(RolloutItem::RetainedContext(original.clone()))?; let earlier = serde_json::from_value::(wire)?; let EarlierRolloutItem::ResponseItem { payload, metadata } = &earlier; @@ -432,6 +442,13 @@ fn delivered_assistant_rollout_survives_an_earlier_read_and_rewrite() -> Result< panic!("new readers need the model-invisible delivery event"); }; assert_eq!(restored, original); + let mut replayed = RetainedContext::default(); + assert!(replayed.record(&restored, &generator)); + assert_eq!(replayed, live); + assert_eq!( + crate::next_model_id_ordinals(&[RolloutItem::RetainedContext(restored)]), + (1, 0) + ); } Ok(()) } @@ -679,6 +696,7 @@ fn compacted_replacement_history_stores_metadata_in_an_aligned_sidecar() -> Resu #[test] fn compacted_resume_metadata_presence_round_trips_empty_values() -> Result<()> { let resume_metadata = CompactionResumeMetadata { + next_item_ordinal: None, rejected_audio_file_ids: Default::default(), dynamic_tools: None, multi_agent_version: None, @@ -792,6 +810,7 @@ fn compacted_metadata_remains_compatible_with_legacy_response_item_readers() -> compaction_response_id: None, latest_token_usage_record: None, resume_metadata: Some(CompactionResumeMetadata { + next_item_ordinal: None, rejected_audio_file_ids: Default::default(), dynamic_tools: None, multi_agent_version: Some(MultiAgentVersion::V2), @@ -1158,6 +1177,46 @@ fn compacted_item_serializes_window_number_and_id() -> Result<()> { Ok(()) } +#[test] +fn model_id_ordinals_resume_at_the_checkpoint_and_advance_for_local_items() -> Result<()> { + let record = |ordinal| { + json!({ + "type": "response_item", "payload": {"type": "message", "role": "assistant", "content": [], + "id": "external", "id_allocation": {"salt": "item", "ordinal": ordinal, "value": 3}} + }) + }; + let source = |ordinal| { + json!({ + "id": {"turn_id": "turn", "message_id": "external", "role": "assistant"}, + "revision": {"id": "retained_item007", + "id_allocation": {"prefix": "retained", "salt": "item", "ordinal": ordinal, "value": 7}}, + "complete": true + }) + }; + let mut tail = record(24); + tail["metadata"] = json!({ + "retained_source": source(25), + "guardian_sources": [source(300)], + "guardian_review_ids": [{"id": "review_item008", + "id_allocation": {"prefix": "review", "salt": "item", "ordinal": 301, "value": 8}}] + }); + let records: Vec = serde_json::from_value(json!([ + record(19), + {"type": "compacted", "payload": { + "message": "", "window_number": 4, + "window_id": {"salt": "window", "ordinal": 4, "value": 812}, + "replacement_history": [record(8)["payload"]], + "resume_metadata": {"next_item_ordinal": 24} + }}, + tail + ]))?; + assert_eq!(crate::next_model_id_ordinals(&[]), (0, 0)); + assert_eq!(crate::next_model_id_ordinals(&records[1..2]), (24, 5)); + assert_eq!(crate::next_model_id_ordinals(&records[1..]), (26, 5)); + assert_eq!(crate::next_model_id_ordinals(&records), (26, 5)); + Ok(()) +} + #[test] fn structured_window_ids_round_trip_in_session_metadata_and_compaction() -> Result<()> { let uuid = "019020ef-10ff-7000-8000-000000000001"; diff --git a/codex-rs/thread-store/src/local/model_context_tests.rs b/codex-rs/thread-store/src/local/model_context_tests.rs index 646c1f12ff..472d8b811f 100644 --- a/codex-rs/thread-store/src/local/model_context_tests.rs +++ b/codex-rs/thread-store/src/local/model_context_tests.rs @@ -95,6 +95,14 @@ async fn stops_at_newest_usable_compaction_and_keeps_companions() { let thread_id = ThreadId::from_string(&uuid.to_string()).expect("thread id"); let world_state = RolloutItem::WorldState(WorldStateItem::full(Default::default())); let turn_context = turn_context(home.path(), "companion"); + let checkpoint: RolloutItem = serde_json::from_value(serde_json::json!({ + "type": "compacted", "payload": { + "message": "latest compaction", "replacement_history": [], "window_number": 2, + "window_id": {"salt": "window", "ordinal": 2, "value": 6}, + "resume_metadata": {"next_item_ordinal": 24} + } + })) + .expect("seeded checkpoint"); let path = write_paginated_rollout( home.path(), "2025-01-03T13-00-00", @@ -103,7 +111,7 @@ async fn stops_at_newest_usable_compaction_and_keeps_companions() { user_message("older history"), compacted("older compaction", Some(Vec::new())), user_message("superseded suffix"), - compacted("latest compaction", Some(Vec::new())), + checkpoint.clone(), world_state.clone(), turn_context.clone(), ], @@ -125,7 +133,7 @@ async fn stops_at_newest_usable_compaction_and_keeps_companions() { serde_json::to_value(context.items).expect("serialize context"), serde_json::to_value(vec![ RolloutItem::SessionMeta(Box::new(session_meta)), - compacted("latest compaction", Some(Vec::new())), + checkpoint, world_state, turn_context, ]) @@ -272,6 +280,7 @@ async fn fork_version_stops_before_older_segments_once_resolved() { unreachable!(); }; compacted.resume_metadata = Some(codex_rollout::CompactionResumeMetadata { + next_item_ordinal: None, rejected_audio_file_ids: Default::default(), dynamic_tools: None, multi_agent_version: Some(MultiAgentVersion::V2), diff --git a/codex-rs/thread-store/src/local/rollout_migration/rollback_plan.rs b/codex-rs/thread-store/src/local/rollout_migration/rollback_plan.rs index 4b52fbe44d..12ae33c540 100644 --- a/codex-rs/thread-store/src/local/rollout_migration/rollback_plan.rs +++ b/codex-rs/thread-store/src/local/rollout_migration/rollback_plan.rs @@ -312,6 +312,7 @@ impl RollbackPlanner { codex_rollout::RetainedContextEvent::DeliveredAssistantMessage { message, acceptance_order, + .. }, ) => { // Nested calls have no response item; match them to instructions by acceptance order.