Skip to content

コンテキスト圧縮の進化

源码版本rust-v0.145.0

codex は対話履歴 (conversation history) を有限の資源として扱う。累積 token がモデルのコンテキストウィンドウ (context window) に近づいたら、古い履歴を要約に折り畳んで空きを作り、次の turn が続けて走れるようにする。この圧縮 (compaction) パスは core/src/compact*.rs の中で何度も書き直された:ローカルでモデル自身に要約を生成する版、後に追加された server-side のリモート compaction、さらにその後の v2 進化。三つの実装は同じ hook と analytics lifecycle を共有するが、得られる「要約」の入手元は全く異なる。

責務

  1. 圧縮の起爆タイミング判定:auto(token 閾値ヒット)、manual(ユーザが /compact を叩く)、CompactionReason(モデルダウングレード、comp hash 変更など)の三つのソースを run_inline_auto_compact_taskrun_compact_task が振り分ける。
  2. 要約 turn の実行:ローカル版は自分で Responses API を呼んで SUMMARIZATION_PROMPT の結果を取り、remote 版は server に ResponseItem::Compaction を直接吐かせ、v2 は server に単一の compaction item を厳密に返すことを要求する。
  3. replacement history の構築:要約 + 末尾の数件のユーザメッセージを新履歴にまとめ、古い履歴は丸ごと捨てる。build_compacted_history_with_limit が 20k token 上限でユーザメッセージを切り詰める。
  4. コンテキストウィンドウ (context window) の管理:圧縮のたびに新ウィンドウを開き、advance_auto_compact_window / start_new_context_windowwindow_idprevious_window_id を更新し、後続の token カウントと rollout trace に使わせる。
  5. hook と analytics の露出:PreCompactHookOutcome / PostCompactHookOutcome でプラグインが圧縮を止められるようにし、CompactionAnalyticsAttempt が各試行の trigger、reason、implementation、phase を上报する。

設計動機

最初のローカル圧縮 (compact.rs) は「クライアントが自分でもう一度モデルにリクエストを送って要約を作る」方式だった。問題は要約自体も token を食うことと、要約品質がモデルの SUMMARIZATION_PROMPT 実行に依存することだった。モデル provider が server-side compaction をネイティブサポートするようになると compact_remote.rs が生えた:server が一度の Responses リクエストで折り畳んだ Compaction item を直接返し、クライアントは要約生成に関与しない。

しかし v1 remote は履歴全体を server に送り、server は要約だけを返し、クライアントが自分で function call 出力を切り詰め直す必要があった。v2 (compact_remote_v2.rs) はこれを引き締める:server は要約を返すだけでなく、どのメッセージを保留 (retained messages) するかまで決め、クライアントは単一の Compaction 出力を集めるだけにする。エラー時は fatal にする。同時に v2 は RETAINED_MESSAGE_TOKEN_BUDGET = 64_000 の上限を導入し、保留メッセージ自体がコンテキストを食い尽くすのを防ぐ。

compact_token_budget.rs は別の分岐で、要約生成を飛ばして直接新ウィンドウを開く。このパスは「もう一度モデル呼び出しをして要約を作りたくない」シーン、例えばユーザが「コンテキストを消去」と明示した場合のために残されている。ただし完全な hook と ContextCompaction turn item lifecycle を通り、モデルは呼ばないだけだ。

主要ファイル

codex-rs/core/src/compact.rs:150-219run_compact_task_inner。ローカル圧縮の主入口で、pre/post hook と analytics ラップを含む。codex-rs/core/src/compact.rs:221-378run_compact_task_inner_impl。モデル stream を走らせ、リトライし、最後に replace_compacted_history を呼ぶ。codex-rs/core/src/compact.rs:602-663build_compacted_history_with_limit。ユーザメッセージを末尾から前に詰め、20k token 上限に達するまで詰め込む。codex-rs/core/src/compact_remote.rs:76-107run_remote_compact_task。remote v1 の入口、ResponsesCompact とマーク。codex-rs/core/src/compact_remote_v2.rs:85-115run_remote_compact_task の v2 版、ResponsesCompactionV2 とマーク。codex-rs/core/src/compact_remote_v2.rs:385-443collect_compaction_output。server に compaction item 一つだけを返すことを強制し、違反なら fatal。codex-rs/core/src/compact_remote_v2_attempt.rs:32-142run_remote_compact_v2_attempt。先に trim_function_call_history_to_fit_context_window をしてからリクエストを送る。codex-rs/core/src/compact_model_fallback.rs:8-19should_retry_with_current_model。どのエラーがモデル切り替え再試行に値するかを定義。codex-rs/core/src/compact_token_budget.rs:64-90 — token-budget 圧縮。要約を飛ばして直接新ウィンドウを開く。

