Codex Desktop: opening a large thread causes sustained 100%+ CPU until app restart

Open 💬 9 comments Opened May 30, 2026 by ZhangChaoquan
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Summary

In Codex Desktop, opening a large existing conversation/thread can cause the Codex main process to stay at 100%+ CPU. Switching away to another conversation does not recover CPU usage; only fully restarting Codex clears it.

Version

  • Codex Desktop version: 26.527.31326 (3390)
  • Platform: macOS

Reproduction

  1. Start Codex Desktop.
  2. Open a new/empty conversation: CPU remains normal.
  3. Open an existing large conversation/thread.
  4. Observe Codex main process CPU spikes to roughly 100%-200%.
  5. Switch to another conversation, including smaller conversations.
  6. CPU remains high until Codex Desktop is fully restarted.

Observed Details

The problematic thread file is around 501 MB and contains many saved tool results, not just regular chat text. Local inspection showed the size was mostly from persisted tool output records:

  • function_call_output: about 228 MB
  • mcp_tool_call_end: about 171 MB
  • assistant/user messages are much smaller by comparison

Cleaning the local logs database did not fix the issue. The active thread itself was small in another test case, but opening this large thread first made the whole app remain slow until restart.

Sampling the Codex Desktop main process while CPU was high showed activity concentrated around Electron/Node IPC paths, including libuv stream -> JS callback -> Buffer/memmove style work. The renderer also showed V8/renderer work during the spike.

Expected Behavior

Opening a large thread should not permanently peg CPU. If the large thread is expensive to load, Codex should either:

  • lazily load/virtualize thread history,
  • truncate or summarize persisted tool outputs in the UI,
  • avoid loading full tool output payloads unless expanded,
  • cancel/release parsing and rendering work when switching away from the thread,
  • and release memory/CPU pressure after navigating to another conversation.

Actual Behavior

After the large thread is opened once, CPU remains high even after switching away. A full app restart is required to recover.

Impact

Large long-running agent threads with many tool calls become effectively unsafe to open in Codex Desktop, because one open can make the whole app sluggish until restart.

View original on GitHub ↗

9 Comments

github-actions[bot] contributor · 3 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #25163
  • #25215
  • #24676
  • #24510

Powered by Codex Action

guidedways contributor · 3 months ago
Audacity88 · 2 months ago

I’m seeing what looks like the same class of issue on macOS, with a stronger native app-server memory signal.

Repro shape:

  1. Open Codex Desktop.
  2. Select/open a long, tool-heavy local thread from the sidebar.
  3. The app becomes sluggish while the native app-server process spikes in memory.
  4. The spike can last long enough to catch with vmmap / sample, then may partially fall back down.

Captured evidence from one reproduction:

  • Process: /Applications/Codex.app/Contents/Resources/codex app-server --analytics-default-enabled
  • sample captured while hot:
  • Physical footprint: 13.2G
  • Physical footprint (peak): 13.2G
  • vmmap -summary captured during the same spike:
  • Physical footprint: 10.2G
  • Physical footprint (peak): 13.0G
  • DefaultMallocZone: 10.5G allocated
  • MALLOC_MEDIUM: 9.6G
  • most of the footprint was swapped: about 10.2G swapped
  • Renderer/helper processes were not the main memory consumer in the process snapshot.
  • The active sample stacks are mostly stripped, but show heavy native heap/copy/path work such as _platform_memmove, _realloc, szone_malloc_should_clear, read, realpath, stat/lstat, and some fork.

This seems consistent with Desktop eagerly hydrating/copying a large local thread/tool-output history into native app-server memory when the thread is opened. I’m avoiding thread names/details here, but the affected local history is a long-running tool-heavy Desktop thread.

This may also relate to #21134, but this repro is specifically “opening/selecting a large existing thread causes the native app-server to spike to ~13GB physical footprint.”

Jorjon · 2 months ago

I can reproduce a similar large-history CPU/memory case on Codex Desktop.

Observed behavior:

  • Opening/selecting one large historical thread leaves a Codex renderer process hot even when no tool command is doing work.
  • The hot process is the Electron renderer, not codex app-server and not an external child command.
  • The renderer stayed around 100%+ CPU with multi-GB RSS.
  • The GPU process also showed sustained CPU usage while the renderer was hot.
  • A macOS sample of the renderer showed a very large physical footprint and stack activity in V8 / JS / serialization / allocation paths.

Local history shape:

  • The affected local rollout JSONL is in the 400-450 MB range.
  • It contains thousands of response_item and event_msg records.
  • It also contains multiple type: "compacted" records.
  • The largest JSONL lines are in the ~18-21 MB range.
  • The largest lines I inspected were compacted events with large payload.replacement_history arrays.

This suggests the issue is not only large persisted tool outputs. Very large compacted / replacement_history payloads also seem to be enough to make the desktop renderer do expensive hydration/parsing/rendering work. Once triggered, the UI/GPU side appears to remain hot even though tool subprocesses are idle.

