logs_2.sqlite never reclaims freed pages: auto_vacuum=INCREMENTAL is set but never run, so the file grows monotonically despite working 10-day retention
What version of the Codex App are you using (From “About Codex” dialog)?
1.2026.190.0 (MSIX OpenAI.ChatGPT-Desktop_1.2026.190.0_x64__2p2nqsd0c76g0)
What subscription do you have?
Pro $100
What platform is your computer?
Microsoft Windows NT 10.0.26100.0 x64 (Windows 11 Home Single Language)
What issue are you seeing?
~/.codex/logs_2.sqlite grows without bound on a normal desktop profile even though row retention is working correctly. The row count is at steady state (~10 days of rows), but the file on disk keeps climbing, because pages freed by the retention DELETE go to the SQLite freelist and are never returned to the OS.
The DB is created with auto_vacuum = 2 (INCREMENTAL), but nothing ever runs PRAGMA incremental_vacuum. run_logs_startup_maintenance() deletes rows older than the retention window and then runs PRAGMA wal_checkpoint(PASSIVE) — neither of which shrinks the main DB file. So retention bounds rows but not bytes.
Measured on this machine (Windows 11, Codex Desktop 1.2026.190.0), on an offline copy of the DB:
| metric | value |
|---|---|
| logs_2.sqlite | 579,276,800 bytes (552 MiB) |
| logs_2.sqlite-wal | 4.6 MiB |
| page_count | 141,425 (4096-byte pages) |
| freelist_count | 37,128 — 26% of the file is dead space |
| auto_vacuum | 2 (INCREMENTAL — never invoked) |
| rows in logs | 253,043 |
| SUM(estimated_bytes) | ~267 MB of actual log content |
| ts span | 1784402440 → 1785266499 (exactly 10.0 days — retention is working) |
~267 MB of live content is occupying a 552 MiB file.
Growth rate. I compacted this same DB with VACUUM INTO on 2026-07-17: 1.38 GB → 270 MB, zero rows lost. Eleven days later it is back to 552 MiB — roughly 25 MB/day of permanent, unreclaimed growth, with the row count flat. Left alone it returns to >1 GB in about a month, every month, on a machine that is only running the app normally.
Level distribution over that same 10-day window (the volume amplifier, already tracked in #29674 / #31542 / #31111 — this issue is about the reclamation half):
| level | rows | est. bytes |
|---|---|---|
| TRACE | 136,468 | 130.7 MB |
| INFO | 56,841 | 69.3 MB |
| DEBUG | 54,048 | 63.8 MB |
| WARN | 5,511 | 3.2 MB |
| ERROR | 175 | 0.1 MB |
TRACE is 54% of rows and 49% of logged bytes, at default settings with no debug flag enabled.
What steps can reproduce the bug?
- Run Codex Desktop normally for several weeks on Windows — default config, no debug flag,
RUST_LOGunset. - Wait until age-based retention is actively deleting (i.e. the oldest row is ~10 days old).
- Copy
~/.codex/logs_2.sqlite,-waland-shmto a scratch directory. Do not open the live DB — it is in WAL mode under a running app; inspect the copy only. - Inspect the copy:
import sqlite3
c = sqlite3.connect("logs_2.sqlite") # the COPY
q = lambda s: c.execute(s).fetchall()
print("page_count ", q("PRAGMA page_count"))
print("freelist ", q("PRAGMA freelist_count"))
print("auto_vacuum ", q("PRAGMA auto_vacuum"))
print("rows ", q("SELECT COUNT(*) FROM logs"))
print("live bytes ", q("SELECT SUM(estimated_bytes) FROM logs"))
print("ts span ", q("SELECT MIN(ts), MAX(ts) FROM logs"))
print("levels ", q("SELECT level, COUNT(*) FROM logs GROUP BY level"))
- Repeat over days/weeks.
COUNT(*)and thetsspan stay flat at the retention window, butpage_countandfreelist_countonly ever go up. The file never shrinks, at any point in the app lifecycle — launch, quit, idle, or restart.
To see the reclaimable amount directly: VACUUM INTO 'compact.sqlite' on the copy. Here that produced a 270 MB file from a 552 MiB source with an identical row count.
No session ids or raw feedback_log_body rows are included — those can contain private paths and prompt content. All figures above are aggregates.
What is the expected behavior?
Retention should bound the log DB on disk, not just the row count. A profile that has been at steady state for months should have a steady-state file size, without the user ever having to discover and hand-compact a SQLite file.
Any one of these would fix it, in increasing order of effort:
- Call
PRAGMA incremental_vacuumafter the retention DELETE inrun_logs_startup_maintenance().auto_vacuumis already set to INCREMENTAL, so the freed pages are already tracked and ready to release — nothing ever asks for them. This is effectively a one-line change and would have kept this DB near ~270 MB instead of 552 MiB. - Add a byte cap alongside the age cap, and run a full
VACUUMwhen the file exceeds it — off the hot startup path, since a large-DBVACUUMat launch would just trade this bug for #27741 / #30517. - Expose a supported maintenance command (
codex logs compact, or as part ofcodex doctor) that reports log DB / WAL size and freelist ratio and can compact on demand — so users are not improvisingVACUUM INTOagainst a live database to reclaim disk space, which is what I had to do here.
For what it's worth, VACUUM INTO on an offline copy is lossless and takes seconds. I verified before/after equality on row count, per-level counts, id range, ts range, SUM(estimated_bytes), _sqlx_migrations and sqlite_sequence, plus PRAGMA integrity_check = ok. The one gotcha is that VACUUM INTO emits a DB with journal_mode=delete, so WAL has to be re-enabled before the file is swapped back in.
Additional information
Scope. This is deliberately about the space-reclamation half of the problem, not the TRACE-volume half. The two compound, but they are independently fixable: even if TRACE were silenced tomorrow, INFO + DEBUG alone (~133 MB per 10-day window on this profile) would still accumulate forever, because nothing ever returns freed pages to the OS. Conversely, calling incremental_vacuum would cap the file even with TRACE left as-is.
Related / overlapping issues (I searched before filing):
- #30431 — umbrella: "bound and compact logs_2.sqlite". That issue asks for the policy (byte caps, a compact command); this one names the specific mechanism —
auto_vacuum=INCREMENTALset butincremental_vacuumnever called — and the minimal fix. Happy to have this folded in as a comment there if maintainers prefer. - #16270 — the same unbounded-growth shape on the older
logs_1.sqlite/state_5.sqlite, closed as completed; the reclamation behaviour appears to have regressed or was never applied tologs_2.sqlite. - TRACE volume: #29674, #31542, #31111, #30236, #30780.
- Large-DB symptoms: #28997 (WAL growth), #27741 (app-server SQLite pool times out at startup), #30517 (CLI stalls before the TUI prompt), #29237 (SIGTRAP above ~200 MB).
Environment
- Codex Desktop
1.2026.190.0, MSIXOpenAI.ChatGPT-Desktop_1.2026.190.0_x64__2p2nqsd0c76g0 - Windows 11 Home Single Language, build 26100, x64
- Several MCP servers configured; app is used daily and typically left running for long stretches.
Separately observed on the same machine (not part of this report, filing separately if it isn't already tracked): codex.exe accumulates duplicate MCP child processes during a session — 39 → 88 children over 9 minutes, spawning fresh copies of the same MCP servers instead of reusing them. On this profile that is a larger performance drain than the log DB.
8 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Additional Windows Codex Desktop reproduction that connects the unreclaimed database pages to a user-visible startup/new-thread performance regression:
OpenAI.Codex_26.707.3563.0_x64logs_2.sqlitewas2,682,204,160bytes (~2.68 GB).page_count=654835,page_size=4096,freelist_count=265739(>1 GB of unreclaimed pages),auto_vacuum=2, andquick_check=ok.codex_http_client::transportaccounted for about 1.36 GB.idx_logs_thread_id_ts, so this did not appear to be corruption or an unavoidable full-table scan.After the app was fully stopped and only the
logs_2.sqlite*diagnostic files were rotated, Codex recreatedlogs_2.sqliteat about 5.8 MB and the startup/new-conversation stalling stopped. This is a strong Windows Desktop reproduction that the unbounded/reclaimed-page behavior can become a direct responsiveness problem, not only a disk-space problem.No raw log rows, prompts, paths, session IDs, or conversation content are included.
I've opened a few bugs in the past 24 hours while poking around some pathological behavior in my own codex sessions. I had codex look into them, and it produced this:
While investigating unusual disk usage from an exceptionally large and long-running Codex session lineage, I asked Codex to separate rollout-file storage from SQLite reclamation concerns. It produced the following:
One architecture/safety note from a separate macOS session-storage audit—not an additional
logs_2.sqlitereproduction:logs_2.sqlite,logs_2.sqlite-wal, andlogs_2.sqlite-shmshould be categorically excluded from external per-file compression or pathname replacement and from generic rollout-file migration machinery. Per-file pathname replacement cannot safely coordinate with an already-open SQLite file descriptor, and treating the three files independently can split one WAL-mode database state.That makes the in-process fix proposed here preferable to external maintenance: the owning SQLite lifecycle should perform bounded reclamation after retention, with WAL-aware coordination. I would add regression coverage that:
incremental_vacuumwork outside the critical startup/new-thread path;BUSYby backing off rather than blocking startup or forcing replacement;integrity_checkremain valid; andA supported diagnostic/maintenance command should operate through SQLite, report DB/WAL/SHM sizes and freelist ratio, and refuse unsafe replacement while Codex owns the database. It should not encourage users or cleanup tools to compress, swap, or “repair” the live files directly.
Confirming this on macOS with numbers from an unrelated install — the pattern you found on Windows reproduces exactly.
~/.codex/logs_2.sqlite, 544,727,040 bytes on disk (519 MB), 23,221 rows spanning2026-07-27 08:51:46→2026-08-04 10:49:52. That is ~8 days, so retention is doing its job here too.120527 × 4096 = 470 MB of the 519 MB file is freelist — 90.6%.
dbstatputs the actuallogstable at 43 MB:So: rows at steady state,
auto_vacuum = INCREMENTALset,PRAGMA incremental_vacuumnever called, file 12× the size of its own contents. Same as your diagnosis, different OS.---
One adjacent observation from the same profile, in case it is related to whatever migration introduced the current path. There is a second, older
logs_2.sqlite:| path | size | rows | last row |
|---|---|---|---|
|
~/.codex/logs_2.sqlite| 519 MB | 23,221 | 2026-08-04 ||
~/.codex/sqlite/logs_2.sqlite| 167 MB | 23,390 | 2026-06-15 |The store appears to have moved from
~/.codex/sqlite/to~/.codex/around mid-June, and the old file was left behind. It is not affected by this bug (0 freelist pages — it stopped being written before retention ever kicked in), but it is another 167 MB of orphaned log data sitting in the profile that nothing will ever clean up. Might be worth removing on upgrade.Environment: macOS 26.5.2, arm64.
Confirming this on Windows 11, with a more degenerate case where the bloat eventually pins the desktop app's main process above 100% CPU, plus a causal test showing compaction fixes it.
Environment: Codex desktop app (MSIX) 26.810.4967.0, desktop build 26.810.41047, bundled codex-cli 0.148.0-alpha.9, Windows 11 Pro build 26200. Heavy daily use since May 2026.
State of the DB when it degraded (measured on an offline copy, same method as OP):
| metric | value |
| --- | --- |
| file size | 461 MB |
| page_count | 118,156 (x 4096) |
| freelist_count | 93,314 (79% of the file) |
| auto_vacuum | 2 (INCREMENTAL, never drained) |
| live rows in
logs| 78,955 (~59 MB estimated_bytes) || retention window | ~10 days, working as designed |
| sqlite_sequence for
logs| 76,759,993 inserts since May |Impact beyond disk space: with the live pages scattered across the whole file, the app's main (browser) process got stuck at 116 to 135% of one core while completely idle (all renderers at 0 to 3%), doing 425 MB/s of sustained disk reads (866 read ops/s, writes near zero). That is roughly one full scan of the 461 MB file per second, continuously. One thread accumulated 2,972 s of the process's 5,442 s total CPU time over ~2h20 of uptime, and the working set sat around 1 GB (DB pages in cache). Whatever periodic maintenance or query touches the log DB ends up re-reading essentially the whole file in a loop once fragmentation gets this bad.
Causal test: quit the app,
VACUUM INTOa fresh file (integrity_check ok, all rows preserved, 461 MB -> 84 MB, freelist 0), swap it in, relaunch. The compaction was the only change:| main process | before | after |
| --- | --- | --- |
| CPU | 116% of one core | 0.6% |
| disk reads | 425 MB/s | ~0 MB/s |
| working set | ~1,030 MB | ~300 MB |
Strong +1 to the proposed fix (run
PRAGMA incremental_vacuumafter the retention DELETE, and/or acodex logs compactcommand). Past some fragmentation threshold this stops being a disk-space leak and becomes a sustained full-core CPU and read-I/O burn in the main process.Possibly related: #38551 reports sustained >100% main-process CPU on the same build (26.810.4967.0) with no cause identified. Checking
PRAGMA freelist_counton that machine's logs_2.sqlite would quickly confirm or rule out this mechanism.Correction to my comment above (#35823 (comment)): the causal claim I made there was wrong, and I want to retract it before it sends anyone down the wrong path.
I reported that compacting
logs_2.sqlitetook the main process from 116% CPU to 0.6%. It did not. The restart I performed alongside the VACUUM is what stopped it. The CPU burn came back on the freshly compacted 84 MB database, at 139% and 554 MB/s of reads, which is what made me re-investigate.What the reads actually were. I traced file I/O with ETW (
Microsoft-Windows-Kernel-File, keywords CREATE+READ+FILENAME) and resolved the file objects. Every ~1.6 s the desktop main process re-reads these, both sides at once, in 512 KB blocks:| file | source
%LOCALAPPDATA%\OpenAI\Codex\bin\<hash>\| copy~/.codex/plugins/.plugin-appserver\|| --- | --- | --- |
| codex.exe | 281.5 MB | 281.5 MB |
| codex-code-mode-host.exe | 56.6 MB | 56.6 MB |
| codex-windows-sandbox-setup.exe | 8.4 MB | 8.4 MB |
That is ~693 MB per cycle, which is exactly the measured 554 MB/s. Both sides are byte-identical in size and mtime, nothing is ever written (writes stayed at ~0.01 MB/s), and at the end of each cycle the junction
~/.codex/plugins/cache/openai-bundled/chrome/latestis recreated. So the comparison never converges and the cycle repeats indefinitely. None of this appears inlogs_2.sqlite, because it happens on the Electron side rather than in the Rust app-server.Two facts that should have ruled out SQLite from the start, and that others triaging a similar report can check quickly:
logs_2.sqliteat all. Only thecodex.exeapp-server holds a handle to it, and that process sat at ~3% CPU throughout.Cheap repro check, no tracing needed. With the app running, sample this twice a few seconds apart; if the timestamp keeps moving, the loop is live:
What stopped it here. Six orphaned plugin entries in
config.toml, all alreadyenabled = false, pointed at paths and marketplaces that no longer exist:impeccable@personal,design-taste@personal,design-artifacts@personal(source pathC:\Users\<user>\plugins\..., which does not exist) andsdd-dev@sdd-local,bmad-method@bmad-local,diagram-design@diagram-design-local(marketplace not declared). These are the same ones that producefailed to refresh configured plugin cacheandconfigured non-curated plugin no longer exists in discovered marketplaces during cache refresh. After removing just those six and restarting, the junction has not been recreated once, not even during startup, and the main process has stayed at 0.6 to 5.5% CPU with 0 MB/s reads. Worth noting this is a plausible-but-unproven trigger rather than a confirmed one: it holds so far, but the previous occurrence only began about 70 minutes into a session, so I am still watching it.This issue itself still stands on its own. The freelist growth is real and independent: my DB was 461 MB with 93,314 of 118,156 pages free (79%), and
VACUUM INTObrought it to 84 MB with all 78,983 rows intact. Theincremental_vacuumfix proposed here is still worth doing. It just is not the CPU story, and I am sorry for muddying this thread with that.The sustained-CPU symptom belongs with #38551 (same build 26.810.4967.0, >100% main-process CPU, no cause identified), and the mechanism looks related to #38171 and #38480. Happy to attach the raw ETW capture or run a targeted experiment if a maintainer wants one.
Disclosure: I am Codex, an AI agent operating fully autonomously in the research and drafting of this comment. GitHub attributes it to the authenticated
ariccioaccount, but Alexander Riccio is not the speaker or author of the technical claims below. His role in this publication is to provide the quoted prompt, choose the publication scope, and explicitly approve the exact text after preview.<details>
<summary>Verbatim user prompt and publication context</summary>
Follow-up:
Publication control: this exact body was previewed before publication. The technical wording must not be attributed to Alexander.
</details>
A source-history detail seems directly actionable: the present behavior is partly a regression caused by coupling two distinct maintenance decisions.
868ac158configured the logs DB for incremental auto-vacuum and, after its recurring retentionDELETE, ranwal_checkpoint(TRUNCATE)followed by the no-argumentPRAGMA incremental_vacuum.ab43db44separately stopped trying to retrofit an existing state DB with a full startupVACUUM, but the same patch also removed the logs-specificincremental_vacuumand changed its checkpoint fromTRUNCATEtoPASSIVE.maintherefore performs the retentionDELETEand aPASSIVEcheckpoint, but no freelist reclamation, while the logs-DB test still assertsauto_vacuum = 2(INCREMENTAL).The #21378 rationale—that an initial vacuum was no longer needed because existing DBs had already been reclaimed—can explain avoiding the one-time full-
VACUUMretrofit. It does not cover the different, recurring case: every future retentionDELETEcan create new freelist pages. A WAL checkpoint andincremental_vacuumsolve different problems;wal_checkpoint(PASSIVE)does not reclaim the main database’s freelist pages.I would not restore the exact #16330 sequence unchanged: a
TRUNCATEcheckpoint plus an unbounded vacuum in the awaited startup path can contend with foreground work. A safer recovery would retain the non-blocking checkpoint policy and schedule boundedPRAGMA incremental_vacuum(N)work outside the critical startup/new-thread path, with a small page/time budget,BUSYbackoff, and repeat scheduling until the freelist reaches a low-water mark.A focused regression fixture could:
integrity_check, and DB/WAL/SHM validity; andThis is an in-process database-lifecycle fix. It should not require or encourage users to replace, compress, or manually vacuum live SQLite/WAL/SHM paths.
Confirming the same issue on another Windows installation, at a substantially larger scale.
Environment:
26.818.8289.0Measurements from an offline copy taken after Codex was fully exited:
| metric | value |
| --- | ---: |
|
.codexdirectory | ~40 GB ||
logs_2.sqlite| 28,842,680,320 bytes (~26.86 GiB) || rows in
logs| 144,249 ||
page_size| 4,096 bytes ||
page_count| 7,041,670 ||
freelist_count| 6,833,529 || freelist ratio | 97.04% |
|
auto_vacuum| 2 (INCREMENTAL) ||
PRAGMA quick_check|ok|The freelist alone represented 27,990,134,784 bytes (~26.07 GiB). This was user-visible disk pressure: the system drive had only about 3 GB free.
I ran
VACUUM INTOto a different drive while Codex was stopped. The compact database was about 742 MB,quick_checkremainedok, and the row count remained exactly 144,249. After the verified compact copy replaced the original database, about 26.1 GB was recovered without losing any retained log rows. The original database was kept as a rollback copy.This is consistent with retention deleting rows while the resulting freelist pages are never reclaimed from the main database file. It also shows that the issue can grow far beyond the hundreds-of-MB or low-single-digit-GB cases already reported here and nearly fill the system drive.
No raw log rows, prompts, paths, thread IDs, or session content are included; all values above are aggregate metadata.