Skip to content

Codex Desktop 26.810: repeated compacted image snapshots grow rollout to 16.9 GB and app-server to 19.5 GB #39013

Open

Description

@zoeytabmedia

What version of the Codex App are you using (From “About Codex” dialog)?

ChatGPT for macOS 26.810.52044 (build 6662); bundled codex-cli 0.148.0-alpha.9

What subscription do you have?

ChatGPT subscription (exact tier omitted for privacy)

What platform is your computer?

Darwin 27.0.0 arm64 arm

What issue are you seeing?

Summary

A long-running Codex Desktop thread produced a 16.94 GB rollout JSONL and caused the bundled codex app-server to peak at approximately 19.5 GB physical footprint. System swap reached 28.6 GB used on a Mac with 32 GB RAM, and the app crashed under memory pressure.

This is still reproducible on ChatGPT for macOS 26.810.52044 with bundled codex-cli 0.148.0-alpha.9.

Content-safe local evidence

The affected rollout is a user thread fork using multi-agent v2. No raw prompts, paths, screenshots, session identifiers, or private project content are included here.

  • Rollout size: 16,942,114,729 bytes
  • Compaction windows recorded: 798
  • Recent compacted.payload.replacement_history: approximately 20–21 MB per record
  • Images retained per recent compacted snapshot: 75–83 input_image items
  • Inline image data per snapshot: approximately 20.5 MB
  • More than 98% of the sampled replacement-history bytes were inline image data
  • Context remained near the limit: approximately 233k input tokens out of a 258.4k context window
  • codex app-server peak footprint: approximately 19.5 GB
  • System swap: 28.6 / 29.7 GB used
  • Sustained app-server CPU during history processing: approximately 111%

Two file descriptors for the same oversized rollout were open in app-server: one read descriptor and one write descriptor.

A symbolized macOS process sample showed the active path passing through:

tokio::fs::file::File::poll_read
serde_json::read::StrRead::parse_str
serde::private::de::content::content_clone
codex_app_server::thread_state::ThreadState::track_current_turn_event
codex_app_server_protocol::protocol::thread_history::ThreadHistoryBuilder::handle_event

Allocator inspection showed a large transient allocation peak followed by substantial empty/fragmented small-allocation arenas. This looks like unbounded history materialization and cloning plus allocator fragmentation, rather than a small conventional object-retention leak.

Why this appears pathological

Each compaction persists another full replacement-history snapshot containing dozens of historical inline images. Because the post-compaction context remains close to the context limit, the thread compacts repeatedly without converging to a small checkpoint. Opening or continuing the thread then makes app-server parse and clone a multi-gigabyte append-only history.

What steps can reproduce the bug?

  1. Create or continue a Codex Desktop thread that uses repeated image/browser tooling.
  2. Allow the thread to run long enough to compact many times.
  3. Fork or continue the thread with full history.
  4. Observe that compacted replacement_history retains the historical inline input_image data.
  5. Continue until the post-compaction context remains close to the model context limit.
  6. Quit and reopen Codex Desktop, or reopen the affected thread.
  7. Observe app-server reading the oversized rollout, high CPU, a large transient heap, heavy swap growth, and eventual memory-pressure failure.

The private session identifier and local file path are intentionally omitted. I can provide additional sanitized aggregate measurements if useful.

What is the expected behavior?

  • Compaction should remove, externalize, summarize, or deduplicate historical inline image bytes.
  • Superseded compacted checkpoints should not remain on the active hydration path.
  • Resume and thread-history pagination should stream or use a persisted index instead of fully materializing and cloning the rollout.
  • Codex should enforce per-record, per-thread, and retained-media limits.
  • One oversized thread should fail safely with an actionable warning instead of destabilizing the app and operating system.

Additional information

This appears closely related to:

The reason for filing this separately is that the failure persists on the newer 26.810 / codex-cli 0.148 build, and this report includes a symbolized deserialization/history-builder stack plus a measured 798-compaction case.

Privacy note: the original rollout contains private prompts, local paths, tool output, and screenshots. It will not be uploaded publicly. All measurements above were collected using bounded, content-safe sampling.