beverm2391 · 17 days ago

I reproduced this same failure class on a substantially newer desktop build, with a much smaller thread and a captured main-process stack.

Environment:

  • ChatGPT/Codex Desktop 26.803.41515 (build 6321)
  • macOS 15.2 (24C101), Apple Silicon (Mac16,7, arm64)

Trigger and timing:

  • A short follow-up was submitted to a long-running local task.
  • The backend emitted task_complete at 14:54:03.905 local time.
  • The desktop main process reached 100% CPU by the next sample at 14:54:05 and remained at approximately 99-100% until force-quit at 14:58:14.
  • macOS classified the app as unresponsive for 155 seconds in the generated hang report.

The affected rollout JSONL was only 19,106,230 bytes with 4,180 records. The final turn reported 210,304 input tokens against a 258,400 context window. A previous matching hang involved another long-running task whose rollout JSONL was 18,834,563 bytes, suggesting the unsafe threshold can be far below the 501 MB case originally reported here.

A five-second live sample captured during the hang showed all 230 samples on the Electron main thread in the same Node/V8 callback path. The symbolized stack passes through:

node::InternalCallbackScope::Close()
v8::ValueSerializer::WriteValue(...)
<optimized/JIT JavaScript frames>

The renderer and app-server children were not the CPU source. System memory remained healthy (58% free), the ChatGPT main process RSS was approximately 382 MB, and no chrome-headless-shell process was active. This appears consistent with pathological main-process serialization/IPC or task-state persistence immediately after turn completion, rather than model execution, browser automation, or memory pressure.

I have not attached the raw task transcript or process listing because they contain private project context/helper arguments. I can provide the sanitized sample, macOS .hang, cpu_resource.diag, and a timing/metrics summary if maintainers indicate the preferred channel.

beverm2391 · 15 days ago

Follow-up: I found a concrete trigger for the Aug 11 recurrence. This one was not caused solely by opening a large thread; a malformed persisted heartbeat automation entered an unbounded resume loop and eventually pegged the Electron main process.

Environment

  • ChatGPT/Codex Desktop 26.803.41515 (build 6321)
  • macOS 15.2 (24C101), Apple Silicon (Mac16,7)

Malformed persisted automation

A stale heartbeat created for a PR watcher was still ACTIVE even though the PR had already merged. Its persisted record contained:

kind = "heartbeat"
status = "ACTIVE"
rrule = "FREQ=MINUTELY;INTERVAL=5"
target_thread_id = "thread"

The literal value "thread" is not a valid thread UUID.

When the heartbeat fired, the desktop log repeated this sequence approximately every 0.78 seconds:

maybe_resume_started conversationId=thread
thread/read -> -32600 invalid thread id
thread/resume -> -32600 invalid session id
heartbeat_automation_resume_failed threadId=thread

There was no terminal stop or meaningful backoff. On Aug 11, the log recorded 1,142 heartbeat_automation_resume_failed events from 12:58:02 PM through 1:12:59 PM ET.

CPU/hang correlation

  • Main-process CPU was 2.6% at 1:12:50 PM.
  • It jumped to 82.3% at 1:13:00 PM, then remained around 98.5–100% until I manually quit the app at 2:04:03 PM.
  • macOS generated a ChatGPT.cpu_resource diagnostic: 90 seconds of CPU over 121 seconds, exceeding the 50% limit.
  • A live sample captured all 232 main-thread samples in the same path: node::InternalCallbackScope::Close() -> v8::ValueSerializer::WriteValue(...) -> optimized/JIT JS frames.
  • Main-process RSS was only about 377 MB and system memory was healthy (59% free), so this was not OOM pressure.

The invalid automation survived an app restart and immediately resumed the same retry loop. The new launch accumulated another 426 failures before cleanup.

Recovery confirmation

I deleted that exact obsolete heartbeat through the app’s automation API. Its persisted file was removed, and the failure count stayed fixed at 426 during verification instead of increasing every ~0.78 seconds. Main-process CPU returned to normal.

This suggests at least one path to the sustained-main-process-CPU failure is repeated serialization/IPC/error-state work caused by a malformed heartbeat target, not only oversized conversation hydration.

Potential safeguards:

  1. Validate target_thread_id as a real thread UUID when creating, updating, and loading heartbeat records.
  2. Quarantine or disable invalid persisted automations instead of attempting to resume them.
  3. Treat -32600 invalid thread/session id as non-retryable.
  4. Add bounded retries and backoff so one malformed heartbeat cannot spin forever.
  5. Ensure terminal PR watchers delete themselves when their watched PR is merged.

I can provide the sanitized desktop log, CPU resource diagnostic, live sample, and monitor timeline if maintainers indicate the preferred channel.

shleder · 14 days ago

A 501 MB thread dominated by ~228 MB of function_call_output and ~171 MB of MCP results is a useful persisted-tool-output case, especially because opening it leaves the app pegged until restart.