InitialContextInjection は一見些細な列挙型だが重要:圧縮後の新履歴に initial context を挿入するかを決める。pre-turn / manual 圧縮は DoNotInject を使い、次の通常 turn に自分で再注入させる。mid-turn 圧縮は BeforeLastUserMessage を必ず使い、モデルが「要約の後には最後のメッセージが来る」と学習しているため、最後の real user message の前にコンテキストを挿入しなければならない。

rust
// compact.rs:56-69 — mid-turn 与 pre-turn 压缩的上下文注入策略
#[derive(Debug)]
pub(crate) enum InitialContextInjection {
    BeforeLastUserMessage(Arc<WorldState>),
    DoNotInject,
}

ローカル圧縮の核心は、履歴全体 + SUMMARIZATION_PROMPT をモデルに送り、stream が終わった後に最後の assistant メッセージを要約として取り出すことだ。注意点として、モデル出力をそのまま新履歴にするのではなく、format!("{SUMMARY_PREFIX}\n{summary_suffix}") でプレフィックスを被せ、後で is_summary_message が識別しやすくする。

rust
// compact.rs:323-336 — 从模型 stream 结果里取出摘要并打标
let history_snapshot = sess.clone_history().await;
let history_items = history_snapshot.raw_items();
let summary_suffix = get_last_assistant_message_from_turn(history_items).unwrap_or_default();
let summary_text = format!("{SUMMARY_PREFIX}\n{summary_suffix}");
let user_messages = collect_user_messages(history_items);

let mut new_history = build_compacted_history(Vec::new(), &user_messages, &summary_text);

v2 remote の关键制約は server がちょうど一つResponseItem::Compaction を返すことで、違反なら fatal:

rust
// compact_remote_v2.rs:419-434 — server 返回多条 compaction 视为协议错误
if !saw_completed {
    return Err(CodexErr::Stream(
        "remote compaction v2 stream closed before response.completed".to_string(),
        None,
    ));
}
if compaction_count != 1 {
    return Err(CodexErr::Fatal(format!(
        "remote compaction v2 expected exactly one compaction output item, got {compaction_count} from {output_item_count} output items"
    )));
}

データフロー

境界と失敗

  • ContextWindowExceeded の自己修復:ローカル圧縮が要約を走らせる時に自身の prompt がウィンドウを超えたら、history.remove_first_item() で最古の項目を削ってリトライし、残り一つでもまだ超える場合にだけエラーで抜ける (codex-rs/core/src/compact.rs:285-300)。
  • Stream リトライ上限はより厳しい:v2 remote はリトライ回数を MAX_REMOTE_COMPACTION_V2_STREAM_RETRIES = 2 に clamp する。圧縮自体が長時間かかるため、汎用 stream のリトライ予算を流用すると一回の圧縮で長く詰まるのを避けるため (codex-rs/core/src/compact_remote_v2.rs:54-57)。
  • モデルフォールバック:InvalidRequestContextWindowExceededServerOverloaded などのエラーは should_retry_with_current_model を発火し、現在のメインモデルに切り替えて再試行する。provider 特有の問題で圧縮が詰まるのを避ける (codex-rs/core/src/compact_model_fallback.rs:8-19)。
  • mid-turn の注入位置:要約後の新履歴で、initial context は単純に末尾に push せず、最後の real user message の前に splice する。real user message が無ければ要約の前に挿入し、要約が常に最後になるようにする (codex-rs/core/src/compact.rs:542-587)。
  • token-budget 圧縮は要約なし:start_new_context_window で直接ウィンドウを切り替え、モデルは呼ばない。それでも ContextCompaction turn item を発火し、パスの違いで hook と UI がイベントを取りこぼさないようにする (codex-rs/core/src/compact_token_budget.rs:76-82)。

まとめ

圧縮周りのコードの複雑さは主に「三つの実装が共存する」ことに由来する:ローカル stream、remote v1、remote v2 はそれぞれ独立した entry と inner_impl を持つが、CompactionAnalyticsAttempt、hook、InitialContextInjection の骨格を共有する。新機能を書く時はまず provider がどのパスを通るかを確認する——should_use_remote_compact_task が分岐点——その後に対応ファイルを見る。次の turn でモデル呼び出し自体がどう発火するかは クライアントと Responses API へ、圧縮結果がどう履歴に保存されるかは セッションスレッドとメッセージ履歴 へ。