Desktop thrashes resume/unsubscribe when >4 heartbeat automations have target_thread_id
What version of the Codex App are you using?
Codex Desktop 26.422.30944 (CFBundleVersion 2080)
Bundled CLI observed during the investigation: codex-cli 0.125.0-alpha.3
What platform is your computer?
macOS 26.0.1 (25A362), Apple Silicon / arm64
What issue are you seeing?
Codex Desktop can enter a persistent local CPU/Energy loop when more than four heartbeat automations have a linked target_thread_id.
The Desktop repeatedly cycles completed heartbeat target threads through:
thread/read -> thread/resume -> maybe_resume_success -> thread/unsubscribe
The suspicious detail is that the turns are already completed, but the resume path still marks the conversation streaming:
maybe_resume_success ... latestTurnStatus=completed markedStreaming=true
The cleaner finding is that this does not require giant histories, pinned threads, stale Chronicle state, memory backfill, or actual automation execution. It reproduces with fresh tiny target threads when the fifth linked heartbeat automation is enabled.
In the original real-world case, large histories made the impact much worse:
Codex main CPU: up to ~190-224%
Renderer CPU: up to ~63-108%
App server CPU: up to ~36-69%
phys_footprint during loop: ~4.9 GB
phys_footprint_peak: 9.2 GB
What steps can reproduce the bug?
A clean repro from the same machine:
- Create five heartbeat automations.
- Give each one a
target_thread_idpointing to a target thread. - The target threads can be fresh and tiny; each only had one trivial turn like user
say ok, assistantok. - Enable the fifth linked heartbeat automation.
- Leave Desktop idle and watch
~/Library/Logs/com.openai.codex/...plus Activity Monitor / process CPU.
When the fifth linked heartbeat was enabled, Desktop entered the resume/unsubscribe loop across all five fresh target threads.
Approximate one-minute log counts from the clean repro:
thread/read: 1,424
thread/resume: 1,423
thread/unsubscribe: 1,423
maybe_resume_success: 1,423
The loop was evenly distributed across the five heartbeat target threads. Each showed latestTurnStatus=completed markedStreaming=true and turnCount=1.
A negative-control case did not reproduce:
Five active cron automations with no linked target thread:
- no CPU/Energy explosion
- no sustained resume/unsubscribe loop
So the narrowed trigger looks like:
more than 4 heartbeat automations with target_thread_id
It does not appear to be:
pinned threads
large thread histories
stale plugins
malformed old automation state
actual automation execution
no-thread cron automations
runaway model calls
automation_runs remained 0 during the loop, and token-count events appeared only for actual thread/heartbeat turns, not for each resume/unsubscribe cycle. This looks like a local Desktop subscription/resume loop rather than runaway token usage.
Additional real-world mitigation evidence
In the original affected Desktop state, the loop involved five heartbeat automation target threads:
4 PAUSED heartbeat automations with target_thread_id
1 ACTIVE heartbeat automation with target_thread_id
That means paused automation target links were still enough to keep completed target threads in the UI/subscription/resume set.
I commented out only the target_thread_id lines for the four PAUSED heartbeat automations, leaving the automation files present and still paused. The active heartbeat was left unchanged.
Immediately after detaching those paused target threads:
Codex main CPU: ~224% -> ~0.9%
app-server CPU: ~25% -> ~0.1%
renderer CPU: ~28% -> ~0.0%
codex_chronicle: ~2.5%
A fresh 8-second log window after the detach had no new entries for:
thread/resume
maybe_resume_success
inactive_thread_unsubscribed
This suggests the trigger is the Desktop handling of linked heartbeat target threads, not the heartbeat jobs actually firing.
Code-path clue from the packaged app
From the local packaged app (/Applications/Codex.app/Contents/Resources/app.asar), the behavior lines up with a subscription state-machine loop:
- The inactive owner stream cap is
4(maxInactiveOwnerThreads). - The resume success path calls the equivalent of
markConversationStreaming(...)andsetConversationStreamRole(... { role: "owner" })even when the latest turn is already completed. - The inactive cleanup path then unsubscribes completed inactive owner threads and sets them back to a
needs_resumestate.
That appears to create this cycle once there are five linked heartbeat targets:
resume completed target -> mark streaming/owner -> exceed inactive owner cap -> unsubscribe -> needs_resume -> resume again
What is the expected behavior?
- Completed heartbeat target threads should not be repeatedly resumed and marked streaming while Desktop is idle.
- Paused heartbeat automations should not keep their target threads in a hot resume/subscription loop.
- If Desktop needs target-thread metadata for the automations UI, it should use a cheap metadata path rather than repeatedly hydrating/resuming completed conversations.
- More than four linked heartbeat automations should not cause sustained multi-core CPU usage or multi-GB memory footprint spikes.
Additional information
This is separate from #19516. That issue is about an orphaned codex_chronicle process surviving a killed Desktop app and holding the Chronicle lock. The bug reported here reproduces without an orphaned Chronicle process, with Chronicle and memory disabled, and with fresh tiny heartbeat target threads.
Related issues:
- #19516: separate orphaned Chronicle lock-wait loop after killing Desktop.
- #10952: includes a similar
maybe_resume_success ... latestTurnStatus=completed markedStreaming=truesymptom, but appears framed around one desynced thread afterrequest_user_inputin Default mode. - #16271: crash/resume loop around interrupted threads and missing rollout files on Windows.
Possible fix directions:
- Do not call
markConversationStreamingin the resume path when the latest turn status is already completed. - Add a guard/backoff around repeated
thread/resumefor completed idle target threads. - Keep paused heartbeat target threads out of the hot subscription/resume set.
- Treat inactive unsubscribe state transitions carefully so
resumeState=needs_resumedoes not immediately re-trigger hydration for non-active completed target threads. - Add a cheap metadata path for automation target-thread lists that does not require full conversation snapshots.
15 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
I’m seeing what looks like the same issue on Codex Desktop 26.422.30944.
My repro is very consistent around automations:
The logs show the same broad pattern described here: repeated
thread/read,thread/resume, andthread/unsubscribeacross multiple automation/workflow threads, not one poisoned thread.In my bad window I also see lots of:
composing running thread resume response ... active_turn_present=falseand warnings like:
thread/resume overrides ignored for running threadThe affected threads include pinned/recent automation target threads, including some paused automation targets, so it does not look like actual automation execution. It looks more like Desktop repeatedly hydrating/resuming automation target threads from the UI/subscription layer.
This may be the same root bug as the
maybe_resume_success ... latestTurnStatus=completed markedStreaming=trueloop, or a sibling path in the same resume/subscription state machine. The useful extra signal from my machine is that crossing from 4 to 5 enabled automations is a reliable trigger.Update with a cleaner repro:
For me, this no longer looks related to pinned threads, large histories, or old local ACE state.
I tested two separate cases:
In one minute, around
2026-04-25T19:13Z, I saw approximately:thread/read: 1,424thread/resume: 1,423thread/unsubscribe: 1,423maybe_resume_success: 1,423The loop was evenly distributed across the five fresh heartbeat target threads. Each showed
latestTurnStatus=completed markedStreaming=trueandturnCount=1.This seems to narrow the trigger to:
Not:
Also worth noting: I do not see evidence that the resume/unsubscribe loop is making model calls.
automation_runsremained0, and token-count events appeared only for actual thread/heartbeat turns, not for each resume/unsubscribe cycle. So this appears to be a local Desktop subscription/resume loop causing CPU/energy impact, rather than runaway token usage.I rewrote the title/body with the newer narrowed repro and mitigation evidence.
Key correction: this no longer looks dependent on large histories. The clean trigger is more than 4 heartbeat automations with
target_thread_id, including fresh tiny target threads. In the real-world case, four PAUSED heartbeat automations with linked target threads plus one active linked heartbeat were enough to keep Desktop in the resume/unsubscribe loop; commenting out only the pausedtarget_thread_idlines made CPU drop back to idle immediately.I am keeping #19516 separate because that one is specifically the orphaned
codex_chroniclelock-wait bug after killing Desktop.@etraut-openai @ambrosino-oai Sorry for the direct ping, but this looks like it sits right at the heartbeat automation / app-server resume boundary.
We narrowed this to a clean trigger: more than 4 heartbeat automations with
target_thread_id. It reproduces with fresh tiny completed target threads, does not require large histories, and does not appear to make model calls. Desktop loops locally throughthread/read -> thread/resume -> maybe_resume_success -> thread/unsubscribe, withlatestTurnStatus=completed markedStreaming=true.Could one of you route this to the right owner if this is not your area?
#19568 threads without a project folder also cause CPU spikes but the 5 heartbeats issue is cleanly reproducible and the biggest factor in the high CPU usage being widely reported imo.
I've had to neuter my Codex workflow since projectless
chatswere added.Heartbeats being tied to a single
threadsuggests to me that this bug was introduced by part of thechatsenhancement.Linking a closely related report for maintainers: #19690.
I compared the two more closely and they do not look like strict duplicates, but they may share the same underlying Desktop resume/stream-state defect.
Common symptom:
maybe_resume_successreports an already ended turn (completedorfailed) while the conversation is still marked streaming (markedStreaming=true)Difference in trigger path:
target_thread_id, followed by repeatedthread/read -> thread/resume -> maybe_resume_success -> thread/unsubscribecycling>4 target_thread_idconditionSo my current read is: related issue, possibly same bug family, but not the same exact repro surface.
This bug persists in the latest update: Version 26.422.62136 (2180)
I am also facing this bug consistently on Version 26.422.62136 (2180)
<img width="342" height="205" alt="Image" src="https://github.com/user-attachments/assets/055f05ec-6b6b-4779-823e-bfa7829d6d33" />
Bug persists in the latest update Version 26.422.71525 (2210)
This doesn't seem like the same bug. Connection issue is not related to the high CPU/Energy usage from my testing.
@markmdev fixed in Version 26.429.20946 (2312) on my machine.
I’m seeing what looks like the Windows side of this same
target_thread_idheartbeat problem.Current local state on Windows Desktop:
kind = "heartbeat"target_thread_idcwds,executionEnvironment, ordestination, so the run context is effectively tied to the linked target threadThe user-visible failures are now:
Could not start automation — Timed out waiting for MCP response to thread/resumeFailed to resume chat — cannot resume running thread with mismatched pathThis happened after I tried using existing-thread heartbeat automations as a workaround for the cron/new-thread empty-session issue tracked in #19011. That workaround avoided one failure mode, but it introduced this target-thread resume failure mode when applied broadly.
My current mitigation is to pause those heartbeat/thread automations, stop reusing old
target_thread_idvalues, and recreate necessary jobs as fresh standalone runs or move repo-only maintenance to cloud/GitHub paths.I see the latest comment says this is fixed on macOS in
26.429.20946. It would be useful to know whether that same fix covers Windows Desktop and whether it includes themismatched pathresume case, not only the high-CPU resume/unsubscribe loop.@markmdev I got the spike again today. Turning automation heartbeats back down to 4 fixed it again. It drained my Macbook M5 Pro battery that lasts all day in 45 mins before I noticed!
Possible regression in Version 26.429.30905 (2345) of this issue @etraut-openai
I am seeing what looks like the same underlying issue, with one additional symptom that may help narrow it down.
Environment observed:
Observed behavior:
thread/read -> thread/resume -> maybe_resume_success -> thread/unsubscribe -> inactive_thread_unsubscribed
No handler registered for tool: automation_update
Why this seems related to this issue:
The extra failure mode is that the unsubscribe/resume cycle appears to break the dynamic handler binding for at least codex_app.automation_update in some threads. The thread can still be readable/resumable, and the dynamic tool schema can still be visible to the model, but the actual invocation path no longer has a registered handler until Desktop is restarted.
A refined hypothesis:
Expected behavior:
Potentially useful diagnostic distinction: