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
This commit is contained in:
rka-oai
2026-07-15 04:48:45 +00:00
committed by copyberry
parent fca51f6daf
commit 8aae858958
5 changed files with 135 additions and 0 deletions
+11
View File
@@ -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"
},
+73
View File
@@ -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()?;
+39
View File
@@ -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<i64>,
pub reminder_message_template: String,
pub guidance_message: Option<String>,
pub auto_compact_fallback_prompt: Option<String>,
pub auto_compact_fallback_buffer_tokens: Option<i64>,
}
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,
}))
}
+4
View File
@@ -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),
}))
);
+8
View File
@@ -98,6 +98,14 @@ pub struct TokenBudgetConfigToml {
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(length(max = 2000))]
pub guidance_message: Option<String>,
/// 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<String>,
/// 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<i64>,
}
impl FeatureConfig for TokenBudgetConfigToml {