0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

この記事はCodexのコードを実際に確認しながら/compactコマンドを詳しく解説した記事になります。辞書代わりに使っていただければと思います。

目次

  1. 概要
  2. コマンドの目的
  3. 実行フロー
  4. 使用されるプロンプト
  5. 会話履歴の保存場所
  6. 技術的な詳細
  7. 使用例と注意点
  8. セッションの再開(Resume)と会話履歴の復元
  9. カスタムCompactの実現可能性
  10. まとめ

概要

/compact は、Codexの会話履歴を要約してコンテキストウィンドウの制限に達しないようにするためのスラッシュコマンドです。長時間の会話セッションでトークン数が増加した際に、会話の文脈を保持しながらトークン使用量を削減します。

コマンド形式: /compact

説明: 「summarize conversation to prevent hitting the context limit」(コンテキスト制限に達するのを防ぐために会話を要約する)


コマンドの目的

なぜ /compact が必要か?

LLM(大規模言語モデル)にはコンテキストウィンドウという制限があります。これは、モデルが一度に処理できるトークン数の上限です。長い会話を続けると:

  1. トークン数の増加: ユーザーメッセージ、アシスタントの応答、ツール呼び出しなどが蓄積
  2. コストの増加: 多くのAPIプロバイダーはトークン数に応じて課金
  3. コンテキスト超過エラー: 制限を超えると会話を継続できなくなる

/compact は、これらの問題を解決するために:

  • 古い会話履歴を要約に置き換える
  • 重要な情報を保持しながらトークン数を削減
  • 会話の継続性を維持

実行フロー

詳細ステップ

ステップ1: 会話履歴の収集

// 実装: codex-rs/core/src/codex/compact.rs
pub(crate) fn collect_user_messages(items: &[ResponseItem]) -> Vec<String>
  • メモリ内の ConversationHistory から全履歴を取得
  • role == "user" のメッセージのみを抽出
  • 以下のシステムメッセージは除外:
    • <user_instructions> タグを含むメッセージ
    • <ENVIRONMENT_CONTEXT> タグを含むメッセージ

ステップ2: 要約タスクの実行

async fn run_compact_task_inner(
    sess: Arc<Session>,
    turn_context: Arc<TurnContext>,
    sub_id: String,
    input: Vec<InputItem>,
)

使用するプロンプト: 要約生成プロンプト (prompt.md)

処理内容:

  1. SUMMARIZATION_PROMPTInputItem::Text として準備
  2. 現在の会話履歴とともにLLMに送信
  3. LLMが作業状況を要約
  4. 要約テキスト(summary_text)を受信

特徴:

  • 独立したLLMターン: 要約生成は別のLLMリクエストとして実行
  • エラーハンドリング:
    • ネットワークエラー時は最大10回リトライ
    • コンテキスト超過時は古い履歴を削除して再試行
  • トークン使用量の追跡: 要約タスク自体のトークン使用も記録

ステップ3: トークン制限の適用

const COMPACT_USER_MESSAGE_MAX_TOKENS: usize = 20_000;
  • ユーザーメッセージの合計: 最大20,000トークン(約80,000バイト)
  • 超過時: truncate_middle() で中央部分を切り詰め
  • 切り詰めマーカー: "... [tokens truncated] ..." が挿入される

ステップ4-5: 履歴の再構築

pub(crate) fn build_compacted_history(
    initial_context: Vec<ResponseItem>,
    user_messages: &[String],
    summary_text: &str,
) -> Vec<ResponseItem>

使用するプロンプト: 履歴ブリッジテンプレート (history_bridge.md)

処理内容:

  1. ステップ1で収集した user_messages を結合(改行区切り)
  2. ステップ2で生成された summary_text を取得
  3. HistoryBridgeTemplate に両方をセット
  4. テンプレートをレンダリングして履歴ブリッジメッセージを生成
  5. 新しい履歴に追加

新しい履歴の構成:

  1. initial_context: セッション開始時のコンテキスト(環境情報など)
  2. 履歴ブリッジメッセージ: ユーザーメッセージ + 要約を含む特殊なメッセージ

使用されるプロンプト

/compact コマンドでは2つのプロンプトが使用されます。それぞれが実行フローの異なるステップで機能します。

プロンプトの使用タイミング


1. 要約生成プロンプト

ファイル: codex-rs/core/templates/compact/prompt.md

使用タイミング: ステップ2 - 要約タスクの実行

日本語訳:

最大トークン数を超えました。コーディングを停止し、代わりに次のエージェント
へのメモメッセージを書いてください。メモには以下を含めてください:

- 完了したことと、まだ作業が必要なことの要約。最近update_planの呼び出しが
  あった場合は、そのステップを逐語的に繰り返してください。
- ファイルパス/行番号付きの未完了のTODOリスト(簡単に見つけられるように)。
- より多くのテスト(エッジケース、パフォーマンス、統合など)が必要なコード
  をフラグ付けしてください。
- 未解決のバグ、癖、セットアップ手順を記録し、次のエージェントが中断した
  ところから簡単に引き継げるようにしてください。

目的: LLMに「次のエージェントへの引き継ぎメモ」という形式で要約を生成させる

実装での使用:

// codex-rs/core/src/codex/compact.rs (45-46行目)
let input = vec![InputItem::Text {
    text: SUMMARIZATION_PROMPT.to_string(),  // ← このプロンプトを送信
}];
run_compact_task_inner(sess, turn_context, sub_id, input).await;

フロー内の処理:

  1. このプロンプトを会話履歴とともにLLMに送信
  2. LLMが現在の作業状況を要約
  3. 要約テキストが返される(summary_text として保存)

出力形式: 自由形式のテキストだが、以下を含むことが期待される:

  • 完了タスクと未完了タスク
  • 具体的なファイルパスと行番号
  • 技術的な注意点
  • 次のステップの提案

出力例:

完了したタスク:
- src/main.rs に HTTP サーバーを実装(45-120行目)
- データベース接続機能を追加(db.rs: 10-50行目)

未完了タスク:
- エラーハンドリングの強化が必要
- tests/integration_test.rs にテストを追加

TODO:
- src/main.rs:75 - タイムアウト処理を追加
- db.rs:30 - コネクションプールのサイズを調整可能にする

技術的注意:
- Tokio 1.x を使用、async/await パターン
- データベースは PostgreSQL を想定

