diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index eb81b7e952..f624370d59 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -19,7 +19,7 @@ use crate::analytics_client::build_track_events_context; use crate::apps::render_apps_section; use crate::commit_attribution::commit_message_trailer_instruction; use crate::compact; -use crate::compact::AutoCompactCallsite; +use crate::compact::CompactCallsite; use crate::compact::run_inline_auto_compact_task; use crate::compact::should_use_remote_compact_task; use crate::compact_remote::run_inline_remote_auto_compact_task; @@ -4517,7 +4517,7 @@ pub(crate) async fn run_turn( if let Err(err) = run_auto_compact( &sess, &turn_context, - AutoCompactCallsite::MidTurnContinuation, + CompactCallsite::MidTurnContinuation, None, ) .await @@ -4670,7 +4670,7 @@ async fn maybe_run_previous_model_inline_compact( sess, // We use previous turn context here because we compact with the previous model &previous_turn_context, - AutoCompactCallsite::PreSamplingModelSwitch, + CompactCallsite::PreSamplingModelSwitch, None, ) .await @@ -4784,7 +4784,7 @@ async fn run_pre_turn_auto_compaction_if_needed( let compact_result = run_auto_compact( sess, turn_context, - AutoCompactCallsite::PreTurnIncludingIncomingUserMessage, + CompactCallsite::PreTurnIncludingIncomingUserMessage, Some(incoming_turn_items.to_vec()), ) .await; @@ -4803,7 +4803,7 @@ async fn run_pre_turn_auto_compaction_if_needed( CodexErr::ContextWindowExceeded => { error!( turn_id = %turn_context.sub_id, - auto_compact_callsite = ?AutoCompactCallsite::PreTurnIncludingIncomingUserMessage, + auto_compact_callsite = ?CompactCallsite::PreTurnIncludingIncomingUserMessage, incoming_items_tokens_estimate, auto_compact_limit, reason = "pre-turn compaction exceeded context window", @@ -4859,7 +4859,7 @@ fn is_projected_submission_over_auto_compact_limit( async fn run_auto_compact( sess: &Arc, turn_context: &Arc, - auto_compact_callsite: AutoCompactCallsite, + auto_compact_callsite: CompactCallsite, incoming_items: Option>, ) -> CodexResult<()> { let result = if should_use_remote_compact_task(&turn_context.provider) { diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index c662798717..95a79673ba 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -35,7 +35,9 @@ pub const SUMMARY_PREFIX: &str = include_str!("../templates/compact/summary_pref const COMPACT_USER_MESSAGE_MAX_TOKENS: usize = 20_000; #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum AutoCompactCallsite { +pub(crate) enum CompactCallsite { + /// Manual `/compact` task. + ManualCompact, /// Pre-turn auto-compaction where the incoming turn context + user message are included in /// the compaction request. PreTurnIncludingIncomingUserMessage, @@ -94,7 +96,7 @@ pub(crate) fn extract_latest_model_switch_update_from_items( pub(crate) async fn run_inline_auto_compact_task( sess: Arc, turn_context: Arc, - auto_compact_callsite: AutoCompactCallsite, + auto_compact_callsite: CompactCallsite, incoming_items: Option>, ) -> CodexResult<()> { let prompt = turn_context.compact_prompt().to_string(); @@ -108,7 +110,7 @@ pub(crate) async fn run_inline_auto_compact_task( sess, turn_context, input, - Some(auto_compact_callsite), + auto_compact_callsite, incoming_items, ) .await?; @@ -126,14 +128,21 @@ pub(crate) async fn run_compact_task( collaboration_mode_kind: turn_context.collaboration_mode.mode, }); sess.send_event(&turn_context, start_event).await; - run_compact_task_inner(sess, turn_context, input, None, None).await + run_compact_task_inner( + sess, + turn_context, + input, + CompactCallsite::ManualCompact, + None, + ) + .await } async fn run_compact_task_inner( sess: Arc, turn_context: Arc, input: Vec, - auto_compact_callsite: Option, + auto_compact_callsite: CompactCallsite, incoming_items: Option>, ) -> CodexResult<()> { let compaction_item = TurnItem::ContextCompaction(ContextCompactionItem::new()); @@ -285,14 +294,22 @@ async fn run_compact_task_inner( COMPACT_USER_MESSAGE_MAX_TOKENS, ); let mut new_history = process_compacted_history(compacted_history); - if matches!( - auto_compact_callsite, - Some( - AutoCompactCallsite::MidTurnContinuation | AutoCompactCallsite::PreSamplingModelSwitch - ) - ) { - let initial_context = sess.build_initial_context(turn_context.as_ref()).await; - insert_initial_context_before_last_real_user(&mut new_history, initial_context); + match auto_compact_callsite { + CompactCallsite::MidTurnContinuation | CompactCallsite::PreSamplingModelSwitch => { + // Mid-turn and pre-sampling model-switch compaction continue the in-flight turn and + // therefore must keep canonical context anchored above the latest real user turn. + let initial_context = sess.build_initial_context(turn_context.as_ref()).await; + insert_initial_context_before_last_real_user(&mut new_history, initial_context); + } + CompactCallsite::ManualCompact => { + // Manual `/compact` intentionally rewrites transcript history without reseeding turn + // context here; the task marks initial context unseeded for the next user turn. + } + CompactCallsite::PreTurnIncludingIncomingUserMessage + | CompactCallsite::PreTurnExcludingIncomingUserMessage => { + // Pre-turn compaction persists canonical context directly above the incoming user + // message in `run_turn`, not inside compacted replacement history. + } } // Reattach stripped model-switch updates into replacement history so post-compaction // sampling keeps model-switch guidance regardless of compaction callsite. diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index 8695ea7582..5d60703cef 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use crate::Prompt; use crate::codex::Session; use crate::codex::TurnContext; -use crate::compact::AutoCompactCallsite; +use crate::compact::CompactCallsite; use crate::compact::extract_latest_model_switch_update_from_items; use crate::compact::extract_trailing_model_switch_update_for_compaction_request; use crate::compact::insert_initial_context_before_last_real_user; @@ -32,7 +32,7 @@ use tracing::info; pub(crate) async fn run_inline_remote_auto_compact_task( sess: Arc, turn_context: Arc, - auto_compact_callsite: AutoCompactCallsite, + auto_compact_callsite: CompactCallsite, incoming_items: Option>, ) -> CodexResult<()> { run_remote_compact_task_inner(&sess, &turn_context, auto_compact_callsite, incoming_items) @@ -51,19 +51,13 @@ pub(crate) async fn run_remote_compact_task( }); sess.send_event(&turn_context, start_event).await; - run_remote_compact_task_inner( - &sess, - &turn_context, - AutoCompactCallsite::PreTurnExcludingIncomingUserMessage, - None, - ) - .await + run_remote_compact_task_inner(&sess, &turn_context, CompactCallsite::ManualCompact, None).await } async fn run_remote_compact_task_inner( sess: &Arc, turn_context: &Arc, - auto_compact_callsite: AutoCompactCallsite, + auto_compact_callsite: CompactCallsite, incoming_items: Option>, ) -> CodexResult<()> { if let Err(err) = run_remote_compact_task_inner_impl( @@ -88,7 +82,7 @@ async fn run_remote_compact_task_inner( async fn run_remote_compact_task_inner_impl( sess: &Arc, turn_context: &Arc, - auto_compact_callsite: AutoCompactCallsite, + auto_compact_callsite: CompactCallsite, incoming_items: Option>, ) -> CodexResult<()> { let compaction_item = TurnItem::ContextCompaction(ContextCompactionItem::new()); @@ -168,12 +162,22 @@ async fn run_remote_compact_task_inner_impl( }) .await?; new_history = process_compacted_history(new_history); - if matches!( - auto_compact_callsite, - AutoCompactCallsite::MidTurnContinuation | AutoCompactCallsite::PreSamplingModelSwitch - ) { - let initial_context = sess.build_initial_context(turn_context.as_ref()).await; - insert_initial_context_before_last_real_user(&mut new_history, initial_context); + match auto_compact_callsite { + CompactCallsite::MidTurnContinuation | CompactCallsite::PreSamplingModelSwitch => { + // Mid-turn and pre-sampling model-switch compaction continue the in-flight turn and + // therefore must keep canonical context anchored above the latest real user turn. + let initial_context = sess.build_initial_context(turn_context.as_ref()).await; + insert_initial_context_before_last_real_user(&mut new_history, initial_context); + } + CompactCallsite::ManualCompact => { + // Manual `/compact` intentionally rewrites transcript history without reseeding turn + // context here; the task marks initial context unseeded for the next user turn. + } + CompactCallsite::PreTurnIncludingIncomingUserMessage + | CompactCallsite::PreTurnExcludingIncomingUserMessage => { + // Pre-turn compaction persists canonical context directly above the incoming user + // message in `run_turn`, not inside compacted replacement history. + } } if let Some(incoming_items) = incoming_items.as_ref() { let incoming_history_items: Vec = incoming_items @@ -271,7 +275,7 @@ fn remove_incoming_echoes_from_compacted_history( fn log_remote_compact_failure( turn_context: &TurnContext, - auto_compact_callsite: AutoCompactCallsite, + auto_compact_callsite: CompactCallsite, log_data: &CompactRequestLogData, total_usage_breakdown: TotalTokenUsageBreakdown, err: &CodexErr,