From 8aae85895809601a055902f1b85647620e01a523 Mon Sep 17 00:00:00 2001 From: rka-oai Date: Wed, 15 Jul 2026 04:44:20 +0000 Subject: [PATCH] Add auto-compaction fallback token-budget settings (#33243) ## What changed - Add `auto_compact_fallback_prompt` and `auto_compact_fallback_buffer_tokens` to `features.token_budget` and the generated configuration schema. - Trim empty fallback prompts, limit prompts to 2,000 bytes, require a buffer when a prompt is configured, and reject non-positive buffer values. - Preserve the new settings when locking resolved session configuration. ## Testing - Cover config resolution and validation for overlong prompts, missing buffers, and non-positive buffers. - Extend the configuration-lock test to cover both settings. GitOrigin-RevId: 6463f963ba1dbd633d76601b8344360a8c85cf8b --- codex-rs/core/config.schema.json | 11 ++++ codex-rs/core/src/config/config_tests.rs | 73 ++++++++++++++++++++++++ codex-rs/core/src/config/mod.rs | 39 +++++++++++++ codex-rs/core/src/session/config_lock.rs | 4 ++ codex-rs/features/src/feature_configs.rs | 8 +++ 5 files changed, 135 insertions(+) diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index eed232864ce4..b55512b1691a 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -2923,6 +2923,17 @@ "TokenBudgetConfigToml": { "additionalProperties": false, "properties": { + "auto_compact_fallback_buffer_tokens": { + "description": "Additional tokens available after the compaction threshold for fallback note-taking.", + "format": "int64", + "minimum": 1.0, + "type": "integer" + }, + "auto_compact_fallback_prompt": { + "description": "Developer message sampled before an automatic context-window rollover.", + "maxLength": 2000, + "type": "string" + }, "enabled": { "type": "boolean" }, diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index c314780955c0..11ec23fc5227 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -572,11 +572,15 @@ enabled = true reminder_threshold_tokens = 16000 reminder_message_template = "Custom reminder: {n_remaining} tokens." guidance_message = "Preserve important state before compaction." +auto_compact_fallback_prompt = " Write notes immediately. " +auto_compact_fallback_buffer_tokens = 8000 "#, TokenBudgetConfig { reminder_threshold_tokens: Some(16_000), reminder_message_template: "Custom reminder: {n_remaining} tokens.".to_string(), guidance_message: Some("Preserve important state before compaction.".to_string()), + auto_compact_fallback_prompt: Some("Write notes immediately.".to_string()), + auto_compact_fallback_buffer_tokens: Some(8_000), }, ), ] { @@ -595,6 +599,26 @@ guidance_message = "Preserve important state before compaction." Ok(()) } +#[tokio::test] +async fn load_config_rejects_overlong_auto_compact_fallback_prompt() -> std::io::Result<()> { + let codex_home = tempdir()?; + let prompt = "x".repeat(AUTO_COMPACT_FALLBACK_PROMPT_MAX_BYTES + 1); + let config_toml = toml::from_str(&format!( + "[features.token_budget]\nenabled = true\nauto_compact_fallback_prompt = {prompt:?}\n" + )) + .expect("TOML should deserialize"); + let error = Config::load_from_base_config_with_overrides( + config_toml, + ConfigOverrides::default(), + codex_home.abs(), + ) + .await + .expect_err("overlong fallback prompt should be rejected"); + + assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput); + Ok(()) +} + #[tokio::test] async fn load_config_rejects_invalid_token_budget_reminder_template() -> std::io::Result<()> { for reminder_message_template in [ @@ -644,6 +668,55 @@ async fn load_config_rejects_non_positive_token_budget_reminder_threshold() -> s Ok(()) } +#[tokio::test] +async fn load_config_rejects_non_positive_auto_compact_fallback_buffer() -> std::io::Result<()> { + for auto_compact_fallback_buffer_tokens in [-1, 0] { + let codex_home = tempdir()?; + let config_toml = toml::from_str(&format!( + "[features.token_budget]\nenabled = true\nauto_compact_fallback_prompt = \"Write notes.\"\nauto_compact_fallback_buffer_tokens = {auto_compact_fallback_buffer_tokens}\n" + )) + .expect("TOML should deserialize"); + let error = Config::load_from_base_config_with_overrides( + config_toml, + ConfigOverrides::default(), + codex_home.abs(), + ) + .await + .expect_err("non-positive fallback buffer should be rejected"); + + assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput); + assert_eq!( + error.to_string(), + "features.token_budget.auto_compact_fallback_buffer_tokens must be positive" + ); + } + Ok(()) +} + +#[tokio::test] +async fn load_config_rejects_missing_auto_compact_fallback_buffer() -> std::io::Result<()> { + let codex_home = tempdir()?; + let config_toml = toml::from_str( + "[features.token_budget]\nenabled = true\nauto_compact_fallback_prompt = \"Write notes.\"\n", + ) + .expect("TOML should deserialize"); + + let error = Config::load_from_base_config_with_overrides( + config_toml, + ConfigOverrides::default(), + codex_home.abs(), + ) + .await + .expect_err("missing fallback buffer should be rejected"); + assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput); + assert_eq!( + error.to_string(), + "features.token_budget.auto_compact_fallback_buffer_tokens is required when auto_compact_fallback_prompt is set" + ); + + Ok(()) +} + #[tokio::test] async fn load_config_resolves_rollout_budget() -> std::io::Result<()> { let codex_home = tempdir()?; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 646407cb72a4..a2b05dffe39e 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1143,12 +1143,15 @@ pub(crate) const DEFAULT_TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE: &str = concat!( ); const TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE_MAX_BYTES: usize = 2000; const TOKEN_BUDGET_GUIDANCE_MESSAGE_MAX_BYTES: usize = 2000; +const AUTO_COMPACT_FALLBACK_PROMPT_MAX_BYTES: usize = 2000; #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct TokenBudgetConfig { pub reminder_threshold_tokens: Option, pub reminder_message_template: String, pub guidance_message: Option, + pub auto_compact_fallback_prompt: Option, + pub auto_compact_fallback_buffer_tokens: Option, } impl Default for TokenBudgetConfig { @@ -1157,6 +1160,8 @@ impl Default for TokenBudgetConfig { reminder_threshold_tokens: None, reminder_message_template: DEFAULT_TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE.to_string(), guidance_message: None, + auto_compact_fallback_prompt: None, + auto_compact_fallback_buffer_tokens: None, } } } @@ -2687,10 +2692,44 @@ fn resolve_token_budget_config( )); } + let auto_compact_fallback_prompt = token_budget_config + .and_then(|config| config.auto_compact_fallback_prompt.as_deref()) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(str::to_string); + if auto_compact_fallback_prompt + .as_ref() + .is_some_and(|prompt| prompt.len() > AUTO_COMPACT_FALLBACK_PROMPT_MAX_BYTES) + { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!( + "features.token_budget.auto_compact_fallback_prompt must not exceed {AUTO_COMPACT_FALLBACK_PROMPT_MAX_BYTES} bytes" + ), + )); + } + + let auto_compact_fallback_buffer_tokens = + token_budget_config.and_then(|config| config.auto_compact_fallback_buffer_tokens); + if auto_compact_fallback_prompt.is_some() && auto_compact_fallback_buffer_tokens.is_none() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "features.token_budget.auto_compact_fallback_buffer_tokens is required when auto_compact_fallback_prompt is set", + )); + } + if auto_compact_fallback_buffer_tokens.is_some_and(|tokens| tokens <= 0) { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "features.token_budget.auto_compact_fallback_buffer_tokens must be positive", + )); + } + Ok(Some(TokenBudgetConfig { reminder_threshold_tokens, reminder_message_template, guidance_message, + auto_compact_fallback_prompt, + auto_compact_fallback_buffer_tokens, })) } diff --git a/codex-rs/core/src/session/config_lock.rs b/codex-rs/core/src/session/config_lock.rs index 606d9d2be0de..e3dfb174b02e 100644 --- a/codex-rs/core/src/session/config_lock.rs +++ b/codex-rs/core/src/session/config_lock.rs @@ -249,6 +249,8 @@ mod tests { reminder_threshold_tokens: Some(16_000), reminder_message_template: "Locked reminder: {n_remaining} tokens.".to_string(), guidance_message: Some("Locked context-window guidance.".to_string()), + auto_compact_fallback_prompt: Some("Write notes before rollover.".to_string()), + auto_compact_fallback_buffer_tokens: Some(8_000), }); config .features @@ -342,6 +344,8 @@ mod tests { "Locked reminder: {n_remaining} tokens.".to_string() ), guidance_message: Some("Locked context-window guidance.".to_string()), + auto_compact_fallback_prompt: Some("Write notes before rollover.".to_string()), + auto_compact_fallback_buffer_tokens: Some(8_000), })) ); diff --git a/codex-rs/features/src/feature_configs.rs b/codex-rs/features/src/feature_configs.rs index 8d006f255c8c..88062d554da2 100644 --- a/codex-rs/features/src/feature_configs.rs +++ b/codex-rs/features/src/feature_configs.rs @@ -98,6 +98,14 @@ pub struct TokenBudgetConfigToml { #[serde(skip_serializing_if = "Option::is_none")] #[schemars(length(max = 2000))] pub guidance_message: Option, + /// Developer message sampled before an automatic context-window rollover. + #[serde(skip_serializing_if = "Option::is_none")] + #[schemars(length(max = 2000))] + pub auto_compact_fallback_prompt: Option, + /// Additional tokens available after the compaction threshold for fallback note-taking. + #[serde(skip_serializing_if = "Option::is_none")] + #[schemars(range(min = 1))] + pub auto_compact_fallback_buffer_tokens: Option, } impl FeatureConfig for TokenBudgetConfigToml {