2. 履歴ブリッジテンプレート

ファイル: codex-rs/core/templates/compact/history_bridge.md

使用タイミング: ステップ4 - 履歴ブリッジメッセージの生成

日本語訳:

あなたは元々、1つ以上のターンでユーザーから指示を受けました。
以下がユーザーメッセージです:

{{ user_messages_text }}

別の言語モデルがこの問題の解決を開始し、その思考プロセスの要約を作成しました。
また、その言語モデルが使用したツールの状態にもアクセスできます。これを使用
して、すでに行われた作業を基に構築し、作業の重複を避けてください。以下は、
その言語モデルが作成した要約です。この要約の情報を自分の分析に役立ててください:

{{ summary_text }}

テンプレート変数:

  • {{ user_messages_text }}: 収集されたユーザーメッセージ(改行区切り)
  • {{ summary_text }}: LLMが生成した要約テキスト

実装での使用:

// codex-rs/core/src/codex/compact.rs (241-247行目)
let Ok(bridge) = HistoryBridgeTemplate {
    user_messages_text: &user_messages_text,  // ← ステップ1で収集
    summary_text: &summary_text,              // ← ステップ2で生成
}
.render() else {
    return vec![];
};

フロー内の処理:

  1. ステップ1で収集したユーザーメッセージを user_messages_text にセット
  2. ステップ2で生成された要約を summary_text にセット
  3. テンプレートをレンダリングして履歴ブリッジメッセージを生成
  4. このメッセージを新しい履歴の一部として追加

生成される実際のメッセージ例:

あなたは元々、1つ以上のターンでユーザーから指示を受けました。
以下がユーザーメッセージです:

機能Aを実装してください

テストも書いてください

エラーハンドリングを改善してください

別の言語モデルがこの問題の解決を開始し、その思考プロセスの要約を作成しました。
また、その言語モデルが使用したツールの状態にもアクセスできます。これを使用
して、すでに行われた作業を基に構築し、作業の重複を避けてください。以下は、
その言語モデルが作成した要約です。この要約の情報を自分の分析に役立ててください:

完了したタスク:
- src/main.rs に機能Aを実装(45-120行目)
- tests/ にテストを追加

未完了タスク:
- エラーハンドリングの強化が必要

TODO:
- src/main.rs:75 - タイムアウト処理を追加

目的:

  • 次のLLMターンに文脈を提供
  • 「別のエージェントが作業を開始した」というメタファーで要約を提示
  • 作業の重複を避けるよう明示的に指示

重要なポイント:

  • このメッセージが新しいメモリ内履歴の中核となる
  • ユーザーの元の質問と作業の進捗状況の両方を保持
  • トークン数を大幅に削減しつつ、文脈は維持

会話履歴の保存場所

Codexは会話履歴を3つのレイヤーで管理しています:

レイヤー1: メモリ内履歴(セッション中のみ)

実装: codex-rs/core/src/conversation_history.rs

pub(crate) struct ConversationHistory {
    items: Vec<ResponseItem>,
}

特徴:

  • セッション実行中のみメモリに存在
  • Vec<ResponseItem> として保持
  • ResponseItem は以下のいずれか:
    • Message: ユーザーまたはアシスタントのメッセージ
    • FunctionCall / FunctionCallOutput: ツール呼び出しとその結果
    • CustomToolCall / CustomToolCallOutput: カスタムツール
    • Reasoning: モデルの推論プロセス

/compact での操作:

// 履歴を取得
let history_snapshot = sess.history_snapshot().await;

// 新しい履歴で置き換え
sess.replace_history(new_history).await;

/compact 実行時の3層同期の詳細

重要: 3つのレイヤーは同期されていません。各レイヤーは異なる目的で異なるデータを保持します。

各レイヤーの役割と /compact の影響

詳細な動作

レイヤー1: メモリ内履歴(置き換え)

// codex-rs/core/src/codex/compact.rs (164行目)
sess.replace_history(new_history).await;

動作:

async fn replace_history(&self, items: Vec<ResponseItem>) {
    let mut state = self.state.lock().await;
    state.replace_history(items);  // 古い履歴を完全に削除して置き換え
}

変化:

置き換え前のメモリ:
  [ResponseItem #1]
  [ResponseItem #2]
  ...
  [ResponseItem #50]
  合計トークン: 50,000

         ↓ replace_history()
         
置き換え後のメモリ:
  [初期コンテキスト]
  [履歴ブリッジメッセージ]  ← ユーザーメッセージ + 要約
  合計トークン: 5,000

目的: LLMへのプロンプト構築に使用される履歴を削減

レイヤー2: グローバル履歴ファイル(変化なし)

// codex-rs/core/src/message_history.rs
pub(crate) async fn append_entry(
    text: &str,
    conversation_id: &ConversationId,
    config: &Config,
) -> Result<()>

呼び出しタイミング: Op::AddToHistory 経由(TUIからユーザー入力時)

/compact 実行時: 何も記録されない

ファイルの内容:

{"session_id":"xxx","ts":1708123456,"text":"機能Aを実装してください"}
{"session_id":"xxx","ts":1708123500,"text":"機能Bも追加してください"}
{"session_id":"xxx","ts":1708123600,"text":"テストを書いてください"}

特徴:

  • ユーザーの入力テキストのみを記録
  • アシスタントの応答は記録されない
  • /compact の実行も記録されない
  • 追記専用(append-only)
  • 全セッションのユーザー入力を時系列で保持

目的:

  • ユーザー入力の検索
  • 統計分析
  • 監査ログ

結論: /compact の影響を一切受けない

レイヤー3: Rolloutファイル(追記のみ)

// codex-rs/core/src/codex/compact.rs (166-169行目)
let rollout_item = RolloutItem::Compacted(CompactedItem {
    message: summary_text.clone(),
});
sess.persist_rollout_items(&[rollout_item]).await;

動作:

async fn persist_rollout_items(&self, items: &[RolloutItem]) {
    // Rolloutファイルに追記(既存の内容はそのまま)
    recorder.add_items(items.to_vec()).await;
}

変化:

# /compact 実行前の Rolloutファイル
{"type":"rollout_item","payload":{"ResponseItem":{"Message":{...}}}}  # 1
{"type":"rollout_item","payload":{"ResponseItem":{"Message":{...}}}}  # 2
...
{"type":"rollout_item","payload":{"ResponseItem":{"Message":{...}}}}  # 50

         ↓ /compact 実行

# /compact 実行後の Rolloutファイル
{"type":"rollout_item","payload":{"ResponseItem":{"Message":{...}}}}  # 1
{"type":"rollout_item","payload":{"ResponseItem":{"Message":{...}}}}  # 2
...
{"type":"rollout_item","payload":{"ResponseItem":{"Message":{...}}}}  # 50
{"type":"rollout_item","payload":{"Compacted":{"message":"要約テキスト"}}}  # ← 追加

重要なポイント:

  • 古い50個の ResponseItem削除されず保持される
  • CompactedItem が追記される
  • Resume時、この CompactedItem を見つけると履歴を再構築
  • 完全なセッション履歴として機能

目的:

  • セッションの完全な記録
  • Resume時の履歴再構築
  • デバッグ・監査

/compact 実行のステップバイステップ

3層の比較表

項目 メモリ内履歴 グローバル履歴ファイル Rolloutファイル
パス メモリ内 ~/.codex/history.jsonl ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl
記録内容 ResponseItem ユーザー入力テキストのみ RolloutItem
/compact の影響 置き換え(削除→新規) なし(無関係) 追記(保持)
古い履歴 削除される 元々記録されていない 保持される
CompactedItem 履歴ブリッジに変換 記録されない そのまま追記
トークン削減 ✅ 大幅に削減 - ❌ 増加(追記のため)
Resume時の使用 再構築される 使用されない 完全に再現される
目的 LLMプロンプト構築 ユーザー入力の検索 セッション完全記録
スコープ 現在のセッション 全セッション 単一セッション
永続性 揮発性 永続 永続

なぜ同期されていないのか?

各レイヤーは異なる目的を持つため、同期する必要がありません:

メモリ内履歴

目的: LLMへのプロンプト生成
要件: トークン数を最小限に抑える
戦略: 古い履歴を要約に置き換える

グローバル履歴ファイル

目的: ユーザー入力の検索・統計
要件: 全セッションのユーザー入力を保持
戦略: ユーザー入力のみを時系列で追記

Rolloutファイル

目的: セッションの完全な記録・再生
要件: すべてのイベントを保持
戦略: すべての RolloutItem を追記

実際の例で確認

/compact 実行前

メモリ内履歴:
├── ResponseItem: User "機能Aを実装して"
├── ResponseItem: Assistant "実装します"
├── ResponseItem: FunctionCall "write_file"
├── ResponseItem: FunctionCallOutput "成功"
├── ... (合計50個、50,000トークン)

グローバル履歴ファイル:
├── {"text":"機能Aを実装して"}
├── {"text":"テストも書いて"}

Rolloutファイル:
├── ResponseItem #1: User message
├── ResponseItem #2: Assistant message
├── ResponseItem #3: FunctionCall
├── ResponseItem #4: FunctionCallOutput
├── ... (合計50個 + その他のアイテム)

/compact 実行後

メモリ内履歴:
├── ResponseItem: User Instructions (初期コンテキスト)
├── ResponseItem: User "履歴ブリッジメッセージ"
     ├── "元のユーザーメッセージ: ..."
     └── "要約: 機能Aを実装完了、テスト未完了"
   (合計2個、5,000トークン)

グローバル履歴ファイル:
├── {"text":"機能Aを実装して"}  ← 変化なし
├── {"text":"テストも書いて"}   ← 変化なし

Rolloutファイル:
├── ResponseItem #1: User message      ← 保持
├── ResponseItem #2: Assistant message ← 保持
├── ResponseItem #3: FunctionCall      ← 保持
├── ResponseItem #4: FunctionCallOutput← 保持
├── ... (50個すべて保持)
├── CompactedItem: "要約: 機能Aを実装完了、テスト未完了" ← 追加

Resume時の動作

Resume時、Rolloutファイルのみが使用されます:

  1. Rolloutファイルを読み込み
  2. ResponseItem #1-50 を順次メモリに追加
  3. CompactedItem を検出
  4. その時点で build_compacted_history を実行
  5. メモリ内履歴を履歴ブリッジメッセージに置き換え
  6. その後の ResponseItem があれば追加

結果: /compact 実行後と同じメモリ内履歴が再現される

グローバル履歴ファイルは Resume で使用されません


レイヤー2: グローバル履歴ファイル

実装: codex-rs/core/src/message_history.rs

パス: ~/.codex/history.jsonl

形式: JSON Lines(1行に1つのJSONオブジェクト)

レコード構造:

{
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "ts": 1708123456,
  "text": "ユーザーまたはアシスタントのメッセージ"
}

フィールド説明:

  • session_id: セッションの一意識別子(UUID)
  • ts: Unix タイムスタンプ(秒単位)
  • text: メッセージのテキスト内容

特徴:

  • すべてのセッションのメッセージを時系列で記録
  • 追記専用(append-only)
  • POSIX アトミック書き込み保証(O_APPEND フラグ)
  • ファイルパーミッション: 0600(所有者のみ読み書き可能)
  • 複数プロセスからの同時書き込みをサポート

用途:

  • 全セッションの検索
  • 統計分析
  • 監査ログ

プライバシー設定:

# ~/.codex/config.toml
[history]
persistence = "save_all"  # または "none"

レイヤー3: Rollout ファイル(セッション単位)

実装: codex-rs/core/src/rollout/recorder.rs

パス: ~/.codex/sessions/YYYY/MM/DD/rollout-YYYY-MM-DDThh-mm-ss-<UUID>.jsonl

パス例:

~/.codex/sessions/2025/10/16/rollout-2025-10-16T14-30-45-a1b2c3d4-e5f6-4a5b-8c7d-9e0f1a2b3c4d.jsonl

ディレクトリ構造:

~/.codex/
├── sessions/
│   ├── 2025/
│   │   ├── 10/
│   │   │   ├── 15/
│   │   │   │   ├── rollout-2025-10-15T09-15-30-<UUID>.jsonl
│   │   │   │   └── rollout-2025-10-15T16-45-22-<UUID>.jsonl
│   │   │   └── 16/
│   │   │       └── rollout-2025-10-16T14-30-45-<UUID>.jsonl
│   │   └── 11/
│   └── 2024/
└── archived_sessions/  # アーカイブされたセッション

ファイル形式: JSON Lines

レコードタイプ:

1. セッションメタデータ(最初の行)

{
  "timestamp": "2025-10-16T14:30:45.123Z",
  "type": "session_meta",
  "payload": {
    "id": "a1b2c3d4-e5f6-4a5b-8c7d-9e0f1a2b3c4d",
    "timestamp": "2025-10-16T14:30:45.123Z",
    "cwd": "/Users/username/project",
    "originator": "cli",
    "cli_version": "0.1.0",
    "instructions": null,
    "source": "Cli"
  }
}

2. イベントメッセージ

{
  "timestamp": "2025-10-16T14:31:00.456Z",
  "type": "event_msg",
  "payload": {
    "type": "user_message",
    "message": "ファイルを作成してください",
    "kind": "plain"
  }
}

3. Rollout アイテム

ResponseItem の例:

{
  "timestamp": "2025-10-16T14:31:05.789Z",
  "type": "rollout_item",
  "payload": {
    "ResponseItem": {
      "Message": {
        "id": "msg_001",
        "role": "assistant",
        "content": [
          {
            "OutputText": {
              "text": "ファイルを作成します"
            }
          }
        ]
      }
    }
  }
}

Compacted アイテムの例 (/compact 実行後):

{
  "timestamp": "2025-10-16T14:35:00.000Z",
  "type": "rollout_item",
  "payload": {
    "Compacted": {
      "message": "完了したタスク:\n- ファイル作成機能を実装\n\n未完了:\n- テストが必要\n\nTODO:\n- src/main.rs:45 - エラーハンドリングを追加"
    }
  }
}

特徴:

  • セッション全体の完全な記録
  • 再生可能(デバッグ用)
  • /compact の結果も記録
  • タイムスタンプ付き
  • JSON Lines形式で解析が容易

検査コマンド:

# jqで整形表示
jq -C . ~/.codex/sessions/2025/10/16/rollout-*.jsonl | less -R

# 特定のタイプのみ抽出
jq 'select(.type == "rollout_item")' ~/.codex/sessions/2025/10/16/rollout-*.jsonl

# Compactedアイテムのみ表示
jq 'select(.payload.Compacted != null)' ~/.codex/sessions/2025/10/16/rollout-*.jsonl

技術的な詳細

/compact の実装ファイル

主要ファイル: codex-rs/core/src/codex/compact.rs

主要関数

1. run_compact_task

pub(crate) async fn run_compact_task(
    sess: Arc<Session>,
    turn_context: Arc<TurnContext>,
    sub_id: String,
    input: Vec<InputItem>,
) -> Option<String>

役割: ユーザーが /compact を実行した際のエントリーポイント

処理:

  1. TaskStarted イベントを送信
  2. run_compact_task_inner を呼び出し
  3. タスク完了を待機

2. run_inline_auto_compact_task

pub(crate) async fn run_inline_auto_compact_task(
    sess: Arc<Session>,
    turn_context: Arc<TurnContext>,
)

役割: 自動コンパクト(将来的な機能?)

特徴:

  • デフォルトの要約プロンプトを使用
  • 内部サブIDを自動生成

3. collect_user_messages

pub(crate) fn collect_user_messages(items: &[ResponseItem]) -> Vec<String>

役割: 履歴からユーザーメッセージを抽出

フィルタリング:

pub fn is_session_prefix_message(text: &str) -> bool {
    matches!(
        InputMessageKind::from(("user", text)),
        InputMessageKind::UserInstructions | InputMessageKind::EnvironmentContext
    )
}

除外されるメッセージ:

  • <user_instructions>...</user_instructions> を含むもの
  • <ENVIRONMENT_CONTEXT>...</ENVIRONMENT_CONTEXT> を含むもの

4. build_compacted_history

pub(crate) fn build_compacted_history(
    initial_context: Vec<ResponseItem>,
    user_messages: &[String],
    summary_text: &str,
) -> Vec<ResponseItem>

役割: 新しい履歴を構築

処理フロー:

// 1. 初期コンテキストから開始
let mut history = initial_context;

// 2. ユーザーメッセージを結合
let mut user_messages_text = user_messages.join("\n\n");

// 3. トークン制限を適用(20,000トークン = 80,000バイト)
if user_messages_text.len() > 80_000 {
    user_messages_text = truncate_middle(&user_messages_text, 80_000).0;
}

// 4. ブリッジメッセージを生成
let bridge = HistoryBridgeTemplate {
    user_messages_text: &user_messages_text,
    summary_text: &summary_text,
}.render()?;

// 5. 履歴に追加
history.push(ResponseItem::Message {
    id: None,
    role: "user".to_string(),
    content: vec![ContentItem::InputText { text: bridge }],
});

エラーハンドリング

エラーハンドリングフロー

コンテキストウィンドウ超過

Err(CodexErr::ContextWindowExceeded) => {
    if turn_input.len() > 1 {
        // 古い履歴アイテムを削除
        turn_input.remove(0);
        truncated_count += 1;
        retries = 0;
        continue;  // 再試行
    }
    // これ以上削除できない場合はエラー
}

戦略: 段階的な履歴削減

  1. 最も古い会話アイテムを削除
  2. 要約タスクを再実行
  3. それでも収まらない場合、さらに削除
  4. 最小限の履歴でも収まらない場合はエラー

ネットワークエラー

Err(e) => {
    if retries < max_retries {  // デフォルト: 10回
        retries += 1;
        let delay = backoff(retries);  // 指数バックオフ
        sess.notify_stream_error(
            &sub_id,
            format!("Re-connecting... {retries}/{max_retries}"),
        ).await;
        tokio::time::sleep(delay).await;
        continue;
    } else {
        // 最大リトライ回数に達したらエラー
    }
}

バックオフ戦略: 指数バックオフで再接続を試行

トークン管理

トークン制限とフロー

トークン使用量の追跡

Ok(ResponseEvent::Completed { token_usage, .. }) => {
    sess.update_token_usage_info(sub_id, turn_context, token_usage.as_ref())
        .await;
}

記録される情報:

  • 入力トークン数
  • 出力トークン数
  • 合計トークン数
  • モデルのコンテキストウィンドウサイズ

トークン制限の定数

const COMPACT_USER_MESSAGE_MAX_TOKENS: usize = 20_000;

換算:

  • 20,000 トークン
  • ≈ 80,000 バイト(1トークン ≈ 4バイト)
  • ≈ 15,000〜20,000 ワード(英語)

TUIでの処理

ファイル: codex-rs/tui/src/chatwidget.rs

SlashCommand::Compact => {
    self.clear_token_usage();  // トークン表示をクリア
    self.app_event_tx.send(AppEvent::CodexOp(Op::Compact));
}

UI動作:

  1. トークン使用量の表示をリセット
  2. バックエンドに Op::Compact を送信
  3. タスク進行中は進捗表示
  4. 完了時に "Compact task completed" メッセージ

使用例と注意点

使用例1: 長時間セッション

シナリオ: 複数の機能を実装し、会話が100ターン以上続いている

効果:

  • トークン数: 50,000 → 5,000 に削減
  • 文脈: 保持(要約により)
  • 次のターン: 通常通り実行可能

使用例2: コンテキスト制限に近づいた時

[警告] Token usage: 95,000 / 100,000

ユーザー: /compact
[要約が生成され、トークン数が大幅に削減]

ユーザー: /status
Token usage: 6,500 / 100,000
Context: 6,500 tokens in context

注意点

⚠️ 1. タスク実行中は使用不可

pub fn available_during_task(self) -> bool {
    match self {
        SlashCommand::Compact => false,  // ❌ タスク中は使用不可
        // ...
    }
}

理由:

  • 実行中のタスクの状態が失われる可能性
  • タスク完了を待ってから実行すべき

⚠️ 2. 詳細情報の損失

/compact 実行後、以下の情報は要約に凝縮されます:

  • ツール呼び出しの詳細
  • エラーメッセージの完全なスタックトレース
  • 中間的な試行錯誤のプロセス

対策:

  • 重要なセッションは実行前にRolloutファイルをバックアップ
  • 要約が重要な詳細を含むことを確認

⚠️ 3. トークン消費

要約生成自体がトークンを消費します:

  • 要約プロンプト: 約100トークン
  • 履歴の送信: 現在の履歴のトークン数
  • 要約の生成: 500〜2,000トークン(典型的)

コスト: 要約タスクの実行にもAPI料金が発生

⚠️ 4. 要約の品質

要約はLLMが生成するため:

  • 品質はモデルに依存
  • 重要な情報が省略される可能性
  • 複雑な技術的詳細は簡略化されることがある

ベストプラクティス:

  • 要約前に重要な情報をメモ
  • 必要に応じて要約後に補足情報を提供

⚠️ 5. 復元不可能

sess.replace_history(new_history).await;

メモリ内の履歴は完全に置き換えられます。

復元方法:

  • Rolloutファイルから再構築は可能
  • ただし、セッションの再開が必要

ベストプラクティス

✅ 1. 適切なタイミング

推奨: トークン使用量が70-80%に達した時
早すぎる: 文脈が少なく、要約の必要性が低い
遅すぎる: コンテキスト制限エラーのリスク

✅ 2. 区切りの良いポイント

  • 大きな機能の実装が完了した後
  • 新しい機能に移る前
  • エラー解決が完了した後

✅ 3. /status で確認

ユーザー: /status
→ Token usage: 45,000 / 100,000
→ まだ余裕あり

ユーザー: /status
→ Token usage: 85,000 / 100,000
→ /compact の実行を検討

✅ 4. 重要なセッションのバックアップ

# Rolloutファイルをバックアップ
cp ~/.codex/sessions/2025/10/16/rollout-*.jsonl ~/backups/

トラブルシューティング

問題1: 要約が不完全

症状: 要約が重要な情報を省略している

解決策:

  1. もう一度 /compact を実行(異なる要約が生成される場合がある)
  2. 省略された情報を手動で補足
  3. より詳細なモデル(GPT-4など)を使用

問題2: コンテキスト制限エラー

症状: /compact 実行中に "Context window exceeded" エラー

原因: 要約タスク自体が大きすぎる履歴を処理しようとしている

解決策フロー:

自動処理: Codexは古い履歴を段階的に削除して再試行
手動介入: それでも失敗する場合、新しいセッションを開始

問題3: ネットワークエラー

症状: 要約生成中にネットワークエラー

原因: API接続の一時的な問題

自動リトライフロー:

バックオフ戦略:

  • 最大10回リトライ
  • 待機時間: 指数的に増加(100ms → 200ms → 400ms...)
  • 自動復旧により手動介入不要

問題4: 要約が長すぎる

症状: 要約自体が数千トークンを消費

原因: LLMが詳細すぎる要約を生成

解決策:

  • 仕様通り(要約プロンプトが簡潔さを要求)
  • 通常、自動的に適切な長さに調整される

関連コマンドとの比較

/compact vs /new

特徴 /compact /new
履歴 要約して保持 完全にクリア
文脈 継続 新規開始
トークン削減 大幅削減 最小化
用途 長い会話の継続 新しいトピック開始

/compact vs /status

特徴 /compact /status
機能 履歴を要約 状態を表示
トークン 削減する 表示のみ
タスク実行中 不可 可能

セッションの再開(Resume)と会話履歴の復元

Codexを一旦終了して再度開いた時、以前のセッションを再開(resume)することができます。このとき、会話履歴は Rollout ファイル から完全に復元されます。

Resume の仕組み

詳細なステップ

ステップ1: Rolloutファイルの検索と読み込み

// 実装: codex-rs/core/src/rollout/recorder.rs
pub async fn from_path(path: &Path) -> std::io::Result<InitialHistory>

処理:

  1. 指定されたRolloutファイルパス(または最新のセッション)を開く
  2. ファイルを1行ずつ読み込み(JSON Lines形式)
  3. 各行を RolloutLine としてパース
  4. RolloutItem のリストを構築

Rolloutファイルの構造:

{"timestamp":"2025-10-16T14:30:45.123Z","type":"session_meta","payload":{...}}
{"timestamp":"2025-10-16T14:31:00.456Z","type":"event_msg","payload":{...}}
{"timestamp":"2025-10-16T14:31:05.789Z","type":"rollout_item","payload":{"ResponseItem":{...}}}
{"timestamp":"2025-10-16T14:35:00.000Z","type":"rollout_item","payload":{"Compacted":{...}}}

ステップ2: ResumedHistory の作成

// codex-rs/protocol/src/protocol.rs
pub struct ResumedHistory {
    pub conversation_id: ConversationId,
    pub history: Vec<RolloutItem>,  // Rolloutファイルから読み込んだ全アイテム
    pub rollout_path: PathBuf,
}

含まれる情報:

  • conversation_id: セッションの一意識別子(UUID)
  • history: Rolloutファイルから読み込んだ全 RolloutItem
  • rollout_path: Rolloutファイルのパス(追記用)

ステップ3: 会話履歴の再構築

// codex-rs/core/src/codex.rs
fn reconstruct_history_from_rollout(
    &self,
    turn_context: &TurnContext,
    rollout_items: &[RolloutItem],
) -> Vec<ResponseItem>

処理ロジック:

let mut history = ConversationHistory::new();
for item in rollout_items {
    match item {
        RolloutItem::ResponseItem(response_item) => {
            // 通常のレスポンスアイテム:履歴に追加
            history.record_items(std::iter::once(response_item));
        }
        RolloutItem::Compacted(compacted) => {
            // Compactedアイテム:履歴を再構築
            let snapshot = history.contents();
            let user_messages = collect_user_messages(&snapshot);
            let rebuilt = build_compacted_history(
                self.build_initial_context(turn_context),
                &user_messages,
                &compacted.message,
            );
            history.replace(rebuilt);
        }
        _ => {}  // その他のアイテムはスキップ
    }
}
history.contents()

重要なポイント:

  • ResponseItem は順次追加される
  • CompactedItem を見つけると、その時点で /compact が実行されたのと同じ処理を再現
    1. それまでの履歴からユーザーメッセージを収集
    2. build_compacted_history で履歴ブリッジメッセージを生成
    3. 履歴を置き換え

Resume 時の /compact の再現

セッション中に /compact を実行していた場合、Rolloutファイルには以下のように記録されています:

{"type":"rollout_item","payload":{"ResponseItem":{"Message":{...}}}}  // ユーザーメッセージ
{"type":"rollout_item","payload":{"ResponseItem":{"Message":{...}}}}  // アシスタント応答
{"type":"rollout_item","payload":{"ResponseItem":{"Message":{...}}}}  // さらに会話が続く
...
{"type":"rollout_item","payload":{"Compacted":{"message":"完了タスク:\n- 機能Aを実装\n\n未完了:\n- 機能Bのテストが必要"}}}
{"type":"rollout_item","payload":{"ResponseItem":{"Message":{...}}}}  // compact後の会話

Resume時の動作:

Resume と通常セッションの違い

特徴 新規セッション Resume セッション
会話履歴 初期コンテキストのみ Rolloutから完全復元
メモリ内履歴 再構築された履歴
Rolloutファイル 新規作成 既存ファイルに追記
/compactの効果 実行時のみ 再構築時に再現
conversation_id 新規UUID 元のUUID(継続)

/compact と Resume の相互作用

シナリオ1: Compact後にResume

1. セッション開始
2. 長時間の会話(50,000トークン)
3. /compact 実行 → トークン削減(5,000トークン)
4. さらに会話を続ける
5. セッション終了
6. 【Resume】
   → Rolloutファイルを読み込み
   → CompactedItemで履歴を再構築
   → 5,000トークンの状態から再開

シナリオ2: 複数回Compact後にResume

Rolloutファイルの例:

{"payload":{"ResponseItem":{...}}}  // 初期の会話 (50個)
...
{"payload":{"Compacted":{"message":"1回目の要約"}}}
{"payload":{"ResponseItem":{...}}}  // 1回目のcompact後の会話 (30個)
...
{"payload":{"Compacted":{"message":"2回目の要約"}}}
{"payload":{"ResponseItem":{...}}}  // 2回目のcompact後の会話 (20個)
...

Resume時の処理:

  1. 最初の50個の ResponseItem を履歴に追加
  2. 1回目の CompactedItem を検出 → 履歴を要約に置き換え
  3. 次の30個の ResponseItem を履歴に追加
  4. 2回目の CompactedItem を検出 → 履歴を再度要約に置き換え
  5. 最後の20個の ResponseItem を履歴に追加
  6. 完成!

Resume のコードフロー全体像

重要な実装の詳細

Compacted アイテムの再現性

/compact の結果は決定的(deterministic)ではありません:

同じ入力でも異なる要約が生成される可能性:

  • LLMの温度設定(temperature)
  • モデルのバージョン
  • 実行時のランダム性

しかし、Resume時は:

  • 元のLLM出力(要約テキスト)をそのまま使用
  • build_compacted_history で同じ履歴ブリッジを再生成
  • 完全に同じ履歴が復元される

つまり、Resume時は要約を再生成するのではなく、Rolloutファイルに保存された要約テキストを使って履歴ブリッジを再構築します。

メモリ効率

Resume時、Rolloutファイル全体をメモリに読み込みますが:

  • ResponseItem のみがメモリ内履歴に保持される
  • CompactedItem は処理後、要約テキストのみが残る
  • その他のメタデータ(TurnContext等)は破棄される

これにより、長時間のセッションでもメモリ使用量は適切に管理されます。

Resume の実用例

例1: 前日の作業を続ける

メリット:

  • 前回のcompactの結果が保持される
  • トークン使用量が抑えられた状態で再開
  • 会話の文脈は失われない

まとめ

/compact コマンドは、Codexの長時間セッションを可能にする重要な機能です:

主要な利点

  1. トークン数の削減: 大幅なトークン削減により長時間の会話が可能
  2. コスト削減: トークン使用量に応じた課金の削減
  3. 文脈の保持: 要約により重要な情報を保持
  4. 自動リトライ: ネットワークエラーやコンテキスト超過に対する堅牢性

実装の特徴

  • 3層の履歴管理: メモリ、グローバルファイル、Rolloutファイル(各レイヤーは独立して動作)
  • 非同期設計: メモリは置き換え、グローバルは無関係、Rolloutは追記のみ
  • 2段階のプロンプト:
    • 要約生成プロンプト(ステップ2): LLMが作業状況を要約
    • 履歴ブリッジテンプレート(ステップ4): ユーザーメッセージと要約を組み合わせ
  • エラーハンドリング: 段階的な履歴削減と指数バックオフ
  • トークン制限: 20,000トークンの上限と自動切り詰め
  • 完全な記録: Rolloutファイルへの永続化(古い履歴も保持)
  • Resume対応: /compact の結果はRolloutファイルから完全に再現可能

推奨される使用方法

  • トークン使用量が70-80%に達した時点で実行
  • 機能実装の区切りで実行
  • 定期的な /status でのモニタリング
  • 重要なセッションのバックアップ
  • Resume後も /compact の効果が保持されるため、長期プロジェクトに最適
  • 要約形式をカスタマイズしたい場合はカスタムプロンプトが有効

カスタムCompactの実現可能性

質問: 自作のcompactコマンドは可能か?

結論: 完全なカスタムcompactの実装は技術的に困難ですが、部分的なカスタマイズは可能です。

なぜ難しいのか?

メモリ内履歴は Codex Core の内部状態であり、外部から直接操作するAPIが公開されていません:

// codex-rs/core/src/state/session.rs
pub(crate) struct SessionState {
    pub(crate) history: ConversationHistory,  // ← private、外部からアクセス不可
    // ...
}

制限:

  • replace_history()pub(crate) - クレート内部のみ
  • MCPツールからメモリ内履歴を直接操作する手段がない
  • スラッシュコマンドの追加にはRustコードの変更とコンパイルが必要

実現可能なカスタマイズ方法

方法1: プロンプトのカスタマイズ ⭐ 最も簡単

要約生成プロンプトをカスタマイズすることで、要約の内容をコントロールできます。

手順:

  1. プロンプトファイルを編集:

    # Codexのソースコードをクローン
    cd codex-rs/core/templates/compact/
    
    # prompt.md を編集
    vim prompt.md
    
  2. カスタムプロンプトの例:

    ---
    # カスタム要約プロンプト
    ---
    
    最大トークン数を超えました。以下の形式で要約を作成してください:
    
    ## 実装済み機能
    [実装したファイルと関数を列挙]
    
    ## 保留中のタスク
    [未完了のタスクをリスト化]
    
    ## 技術スタック
    [使用している技術・ライブラリを記録]
    
    ## 次のステップ
    [推奨される次のアクション]
    
  3. Codexを再ビルド:

    cd codex-rs
    cargo build --release
    

メリット:

  • 比較的簡単
  • 要約の形式と内容を完全にコントロール

デメリット:

  • ソースコードの変更が必要
  • Codex本体の再ビルドが必要
  • 公式更新時にマージが必要

方法2: カスタムプロンプトコマンド 🔧 実用的

Codexの「カスタムプロンプト」機能を使って、疑似的なcompactコマンドを作成できます。

仕組み:

  • ~/.codex/prompts/ ディレクトリにMarkdownファイルを配置
  • /prompts:ファイル名 で呼び出し
  • メモリ内履歴は操作できないが、要約を生成してユーザーに提示

実装例:

# カスタムプロンプトディレクトリを作成
mkdir -p ~/.codex/prompts

# カスタムcompactプロンプトを作成
cat > ~/.codex/prompts/my-compact.md << 'EOF'
---
description: "カスタム要約を生成"
argument-hint: "[詳細レベル]"
---

これまでの会話を以下の形式で要約してください:

## 📋 完了タスク
- 実装したファイルと機能
- 解決した問題

## 🚧 進行中
- 現在作業中の内容

## ⏸️ 保留中
- 未着手のタスク

## 🔧 技術スタック
- 使用しているフレームワーク・ライブラリ

## 💡 推奨事項
- 次に取り組むべきこと
- リファクタリングが必要な箇所

引数 "$1" が "detailed" の場合、各項目をより詳細に記述してください。
EOF

使用方法:

ユーザー: /prompts:my-compact
または
ユーザー: /prompts:my-compact detailed

動作:

  1. Codexが現在の会話履歴とともにこのプロンプトをLLMに送信
  2. LLMが指定された形式で要約を生成
  3. 要約がメッセージとして返される(履歴は置き換えられない)
  4. ユーザーが要約を確認し、必要に応じて手動でコピー

メリット:

  • ソースコードの変更不要
  • 複数のカスタム要約スタイルを作成可能
  • 簡単に更新・削除できる

デメリット:

  • メモリ内履歴は自動的に置き換わらない
  • 要約を見ながら /new で新しいセッションを開始する必要がある
  • 本物の /compact とは異なる動作

改善されたワークフロー:

1. トークンが多くなってきた
2. /prompts:my-compact detailed を実行
3. 生成された要約を確認
4. /new で新しいセッションを開始
5. 要約の重要部分を新しいセッションに貼り付け

方法3: MCPサーバーの実装 🔨 高度

MCP (Model Context Protocol) サーバーを実装して、カスタムツールを提供できます。

可能なこと:

  • 要約の生成ロジックをカスタマイズ
  • 外部APIを使って高度な要約を生成
  • 要約結果を外部ファイルに保存

不可能なこと:

  • メモリ内履歴の直接操作(APIが公開されていない)

実装例の概要:

// mcp-server-custom-compact/index.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new Server({
  name: "custom-compact",
  version: "1.0.0",
}, {
  capabilities: {
    tools: {},
  },
});

server.setRequestHandler("tools/list", async () => {
  return {
    tools: [{
      name: "custom_summarize",
      description: "カスタム要約を生成",
      inputSchema: {
        type: "object",
        properties: {
          style: {
            type: "string",
            enum: ["brief", "detailed", "technical"],
            description: "要約のスタイル"
          }
        }
      }
    }]
  };
});

server.setRequestHandler("tools/call", async (request) => {
  if (request.params.name === "custom_summarize") {
    // ここでカスタム要約ロジックを実装
    const summary = generateCustomSummary(request.params.arguments.style);
    return {
      content: [{ type: "text", text: summary }]
    };
  }
});

// サーバー起動
const transport = new StdioServerTransport();
await server.connect(transport);

設定:

// ~/.codex/mcp.json
{
  "mcpServers": {
    "custom-compact": {
      "command": "node",
      "args": ["/path/to/mcp-server-custom-compact/index.js"]
    }
  }
}

使用方法:

ユーザー: カスタム要約を生成してください(スタイル: detailed)
→ Codexがcustom_summarizeツールを呼び出し
→ 要約が生成される

メリット:

  • 完全にカスタマイズ可能なロジック
  • 外部サービス(GPT-4、Claude等)を活用可能
  • 要約結果を任意の形式で保存

デメリット:

  • TypeScript/JavaScript の実装が必要
  • メモリ内履歴は依然として操作不可
  • セットアップが複雑

方法4: Codexをフォークして独自実装 💪 最も柔軟

最も柔軟だが最も労力が必要な方法です。

手順:

  1. Codexリポジトリをフォーク:

    git clone https://github.com/your-username/codex.git
    cd codex
    
  2. 新しいスラッシュコマンドを追加:

    // codex-rs/tui/src/slash_command.rs
    pub enum SlashCommand {
        // ... 既存のコマンド
        MyCompact,  // ← 追加
    }
    
    impl SlashCommand {
        pub fn description(self) -> &'static str {
            match self {
                // ...
                SlashCommand::MyCompact => "カスタム要約を実行",
            }
        }
    }
    
  3. カスタムcompactロジックを実装:

    // codex-rs/core/src/codex/my_compact.rs
    pub async fn run_my_compact_task(
        sess: Arc<Session>,
        turn_context: Arc<TurnContext>,
        sub_id: String,
    ) {
        // カスタムロジック:
        // 1. 特定の条件でメッセージをフィルタ
        // 2. カスタムプロンプトで要約生成
        // 3. 独自の形式で履歴を再構築
        // 4. メタデータを追加
    }
    
  4. ビルドとインストール:

    cargo build --release
    cargo install --path codex-rs/cli
    

