この記事は AIHubMix が作成しました。内容は 2026年10月10日時点の公開情報(openai/codex の Issue、OpenAI Developer Community、いくつかのオープンソースのゲートウェイプロジェクトで共有された検証結果)にもとづいています。原因となるルールはコミュニティの A/B テストで特定されたもので、OpenAI はまだ公式に認めていません。
TL;DR
- Codex の圧縮(compaction)リクエストは、履歴に残った
web_search_callをそのまま送り返しながら、tools: []を送ります。 - 2026年10月6日ごろから、ChatGPT の Codex バックエンドはこの組み合わせを
response protection is unavailableで拒否しています。 - 同じリクエストに
web_searchの宣言とtool_choice: "none"を足すだけで、圧縮は完了します。コンテキストの大きさやネットワークは原因ではありません。 - 公式の Codex にはまだ修正がありません。新しいセッションでは
web_search = "disabled"で回避できます。ゲートウェイを自前で運用している場合は、ゲートウェイ側で宣言を足せます。
何が起きているのか
先週まで通っていたリクエストボディが、中身は何も変わっていないのに拒否されるようになりました。履歴には検索の記録が残っていて、圧縮リクエストは要約にツールが要らないのでツールを宣言しません。どちらも以前からそうです。変わったのは上流側で、再送された Web 検索に対して web_search ツールが宣言されているかを確認するようになりました。
その結果、長いセッションの序盤で一度でも Web 検索を使うと、その後の圧縮はすべて失敗します。プロキシ経由でも、ChatGPT アカウントで直接ログインした公式 Codex アプリでも同じです。報告にもっとも多く出てくるモデルは gpt-6.1-sol ですが、gpt-6-astra や gpt-5.6-sol でも同じように失敗します。
この障害かどうかを確認する
「stream disconnected before completion」は Codex の汎用メッセージで、上流の過負荷や WebSocket のアイドルタイムアウトでも表示されます。今回の障害で出るのは次のどちらかです。
stream disconnected before completion: response protection is unavailable
Error running remote compact task: stream disconnected before completion: stream closed before response.completed
2行目も同じ障害の可能性があります。Codex は上流の SSE エラーを表示せず、汎用メッセージにフォールバックすることがあるためです。見分けるには、次の3点を確認します。
1. Codex のローカルログ
影響を受けた環境では、~/.codex/logs_2.sqlite に remote compaction v2 stream failed と Failed to run pre-sampling compact が繰り返し記録されます。Codex は5回ほどリトライしてからあきらめます。セッションを再開しても同じ圧縮が走るので、セッションは止まったままです。
2. 上流が返した内容
ゲートウェイを運用している場合は、次の2つの形のどちらかを探します。1つ目は HTTP 502 で、ボディは次のとおりです。gpt-6.1-sol を使う ChatGPT Plus ユーザーも、同じものを OpenAI Developer Community に投稿しています。
{"message": "response protection is unavailable", "type": "internal_error"}
2つ目は HTTP 200 で、SSE ストリームが code: "upstream_error" の response.failed、または error.message に文言を入れた event: error で終わり、response.completed に届きません。あるゲートウェイのログでは、失敗した試行は1回あたり2〜13秒で終わり、トークンは0でした。
3. 履歴に検索が含まれているか
通常のターンは動きます。失敗するのは、ツールを宣言せずに履歴を送るリクエストだけです。Codex はセッション履歴を ~/.codex/sessions/ に保存しているので、次のコマンドですぐ確認できます。
grep -rl 'web_search_call' ~/.codex/sessions/
止まったセッションのファイルが出てきたら、その履歴にはホスト型の検索記録が入っています。Codex の Web 検索は既定で "cached" なので、自分で検索を有効にした覚えがなくても記録が入っていることがあります。
リクエストの形
検証者がキャプチャしたものと同じ種類の圧縮リクエストを短くしたものです。POST /v1/responses、ストリーミング、store: false、末尾に compaction_trigger(remote compaction v2)、そして空の tools です。値はプレースホルダーです。
{
"model": "gpt-6.1-sol",
"stream": true,
"store": false,
"tools": [],
"input": [
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "..."}]},
{"type": "web_search_call", "id": "ws_...", "status": "completed", "action": {"type": "search", "query": "..."}},
{"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "..."}]},
{"type": "compaction_trigger"}
]
}
カスタムプロバイダーの場合、Codex は代わりにローカルで圧縮します(codex-rs/core/src/compact.rs)。このリクエストは履歴全体を載せた通常の /responses 呼び出しで、tools: [] のまま、compaction_trigger はありません。これも同じように失敗します。
通常のターンが失敗しないのは、{"type": "web_search"} を宣言しているからです。上のリクエストを通すには、2つのフィールドを変えるだけです。
{
"tools": [{"type": "web_search", "external_web_access": false}],
"tool_choice": "none"
}
external_web_access: false はツールをキャッシュ済みのインデックスに限定します。宣言はチェックを通すためだけのものです。tool_choice: "none" にすると、モデルはこのツールを呼べません。ある検証では、宣言を足したリクエストは2.63秒で完了し、新しい検索呼び出しは0件でした。
複数のグループがそれぞれ A/B テストを行い、openai/codex の Issue や、いくつかのオープンソースのゲートウェイプロジェクトの Issue に結果を投稿しています。まとめると次のとおりです。
| 履歴 | 宣言したツール | 結果 |
|---|---|---|
| メッセージと推論のみ | なし | 完了 |
| 関数呼び出しとその出力 | なし | 完了 |
| web_search_call を含む | なし | 失敗 |
| web_search_call の ID を変更・削除 | なし | 失敗 |
| web_search_call を含む | 関数ツールのみ | 失敗 |
| web_search_call を含む | web_search | 完了 |
| 検索記録を削除 | なし | 完了 |
推論アイテムを消しても直らなかった、という報告もあります。リクエストサイズも原因から外れました。同じ結果は、ChatGPT アカウントでログインした公式の未改変 codex-cli でも出ています。検索記録を3件含む履歴は圧縮に失敗し、その3件だけを消した同じ履歴は問題なく圧縮できました。生のリクエストをキャプチャした開発者たちは、ルールを次のようにまとめています。入力がホスト型の web_search_call を再送していて、web_search* ツールが1つも宣言されていなければ、上流はストリームを失敗させる。
最小再現
サイズは関係ないので、長いセッションを用意する必要はありません。ある検証者は、同じキーとモデルで次のように2回試しています。
- 圧縮のしきい値をごく小さくして Codex を起動します:
codex -c model_auto_compact_token_limit=2000 - 1つ目のセッションでは、ツールを使わずに会話を続けて圧縮を走らせます。200 で圧縮できます。
- 2つ目のセッションでは、内蔵の Web 検索を一度使わせてから、圧縮が走るまで会話を続けます。
この検証では、2つ目のセッションの圧縮が「response protection」のエラーで6回連続して失敗しました。影響を受けるのは圧縮だけではありません。あるゲートウェイでは、10月3日から9日のあいだに gpt-6-luna の thread_description リクエストも66件失敗していました。
回避策:Codex 側
公式の Codex にはまだ修正がありません。10月10日時点で、Codex 0.162.1 と 0.163.0-alpha.5 までのリリースノートに該当する記載はなく、openai/codex の Issue にもメンテナーの返答はありません。
それまでは、~/.codex/config.toml で検索をオフにします。
web_search = "disabled"
Codex の設定リファレンスによると、値は disabled、cached(既定)、indexed、live の4つです。--yolo などフルアクセスのサンドボックス設定では既定値が live になるので、明示的に書いておきます。この設定は新しいセッションにだけ使ってください。検索が必要なときは、別の短いセッションで調べて、結果をファイルで渡します。
止まったセッションを救うには、まずそのセッションにプロンプトを送るのをやめます。送るたびに失敗する圧縮がリトライされるからです。~/.codex/sessions/ のファイルはそのまま残し、検索をオフにした新しいセッションを始めます。そこに古いファイルを読ませるか、目的・変更したファイル・決めたこと・残作業をまとめた引き継ぎメモを渡します。
修正:自前のゲートウェイ側
うまくいっているパッチは、どれも同じルールに従っています。input に web_search_call があり、web_search* ツールが宣言されていなければ、宣言を足します。呼び出し側がツールを1つも宣言していなかった場合は、tool_choice を "none" にします。input には手を加えません。
def declare_replayed_web_search(body: dict) -> dict:
"""Let the upstream accept a replayed web_search_call in a tool-less request."""
items = body.get("input")
if not isinstance(items, list):
return body
if not any(isinstance(i, dict) and i.get("type") == "web_search_call" for i in items):
return body
tools = body.get("tools") or []
if any(isinstance(t, dict) and str(t.get("type", "")).startswith("web_search") for t in tools):
return body
caller_had_tools = bool(tools)
# Cached index only: the declaration exists to satisfy the check, not to search.
body["tools"] = tools + [{"type": "web_search", "external_web_access": False}]
if not caller_had_tools:
body["tool_choice"] = "none"
return body
関数を正しく書くのは簡単です。難しいのは、必要な場所すべてに適用することです。
ハマりどころ
コンテキストを小さくしても直らない
しきい値を2,000トークンにしても再現するので、長さはトリガーではありません。長さが原因に見えるのは、圧縮が長いセッションでしか走らず、長いセッションほど途中で検索を使っている可能性が高いからです。
nginx の設定や IP 直結に変えても直らない
タイムアウトやアイドル切断は別の「stream disconnected」エラーの原因になりますが、このメッセージを生むことはありません。関係するプロキシのメンテナーも、この文言はプロキシが作ったものではなく、上流から返ってきたものだと確認しています。このメッセージが見えている時点で、リクエストは上流に届き、応答も戻ってきています。
WebSocket に切り替えたら直った「ように見える」
WebSocket のリクエストも同じボディを運ぶので、転送方式を変えてもルールは避けられません。うまくいっているパッチは、HTTP のパススルーに加えて WebSocket の経路にも適用されています。切り替えて「直った」セッションは、もともと履歴に検索がなかった可能性が高いです。
検索を途中で無効にすると、通常のターンも落ちる可能性がある
履歴にすでに web_search_call があるセッションで検索を無効にすると、通常のターンもツールを宣言しなくなり、同じように失敗するはずです。ルールから導いた推論で、実際に試したという報告はまだありません。
compaction_trigger だけで圧縮を判定すると漏れる
カスタムプロバイダーのローカル圧縮には compaction_trigger がありません。初期のあるパッチは、このエラーでプレーンテキストのリトライをするものでしたが、カスタムプロバイダーの圧縮はその処理に届きませんでした。最終的にうまくいったパッチは、履歴に検索があるときだけ宣言を足し、それ以外のリクエストには手を触れません。thread_description も含め、上流に送る最終的なリクエストボディすべてでチェックすることを勧める検証者もいます。
HTTP ステータスだけを見るとエラーを取りこぼす
エラーが HTTP 200 のあとの SSE エラーイベントとして届くと、ステータスコードしか見ないハンドラーには「途中で切れたストリーム」にしか見えません。あるゲートウェイの当初のパーサーは、トップレベルの message しか読まずにネストした error.message を見落とし、「compaction: the model failed」と報告していました。
Responses Lite ではトップレベルの web_search が 400 になる
Responses Lite では、web_search をトップレベルの tools に置くと 400 が返ります。うまくいっているパッチは、最初の additional_tools 入力アイテムにツールを足すか、ツールを入れた developer ロールのアイテムを挿入し、末尾の compaction_trigger を最後に保ちます。
検索記録を消すと要約から中身が抜ける
圧縮リクエストから検索記録を取り除く方法でも通りますが、そのときの検索で得た内容は要約から抜け落ちます。もう少し穏やかなやり方として、圧縮の前に web_search_call を普通のメモに書き換える方法もあります。
API キーで使う場合
報告はすべて ChatGPT のサブスクリプション用バックエンド、つまり ChatGPT アカウントでログインしたときに Codex が使う経路で起きています。Codex は API キーを使って公開の Responses API で動かすこともでき、こちらの経路ではこのエラーの報告はありません。AIHubMix の API キーで Codex を設定する手順は、Codex CLI × AIHubMix 連携チュートリアルにあります。
model = "gpt-6.1-sol"
model_provider = "aihubmix"
[model_providers.aihubmix]
name = "AIHubMix"
base_url = "https://aihubmix.com/v1"
wire_api = "responses"
env_key = "AIHUBMIX_API_KEY"
課金はトークン単位で、料金は gpt-6.1-sol のモデルページに載っています。
検証できていないこと
- API キーの経路:公開の Responses API では再現を試していません。「報告がない」というのは観察であって、テスト結果ではありません。
- ルールそのもの:コミュニティの A/B テストから導かれたものです。OpenAI は確認も返答もしておらず、意図した仕様かどうかもわかっていません。
- 途中のセッションで検索を無効にした場合:通常のターンも失敗するというのはルールからの推論で、試した報告はありません。
-
既定の「cached」モード:同じホスト型の
web_search_callが記録されると考えていますが、cached モードのセッションファイルを自分たちで確かめてはいません。
まとめ
- 圧縮が「response protection is unavailable」で落ちる原因は、履歴に残った
web_search_callと、ツールを宣言しない圧縮リクエストの組み合わせです。 - コンテキストの長さ、nginx、IP 直結、WebSocket への切り替えでは直りません。
- 公式 Codex に修正が入るまでは、新しいセッションで
web_search = "disabled"にし、止まった作業は引き継ぎメモを添えて新しいセッションに移します。 - 自前のゲートウェイでは、履歴に検索があるときに
web_searchを宣言し、ツールがなかったリクエストにはtool_choice: "none"を付けます。
サブスクリプションのバックエンドに依存しない構成で Codex を動かしたい場合は、上のチュートリアルに沿って AIHubMix の API キーで設定できます。
参考
- Windows Codex Desktop: context compaction always fails(openai/codex)
- Remote compaction fails after resuming hosted web_search_call history(openai/codex)
- Codex remote compaction fails with 502 "response protection is unavailable"(OpenAI Developer Community)
- Configuration Reference(OpenAI)
- Codex CLI × AIHubMix 連携チュートリアル(AIHubMix)