Codexで長期間続けていたスレッドが、突然使えなくなりました。
表示されたエラーはこれです。
Error running remote compact task: {"detail":"Bad Request"}
一度失敗しただけなら再試行すればよさそうですが、このスレッドではcompactを試すたびに同じHTTP 400が返り、そのうち通常の応答まで失敗するようになりました。
新しいスレッドへ移れば作業自体は続けられます。ただ、今回は長期間動かしていたGoalがあり、それまでの調査内容や判断もできるだけ維持したかったので、壊れたスレッドを調べて復旧することにしました。
compactとは何か
長い会話を続けると、過去のやり取りをすべてそのままモデルへ渡し続けることはできません。
そこで、それまでの会話を圧縮した状態へ置き換え、続きを処理できるようにするのがcompactionです。
OpenAIのcompaction APIでは、会話をcompactすると、ユーザーメッセージと暗号化されたcompaction itemを含む出力が返ります。画像も入力として扱える仕様になっています。
ただし、Codexが内部で使うremote compactionと公開APIが、まったく同じ制限で動いているとは限りません。
今回のエラー本文はBad Requestだけで、何が制限に当たったのかまでは分かりませんでした。
まず履歴を壊さずに調べる
復旧作業を始める前に、元のセッション履歴と関連するデータベースをコピーしました。
元の履歴は約356 MBあり、5万行を超えていました。
直接書き換えて失敗すると戻せないので、調査と再現試験はすべてコピー側で行います。コピー後はハッシュを比較し、元の履歴と同じ内容であることも確認しました。
また、履歴を読むためのデータベースは読み取り専用で開き、バックアップAPIを使って複製しました。コピーしたデータベースには整合性検査もかけています。
この時点では、元のセッション、Goal、作業ツリーには触れていません。
有効なコンテキストの98%が画像だった
最後に成功したcompactと、それ以降の履歴から、現在の処理に使われるコンテキストを再構成しました。
再構成できた項目は530件です。その中に、Base64のdata URLとして保存されたPNG画像が40枚ありました。
集計すると、次のようになっていました。
| 項目 | サイズ |
|---|---|
| PNGの実体 | 約42.4 MB |
| Base64 data URL | 約56.6 MB |
| 再構成したJSONに占める割合 | 約98.1% |
ほぼ画像です。
40枚はすべて800×600ピクセルでした。PNGのチャンクCRCとzlib展開も確認しましたが、ファイルが壊れているわけではありません。
問題は圧縮方法でした。
自作側が無圧縮PNGを出していた
履歴に入っていた画像は、自作ソフトウェアの表示確認中に生成したものです。
そのPNG生成処理を確認すると、IDATへ非圧縮のDEFLATEブロックを書き出していました。
40枚のうち22枚は、1枚あたり約1.92 MBあります。800×600の画像としてはかなり大きいですが、画素データをほぼそのままPNGへ詰めているので、このサイズになります。
画像表示ツールへ渡したあとも、そのPNGがBase64へ変換され、セッション履歴へ保存されていました。
無圧縮に近いPNGを生成
↓
画像表示ツールへ渡す
↓
Base64 data URLとして履歴へ入る
↓
長期セッション内で画像が積み上がる
同じ画像がcompact後の履歴にも残るため、長く使っているうちにセッション全体も大きくなっていました。
サイズだけが原因とはまだ言えない
画像が履歴の大部分を占めているので、原因はこれに見えます。
ただ、記録を確認すると、約57.6 MBの通常リクエストが一度はHTTP 200で成功していました。その次の同程度のリクエストではHTTP 400になっています。
そのため、単純に「リクエストが何MBを超えると必ず失敗する」とは言えません。
通常応答とremote compactionで処理が異なる可能性もあります。WebSocket切断後のHTTPフォールバックや、compact固有のデータ構造が影響した可能性も残ります。
バックエンドが返したのはBad Requestだけなので、内部の直接的な失敗条件は不明です。
そこで、画像のデータ量だけを変えるA/B試験を行いました。
画素を変えずにPNGだけ圧縮する
比較条件を崩さないため、画像の枚数、解像度、履歴上の順番、項目ID、テキスト、compact済みの暗号化データは変更しません。
変更するのは、PNGのIDATに入っている圧縮データだけです。
40枚を通常のzlib圧縮で作り直したところ、画像本体の合計は次のように変わりました。
42,418,652 bytes
↓
277,140 bytes
約42.4 MBから約277 KBです。
PNGファイル自体のハッシュは変わりますが、これは圧縮方法を変えたので当然です。
代わりに、次の内容が圧縮前後で一致することを確認しました。
- 幅と高さ
- PNGの付帯チャンク
- zlib展開後の走査線データ
つまり、モデルへ渡される画像の画素は同じで、保存方法だけが変わっています。
コピー側でcompactが成功した
まず、変更していない履歴のコピーでremote compactを再実行しました。
こちらは約3.8秒で、元と同じHTTP 400になりました。
次に、PNGだけを可逆圧縮したコピーで試しました。
画像の枚数: 40枚のまま
画像の画素: 同一
履歴の項目: 同一
Goal: 維持
PNGの格納サイズだけを縮小
この状態では、remote compactが約84秒で成功しました。
その後の通常応答も成功し、別プロセスからセッションを読み直して、最新の応答まで参照できることを確認しました。
元の状態では失敗し、画像だけを可逆圧縮すると成功しています。この比較から、履歴内の巨大なinline PNGが失敗の主要な条件だった可能性はかなり高くなりました。
元のセッションIDのまま復旧する
コピー側で成功を確認したあと、元のセッションにも同じ軽量化済みコンテキストを復旧用チェックポイントとして追加しました。
既存の約356 MBの履歴は削除も置換もしていません。元履歴がそのまま先頭に残っていることを、作業前のハッシュと比較して確認しています。
その状態で、Codex自身のremote compactを実行しました。
結果は成功です。
compact後の通常応答も成功し、Codexを別プロセスから起動して同じセッションを読み直すこともできました。
新しいスレッドへ内容を要約して移したわけではなく、元のセッションID、Goal、作業状態を維持したまま復旧できています。
復旧確認中に開発作業が勝手に再開されないよう、Goalの状態だけは一時停止へ変更しました。目的や使用量などの情報は維持されています。
復旧後は約2.9万トークンになった
復旧後の応答で記録された入力は約29,000トークンでした。
壊れる直前に記録されていた通常応答の入力は約20万トークンだったので、compactによってかなり余裕が戻っています。
この結果を見る限り、今回詰まっていたのはトークン上限だけではなさそうです。
画像の内容を変えず、実バイト表現を小さくしたことでremote compactが通ったため、inline画像のサイズとcompaction処理の組み合わせが問題だったと考えています。
内部履歴の変更は普通の復旧方法ではない
今回は原因を調べるため、Codexを終了した状態で履歴のコピーを作り、検証後に復旧用データを追加しました。
これは通常の利用方法ではありません。履歴やデータベースの形式は変更される可能性があり、書き換え方を間違えるとセッションを完全に読めなくする危険があります。
同じエラーが出た場合、最初に試すべきなのは再起動や新しいスレッドへの移行です。どうしても長期セッションを維持する必要がある場合でも、元データを直接変更せず、完全なバックアップとコピー側での再現確認が必要です。
今回は具体的なセッション識別子、リクエスト識別子、ローカルパス、Goalの内容などは記事から省いています。
今後やること
直接の入口は、自作ソフトウェアが無圧縮に近いPNGを生成していたことです。
同じ800×600の画像でも、40枚で約42.4 MBだったものが約277 KBまで縮んでいます。画素が変わらないなら、無圧縮で保持する理由はほとんどありません。
まずはPNG生成処理を通常の圧縮へ変更します。
Codex側についても、画像をinline Base64で長期履歴へ積み続ける場合は、トークン数だけでなく実際のデータ量も問題になります。画像を履歴の外へ保存して参照だけ持つ方法や、compaction前に巨大な画像データを整理する仕組みがあると安全そうです。
まとめ
Codexの長期セッションでremote compactがHTTP 400になり、その後は通常応答も失敗するようになりました。
履歴を調べると、有効なコンテキストの約98%をBase64化されたPNGが占めていました。元になった画像は、自作ソフトウェアが非圧縮のDEFLATEブロックで生成したものです。
画像40枚の画素や履歴構造を維持したまま、PNGだけを可逆圧縮すると、画像本体は約42.4 MBから約277 KBになりました。
変更前のコピーではremote compactがHTTP 400になり、軽量化したコピーでは成功しました。元のセッションにも同じ方法を適用し、同じセッションIDとGoalを維持したまま復旧できています。
バックエンド内部でHTTP 400になった直接の条件までは分かりません。ただ、今回のA/B試験では、巨大なinline PNGが主要なトリガーだったことをかなり強く確認できました。