カスタマイズ例:

  • 特定のファイル種類のみ保持: .rs ファイルの変更のみを要約
  • 時系列で区切る: 1時間ごとに要約を分割
  • 優先度付き要約: 重要なファイルの変更を優先的に記録
  • 外部サービス連携: 要約をNotion、Slack等に自動投稿

メリット:

  • 完全な自由度
  • メモリ内履歴を直接操作可能
  • 独自のロジックを実装可能

デメリット:

  • Rustのコーディングが必要
  • メンテナンスが必要(上流の変更を追従)
  • 公式サポートなし
  • ビルド時間がかかる

比較表

方法 難易度 メモリ操作 カスタマイズ性 メンテナンス
プロンプト編集 ⭐ 簡単 ❌ 不可 ⭐⭐ 中 手動マージ必要
カスタムプロンプト ⭐ 簡単 ❌ 不可 ⭐⭐ 中 不要
MCPサーバー ⭐⭐ 中 ❌ 不可 ⭐⭐⭐ 高 独立
フォーク実装 ⭐⭐⭐ 難 ✅ 可能 ⭐⭐⭐⭐ 最高 継続的に必要

推奨アプローチ

🎯 ユースケース別の推奨

ケース1: 要約の形式だけ変えたい
カスタムプロンプトコマンド(方法2)を推奨

  • 手軽で実用的
  • /prompts:my-compact で好きな形式の要約を生成

