技術約15分で読めます

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

いけさん目次

TL;DR

影響 Codexが ~/.codex/logs_2.sqlite にTRACEログを書き続け、21日で約37TB(年約640TB規模)のSSD書き込み

回避 SQLite保存先をtmpfs等へ退避する手はあるが、ログ以外の再開状態も一緒に移り、書き込み量の減少幅は未確認。肥大化したファイルはCodex終了後にVACUUMで圧縮

ステータス 随時更新。6/22の3PRで約85%減も、0.145.0とDesktop同梱0.146.0-alpha.3.1では書き込み経路の残存報告が続く(8/6時点、末尾の続報)


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、古いプロセスのファイル保持まで同時に潰す。

2026年7月1日時点の続報

Issue #28224 はコメントが119件規模まで伸び、書き込み量の見積もりも「21日で37TB」から「年約640TB」に整理し直された。
6/22の3つのPRで約85%は削減されたが、これは完全修正ではない。残る分を直すPR #29599 は 0.143.0(まだアルファ版のプレリリース)にしか入っていない。
最新の安定版 rust-v0.142.5(WebSocketのTRACEログを外すPR #30771 / #30757 のバックポート)を入れても、7/1時点で「まだ直っていない」という報告が出ている。Windows版・Mac Desktop版でも更新後の再発報告(#29177 / #29463 / #29532)が続く。
確実に書き込みを止めたいなら、ログの出力先である ~/.codex/ をtmpfsなどのRAMディスクへ逃がすのが現時点で最も堅い。再起動でログは消えるが、SSDへの書き込みは発生しない。
大手技術メディアも本件を報じているが、最新版でも完全には止まっていない点は上記のとおり。見出しだけ見て解決済みと受け取らないほうがいい。

2026年8月6日時点の続報

最新の安定版はCodex CLI 0.146.1(8/6時点)。0.146.1の変更はサイバー系能力を持つモデルの自動レビュー既定値を安全側へ寄せる1件だけで、ログまわりの修正は含まれない。6/22の3PRで潰した経路とは別に、まだ解決していない問題が複数ある。0.145.0では#35092が、SSEのデルタイベントとHTTPトランスポートのペイロードがTRACEで永続化され続け、応答をストリーミングしている間は毎秒20〜41件、アクティブなターン中は70〜114KiB/sの書き込みが続いていると報告している。Desktop同梱の0.146.0-alpha.3.1でも、#35308の報告ではある30秒窓でapp-serverプロセス全体が約14.5MB、より活発な別の30秒窓では約43.9MBを書いていた(このカウンターはlogs_2.sqlite単体ではなく、app-serverが読み書きする他の状態ファイルも含む)。アクティブなサンプルでは1秒あたり35〜42件のINSERTが続いていた。WALが数十GBまで肥大化する別件の#28997も未解決のまま残っている。

手元のM1 Max Mac(Codex CLI 0.146.0)でも実際に確認した。~/.codex/logs_2.sqliteは63.5MBあったが、logsテーブルの行数は保持上限の3,000件で、最後に行が挿入されたのは確認時点から約40時間前だった。直近では動いていない。このファイルをコピーしてVACUUMをかけると、63,557,632バイトから2,359,296バイトまで縮んだ。96%が、削除済みのはずなのに回収されていない領域だった。本文で触れたAUTOINCREMENT IDと保持行数の乖離が、このファイルではそのまま回収されない領域になっていた。

# Codexを完全終了してからコピーで試す(本体を直接VACUUMしない)
cp ~/.codex/logs_2.sqlite /tmp/logs_2_copy.sqlite
sqlite3 /tmp/logs_2_copy.sqlite "VACUUM;"
ls -la /tmp/logs_2_copy.sqlite

codex exec(対話TUIでなく単発実行するモード)についても確認した。CODEX_SQLITE_HOMEを空の捨てディレクトリへ向け、codex execを2回叩いたところ、logsテーブルの行数は0のままだった。手元で2回試した範囲ではこの条件で同じログ経路を確認できなかった、というだけで、TUIやDesktopのapp-serverと同じ経路を全条件で通らないと断定はできない。

ただしexecでも別のSQLiteファイルは書き込まれていた。同じ捨てディレクトリで、state_5.sqlitethreadsテーブルに386行・約3MBが一度に入った。中身はこのM1 Max Macで過去に実行したCodexセッションのプロジェクトパス、Gitブランチ、Git origin URL、冒頭メッセージのプレビューなどで、backfill_stateテーブルにはcompleteのステータスが1行残っていた。この捨てディレクトリを試した際は、sqlite_homeを新しい場所へ向けた初回起動で過去の全プロジェクト分のセッション履歴が一度だけまとめて書き戻され、ログ用のディレクトリを分けたつもりでも、プロジェクト名や実パスを含む複数プロジェクトの作業履歴がそこへ一括で書き出されていた。

回避策も、公式ドキュメントとissueコメントの記述を実際に試した。sqlite_homeconfig.toml)とCODEX_SQLITE_HOME(環境変数)は、SQLiteバックエンドの状態全体(ログだけでなくthreadsmemoriesgoalsも含む)の保存先を変える。log_dirは平文のTUIログ専用で、SQLite側には効かない。[analytics].enabled = false[otel].exporter = "none"は、公式資料上はそれぞれanalyticsとOpenTelemetryエクスポーターの設定で、SQLiteログを制御する設定とは書かれていない。本文で触れた#29463でも、Windows Desktop 26.616で両方を無効にしたままローカルSQLiteへのTRACE永続化が続いたと報告されている。[feedback].enabled = falseも、公式資料では/feedback送信機能のオンオフとされているだけで、SQLite永続化を止める設定ではない。SQLiteログだけを狙って無効化する公式フラグは、issueで要望が出ているだけで実在しない。

INSERT無視トリガー(logsテーブルへの挿入をRAISE(IGNORE)で握りつぶす)は、手元のSQLiteで動作を確認した。

-- Codexを完全終了してから設置する
CREATE TRIGGER IF NOT EXISTS block_log_inserts
BEFORE INSERT ON logs
BEGIN
  SELECT RAISE(IGNORE);
END;

設置後にINSERT文を投げても行数は増えなかった。ただし診断ログはTRACEだけでなくWARN・ERRORも含めて全部止まるので、あとから障害調査に使う余地はなくなる。

sqlite_homeをtmpfsへ逃がす方法も、macOS標準のmount_tmpfsコマンドで手順自体は確認した。

mkdir -p "$HOME/.codex-sqlite-ram"
sudo /sbin/mount_tmpfs -s 2147483648 "$HOME/.codex-sqlite-ram"
sudo chown "$(id -u):$(id -g)" "$HOME/.codex-sqlite-ram"
CODEX_SQLITE_HOME="$HOME/.codex-sqlite-ram" codex

macOS 11以降ならmount_tmpfsが標準搭載されていて、Linuxの/dev/shmと同じ考え方で使える。古いmacOSのようにHFS+のRAMディスクをhdiutilで作る必要はない。この手順はsudoのパスワード入力がいるため今回は実行していない。実際に書き込み量がどれだけ減るかも未確認だ。sqlite_homeを移すとthreadsテーブルの再開可能なセッション履歴も一緒に移り、アンマウントや再起動で消える。

参考