[codex] add configurable token budget compaction reminder (#29255)

## Why

The token-budget feature reports coarse remaining-context milestones,
but it does not give the model a configurable wrap-up prompt before
automatic compaction. A strict threshold-crossing check can also miss
resumed or reconfigured windows that are already inside the threshold.

## What changed

- Add structured `[features.token_budget]` configuration for an absolute
`reminder_threshold_tokens` and bounded `reminder_message_template`;
`{n_remaining}` is expanded when the reminder is delivered.
- Compute remaining tokens against the next effective auto-compaction
boundary, including scoped `body_after_prefix` accounting and the full
context-window limit.
- Make reminder delivery level-triggered before and after sampling, with
one-shot state owned by `AutoCompactWindow` and re-armed on compaction,
`new_context`, restore, or history replacement.
- Leave the existing initial full-window token-budget context, 25/50/75%
notices, and token-budget tools unchanged.
- Persist the resolved feature configuration in the session config lock
and regenerate the config schema.

## Validation

- `just test -p codex-core token_budget`
- `just test -p codex-core
token_budget_reminder_emits_after_crossing_compaction_threshold`
- `just test -p codex-core auto_compact_window`
- `just test -p codex-core
lock_contains_prompts_and_materializes_features`
- `just test -p codex-features`
- `just test -p codex-config`
This commit is contained in:
pakrym-oai authored and GitHub committed 2026-06-20 19:13:42 -07:00
1 parent b6d6be2a84
commit 6df037d47f
15 files changed
+418 -27

No files matched your search

+84
View File
@@ -460,6 +460,90 @@ direct_only_tool_namespaces = ["mcp__history", "mcp__notes"]
Ok(())
}
#[tokio::test]
async fn load_config_resolves_token_budget_config() -> std::io::Result<()> {
for (config_toml, expected) in [
(
"[features]\ntoken_budget = true\n",
TokenBudgetConfig::default(),
),
(
r#"
[features.token_budget]
enabled = true
reminder_threshold_tokens = 16000
reminder_message_template = "Custom reminder: {n_remaining} tokens."
"#,
TokenBudgetConfig {
reminder_threshold_tokens: Some(16_000),
reminder_message_template: "Custom reminder: {n_remaining} tokens.".to_string(),
},
),
] {
let codex_home = tempdir()?;
let config_toml = toml::from_str(config_toml).expect("TOML should deserialize");
let config = Config::load_from_base_config_with_overrides(
config_toml,
ConfigOverrides::default(),
codex_home.abs(),
)
.await?;
assert!(config.features.enabled(Feature::TokenBudget));
assert_eq!(config.token_budget, Some(expected));
}
Ok(())
}
#[tokio::test]
async fn load_config_rejects_invalid_token_budget_reminder_template() -> std::io::Result<()> {
for reminder_message_template in [
String::new(),
"x".repeat(TOKEN_BUDGET_REMINDER_MESSAGE_TEMPLATE_MAX_BYTES + 1),
] {
let codex_home = tempdir()?;
let config_toml = toml::from_str(&format!(
"[features.token_budget]\nenabled = true\nreminder_message_template = {reminder_message_template:?}\n"
))
.expect("TOML should deserialize");
let error = Config::load_from_base_config_with_overrides(
config_toml,
ConfigOverrides::default(),
codex_home.abs(),
)
.await
.expect_err("invalid reminder template should be rejected");
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
}
Ok(())
}
#[tokio::test]
async fn load_config_rejects_non_positive_token_budget_reminder_threshold() -> std::io::Result<()> {
for reminder_threshold_tokens in [-1, 0] {
let codex_home = tempdir()?;
let config_toml = toml::from_str(&format!(
"[features.token_budget]\nenabled = true\nreminder_threshold_tokens = {reminder_threshold_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 reminder threshold should be rejected");
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(
error.to_string(),
"features.token_budget.reminder_threshold_tokens must be positive"
);
}
Ok(())
}
#[tokio::test]
async fn load_config_resolves_rollout_budget() -> std::io::Result<()> {
let codex_home = tempdir()?;