Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions codex-rs/core/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
73 changes: 73 additions & 0 deletions codex-rs/core/src/config/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},
),
] {
Expand All @@ -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 [
Expand Down Expand Up @@ -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()?;
Expand Down
39 changes: 39 additions & 0 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
}
}
}
Expand Down Expand Up @@ -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,
}))
}

Expand Down
4 changes: 4 additions & 0 deletions codex-rs/core/src/session/config_lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
}))
);

Expand Down
8 changes: 8 additions & 0 deletions codex-rs/features/src/feature_configs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading