SQLite WAL journal mode corrupts runtime DBs (logs_2, state_5, …) when CODEX_HOME is on a network filesystem (NFS)
What version of Codex CLI is running?
codex-cli 0.137.0
What subscription do you have?
Business
Which model were you using?
gpt-5.4-mini
What platform is your computer?
Linux 6.12.94+deb13-amd64 x86_64 unknown
What terminal emulator and version are you using (if applicable)?
ssh
Codex doctor report
What issue are you seeing?
When CODEX_HOME (~/.codex) is on an NFSv4.2 mount, codex's runtime SQLite databases periodically corrupt. logs_2.sqlite is hit most often (it is the high-volume trace/feedback writer), but the same root cause affects all four runtime DBs (state_5.sqlite, logs_2.sqlite, goals_1.sqlite, memories_1.sqlite) because they all open with PRAGMA journal_mode=WAL.
Observed: PRAGMA integrity_check returns database disk image is malformed (DB header corruption); codex's auto-recovery moves the damaged DB into ~/.codex/db-backups/ and rebuilds a fresh one. With logs_2 this is merely annoying (ephemeral, recreatable); with state_5 it degrades the resume index (codex falls back to the JSONL rollout scan, so data isn't lost, but the index is rebuilt repeatedly).
Relevant mount: mini7:/mnt/home on /home type nfs4 (rw,relatime,vers=4.2,hard,proto=tcp,local_lock=none,sec=sys). CODEX_HOME is shared across multiple hosts (codex run from different SSH sessions/machines against the same home).
What steps can reproduce the bug?
- Put CODEX_HOME on NFSv4 (e.g. ~/.codex under an NFSv4.2 mount, local_lock=none, shared by ≥1 host).
- Run codex normally over time / from more than one host against the same CODEX_HOME.
- After some uptime, inspect the runtime DBs:
sqlite3 ~/.codex/logs_2.sqlite 'PRAGMA integrity_check;'
sqlite3 ~/.codex/state_5.sqlite 'PRAGMA integrity_check;'
- Expected to eventually return database disk image is malformed (header) rather than ok.
- Equivalently, observe codex auto-creating ~/.codex/db-backups/<ts>/ and rebuilding the DB on startup.
Root cause is in codex-rs/state/src/runtime.rs::base_sqlite_options, which hardcodes SqliteJournalMode::Wal for every runtime DB.
What is the expected behavior?
Runtime SQLite DBs should not corrupt when CODEX_HOME is on a network filesystem. SQLite's own documentation states WAL must not be used on network filesystems (mmap'd -shm is not coherent across
NFS clients — https://www.sqlite.org/wal.html, §"Use of WAL with Network Filesystems"). Codex should use a rollback journal mode (e.g. Truncate) on NFS and keep Wal only on local filesystems.
Additional information
Root cause. SQLite WAL coordinates readers/writers through a mmap'd shared-memory index file (*-shm). On NFS each client has its own page-cache view of -shm; mmap coherence is not guaranteed
across clients, so a WAL checkpoint can write wrong data back to the DB → header corruption. This is independent of fcntl locking — SQLite's POSIX byte-range locks work on NFSv4; the failure is
the separate mmap'd index. Rollback journal modes (Delete/Truncate) create no -shm, so they don't have this exposure.
Empirical verification (NFSv4.2 host, sqlite 3.46.1):
- fcntl byte-range locks are enforced on this mount (local_lock=none → stateful server-side NFSv4 locks); a conflicting LOCK_EX|LOCK_NB gets EAGAIN.
- WAL creates -shm while a connection is open; Delete/Truncate create no -shm.
- Delete/Truncate on NFS: 4 concurrent writers × 200 rows = 800 rows, 0 database is locked errors, integrity_check=ok.
- Latency on NFS, batched 1000 rows/txn (codex's log_db.rs flush path): WAL 0.0041 s, Truncate 0.0041 s (== WAL), Delete 0.0055 s. Rollback modes are effectively free vs WAL on the batched path.
Proposed fix. In base_sqlite_options, choose journal mode from the DB path's filesystem instead of hardcoding Wal:
- Linux: statfs(2), compare f_type to NFS_SUPER_MAGIC (0x6969).
- macOS: statfs, check MNT_NFS in f_flags.
- Platforms without a portable
statfs(Windows, BSD, …): fall back toWal(current behavior — never worse than today). statfserror or an otherwise-unclassifiable mount: bias toward the NFS-safe mode (Truncate) rather than silently keepingWAL, since this is a corruption-prevention fix.
On NFS use SqliteJournalMode::Truncate (rollback journal, no -shm, NFS-safe; == WAL on the batched path, less metadata churn than Delete because it truncates the journal instead of create+unlink per commit). On local filesystems keep Wal. The decision can be factored into a pure helper for unit testing (the statfs call can't be exercised in CI without an NFS mount). This fixes the corruption for all runtime DBs, not just logs_2.
Current workaround. A wrapper deletes logs_2.sqlite{,-wal,-shm} on each codex startup so every session starts with a clean WAL state. This works only because logs_2 is ephemeral; it does not protect state_5/goals_1/memories_1, and is not a source-level fix.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