I’m testing whether Codex Rescue diagnoses this safely with bounded reads. It cannot fix Desktop’s IPC/CPU loop or automatically truncate the original history. It is local-first, keeps the source rollout untouched, does not replay calls, and verification remains fail-closed.

If the affected session still exists, could you run:

pipx install codex-rescue==0.1.0a3
codex-rescue sessions
codex-rescue doctor --latest

Please share sanitized diagnostic output only—no raw rollout, MCP/tool contents, databases, prompts, repository data, paths, or secrets.

https://github.com/shleder/codex-rescue

Roland0511 · 3 days ago

Adding a current macOS reproduction on a substantially newer Desktop build. This case is notable because the affected rollout is much smaller than the original 501 MB report, and the Desktop logs show a clear split between fast app-server operations and a long renderer/UI stall.

Environment

  • ChatGPT/Codex Desktop: 26.818.61809 (build 7019)
  • Bundled Codex CLI/app-server: 0.149.0-alpha.4.3
  • macOS: 26.5.2 (25F84), Apple Silicon (arm64)
  • No crash or macOS DiagnosticReport was produced; the symptom was a severe foreground UI stall.

Privacy-safe rollout metadata

  • Rollout size: 90,766,549 bytes (86.56 MiB)
  • JSONL records: 9,243
  • Record types included 5,363 response_item, 3,730 event_msg, and 17 compacted
  • Five largest JSONL records: 4,752,249, 4,716,958, 4,244,607, 4,240,702, and 3,751,172 bytes
  • The history is tool-heavy and contains compacted/image payloads. Raw history, task identifiers, prompts, paths, and screenshots are intentionally omitted.

Reproduction and timing

  1. Start Desktop and switch among several ordinary tasks; they remain responsive.
  2. Select the affected older task from the sidebar.
  3. The window becomes severely unresponsive. The next route/activity change only registers roughly 43 seconds later when switching away.

Sanitized Desktop timing from the affected selection:

  • task view became active: 02:09:29.894
  • thread/read: 4 ms
  • thread/resume: 374 ms
  • maybe_resume_success: 02:09:30.404
  • two thread/turns/list responses: 250 ms and 248 ms
  • first ResizeObserver loop completed with undelivered notifications: 02:09:30.447
  • task finally became inactive after navigation away: 02:10:13.901

This run does not look disk- or app-server-bound: the read, resume, and turn-list operations all completed in under 400 ms, but the renderer/UI was effectively unavailable for about 43 seconds afterward. Repeated ResizeObserver loop completed with undelivered notifications errors continued around later interaction with the task.

Ambient suggestions as an amplifier, not the initial trigger

The first stall began before any ambient suggestion generation, so background model inference cannot explain the initial freeze.

However, reopening the same task later activated it at 02:10:41.909, followed by an ambient_suggestions ephemeral generation at 02:10:42.348. That run used:

  • input tokens: 91,568
  • cached input tokens: 62,720
  • model: gpt-5.6-terra, medium reasoning
  • latency: 29,273 ms
  • returned suggestions: 0

A separate home/projectless ambient generation in the same period used 242,577 input tokens (182,016 cached), ran for 63,056 ms, and also returned zero suggestions. This appears to be an additional resource/context-assembly amplifier when revisiting a heavy project, but not the root trigger of the first UI stall. This overlaps with the background-prefetch failure family reported in #35799.

Assessment

This supports treating initial thread loading as a bounded renderer contract rather than only optimizing raw JSONL parsing or app-server reads. A rollout below 100 MiB, with several multi-megabyte records, is already sufficient to make the current macOS Desktop UI appear frozen even though the backend read/resume calls finish quickly.

Useful safeguards would include:

  • render only a bounded recent tail on selection;
  • keep compacted history, images, and large tool outputs out of the initial renderer payload;
  • cancel renderer hydration/layout work when navigating away;
  • do not let ambient suggestions materialize or replay large task histories on the foreground interaction path;
  • add phase-specific renderer hydration, layout, serialization, and GC timings after thread/turns/list completes.
shleder · 22 hours ago

For threads experiencing persistent 100%+ CPU utilization caused by oversized function_call_output and heavy persisted MCP payloads:

The CPU pegging occurs because the renderer and parser attempt unbounded parsing of large base64/JSON strings during session load.

If you need to diagnose or extract the thread without locking the desktop app, Vetto provides bounded session diagnosis and size-capped observation:

# Diagnose oversized records and identify heavy payload lines without UI lockup:
vetto rescue diagnose ~/.codex/sessions/.../session.jsonl

# Generate a clean lightweight snapshot with capped payload budgets:
vetto rescue snapshot ~/.codex/sessions/.../session.jsonl --output ./recovered_session.jsonl

When running future long-running tasks, wrapping with vetto --tui=statusline --agent codex -- codex exec ... enforces bounded memory ringbuffers (5 FPS rendering cap, strict memory limits), preventing bloated tool outputs from freezing host desktop processes.