Add body_after_prefix auto-compact token limit scope (#22870)

## Why

`model_auto_compact_token_limit` has only been able to budget the full
active context. That makes it hard to set a small "growth since
compaction" budget for sessions that preserve a large carried window
prefix: the preserved prefix can consume the whole budget and force
immediate repeated compaction.

This PR adds an opt-in `body_after_prefix` scope so callers can apply
`model_auto_compact_token_limit` to sampled output and later growth
after the current carried prefix, while still forcing compaction before
the full model context window is exhausted.

## What changed

- Adds `AutoCompactTokenLimitScope` with the existing `total` behavior
as the default and a new `body_after_prefix` mode:
[`config_types.rs`](https://github.com/openai/codex/blob/973806b1cb35792555bead994cb3ed94656eb171/codex-rs/protocol/src/config_types.rs#L24-L37).
- Threads `model_auto_compact_token_limit_scope` through config loading,
`Config`, `core-api`, and app-server v2 schema/TypeScript generation.
- Records the first observed input-token count for a `body_after_prefix`
compaction window and uses it as the baseline when deciding whether the
scoped auto-compaction budget is exhausted:
[`turn.rs`](https://github.com/openai/codex/blob/973806b1cb35792555bead994cb3ed94656eb171/codex-rs/core/src/session/turn.rs#L743-L781).
- Keeps a hard context-window cap in `body_after_prefix`, so scoped
budgeting cannot let the active context overrun the usable window.

## Verification

Added compact-suite coverage for the two key behaviors:
`body_after_prefix` does not re-compact just because the carried prefix
is larger than the scoped budget, and it still compacts when the total
active context reaches the configured context window:
[`compact.rs`](https://github.com/openai/codex/blob/973806b1cb35792555bead994cb3ed94656eb171/codex-rs/core/tests/suite/compact.rs#L3003-L3128).
This commit is contained in:
jif-oai authored and GitHub committed 2026-05-19 10:19:46 +00:00
1 parent 05e171094d
commit 80fdd4688f
24 files changed
+933 -58

No files matched your search

+1 -1
View File
@@ -931,7 +931,7 @@ impl Drop for ModelClientSession {
}
impl ModelClientSession {
fn reset_websocket_session(&mut self) {
pub(crate) fn reset_websocket_session(&mut self) {
self.websocket_session.connection = None;
self.websocket_session.last_request = None;
self.websocket_session.last_response_rx = None;
+4
View File
@@ -7711,6 +7711,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
review_model: None,
model_context_window: None,
model_auto_compact_token_limit: None,
model_auto_compact_token_limit_scope: AutoCompactTokenLimitScope::Total,
service_tier: None,
model_provider_id: "openai".to_string(),
model_provider: fixture.openai_provider.clone(),
@@ -8164,6 +8165,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
review_model: None,
model_context_window: None,
model_auto_compact_token_limit: None,
model_auto_compact_token_limit_scope: AutoCompactTokenLimitScope::Total,
service_tier: None,
model_provider_id: "openai-custom".to_string(),
model_provider: fixture.openai_custom_provider.clone(),
@@ -8331,6 +8333,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
review_model: None,
model_context_window: None,
model_auto_compact_token_limit: None,
model_auto_compact_token_limit_scope: AutoCompactTokenLimitScope::Total,
service_tier: None,
model_provider_id: "openai".to_string(),
model_provider: fixture.openai_provider.clone(),
@@ -8483,6 +8486,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
review_model: None,
model_context_window: None,
model_auto_compact_token_limit: None,
model_auto_compact_token_limit_scope: AutoCompactTokenLimitScope::Total,
service_tier: None,
model_provider_id: "openai".to_string(),
model_provider: fixture.openai_provider.clone(),
+8
View File
@@ -77,6 +77,7 @@ use codex_model_provider_info::built_in_model_providers;
use codex_model_provider_info::merge_configured_model_providers;
use codex_models_manager::ModelsManagerConfig;
use codex_protocol::config_types::AltScreenMode;
use codex_protocol::config_types::AutoCompactTokenLimitScope;
use codex_protocol::config_types::ForcedLoginMethod;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary;
@@ -527,6 +528,10 @@ pub struct Config {
/// Token usage threshold triggering auto-compaction of conversation history.
pub model_auto_compact_token_limit: Option<i64>,
/// Controls whether `model_auto_compact_token_limit` applies to the full
/// active context or only tokens after the carried compaction-window prefix.
pub model_auto_compact_token_limit_scope: AutoCompactTokenLimitScope,
/// Key into the model_providers map that specifies which provider to use.
pub model_provider_id: String,
@@ -3373,6 +3378,9 @@ impl Config {
review_model,
model_context_window: cfg.model_context_window,
model_auto_compact_token_limit: cfg.model_auto_compact_token_limit,
model_auto_compact_token_limit_scope: cfg
.model_auto_compact_token_limit_scope
.unwrap_or_default(),
model_provider_id,
model_provider,
cwd: resolved_cwd,
@@ -7,6 +7,7 @@ use std::time::Duration;
use anyhow::anyhow;
use codex_analytics::GuardianReviewAnalyticsResult;
use codex_analytics::GuardianReviewSessionKind;
use codex_protocol::config_types::AutoCompactTokenLimitScope;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::models::PermissionProfile;
@@ -138,6 +139,7 @@ struct GuardianReviewSessionReuseKey {
model_provider: ModelProviderInfo,
model_context_window: Option<i64>,
model_auto_compact_token_limit: Option<i64>,
model_auto_compact_token_limit_scope: AutoCompactTokenLimitScope,
model_reasoning_effort: Option<ReasoningEffortConfig>,
model_reasoning_summary: Option<ReasoningSummaryConfig>,
permissions: Permissions,
@@ -162,6 +164,7 @@ impl GuardianReviewSessionReuseKey {
model_provider: spawn_config.model_provider.clone(),
model_context_window: spawn_config.model_context_window,
model_auto_compact_token_limit: spawn_config.model_auto_compact_token_limit,
model_auto_compact_token_limit_scope: spawn_config.model_auto_compact_token_limit_scope,
model_reasoning_effort: spawn_config.model_reasoning_effort,
model_reasoning_summary: spawn_config.model_reasoning_summary,
permissions: spawn_config.permissions.clone(),
@@ -1155,6 +1158,34 @@ mod tests {
);
}
#[tokio::test]
async fn guardian_review_session_compact_scope_change_invalidates_cached_session() {
let parent_config = crate::config::test_config().await;
let cached_spawn_config = build_guardian_review_session_config(
&parent_config,
/*live_network_config*/ None,
"active-model",
/*reasoning_effort*/ None,
)
.expect("cached guardian config");
let cached_reuse_key =
GuardianReviewSessionReuseKey::from_spawn_config(&cached_spawn_config);
let mut changed_parent_config = parent_config;
changed_parent_config.model_auto_compact_token_limit_scope =
AutoCompactTokenLimitScope::BodyAfterPrefix;
let next_spawn_config = build_guardian_review_session_config(
&changed_parent_config,
/*live_network_config*/ None,
"active-model",
/*reasoning_effort*/ None,
)
.expect("next guardian config");
let next_reuse_key = GuardianReviewSessionReuseKey::from_spawn_config(&next_spawn_config);
assert_ne!(cached_reuse_key, next_reuse_key);
}
#[tokio::test]
async fn guardian_review_session_config_disables_hooks() {
let mut parent_config = crate::config::test_config().await;
+53 -2
View File
@@ -78,6 +78,7 @@ use codex_protocol::approvals::ExecPolicyAmendment;
use codex_protocol::approvals::NetworkPolicyAmendment;
use codex_protocol::approvals::NetworkPolicyRuleAction;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::AutoCompactTokenLimitScope;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::config_types::WebSearchMode;
@@ -288,6 +289,7 @@ use crate::rollout::map_session_init_error;
use crate::session_startup_prewarm::SessionStartupPrewarmHandle;
use crate::shell;
use crate::shell_snapshot::ShellSnapshot;
use crate::state::AutoCompactWindowSnapshot;
use crate::state::PendingRequestPermissions;
use crate::state::SessionServices;
use crate::state::SessionState;
@@ -1089,6 +1091,11 @@ impl Session {
state.get_total_token_usage(state.server_reasoning_included())
}
pub(crate) async fn auto_compact_window_snapshot(&self) -> AutoCompactWindowSnapshot {
let state = self.state.lock().await;
state.auto_compact_window_snapshot()
}
pub(crate) async fn get_total_token_usage_breakdown(&self) -> TotalTokenUsageBreakdown {
let state = self.state.lock().await;
state.history.get_total_token_usage_breakdown()
@@ -1247,11 +1254,41 @@ impl Session {
reconstructed_rollout.reference_context_item,
)
.await;
let prefix_tokens = if matches!(
turn_context.config.model_auto_compact_token_limit_scope,
AutoCompactTokenLimitScope::BodyAfterPrefix
) {
let history = self.clone_history().await;
let base_instructions = self.get_base_instructions().await;
history.estimate_token_count_with_base_instructions(&base_instructions)
} else {
None
};
if let Some(prefix_tokens) = prefix_tokens {
self.set_auto_compact_window_estimated_prefill_for_scope(turn_context, prefix_tokens)
.await;
}
self.set_previous_turn_settings(previous_turn_settings.clone())
.await;
previous_turn_settings
}
async fn set_auto_compact_window_estimated_prefill_for_scope(
&self,
turn_context: &TurnContext,
tokens: i64,
) {
if !matches!(
turn_context.config.model_auto_compact_token_limit_scope,
AutoCompactTokenLimitScope::BodyAfterPrefix
) {
return;
}
let mut state = self.state.lock().await;
state.set_auto_compact_window_estimated_prefill(tokens);
}
fn last_token_info_from_rollout(rollout_items: &[RolloutItem]) -> Option<TokenUsageInfo> {
rollout_items.iter().rev().find_map(|item| match item {
RolloutItem::EventMsg(EventMsg::TokenCount(ev)) => ev.info.clone(),
@@ -2568,8 +2605,11 @@ impl Session {
reference_context_item: Option<TurnContextItem>,
compacted_item: CompactedItem,
) {
self.replace_history(items, reference_context_item.clone())
.await;
{
let mut state = self.state.lock().await;
state.replace_history(items, reference_context_item.clone());
state.start_next_auto_compact_window();
}
self.persist_rollout_items(&[RolloutItem::Compacted(compacted_item)])
.await;
@@ -2927,6 +2967,12 @@ impl Session {
let mut state = self.state.lock().await;
state
.update_token_info_from_usage(token_usage, turn_context.model_context_window());
if matches!(
turn_context.config.model_auto_compact_token_limit_scope,
AutoCompactTokenLimitScope::BodyAfterPrefix
) {
state.ensure_auto_compact_window_server_prefill_from_usage(token_usage);
}
state.token_info()
};
if let Some(token_info) = token_info.as_ref() {
@@ -2974,6 +3020,11 @@ impl Session {
state.set_token_info(Some(info));
}
self.set_auto_compact_window_estimated_prefill_for_scope(
turn_context,
estimated_total_tokens,
)
.await;
self.send_token_count_event(turn_context).await;
}
+147 -54
View File
@@ -75,6 +75,7 @@ use codex_hooks::HookEvent;
use codex_hooks::HookEventAfterAgent;
use codex_hooks::HookPayload;
use codex_hooks::HookResult;
use codex_protocol::config_types::AutoCompactTokenLimitScope;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::error::CodexErr;
@@ -146,26 +147,31 @@ pub(crate) async fn run_turn(
return None;
}
let model_info = turn_context.model_info.clone();
let auto_compact_limit = model_info.auto_compact_token_limit().unwrap_or(i64::MAX);
let mut client_session =
prewarmed_client_session.unwrap_or_else(|| sess.services.model_client.new_session());
// TODO(ccunningham): Pre-turn compaction runs before context updates and the
// new user message are recorded. Estimate pending incoming items (context
// diffs/full reinjection + user input) and trigger compaction preemptively
// when they would push the thread over the compaction threshold.
if let Err(err) = run_pre_sampling_compact(&sess, &turn_context, &mut client_session).await {
if err.to_codex_protocol_error() == CodexErrorInfo::UsageLimitExceeded
&& let Err(err) = sess
.goal_runtime_apply(GoalRuntimeEvent::UsageLimitReached {
turn_context: turn_context.as_ref(),
})
.await
{
warn!("failed to usage-limit active goal after usage-limit error: {err}");
}
error!("Failed to run pre-sampling compact");
return None;
let pre_sampling_compact =
match run_pre_sampling_compact(&sess, &turn_context, &mut client_session).await {
Ok(pre_sampling_compact) => pre_sampling_compact,
Err(err) => {
if err.to_codex_protocol_error() == CodexErrorInfo::UsageLimitExceeded
&& let Err(err) = sess
.goal_runtime_apply(GoalRuntimeEvent::UsageLimitReached {
turn_context: turn_context.as_ref(),
})
.await
{
warn!("failed to usage-limit active goal after usage-limit error: {err}");
}
error!("Failed to run pre-sampling compact");
return None;
}
};
if pre_sampling_compact.reset_client_session {
client_session.reset_websocket_session();
}
sess.record_context_updates_and_set_reference_context_item(turn_context.as_ref())
@@ -342,17 +348,24 @@ pub(crate) async fn run_turn(
can_drain_pending_input = true;
let has_pending_input = sess.input_queue.has_pending_input(&sess.active_turn).await;
let needs_follow_up = model_needs_follow_up || has_pending_input;
let total_usage_tokens = sess.get_total_token_usage().await;
let token_limit_reached = total_usage_tokens >= auto_compact_limit;
let token_status =
auto_compact_token_status(sess.as_ref(), turn_context.as_ref()).await;
let token_limit_reached = token_status.token_limit_reached;
let estimated_token_count =
sess.get_estimated_token_count(turn_context.as_ref()).await;
trace!(
turn_id = %turn_context.sub_id,
total_usage_tokens,
total_usage_tokens = token_status.active_context_tokens,
auto_compact_scope_tokens = token_status.auto_compact_scope_tokens,
estimated_token_count = ?estimated_token_count,
auto_compact_limit,
auto_compact_scope_limit = token_status.auto_compact_scope_limit,
auto_compact_limit_scope = ?turn_context.config.model_auto_compact_token_limit_scope,
auto_compact_window_ordinal = ?token_status.auto_compact_window_ordinal,
auto_compact_window_prefill_tokens = ?token_status.auto_compact_window_prefill_tokens,
full_context_window_limit = ?token_status.full_context_window_limit,
full_context_window_limit_reached = token_status.full_context_window_limit_reached,
token_limit_reached,
model_needs_follow_up,
has_pending_input,
@@ -362,7 +375,7 @@ pub(crate) async fn run_turn(
// as long as compaction works well in getting us way below the token limit, we shouldn't worry about being in an infinite loop.
if token_limit_reached && needs_follow_up {
match run_auto_compact(
let reset_client_session = match run_auto_compact(
&sess,
&turn_context,
&mut client_session,
@@ -372,7 +385,7 @@ pub(crate) async fn run_turn(
)
.await
{
Ok(()) => {}
Ok(reset_client_session) => reset_client_session,
Err(err) => {
if err.to_codex_protocol_error() == CodexErrorInfo::UsageLimitExceeded
&& let Err(err) = sess
@@ -388,6 +401,9 @@ pub(crate) async fn run_turn(
return None;
}
};
if reset_client_session {
client_session.reset_websocket_session();
}
can_drain_pending_input = !model_needs_follow_up;
continue;
}
@@ -743,27 +759,88 @@ async fn track_turn_resolved_config_analytics(
});
}
struct PreSamplingCompactResult {
reset_client_session: bool,
}
#[derive(Debug)]
struct AutoCompactTokenStatus {
// Full active context usage, independent of the configured auto-compact scope.
active_context_tokens: i64,
// Usage counted against `model_auto_compact_token_limit` for the current scope.
auto_compact_scope_tokens: i64,
auto_compact_scope_limit: i64,
full_context_window_limit: Option<i64>,
auto_compact_window_ordinal: Option<u64>,
auto_compact_window_prefill_tokens: Option<i64>,
full_context_window_limit_reached: bool,
token_limit_reached: bool,
}
async fn auto_compact_token_status(
sess: &Session,
turn_context: &TurnContext,
) -> AutoCompactTokenStatus {
let active_context_tokens = sess.get_total_token_usage().await;
let mut auto_compact_window_ordinal = None;
let mut auto_compact_window_prefill_tokens = None;
let (auto_compact_scope_tokens, auto_compact_scope_limit, full_context_window_limit) =
match turn_context.config.model_auto_compact_token_limit_scope {
AutoCompactTokenLimitScope::Total => (
active_context_tokens,
turn_context
.model_info
.auto_compact_token_limit()
.unwrap_or(i64::MAX),
None,
),
AutoCompactTokenLimitScope::BodyAfterPrefix => {
let window = sess.auto_compact_window_snapshot().await;
auto_compact_window_ordinal = Some(window.ordinal);
auto_compact_window_prefill_tokens = window.prefill_input_tokens;
let baseline = window.prefill_input_tokens.unwrap_or(active_context_tokens);
(
active_context_tokens.saturating_sub(baseline),
turn_context
.config
.model_auto_compact_token_limit
.or_else(|| turn_context.model_info.auto_compact_token_limit())
.unwrap_or(i64::MAX),
turn_context.model_context_window(),
)
}
};
let full_context_window_limit_reached =
full_context_window_limit.is_some_and(|full_context_window_limit| {
active_context_tokens >= full_context_window_limit
});
let token_limit_reached =
auto_compact_scope_tokens >= auto_compact_scope_limit || full_context_window_limit_reached;
AutoCompactTokenStatus {
active_context_tokens,
auto_compact_scope_tokens,
auto_compact_scope_limit,
full_context_window_limit,
auto_compact_window_ordinal,
auto_compact_window_prefill_tokens,
full_context_window_limit_reached,
token_limit_reached,
}
}
async fn run_pre_sampling_compact(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,
client_session: &mut ModelClientSession,
) -> CodexResult<()> {
let total_usage_tokens_before_compaction = sess.get_total_token_usage().await;
maybe_run_previous_model_inline_compact(
sess,
turn_context,
client_session,
total_usage_tokens_before_compaction,
)
.await?;
let total_usage_tokens = sess.get_total_token_usage().await;
let auto_compact_limit = turn_context
.model_info
.auto_compact_token_limit()
.unwrap_or(i64::MAX);
// Compact if the total usage tokens are greater than the auto compact limit
if total_usage_tokens >= auto_compact_limit {
run_auto_compact(
) -> CodexResult<PreSamplingCompactResult> {
let mut pre_sampling_compacted =
maybe_run_previous_model_inline_compact(sess, turn_context, client_session).await?;
let mut reset_client_session = pre_sampling_compacted;
let token_status = auto_compact_token_status(sess.as_ref(), turn_context.as_ref()).await;
// Compact if the configured auto-compaction budget or usable context window is exhausted.
if token_status.token_limit_reached {
reset_client_session |= run_auto_compact(
sess,
turn_context,
client_session,
@@ -772,22 +849,26 @@ async fn run_pre_sampling_compact(
CompactionPhase::PreTurn,
)
.await?;
pre_sampling_compacted = true;
}
Ok(())
Ok(PreSamplingCompactResult {
reset_client_session: pre_sampling_compacted && reset_client_session,
})
}
/// Runs pre-sampling compaction against the previous model when switching to a smaller
/// context-window model.
///
/// Returns `Err(_)` only when compaction was attempted and failed.
/// Returns `Ok(true)` when compaction ran successfully, `Ok(false)` when compaction was skipped
/// because the model/context-window preconditions were not met, and `Err(_)` only when compaction
/// was attempted and failed.
async fn maybe_run_previous_model_inline_compact(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,
client_session: &mut ModelClientSession,
total_usage_tokens: i64,
) -> CodexResult<()> {
) -> CodexResult<bool> {
let Some(previous_turn_settings) = sess.previous_turn_settings().await else {
return Ok(());
return Ok(false);
};
let previous_model_turn_context = Arc::new(
turn_context
@@ -796,20 +877,31 @@ async fn maybe_run_previous_model_inline_compact(
);
let Some(old_context_window) = previous_model_turn_context.model_context_window() else {
return Ok(());
return Ok(false);
};
let Some(new_context_window) = turn_context.model_context_window() else {
return Ok(());
return Ok(false);
};
let new_auto_compact_limit = turn_context
.model_info
.auto_compact_token_limit()
.unwrap_or(i64::MAX);
let should_run = total_usage_tokens > new_auto_compact_limit
let active_context_tokens = sess.get_total_token_usage().await;
let previous_model_limit_reached = match turn_context
.config
.model_auto_compact_token_limit_scope
{
AutoCompactTokenLimitScope::Total => {
let new_auto_compact_limit = turn_context
.model_info
.auto_compact_token_limit()
.unwrap_or(i64::MAX);
active_context_tokens > new_auto_compact_limit
|| active_context_tokens >= new_context_window
}
AutoCompactTokenLimitScope::BodyAfterPrefix => active_context_tokens >= new_context_window,
};
let should_run = previous_model_limit_reached
&& previous_model_turn_context.model_info.slug != turn_context.model_info.slug
&& old_context_window > new_context_window;
if should_run {
run_auto_compact(
let _ = run_auto_compact(
sess,
&previous_model_turn_context,
client_session,
@@ -818,8 +910,9 @@ async fn maybe_run_previous_model_inline_compact(
CompactionPhase::PreTurn,
)
.await?;
return Ok(true);
}
Ok(())
Ok(false)
}
async fn run_auto_compact(
@@ -829,7 +922,7 @@ async fn run_auto_compact(
initial_context_injection: InitialContextInjection,
reason: CompactionReason,
phase: CompactionPhase,
) -> CodexResult<()> {
) -> CodexResult<bool> {
if should_use_remote_compact_task(turn_context.provider.info()) {
if turn_context.features.enabled(Feature::RemoteCompactionV2) {
run_inline_remote_auto_compact_task_v2(
@@ -841,7 +934,7 @@ async fn run_auto_compact(
phase,
)
.await?;
return Ok(());
return Ok(false);
}
run_inline_remote_auto_compact_task(
Arc::clone(sess),
@@ -861,7 +954,7 @@ async fn run_auto_compact(
)
.await?;
}
Ok(())
Ok(true)
}
pub(super) fn collect_explicit_app_ids_from_skill_items(
@@ -0,0 +1,145 @@
use codex_protocol::protocol::TokenUsage;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct AutoCompactWindowSnapshot {
pub(crate) ordinal: u64,
pub(crate) prefill_input_tokens: Option<i64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AutoCompactWindowPrefill {
ServerObserved(i64),
Estimated(i64),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct AutoCompactWindow {
ordinal: u64,
/// Absolute input-token baseline for the current compaction window.
///
/// `body_after_prefix` subtracts this from later active-context usage. It is
/// not the growth itself; server-observed usage replaces estimated
/// resume/recompute baselines when available.
prefill_input_tokens: Option<AutoCompactWindowPrefill>,
}
impl AutoCompactWindow {
pub(super) fn new() -> Self {
Self {
ordinal: 1,
prefill_input_tokens: None,
}
}
pub(super) fn clear_prefill(&mut self) {
self.prefill_input_tokens = None;
}
pub(super) fn start_next(&mut self) {
self.ordinal = self.ordinal.saturating_add(1);
self.clear_prefill();
}
/// Records the request-input side of the first server usage sample. The
/// sampled output from that response is body growth and should remain
/// counted against the scoped auto-compact budget.
pub(super) fn ensure_server_observed_prefill_from_usage(&mut self, usage: &TokenUsage) {
if matches!(
self.prefill_input_tokens,
Some(AutoCompactWindowPrefill::ServerObserved(_))
) {
return;
}
self.prefill_input_tokens = Some(AutoCompactWindowPrefill::ServerObserved(
usage.input_tokens.max(0),
));
}
pub(super) fn set_estimated_prefill(&mut self, tokens: i64) {
if matches!(
self.prefill_input_tokens,
Some(AutoCompactWindowPrefill::ServerObserved(_))
) {
return;
}
self.prefill_input_tokens = Some(AutoCompactWindowPrefill::Estimated(tokens.max(0)));
}
pub(super) fn snapshot(&self) -> AutoCompactWindowSnapshot {
let prefill_input_tokens = match self.prefill_input_tokens {
Some(AutoCompactWindowPrefill::ServerObserved(tokens))
| Some(AutoCompactWindowPrefill::Estimated(tokens)) => Some(tokens),
None => None,
};
AutoCompactWindowSnapshot {
ordinal: self.ordinal,
prefill_input_tokens,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn tracks_prefill_and_window_boundaries() {
let mut window = AutoCompactWindow::new();
assert_eq!(
window.snapshot(),
AutoCompactWindowSnapshot {
ordinal: 1,
prefill_input_tokens: None,
}
);
window.set_estimated_prefill(/*tokens*/ 150);
assert_eq!(
window.snapshot(),
AutoCompactWindowSnapshot {
ordinal: 1,
prefill_input_tokens: Some(150),
}
);
window.ensure_server_observed_prefill_from_usage(&TokenUsage {
input_tokens: 120,
total_tokens: 170,
..Default::default()
});
assert_eq!(
window.snapshot(),
AutoCompactWindowSnapshot {
ordinal: 1,
prefill_input_tokens: Some(120),
}
);
window.ensure_server_observed_prefill_from_usage(&TokenUsage {
input_tokens: 130,
total_tokens: 180,
..Default::default()
});
window.set_estimated_prefill(/*tokens*/ 90);
assert_eq!(
window.snapshot(),
AutoCompactWindowSnapshot {
ordinal: 1,
prefill_input_tokens: Some(120),
}
);
window.start_next();
assert_eq!(
window.snapshot(),
AutoCompactWindowSnapshot {
ordinal: 2,
prefill_input_tokens: None,
}
);
}
}
+2
View File
@@ -1,7 +1,9 @@
mod auto_compact_window;
mod service;
mod session;
mod turn;
pub(crate) use auto_compact_window::AutoCompactWindowSnapshot;
pub(crate) use service::SessionServices;
pub(crate) use session::SessionState;
pub(crate) use turn::ActiveTurn;
+26
View File
@@ -5,6 +5,8 @@ use codex_protocol::models::ResponseItem;
use codex_sandboxing::policy_transforms::merge_permission_profiles;
use std::collections::HashSet;
use super::auto_compact_window::AutoCompactWindow;
use super::auto_compact_window::AutoCompactWindowSnapshot;
use crate::context_manager::ContextManager;
use crate::session::PreviousTurnSettings;
use crate::session::session::SessionConfiguration;
@@ -26,6 +28,8 @@ pub(crate) struct SessionState {
/// model/realtime handling on subsequent regular turns (including full-context
/// reinjection after resume or `/compact`).
previous_turn_settings: Option<PreviousTurnSettings>,
/// Runtime accounting state for the active auto-compaction window.
auto_compact_window: AutoCompactWindow,
/// Startup prewarmed session prepared during session initialization.
pub(crate) startup_prewarm: Option<SessionStartupPrewarmHandle>,
pub(crate) active_connector_selection: HashSet<String>,
@@ -45,6 +49,7 @@ impl SessionState {
server_reasoning_included: false,
mcp_dependency_prompted: HashSet::new(),
previous_turn_settings: None,
auto_compact_window: AutoCompactWindow::new(),
startup_prewarm: None,
active_connector_selection: HashSet::new(),
pending_session_start_source: None,
@@ -94,6 +99,7 @@ impl SessionState {
self.history.replace(items);
self.history
.set_reference_context_item(reference_context_item);
self.auto_compact_window.clear_prefill();
}
pub(crate) fn set_token_info(&mut self, info: Option<TokenUsageInfo>) {
@@ -117,6 +123,26 @@ impl SessionState {
self.history.update_token_info(usage, model_context_window);
}
pub(crate) fn ensure_auto_compact_window_server_prefill_from_usage(
&mut self,
usage: &TokenUsage,
) {
self.auto_compact_window
.ensure_server_observed_prefill_from_usage(usage);
}
pub(crate) fn set_auto_compact_window_estimated_prefill(&mut self, tokens: i64) {
self.auto_compact_window.set_estimated_prefill(tokens);
}
pub(crate) fn start_next_auto_compact_window(&mut self) {
self.auto_compact_window.start_next();
}
pub(crate) fn auto_compact_window_snapshot(&self) -> AutoCompactWindowSnapshot {
self.auto_compact_window.snapshot()
}
pub(crate) fn token_info(&self) -> Option<TokenUsageInfo> {
self.history.token_info()
}
+19
View File
@@ -1,5 +1,6 @@
use super::*;
use crate::session::tests::make_session_configuration_for_tests;
use crate::state::AutoCompactWindowSnapshot;
use codex_protocol::protocol::CreditsSnapshot;
use codex_protocol::protocol::RateLimitWindow;
use pretty_assertions::assert_eq;
@@ -61,6 +62,24 @@ async fn set_rate_limits_defaults_limit_id_to_codex_when_missing() {
);
}
#[tokio::test]
async fn replace_history_clears_auto_compact_window_prefill_without_advancing() {
let session_configuration = make_session_configuration_for_tests().await;
let mut state = SessionState::new(session_configuration);
state.start_next_auto_compact_window();
state.set_auto_compact_window_estimated_prefill(/*tokens*/ 100);
state.replace_history(Vec::new(), /*reference_context_item*/ None);
assert_eq!(
state.auto_compact_window_snapshot(),
AutoCompactWindowSnapshot {
ordinal: 2,
prefill_input_tokens: None,
}
);
}
#[tokio::test]
async fn set_rate_limits_defaults_to_codex_when_limit_id_missing_after_other_bucket() {
let session_configuration = make_session_configuration_for_tests().await;