技術 約10分で読めます

CodexのSQLiteログがSSDに37TB書いた報告と6月22日の修正PR

いけさん目次

Codexが裏でログを書き続けてSSDを削る、という報告は、誇張ではなかった。
GitHub Issue openai/codex #28224では、~/.codex/logs_2.sqlite とそのWALファイルが主な連続書き込み元になり、21日ほどの稼働でメインSSDに約37TBを書いたと報告されている。

単純換算では年640TB前後。
1TBクラスの一般向けNVMe SSDだと、保証書き込み量が600TBWあたりの製品もあるので、常時起動に近い使い方だと無視できない。

TBW(Total Bytes Written)は、メーカーが保証する累計書き込み量の上限だ。
SSDのNANDフラッシュは書き換え回数に寿命があり、TBWはその寿命をユーザー視点のバイト数に換算した値で、保証期間とあわせて「このバイト数までは保証する」というラインを示す。
報告の年640TBは、1TBドライブを丸ごと640回書き直す量で、600TBWのドライブなら1年もたずに保証分の書き込み寿命を使い切る。
TBWを超えたら即故障というわけではないが、保証対象から外れ、以降は寿命が見通せなくなる。

同じCodexまわりでも、以前書いたSelected model is at capacityが出たらまず続行するはサーバー側の処理枠で止まる話だった。
今回はローカル端末に残る永続ログの話だ。画面の応答が普通に返っていても、裏ではSQLiteへの書き込みが止まらない。

logs_2.sqliteとWALが増える

報告に出ているファイルはこの3つだ。

~/.codex/logs_2.sqlite
~/.codex/logs_2.sqlite-wal
~/.codex/logs_2.sqlite-shm

SQLiteのWALはWrite-Ahead Loggingの略で、更新内容をいったん別ファイルへ追記してから本体へ反映する仕組みだ。
データベース本体の整合性を保ちやすく、書き込み中でも別プロセスが読み取りを続けられる。
3つのファイルはそれぞれ役割が違う。

ファイル役割
logs_2.sqliteデータベース本体。確定したページが入る
logs_2.sqlite-walWALファイル。コミット済みの変更を本体へ反映する前に追記でためる
logs_2.sqlite-shm共有メモリインデックス。WAL内のどのフレームが最新かを複数プロセスで共有する

書き込みはまずWALに追記され、ある条件でcheckpointが走ってWALの内容が本体へ書き戻される。
checkpointの起点はいくつかあるが、デフォルトのSQLiteはWALが約1000ページ(既定のページサイズ4KiBなら約4MiB)たまった時点で自動checkpointを試みる。
このため、ログのように高頻度で書き込むと「WALへ追記 → 4MiB前後でcheckpoint → 本体へ書き戻し → WALを巻き戻して再利用」というサイクルが回り続ける。

ここで物理的な書き込みを膨らませるのがwrite amplification(書き込み増幅)だ。
SQLite側では、1行のINSERTでもテーブル本体のページとインデックスのページが書き換わり、それがWALフレームとして書かれ、checkpoint時に本体へもう一度書かれる。論理的には1行でも、物理的には何度も書き込みが走る。
さらにSSD側でも増幅が起きる。SSDはバイト単位ではなくページ(数KiB〜十数KiB)単位で書き、消去はもっと大きなブロック単位でしかできない。少量の更新でも、ブロックの空き作り(ガベージコレクション)やウェアレベリングのために、コントローラが裏で元データを別ブロックへ書き直す。アプリが書いた量よりNANDフラッシュへ実際に書かれる量のほうが大きくなり、この比をWAF(Write Amplification Factor)と呼ぶ。
アプリ層(SQLiteのWALとcheckpoint)とデバイス層(SSDコントローラ)の増幅が重なるので、du で見えるDBサイズと、SSDが実際に書いた量は食い違う。

#28224の報告では、保持されている行は約50万件(あるスナップショットで506,149行、別のスナップショットで681,774行)なのに、AUTOINCREMENTのIDは55億を超えていた(5,543,677,486)。
保持行とこれまでに採番されたIDの差はおよそ1万倍ある。
残っているログ量だけを見ると1GB程度でも、過去に挿入して消した行が桁違いに多い。
「ファイルサイズが今1GBだから大丈夫」とは言い切れない。

