Codex Desktop 26.810: repeated compacted image snapshots grow rollout to 16.9 GB and app-server to 19.5 GB
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_imageitems - 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-serverpeak 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?
- Create or continue a Codex Desktop thread that uses repeated image/browser tooling.
- Allow the thread to run long enough to compact many times.
- Fork or continue the thread with full history.
- Observe that compacted
replacement_historyretains the historical inlineinput_imagedata. - Continue until the post-compaction context remains close to the model context limit.
- Quit and reopen Codex Desktop, or reopen the affected thread.
- 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:
- #34863 — compacted records retain repeated inline PNG data and drive app-server to 27 GB
- #28866 — resume can OOM on very large local session JSONL
- #34915 — thread-history pages repeatedly rebuild from large rollout JSONL files
- #34268 — full-history forks duplicate historical compaction snapshots and inline images
- #33493 — retained input images keep post-compaction context near the threshold
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.
2 Comments
The per-record payload math (~20–21 MB inline across 75–83
input_imageitems per compaction) is what jumps out...... that's not unbounded growth, that's the compactor preserving full base64 history inreplacement_historyinstead of externalizing or summarizing it. Combined withThreadHistoryBuildermaterializing 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.Your "does not converge" observation has a precise mechanism on
main@ 1f41cc5d92: 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/systemmessages 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 toRETAINED_MESSAGE_TOKEN_BUDGET = 64_000tokens — but look at how a regular user message is priced:https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/core/src/compact_remote_v2.rs#L557-L565
source_tokens = message_text_token_count(...)— text only. Only client-authored developer messages use the fullestimate_item_token_count. A user message carrying 20 MB of inlineinput_imagedata costs its text length against the 64k budget, so all 75-83 images sail through retention on every compaction. (truncate_message_text_to_token_budgetlikewise 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+ serdecontent_clonematerializing and cloning the whole history on open/continue).Fix outline, ordered by leverage:
estimate_item_token_countinstead ofmessage_text_token_countfor 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.input_imageitems 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.replacement_historyrecords reference rather than duplicate them; this turns rollout growth from O(compactions × images) back to O(unique content).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.