mirror of
https://github.com/openai/codex.git
synced 2026-11-03 00:21:31 +00:00
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:
1 parent
05e171094d
commit
80fdd4688f
24 files changed
+933
-58
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user