logs_2.sqlite TRACE churn: root cause (payload callsites bypass the sink filter) and minimal fix

Open 💬 1 comment Opened Jul 8, 2026 by Gloridust

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 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.rsTargets::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.
  • Independent samples in #31111 show 25–41 rows/sec written during normal interactive use, 97% of them TRACE.

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):

  1. 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.
  2. 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):

Per the contribution guidelines I'm not opening a PR unsolicited — happy to if the team wants either or both.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