From 40de788c4d703b504ba13dd74b50f36863afc698 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Wed, 11 Feb 2026 17:41:08 -0800 Subject: [PATCH] Clamp auto-compact limit to context window (#11516) - Clamp auto-compaction to the minimum of configured limit and 90% of context window - Add an e2e compact test for clamped behavior - Update remote compact tests to account for earlier auto-compaction in setup turns --- codex-rs/core/tests/suite/compact.rs | 58 +++++++++++++++++++++ codex-rs/core/tests/suite/compact_remote.rs | 5 -- codex-rs/protocol/src/openai_models.rs | 17 ++++-- 3 files changed, 70 insertions(+), 10 deletions(-) diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 6106369c3..914673bdb 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -2553,6 +2553,64 @@ async fn auto_compact_triggers_after_function_call_over_95_percent_usage() { ); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn auto_compact_clamps_config_limit_to_context_window() { + skip_if_no_network!(); + + let server = start_mock_server().await; + + let context_window = 100; + let config_limit = 200; + let over_limit_tokens = context_window * 90 / 100 + 1; + + let first_turn = sse(vec![ + ev_assistant_message("m1", FIRST_REPLY), + ev_completed_with_tokens("r1", over_limit_tokens), + ]); + let auto_summary_payload = auto_summary(AUTO_SUMMARY_TEXT); + let auto_compact_turn = sse(vec![ + ev_assistant_message("m2", &auto_summary_payload), + ev_completed_with_tokens("r2", 10), + ]); + let post_auto_compact_turn = sse(vec![ev_completed_with_tokens("r3", 10)]); + + let first_turn_mock = mount_sse_once(&server, first_turn).await; + let auto_compact_mock = mount_sse_once(&server, auto_compact_turn).await; + mount_sse_once(&server, post_auto_compact_turn).await; + + let model_provider = non_openai_model_provider(&server); + let mut builder = test_codex().with_config(move |config| { + config.model_provider = model_provider; + set_test_compact_prompt(config); + config.model_context_window = Some(context_window); + config.model_auto_compact_token_limit = Some(config_limit); + }); + let codex = builder.build(&server).await.unwrap(); + + codex.submit_turn("OVER_LIMIT_TURN").await.unwrap(); + codex.submit_turn("FOLLOW_UP_AFTER_CLAMP").await.unwrap(); + + assert!( + first_turn_mock.single_request().input().iter().any(|item| { + item.get("type").and_then(|value| value.as_str()) == Some("message") + && item + .get("content") + .and_then(|content| content.as_array()) + .and_then(|entries| entries.first()) + .and_then(|entry| entry.get("text")) + .and_then(|value| value.as_str()) + == Some("OVER_LIMIT_TURN") + }), + "first request should contain the over-limit user input" + ); + + let auto_compact_body = auto_compact_mock.single_request().body_json().to_string(); + assert!( + body_contains_text(&auto_compact_body, SUMMARIZATION_PROMPT), + "auto compact should run with the summarization prompt when config limit exceeds context" + ); +} + #[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 47a9a0a11..3311427d4 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -266,7 +266,6 @@ async fn remote_compact_trims_function_call_history_to_fit_context_window() -> R responses::ev_shell_command_call(trimmed_call_id, trimmed_command), responses::ev_completed("trimmed-call-response"), ]), - sse(vec![responses::ev_completed("trimmed-final-response")]), ], ) .await; @@ -385,10 +384,6 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window() "trimmed-final-response", 500_000, )]), - sse(vec![ - responses::ev_assistant_message("post-compact-assistant", "post compact complete"), - responses::ev_completed("post-compact-final-response"), - ]), ], ) .await; diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index f8e61fd57..d9275e383 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -238,7 +238,8 @@ pub struct ModelInfo { #[serde(default, skip_serializing_if = "Option::is_none")] pub context_window: Option, /// Token threshold for automatic compaction. When omitted, core derives it - /// from `context_window` (90%). + /// from `context_window` (90%). When provided, core clamps it to 90% of the + /// context window when available. #[serde(default, skip_serializing_if = "Option::is_none")] pub auto_compact_token_limit: Option, /// Percentage of the context window considered usable for inputs, after @@ -256,10 +257,16 @@ pub struct ModelInfo { impl ModelInfo { pub fn auto_compact_token_limit(&self) -> Option { - self.auto_compact_token_limit.or_else(|| { - self.context_window - .map(|context_window| (context_window * 9) / 10) - }) + let context_limit = self + .context_window + .map(|context_window| (context_window * 9) / 10); + let config_limit = self.auto_compact_token_limit; + if let Some(context_limit) = context_limit { + return Some( + config_limit.map_or(context_limit, |limit| std::cmp::min(limit, context_limit)), + ); + } + config_limit } pub fn supports_personality(&self) -> bool {