Skip to content

macOS: compaction checkpoints re-embed screenshots until rollout hits 3.9 GB; ingestion then uses 10–20 GB RSS, causing jetsam kills and a kernel watchdog panic on a 16 GB Mac #31040

Open

Description

@chyewon

What version of the Codex App are you using?

  • Codex Desktop (macOS): 26.623.81905 (build 4598)
  • codex-cli: 0.142.5
  • macOS 26.5.1 (25F80), MacBook Air (Mac16,12, Apple Silicon, 16 GB RAM)

What platform is your computer?

macOS (Apple Silicon).

What issue are you seeing?

The root cause appears to be the same as #23257 (compaction checkpoints repeatedly re-embed full inline image payloads), but on macOS the consequence escalates beyond bloated files to system-wide crashes: jetsam kills of system daemons and eventually a kernel watchdog panic.

1. Rollout files snowball on disk

One long browser-automation session produced a single rollout JSONL of 3.9 GB:

  • 29,406 records total; 98 compacted records account for 3.99 GB of the file
  • compacted snapshot sizes grow monotonically: 0.1 MB → 3.4 MB → … → 53.4 MB each by the end of the session
  • A sampled 42.8 MB compacted record contains 81 data:image base64 payloads (the same screenshots re-embedded on every compaction)
  • Other sessions from the same week: 973 MB, 544 MB, 394 MB, 254 MB, 231 MB; ~/.codex totaled 10 GB (logs_2.sqlite alone 721 MB)

2. Session ingestion then uses 10–20 GB RSS

macOS resource-exceedance reports (Microstackshots) for the codex process show repeated memory explosions from a ~100 MB baseline while a sqlx/SQLite thread spins in sqlite3_step / sqlite3_wal_checkpoint_v2 / pwrite:

  • Footprint: 98.66 MB -> 6167.55 MB (+6068.89 MB) (max 12.19 GB) within a 7.5-minute sample window
  • Footprint: 104.02 MB -> 6774.80 MB (+6670.78 MB) (max 11.81 GB) (different pid, same day)
  • Five separate "disk writes" resource-exceedance reports over 3 days with the same stack signature (~10.5 MB written per sample step)

3. System impact on a 16 GB Mac

  • A JetsamEvent-*.ips report names largestProcess: codex at 15.5 GB RSS (lifetimeMax 20.5 GB) while macOS jettisoned dozens of system daemons
  • The following night: kernel panic — watchdog timeout: no checkins from watchdogd in 90 seconds. The panic snapshot shows codex at 10.7 GB RSS, compressor 12.2 GB, 0.01 GB free
  • Collateral during the week: a WindowServer userspace watchdog timeout (forced session restart) and bursts of Chrome renderer crashes (abort traps) under memory pressure

What steps can reproduce the bug?

  1. On a 16 GB Mac, run a long Codex session that uses browser/screenshot tools heavily, long enough to cross many automatic compactions.
  2. Inspect the session rollout JSONL: each new compacted record re-embeds prior screenshots as inline base64, so checkpoints grow roughly linearly and the file quadratically.
  3. Leave the desktop app running: when codex (re)ingests the rollout into SQLite, RSS climbs to 10–20 GB → jetsam kills → possible watchdog kernel panic overnight.

What is the expected behavior?

Related: #23257 (compaction re-embeds images, Windows), #24676 (desktop hangs opening image-heavy rollout), #24550.

Happy to attach sanitized excerpts of the resource reports, jetsam event, or panic log on request.