もう1つ前のIssue #17320では、ストリーミング中に ~/.codex/logs_2.sqlite-wal へ毎秒5MiB前後、観測上は最大16MiB/sほど書いていたと報告されている。
プロセス環境では RUST_LOG=warn が設定されているのに、SQLite側にはTRACEレベルの行が残っていた、という点も指摘されている。

TRACEログが大半を占めていた

#28224の内訳では、保持されていたログ本文の推定サイズのうちTRACEが70.7%を占めていた。
上位には codex_api::endpoint::responses_websocketcodex_otel.log_onlycodex_otel.trace_safelogcodex_client::transport が並ぶ。
WebSocketやSSEの低レベルイベント、OpenTelemetryのミラーイベント、依存ライブラリ由来のログがSQLiteに流れ込んでいた。

ログ保持数を絞っていても、書き込み自体は減らない。
新しい行を入れる。古い行を削る。WALに追記する。条件が揃えばcheckpointする。
この挿入と削除の繰り返しが、見た目のDBサイズ以上にSSDを削っていく。

Codexのログテーブルは保持上限が決まっていて、上限を超えると古い行から削る。
だから行数は一定でも、裏では「採番されたID=これまで挿入した累計行数」がひたすら増えていく。
#28224の15秒サンプルでは、保持行が681,774行のまま動かないのに max(id) が約36,211増えた。1秒あたり2,400行前後を挿入しては同じだけ削っていた計算だ。
DBサイズが横ばいでも、その裏でSSDへの書き込みは止まっていない。

図にするとこうなる。1行が物理的には何段階も書き込みを生む。

flowchart TD
    A[WebSocketイベント発生] --> B[TRACEログ行を生成]
    B --> C[logsテーブルにINSERT]
    C --> D[WALへフレーム追記]
    C --> E[インデックス更新]
    E --> D
    F[保持上限を超過] --> G[古い行をDELETE]
    G --> D
    D --> H{WALが約4MiB?}
    H -->|Yes| I[checkpointで本体へ書き戻し]
    H -->|No| J[追記を継続]
    I --> K[SSDコントローラが物理書き込み]
    J --> K
    K --> L[NANDへの書き込み量がさらに増幅]

論理的には小さな1行でも、INSERT・インデックス更新・WAL追記・DELETE・checkpoint・SSD内部の再配置と、物理書き込みが何重にも積み上がる。
これがDBサイズと実書き込み量が乖離する理由だ。

Windows側でも同じ症状の報告が上がっている。
openai/codex #29463では、Codex Desktop 26.616系で logs_2.sqlite にTRACEのWebSocketログが入り続け、[analytics].enabled = false[otel].exporter = "none" を設定しても止まらないと書かれている。
28秒のサンプルで max(id) が573進み、直近1000件のIDでは TRACE log が839行を占めていた。

6月22日に2つの修正PRが入った

#28224は2026年6月22日に閉じられている。
報告者は、同日に2つのPRがマージされ、手元のCodexではログを85%ほど避けられそうだとしてIssueを閉じた。

1つ目は #29432 で、成功したResponses WebSocketイベントごとに3種類のローカルログを出していたのを止める変更だ。
PR本文では、各WebSocketイベントがTRACEのフルペイロード、OpenTelemetryのログイベント、トレースイベントを作り、忙しいスレッドでは1000行パーティションが数秒で埋まって挿入と削除が高速で繰り返されると説明している。
止めるのは「成功したWebSocketイベントごとのペイロードログとミラーイベント」だけで、WebSocketイベントのカウンター、所要時間メトリクス、レスポンスのタイミング計測、パース処理、エラーハンドリングは残す。診断に使う集計値は捨てず、毎イベントの生ログだけ落とす切り分けだ。

2つ目は #29457 で、永続ログに入れる対象からノイズの大きいtargetを外す変更だ。
target=log 経由で橋渡しされた依存ライブラリログと、codex_otel.log_onlycodex_otel.trace_safe をSQLite sinkから除外する。
ここで言う target は、Rustの tracing クレートでログ行に付くモジュール名のようなラベルだ。target=loglog クレート経由で流れ込む依存ライブラリ(tokio-tungsteniteやhyper_utilなど)のログ、codex_otel.log_onlycodex_otel.trace_safe はOpenTelemetry用に複製されたミラーイベントを指す。
PRはこの3つのtargetだけをSQLite sinkから外し、それ以外のtargetのTRACE永続化は残す。app-serverとTUIで同じフィルタを共有する、ともある。
注意点として、外すのはローカルSQLiteへの永続化だけで、リモートのOpenTelemetryエクスポートとメトリクスはそのまま動く。「テレメトリを全部止めた」わけではなく、「ローカルにベタ書きしていた重複ログを止めた」のが正確な範囲だ。

