手元のSSDが、キー入力していない時間帯まで一定のペースで削られていく。プロセスを追い詰めると、犯人はエディタでもブラウザでもなく、常駐させていたOpenAI Codex CLIのログ機構だった。あるユーザーの計測では、約21日の連続稼働でSSDへの書き込みが約37TBに達し、そのほとんどをCodexのSQLiteログが占めていた。年換算でおよそ640TB。1TBのSSDなら1年で約640回、ドライブ全体を上書きし尽くす計算になる。
この話が怖いのは、バグそのものよりも「静かさ」だ。エラーも警告も出ないまま、消耗品であるNANDフラッシュの寿命だけが削られる。報告はopenai/codex の Issue #28224にまとまっており、投稿者(Rui Fan氏)は実測データ付きで原因まで切り分けている。以下はその一次情報を追ったものだ。
💾 なぜ37TBも書けてしまうのか
Codexは操作フィードバック用のログを、ホームディレクトリのSQLiteファイルに書き続ける。実体は次の3ファイルだ。
~/.codex/logs_2.sqlite
~/.codex/logs_2.sqlite-wal
~/.codex/logs_2.sqlite-shm
面白いのは、ファイルサイズが暴れないことだ。DBは古い行を刈り取りながら運用されるため、見た目は1GB前後で落ち着いている。ところがSQLiteのAUTOINCREMENTカウンタを覗くと、保持している行と累計採番数のあいだに10000倍近い開きがある。
| 指標 | 値 |
|---|---|
| 現在のファイルサイズ | 1.2 GiB |
| 保持している行数 | 約50万行 |
| 採番済みのrow id累計 | 約55億 |
つまりファイルは太っていないのに、裏では膨大な行が「挿入されては刈られる」を繰り返している。投稿者が15秒間サンプリングしたところ、保持行数は変わらないまま36,211行が挿入されていた。挿入・インデックス更新・WALへの書き出し・刈り取りという一連の処理が、そのつどストレージへの物理書き込み(write amplification、書き込み増幅)を生む。ファイル監視ツールでサイズだけ見ていては絶対に気づけない類のコストだ。
TRACEログが7割を食っていた
では何がそんなに書かれているのか。保持バイトをログレベル別に割ると偏りが露骨だった。
| レベル | 保持バイトの割合 |
|---|---|
| TRACE | 70.7% |
| INFO | 25.7% |
| DEBUG | 3.0% |
| WARN | 0.6% |
TRACEは本来、開発者がバグを追うときだけ有効にする最も詳細なレベルだ。それが常時ディスクに焼かれている。内訳の筆頭はcodex_api::endpoint::responses_websocketのTRACEで527.4 MiB。APIとのWebSocketイベントの生ペイロードを一つ残らず記録していた。加えてcodex_otel.log_onlyとcodex_otel.trace_safeという、OpenTelemetryのミラー出力が二重に書き込まれ、これだけで25%を超える。要はテレメトリの下書きを、ローカルSSDに全文保存し続けていた。
根本原因はシンク(ログ出力先)の初期化にあった。SQLiteログ用のフィルタが、既定値をまるごとTRACEに設定していたのだ。
Targets::new().with_default(Level::TRACE)
この一行が効いているのがポイントで、コンソール表示の詳細度をどう絞ろうと、SQLiteシンクは自前の既定TRACEで全ターゲットを拾ってしまう。依存ライブラリの内部ログ(tokio-tungsteniteのWebSocket処理や、ld.so.cacheを開いたといったinotifyイベント)まで無差別に永続化されていた。ログレベル設計を一箇所間違えると、通信量ではなくストレージ寿命という思わぬ形で跳ね返る好例だと思う。
🛠 止め方と、公式の直し方
すぐ止めたいなら、Issue内で共有されている回避策が手堅い。Codexを終了してから、logsテーブルへのINSERTをトリガーで無効化する。
sqlite3 ~/.codex/logs_2.sqlite "CREATE TRIGGER IF NOT EXISTS \
block_log_inserts BEFORE INSERT ON logs BEGIN SELECT RAISE(IGNORE); END;"
公式側も6月中に動いた。修正は3本のPRに分かれている。
| PR | 内容 | 反映バージョン |
|---|---|---|
| #29432 | 成功したWebSocketイベントのTRACE記録とOpenTelemetryログ/トレース発行を停止 | 0.142.0 |
| #29457 |
target=logとcodex_otelミラーをSQLiteシンクから除外 |
0.142.0 |
| #29599 | ブリッジ経由で漏れていた依存ログをシンク内で追加拒否 | 0.143.0 |
投稿者の再計測では、これらでログ量の約85%が削減された。カウンタや所要時間などのメトリクス、リモートへのOpenTelemetryエクスポートは残しつつ、ローカルへの生ペイロード保存だけを外す方針で、直し方としても筋がいい。
The local SQLite log sink currently enables TRACE for every target.(PR #29457より)
開発機のログは誰のためのものか
この一件は、Codex固有のうっかりミスとして片づけるにはもったいない。エージェント型ツールは「あとで挙動を再現・分析できるように」と観測ログを厚く取りたがるが、その保存先が開発者の物理ドライブになった瞬間、詳細度の既定値は寿命コストに直結する。SSDのTBW(総書き込み保証量)は消耗品の残量そのものであり、静かに減る。
実務者として持ち帰りたい教訓は二つある。ひとつは、常駐する開発ツールを入れたら~/.cacheや~/.local配下に肥大するログDBがないか一度確認しておくこと。もうひとつは、自分がツールを作る側なら、テレメトリの既定はTRACEではなくINFO以上に置き、生ペイロードの永続化はオプトインにすること。観測性は正義だが、その代金を無言でユーザーのハードウェアに請求してはいけない。バージョンを0.142.0以降に上げているかは、今日のうちに見ておいて損はない。