Codex CLI and Desktop session rollouts can silently consume tens to hundreds of GiB

Open
#34,337 11 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
25/100
Issue type
Bug
Clarity
Needs clarification
Activity status
Active
Tech stack
rust

Research direction

Start with compression.rs and compression_tests.rs, then inspect the local_thread_store_compression activation path and the codex-rollout, codex-thread-store, and codex-features tests. The immediate proposed scope is a codec-only change, while the issue also describes activation, coordination, retention, and storage-budget work; done requires an explicitly agreed scope and passing the relevant regression tests.

Written by the indexing model from the issue text.

Description

CLI enhancement session
What issue are you seeing?

Hi Codex team — Codex CLI and Codex Desktop share the same local rollout/session storage, and that store can grow silently from tens of GiB to hundreds of GiB—or even TiB-scale—during otherwise normal long-running use.

This is larger than one zstd setting and larger than the CLI alone. Long-lived conversations, compaction, subagent fan-out, images, and large tool outputs all write into the same persistence model under ~/.codex/sessions. Users often discover the problem only when the disk is nearly full, Desktop becomes slow to start, a thread causes sustained CPU work, or they must choose between deleting resumable history and moving it to another drive.

My shared CLI/Desktop store reached 75,412,910,173 bytes (70.23 GiB) across 644 rollouts after roughly one to two months of heavy use. This is not an isolated outlier:

