From 669449b02c4b58de4bcd22485eb60c98a6e38a54 Mon Sep 17 00:00:00 2001 From: rka-oai Date: Tue, 13 Oct 2026 07:23:26 +0000 Subject: [PATCH] Add async compaction feature configuration (#53595) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What changed Register the disabled-by-default `async_compaction` feature and accept either a boolean or a `[features.async_compaction]` table. Add `model_async_compact_threshold_percent` with a default of 65 and a valid range of 1–100, validated even when the feature is disabled. Setting a threshold alone does not enable the feature. Expose the threshold in the runtime configuration and update the JSON schema for both configuration forms. ## Testing Add configuration loader tests covering boolean and table forms, default and custom thresholds, explicit enablement, and rejection of out-of-range values. GitOrigin-RevId: cf33de76878e7044bc8cf5d146375a07ea9eb078 --- codex-rs/config/src/schema.rs | 9 ++++ codex-rs/core/config.schema.json | 33 ++++++++++++ .../core/src/config/config_loader_tests.rs | 52 +++++++++++++++++++ codex-rs/core/src/config/mod.rs | 18 +++++++ codex-rs/features/src/feature_configs.rs | 19 +++++++ codex-rs/features/src/lib.rs | 18 +++++++ codex-rs/thread-manager-sample/src/main.rs | 1 + 7 files changed, 150 insertions(+) diff --git a/codex-rs/config/src/schema.rs b/codex-rs/config/src/schema.rs index fd144796ee..770e73ee5c 100644 --- a/codex-rs/config/src/schema.rs +++ b/codex-rs/config/src/schema.rs @@ -99,6 +99,15 @@ pub fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema { ); continue; } + if feature.id == codex_features::Feature::AsyncCompaction { + validation.properties.insert( + feature.key.to_string(), + schema_gen.subschema_for::>(), + ); + continue; + } if feature.id == codex_features::Feature::TokenBudget { validation.properties.insert( feature.key.to_string(), diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 78b980893d..04a8593f0b 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -353,6 +353,23 @@ ], "type": "string" }, + "AsyncCompactionConfigToml": { + "additionalProperties": false, + "description": "Background compaction settings. A threshold alone does not enable the feature.", + "properties": { + "enabled": { + "type": "boolean" + }, + "model_async_compact_threshold_percent": { + "description": "Percentage of the model's full context window that starts background compaction. Defaults to 65; valid values are 1–100, including when disabled.", + "format": "uint8", + "maximum": 100.0, + "minimum": 1.0, + "type": "integer" + } + }, + "type": "object" + }, "AuthCredentialsStoreMode": { "description": "Determine where Codex should store CLI auth credentials.", "oneOf": [ @@ -767,6 +784,9 @@ } ] }, + "async_compaction": { + "$ref": "#/definitions/FeatureToml_for_AsyncCompactionConfigToml" + }, "auth_elicitation": { "type": "boolean" }, @@ -1581,6 +1601,16 @@ }, "type": "object" }, + "FeatureToml_for_AsyncCompactionConfigToml": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/definitions/AsyncCompactionConfigToml" + } + ] + }, "FeatureToml_for_CodeModeConfigToml": { "anyOf": [ { @@ -7078,6 +7108,9 @@ } ] }, + "async_compaction": { + "$ref": "#/definitions/FeatureToml_for_AsyncCompactionConfigToml" + }, "auth_elicitation": { "type": "boolean" }, diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 45fbf4930a..5e0c5b4435 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -4827,3 +4827,55 @@ prefix_rules = [] Ok(()) } } + +#[tokio::test] +async fn async_compaction_config() { + use std::io::ErrorKind::InvalidInput; + + for (settings, expected) in [ + ("[features]\nasync_compaction = true", Ok((true, 65))), + ("[features]\nasync_compaction = false", Ok((false, 65))), + ("[features.async_compaction]", Ok((false, 65))), + ( + "[features.async_compaction]\nenabled = true", + Ok((true, 65)), + ), + ( + "[features.async_compaction]\nmodel_async_compact_threshold_percent = 80", + Ok((false, 80)), + ), + ( + "[features.async_compaction]\nenabled = false\nmodel_async_compact_threshold_percent = 80", + Ok((false, 80)), + ), + ( + "[features.async_compaction]\nenabled = true\nmodel_async_compact_threshold_percent = 75", + Ok((true, 75)), + ), + ( + "[features.async_compaction]\nmodel_async_compact_threshold_percent = 0", + Err(InvalidInput), + ), + ( + "[features.async_compaction]\nmodel_async_compact_threshold_percent = 101", + Err(InvalidInput), + ), + ] { + let tmp = tempdir().expect("tempdir"); + std::fs::write(tmp.path().join(CONFIG_TOML_FILE), settings).expect("write config"); + let actual = ConfigBuilder::default() + .codex_home(tmp.path().to_path_buf()) + .fallback_cwd(Some(tmp.path().to_path_buf())) + .loader_overrides(LoaderOverrides::without_managed_config_for_tests()) + .build() + .await + .map(|config| { + ( + config.features.enabled(Feature::AsyncCompaction), + config.async_compaction_threshold_percent, + ) + }) + .map_err(|error| error.kind()); + assert_eq!(actual, expected, "{settings}"); + } +} diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index f4cd90538c..d55d48bd4a 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -222,6 +222,7 @@ pub use windows_sandbox_config::windows_mxc_allowed_by_config; const DEFAULT_IGNORE_LARGE_UNTRACKED_DIRS: i64 = 200; const DEFAULT_IGNORE_LARGE_UNTRACKED_FILES: i64 = 10 * 1024 * 1024; +const DEFAULT_ASYNC_COMPACTION_THRESHOLD_PERCENT: u8 = 65; /// Signals that a public config selected the retired `untrusted` approval policy. #[derive(Debug, thiserror::Error)] @@ -660,6 +661,9 @@ pub struct Config { /// Zero disables turn-end compaction. pub model_post_turn_compact_threshold_percent: u8, + /// Percentage of the model's full context window at which background compaction starts. + pub async_compaction_threshold_percent: u8, + /// Key into the model_providers map that specifies which provider to use. pub model_provider_id: String, @@ -3303,6 +3307,19 @@ impl Config { "model_post_turn_compact_threshold_percent must be between 0 and 100", )); } + let async_compaction_threshold_percent = cfg.features.as_ref() + .and_then(|features| features.async_compaction.as_ref()) + .and_then(|feature| match feature { + FeatureToml::Enabled(_) => None, + FeatureToml::Config(config) => config.model_async_compact_threshold_percent, + }) + .unwrap_or(DEFAULT_ASYNC_COMPACTION_THRESHOLD_PERCENT); + if !(1..=100).contains(&async_compaction_threshold_percent) { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "model_async_compact_threshold_percent must be between 1 and 100", + )); + } if let Some(responses_api_metadata) = cfg.responses_api_metadata.as_ref() { validate_extra_metadata(responses_api_metadata.iter()).map_err(|message| { std::io::Error::new(std::io::ErrorKind::InvalidInput, message) @@ -4395,6 +4412,7 @@ impl Config { model_post_turn_compact_threshold_percent: cfg .model_post_turn_compact_threshold_percent .unwrap_or_default(), + async_compaction_threshold_percent, model_provider_id, model_provider, cwd: resolved_cwd, diff --git a/codex-rs/features/src/feature_configs.rs b/codex-rs/features/src/feature_configs.rs index 09562e8722..e8b67257db 100644 --- a/codex-rs/features/src/feature_configs.rs +++ b/codex-rs/features/src/feature_configs.rs @@ -564,3 +564,22 @@ pub enum NetworkProxyUnixSocketPermissionToml { Allow, Deny, } + +/// Background compaction settings. A threshold alone does not enable the feature. +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct AsyncCompactionConfigToml { + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + /// Percentage of the model's full context window that starts background compaction. + /// Defaults to 65; valid values are 1–100, including when disabled. + #[serde(skip_serializing_if = "Option::is_none")] + #[schemars(range(min = 1, max = 100))] + pub model_async_compact_threshold_percent: Option, +} + +impl FeatureConfig for AsyncCompactionConfigToml { + fn enabled(&self) -> Option { + self.enabled + } +} diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 3d4fa0fd70..071b8ebce2 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -16,6 +16,7 @@ use toml::Table; mod feature_configs; mod legacy; +pub use feature_configs::AsyncCompactionConfigToml; pub use feature_configs::CodeModeConfigToml; pub use feature_configs::CodeModeHostConfigToml; pub use feature_configs::ContextManagementConfigToml; @@ -419,6 +420,8 @@ pub enum Feature { PreventIdleSleep, /// Removed compatibility key, still advertised to the Responses API. RemoteCompactionV2, + /// Prepare remote compaction in the background before the foreground limit is reached. + AsyncCompaction, /// Include retained images in the remote compaction context budget. CompactionImageBudget, /// Retain client-authored developer messages across compacted context windows. @@ -880,6 +883,8 @@ pub struct FeaturesToml { #[serde(default, skip_serializing_if = "Option::is_none")] pub token_budget: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] + pub async_compaction: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] pub context_management: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub rollout_budget: Option>, @@ -906,6 +911,13 @@ impl Features { impl FeaturesToml { pub fn entries(&self) -> BTreeMap { let mut entries = self.entries.clone(); + if let Some(enabled) = self + .async_compaction + .as_ref() + .and_then(FeatureToml::enabled) + { + entries.insert(Feature::AsyncCompaction.key().to_string(), enabled); + } if let Some(enabled) = self.code_mode.as_ref().and_then(FeatureToml::enabled) { entries.insert(Feature::CodeMode.key().to_string(), enabled); } @@ -2042,6 +2054,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Removed, default_enabled: false, }, + FeatureSpec { + id: Feature::AsyncCompaction, + key: "async_compaction", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::CompactionImageBudget, key: "compaction_image_budget", diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index 608afbbe37..0d53be0050 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -250,6 +250,7 @@ async fn new_config( model_auto_compact_token_limit: None, model_auto_compact_token_limit_scope: AutoCompactTokenLimitScope::Total, model_post_turn_compact_threshold_percent: 0, + async_compaction_threshold_percent: 65, model_provider_id, model_provider, personality: None,