ケース2: 要約ロジックを高度にカスタマイズ
MCPサーバー(方法3)を推奨

  • 外部APIや独自アルゴリズムを活用
  • 要約結果を他のツールと連携

ケース3: 完全なカスタムcompact実装
フォーク実装(方法4)のみ可能

  • メモリ内履歴を直接操作
  • 独自のスラッシュコマンドを追加

将来的な可能性

Codexが以下の機能を追加すれば、より柔軟なカスタマイズが可能になります:

  1. 公開API: メモリ内履歴操作のAPI
  2. プラグインシステム: 動的にコマンドを追加
  3. カスタムフック: compact実行前後のカスタム処理

これらの機能が実装されれば、コンパイル不要でカスタムcompactが実現可能になります。


参考資料

関連ファイル

  • codex-rs/core/src/codex/compact.rs - メインの実装
    • run_compact_task - /compact のエントリーポイント
    • build_compacted_history - 履歴ブリッジメッセージの生成
    • collect_user_messages - ユーザーメッセージの抽出
  • codex-rs/core/templates/compact/prompt.md - 要約プロンプト
  • codex-rs/core/templates/compact/history_bridge.md - 履歴ブリッジテンプレート
  • codex-rs/core/src/conversation_history.rs - 会話履歴の管理(メモリ内)
    • replace - 履歴の置き換えロジック
  • codex-rs/core/src/message_history.rs - グローバル履歴ファイル(ユーザー入力のみ)
    • append_entry - 追記専用、/compact では使用されない
  • codex-rs/core/src/rollout/recorder.rs - Rolloutファイルの記録
    • add_items - RolloutItem の追記
    • from_path - Resume時のファイル読み込み
  • codex-rs/core/src/codex.rs - セッション管理
    • replace_history - メモリ内履歴の置き換え
    • reconstruct_history_from_rollout - Resume時の履歴再構築
  • codex-rs/tui/src/slash_command.rs - スラッシュコマンドの定義
  • codex-rs/core/src/custom_prompts.rs - カスタムプロンプトのディスカバリー
    • discover_prompts_in - ~/.codex/prompts/ からプロンプトを検索
  • codex-rs/protocol/src/custom_prompts.rs - カスタムプロンプトの定義

関連設定

# ~/.codex/config.toml
[history]
persistence = "save_all"  # または "none"

カスタムプロンプトディレクトリ:

~/.codex/prompts/        # カスタムプロンプトの配置場所
~/.codex/history.jsonl   # グローバル履歴ファイル
~/.codex/sessions/       # Rolloutファイルのルートディレクトリ

デバッグコマンド

# グローバル履歴の確認
cat ~/.codex/history.jsonl | jq

# Rolloutファイルの検索
find ~/.codex/sessions -name "rollout-*.jsonl"

# 最新のRolloutファイルを表示
ls -t ~/.codex/sessions/**/**/**/rollout-*.jsonl | head -1 | xargs jq -C . | less -R

# Compactedアイテムの抽出
jq 'select(.payload.Compacted)' ~/.codex/sessions/**/**/**/rollout-*.jsonl

# カスタムプロンプトの一覧
ls -la ~/.codex/prompts/

# カスタムプロンプトの内容確認
cat ~/.codex/prompts/my-compact.md

現時点(2025年10月16日 openai/codex version 0.46.0)での内容となります。
この記事が何かの参考になれば幸いです。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?