Activity

  1. added
    bugSomething isn't working
    appIssues related to the Codex desktop app
    contextIssues related to context management (including compaction)
    sessionIssues involving session (thread) management, resuming, forking, naming, archiving
    on Jul 3, 2026
  2. github-actions commented on Jul 3, 2026

    @github-actions
    Contributor
  3. chyewon commented on Jul 3, 2026

    @chyewon
    Author

    Follow-up: I root-caused this and have a validated patch on a fork branch. Since external PRs are invitation-only per CONTRIBUTING.md, posting the results here — happy to submit it as a PR if the team wants to invite it, or feel free to lift the branch directly.

    Branch: chyewon/codex:fix/rollout-memory-compacted-image-bloat (single commit, codex-rs/rollout only; cargo test -p codex-rollout green, clippy clean)

    Root cause (confirmed in source + live instrumentation)

    1. Disk snowball: compaction persists compacted records whose replacement_history re-embeds all prior inline screenshots (base64 data: URLs inside kept user messages), so every checkpoint re-persists every historical screenshot — 98 checkpoints growing 0.1→53 MB each in my 3.9 GB rollout (mechanism of Desktop compaction repeatedly embeds full image base64 in compacted checkpoints #23257 / Codex session logs grow to 700MB-2GB from repeated compaction history and raw tool output #24948).
    2. Memory: rollout loading is eager (load_rollout_items collects everything into a Vec), and state-DB backfill visits every session file through it (extract_metadata_from_rollout). Measured on a real 973 MB production rollout: backfill peaks at ~1.0–1.6 GB; thread/resume via app-server peaks at 4.8 GB (~5× file size — scaled to the 3.9 GB rollout that predicts ~19.6 GB, matching the jetsam/panic forensics above).

    The patch

    1. Redacts inline data: images >16 KB from persisted compacted checkpoints (schema-valid 1×1 PNG placeholder; in-memory history untouched; small/remote images and non-compacted records unchanged) — stops the snowball at the source.
    2. Streams backfill metadata extraction via a new for_each_rollout_item visitor — backfill memory becomes O(largest line) instead of O(file).
    3. Deserializes rollout lines from a borrow instead of from_value(v.clone()) — removes a per-line full-DOM copy (transient-only win).

    Measured (same 973 MB production rollout)

    Metric Before After
    Backfill (state_db::init) peak RSS, same in-process harness 1,604 MB 701 MB (bounded by line size, not file size)
    Rollout size with checkpoint-image redaction 973 MB 130 MB (−87%)
    thread/resume peak RSS on the redacted file 4,825 MB 952 MB (−80%)

    End-to-end checks on the redacted file: thread/resume returns the same thread id with zero parse errors, and a live model turn on the resumed session completed normally (provider accepted placeholder data URLs; the model replied).

    Includes a regression test (recorder_redacts_large_inline_images_in_persisted_compacted_history) covering raw persisted bytes, placeholder round-trip through load_rollout_items, and non-compacted records passing through unchanged.


    Update (same day): after an adversarial completeness review, the branch gained a second commit (802e92a) extending the persist-time sanitizer beyond message InputImage content to the other image-bearing shapes in replacement_history: FunctionCallOutput content items (FunctionCallOutputContentItem::InputImage) and ImageGenerationCall.result (bare base64), with call_id pairing preserved. The regression test now also asserts the resume entry point (get_rollout_history) consumes a redacted rollout cleanly. cargo test -p codex-rollout green, clippy clean. Known remaining limitation (intentionally out of scope): the resume path still eagerly materializes the full item Vec; with redaction in place files stop reaching GBs, but streaming resume reconstruction would be a worthwhile follow-up.

  4. loremcdmx commented on Jul 26, 2026

    @loremcdmx

    Confirming this on macOS, at roughly 10× the scale reported in the opening post. Same mechanism, same log signatures.

    Environment

    • Codex Desktop: ChatGPT.app 26.721.41059, Codex Framework 150.0.7871.128
    • macOS 26 (Darwin 27.0.0), Apple Silicon, 18 cores / 48 GB RAM
    • Observed 2026-07-26

    Scale

    Metric This issue Mine
    Largest single rollout 3.9 GB 45.14 GB (48,465,379,354 bytes)
    ~/.codex total 10 GB 463 GB
    ~/.codex/sessions — 434 GB (433 GB from July alone)
    Rollout files > 100 MB — 313
    logs_2.sqlite 721 MB 1.5 GB + 192 MB un-checkpointed WAL

    state_5.sqlite was an additional 97 MB with a 4 MB WAL. The disk hit 94% full.

    Evidence that compaction re-embedding is the driver

    After stripping image payloads, the 45 GB rollout is 79,899 lines. The 52,004 base64 PNGs it contained were packed into just 1,018 lines — about 51 images per line. That density is only explicable by snapshots re-embedding the same screenshots, exactly as described here.

    Record breakdown of that file:

    response_item   48,275     compacted       617
    event_msg       28,724     turn_context    764
                               session_meta     52
    

    Head sample of the first 3,000 lines, by payload subtype:

    function_call_output   744 records   34,393,039 bytes   (89% of sampled volume)
    message                 48 records    2,544,401 bytes
    reasoning              452 records      963,614 bytes
    

    Largest single JSON line: 2,274,751 bytes, a function_call_output whose output[0] is {"type":"input_image","image_url":"data:image/png;base64,..."}.

    Across the whole corpus, images were 1.6% of lines but 87.3% of bytes.

    User-visible impact beyond disk

    This is not only a storage problem — it degrades the app:

    [AppServerConnection] response_routed method=thread/turns/list durationMs=40139 queueWaitMs=8735
    [AppServerConnection] response_routed method=thread/turns/list durationMs=56454 queueWaitMs=15548
    

    Opening the affected thread took 40–56 s. The app-server request queue sat pinned at inFlightRequestCount=6 with background requests expiring (reason=queue-expired), and turn/start timed out at 30 s, surfacing as the "Error submitting message" banner (see #27395).

    Result of stripping images

    Replacing every data:image/png;base64,… payload with a 1×1 transparent PNG across 754 rollout files:

    • 534,529 images removed
    • 409.4 GB reclaimed
    • Individual files shrink 97–98% (45.14 GB → 0.63 GB for the worst one)
    • 0 errors; verified line counts unchanged, all lines still valid JSON, and every non-image line byte-identical

    That ratio suggests the actual conversational content of even a very heavy multi-month workload is on the order of a few GB — the rest is duplicated pixels.

    Suggested fix

    Store each image once in a content-addressed blob directory and reference it by hash in the rollout, rather than inlining base64. Failing that, excluding image payloads from compaction snapshots alone would remove the large majority of the growth, since the re-embedding is what turns linear growth into quadratic.

    A checkpoint of logs_2.sqlite/state_5.sqlite WALs on shutdown would also help; mine had 192 MB that never got merged.

    Workaround for anyone hitting this now

    Rewriting each rollout line and substituting the base64 payload with a valid 1×1 transparent PNG keeps the JSON structure intact and the app reads the files normally. Non-image lines can be passed through byte-for-byte. Important: skip any file currently open by the app (lsof -c codex -c ChatGPT -c Codex) — replacing an open file leaves the descriptor pointing at the old inode, so the space is never released.

    Related: #32128, #23257, #25815, #29531, #20864.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    appIssues related to the Codex desktop appbrowserbugSomething isn't workingcontextIssues related to context management (including compaction)performancesessionIssues involving session (thread) management, resuming, forking, naming, archiving

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions