You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
logs_2.sqlite TRACE churn: root cause (payload callsites bypass the sink filter) and minimal fix #31542
There is a large cluster of open reports about ~/.codex/logs_2.sqlite (and its WAL) growing and being written to continuously at TRACE, even with RUST_LOG=warn (#17320, #24275, #28997, #29463, #29674, #30236, #30431, #30780, #31111, #31132, #31142, #31478, and ~15 more). They are all the same defect. This issue is not another repro — it's the root cause and a minimal fix, reproduced against current main (f1affbac5e).
Root cause
Two callsites serialize model payloads into a log event on the hot path, both guarded by enabled!(Level::TRACE):
codex-rs/http-client/src/transport.rs:99 and :131 — the full request body (the entire prompt context, tool outputs and images included) on every model call.
codex-rs/codex-api/src/sse/responses.rs:530 — every streamed SSE event's payload, one event per delta during a turn.
Those enabled!(TRACE) guards were written on the assumption that TRACE is off in production. It isn't: the SQLite log sink installed by both the TUI and the app-server persists TRACE for every target. See codex_state::log_db::default_filter() in codex-rs/state/src/log_db.rs — Targets::new().with_default(LevelFilter::TRACE) with only a handful of targets turned off. So the guard always passes and both payloads get serialized and written.
This is also why RUST_LOG=warn does nothing (#29463, #30236, #30780, #31111): RUST_LOG filters the stderr layer only. The sink has its own filter and ignores it. The level is effectively hardcoded (#31132).
Impact, measured
On a real ~/.codex:
87% of retained log content (33 of 38 MiB) came from the transport target alone, with individual rows up to 4 MiB.
The sink enforces a 10 MiB per-partition retention budget. A single 4 MiB request-body row eats most of that budget in one write and evicts everything else — so this defect also degrades what /feedback can attach, on top of the disk churn.
Separate but adjacent: logs_2.sqlite is opened with auto_vacuum=INCREMENTAL (codex-rs/state/src/runtime.rs), but nothing ever runs PRAGMA incremental_vacuum. Pages freed by retention and per-partition pruning stay on the freelist and the file never shrinks below its high-water mark. My logs_2.sqlite was 72 MB with 42% of its pages (30 MB) on the freelist. This is the mechanism behind #28997 and #30431.
Proposed fix
Both changes are small and follow the exact pattern of prior noise-filtering fixes (#26396, #29457, #29599):
Cap codex_http_client::transport and codex_api::sse at DEBUG in log_db::default_filter(). Because no installed sink then enables those targets at TRACE, the enabled!(TRACE) callsites are disabled outright — the multi-MB serialization is skipped, not just the write. RUST_LOG=trace on stderr still works for local debugging.
Run a capped PRAGMA incremental_vacuum(2048) (8 MiB at the default page size) from logs startup maintenance, between the retention delete and the passive WAL checkpoint, so oversized files converge over a few restarts without a large write burst.
I've implemented and tested both (just test -p codex-state, 153 passed):
Summary
There is a large cluster of open reports about
~/.codex/logs_2.sqlite(and its WAL) growing and being written to continuously at TRACE, even withRUST_LOG=warn(#17320, #24275, #28997, #29463, #29674, #30236, #30431, #30780, #31111, #31132, #31142, #31478, and ~15 more). They are all the same defect. This issue is not another repro — it's the root cause and a minimal fix, reproduced against currentmain(f1affbac5e).Root cause
Two callsites serialize model payloads into a log event on the hot path, both guarded by
enabled!(Level::TRACE):codex-rs/http-client/src/transport.rs:99and:131— the full request body (the entire prompt context, tool outputs and images included) on every model call.codex-rs/codex-api/src/sse/responses.rs:530— every streamed SSE event's payload, one event per delta during a turn.Those
enabled!(TRACE)guards were written on the assumption that TRACE is off in production. It isn't: the SQLite log sink installed by both the TUI and the app-server persists TRACE for every target. Seecodex_state::log_db::default_filter()incodex-rs/state/src/log_db.rs—Targets::new().with_default(LevelFilter::TRACE)with only a handful of targets turned off. So the guard always passes and both payloads get serialized and written.This is also why
RUST_LOG=warndoes nothing (#29463, #30236, #30780, #31111):RUST_LOGfilters the stderr layer only. The sink has its own filter and ignores it. The level is effectively hardcoded (#31132).Impact, measured
On a real
~/.codex:/feedbackcan attach, on top of the disk churn.Separate but adjacent:
logs_2.sqliteis opened withauto_vacuum=INCREMENTAL(codex-rs/state/src/runtime.rs), but nothing ever runsPRAGMA incremental_vacuum. Pages freed by retention and per-partition pruning stay on the freelist and the file never shrinks below its high-water mark. Mylogs_2.sqlitewas 72 MB with 42% of its pages (30 MB) on the freelist. This is the mechanism behind #28997 and #30431.Proposed fix
Both changes are small and follow the exact pattern of prior noise-filtering fixes (#26396, #29457, #29599):
codex_http_client::transportandcodex_api::sseat DEBUG inlog_db::default_filter(). Because no installed sink then enables those targets at TRACE, theenabled!(TRACE)callsites are disabled outright — the multi-MB serialization is skipped, not just the write.RUST_LOG=traceon stderr still works for local debugging.PRAGMA incremental_vacuum(2048)(8 MiB at the default page size) from logs startup maintenance, between the retention delete and the passive WAL checkpoint, so oversized files converge over a few restarts without a large write burst.I've implemented and tested both (
just test -p codex-state, 153 passed):Per the contribution guidelines I'm not opening a PR unsolicited — happy to if the team wants either or both.