From d839bfe680a4cbbb5c6112d86bc3c7603001777c Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Fri, 6 Feb 2026 10:14:14 -0800 Subject: [PATCH] Stop auto-compact loop on compact failure --- codex-rs/core/src/codex.rs | 14 +++-- codex-rs/core/src/compact.rs | 15 ++--- codex-rs/core/src/compact_remote.rs | 13 ++-- codex-rs/core/tests/suite/compact.rs | 69 +++++++++++++++++++++ codex-rs/core/tests/suite/compact_remote.rs | 1 + 5 files changed, 95 insertions(+), 17 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 68c99c599c..7bf0f1012d 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -3654,8 +3654,8 @@ pub(crate) async fn run_turn( collaboration_mode_kind: turn_context.collaboration_mode.mode, }); sess.send_event(&turn_context, event).await; - if total_usage_tokens >= auto_compact_limit { - run_auto_compact(&sess, &turn_context).await; + if total_usage_tokens >= auto_compact_limit && !run_auto_compact(&sess, &turn_context).await { + return None; } let skills_outcome = Some( @@ -3837,7 +3837,9 @@ pub(crate) async fn run_turn( // as long as compaction works well in getting us way below the token limit, we shouldn't worry about being in an infinite loop. if token_limit_reached && needs_follow_up { - run_auto_compact(&sess, &turn_context).await; + if !run_auto_compact(&sess, &turn_context).await { + break; + } continue; } @@ -3895,11 +3897,11 @@ pub(crate) async fn run_turn( last_agent_message } -async fn run_auto_compact(sess: &Arc, turn_context: &Arc) { +async fn run_auto_compact(sess: &Arc, turn_context: &Arc) -> bool { if should_use_remote_compact_task(&turn_context.provider) { - run_inline_remote_auto_compact_task(Arc::clone(sess), Arc::clone(turn_context)).await; + run_inline_remote_auto_compact_task(Arc::clone(sess), Arc::clone(turn_context)).await } else { - run_inline_auto_compact_task(Arc::clone(sess), Arc::clone(turn_context)).await; + run_inline_auto_compact_task(Arc::clone(sess), Arc::clone(turn_context)).await } } diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index 72e002d45a..63f6013bd8 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -40,7 +40,7 @@ pub(crate) fn should_use_remote_compact_task(provider: &ModelProviderInfo) -> bo pub(crate) async fn run_inline_auto_compact_task( sess: Arc, turn_context: Arc, -) { +) -> bool { let prompt = turn_context.compact_prompt().to_string(); let input = vec![UserInput::Text { text: prompt, @@ -48,7 +48,7 @@ pub(crate) async fn run_inline_auto_compact_task( text_elements: Vec::new(), }]; - run_compact_task_inner(sess, turn_context, input).await; + run_compact_task_inner(sess, turn_context, input).await } pub(crate) async fn run_compact_task( @@ -61,14 +61,14 @@ 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.clone(), turn_context, input).await; + let _ = run_compact_task_inner(sess.clone(), turn_context, input).await; } async fn run_compact_task_inner( sess: Arc, turn_context: Arc, input: Vec, -) { +) -> bool { let compaction_item = TurnItem::ContextCompaction(ContextCompactionItem::new()); sess.emit_turn_item_started(&turn_context, &compaction_item) .await; @@ -143,7 +143,7 @@ async fn run_compact_task_inner( break; } Err(CodexErr::Interrupted) => { - return; + return false; } Err(e @ CodexErr::ContextWindowExceeded) => { if turn_input_len > 1 { @@ -159,7 +159,7 @@ async fn run_compact_task_inner( sess.set_total_tokens_full(turn_context.as_ref()).await; let event = EventMsg::Error(e.to_error_event(None)); sess.send_event(&turn_context, event).await; - return; + return false; } Err(e) => { if retries < max_retries { @@ -176,7 +176,7 @@ async fn run_compact_task_inner( } else { let event = EventMsg::Error(e.to_error_event(None)); sess.send_event(&turn_context, event).await; - return; + return false; } } } @@ -211,6 +211,7 @@ async fn run_compact_task_inner( message: "Heads up: Long threads and multiple compactions can cause the model to be less accurate. Start a new thread when possible to keep threads small and targeted.".to_string(), }); sess.send_event(&turn_context, warning).await; + true } pub fn content_items_to_text(content: &[ContentItem]) -> Option { diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index f627bcb473..97f8d8f739 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -19,8 +19,8 @@ use tracing::info; pub(crate) async fn run_inline_remote_auto_compact_task( sess: Arc, turn_context: Arc, -) { - run_remote_compact_task_inner(&sess, &turn_context).await; +) -> bool { + run_remote_compact_task_inner(&sess, &turn_context).await } pub(crate) async fn run_remote_compact_task(sess: Arc, turn_context: Arc) { @@ -30,16 +30,21 @@ pub(crate) async fn run_remote_compact_task(sess: Arc, turn_context: Ar }); sess.send_event(&turn_context, start_event).await; - run_remote_compact_task_inner(&sess, &turn_context).await; + let _ = run_remote_compact_task_inner(&sess, &turn_context).await; } -async fn run_remote_compact_task_inner(sess: &Arc, turn_context: &Arc) { +async fn run_remote_compact_task_inner( + sess: &Arc, + turn_context: &Arc, +) -> bool { if let Err(err) = run_remote_compact_task_inner_impl(sess, turn_context).await { let event = EventMsg::Error( err.to_error_event(Some("Error running remote compact task".to_string())), ); sess.send_event(turn_context, event).await; + return false; } + true } async fn run_remote_compact_task_inner_impl( diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 183732f0a4..8c266b7b09 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -2226,6 +2226,75 @@ async fn auto_compact_triggers_after_function_call_over_95_percent_usage() { ); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn auto_compact_failure_stops_follow_up_sampling_loop() { + skip_if_no_network!(); + + let server = start_mock_server().await; + + let context_window = 100; + let limit = context_window * 90 / 100; + let over_limit_tokens = context_window * 95 / 100 + 1; + + let first_turn = sse(vec![ + ev_function_call(DUMMY_CALL_ID, DUMMY_FUNCTION_NAME, "{}"), + ev_completed_with_tokens("r1", over_limit_tokens), + ]); + let follow_up_turn = sse(vec![ + ev_assistant_message("m2", "SHOULD_NOT_BE_REQUESTED"), + ev_completed_with_tokens("r2", 1), + ]); + + mount_sse_once(&server, first_turn).await; + let follow_up_mock = mount_sse_once(&server, follow_up_turn).await; + let compact_mock = + mount_compact_json_once(&server, serde_json::json!({ "bad": "shape" })).await; + + let codex = test_codex() + .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) + .with_config(move |config| { + set_test_compact_prompt(config); + config.model_context_window = Some(context_window); + config.model_auto_compact_token_limit = Some(limit); + }) + .build(&server) + .await + .expect("build codex") + .codex; + + codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: FUNCTION_CALL_LIMIT_MSG.into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + }) + .await + .unwrap(); + + let error_message = wait_for_event_match(&codex, |msg| match msg { + EventMsg::Error(err) => Some(err.message.clone()), + _ => None, + }) + .await; + assert!( + error_message.contains("Error running remote compact task"), + "expected remote auto compact failure message, got: {error_message}" + ); + wait_for_event(&codex, |msg| matches!(msg, EventMsg::TurnComplete(_))).await; + + assert_eq!( + compact_mock.requests().len(), + 1, + "auto compact should run once before failing" + ); + assert!( + follow_up_mock.requests().is_empty(), + "turn should stop after auto compact failure and avoid follow-up sampling" + ); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn auto_compact_counts_encrypted_reasoning_before_last_user() { skip_if_no_network!(); diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index a2ae58cf25..1361f119f7 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -245,6 +245,7 @@ async fn remote_compact_trims_function_call_history_to_fit_context_window() -> R .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { config.model_context_window = Some(2_000); + config.model_auto_compact_token_limit = Some(i64::MAX); }), ) .await?;