Surface / report Observed local session storage Dominant shape
My CLI/Desktop store 70.23 GiB repeated compacted history, inline images, parent/child history
Current Desktop 26.721.41059, bundled CLI 0.146.0-alpha.3.1 (report) 122.2 GiB an active 111.9 MB rollout was dominated by raw function/tool output
Desktop / CLI 0.145.0-alpha.27 (report) 135 GB subagent fan-out; 57 files over 1 GB
CLI 0.144.6 (#34061) ~755 GiB 2,393 children from one parent accounted for ~731.5 GiB
CLI on WSL, 0.144.x (report) 34.9 GB largest rollout 2.36 GB; valid JSONL
Another user's store (report) 1.5 TB moved to a slow external drive because deletion felt unsafe

These are independent, privacy-safe measurements from real users, not an extrapolation from one fixture.

Why CLI and Desktop should be treated together

Desktop embeds Codex and persists the same rollout representation used for resume, fork, compaction, and thread listing. The visible symptoms differ by surface, but the oversized local history is shared infrastructure:

  • CLI: rapid disk growth during long-lived or subagent-heavy work; deleting files sacrifices resumable history.
  • Desktop: the same disk growth plus startup/reopen pressure. In #33786, an oversized legacy rollout is repeatedly replayed by Desktop and produces sustained CPU load. That replay loop is a separate code path, but oversized persisted history makes its impact much worse.
  • Both: no clear storage budget, retention policy, warning, or currently effective automatic compression before the store becomes operationally dangerous.
What is actually growing?

Local inspection and independent reports show at least three mechanisms. A complete fix should not assume there is only one:

  1. Compaction copies prior content into new snapshots. compacted.payload.replacement_history may repeatedly contain the same text, images, or other large content. #33493 shows the related active-context failure mode: inline images survive compaction, leave almost no context headroom, and are copied again.
  2. Parent history is physically inherited by subagents. In one local child, 99.874% of its large-string bytes already existed in its parent. An independent reproduction in #34061 found 117,949,758 of 118,285,692 child bytes (99.72%) before the child's first real turn.
  3. Ordinary tool output can be large without compaction dominating. On current Desktop 26.721.41059, one active rollout contained 80.6 MiB of response_item/function_call_output across 440 records. Compression alone helps, but record-size policy and artifact references are also needed.

Exact-line deduplication is not sufficient for the first case because timestamps and enclosing JSON differ. In a representative 760,247,640-byte rollout, exact-line dedup would save only about 1.10 MiB, while repeated strings of at least 256 bytes accounted for 651.49 MiB.

Expected behavior
  • Session storage should remain bounded enough for ordinary developer machines.
  • Users should receive a warning before Codex threatens available disk space.
  • Resume, fork, search, transcript viewing, and debugging should remain available without keeping every repeated byte inline.
  • Compression and cleanup should run safely in the background without adding meaningful turn latency or racing an active writer.
  • CLI and Desktop should behave consistently because they consume the same stored representation.
Actual behavior
  • A few weeks or months of heavy use can consume tens to hundreds of GiB without a warning.
  • Compaction, parent/child inheritance, images, and tool outputs can duplicate large payloads.
  • Manual cleanup is risky because users cannot easily tell which history is still needed or being written.
  • Large histories can amplify Desktop startup/replay CPU problems in addition to consuming disk.
Existing work and the remaining product gap

Thank you for the foundation in #25087 and #25089: transparent .jsonl.zst reads, materialize-before-append behavior, and the cold-compression worker are the right representation and compatibility direction.

On current main, however, local_thread_store_compression is still under development and default-off, eligible rollouts wait seven days, and the worker uses zstd level 3 with its default match window. Consequently, normal users do not yet receive automatic relief, and when the worker is exercised the default window misses distant copies common in compacted and inherited histories.

On one real 760,247,640-byte rollout:

Encoding Compressed bytes Ratio
Current level-3/default-window worker path 463,794,252 1.64x
Bounded 32 MiB long-distance worker path 34,585,710 21.982x

Both outputs were fully decompressed and SHA-256 checked byte-for-byte. A second 576,966,356-byte fixture became 30,433,952 bytes (18.958x).

Proposed staged solution

I suggest a staged path so each risk and rollback boundary remains reviewable.

  1. Improve the codec without changing the schema. For rollouts at least 8 MiB, set the pledged source size, enable long-distance matching, select ceil(log2(source_size)) clamped to an 8–32 MiB window, and write a frame checksum. The focused patch is 55 inserted lines across compression.rs and compression_tests.rs. It changes neither JSONL semantics nor read/resume/materialization behavior.
  2. Make automatic activation safe. Writer/compressor coordination, recurring policy, durable worker ownership, crash recovery, representation conflict handling, archive/unarchive coordination, and materialization validation must precede default activation. I implemented these as eight dependency-ordered local commits rather than hiding them inside the codec change.
  3. Reduce bytes before they are written. Use immutable history/content references for parent-child sharing; content-addressed artifacts for images and large tool outputs; explicit per-record and per-store budgets; lifecycle/GC/repair rules; and a user-visible storage warning. This is higher-upside but requires an agreed compatibility and ownership model, so it should not be bundled into the low-risk codec PR.

The codec is a strong first layer, not a claim that compression alone solves every growth source. It reaches more than 10x on the affected files only when the worker processes them; activation is what turns that into a default product improvement.

Requested maintainer decision

Could this be triaged as a shared CLI + Desktop session-storage/performance issue rather than a CLI-only enhancement, and routed to the owner of rollout persistence?

If the staged direction aligns, I would appreciate an explicit invitation for the codec-only PR first. I can keep that contribution narrow, own CI and review changes, and follow through on release validation. The broader safety/activation work can then be reviewed in the order the team prefers.

Thanks for taking a look—and for the existing rollout-compression work. The goal here is to preserve debuggable, resumable history while making its storage cost predictable enough that users do not have to manage Codex like an unbounded database.

Extended validation / reviewer reference
Representative-file structure

In the 760,247,640-byte sample:

  • compacted records accounted for 652.06 MiB / 89.936%.
  • payload.replacement_history[].content[].image_url accounted for 632.39 MiB.
  • repeated strings at least 256 bytes long accounted for 651.49 MiB.
  • one 977,978-byte image value appeared 138 times.
  • exact-line deduplication would save only about 1.10 MiB.

The raw rollout is not attached because it contains private prompts, tool outputs, paths, and images. Only sizes, counts, field paths, timings, and hashes are reported.

Why cap the long-distance window at 32 MiB?

A 128 MiB window saved only another 4.12 MiB on the large sample while raising measured compression memory to about 141.9 MiB. The 32 MiB cap retained most of the storage benefit. On the second fixture, the proposed worker measured about 39.7 MiB peak RSS.

Whole-store and foreground-path validation
  • An APFS shadow of the complete store was used; the live sessions tree was never modified.
  • 79.613474 GiB of logical JSONL became 7.849757 GiB physically (10.142x).
  • 660/660 logical rollouts remained byte-exact.
  • Initial backlog processing completed in 24.399 seconds. This was the whole-store background pass, not per-turn latency.
  • During a separate 22.534-second worker pass, 273 foreground thread/list(limit=1) calls measured p50/p95/p99 of 23.871/26.386/27.969 ms, versus 24.258/26.007/26.381 ms after completion. No meaningful foreground regression was observed in this sample.
  • A no-op steady-state scan took 0.5 seconds, made no rollout writes, and peaked at 3.172 MiB RSS.
Safety/activation work already exercised locally

The broader prototype is split into eight commits across 17 files (+1,691/-200):

  1. hold a shared representation lock for the append-handle lifetime and an exclusive lock during representation replacement;
  2. replace the one-shot seven-day policy with startup plus 30-minute recurrence, an 8 MiB size floor, and a 30-minute cold age;
  3. keep one locked worker-marker handle through the scan and persist terminal state in place;
  4. make representation installation/retirement directory-durable on Unix/macOS and fail closed on divergent plain/zstd siblings;
  5. validate the exact materialized destination before retiring zstd, including the install-to-retirement race;
  6. coordinate archive/unarchive with the same representation lock and re-resolve after inode replacement;
  7. enable the feature in a separately revertible commit with legacy and paginated public-lifecycle regressions;
  8. include the bounded long-distance codec as an independent unit.

Before required fix/format, the combined branch passed codex-rollout 117/117, codex-thread-store 117/117, codex-features 29/29, and focused legacy/paginated app-server lifecycle tests.

Compatibility and recovery boundaries
  • The codec output is a standard zstd frame; there is no JSONL schema or path-semantics change.
  • Existing transparent read/search/list/materialize behavior remains unchanged.
  • Digest-verified macOS arm64 stable binaries 0.137.0, 0.141.0, 0.144.2, and 0.144.6 read the tested compressed representation and materialized it before resume. Versions 0.135.0 and 0.136.0 did not, so future default activation needs an explicit downgrade-support decision.
  • A 3.544552 GiB cold rollout was materialized through the actual RolloutRecorder::resume append path in 2.598 seconds at 37.438 MiB peak RSS, with byte-exact recovery.
  • The codec-only patch does not claim to solve the writer/compressor race. Deterministic race tests and a representation-lock design exist separately.
  • Structural deduplication requires explicit ownership, delete/GC, repair, mixed-version, downgrade, and privacy semantics. Local shadow-reader work therefore retains inline history as a safety oracle and claims no production savings yet.
Focused codec validation
  • just test -p codex-rollout
  • just fix -p codex-rollout
  • just fmt
  • git diff --check
  • deterministic long-distance regression with byte-exact decode
  • two real-file frame/decode/SHA-256 checks
  • compressed thread/read and resume/materialization checks
  • complete shadow-store logical-tree comparison

Prior measurements and source trace: initial analysis, race/activation follow-up, and review-boundary summary.

Dominant language
Rust
Stars
125k
Forks
19.5k
Avg merge
1m
Merged PRs (30d)
1k

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from openai/codex

All issues in openai/codex

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.