Activity

  1. added
    appIssues related to the Codex desktop app
    on Aug 17, 2026
  2. MilkyWay008 commented on Aug 17, 2026

    @MilkyWay008

    The per-record payload math (~20–21 MB inline across 75–83 input_image items per compaction) is what jumps out...... that's not unbounded growth, that's the compactor preserving full base64 history in replacement_history instead of externalizing or summarizing it. Combined with ThreadHistoryBuilder materializing the whole rollout instead of streaming from the JSONL offset, every compaction re-approaches the same ceiling. Until the server-side fix lands, the only user-side mitigation is avoid resuming threads that have crossed several hundred compactions; closing and starting fresh is the safe path. Worth +1'ing #34863 / #33493 since it's the same retention pattern.

  3. jdcodes1 commented on Aug 17, 2026

    @jdcodes1

    Your "does not converge" observation has a precise mechanism on main @ 1f41cc5: inline images cost zero against the compaction retention budget, but full price against the model's context — that asymmetry produces both the 16.9 GB rollout and the never-converging compaction loop.

    1. Retention keeps every user message, and the budget only counts their text. Remote compaction v2 retains all user/developer/system messages verbatim (is_retained_for_remote_compaction_v2, compact_remote_v2.rs#L512-L527 — only agent messages get a token cap). The retained set is then trimmed to RETAINED_MESSAGE_TOKEN_BUDGET = 64_000 tokens — but look at how a regular user message is priced:

    let notice_tokens = group
    .attached_notice
    .as_ref()
    .map_or(0, |notice| message_text_token_count(&notice.item).max(1));
    let source_tokens = if client_developer {
    usize::try_from(estimate_item_token_count(&group.source.item)).unwrap_or(usize::MAX)
    } else {
    message_text_token_count(&group.source.item).max(1)
    };

    source_tokens = message_text_token_count(...) — text only. Only client-authored developer messages use the full estimate_item_token_count. A user message carrying 20 MB of inline input_image data costs its text length against the 64k budget, so all 75-83 images sail through retention on every compaction. (truncate_message_text_to_token_budget likewise truncates text and passes images through untouched.)

    2. But the model-side accounting counts those images at full price. The context manager estimates each image at ~7,373 bytes ≈ 1,844 tokens (RESIZED_IMAGE_BYTES_ESTIMATE, context_manager/history.rs#L618-L628). Your ~80 retained images ≈ ~150k tokens of permanent context floor in a 258.4k window — matching your measured "233k of 258.4k immediately after compaction". So each compaction frees almost nothing, the threshold re-trips quickly, and the loop repeats: your 798 compaction windows.

    3. Each iteration persists the whole snapshot. Every compaction writes a complete replacement_history (with all retained inline images) as a new append-only rollout record (compact_remote_v2.rs#L317-L323). 798 windows × ~21 MB ≈ your 16.9 GB — the growth is O(compactions × retained-image bytes), i.e. quadratic-ish in session length once the loop starts. The 19.5 GB app-server footprint then follows from the read path in your symbolized stack (ThreadHistoryBuilder::handle_event + serde content_clone materializing and cloning the whole history on open/continue).

    Fix outline, ordered by leverage:

    1. Price images in the retention budget with the same estimator the context manager already uses (estimate_item_token_count instead of message_text_token_count for user messages, or at minimum add the per-image constant). This alone breaks the non-convergence: old images fall out of the retained set, the post-compaction context actually shrinks, and the loop stops.
    2. Cap retained images structurally — keep the most recent N input_image items and replace older ones with a text placeholder ("[image from earlier in the conversation, dropped during compaction]"), mirroring what already happens to oversized agent messages.
    3. Stop re-embedding bytes per snapshot — content-address inline images (hash → stored once) so replacement_history records reference rather than duplicate them; this turns rollout growth from O(compactions × images) back to O(unique content).
    4. Reader side: the open-thread path should stream rather than materialize+clone — but with (1)-(3) the multi-GB input stops existing, which is the cheaper win.

    Test shape: a thread with a handful of large synthetic images + forced repeated compaction; assert retained-set token cost respects the budget with images priced in, post-compaction context drops below the trigger threshold, and rollout growth per compaction is bounded by new content, not history size.

  4. IlIIlIIIlIII commented on Sep 4, 2026

    @IlIIlIIIlIII

    Still reproducible on ChatGPT/Codex Desktop 26.901.20858 (build 7658) with bundled Codex CLI 0.153.0-alpha.5.

    Environment:

    • macOS 26.6.2 (25G83)
    • MacBook Pro Mac15,11, M3 Max
    • 36 GB unified memory

    Additional content-safe evidence:

    • After long multi-agent workloads, the main codex app-server process reached approximately 20 GB physical footprint, including about 16 GB compressed.
    • The Codex app group had 112 processes, and system swap reached approximately 21 GB.
    • Quitting and reopening the app reduced the main app-server process to approximately 363 MB and immediately normalized memory pressure, while displays and other applications remained running.
    • Within about five minutes of relaunch, app-server grew from 363 MB to approximately 1.4 GB while active tasks were reloaded.
    • vmmap showed growth mainly in MALLOC_SMALL and MALLOC_LARGE; mapped files remained around 34.5 MB.
    • 234 subagent rollout files were created in one day, totaling approximately 26.95 GiB.
    • Several individual rollout JSONL files reached 1.5–2.1 GB and contained hundreds of embedded PNG/JPEG payloads. The largest individual JSONL record observed was approximately 92 MB.

    This supports the same inline-image/session-history retention and hydration pattern described here. No raw rollout files, prompts, local paths, screenshots, or session identifiers are being shared publicly.

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 app

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions