core: nest auto-compaction fallback config

This commit is contained in:
Bo Xie
2026-07-08 15:30:52 -07:00
parent 5c9903d067
commit 248115bed4
9 changed files with 195 additions and 26 deletions
+1 -1
View File
@@ -115,7 +115,7 @@ struct AutoCompactFallbackPrompt(Option<String>);
impl AutoCompactFallbackPromptExtension {
fn store(thread_store: &ExtensionData, config: &Config) {
thread_store.insert(AutoCompactFallbackPrompt(
config.auto_compact_fallback_prompt.clone(),
config.auto_compact_fallback.prompt.clone(),
));
}
}
-4
View File
@@ -243,10 +243,6 @@ pub struct ConfigToml {
/// Compact prompt used for history compaction.
pub compact_prompt: Option<String>,
/// Developer message contributed by the app-server extension for the single
/// fallback turn before an automatic compaction rollover.
pub auto_compact_fallback_prompt: Option<String>,
/// When set, restricts ChatGPT login to one or more workspace identifiers.
#[serde(default)]
pub forced_chatgpt_workspace_id: Option<ForcedChatgptWorkspaceIds>,
+9
View File
@@ -53,6 +53,15 @@ pub fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema {
);
continue;
}
if feature.id == codex_features::Feature::AutoCompactFallback {
validation.properties.insert(
feature.key.to_string(),
schema_gen.subschema_for::<codex_features::FeatureToml<
codex_features::AutoCompactFallbackConfigToml,
>>(),
);
continue;
}
if feature.id == codex_features::Feature::RolloutBudget {
validation.properties.insert(
feature.key.to_string(),
+26 -7
View File
@@ -315,6 +315,19 @@
}
]
},
"AutoCompactFallbackConfigToml": {
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"prompt": {
"description": "Developer message contributed for the single fallback turn before an automatic compaction rollover.",
"type": "string"
}
},
"type": "object"
},
"AutoCompactTokenLimitScope": {
"description": "Selects which part of the active context is charged against `model_auto_compact_token_limit`.",
"oneOf": [
@@ -435,7 +448,7 @@
"type": "boolean"
},
"auto_compact_fallback": {
"type": "boolean"
"$ref": "#/definitions/FeatureToml_for_AutoCompactFallbackConfigToml"
},
"browser_use": {
"type": "boolean"
@@ -946,6 +959,16 @@
},
"type": "object"
},
"FeatureToml_for_AutoCompactFallbackConfigToml": {
"anyOf": [
{
"type": "boolean"
},
{
"$ref": "#/definitions/AutoCompactFallbackConfigToml"
}
]
},
"FeatureToml_for_CodeModeConfigToml": {
"anyOf": [
{
@@ -4668,10 +4691,6 @@
"default": null,
"description": "Machine-local realtime audio device preferences used by realtime voice."
},
"auto_compact_fallback_prompt": {
"description": "Developer message contributed by the app-server extension for the single fallback turn before an automatic compaction rollover.",
"type": "string"
},
"auto_review": {
"allOf": [
{
@@ -4814,7 +4833,7 @@
"type": "boolean"
},
"auto_compact_fallback": {
"type": "boolean"
"$ref": "#/definitions/FeatureToml_for_AutoCompactFallbackConfigToml"
},
"browser_use": {
"type": "boolean"
@@ -5545,4 +5564,4 @@
},
"title": "ConfigToml",
"type": "object"
}
}
+44 -3
View File
@@ -7095,10 +7095,18 @@ async fn loads_compact_prompt_from_file() -> std::io::Result<()> {
}
#[tokio::test]
async fn loads_auto_compact_fallback_prompt() -> std::io::Result<()> {
async fn loads_auto_compact_fallback_settings() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let features = toml::from_str(
r#"
[auto_compact_fallback]
enabled = true
prompt = " write notes immediately "
"#,
)
.expect("auto compact fallback feature config should deserialize");
let cfg = ConfigToml {
auto_compact_fallback_prompt: Some(" write notes immediately ".to_string()),
features: Some(features),
..Default::default()
};
@@ -7110,9 +7118,42 @@ async fn loads_auto_compact_fallback_prompt() -> std::io::Result<()> {
.await?;
assert_eq!(
config.auto_compact_fallback_prompt.as_deref(),
config.auto_compact_fallback.prompt.as_deref(),
Some("write notes immediately")
);
assert!(config.features.enabled(Feature::AutoCompactFallback));
Ok(())
}
#[tokio::test]
async fn prompt_only_auto_compact_fallback_settings_do_not_enable_feature() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let features = toml::from_str(
r#"
[auto_compact_fallback]
prompt = " write notes immediately "
"#,
)
.expect("auto compact fallback feature config should deserialize");
let cfg = ConfigToml {
features: Some(features),
..Default::default()
};
let config = Config::load_from_base_config_with_overrides(
cfg,
ConfigOverrides::default(),
codex_home.abs(),
)
.await?;
assert_eq!(
config.auto_compact_fallback.prompt.as_deref(),
Some("write notes immediately")
);
assert!(!config.features.enabled(Feature::AutoCompactFallback));
Ok(())
}
+30 -11
View File
@@ -56,6 +56,7 @@ use codex_core_plugins::PluginLoadOutcome;
use codex_core_plugins::PluginsConfigInput;
use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::LOCAL_FS;
use codex_features::AutoCompactFallbackConfigToml;
use codex_features::CodeModeConfigToml;
use codex_features::CurrentTimeReminderConfigToml;
use codex_features::CurrentTimeReminderDeliveryMode;
@@ -711,8 +712,8 @@ pub struct Config {
/// Compact prompt override.
pub compact_prompt: Option<String>,
/// Developer message contributed for the optional pre-rollover fallback turn.
pub auto_compact_fallback_prompt: Option<String>,
/// Settings for the optional pre-rollover fallback turn.
pub auto_compact_fallback: AutoCompactFallbackConfig,
/// Optional external notifier command. When set, Codex will spawn this
/// program after each completed *turn* (i.e. when the agent finishes
@@ -1103,6 +1104,11 @@ pub struct TokenBudgetConfig {
pub guidance_message: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct AutoCompactFallbackConfig {
pub prompt: Option<String>,
}
impl Default for TokenBudgetConfig {
fn default() -> Self {
Self {
@@ -2615,6 +2621,17 @@ fn resolve_token_budget_config(
}))
}
fn resolve_auto_compact_fallback_config(config_toml: &ConfigToml) -> AutoCompactFallbackConfig {
let configured = auto_compact_fallback_toml_config(config_toml.features.as_ref());
let prompt = configured
.and_then(|config| config.prompt.as_deref())
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string);
AutoCompactFallbackConfig { prompt }
}
fn resolve_rollout_budget_config(
config_toml: &ConfigToml,
features: &ManagedFeatures,
@@ -2758,6 +2775,15 @@ fn token_budget_toml_config(features: Option<&FeaturesToml>) -> Option<&TokenBud
}
}
fn auto_compact_fallback_toml_config(
features: Option<&FeaturesToml>,
) -> Option<&AutoCompactFallbackConfigToml> {
match features?.auto_compact_fallback.as_ref()? {
FeatureToml::Enabled(_) => None,
FeatureToml::Config(config) => Some(config),
}
}
fn current_time_reminder_toml_config(
features: Option<&FeaturesToml>,
) -> Option<&CurrentTimeReminderConfigToml> {
@@ -3414,6 +3440,7 @@ impl Config {
let code_mode = resolve_code_mode_config(&cfg);
let multi_agent_v2 = resolve_multi_agent_v2_config(&cfg);
let token_budget = resolve_token_budget_config(&cfg, &features)?;
let auto_compact_fallback = resolve_auto_compact_fallback_config(&cfg);
let rollout_budget = resolve_rollout_budget_config(&cfg, &features)?;
let current_time_reminder = resolve_current_time_reminder_config(&cfg, &features)?;
let terminal_resize_reflow = resolve_terminal_resize_reflow_config(&cfg);
@@ -3602,14 +3629,6 @@ impl Config {
Some(trimmed.to_string())
}
});
let auto_compact_fallback_prompt = cfg.auto_compact_fallback_prompt.and_then(|value| {
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
});
// Load base instructions override from a file if specified. If the
// path is relative, resolve it against the effective cwd so the
// behaviour matches other path-like config values.
@@ -3825,7 +3844,7 @@ impl Config {
personality,
developer_instructions,
compact_prompt,
auto_compact_fallback_prompt,
auto_compact_fallback,
include_permissions_instructions,
include_apps_instructions,
include_collaboration_mode_instructions,
+21
View File
@@ -106,6 +106,27 @@ impl FeatureConfig for TokenBudgetConfigToml {
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct AutoCompactFallbackConfigToml {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
/// Developer message contributed for the single fallback turn before an
/// automatic compaction rollover.
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt: Option<String>,
}
impl FeatureConfig for AutoCompactFallbackConfigToml {
fn enabled(&self) -> Option<bool> {
self.enabled
}
fn set_enabled(&mut self, enabled: bool) {
self.enabled = Some(enabled);
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct RolloutBudgetConfigToml {
+13
View File
@@ -16,6 +16,7 @@ use toml::Table;
mod feature_configs;
mod legacy;
pub use feature_configs::AutoCompactFallbackConfigToml;
pub use feature_configs::CodeModeConfigToml;
pub use feature_configs::CurrentTimeReminderConfigToml;
pub use feature_configs::CurrentTimeReminderDeliveryMode;
@@ -644,6 +645,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 auto_compact_fallback: Option<FeatureToml<AutoCompactFallbackConfigToml>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rollout_budget: Option<FeatureToml<RolloutBudgetConfigToml>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_time_reminder: Option<FeatureToml<CurrentTimeReminderConfigToml>>,
@@ -682,6 +685,13 @@ impl FeaturesToml {
if let Some(enabled) = self.token_budget.as_ref().and_then(FeatureToml::enabled) {
entries.insert(Feature::TokenBudget.key().to_string(), enabled);
}
if let Some(enabled) = self
.auto_compact_fallback
.as_ref()
.and_then(FeatureToml::enabled)
{
entries.insert(Feature::AutoCompactFallback.key().to_string(), enabled);
}
if let Some(enabled) = self.rollout_budget.as_ref().and_then(FeatureToml::enabled) {
entries.insert(Feature::RolloutBudget.key().to_string(), enabled);
}
@@ -704,6 +714,7 @@ impl FeaturesToml {
code_mode,
multi_agent_v2,
token_budget,
auto_compact_fallback,
rollout_budget,
current_time_reminder,
removed_apps_mcp_path_override: _,
@@ -721,6 +732,8 @@ impl FeaturesToml {
materialize_resolved_feature_enabled(multi_agent_v2, enabled);
} else if spec.id == Feature::TokenBudget {
materialize_resolved_feature_enabled(token_budget, enabled);
} else if spec.id == Feature::AutoCompactFallback {
materialize_resolved_feature_enabled(auto_compact_fallback, enabled);
} else if spec.id == Feature::RolloutBudget {
materialize_resolved_feature_enabled(rollout_budget, enabled);
} else if spec.id == Feature::CurrentTimeReminder {
+51
View File
@@ -642,11 +642,51 @@ non_code_mode_only = true
);
}
#[test]
fn auto_compact_fallback_feature_config_deserializes_boolean_toggle() {
let features: FeaturesToml =
toml::from_str("auto_compact_fallback = true").expect("features table should deserialize");
assert_eq!(
features.entries(),
BTreeMap::from([("auto_compact_fallback".to_string(), true)])
);
assert_eq!(
features.auto_compact_fallback,
Some(FeatureToml::Enabled(true))
);
}
#[test]
fn auto_compact_fallback_feature_config_deserializes_table() {
let features: FeaturesToml = toml::from_str(
r#"
[auto_compact_fallback]
enabled = true
prompt = "Persist the important state."
"#,
)
.expect("features table should deserialize");
assert_eq!(
features.entries(),
BTreeMap::from([("auto_compact_fallback".to_string(), true)])
);
assert_eq!(
features.auto_compact_fallback,
Some(FeatureToml::Config(crate::AutoCompactFallbackConfigToml {
enabled: Some(true),
prompt: Some("Persist the important state.".to_string()),
}))
);
}
#[test]
fn materialize_resolved_enabled_writes_all_features_and_preserves_custom_config() {
let mut features = Features::with_defaults();
features.enable(Feature::CodeMode);
features.enable(Feature::MultiAgentV2);
features.enable(Feature::AutoCompactFallback);
features.enable(Feature::NetworkProxy);
features.enable(Feature::RespectSystemProxy);
@@ -656,6 +696,10 @@ fn materialize_resolved_enabled_writes_all_features_and_preserves_custom_config(
min_wait_timeout_ms: Some(2500),
..Default::default()
})),
auto_compact_fallback: Some(FeatureToml::Config(crate::AutoCompactFallbackConfigToml {
enabled: Some(false),
prompt: Some("Persist the important state.".to_string()),
})),
network_proxy: Some(FeatureToml::Config(crate::NetworkProxyConfigToml {
enabled: Some(false),
proxy_url: Some("http://127.0.0.1:43128".to_string()),
@@ -684,6 +728,13 @@ fn materialize_resolved_enabled_writes_all_features_and_preserves_custom_config(
..Default::default()
}))
);
assert_eq!(
features_toml.auto_compact_fallback,
Some(FeatureToml::Config(crate::AutoCompactFallbackConfigToml {
enabled: Some(true),
prompt: Some("Persist the important state.".to_string()),
}))
);
assert_eq!(
features_toml.network_proxy,
Some(FeatureToml::Config(crate::NetworkProxyConfigToml {