diff --git a/codex-rs/app-server/src/external_agent_migration/session_importer.rs b/codex-rs/app-server/src/external_agent_migration/session_importer.rs index 4a05a1d078..dabce8a39f 100644 --- a/codex-rs/app-server/src/external_agent_migration/session_importer.rs +++ b/codex-rs/app-server/src/external_agent_migration/session_importer.rs @@ -490,7 +490,7 @@ impl ExternalAgentSessionImporter { history_mode: ThreadHistoryMode::Legacy, history_base: None, subagent_history_start_ordinal: None, - initial_window_id: uuid::Uuid::now_v7().to_string(), + initial_window_id: codex_protocol::ContextWindowId::new_uuid_v7().to_string(), metadata: ThreadPersistenceMetadata { cwd: Some(cwd.clone()), model_provider: model_provider.clone(), diff --git a/codex-rs/app-server/tests/suite/conversation_summary.rs b/codex-rs/app-server/tests/suite/conversation_summary.rs index aaeed4ec19..cf5bd8ea3a 100644 --- a/codex-rs/app-server/tests/suite/conversation_summary.rs +++ b/codex-rs/app-server/tests/suite/conversation_summary.rs @@ -140,7 +140,7 @@ async fn get_conversation_summary_by_thread_id_reads_pathless_store_thread() -> history_mode: Default::default(), history_base: None, subagent_history_start_ordinal: None, - initial_window_id: Uuid::now_v7().to_string(), + initial_window_id: codex_protocol::ContextWindowId::new_uuid_v7().to_string(), runtime_workspace_roots: None, metadata: ThreadPersistenceMetadata { cwd: None, diff --git a/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs b/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs index 0850c13700..b59ac6e0c0 100644 --- a/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs +++ b/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs @@ -13,6 +13,7 @@ //! is a stop-gap that prevents additional local persistence writes from slipping //! in unnoticed. +use codex_protocol::ContextWindowId; use std::collections::BTreeSet; use std::path::Path; use std::sync::Arc; @@ -300,7 +301,7 @@ async fn thread_list_repeated_cursor_returns_internal_error() -> Result<()> { history_mode: Default::default(), history_base: None, subagent_history_start_ordinal: None, - initial_window_id: Uuid::now_v7().to_string(), + initial_window_id: ContextWindowId::new_uuid_v7().to_string(), runtime_workspace_roots: None, metadata: ThreadPersistenceMetadata { cwd: Some(codex_home.path().to_path_buf()), @@ -548,7 +549,7 @@ async fn thread_delete_with_non_local_thread_store_does_not_create_local_persist history_mode: Default::default(), history_base: None, subagent_history_start_ordinal: None, - initial_window_id: Uuid::now_v7().to_string(), + initial_window_id: ContextWindowId::new_uuid_v7().to_string(), runtime_workspace_roots: None, metadata: ThreadPersistenceMetadata { cwd: Some(codex_home.path().to_path_buf()), diff --git a/codex-rs/app-server/tests/suite/v2/thread_read.rs b/codex-rs/app-server/tests/suite/v2/thread_read.rs index 4daf547939..ff22262fe8 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_read.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_read.rs @@ -63,6 +63,7 @@ use codex_core::ARCHIVED_SESSIONS_SUBDIR; use codex_core::config::ConfigBuilder; use codex_exec_server::EnvironmentManager; use codex_feedback::CodexFeedback; +use codex_protocol::ContextWindowId; use codex_protocol::items::AgentMessageContent; use codex_protocol::items::AgentMessageItem; use codex_protocol::items::TurnItem as CoreTurnItem; @@ -653,7 +654,7 @@ async fn thread_search_occurrences_reads_paginated_projection() -> Result<()> { history_mode: codex_protocol::protocol::ThreadHistoryMode::Paginated, history_base: None, subagent_history_start_ordinal: None, - initial_window_id: Uuid::now_v7().to_string(), + initial_window_id: ContextWindowId::new_uuid_v7().to_string(), runtime_workspace_roots: None, metadata: ThreadPersistenceMetadata { cwd: Some(codex_home.path().to_path_buf()), @@ -1864,7 +1865,7 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items( history_mode: codex_protocol::protocol::ThreadHistoryMode::Paginated, history_base: None, subagent_history_start_ordinal: None, - initial_window_id: Uuid::now_v7().to_string(), + initial_window_id: ContextWindowId::new_uuid_v7().to_string(), runtime_workspace_roots: None, metadata: ThreadPersistenceMetadata { cwd: Some(codex_home.path().to_path_buf()), @@ -2833,7 +2834,7 @@ async fn seed_pathless_store_thread( history_mode: Default::default(), history_base: None, subagent_history_start_ordinal: None, - initial_window_id: Uuid::now_v7().to_string(), + initial_window_id: ContextWindowId::new_uuid_v7().to_string(), runtime_workspace_roots: None, metadata: ThreadPersistenceMetadata { cwd: None, diff --git a/codex-rs/app-server/tests/suite/v2/thread_timeline.rs b/codex-rs/app-server/tests/suite/v2/thread_timeline.rs index f336be27d1..68ebe2be11 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_timeline.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_timeline.rs @@ -37,7 +37,6 @@ use codex_thread_store::ThreadStore; use codex_utils_absolute_path::test_support::PathExt; use pretty_assertions::assert_eq; use tempfile::TempDir; -use uuid::Uuid; #[tokio::test] async fn timeline_pages_mix_items_and_resolve_the_opening_realtime_session() -> Result<()> { @@ -77,7 +76,7 @@ async fn timeline_pages_mix_items_and_resolve_the_opening_realtime_session() -> history_mode: ThreadHistoryMode::Paginated, history_base: None, subagent_history_start_ordinal: None, - initial_window_id: Uuid::now_v7().to_string(), + initial_window_id: codex_protocol::ContextWindowId::new_uuid_v7().to_string(), runtime_workspace_roots: None, metadata: ThreadPersistenceMetadata { cwd: Some(codex_home.path().to_path_buf()), diff --git a/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs b/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs index 2a18e911fd..fff83e6fe5 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_unarchive.rs @@ -265,7 +265,7 @@ async fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()> { history_mode: Default::default(), history_base: None, subagent_history_start_ordinal: None, - initial_window_id: Uuid::now_v7().to_string(), + initial_window_id: codex_protocol::ContextWindowId::new_uuid_v7().to_string(), runtime_workspace_roots: None, metadata: ThreadPersistenceMetadata { cwd: None, diff --git a/codex-rs/core/src/compact_remote_v2_attempt.rs b/codex-rs/core/src/compact_remote_v2_attempt.rs index 72f00f2e30..e10fb74092 100644 --- a/codex-rs/core/src/compact_remote_v2_attempt.rs +++ b/codex-rs/core/src/compact_remote_v2_attempt.rs @@ -64,7 +64,7 @@ pub(crate) struct BackgroundCompactionCandidate { pub(crate) struct BackgroundCompactionInput { prefix: CompactionPrefix, turn_state: Option, - window: (String, u64, uuid::Uuid), + window: (String, u64, codex_protocol::ContextWindowId), base_instructions: BaseInstructions, input: Vec, metadata: Vec>, @@ -283,7 +283,7 @@ pub(crate) async fn capture_background_input( base_instructions: BaseInstructions, turn_state: Option, auth_owner_generation: Option, - window: (String, u64, uuid::Uuid), + window: (String, u64, codex_protocol::ContextWindowId), ) -> BackgroundCompactionInput { let base_instructions = if step_context.settings.model_info.use_responses_lite && snapshot diff --git a/codex-rs/core/src/context/token_budget_context.rs b/codex-rs/core/src/context/token_budget_context.rs index 454d40a606..7aa415526d 100644 --- a/codex-rs/core/src/context/token_budget_context.rs +++ b/codex-rs/core/src/context/token_budget_context.rs @@ -6,6 +6,7 @@ use crate::context::world_state::WorldStateUpdate; use codex_context_fragments::AnnotatedContent; use codex_context_fragments::message_from_parts; use codex_protocol::AgentPath; +use codex_protocol::ContextWindowId; use codex_protocol::models::ContentItem; use codex_protocol::models::ContentItemKind; use codex_protocol::models::ResponseItem; @@ -14,22 +15,21 @@ use codex_protocol::protocol::CONTEXT_WINDOW_CLOSE_TAG; use codex_protocol::protocol::CONTEXT_WINDOW_GUIDANCE_CLOSE_TAG; use codex_protocol::protocol::CONTEXT_WINDOW_GUIDANCE_OPEN_TAG; use codex_protocol::protocol::CONTEXT_WINDOW_OPEN_TAG; -use uuid::Uuid; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct TokenBudgetContext { agent_path: AgentPath, - first_window_id: Uuid, - previous_window_id: Option, - window_id: Uuid, + first_window_id: ContextWindowId, + previous_window_id: Option, + window_id: ContextWindowId, } impl TokenBudgetContext { pub(crate) fn new( agent_path: AgentPath, - first_window_id: Uuid, - previous_window_id: Option, - window_id: Uuid, + first_window_id: ContextWindowId, + previous_window_id: Option, + window_id: ContextWindowId, ) -> Self { Self { agent_path, @@ -73,14 +73,14 @@ impl ContextualUserFragment for TokenBudgetContext { } fn body(&self) -> ContentItem { - let first_window_id = self.first_window_id; - let window_id = self.window_id; + let first_window_id = &self.first_window_id; + let window_id = &self.window_id; let mut lines = vec![ format!("Agent name: {}", self.agent_path), format!("First context window id: {first_window_id}"), format!("Current context window id: {window_id}"), ]; - if let Some(previous_window_id) = self.previous_window_id { + if let Some(previous_window_id) = &self.previous_window_id { lines.push(format!("Previous context window id: {previous_window_id}")); } input_text(format!("\n{}\n", lines.join("\n"))) diff --git a/codex-rs/core/src/responses_metadata.rs b/codex-rs/core/src/responses_metadata.rs index 27bd6e4e0c..8f886779c0 100644 --- a/codex-rs/core/src/responses_metadata.rs +++ b/codex-rs/core/src/responses_metadata.rs @@ -8,6 +8,7 @@ use codex_analytics::CompactionReason; use codex_analytics::CompactionStrategy; use codex_analytics::CompactionTrigger; use codex_git_utils::SanitizedGitUrl; +use codex_protocol::ContextWindowId; use codex_protocol::ThreadId; use codex_protocol::mcp::McpAttribution; use codex_protocol::protocol::SessionSource; @@ -19,7 +20,6 @@ use http::HeaderMap as ApiHeaderMap; use http::HeaderValue; use serde::Serialize; use serde_json::Value; -use uuid::Uuid; use crate::client::X_CODEX_INSTALLATION_ID_HEADER; use crate::client::X_CODEX_PARENT_THREAD_ID_HEADER; @@ -248,7 +248,7 @@ pub struct CodexResponsesMetadata { pub(crate) routing_hint: Option, pub(crate) window_id: String, pub(crate) window_number: Option, - pub(crate) context_window_id: Option, + pub(crate) context_window_id: Option, pub(crate) history_initialization: Option, pub(crate) request_kind: Option, pub(crate) forked_from_thread_id: Option, @@ -439,7 +439,7 @@ impl CodexResponsesMetadata { window_id: has_request_identity.then_some(self.window_id.as_str()), window_number: has_request_identity.then_some(self.window_number).flatten(), context_window_id: has_request_identity - .then_some(self.context_window_id) + .then_some(self.context_window_id.as_ref()) .flatten(), request_kind: request_kind_value, history_initialization: self.history_initialization, @@ -572,7 +572,7 @@ struct CodexTurnMetadataPayload<'a> { #[serde(default, skip_serializing_if = "Option::is_none")] window_number: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - context_window_id: Option, + context_window_id: Option<&'a ContextWindowId>, #[serde(default, skip_serializing_if = "Option::is_none")] request_kind: Option<&'static str>, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/codex-rs/core/src/session/compaction_replay_tests.rs b/codex-rs/core/src/session/compaction_replay_tests.rs new file mode 100644 index 0000000000..8a0e6c5902 --- /dev/null +++ b/codex-rs/core/src/session/compaction_replay_tests.rs @@ -0,0 +1,90 @@ +//! Checks reconstructed history against live compaction and persisted replacement items. +#![cfg(test)] + +use super::*; +use pretty_assertions::assert_eq; + +#[tokio::test] +async fn reconstruct_history_matches_live_compactions() { + let (session, turn_context) = make_session_and_context().await; + let (rollout_items, expected) = sample_rollout(&session, &turn_context).await; + + let reconstruction_turn = session.new_default_turn().await; + let reconstructed = session + .reconstruct_history_from_rollout(reconstruction_turn.as_ref(), &rollout_items) + .await; + + assert_eq!(expected, raw_envelopes(&reconstructed.history)); + assert_eq!(2, reconstructed.window_number); + assert_eq!( + reconstructed + .window_id + .map(|window_id| Uuid::parse_str(&window_id.to_string()) + .unwrap() + .get_version_num()), + Some(7) + ); +} + +#[tokio::test] +async fn reconstruct_history_uses_replacement_history_verbatim() { + let (session, turn_context) = make_session_and_context().await; + let summary_item = ResponseItem::Message { + status: None, + encrypted_content: None, + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "summary".to_string(), + }], + phase: None, + internal_chat_message_metadata_passthrough: Some(InternalChatMessageMetadataPassthrough { + turn_id: Some("compact-turn".to_string()), + ..Default::default() + }), + }; + let replacement_history = vec![ + ResponseItemEnvelope { + item: summary_item.clone(), + metadata: Some(CodexHarnessMetadata::default()), + }, + ResponseItemEnvelope::new(ResponseItem::Message { + status: None, + encrypted_content: None, + id: None, + role: "developer".to_string(), + content: vec![ContentItem::InputText { + text: "stale developer instructions".to_string(), + }], + phase: None, + internal_chat_message_metadata_passthrough: None, + }), + ]; + let first_window_id = ContextWindowId::new_uuid_v7(); + let previous_window_id = ContextWindowId::new_uuid_v7(); + let window_id = ContextWindowId::new_uuid_v7(); + let rollout_items = vec![RolloutItem::Compacted(Box::new(CompactedItem { + message: String::new(), + replacement_history: Some(replacement_history.clone()), + retained_context: None, + guardian_history: None, + mcp_resource_origins: None, + window_number: Some(42), + first_window_id: Some(first_window_id.to_string()), + previous_window_id: Some(previous_window_id.to_string()), + window_id: Some(window_id.to_string()), + compaction_response_id: None, + latest_token_usage_record: None, + resume_metadata: None, + }))]; + + let reconstructed = session + .reconstruct_history_from_rollout(&turn_context, &rollout_items) + .await; + + assert_eq!(reconstructed.history, replacement_history); + assert_eq!(42, reconstructed.window_number); + assert_eq!(Some(first_window_id), reconstructed.first_window_id); + assert_eq!(Some(previous_window_id), reconstructed.previous_window_id); + assert_eq!(Some(window_id), reconstructed.window_id); +} diff --git a/codex-rs/core/src/session/file_audio_tests.rs b/codex-rs/core/src/session/file_audio_tests.rs index a3cff4108b..0e12b5b340 100644 --- a/codex-rs/core/src/session/file_audio_tests.rs +++ b/codex-rs/core/src/session/file_audio_tests.rs @@ -6,12 +6,13 @@ use crate::context_manager::ContextManager; use codex_protocol::models::BaseInstructions; use codex_protocol::models::ResponseInputItem; use codex_protocol::user_input::UserInput; +use codex_protocol_test_support::uuid_v7_item_id_generator_for_test; use codex_utils_output_truncation::TruncationPolicy; use pretty_assertions::assert_eq; #[test] fn history_sums_measured_and_unknown_file_audio() { - let mut history = ContextManager::new(); + let mut history = ContextManager::new(uuid_v7_item_id_generator_for_test()); let item = ResponseInputItem::from( ["short", "unknown"] .map(|file_id| UserInput::Audio { diff --git a/codex-rs/core/src/session/guardian_checkpoint.rs b/codex-rs/core/src/session/guardian_checkpoint.rs index 4ccfef86ab..6df9b2669e 100644 --- a/codex-rs/core/src/session/guardian_checkpoint.rs +++ b/codex-rs/core/src/session/guardian_checkpoint.rs @@ -20,7 +20,10 @@ impl Session { mcp_resource_origins: self.services.mcp_runtime.resource_origin_checkpoint(), window_number: Some(state.auto_compact_window_number()), first_window_id: Some(window_ids.first_window_id.to_string()), - previous_window_id: window_ids.previous_window_id.map(|id| id.to_string()), + previous_window_id: window_ids + .previous_window_id + .as_ref() + .map(ToString::to_string), window_id: Some(window_ids.window_id.to_string()), compaction_response_id: None, latest_token_usage_record: state.latest_token_usage_record.clone(), diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 08e68e45c2..e2b43fec18 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -367,6 +367,7 @@ use codex_mcp::effective_mcp_servers; use codex_otel::SessionTelemetry; use codex_otel::THREAD_STARTED_METRIC; use codex_otel::TelemetryAuthMode; +use codex_protocol::ContextWindowId; use codex_protocol::ResponseItemId; use codex_protocol::approvals::ExecApprovalKind; use codex_protocol::config_types::CollaborationMode; @@ -2017,11 +2018,11 @@ impl Session { state.history.set_world_state_baseline(world_state); } let fallback_ids = state.auto_compact_window_ids(); - let window_id = window_id.unwrap_or(fallback_ids.window_id); + let window_id = window_id.unwrap_or_else(|| fallback_ids.window_id.clone()); state.restore_auto_compact_window( window_number, AutoCompactWindowIds { - first_window_id: first_window_id.unwrap_or(window_id), + first_window_id: first_window_id.unwrap_or_else(|| window_id.clone()), previous_window_id, window_id, }, @@ -4952,7 +4953,7 @@ impl Session { let state = self.state.lock().await; ( state.session_configuration.session_source.clone(), - state.auto_compact_window_ids(), + state.auto_compact_window_ids().clone(), ) }; let separate_guardian_developer_message = @@ -5254,15 +5255,18 @@ impl Session { self.current_window().await.0 } - pub(crate) async fn current_window(&self) -> (String, u64, Uuid) { + pub(crate) async fn current_window(&self) -> (String, u64, ContextWindowId) { let state = self.state.lock().await; self.current_window_from_state(&state) } - pub(crate) fn current_window_from_state(&self, state: &SessionState) -> (String, u64, Uuid) { + pub(crate) fn current_window_from_state( + &self, + state: &SessionState, + ) -> (String, u64, ContextWindowId) { let thread_id = self.thread_id; let window_number = state.auto_compact_window_number(); - let context_window_id = state.auto_compact_window_ids().window_id; + let context_window_id = state.auto_compact_window_ids().window_id.clone(); ( format!("{thread_id}:{window_number}"), window_number, diff --git a/codex-rs/core/src/session/rollout_reconstruction.rs b/codex-rs/core/src/session/rollout_reconstruction.rs index 56d931e03c..134e64991d 100644 --- a/codex-rs/core/src/session/rollout_reconstruction.rs +++ b/codex-rs/core/src/session/rollout_reconstruction.rs @@ -3,11 +3,11 @@ use crate::context::world_state::WorldStateSnapshot; use crate::context_manager::is_user_turn_boundary; use codex_history::ResponseItemEnvelope; use codex_history::TurnAttribution; +use codex_protocol::ContextWindowId; use codex_protocol::protocol::SessionContextWindow; use codex_protocol::protocol::ThreadHistoryMode; use codex_utils_output_truncation::TruncationPolicy; use std::collections::BTreeSet; -use uuid::Uuid; // Return value of `Session::reconstruct_history_from_rollout`, bundling the rebuilt history with // the resume/fork hydration metadata derived from the same replay. @@ -23,17 +23,17 @@ pub(crate) struct RolloutReconstruction { pub(super) turn_attribution: Option, pub(super) world_state_baseline: Option, pub(super) window_number: u64, - pub(super) first_window_id: Option, - pub(super) previous_window_id: Option, - pub(super) window_id: Option, + pub(super) first_window_id: Option, + pub(super) previous_window_id: Option, + pub(super) window_id: Option, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] struct ReconstructedWindow { number: u64, - first_id: Option, - previous_id: Option, - id: Option, + first_id: Option, + previous_id: Option, + id: Option, } #[derive(Debug, Default)] @@ -653,31 +653,31 @@ impl ContextManager { } } -fn parse_uuid_v7(value: &str) -> Option { - Uuid::parse_str(value) - .ok() - .filter(|uuid| uuid.get_version_num() == 7) -} - fn reconstructed_window_from_compaction(compacted: &CompactedItem) -> Option { Some(ReconstructedWindow { number: compacted.window_number?, - first_id: compacted.first_window_id.as_deref().and_then(parse_uuid_v7), + first_id: compacted + .first_window_id + .as_deref() + .and_then(ContextWindowId::parse), previous_id: compacted .previous_window_id .as_deref() - .and_then(parse_uuid_v7), - id: compacted.window_id.as_deref().and_then(parse_uuid_v7), + .and_then(ContextWindowId::parse), + id: compacted + .window_id + .as_deref() + .and_then(ContextWindowId::parse), }) } fn reconstructed_window_from_session_context_window( context_window: &SessionContextWindow, ) -> Option { - let id = parse_uuid_v7(&context_window.window_id)?; + let id = ContextWindowId::parse(&context_window.window_id)?; Some(ReconstructedWindow { number: 0, - first_id: Some(id), + first_id: Some(id.clone()), previous_id: None, id: Some(id), }) diff --git a/codex-rs/core/src/session/rollout_reconstruction_tests.rs b/codex-rs/core/src/session/rollout_reconstruction_tests.rs index 280691b867..3e20cf7301 100644 --- a/codex-rs/core/src/session/rollout_reconstruction_tests.rs +++ b/codex-rs/core/src/session/rollout_reconstruction_tests.rs @@ -10,6 +10,7 @@ use codex_history::InitialHistory; use codex_history::ResponseItemEnvelope; use codex_history::ResumedHistory; use codex_protocol::AgentPath; +use codex_protocol::ContextWindowId; use codex_protocol::ThreadId; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; @@ -30,7 +31,6 @@ use serde_json::json; use std::collections::BTreeMap; use std::path::PathBuf; use test_case::test_case; -use uuid::Uuid; #[tokio::test] async fn recorded_questions_share_queued_input_order_across_resume() { @@ -1444,7 +1444,7 @@ async fn record_initial_history_resumed_does_not_seed_reference_context_item_aft async fn reconstruct_history_restores_initial_window_from_session_meta() { let (session, turn_context) = make_session_and_context().await; let thread_id = ThreadId::default(); - let initial_window_id = Uuid::now_v7(); + let initial_window_id = ContextWindowId::new_uuid_v7(); let rollout_items = vec![RolloutItem::SessionMeta(Box::new(SessionMetaLine { meta: SessionMeta { session_id: thread_id.into(), @@ -1462,7 +1462,10 @@ async fn reconstruct_history_restores_initial_window_from_session_meta() { .await; assert_eq!(reconstructed.window_number, 0); - assert_eq!(reconstructed.first_window_id, Some(initial_window_id)); + assert_eq!( + reconstructed.first_window_id.as_ref(), + Some(&initial_window_id) + ); assert_eq!(reconstructed.previous_window_id, None); assert_eq!(reconstructed.window_id, Some(initial_window_id)); } @@ -1471,10 +1474,10 @@ async fn reconstruct_history_restores_initial_window_from_session_meta() { async fn reconstruct_history_prefers_compacted_window_over_session_meta() { let (session, turn_context) = make_session_and_context().await; let thread_id = ThreadId::default(); - let initial_window_id = Uuid::now_v7(); - let compacted_first_window_id = Uuid::now_v7(); - let compacted_previous_window_id = Uuid::now_v7(); - let compacted_window_id = Uuid::now_v7(); + let initial_window_id = ContextWindowId::new_uuid_v7(); + let compacted_first_window_id = ContextWindowId::new_uuid_v7(); + let compacted_previous_window_id = ContextWindowId::new_uuid_v7(); + let compacted_window_id = ContextWindowId::new_uuid_v7(); let rollout_items = vec![ RolloutItem::SessionMeta(Box::new(SessionMetaLine { meta: SessionMeta { @@ -1593,7 +1596,11 @@ async fn bounded_replay_matches_full_replay_after_empty_turn_compactions( user_message("original task").into(), )], )); - let window_ids = [Uuid::now_v7(), Uuid::now_v7(), Uuid::now_v7()]; + let window_ids = [ + ContextWindowId::new_uuid_v7(), + ContextWindowId::new_uuid_v7(), + ContextWindowId::new_uuid_v7(), + ]; let current = matches!(compaction_format, CompactionFormat::Current); let mut latest_compaction_index = 0; for window_number in 1..=2 { @@ -1961,7 +1968,7 @@ async fn completed_turn_suffix_after_compaction_overrides_resume_metadata( async fn reconstruct_history_preserves_legacy_compaction_count_with_session_meta_window() { let (session, turn_context) = make_session_and_context().await; let thread_id = ThreadId::default(); - let initial_window_id = Uuid::now_v7(); + let initial_window_id = ContextWindowId::new_uuid_v7(); let rollout_items = vec![ RolloutItem::SessionMeta(Box::new(SessionMetaLine { meta: SessionMeta { diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index e94527c80a..7677e92296 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -33,6 +33,7 @@ use codex_http_client::RouteAwareClientPool; use codex_login::auth::AgentIdentityAuthPolicy; use codex_model_provider::SharedModelProvider; use codex_prompts::render_model_instructions; +use codex_protocol::ContextWindowId; use codex_protocol::ItemIdGenerator; use codex_protocol::SessionId; use codex_protocol::capabilities::SelectedCapabilityRoot; @@ -815,7 +816,7 @@ impl Session { &self, turn_context: &TurnContext, compaction_metadata: CompactionTurnMetadata, - (window_id, window_number, context_window_id): (String, u64, uuid::Uuid), + (window_id, window_number, context_window_id): (String, u64, ContextWindowId), ) -> CodexResponsesMetadata { let responses_metadata = turn_context.turn_metadata_state.to_responses_metadata( self.installation_id.clone(), @@ -835,7 +836,7 @@ impl Session { turn_context: &TurnContext, responses_metadata: CodexResponsesMetadata, window_number: u64, - context_window_id: uuid::Uuid, + context_window_id: ContextWindowId, ) -> CodexResponsesMetadata { CodexResponsesMetadata { history_initialization: Some(self.history_initialization), @@ -1631,7 +1632,7 @@ impl Session { session_configuration.thread_name = thread_name.clone(); let mut state = SessionState::new_with_auto_compact_window_ids( session_configuration.clone(), - initial_auto_compact_window_ids, + initial_auto_compact_window_ids.clone(), ContextManager::for_session( session_configuration.root_capabilities_restricted, &config.features, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 9edd5643ec..aaf6e5f8e9 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -1,4 +1,8 @@ use crate::agent::LocalAgentControl; +use codex_protocol::ContextWindowId; +#[path = "compaction_replay_tests.rs"] +mod compaction_replay_tests; + #[path = "notification_tests.rs"] mod notification_tests; @@ -2465,89 +2469,6 @@ fn collect_explicit_app_ids_from_skill_items_skips_plain_mentions_with_skill_con assert_eq!(connector_ids, HashSet::::new()); } -#[tokio::test] -async fn reconstruct_history_matches_live_compactions() { - let (session, turn_context) = make_session_and_context().await; - let (rollout_items, expected) = sample_rollout(&session, &turn_context).await; - - let reconstruction_turn = session.new_default_turn().await; - let reconstructed = session - .reconstruct_history_from_rollout(reconstruction_turn.as_ref(), &rollout_items) - .await; - - assert_eq!(expected, raw_envelopes(&reconstructed.history)); - assert_eq!(2, reconstructed.window_number); - assert_eq!( - reconstructed - .window_id - .map(|window_id| window_id.get_version_num()), - Some(7) - ); -} - -#[tokio::test] -async fn reconstruct_history_uses_replacement_history_verbatim() { - let (session, turn_context) = make_session_and_context().await; - let summary_item = ResponseItem::Message { - status: None, - encrypted_content: None, - id: None, - role: "user".to_string(), - content: vec![ContentItem::InputText { - text: "summary".to_string(), - }], - phase: None, - internal_chat_message_metadata_passthrough: Some(InternalChatMessageMetadataPassthrough { - turn_id: Some("compact-turn".to_string()), - ..Default::default() - }), - }; - let replacement_history = vec![ - ResponseItemEnvelope { - item: summary_item.clone(), - metadata: Some(CodexHarnessMetadata::default()), - }, - ResponseItemEnvelope::new(ResponseItem::Message { - status: None, - encrypted_content: None, - id: None, - role: "developer".to_string(), - content: vec![ContentItem::InputText { - text: "stale developer instructions".to_string(), - }], - phase: None, - internal_chat_message_metadata_passthrough: None, - }), - ]; - let first_window_id = Uuid::now_v7(); - let previous_window_id = Uuid::now_v7(); - let window_id = Uuid::now_v7(); - let rollout_items = vec![RolloutItem::Compacted(Box::new(CompactedItem { - message: String::new(), - replacement_history: Some(replacement_history.clone()), - retained_context: None, - guardian_history: None, - mcp_resource_origins: None, - window_number: Some(42), - first_window_id: Some(first_window_id.to_string()), - previous_window_id: Some(previous_window_id.to_string()), - window_id: Some(window_id.to_string()), - compaction_response_id: None, - latest_token_usage_record: None, - resume_metadata: None, - }))]; - - let reconstructed = session - .reconstruct_history_from_rollout(&turn_context, &rollout_items) - .await; - - assert_eq!(reconstructed.history, replacement_history); - assert_eq!(42, reconstructed.window_number); - assert_eq!(Some(first_window_id), reconstructed.first_window_id); - assert_eq!(Some(previous_window_id), reconstructed.previous_window_id); - assert_eq!(Some(window_id), reconstructed.window_id); -} - #[tokio::test] async fn record_initial_history_reconstructs_resumed_transcript() { let (session, turn_context) = make_session_and_context().await; @@ -5075,7 +4996,7 @@ async fn open_thread_persistence(session: &mut Session) -> PathBuf { history_mode: Default::default(), subagent_history_start_ordinal: None, history_base: None, - initial_window_id: Uuid::now_v7().to_string(), + initial_window_id: ContextWindowId::new_uuid_v7().to_string(), runtime_workspace_roots: None, metadata: ThreadPersistenceMetadata { cwd: Some(config.cwd.to_path_buf()), @@ -8867,7 +8788,7 @@ async fn shutdown_complete_does_not_append_to_thread_store_after_shutdown() { history_mode: Default::default(), subagent_history_start_ordinal: None, history_base: None, - initial_window_id: Uuid::now_v7().to_string(), + initial_window_id: ContextWindowId::new_uuid_v7().to_string(), runtime_workspace_roots: None, metadata: ThreadPersistenceMetadata { cwd: Some(config.cwd.to_path_buf()), @@ -9006,7 +8927,7 @@ async fn submission_loop_channel_close_runs_full_thread_teardown() -> anyhow::Re history_mode: Default::default(), subagent_history_start_ordinal: None, history_base: None, - initial_window_id: Uuid::now_v7().to_string(), + initial_window_id: ContextWindowId::new_uuid_v7().to_string(), runtime_workspace_roots: None, metadata: ThreadPersistenceMetadata { cwd: Some(config.cwd.to_path_buf()), @@ -12008,7 +11929,7 @@ async fn attach_in_memory_thread_store( history_mode: Default::default(), subagent_history_start_ordinal: None, history_base: None, - initial_window_id: Uuid::now_v7().to_string(), + initial_window_id: ContextWindowId::new_uuid_v7().to_string(), runtime_workspace_roots: None, metadata: ThreadPersistenceMetadata { cwd: Some(config.cwd.to_path_buf()), diff --git a/codex-rs/core/src/session/time_reminder.rs b/codex-rs/core/src/session/time_reminder.rs index 92c60389fe..02f1cecb74 100644 --- a/codex-rs/core/src/session/time_reminder.rs +++ b/codex-rs/core/src/session/time_reminder.rs @@ -41,7 +41,7 @@ pub(super) fn apply_persistent_defaults(config: &mut Config) { pub(crate) struct CurrentTimeReminderState { last_delivery_time: Option>, last_window_id: Option, - last_clock_failure: Option<(String, uuid::Uuid)>, + last_clock_failure: Option<(String, codex_protocol::ContextWindowId)>, pending_user_or_tool_output_boundary: bool, } @@ -134,7 +134,7 @@ impl Session { let mut state = self.state.lock().await; let failure = ( turn_context.sub_id.clone(), - state.auto_compact_window_ids().window_id, + state.auto_compact_window_ids().window_id.clone(), ); // A compacted window may no longer contain the earlier notice. if state.current_time_reminder.last_clock_failure.as_ref() == Some(&failure) { diff --git a/codex-rs/core/src/session/world_state.rs b/codex-rs/core/src/session/world_state.rs index 49fef5f3ac..f4c8ee6249 100644 --- a/codex-rs/core/src/session/world_state.rs +++ b/codex-rs/core/src/session/world_state.rs @@ -195,7 +195,7 @@ impl Session { .resolved_context_window() .is_some(); if token_budget_enabled { - let window_ids = self.state.lock().await.auto_compact_window_ids(); + let window_ids = self.state.lock().await.auto_compact_window_ids().clone(); world_state.add_section(TokenBudgetContext::new( turn_context .session_source diff --git a/codex-rs/core/src/state/auto_compact_window.rs b/codex-rs/core/src/state/auto_compact_window.rs index 2571749ed6..600df8c0cd 100644 --- a/codex-rs/core/src/state/auto_compact_window.rs +++ b/codex-rs/core/src/state/auto_compact_window.rs @@ -1,18 +1,18 @@ +use codex_protocol::ContextWindowId; use codex_protocol::protocol::TokenUsage; -use uuid::Uuid; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct AutoCompactWindowIds { - pub(crate) first_window_id: Uuid, - pub(crate) previous_window_id: Option, - pub(crate) window_id: Uuid, + pub(crate) first_window_id: ContextWindowId, + pub(crate) previous_window_id: Option, + pub(crate) window_id: ContextWindowId, } impl AutoCompactWindowIds { pub(crate) fn new_initial() -> Self { - let window_id = Uuid::now_v7(); + let window_id = ContextWindowId::new_uuid_v7(); Self { - first_window_id: window_id, + first_window_id: window_id.clone(), previous_window_id: None, window_id, } @@ -30,7 +30,7 @@ enum AutoCompactWindowPrefill { Estimated(i64), } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub(super) struct AutoCompactWindow { window_number: u64, ids: AutoCompactWindowIds, @@ -65,8 +65,8 @@ impl AutoCompactWindow { self.window_number } - pub(super) fn ids(&self) -> AutoCompactWindowIds { - self.ids + pub(super) fn ids(&self) -> &AutoCompactWindowIds { + &self.ids } pub(super) fn restore(&mut self, window_number: u64, ids: AutoCompactWindowIds) { @@ -76,12 +76,14 @@ impl AutoCompactWindow { pub(super) fn advance(&mut self) -> (u64, AutoCompactWindowIds) { self.window_number = self.window_number.saturating_add(1); - self.ids.previous_window_id = Some(self.ids.window_id); - self.ids.window_id = Uuid::now_v7(); + self.ids.previous_window_id = Some(std::mem::replace( + &mut self.ids.window_id, + ContextWindowId::new_uuid_v7(), + )); self.new_context_window_requested = false; self.token_budget_reminder_delivered = false; self.auto_compact_fallback_delivered = false; - (self.window_number, self.ids) + (self.window_number, self.ids.clone()) } pub(super) fn claim_token_budget_reminder(&mut self) -> bool { @@ -151,25 +153,30 @@ mod tests { let mut window = AutoCompactWindow::new_with_ids(AutoCompactWindowIds::new_initial()); assert_eq!(window.window_number(), 0); - let initial_window_id = window.ids().window_id; - assert_eq!(initial_window_id.get_version_num(), 7); + let initial_window_id = window.ids().window_id.clone(); + assert_eq!( + uuid::Uuid::parse_str(&initial_window_id.to_string()) + .unwrap() + .get_version_num(), + 7 + ); assert_eq!( window.ids(), - AutoCompactWindowIds { - first_window_id: initial_window_id, + &AutoCompactWindowIds { + first_window_id: initial_window_id.clone(), previous_window_id: None, - window_id: initial_window_id, + window_id: initial_window_id.clone(), } ); let first_window_id = initial_window_id; - let restored_window_id = Uuid::now_v7(); - let restored_previous_window_id = Uuid::now_v7(); + let restored_window_id = ContextWindowId::new_uuid_v7(); + let restored_previous_window_id = ContextWindowId::new_uuid_v7(); window.restore( /*window_number*/ 3, AutoCompactWindowIds { - first_window_id, + first_window_id: first_window_id.clone(), previous_window_id: Some(restored_previous_window_id), - window_id: restored_window_id, + window_id: restored_window_id.clone(), }, ); assert_eq!(window.window_number(), 3); @@ -185,10 +192,15 @@ mod tests { let (window_number, ids) = window.advance(); assert_eq!(window_number, 4); assert_eq!(window.window_number(), 4); - assert_eq!(window.ids(), ids); + assert_eq!(window.ids(), &ids); assert_eq!(ids.first_window_id, first_window_id); - assert_eq!(ids.previous_window_id, Some(restored_window_id)); - assert_eq!(ids.window_id.get_version_num(), 7); + assert_eq!(ids.previous_window_id, Some(restored_window_id.clone())); + assert_eq!( + uuid::Uuid::parse_str(&ids.window_id.to_string()) + .unwrap() + .get_version_num(), + 7 + ); assert_ne!(ids.window_id, restored_window_id); assert!(!window.take_new_context_window_request()); assert!(window.claim_token_budget_reminder()); diff --git a/codex-rs/core/src/state/session.rs b/codex-rs/core/src/state/session.rs index 972cf5092b..a9853cf486 100644 --- a/codex-rs/core/src/state/session.rs +++ b/codex-rs/core/src/state/session.rs @@ -337,7 +337,7 @@ impl SessionState { self.auto_compact_window.window_number() } - pub(crate) fn auto_compact_window_ids(&self) -> AutoCompactWindowIds { + pub(crate) fn auto_compact_window_ids(&self) -> &AutoCompactWindowIds { self.auto_compact_window.ids() } diff --git a/codex-rs/protocol/src/context_window_id.rs b/codex-rs/protocol/src/context_window_id.rs new file mode 100644 index 0000000000..38e77f4a94 --- /dev/null +++ b/codex-rs/protocol/src/context_window_id.rs @@ -0,0 +1,30 @@ +//! Context window identity used for compaction, rollout restoration, and request metadata. + +use serde::Serialize; +use std::fmt; +use uuid::Uuid; + +#[derive(derive_more::Debug, Clone, PartialEq, Eq, Serialize)] +#[debug("{_0}")] +#[serde(transparent)] +pub struct ContextWindowId(String); + +impl ContextWindowId { + pub fn new_uuid_v7() -> Self { + Self(Uuid::now_v7().to_string()) + } + + /// Reads a persisted window ID, ignoring IDs outside the supported format. + pub fn parse(value: &str) -> Option { + Uuid::parse_str(value) + .ok() + .filter(|uuid| uuid.get_version_num() == 7) + .map(|uuid| Self(uuid.to_string())) + } +} + +impl fmt::Display for ContextWindowId { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(formatter) + } +} diff --git a/codex-rs/protocol/src/lib.rs b/codex-rs/protocol/src/lib.rs index e399d52d5c..8c7442499d 100644 --- a/codex-rs/protocol/src/lib.rs +++ b/codex-rs/protocol/src/lib.rs @@ -1,6 +1,7 @@ pub mod account; mod agent_path; pub mod auth; +mod context_window_id; mod item_id_generator; mod response_item_id; mod response_usage; @@ -9,6 +10,7 @@ mod session_id; mod thread_id; mod tool_name; pub use agent_path::AgentPath; +pub use context_window_id::ContextWindowId; pub use item_id_generator::ItemIdGenerator; #[cfg(test)] use item_id_generator::uuid_v7_item_id_generator_for_test; diff --git a/codex-rs/rollout/src/recorder_tests.rs b/codex-rs/rollout/src/recorder_tests.rs index eac55e6fbb..b1d3e7b04f 100644 --- a/codex-rs/rollout/src/recorder_tests.rs +++ b/codex-rs/rollout/src/recorder_tests.rs @@ -698,7 +698,7 @@ async fn recorder_materializes_on_flush_with_pending_items() -> std::io::Result< let config = test_config(home.path()); let session_id = SessionId::default(); let thread_id = ThreadId::new(); - let initial_window_id = Uuid::now_v7().to_string(); + let initial_window_id = codex_protocol::ContextWindowId::new_uuid_v7().to_string(); let recorder = RolloutRecorder::new( &config, RolloutRecorderParams::new( diff --git a/codex-rs/thread-store/src/in_memory.rs b/codex-rs/thread-store/src/in_memory.rs index 137d0dda0c..049e89cafc 100644 --- a/codex-rs/thread-store/src/in_memory.rs +++ b/codex-rs/thread-store/src/in_memory.rs @@ -64,6 +64,7 @@ mod tests { use crate::StoredTurnItemsView; use crate::ThreadPersistenceMetadata; use crate::ThreadSortKey; + use codex_protocol::ContextWindowId; use codex_protocol::models::BaseInstructions; use codex_protocol::protocol::SessionSource; @@ -184,7 +185,7 @@ mod tests { history_mode: ThreadHistoryMode::Legacy, history_base: None, subagent_history_start_ordinal: None, - initial_window_id: uuid::Uuid::now_v7().to_string(), + initial_window_id: ContextWindowId::new_uuid_v7().to_string(), runtime_workspace_roots: None, metadata: ThreadPersistenceMetadata { cwd: None, @@ -461,7 +462,7 @@ mod tests { history_mode, history_base: None, subagent_history_start_ordinal: None, - initial_window_id: uuid::Uuid::now_v7().to_string(), + initial_window_id: ContextWindowId::new_uuid_v7().to_string(), runtime_workspace_roots: None, metadata: thread_metadata(), } diff --git a/codex-rs/thread-store/src/local/mod.rs b/codex-rs/thread-store/src/local/mod.rs index 23f533d9f7..f2e7c3ea6e 100644 --- a/codex-rs/thread-store/src/local/mod.rs +++ b/codex-rs/thread-store/src/local/mod.rs @@ -2393,7 +2393,7 @@ mod tests { history_mode: ThreadHistoryMode::Legacy, history_base: None, subagent_history_start_ordinal: None, - initial_window_id: uuid::Uuid::now_v7().to_string(), + initial_window_id: codex_protocol::ContextWindowId::new_uuid_v7().to_string(), runtime_workspace_roots: None, metadata: thread_metadata(), } diff --git a/codex-rs/thread-store/src/local/pending_thread_metadata_tests.rs b/codex-rs/thread-store/src/local/pending_thread_metadata_tests.rs index a81eb389a7..d6973cf3c5 100644 --- a/codex-rs/thread-store/src/local/pending_thread_metadata_tests.rs +++ b/codex-rs/thread-store/src/local/pending_thread_metadata_tests.rs @@ -600,7 +600,7 @@ fn create_thread_params(thread_id: ThreadId) -> CreateThreadParams { history_mode: ThreadHistoryMode::Legacy, history_base: None, subagent_history_start_ordinal: None, - initial_window_id: uuid::Uuid::now_v7().to_string(), + initial_window_id: codex_protocol::ContextWindowId::new_uuid_v7().to_string(), runtime_workspace_roots: None, metadata: ThreadPersistenceMetadata { cwd: Some(std::env::current_dir().expect("cwd")), diff --git a/codex-rs/thread-store/src/thread_metadata_sync.rs b/codex-rs/thread-store/src/thread_metadata_sync.rs index 6f1a7d1791..f5c3191ba1 100644 --- a/codex-rs/thread-store/src/thread_metadata_sync.rs +++ b/codex-rs/thread-store/src/thread_metadata_sync.rs @@ -504,7 +504,7 @@ mod tests { history_mode: ThreadHistoryMode::Legacy, history_base: None, subagent_history_start_ordinal: None, - initial_window_id: uuid::Uuid::now_v7().to_string(), + initial_window_id: codex_protocol::ContextWindowId::new_uuid_v7().to_string(), runtime_workspace_roots: None, metadata: ThreadPersistenceMetadata { cwd: None,