From 48524f337a84b570f0c2453ca43ec9a1a29375c0 Mon Sep 17 00:00:00 2001 From: jif Date: Wed, 4 Nov 2026 12:49:34 +0000 Subject: [PATCH] Preserve pending agent messages across compaction (#59404) ## Why Agent messages that arrive after a model request starts must survive compaction until a normal inference request includes them. Compactor output alone does not establish that those messages were delivered. ## What changed - Persist history sequences and sampling checkpoints in harness metadata, capturing the input prefix for each normal inference request. - Retain pending agent messages within existing size budgets during local and remote compaction, placing the summary before pending messages and subsequent caller input. - Preserve sampling progress across history replacement and resume, and drain queued mail after interrupted sampling even when compaction leaves context above the token limit. ## Testing Add regression coverage for request-boundary sampling checkpoints, pending messages across local compaction and repeated remote compaction with resume, and injected messages that interrupt sampling before mid-turn compaction. GitOrigin-RevId: 26c04ab96b5a1c3cc1d6625ccc74a0f00201d14c --- .../app-server-exports-stable.json.zst | Bin 166985 -> 167079 bytes .../tests/suite/v2/thread_inject_items.rs | 10 +- codex-rs/core/src/compact.rs | 29 ++++ codex-rs/core/src/compact_pending_input.rs | 49 +++++++ codex-rs/core/src/compact_remote_v2.rs | 15 ++- codex-rs/core/src/context_manager/history.rs | 15 ++- .../core/src/context_manager/history_tests.rs | 43 +++++- codex-rs/core/src/context_manager/mod.rs | 2 + codex-rs/core/src/context_manager/sampling.rs | 76 +++++++++++ codex-rs/core/src/lib.rs | 1 + .../src/session/compaction_replay_tests.rs | 7 +- codex-rs/core/src/session/inject_tests.rs | 5 +- codex-rs/core/src/session/mod.rs | 2 + .../session/rollout_reconstruction_tests.rs | 27 +++- codex-rs/core/src/session/step_context.rs | 2 + codex-rs/core/src/session/tests.rs | 25 +++- codex-rs/core/src/session/turn.rs | 17 ++- codex-rs/core/src/stream_events_utils.rs | 13 +- codex-rs/core/tests/suite/compact.rs | 15 +++ codex-rs/core/tests/suite/image_rollout.rs | 2 +- .../tests/suite/scenarios_compaction_tests.rs | 100 ++++++++------ .../suite/scenarios_injected_agent_mail.rs | 126 ++++++++++++++++++ ...t__manual_compact_with_history_shapes.snap | 13 +- ...ios__compaction__remote_slash_compact.snap | 56 ++++++-- ...sampling_survives_mid_turn_compaction.snap | 93 +++++++++++++ codex-rs/history/src/lib.rs | 9 ++ codex-rs/history/src/tests.rs | 6 + 27 files changed, 671 insertions(+), 87 deletions(-) create mode 100644 codex-rs/core/src/compact_pending_input.rs create mode 100644 codex-rs/core/src/context_manager/sampling.rs create mode 100644 codex-rs/core/tests/suite/snapshots/all__suite__scenarios__injected_agent_mail__injected_agent_message_interrupting_sampling_survives_mid_turn_compaction.snap 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 aaed74c052f89e39b0a76773d2aefebf5428466f..0b19013af9dd2444078cf6999a927f2c12c88e02 100644 GIT binary patch delta 23043 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� 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 WMNe_c6XXc<-tsRWQ$E{d;j{$D79KPJ delta 22948 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 diff --git a/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs b/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs index 2230e6f2da..95ad84b2c4 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs @@ -253,7 +253,7 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history( .collect::>(); assert_eq!( persisted_additional_context, - vec![("developer", Some(true)), ("user", None)] + vec![("developer", Some(true)), ("user", Some(false))] ); let injected_value = serde_json::to_value(&injected_item)?; @@ -616,7 +616,13 @@ async fn thread_inject_items_adds_raw_response_items_after_a_turn() -> Result<() _ => None, }) .context("injected developer item should be persisted")?; - assert_eq!(persisted_developer_item.metadata, None); + assert_eq!( + persisted_developer_item.metadata, + Some(serde_json::from_value(json!({ + "history_sequence": 5, + "sampled_through_sequence": 3, + }))?) + ); let second_turn_req = mcp .send_turn_start_request(TurnStartParams { diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index 7203b70f16..91e7571270 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -6,6 +6,7 @@ use std::time::Instant; use crate::Prompt; use crate::client::ModelClientSession; use crate::client_common::ResponseEvent; +use crate::compact_pending_input::is_retained_pending_mail; use crate::context::BaseInstructionsFragment; use crate::context::CompactionSummary; use crate::context::ContextualUserFragment; @@ -414,6 +415,34 @@ async fn run_compact_task_inner_impl( // belongs to this compaction turn. summary_item.set_turn_id_if_missing(&turn_context.sub_id); } + // Use the final compactor input, so an overflow retry cannot restore discarded input. + let pending_sequence = + crate::compact_pending_input::pending_mail_sequence(history.annotated_items()); + if let Some(first) = pending_sequence { + let summary = new_history + .pop() + .ok_or_else(|| CodexErr::Fatal("local compaction summary is missing".to_string()))?; + new_history.extend( + history + .annotated_items() + .iter() + .filter(|envelope| is_retained_pending_mail(envelope, first)) + .cloned(), + ); + new_history.sort_by_key(|envelope| { + envelope + .metadata + .as_ref() + .and_then(|metadata| metadata.history_sequence) + }); + new_history = crate::compact_remote_v2::truncate_retained_messages_for_remote_compaction( + new_history, + COMPACT_USER_MESSAGE_MAX_TOKENS, + &history.file_audio_durations, + ); + new_history = + crate::compact_pending_input::insert_summary(new_history, summary, pending_sequence); + } let (window_number, window_ids) = sess.advance_auto_compact_window().await; let (new_history, world_state_baseline) = build_compaction_replacement_history( diff --git a/codex-rs/core/src/compact_pending_input.rs b/codex-rs/core/src/compact_pending_input.rs new file mode 100644 index 0000000000..6d5149828b --- /dev/null +++ b/codex-rs/core/src/compact_pending_input.rs @@ -0,0 +1,49 @@ +//! Retains bounded unsampled mail and positions summaries before it and subsequent caller input. +//! Delivery is proven only by a regular inference checkpoint, never by compactor output. + +use codex_history::ResponseItemEnvelope; +use codex_protocol::models::ResponseItem; + +use crate::context_manager::estimate_item_token_count; +use crate::context_manager::sampled_through; + +pub(crate) const MAX_RETAINED_AGENT_MESSAGE_TOKENS: i64 = 10_000; + +pub(crate) fn pending_mail_sequence(history: &[ResponseItemEnvelope]) -> Option { + let sampled_through = sampled_through(history); + history + .iter() + .filter(|envelope| matches!(envelope.item, ResponseItem::AgentMessage { .. })) + .filter_map(|envelope| envelope.metadata.as_ref()?.history_sequence) + .find(|sequence| *sequence > sampled_through) +} + +pub(crate) fn is_retained_pending_mail(envelope: &ResponseItemEnvelope, first: u64) -> bool { + matches!(envelope.item, ResponseItem::AgentMessage { .. }) + && envelope + .metadata + .as_ref() + .and_then(|metadata| metadata.history_sequence) + .is_some_and(|sequence| sequence >= first) + && estimate_item_token_count(&envelope.item) <= MAX_RETAINED_AGENT_MESSAGE_TOKENS +} + +pub(crate) fn insert_summary( + mut retained: Vec, + summary: ResponseItemEnvelope, + pending_sequence: Option, +) -> Vec { + let position = pending_sequence + .and_then(|first| { + retained.iter().position(|envelope| { + envelope + .metadata + .as_ref() + .and_then(|metadata| metadata.history_sequence) + .is_some_and(|sequence| sequence >= first) + }) + }) + .unwrap_or(retained.len()); + retained.insert(position, summary); + retained +} diff --git a/codex-rs/core/src/compact_remote_v2.rs b/codex-rs/core/src/compact_remote_v2.rs index 7e1fa4668d..dd9b289780 100644 --- a/codex-rs/core/src/compact_remote_v2.rs +++ b/codex-rs/core/src/compact_remote_v2.rs @@ -15,6 +15,8 @@ use crate::compact::CompactionAnalyticsDetails; use crate::compact::compaction_status_from_result; use crate::compact_model_fallback::record_model_fallback; use crate::compact_model_fallback::should_retry_with_current_model; +use crate::compact_pending_input::MAX_RETAINED_AGENT_MESSAGE_TOKENS; +use crate::compact_pending_input::is_retained_pending_mail; use crate::compact_remote_history::HistoryItemGroup; use crate::compact_remote_history::history_item_groups; use crate::context::world_state::WorldState; @@ -88,7 +90,6 @@ enum RetainedImageBudget { } pub(crate) const RETAINED_MESSAGE_TOKEN_BUDGET: usize = 64_000; -const MAX_RETAINED_AGENT_MESSAGE_TOKENS: i64 = 10_000; // Compact attempts can run much longer than normal turns, so keep the per-transport // retry budget smaller than the general Responses stream retry budget. const MAX_REMOTE_COMPACTION_V2_STREAM_RETRIES: u64 = 2; @@ -507,11 +508,16 @@ fn build_v2_compacted_history( crate::context_manager::rejected_audio::checkpoint_input_has_no_file_error_sources( &prompt_input, ); + let pending_sequence = crate::compact_pending_input::pending_mail_sequence(&prompt_input); let retained = v2_history_item_groups(prompt_input) - .filter(|group| is_retained_for_remote_compaction_v2(&group.source, features)) + .filter(|group| { + is_retained_for_remote_compaction_v2(&group.source, features) + || pending_sequence + .is_some_and(|first| is_retained_pending_mail(&group.source, first)) + }) .flat_map(HistoryItemGroup::into_items) .collect::>(); - let mut retained = truncate_retained_messages( + let retained = truncate_retained_messages( retained, RETAINED_MESSAGE_TOKEN_BUDGET, image_budget, @@ -528,7 +534,8 @@ fn build_v2_compacted_history( .get_or_insert_default() .compaction_without_file_error_sources = true; } - retained.push(checkpoint); + let retained = + crate::compact_pending_input::insert_summary(retained, checkpoint, pending_sequence); (retained, retained_image_count) } diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index 9ee9da0869..d1c8edf070 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -102,6 +102,7 @@ fn next_guardian_review_context_revision() -> u64 { #[derive(Clone)] pub(crate) struct ContextManager { item_id_generator: ItemIdGenerator, + sampling: super::sampling::SamplingProgress, /// The oldest items are at the beginning of the vector. Snapshots share the vector until a /// caller needs to mutate it, avoiding deep copies for read-only history consumers. items: Arc>, @@ -313,6 +314,7 @@ impl ContextManager { pub(crate) fn new(item_id_generator: ItemIdGenerator) -> Self { Self { item_id_generator, + sampling: Default::default(), items: Arc::new(Vec::new()), review_history: None, retained_context: Arc::default(), @@ -622,6 +624,7 @@ impl ContextManager { policy: TruncationPolicy, ) { for envelope in items { + self.sampling.record(envelope); if let Some(source) = self.record_item_with_metadata(&envelope.item, envelope.metadata.as_ref(), policy) { @@ -665,6 +668,7 @@ impl ContextManager { item: item.clone(), metadata: metadata.cloned(), }; + self.sampling.record(&mut processed); if let ResponseItem::FunctionCallOutput { output, .. } | ResponseItem::CustomToolCallOutput { output, .. } = &mut processed.item { @@ -808,6 +812,14 @@ impl ContextManager { self.history_version } + pub(crate) fn input_sequence(&self) -> u64 { + self.sampling.last_sequence + } + + pub(crate) fn checkpoint_sampling(&mut self, items: &mut [ResponseItemEnvelope]) { + self.sampling.checkpoint(items); + } + // Estimate token usage using byte-based heuristics from the truncation helpers. // This is a coarse lower bound, not a tokenizer-accurate count. pub(crate) fn estimate_token_count(&self, turn_context: &TurnContext) -> Option { @@ -865,7 +877,8 @@ impl ContextManager { self.replace_annotated(items.into_iter().map(ResponseItemEnvelope::new).collect()); } - pub(crate) fn replace_annotated(&mut self, items: Vec) { + pub(crate) fn replace_annotated(&mut self, mut items: Vec) { + self.sampling = super::sampling::SamplingProgress::restore(&mut items); self.retained_context = Arc::default(); self.user_message_revision = self.user_message_revision.saturating_add(1); if let Some(review_history) = &mut self.review_history { diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index cdf79822a7..e667b587c9 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -61,6 +61,30 @@ const EXEC_FORMAT_MAX_BYTES: usize = 10_000; const EXEC_FORMAT_MAX_TOKENS: usize = 2_500; const TEST_WAV_SAMPLE_RATE: u32 = 8_000; +#[test] +fn sampling_checkpoint_excludes_input_appended_during_request() { + let mut history = create_history_with_items(vec![user_msg("included in the request")]); + let request_sequence = history.input_sequence(); + history.record_items( + [&user_msg("arrived while sampling")], + TruncationPolicy::Tokens(10_000), + ); + history.record_annotated_items( + &mut [ResponseItemEnvelope { + item: assistant_msg("response to the earlier request"), + metadata: Some(CodexHarnessMetadata { + sampled_through_sequence: Some(request_sequence), + ..Default::default() + }), + }], + TruncationPolicy::Tokens(10_000), + ); + assert_eq!( + crate::context_manager::sampled_through(history.annotated_items()), + request_sequence, + ); +} + fn unknown_content_metadata() -> InternalChatMessageMetadataPassthrough { InternalChatMessageMetadataPassthrough { content_item_kinds: Some(vec![ContentItemKind("unknown".to_string())]), @@ -926,6 +950,8 @@ fn retains_only_harness_authored_configuration_updates() { let trusted = ResponseItemEnvelope { item: update.clone(), metadata: Some(CodexHarnessMetadata { + history_sequence: Some(4), + sampled_through_sequence: Some(0), harness_authored_configuration: true, ..Default::default() }), @@ -991,6 +1017,7 @@ fn drop_last_n_user_turns_removes_post_input_configuration_update_with_its_turn( ResponseItemEnvelope::new(assistant_msg("removed answer")), ]); history.record_annotated_items(&mut items, TruncationPolicy::Tokens(10_000)); + let surviving = &items[..surviving.len()]; history.drop_last_n_user_turns(/*num_turns*/ 1); @@ -1148,7 +1175,11 @@ fn cloned_history_shares_items_until_mutated() { fn annotated_history_apis_preserve_envelopes() { let first_envelope = ResponseItemEnvelope { item: assistant_msg("first"), - metadata: Some(CodexHarnessMetadata::default()), + metadata: Some(CodexHarnessMetadata { + history_sequence: Some(7), + sampled_through_sequence: Some(4), + ..Default::default() + }), }; let mut history = ContextManager::default(); @@ -1238,7 +1269,11 @@ async fn for_prompt_annotated_preserves_metadata_while_normalizing_item() { phase: None, internal_chat_message_metadata_passthrough: None, }, - metadata: Some(CodexHarnessMetadata::default()), + metadata: Some(CodexHarnessMetadata { + history_sequence: Some(7), + sampled_through_sequence: Some(4), + ..Default::default() + }), }; let mut history = ContextManager::default(); history.replace_annotated(vec![envelope.clone()]); @@ -1395,7 +1430,9 @@ async fn for_prompt_replaces_unavailable_message_images() { } })) .unwrap(); - assert_eq!(projected, vec![ResponseItemEnvelope::new(expected)]); + let mut expected_envelope = history.annotated_items()[0].clone(); + expected_envelope.item = expected; + assert_eq!(projected, vec![expected_envelope]); assert_eq!(raw_items(&history), vec![original]); } diff --git a/codex-rs/core/src/context_manager/mod.rs b/codex-rs/core/src/context_manager/mod.rs index 9149702d9e..67be01956c 100644 --- a/codex-rs/core/src/context_manager/mod.rs +++ b/codex-rs/core/src/context_manager/mod.rs @@ -1,6 +1,7 @@ mod history; mod image_availability; mod normalize; +mod sampling; pub(crate) mod updates; pub(crate) use history::ContextManager; @@ -11,3 +12,4 @@ pub(crate) use history::estimate_item_token_count_with_audio; pub(crate) use history::is_user_turn_boundary; pub(crate) use history::rejected_audio; pub(crate) use image_availability::replace_unresolvable_image_files; +pub(crate) use sampling::sampled_through; diff --git a/codex-rs/core/src/context_manager/sampling.rs b/codex-rs/core/src/context_manager/sampling.rs new file mode 100644 index 0000000000..a576e91b6e --- /dev/null +++ b/codex-rs/core/src/context_manager/sampling.rs @@ -0,0 +1,76 @@ +//! Tracks the prefix included in normal inference, independently of later history appends. +//! These checkpoints are harness metadata; compaction and replay never acknowledge new input. + +use codex_history::ResponseItemEnvelope; + +#[derive(Debug, Clone, Default)] +pub(super) struct SamplingProgress { + pub(super) last_sequence: u64, + sampled_through: u64, +} + +impl SamplingProgress { + pub(super) fn record(&mut self, envelope: &mut ResponseItemEnvelope) { + let metadata = envelope.metadata.get_or_insert_default(); + let sequence = metadata + .history_sequence + .get_or_insert_with(|| self.last_sequence.strict_add(/* rhs */ 1)); + self.last_sequence = self.last_sequence.max(*sequence); + self.sampled_through = self + .sampled_through + .max(metadata.sampled_through_sequence.unwrap_or_default()); + metadata.sampled_through_sequence = Some(self.sampled_through); + } + + pub(super) fn restore(items: &mut [ResponseItemEnvelope]) -> Self { + let mut progress = Self::default(); + for metadata in items.iter().filter_map(|item| item.metadata.as_ref()) { + progress.last_sequence = progress + .last_sequence + .max(metadata.history_sequence.unwrap_or_default()); + progress.sampled_through = progress + .sampled_through + .max(metadata.sampled_through_sequence.unwrap_or_default()); + } + progress.last_sequence = progress.last_sequence.max(progress.sampled_through); + // Legacy inputs have no delivery proof. Give them distinct identities and keep + // them pending until a normal request includes them; never guess from output order. + progress.assign_missing_sequences(items); + progress + } + + pub(super) fn checkpoint(&mut self, items: &mut [ResponseItemEnvelope]) { + self.assign_missing_sequences(items); + if let Some(first) = items.first_mut() { + first + .metadata + .get_or_insert_default() + .sampled_through_sequence = Some(self.sampled_through); + } + } + + fn assign_missing_sequences(&mut self, items: &mut [ResponseItemEnvelope]) { + for envelope in items { + if envelope + .metadata + .as_ref() + .and_then(|m| m.history_sequence) + .is_none() + { + self.record(envelope); + } + } + } +} + +pub(crate) fn sampled_through(items: &[ResponseItemEnvelope]) -> u64 { + items + .iter() + .filter_map(|item| { + item.metadata + .as_ref() + .and_then(|metadata| metadata.sampled_through_sequence) + }) + .max() + .unwrap_or_default() +} diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 26940e692d..ea0c5c00bb 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -36,6 +36,7 @@ pub use thread_input::InputWaitOutcome; mod turn_extension_data; pub use turn_extension_data::WithTurnExtensionData; mod compact_model_fallback; +mod compact_pending_input; mod compact_remote_history; mod compact_remote_v2; mod compact_retained_tool_outputs; diff --git a/codex-rs/core/src/session/compaction_replay_tests.rs b/codex-rs/core/src/session/compaction_replay_tests.rs index 8a0e6c5902..86ac0a17fd 100644 --- a/codex-rs/core/src/session/compaction_replay_tests.rs +++ b/codex-rs/core/src/session/compaction_replay_tests.rs @@ -43,7 +43,7 @@ async fn reconstruct_history_uses_replacement_history_verbatim() { ..Default::default() }), }; - let replacement_history = vec![ + let mut replacement_history = vec![ ResponseItemEnvelope { item: summary_item.clone(), metadata: Some(CodexHarnessMetadata::default()), @@ -82,6 +82,11 @@ async fn reconstruct_history_uses_replacement_history_verbatim() { .reconstruct_history_from_rollout(&turn_context, &rollout_items) .await; + for (index, envelope) in replacement_history.iter_mut().enumerate() { + let metadata = envelope.metadata.get_or_insert_default(); + metadata.history_sequence = Some(index as u64 + 1); + metadata.sampled_through_sequence = Some(0); + } assert_eq!(reconstructed.history, replacement_history); assert_eq!(42, reconstructed.window_number); assert_eq!(Some(first_window_id), reconstructed.first_window_id); diff --git a/codex-rs/core/src/session/inject_tests.rs b/codex-rs/core/src/session/inject_tests.rs index b8a67a986a..af12838332 100644 --- a/codex-rs/core/src/session/inject_tests.rs +++ b/codex-rs/core/src/session/inject_tests.rs @@ -33,7 +33,10 @@ async fn harness_authored_configuration_updates_preserve_metadata_and_resume() { ) .await; - expected.metadata.as_mut().unwrap().mcp_attribution = Some( + let metadata = expected.metadata.as_mut().unwrap(); + metadata.history_sequence = Some(1); + metadata.sampled_through_sequence = Some(0); + metadata.mcp_attribution = Some( session .services .executed_tool_calls diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index e2b43fec18..a88383e738 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -4461,6 +4461,7 @@ impl Session { } let (executor_hook_sources, mcp, tool_router, extension_data) = prepared_tools??; Ok(Arc::new(StepContext { + sampled_through_sequence: None, serving_assessment: None, preempt: turn_context .config @@ -4773,6 +4774,7 @@ impl Session { .cloned(), ); } + state.history.checkpoint_sampling(&mut items); let replacement_history = items.clone(); state .history diff --git a/codex-rs/core/src/session/rollout_reconstruction_tests.rs b/codex-rs/core/src/session/rollout_reconstruction_tests.rs index 3e20cf7301..4cd1b144f1 100644 --- a/codex-rs/core/src/session/rollout_reconstruction_tests.rs +++ b/codex-rs/core/src/session/rollout_reconstruction_tests.rs @@ -235,7 +235,19 @@ fn assistant_message(text: &str) -> ResponseItem { } fn annotated(items: Vec) -> Vec { - items.into_iter().map(ResponseItemEnvelope::new).collect() + // Legacy replay assigns append identities without claiming prior sampling. + items + .into_iter() + .enumerate() + .map(|(index, item)| ResponseItemEnvelope { + item, + metadata: Some(codex_history::CodexHarnessMetadata { + history_sequence: Some(index as u64 + 1), + sampled_through_sequence: Some(0), + ..Default::default() + }), + }) + .collect() } fn inter_agent_assistant_message(text: &str) -> ResponseItem { @@ -1614,11 +1626,11 @@ async fn bounded_replay_matches_full_replay_after_empty_turn_compactions( vec![ RolloutItem::Compacted(Box::new(CompactedItem { message: String::new(), - replacement_history: Some(annotated(vec![object!({ + replacement_history: Some(vec![ResponseItemEnvelope::new(object!({ "type": "compaction", "id": format!("checkpoint-{window_number}"), "encrypted_content": format!("summary-{window_number}"), - })])), + }))]), retained_context: None, guardian_history: Some(codex_history::GuardianHistoryCheckpoint(vec![ user_message("original task").into(), @@ -1701,7 +1713,14 @@ async fn bounded_replay_matches_full_replay_after_empty_turn_compactions( bounded.guardian_history.as_ref(), Some(&codex_history::GuardianHistoryCheckpoint(vec![ user_message("original task").into(), - assistant_message("continued working").into() + ResponseItemEnvelope { + item: assistant_message("continued working"), + metadata: Some(codex_history::CodexHarnessMetadata { + history_sequence: Some(2), + sampled_through_sequence: Some(0), + ..Default::default() + }), + } ])), ); if current { diff --git a/codex-rs/core/src/session/step_context.rs b/codex-rs/core/src/session/step_context.rs index 8dc6bdb0fb..58aafb5623 100644 --- a/codex-rs/core/src/session/step_context.rs +++ b/codex-rs/core/src/session/step_context.rs @@ -22,6 +22,8 @@ use tokio_util::sync::CancellationToken; /// Request-scoped state that may change between model sampling requests. #[derive(Clone)] pub(crate) struct StepContext { + /// Input prefix captured for this normal inference attempt, acknowledged with its output. + pub(crate) sampled_through_sequence: Option, /// Final serving value for this invocation; retained by its code cell across waits. pub(crate) serving_assessment: Option>, pub(crate) turn: Arc, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index aaf6e5f8e9..d9474d1f44 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -292,6 +292,7 @@ impl StepContext { }); settings.service_tier = turn.config.service_tier.clone(); Arc::new(Self { + sampled_through_sequence: None, preempt: turn .config .features @@ -2816,6 +2817,8 @@ async fn record_inter_agent_communication_sets_turn_id_in_rollout_and_resume() { item: expected_item.clone(), metadata: Some(CodexHarnessMetadata { user_input_order: Some(0), + history_sequence: Some(1), + sampled_through_sequence: Some(0), ..Default::default() }), }), @@ -2988,11 +2991,11 @@ async fn annotated_history_uses_explicit_model_without_a_step( .executed_tool_calls .mcp_attribution_snapshot(), ); - for envelope in &mut expected { - envelope - .metadata - .get_or_insert_default() - .history_truncation_token_limit = Some(expected_budget); + for (index, envelope) in expected.iter_mut().enumerate() { + let metadata = envelope.metadata.get_or_insert_default(); + metadata.history_sequence = Some(index as u64 + 1); + metadata.sampled_through_sequence = Some(0); + metadata.history_truncation_token_limit = Some(expected_budget); let (ResponseItem::FunctionCallOutput { output, .. } | ResponseItem::CustomToolCallOutput { output, .. }) = &mut envelope.item else { @@ -3171,7 +3174,11 @@ async fn prepares_resumed_history_before_installing_it() { ); assert_eq!( history.annotated_items()[0].metadata, - Some(CodexHarnessMetadata::default()) + Some(CodexHarnessMetadata { + history_sequence: Some(1), + sampled_through_sequence: Some(0), + ..Default::default() + }) ); } @@ -4254,7 +4261,11 @@ async fn record_initial_history_assigns_and_persists_id_for_forked_response_item assert_eq!(raw_history_items(&live_history), vec![expected_item]); assert_eq!( live_history.annotated_items()[0].metadata, - Some(CodexHarnessMetadata::default()) + Some(CodexHarnessMetadata { + history_sequence: Some(1), + sampled_through_sequence: Some(0), + ..Default::default() + }) ); session.flush_rollout().await.expect("rollout should flush"); diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 97e5ba96ed..7261e3378b 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -616,6 +616,7 @@ pub(crate) async fn run_turn( guardian_budget_compacted = false; let SamplingRequestResult { needs_follow_up: model_needs_follow_up, + mailbox_preempted, last_agent_message: sampling_request_last_agent_message, } = sampling_request_output; if model_needs_follow_up { @@ -729,9 +730,10 @@ pub(crate) async fn run_turn( if run_pending_session_start_hooks(&sess, &turn_context).await { return Ok(None); } - // After a signaled step, drain the steer even if compaction + // After interrupted sampling, drain pending input even if compaction // leaves the estimated context above the token limit. can_drain_pending_input = !model_needs_follow_up + || mailbox_preempted || step_context .preempt .as_ref() @@ -1885,6 +1887,10 @@ async fn run_sampling_request( sess.clone_history().await }; let input_item_count = history.raw_items().len(); + let sampling_step_context = Arc::new(StepContext { + sampled_through_sequence: Some(history.input_sequence()), + ..step_context.as_ref().clone() + }); let mut prompt_input = history .for_prompt( &step_context.settings.model_info.input_modalities, @@ -1948,7 +1954,7 @@ async fn run_sampling_request( let err = match try_run_sampling_request( tool_runtime.clone(), Arc::clone(&sess), - Arc::clone(&step_context), + sampling_step_context, Arc::clone(&turn_store), client_session, &responses_metadata, @@ -2017,6 +2023,7 @@ async fn run_sampling_request( return Ok(( SamplingRequestResult { needs_follow_up: true, + mailbox_preempted: draining_for_mailbox, last_agent_message: None, }, notify_messages, @@ -2166,6 +2173,8 @@ pub(crate) async fn built_tools( #[derive(Debug)] struct SamplingRequestResult { needs_follow_up: bool, + /// Queued mail interrupted sampling and must be drained before the next request. + mailbox_preempted: bool, last_agent_message: Option, } @@ -3035,6 +3044,7 @@ async fn try_run_sampling_request( // TODO: Reconcile any response item already being presented to the client. break Ok(SamplingRequestResult { needs_follow_up: true, + mailbox_preempted: false, last_agent_message, }); } @@ -3070,6 +3080,7 @@ async fn try_run_sampling_request( // receiving the interrupt. Do not retry without that pending input. break Ok(SamplingRequestResult { needs_follow_up: true, + mailbox_preempted: true, last_agent_message, }); } @@ -3269,6 +3280,7 @@ async fn try_run_sampling_request( } break Ok(SamplingRequestResult { needs_follow_up: true, + mailbox_preempted: true, last_agent_message, }); } @@ -3478,6 +3490,7 @@ async fn try_run_sampling_request( } break Ok(SamplingRequestResult { needs_follow_up, + mailbox_preempted: *draining_for_mailbox, last_agent_message, }); } diff --git a/codex-rs/core/src/stream_events_utils.rs b/codex-rs/core/src/stream_events_utils.rs index 36396b5bec..4478f53747 100644 --- a/codex-rs/core/src/stream_events_utils.rs +++ b/codex-rs/core/src/stream_events_utils.rs @@ -2,6 +2,7 @@ use std::pin::Pin; use std::sync::Arc; use codex_extension_api::ExtensionData; +use codex_history::CodexHarnessMetadata; use codex_history::ResponseItemEnvelope; use codex_protocol::ResponseItemId; use codex_protocol::config_types::ModeKind; @@ -96,10 +97,18 @@ pub(crate) async fn record_completed_response_item_with_finalized_facts( finalized_facts: Option<&FinalizedTurnItemFacts>, ) { let turn_context = &step_context.turn; - sess.record_conversation_items( + sess.record_annotated_conversation_items( turn_context, &step_context.settings.model_info, - std::slice::from_ref(item), + vec![ResponseItemEnvelope { + item: item.clone(), + metadata: step_context + .sampled_through_sequence + .map(|sequence| CodexHarnessMetadata { + sampled_through_sequence: Some(sequence), + ..Default::default() + }), + }], ) .await; if turn_context.config.otel.agent_response_logging_enabled() { diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index b522e2dedb..ac71c9637f 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -4595,6 +4595,16 @@ async fn manual_compact_twice_preserves_latest_user_messages() { .unwrap(); wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + let mail = json!({ + "type": "agent_message", "id": "agent_pending-result", + "author": "/root/worker", "recipient": "/root", + "content": [{"type": "input_text", "text": "Message Type: FINAL_ANSWER\nThe worker found a release blocker."}], + }); + codex + .inject_response_items(vec![serde_json::from_value(mail.clone()).unwrap()]) + .await + .unwrap(); + codex.submit(Op::Compact).await.unwrap(); wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -4620,6 +4630,11 @@ async fn manual_compact_twice_preserves_latest_user_messages() { wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; let requests = responses_mock.requests(); + assert_eq!( + responses::strip_metadata_from_json(json!(requests[2].inputs_of_type("agent_message"))), + json!([mail]), + ); + assert!(requests[4].inputs_of_type("agent_message").is_empty()); assert_eq!( requests.len(), 5, diff --git a/codex-rs/core/tests/suite/image_rollout.rs b/codex-rs/core/tests/suite/image_rollout.rs index cc2330bce5..668445aef5 100644 --- a/codex-rs/core/tests/suite/image_rollout.rs +++ b/codex-rs/core/tests/suite/image_rollout.rs @@ -881,7 +881,7 @@ async fn resumed_history_only_emits_resize_notices_for_new_images() -> anyhow::R .then(|| envelope.metadata.map(|metadata| metadata.client_authored)) }) .collect::>(); - assert_eq!(persisted_developer_metadata, vec![None, Some(true)]); + assert_eq!(persisted_developer_metadata, vec![Some(false), Some(true)]); Ok(()) } diff --git a/codex-rs/core/tests/suite/scenarios_compaction_tests.rs b/codex-rs/core/tests/suite/scenarios_compaction_tests.rs index dd8e6a9ee2..ddd1ddaa1f 100644 --- a/codex-rs/core/tests/suite/scenarios_compaction_tests.rs +++ b/codex-rs/core/tests/suite/scenarios_compaction_tests.rs @@ -1,7 +1,7 @@ -//! Snapshots remote `/compact`, its persisted replacement, and the next user turn. +//! Snapshots pending mail across remote compaction, restart, and normal inference. use super::*; -use codex_history::RolloutItem; +use core_test_support::responses::strip_metadata_from_json; use pretty_assertions::assert_eq; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -17,75 +17,91 @@ async fn remote_slash_compact_preserves_context_across_windows() -> Result<()> { ev_completed("plan-response"), ]), sse(vec![ - json!({ - "type": "response.output_item.done", - "item": { - "type": "compaction", - "encrypted_content": "REMOTE_SLASH_COMPACT_CHECKPOINT", - }, - }), + compaction_output("REMOTE_SLASH_COMPACT_CHECKPOINT"), ev_completed("compact-response"), ]), + sse(vec![ + compaction_output("RECOMPACTED_CHECKPOINT"), + ev_completed("recompact-response"), + ]), sse(vec![ ev_assistant_message("continue", "Start by validating the release candidate."), ev_completed("continue-response"), ]), + sse(vec![ + compaction_output("AFTER_MAIL_WAS_SAMPLED"), + ev_completed("sampled-compact-response"), + ]), ], ) .await; - let test = test_codex() + let configure = |config: &mut Config| { + configure_scenario_catalog(config); + config.workspace_roots = vec![config.cwd.clone()]; + config.model_auto_compact_token_limit = Some(i64::MAX); + }; + let mut builder = test_codex() .with_model("gpt-6-astra") .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) - .with_config(|config| { - configure_scenario_catalog(config); - config.workspace_roots = vec![config.cwd.clone()]; - }) - .build_with_auto_env(&server) - .await?; + .with_config(configure); + let mut test = builder.build_with_auto_env(&server).await?; test.submit_turn("Plan the release.").await?; + let mail = json!({ + "type": "agent_message", "id": "agent_pending-result", + "author": "/root/worker", "recipient": "/root", + "content": [{"type": "input_text", "text": "Message Type: FINAL_ANSWER\nThe worker found a release blocker."}], + }); + test.codex + .inject_response_items(vec![serde_json::from_value(mail.clone())?]) + .await?; + for compaction in 0..2 { + test.codex.submit(Op::Compact).await?; + wait_for_event(&test.codex, |event| { + assert!(!matches!(event, EventMsg::Error(_)), "{event:?}"); + matches!(event, EventMsg::TurnComplete(_)) + }) + .await; + if compaction == 0 { + builder = builder.with_model("gpt-6-astra").with_config(configure); + test = builder.restart_with_auto_env(&server, &test).await?; + } + } + + test.submit_turn("Continue with the first step.").await?; test.codex.submit(Op::Compact).await?; wait_for_event(&test.codex, |event| { - assert!(!matches!(event, EventMsg::Error(_)), "{event:?}"); matches!(event, EventMsg::TurnComplete(_)) }) .await; - - // Observe the checkpoint before a later turn can initialize or update its context. - test.codex.flush_rollout().await?; - let rollout = fs::read_to_string(test.codex.rollout_path().expect("rollout path"))?; - let replacement = rollout - .lines() - .map(codex_rollout::parse_rollout_line) - .collect::, _>>()? - .into_iter() - .find_map(|line| match line.item { - RolloutItem::Compacted(compacted) => compacted.replacement_history, - _ => None, - }) - .expect("persisted compaction replacement") - .into_iter() - .map(|envelope| serde_json::to_value(envelope.item)) - .collect::, _>>()?; - - test.submit_turn("Continue with the first step.").await?; + let history = test.codex.conversation_history_snapshot().await; + assert!( + !history + .items() + .any(|item| matches!(item, ResponseItem::AgentMessage { .. })) + ); let requests = mock.requests(); - assert_eq!(requests.len(), 3); + assert_eq!(requests.len(), 5); assert_eq!(requests[1].inputs_of_type("compaction_trigger").len(), 1); assert_eq!( - requests[2].inputs_of_type("compaction")[0]["encrypted_content"], - "REMOTE_SLASH_COMPACT_CHECKPOINT" + requests[3].inputs_of_type("compaction")[0]["encrypted_content"], + "RECOMPACTED_CHECKPOINT" + ); + assert_eq!( + strip_metadata_from_json(json!(requests[3].inputs_of_type("agent_message"))), + json!([mail]), ); insta::assert_snapshot!( "remote_slash_compact", context_snapshot::format_context_snapshot( - "A user invokes /compact through remote compaction, persists the replacement history, then continues in the new window.", + "Pending mail survives repeated /compact and cold resume, then normal inference acknowledges it for subsequent compaction.", &[ SnapshotEntry::captured(&requests[0]).labeled("Before /compact"), SnapshotEntry::captured(&requests[1]).labeled("Remote /compact request"), - SnapshotEntry::items(&replacement).labeled("Stored replacement before next turn"), - SnapshotEntry::captured(&requests[2]).labeled("First turn after /compact"), + SnapshotEntry::captured(&requests[2]).labeled("Repeated /compact after resume"), + SnapshotEntry::captured(&requests[3]).labeled("First turn after /compact"), + SnapshotEntry::captured(&requests[4]).labeled("Compaction after mail was sampled"), ], &ContextSnapshotOptions::default().include_request_settings(), ) diff --git a/codex-rs/core/tests/suite/scenarios_injected_agent_mail.rs b/codex-rs/core/tests/suite/scenarios_injected_agent_mail.rs index 1590892853..c2ded73902 100644 --- a/codex-rs/core/tests/suite/scenarios_injected_agent_mail.rs +++ b/codex-rs/core/tests/suite/scenarios_injected_agent_mail.rs @@ -233,3 +233,129 @@ async fn injected_mail_respects_delivery_boundaries() -> anyhow::Result<()> { server.shutdown().await; Ok(()) } + +#[test_case::test_case(true; "instant interrupt enabled")] +#[test_case::test_case(false; "instant interrupt disabled")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn injected_agent_message_interrupting_sampling_survives_mid_turn_compaction( + instant_interrupt: bool, +) -> anyhow::Result<()> { + let (release_boundary, boundary_gate) = oneshot::channel(); + let chunk = |gate, events| StreamingSseChunk { + gate, + body: responses::sse(events), + }; + let long_reasoning = "reasoning ".repeat(400); + let (server, _completions) = start_streaming_sse_server(vec![ + vec![ + chunk( + None, + vec![ + responses::ev_response_created("first"), + responses::ev_reasoning_item_added("boundary", &[long_reasoning.as_str()]), + ], + ), + chunk( + Some(boundary_gate), + vec![responses::ev_reasoning_item( + "boundary", + &[long_reasoning.as_str()], + &[], + )], + ), + ], + vec![chunk( + None, + vec![ + responses::ev_response_created("compact"), + responses::ev_assistant_message("summary", "AUTO_COMPACT_SUMMARY"), + responses::ev_completed_with_tokens("compact", /*total_tokens*/ 50), + ], + )], + vec![chunk( + None, + vec![ + responses::ev_response_created("after-compact"), + responses::ev_assistant_message("answer", "The worker update is included."), + responses::ev_completed_with_tokens("after-compact", /*total_tokens*/ 60), + ], + )], + ]) + .await; + let config_server = responses::start_mock_server().await; + let base_url = format!("{}/v1", server.uri()); + let test = test_codex() + .with_model("gpt-5.4") + .with_config(move |config| { + config.model_provider.name = "OpenAI (test)".to_string(); + config.model_provider.base_url = Some(base_url); + config.model_provider.supports_websockets = false; + config.model_auto_compact_token_limit = Some(200); + config + .features + .set_enabled(Feature::InstantInterrupt, instant_interrupt) + .expect("set instant interrupt"); + config + .features + .disable(Feature::DeferMailboxPreemption) + .expect("enable preemption"); + config + .features + .disable(Feature::EnableRequestCompression) + .expect("capture request bodies"); + }) + .build_with_auto_env(&config_server) + .await?; + let message: ResponseItem = serde_json::from_value(json!({ + "type": "agent_message", "id": "agent_active-before-compaction", + "author": "/root/worker", "recipient": "/root", + "content": [{"type": "input_text", "text": "Use the completed worker result."}], + }))?; + test.codex + .start_or_steer_turn(TurnInputRequest::user_input(vec![super::text( + "Plan the next action.", + )])) + .await?; + wait_for_event( + &test.codex, + |event| matches!(event, EventMsg::ItemStarted(item) if item.item.id() == "boundary"), + ) + .await; + test.codex + .inject_response_items(vec![message.clone()]) + .await?; + release_boundary + .send(()) + .expect("release sampling boundary"); + wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::TurnComplete(_)) + }) + .await; + test.codex.shutdown_and_wait().await?; + + let requests = server + .requests() + .await + .iter() + .map(|body| serde_json::from_slice::(body)) + .collect::>>()?; + assert_eq!(requests.len(), 3); + let after_compact_input = requests[2]["input"].as_array().expect("request input"); + let actual = after_compact_input + .iter() + .filter(|item| item["type"] == "agent_message") + .cloned() + .collect::>(); + assert_eq!( + responses::strip_metadata_from_json(json!(actual)), + responses::strip_metadata_from_json(serde_json::to_value([message])?) + ); + let entries = requests.iter().map(SnapshotEntry::body).collect::>(); + insta::assert_snapshot!(context_snapshot::format_context_snapshot( + "An injected agent message that interrupts sampling is included in the first request after mid-turn compaction.", + &entries, + &ContextSnapshotOptions::default().rewrite_known_segments(), + )); + server.shutdown().await; + Ok(()) +} diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap index f7ea8a443c..724b037da9 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap @@ -35,7 +35,8 @@ Scenario: Manual /compact with prior user history compacts existing history and first manual turn 04:message/assistant: FIRST_REPLY -05:message/user: +05:agent_message +06:message/user: ## Window 3 (after request 2: input diverged at item 04, settings changed (parallel_tool_calls, tools)) @@ -55,7 +56,8 @@ Scenario: Manual /compact with prior user history compacts existing history and 04:message/user: FIRST_MANUAL_SUMMARY -05:message/user: +05:agent_message +06:message/user: second manual turn ## Window 4 (after request 3: settings changed (parallel_tool_calls, tools)) @@ -75,11 +77,12 @@ Scenario: Manual /compact with prior user history compacts existing history and 04:message/user: FIRST_MANUAL_SUMMARY -05:message/user: +05:agent_message +06:message/user: second manual turn -06:message/assistant: +07:message/assistant: SECOND_LARGE_REPLY -07:message/user: +08:message/user: ## Window 5 (after request 4: input diverged at item 01, settings changed (parallel_tool_calls, tools)) diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__compaction__remote_slash_compact.snap b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__compaction__remote_slash_compact.snap index 9fc3765404..2efbf44dad 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__compaction__remote_slash_compact.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__compaction__remote_slash_compact.snap @@ -1,8 +1,8 @@ --- source: core/tests/suite/scenarios_compaction_tests.rs -expression: "context_snapshot::format_context_snapshot(\"A user invokes /compact through remote compaction, persists the replacement history, then continues in the new window.\",\n&[SnapshotEntry::captured(&requests[0]).labeled(\"Before /compact\"),\nSnapshotEntry::captured(&requests[1]).labeled(\"Remote /compact request\"),\nSnapshotEntry::items(&replacement).labeled(\"Stored replacement before next turn\"),\nSnapshotEntry::captured(&requests[2]).labeled(\"First turn after /compact\"),],\n&ContextSnapshotOptions::default().include_request_settings(),)" +expression: "context_snapshot::format_context_snapshot(\"Pending mail survives repeated /compact and cold resume, then normal inference acknowledges it for subsequent compaction.\",\n&[SnapshotEntry::captured(&requests[0]).labeled(\"Before /compact\"),\nSnapshotEntry::captured(&requests[1]).labeled(\"Remote /compact request\"),\nSnapshotEntry::captured(&requests[2]).labeled(\"Repeated /compact after resume\"),\nSnapshotEntry::captured(&requests[3]).labeled(\"First turn after /compact\"),\nSnapshotEntry::captured(&requests[4]).labeled(\"Compaction after mail was sampled\"),],\n&ContextSnapshotOptions::default().include_request_settings(),)" --- -Scenario: A user invokes /compact through remote compaction, persists the replacement history, then continues in the new window. +Scenario: Pending mail survives repeated /compact and cold resume, then normal inference acknowledges it for subsequent compaction. ## Window 1 Settings: @@ -104,11 +104,12 @@ Settings: -- request 2 (compaction; Remote /compact request) -- 08:message/assistant: The release plan has three steps. -09:compaction_trigger +09:agent_message +10:compaction_trigger ## Window 2 (after request 2: input diverged at item 07) -Settings: unavailable (items only) --- request 3 (items; Stored replacement before next turn) -- +Settings: same as window 1 +-- request 3 (compaction; Repeated /compact after resume) -- 00:additional_tools/developer (3; hash=e019c949fc483f83): - namespace/functions; hash=03340277aa23936f - custom/exec @@ -194,8 +195,10 @@ Settings: unavailable (items only) 06:message/user: Plan the release. 07:compaction:encrypted=true; chars=31; hash=8255caf138edede8 +08:agent_message +09:compaction_trigger -## Window 3 (after request 3) +## Window 3 (after request 3: input diverged at item 02) Settings: same as window 1 -- request 4 (turn; First turn after /compact) -- 00:additional_tools/developer (3; hash=e019c949fc483f83): @@ -233,8 +236,21 @@ Settings: same as window 1 02:message/developer[2]: [01] - Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`...ll commands are permitted. Network access is enabled. [hash=c134061cebb94a1b] - Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected. + Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `read-only`: The san... permits reading files. Network access is restricted. [hash=be7da4425f89053c] + # Escalation Requests + + Commands are run outside the sandbox if they are approved by the user, or match an existing rule that al...hell control operators, including but not limited to: [hash=d24ebf5fa4e4b9bb] + + - Pipes: | + - Logical operators: &&, || + + NEVER provide a prefix_rule if your command uses a heredoc or herestring. + + ### Examples + Good examples of prefixes: + - ["npm", "run", "dev"] + - ["gh", "pr", "check"] + - ["cargo", "test"] [02] # Collaboration Mode: Default @@ -274,14 +290,30 @@ Settings: same as window 1 Any earlier instruction enabling proactive multi-agent delegation no longer applies. D...elegation, or parallel agent work. [hash=2b6e037067b4390c] 05:message/user: - + - <...e="unrestricted" /> [hash=d40b2f45cb028888] + ...ntry> [hash=8fc9d186c1133657] 06:message/user: Plan the release. -07:compaction:encrypted=true; chars=31; hash=8255caf138edede8 -08:message/user: +07:compaction:encrypted=true; chars=22; hash=5180312a53382d23 +08:agent_message +09:message/developer: + + Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`...ll commands are permitted. Network access is enabled. [hash=c134061cebb94a1b] + Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected. + +10:message/user: + + + + [hash=46e2a5c52f0891de] + +11:message/user: Continue with the first step. +-- request 5 (compaction; Compaction after mail was sampled) -- +12:message/assistant: + Start by validating the release candidate. +13:compaction_trigger diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__injected_agent_mail__injected_agent_message_interrupting_sampling_survives_mid_turn_compaction.snap b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__injected_agent_mail__injected_agent_message_interrupting_sampling_survives_mid_turn_compaction.snap new file mode 100644 index 0000000000..18d0e6c7b8 --- /dev/null +++ b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__injected_agent_mail__injected_agent_message_interrupting_sampling_survives_mid_turn_compaction.snap @@ -0,0 +1,93 @@ +--- +source: core/tests/suite/scenarios_injected_agent_mail.rs +expression: "context_snapshot::format_context_snapshot(\"An injected agent message that interrupts sampling is included in the first request after mid-turn compaction.\",\n&entries, &ContextSnapshotOptions::default().rewrite_known_segments(),)" +--- +Scenario: An injected agent message that interrupts sampling is included in the first request after mid-turn compaction. + +## Window 1 +-- request 1 (request) -- +00:message/developer: + You are a coding agent running in the Codex CLI, a terminal-based coding assistant. Codex CLI is an open...I. You are expected to be precise, safe, and helpful. [hash=5125daec1ed78d99] + + Your capabilities: + + - Receive user prompts and other context provided by the harness, such as files in the workspace. + - Communicate with the user by streaming thinking & responses, and by making & updating plans. + - Emit function calls to run terminal commands and apply patches. Depending on how this specific run is ... More on this in the "Sandbox and approvals" section. [hash=31cf026fa7b25d6d] + + + ## Shell commands + + When using the shell, you must adhere to the following guidelines: + + - When searching for text or files, prefer using `rg` or `rg --files` respectively because `rg` is much ...he `rg` command is not found, then use alternatives.) [hash=67af3ff3b546e946] + - Do not use python scripts to attempt to output larger chunks of a file. + + +01:message/developer: + +02:message/user: + +03:message/user: + Plan the next action. + +## Window 2 (after request 1: settings changed (parallel_tool_calls, tools)) +-- request 2 (request) -- +00:message/developer: + You are a coding agent running in the Codex CLI, a terminal-based coding assistant. Codex CLI is an open...I. You are expected to be precise, safe, and helpful. [hash=5125daec1ed78d99] + + Your capabilities: + + - Receive user prompts and other context provided by the harness, such as files in the workspace. + - Communicate with the user by streaming thinking & responses, and by making & updating plans. + - Emit function calls to run terminal commands and apply patches. Depending on how this specific run is ... More on this in the "Sandbox and approvals" section. [hash=31cf026fa7b25d6d] + + + ## Shell commands + + When using the shell, you must adhere to the following guidelines: + + - When searching for text or files, prefer using `rg` or `rg --files` respectively because `rg` is much ...he `rg` command is not found, then use alternatives.) [hash=67af3ff3b546e946] + - Do not use python scripts to attempt to output larger chunks of a file. + + +01:message/developer: + +02:message/user: + +03:message/user: + Plan the next action. +04:reasoning:summary=reasoning reasoning reasoning reasoning reasoning reasoning reasoning reasoning reasoning reasoning reas...ing reasoning reasoning reasoning reasoning reasoning [hash=ca3a0b1992c1437f]:encrypted=true +05:message/user: + + +## Window 3 (after request 2: input diverged at item 04, settings changed (parallel_tool_calls, tools)) +-- request 3 (request) -- +00:message/developer: + You are a coding agent running in the Codex CLI, a terminal-based coding assistant. Codex CLI is an open...I. You are expected to be precise, safe, and helpful. [hash=5125daec1ed78d99] + + Your capabilities: + + - Receive user prompts and other context provided by the harness, such as files in the workspace. + - Communicate with the user by streaming thinking & responses, and by making & updating plans. + - Emit function calls to run terminal commands and apply patches. Depending on how this specific run is ... More on this in the "Sandbox and approvals" section. [hash=31cf026fa7b25d6d] + + + ## Shell commands + + When using the shell, you must adhere to the following guidelines: + + - When searching for text or files, prefer using `rg` or `rg --files` respectively because `rg` is much ...he `rg` command is not found, then use alternatives.) [hash=67af3ff3b546e946] + - Do not use python scripts to attempt to output larger chunks of a file. + + +01:message/developer: + +02:message/user: + +03:message/user: + Plan the next action. +04:message/user: + + AUTO_COMPACT_SUMMARY +05:agent_message diff --git a/codex-rs/history/src/lib.rs b/codex-rs/history/src/lib.rs index 6acdf082fd..27c81a3cec 100644 --- a/codex-rs/history/src/lib.rs +++ b/codex-rs/history/src/lib.rs @@ -66,6 +66,15 @@ pub struct ResponseItemEnvelope { /// #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] pub struct CodexHarnessMetadata { + /// Thread-local append sequence. Compaction keeps the identities of retained inputs. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub history_sequence: Option, + + /// Highest input sequence included in a regular request that recorded model output. + /// Compaction carries this checkpoint without advancing it. Never model-visible. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub sampled_through_sequence: Option, + /// Complete retained records actually delivered by this Guardian message. Host-only. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub guardian_sources: Vec, diff --git a/codex-rs/history/src/tests.rs b/codex-rs/history/src/tests.rs index 6c8784fe5f..39f6d7535b 100644 --- a/codex-rs/history/src/tests.rs +++ b/codex-rs/history/src/tests.rs @@ -359,6 +359,8 @@ fn response_item_envelope_stores_metadata_beside_rollout_payload() -> Result<()> item: RolloutItem::ResponseItem(ResponseItemEnvelope { item: response_item.clone(), metadata: Some(CodexHarnessMetadata { + history_sequence: Some(9), + sampled_through_sequence: Some(7), client_authored: true, history_truncation_token_limit: Some(20_000), inherited_user_message: true, @@ -376,6 +378,8 @@ fn response_item_envelope_stores_metadata_beside_rollout_payload() -> Result<()> "type": "response_item", "payload": response_item, "metadata": { + "history_sequence": 9, + "sampled_through_sequence": 7, "client_authored": true, "fallback_token_limit_override": 20_000, "inherited_user_message": true, @@ -391,6 +395,8 @@ fn response_item_envelope_stores_metadata_beside_rollout_payload() -> Result<()> assert_eq!( envelope.metadata, Some(CodexHarnessMetadata { + history_sequence: Some(9), + sampled_through_sequence: Some(7), client_authored: true, history_truncation_token_limit: Some(20_000), inherited_user_message: true,