Skip to content

AutoCompact触发算法

从窗口状态计算追踪 AutoCompact 的 pre-turn、mid-turn、模型降档、失败、取消和重复触发边界。

基于rust-v0.150.0
CodexRustContextCompact

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。

rust
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。

rust
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。

rust
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。

rust
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

rust
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 completion
  • codex-rs/core/src/state/auto_compact_window.rs :: advance
rust
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。

rust
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. 重复抑制 ​

重复触发由四层条件共同抑制:

  1. 已结束 Turn 不进入 mid-turn rollover。
  2. take_new_context_window_request 是一次性消费的 request flag。
  3. 成功 compact 推进 window 并降低或重置当前 scope,下一次检查不会沿用旧窗口的同一状态。
  4. 模型降档要求 slug 变化且新窗口确实更小;缺 hash、缺窗口或未超过新限制都直接返回。

因此不能把 token_limit_reached 当作“每次循环必然 compact”。它只是一个候选触发信号,phase、follow-up、 model change、provider capability 和 cancellation 共同决定是否真的执行。

10. 触发测试 ​

测试覆盖正常、失败、取消相关边界及重复触发:

测试输入关键断言覆盖范围未覆盖
auto_compact_runs_after_token_limit_hittoken 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 后先 compactpre-turn 恢复路径rollout 持久化损坏
auto_compact_body_after_prefix_still_caps_at_context_windowbody limit 高于 hard caphard cap 仍触发 compactscope 与 hard cap 的或关系provider 真实 tokenizer
snapshot_request_shape_pre_turn_compaction_context_window_exceededcompact 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_upmid-turn follow-up 跨限reset 发生后才继续 follow-upmid-turn 成功路径取消中的网络流
mid_turn_auto_compact_session_start_hook_stop_blocks_continuationcompact 后 session-start hook 返回 stopTurn 终止且不继续 samplinghook 停止的取消语义网络传输中断

可执行验证:

bash
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”,按以下源码顺序排查:

  1. 查看 ContextWindowTokenStatus 的 scope tokens、scope limit、hard cap 和 token_limit_reached。
  2. 确认处于 pre-turn 还是 post-sampling mid-turn;mid-turn 再看 needs_follow_up。
  3. 查是否存在一次性 new_context request,或模型切换的 hash/窗口条件。
  4. 进入 run_auto_compact,确认 TokenBudget、Remote V2、Remote V1、Local 的选择分支。
  5. 失败时区分 TurnAborted、context window error 和其他 compact error;取消不要当作普通失败重试。
  6. 成功后检查 advance_auto_compact_window、window id、prefill baseline 和历史 ContextCompaction item。

源码搜索入口:

bash
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 和实现选择,就还没有重建完整触发算法。