AutoCompact触发算法
AutoCompact 不是“token 超过阈值就调用 compact”的单一分支。Codex 先在当前 scope 和有效 hard cap 上计算 ContextWindowTokenStatus,再根据发生时机选择 pre-turn 或 mid-turn;mid-turn 还要求模型或输入队列确实需要 继续采样。真正执行时,TokenBudget、Remote Compact V2、Remote Compact V1 和 Local Compact 又是四条不同的 实现路径。失败和取消不会推进新的 context window,成功后才会清理或重置窗口状态。
本文面向已经读过 ContextWindow与模型限制、 TokenBudget计算与传播 和 RolloutBudget与截断策略 的读者。本文只讲“何时触发、选择哪条 执行路径、触发后如何结束”,不讲本地 compact prompt 的具体内容,也不讲远程 request body;这些由后续 本地Compact提示词构造~Compact模型Fallback 分别负责。读完后应能从日志中的 token_limit_reached、phase 和 reason 反推出触发位置, 并判断一次失败是否可能被错误地重复执行。
1. 触发边界
AutoCompact 的判定输入不是单个 active_context_tokens,而是 ContextWindowTokenStatus 中的多个字段。 token_limit_reached 是 scope limit(可含 fallback buffer)和 full context hard cap 的或; base_window_tokens_remaining 则是没有 buffer 的基础剩余量,供 reminder 和 fallback 使用。
BodyAfterPrefix 时,prefix 由 prefill_input_tokens 抵扣,但 hard cap 仍比较完整 active context;因此“scope 未达到”不能推出“不会 compact”。相反,窗口未知时 full hard cap 不存在,只有显式 auto-compact limit 才可能 触发 scope 路径。
2. PreTurn路径
pre-turn 发生在正式 sampling step 建立之前。Codex 先检查模型切换带来的 compaction,再重新计算窗口状态;只要 token_limit_reached 为真,就捕获 step context 并以 CompactionReason::ContextLimit、 CompactionPhase::PreTurn 执行 compact。
源码位置:codex-rs/core/src/session/turn.rs :: run_pre_sampling_compact。
async fn run_pre_sampling_compact(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,
client_session: &mut ModelClientSession,
cancellation_token: &CancellationToken,
) -> CodexResult<()> {
maybe_run_previous_model_inline_compact(sess, turn_context, client_session, cancellation_token)
.await?;
let token_status =
super::context_window::context_window_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 {
// Pre-turn compaction runs before run_turn creates the normal sampling step.
let step_context = sess
.capture_step_context(Arc::clone(turn_context), cancellation_token)
.await?;
run_auto_compact(
sess,
step_context,
/*fallback_step_context*/ None,
client_session,
InitialContextInjection::DoNotInject,
CompactionReason::ContextLimit,
CompactionPhase::PreTurn,
)
.await?;
}
Ok(())
}pre-turn 的 DoNotInject 意味着新窗口建立后,下一次普通 Turn 负责重新注入初始上下文。输入队列中的新用户 消息不会被当作 compact 摘要输入的一部分;这保证触发 compact 的新消息在压缩完成后才进入正常采样请求。
3. MidTurn路径
mid-turn 检查发生在一次模型采样完成之后。模型可能已经结束 Turn,也可能要求 follow-up;输入队列还可能在模型 运行期间收到新的 steer。只有 model_needs_follow_up || has_pending_input 为真时,达到限制才会 rollover。
源码位置:codex-rs/core/src/session/turn.rs :: post-sampling token decision。
let (has_pending_input, token_status) = async {
let has_pending_input = sess.input_queue.has_pending_input(&sess.active_turn).await;
let token_status = super::context_window::context_window_token_status(
sess.as_ref(),
turn_context.as_ref(),
)
.await;
(has_pending_input, token_status)
}
.await;
let needs_follow_up = model_needs_follow_up || has_pending_input;
let token_limit_reached = token_status.token_limit_reached;
let should_roll_over =
needs_follow_up && (sess.take_new_context_window_request().await || token_limit_reached);
let allow_auto_compact_fallback = !should_roll_over && !token_limit_reached;如果模型已经给出最终答案,needs_follow_up 为假,当前 Turn 不会因为 token limit 再启动 mid-turn compact; 下一个采样从 pre-turn 路径开始。这个条件是重复触发抑制的第一层:已结束 Turn 不会在同一采样之后被强制多做 一次 compact。
4. 模型降档
模型切换是 pre-turn 的另一条触发来源。maybe_run_previous_model_inline_compact 先判断 compaction hash; 两个 hash 都存在且不同,则使用 CompactionReason::CompHashChanged。如果没有 hash 变化,再比较旧/新有效窗口, 只有新模型更小且历史超过新模型限制时才使用 ModelDownshift。
源码位置:codex-rs/core/src/session/turn.rs :: comp_hash_changed, maybe_run_previous_model_inline_compact。
fn comp_hash_changed(previous: Option<&str>, current: Option<&str>) -> bool {
previous
.zip(current)
.is_some_and(|(previous, current)| previous != current)
}
let Some(old_context_window) = previous_model_turn_context.model_context_window() else {
return Ok(());
};
let Some(new_context_window) = turn_context.model_context_window() else {
return Ok(());
};
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,
};降档 compact 只有在模型 slug 变化且 old_context_window > new_context_window 时执行。它不因为任意模型设置 变化就触发,也不在无法计算新旧窗口时猜测。BodyAfterPrefix 分支只用新 hard cap 做降档判断,避免把旧窗口 prefix 当作新模型 body budget。
5. 路径选择
run_auto_compact 是触发算法和具体实现之间的分界。触发方只传入 Session、step context、注入策略、reason 和 phase;实现选择读取当前 Turn 的 feature 与 provider capability。
源码位置:codex-rs/core/src/session/turn.rs :: run_auto_compact。
if turn_context.config.features.enabled(Feature::TokenBudget) {
crate::compact_token_budget::run_inline_auto_compact_task(
Arc::clone(sess),
step_context,
initial_context_injection,
)
.await?;
return Ok(());
}
match turn_context.provider.capabilities().remote_compaction {
RemoteCompactionSupport::V2
if turn_context
.config
.features
.enabled(Feature::RemoteCompactionV2) =>
{
run_inline_remote_auto_compact_task_v2(
Arc::clone(sess),
step_context,
fallback_step_context,
client_session,
initial_context_injection,
reason,
phase,
)
.await?;
}
RemoteCompactionSupport::V1 | RemoteCompactionSupport::V2 => {
run_inline_remote_auto_compact_task(
Arc::clone(sess),
step_context,
fallback_step_context,
client_session.turn_state(),
initial_context_injection,
reason,
phase,
)
.await?;
}
RemoteCompactionSupport::Unsupported => {
run_inline_auto_compact_task(
Arc::clone(sess),
Arc::clone(turn_context),
initial_context_injection,
reason,
phase,
)
.await?;
}
}TokenBudget feature 优先级高于 provider capability:开启后直接建立新窗口,不走远程或本地摘要模型请求。没有 TokenBudget 时,V2 capability 只有在对应 feature 开启才使用 V2;V2 capability 但 feature 关闭会回落到 V1 接口。provider 不支持远程 compact 时才进入 local 摘要路径。
当前 run_auto_compact 的选择顺序是固定的:先检查 Feature::TokenBudget,开启时直接走 compact_token_budget::run_inline_auto_compact_task;否则根据 provider 的 remote capability 和 RemoteCompactionV2 feature 选择 V2、V1 或 local。触发算法本身不重试,也不吞掉 compact error,所有实现都 通过 await? 把失败返回给 Turn loop。
源码位置:codex-rs/core/src/session/turn.rs :: run_auto_compact
if turn_context.config.features.enabled(Feature::TokenBudget) {
crate::compact_token_budget::run_inline_auto_compact_task(
Arc::clone(sess),
step_context,
initial_context_injection,
)
.await?;
return Ok(());
}6. 成功边界
成功并不只是模型返回摘要。各实现最终都必须推进 auto-compact window,并让下一次请求看到新的 window id、 新的 prefill baseline 或新的历史布局。以 local compact 为例,成功路径在写入 ContextCompactionItem 后调用 advance_auto_compact_window。
相关源码:
codex-rs/core/src/compact.rs :: compaction completioncodex-rs/core/src/state/auto_compact_window.rs :: advance
let (window_number, window_ids) = sess.advance_auto_compact_window().await;
sess.record_compaction_item(
turn_context,
compaction_item,
window_number,
window_ids,
)
.await?;窗口推进会清除 new_context_window_requested、提醒投递位和 fallback 投递位;因此新窗口可以重新触发依赖 window id 的提示。它不会把 rollout budget 或历史 token usage 清零,预算账本和 context history 是不同状态。
7. 失败路径
pre-turn local compact 失败时,当前用户 Turn 会得到 context-window error,且请求形状不包含刚提交的 incoming user message。remote/local 实现的具体错误转换由各 compact 模块负责,但触发层只做 await? 传播,不会在 run_pre_sampling_compact 中把失败当成“继续采样”。
失败不等于重复触发保护失效。当前调用返回后,下一次用户 Turn 可能再次满足 token limit 并重新尝试;这是新的 pre-turn 触发,不是同一个 run_auto_compact 递归重试。远程请求是否有内部 retry,由远程 compact 实现和 client 配置决定,不能从触发层推断。
8. 取消路径
触发链接收 CancellationToken,并在捕获 step context、模型请求、hook 和 compact 子任务之间传递。取消后, compact 返回 TurnAborted;mid-turn 的特殊处理只把 TurnAborted 原样返回,其他错误才转成当前 Turn 的 protocol error。
源码位置:codex-rs/core/src/session/turn.rs :: mid-turn compact error handling。
if should_roll_over {
if let Err(err) = run_auto_compact(
&sess,
Arc::clone(&step_context),
/*fallback_step_context*/ None,
&mut client_session,
InitialContextInjection::BeforeLastUserMessage {
world_state: Arc::clone(&world_state),
step_context: Arc::clone(&step_context),
},
CompactionReason::ContextLimit,
CompactionPhase::MidTurn,
)
.await
{
if matches!(err.details(), CodexErrorDetails::TurnAborted) {
return Err(err);
}
let error = err.to_codex_protocol_error();
sess.emit_turn_error_lifecycle(turn_context.as_ref(), error.clone())
.await;
return Ok(None);
}
}取消的关键边界是“没有成功推进窗口”。如果取消发生在 compact 请求中,历史和 window id 仍保持取消前状态; 下一次可由 pre-turn 重新判定。hook 返回 Stopped 也通过 TurnAborted 进入同一取消语义。
9. 重复抑制
重复触发由四层条件共同抑制:
- 已结束 Turn 不进入 mid-turn rollover。
take_new_context_window_request是一次性消费的 request flag。- 成功 compact 推进 window 并降低或重置当前 scope,下一次检查不会沿用旧窗口的同一状态。
- 模型降档要求 slug 变化且新窗口确实更小;缺 hash、缺窗口或未超过新限制都直接返回。
因此不能把 token_limit_reached 当作“每次循环必然 compact”。它只是一个候选触发信号,phase、follow-up、 model change、provider capability 和 cancellation 共同决定是否真的执行。
10. 触发测试
测试覆盖正常、失败、取消相关边界及重复触发:
| 测试 | 输入 | 关键断言 | 覆盖范围 | 未覆盖 |
|---|---|---|---|---|
auto_compact_runs_after_token_limit_hit | token limit 200000,usage 跨阈值 | compact request 出现在下一次采样前 | pre-turn 触发 | provider 摘要质量 |
multiple_auto_compact_per_task_runs_after_token_limit_hit | 同一任务多次跨阈值 | 多个独立窗口按预期触发 | 必要 follow-up 下的重复触发边界 | 无限任务不会无限增长 |
auto_compact_runs_after_resume_when_token_usage_is_over_limit | 恢复时历史 usage 已超限 | resume 后先 compact | pre-turn 恢复路径 | rollout 持久化损坏 |
auto_compact_body_after_prefix_still_caps_at_context_window | body limit 高于 hard cap | hard cap 仍触发 compact | scope 与 hard cap 的或关系 | provider 真实 tokenizer |
snapshot_request_shape_pre_turn_compaction_context_window_exceeded | compact provider 返回 context_length_exceeded | 不带 incoming user 的 compact request,Turn 报错 | 远程/本地失败边界 | 其他 HTTP 错误分类 |
auto_compact_clamps_config_limit_to_context_window | 配置 limit 高于窗口 | compact limit 被窗口约束 | 配置边界 | 动态模型切换所有组合 |
token_budget_mid_turn_auto_compaction_resets_before_active_follow_up | mid-turn follow-up 跨限 | reset 发生后才继续 follow-up | mid-turn 成功路径 | 取消中的网络流 |
mid_turn_auto_compact_session_start_hook_stop_blocks_continuation | compact 后 session-start hook 返回 stop | Turn 终止且不继续 sampling | hook 停止的取消语义 | 网络传输中断 |
可执行验证:
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all auto_compact_runs_after_token_limit_hit
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all multiple_auto_compact_per_task_runs_after_token_limit_hit
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all auto_compact_runs_after_resume_when_token_usage_is_over_limit
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all auto_compact_body_after_prefix_still_caps_at_context_window
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all snapshot_request_shape_pre_turn_compaction_context_window_exceeded
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all auto_compact_clamps_config_limit_to_context_window
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all token_budget_mid_turn_auto_compaction_resets_before_active_follow_up
RUST_MIN_STACK=16777216 cargo test -p codex-core --test all mid_turn_auto_compact_session_start_hook_stop_blocks_continuation这些测试证明的是触发条件、请求顺序、错误边界和窗口更新,不证明摘要保留了所有语义,也不证明远程 provider 会把本地有效窗口值当成其内部 tokenizer 的精确上限。
11. 调试路径
遇到“没有 compact”“compact 过早”或“compact 后又立即 compact”,按以下源码顺序排查:
- 查看
ContextWindowTokenStatus的 scope tokens、scope limit、hard cap 和token_limit_reached。 - 确认处于 pre-turn 还是 post-sampling mid-turn;mid-turn 再看
needs_follow_up。 - 查是否存在一次性
new_contextrequest,或模型切换的 hash/窗口条件。 - 进入
run_auto_compact,确认 TokenBudget、Remote V2、Remote V1、Local 的选择分支。 - 失败时区分
TurnAborted、context window error 和其他 compact error;取消不要当作普通失败重试。 - 成功后检查
advance_auto_compact_window、window id、prefill baseline 和历史 ContextCompaction item。
源码搜索入口:
rg -n "run_pre_sampling_compact|context_window_token_status|should_roll_over|run_auto_compact|CompactionPhase|CompactionReason|advance_auto_compact_window" \
codex-rs/core/src/session/turn.rs codex-rs/core/src/session/context_window.rs codex-rs/core/src/compact*.rs如果只看到 token_limit_reached 而没有同时检查 phase、follow-up 和实现选择,就还没有重建完整触发算法。
