Add async compaction feature configuration (#53595)

## 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
This commit is contained in:
rka-oai
2026-10-13 07:32:21 +00:00
committed by copyberry
parent f82e365922
commit 669449b02c
7 changed files with 150 additions and 0 deletions
+9
View File
@@ -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::<codex_features::FeatureToml<
codex_features::AsyncCompactionConfigToml,
>>(),
);
continue;
}
if feature.id == codex_features::Feature::TokenBudget {
validation.properties.insert(
feature.key.to_string(),
+33
View File
@@ -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"
},
@@ -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}");
}
}
+18
View File
@@ -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,
+19
View File
@@ -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<bool>,
/// 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<u8>,
}
impl FeatureConfig for AsyncCompactionConfigToml {
fn enabled(&self) -> Option<bool> {
self.enabled
}
}
+18
View File
@@ -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<FeatureToml<TokenBudgetConfigToml>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub async_compaction: Option<FeatureToml<AsyncCompactionConfigToml>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_management: Option<FeatureToml<ContextManagementConfigToml>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rollout_budget: Option<FeatureToml<RolloutBudgetConfigToml>>,
@@ -906,6 +911,13 @@ impl Features {
impl FeaturesToml {
pub fn entries(&self) -> BTreeMap<String, bool> {
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",
@@ -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,