2つのPRはCLIの0.142.0に入っている。
GitHub Releasesの0.142.0(タグ rust-v0.142.0、UTCで6月22日22:19公開)では、Choresに「Reduced persistent-log churn by removing per-event WebSocket payload logging and filtering duplicated telemetry records. (#29432, #29457)」と両方の番号が明記されている。
一方、OpenAI公式のCodex changelog(developers.openai.com)側は、6月18日時点のCodex app 26.616に「追加の性能改善とバグ修正」とあるだけで、この2つのPR番号は出ていない。CLIのGitHub Releasesとデスクトップアプリ側のchangelogで反映タイミングがずれる。

なので手元では、単に「最新版にしたから終わり」で済ませず、更新後に logs_2.sqlite-wal のmtimeやサイズがまだ増えるかを見るほうがいい。
特にCodex DesktopとVS Code拡張は内部の app-server のビルドがCLIと別系統で更新される。アプリのバージョン番号(26.616系など)とCLIのバージョン番号(0.142.0など)は体系が違うので、片方を更新しても app-server 側に修正が乗っているとは限らない。

手元で見る場所

macOSやLinuxなら、まず ~/.codex のサイズを見る。

du -sh ~/.codex
ls -lh ~/.codex/logs_2.sqlite*

実行中に増えているかを見るなら、数十秒おいて同じコマンドをもう一度打つ。
logs_2.sqlite-wal のサイズや更新時刻が動き続けるなら、Codexを閉じた状態でも残っているプロセスを確認する。

pgrep -af codex
lsof -nP ~/.codex/logs_2.sqlite-wal

#22444では、tmuxに残した古いCodex TUIセッションが削除済みの巨大WALを握り続け、du では減ったのに df -h では空き容量が戻らない例が出ている。
この形だと、ファイルを消してもプロセスがファイルディスクリプタを閉じるまで容量は戻らない。

WindowsのCodex Desktopなら、C:\Users\<user>\.codex\logs_2.sqlite とWALファイルを見る。
#29177では、Windows側でlogs、state、memories、goals系のSQLiteファイルをRAMディスクへ逃がすと体感が軽くなったという報告も出ている。
これは対症療法であって、永続化したい状態まで揮発領域へ置くと、別の事故を招く。
やるならログ系だけに絞る。

長時間セッションでローカルI/Oも増える

今回の件は、AIエージェントがコードを壊すとか、プロンプトインジェクションで外へ出るとかいう話ではない。
普通に使っているだけで、端末上の補助ログがSSDを地道に消耗させる。

Claude Code側のtool callがcourt付きで壊れる話でも、長時間セッションに寄せた運用で止まるケースが増えていた。
今回はCodex側で、長時間起動や複数セッションがローカルSQLiteに負荷をかける。

エージェントをtmuxやデスクトップアプリで何日も置くなら、応答品質だけでなく、ローカルの副作用も気にする。
CPU、メモリ、GPU、ネットワーク、プロセス数に加えて、~/.codex~/.claude のような作業ディレクトリの書き込み量も見ておく。

現時点でやることは細かい。

  • Codex CLIやCodex Desktopを最新版へ更新する
  • 長期間放置したTUIやDesktopスレッドを閉じる
  • ~/.codex/logs_2.sqlite-wal がGB単位になっていないか見る
  • 削除しても空き容量が戻らない場合は、削除済みファイルを握っているCodexプロセスを探す
  • まだ増えるならIssue番号、Codexのバージョン、OS、logs_2.sqlite の内訳を添えて報告する

派手に「SSDを殺す」と煽るより、TRACEログの永続化設定が本番のローカルアプリに残ってしまった事故、と捉えるほうが対処しやすい。
ログが目的なら保持量だけでなく、書き込み頻度、WAL、古いプロセスのファイル保持まで同時に潰す。

参考