Desktop thrashes resume/unsubscribe when >4 heartbeat automations have target_thread_id

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

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:

  1. Create five heartbeat automations.
  2. Give each one a target_thread_id pointing to a target thread.
  3. The target threads can be fresh and tiny; each only had one trivial turn like user say ok, assistant ok.
  4. Enable the fifth linked heartbeat automation.
  5. 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(...) and setConversationStreamRole(... { 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_resume state.

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=true symptom, but appears framed around one desynced thread after request_user_input in Default mode.
  • #16271: crash/resume loop around interrupted threads and missing rollout files on Windows.

Possible fix directions:

  • Do not call markConversationStreaming in the resume path when the latest turn status is already completed.
  • Add a guard/backoff around repeated thread/resume for 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_resume does 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.

View original on GitHub ↗

15 Comments

github-actions[bot] contributor · 2 months ago

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

  • #19516
  • #18333

Powered by Codex Action

akibablade · 2 months ago

I’m seeing what looks like the same issue on Codex Desktop 26.422.30944.

My repro is very consistent around automations:

  • 4 or fewer enabled automations: app idles normally, roughly 10-15 Energy Impact in Activity Monitor.
  • Enabling any 5th automation: Energy Impact jumps to ~6,000+ while Codex is otherwise idle.
  • This can happen with heartbeat automations and cron automations.
  • A normal active thread peaks far lower, around ~900 Energy Impact.

The logs show the same broad pattern described here: repeated thread/read, thread/resume, and thread/unsubscribe across 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=false

and warnings like:

thread/resume overrides ignored for running thread

The 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=true loop, 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.

akibablade · 2 months ago

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:

  1. Five active cron automations with no linked target thread:
  • No CPU/Energy explosion.
  • No sustained resume/unsubscribe loop.
  1. Five active heartbeat automations, each linked to a fresh empty target thread:
  • Each target thread only had one trivial turn: user “say ok”, assistant “ok”.
  • The issue reproduced exactly when the 5th linked heartbeat was enabled.
  • Energy Impact spiked again.
  • The logs entered the same loop across all five fresh target threads.

In one minute, around 2026-04-25T19:13Z, I saw approximately:

  • 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 fresh heartbeat target threads. Each showed latestTurnStatus=completed markedStreaming=true and turnCount=1.

This seems to narrow the trigger to:

more than 4 active automations with target_thread_id

Not:

  • pinned threads
  • large thread histories
  • stale plugins
  • malformed old automation state
  • actual automation execution
  • no-thread cron automations

Also worth noting: I do not see evidence that the resume/unsubscribe loop is making model calls. automation_runs remained 0, 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.

markmdev · 2 months ago

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 paused target_thread_id lines made CPU drop back to idle immediately.

I am keeping #19516 separate because that one is specifically the orphaned codex_chronicle lock-wait bug after killing Desktop.

markmdev · 2 months ago

@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 through thread/read -> thread/resume -> maybe_resume_success -> thread/unsubscribe, with latestTurnStatus=completed markedStreaming=true.

Could one of you route this to the right owner if this is not your area?

akibablade · 2 months ago

#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 chats were added.

Heartbeats being tied to a single thread suggests to me that this bug was introduced by part of the chats enhancement.

wangyaok2 · 2 months ago

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_success reports an already ended turn (completed or failed) while the conversation is still marked streaming (markedStreaming=true)

Difference in trigger path:

  • #19563 has a clean automation-specific repro involving more than 4 heartbeat automations with target_thread_id, followed by repeated thread/read -> thread/resume -> maybe_resume_success -> thread/unsubscribe cycling
  • #19690 reproduces on normal conversation restore without requiring heartbeat automations or the >4 target_thread_id condition
  • In #19690, the same affected conversation can be resumed successfully from the CLI, while Desktop restore remains stuck behaving as if the thread is still streaming

So my current read is: related issue, possibly same bug family, but not the same exact repro surface.

akibablade · 2 months ago

This bug persists in the latest update: Version 26.422.62136 (2180)

brunribeiro · 2 months ago

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" />

akibablade · 2 months ago

Bug persists in the latest update Version 26.422.71525 (2210)

4 heartbeats still causes CPU usage to spike to over 5X more than a heavy tool use thread.
akibablade · 2 months ago
I am also facing this bug consistently on Version 26.422.62136 (2180) <img alt="Image" width="342" height="205" src="https://private-user-images.githubusercontent.com/181275937/584977022-055f05ec-6b6b-4779-823e-bfa7829d6d33.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Nzc0NTMwOTksIm5iZiI6MTc3NzQ1Mjc5OSwicGF0aCI6Ii8xODEyNzU5MzcvNTg0OTc3MDIyLTA1NWYwNWVjLTZiNmItNDc3OS04MjNlLWJmYTc4MjlkNmQzMy5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjYwNDI5JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI2MDQyOVQwODUzMTlaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT01YzkyMTg4MTMyZDAzYjI3ODUwMmU0NWYzNWFjYzczMjZmYjZhOGE4NWMyZjE1N2NiNzFkNDhkMzIwMzQ5N2JmJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZyZXNwb25zZS1jb250ZW50LXR5cGU9aW1hZ2UlMkZwbmcifQ.BKmcurLytC45cGMIgbh8KvuL6JsKs_ciMNLYHQjIg7A">

This doesn't seem like the same bug. Connection issue is not related to the high CPU/Energy usage from my testing.

akibablade · 2 months ago

@markmdev fixed in Version 26.429.20946 (2312) on my machine.

scottbarbag-ux · 2 months ago

I’m seeing what looks like the Windows side of this same target_thread_id heartbeat problem.

Current local state on Windows Desktop:

  • 7 active automation configs
  • all 7 are kind = "heartbeat"
  • all 7 have persisted target_thread_id
  • none of those TOMLs declares cwds, executionEnvironment, or destination, so the run context is effectively tied to the linked target thread

The user-visible failures are now:

  • Could not start automation — Timed out waiting for MCP response to thread/resume
  • Failed to resume chat — cannot resume running thread with mismatched path
  • repeated resume/read/unsubscribe behavior around old target threads

This 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_id values, 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 the mismatched path resume case, not only the high-CPU resume/unsubscribe loop.

akibablade · 2 months ago

@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

rcbeall1 · 1 month ago

I am seeing what looks like the same underlying issue, with one additional symptom that may help narrow it down.

Environment observed:

  • Codex Desktop: 26.519.41501
  • macOS: Apple Silicon
  • Shape: multiple heartbeat automations with target_thread_id, including more than four linked heartbeat targets
  • Current state when reproduced: five active linked heartbeats plus two paused linked heartbeats

Observed behavior:

  1. After Codex Desktop has been running for a while, affected heartbeat target threads show repeated local lifecycle events like:

thread/read -> thread/resume -> maybe_resume_success -> thread/unsubscribe -> inactive_thread_unsubscribed

  1. The affected thread can later be opened again and can still show the model-visible schema for codex_app.automation_update.
  1. Despite that, calls to automation_update from that same thread can fail with:

No handler registered for tool: automation_update

  1. The failed automation_update attempts do not appear to reach the Desktop app dispatcher logs. Other sessions in the same Desktop process may still be able to call automation_update successfully during the same period, so this looks like per-thread or per-conversation handler drift rather than a process-wide missing handler.
  1. Fully quitting Codex Desktop and reopening it restores automation_update for the affected thread. Merely reopening/resuming the thread was not enough in the observed case.

Why this seems related to this issue:

  • The packaged Desktop app appears to have a renderer-side inactive owner-conversation cleanup path with a max inactive owner thread count of 4 and a 1 hour inactive TTL.
  • The affected threads are heartbeat targets with target_thread_id.
  • When there are more than four linked heartbeat targets, the Desktop appears to resume completed target threads, mark them owner/streaming, then unsubscribe inactive ones again.
  • That lines up with the resume/unsubscribe loop described here.

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:

  • heartbeat target thread is resumed or loaded by Desktop
  • inactive owner cleanup unsubscribes the renderer connection for that target thread, either because the 1 hour TTL expired or because the linked target count exceeds the inactive owner cap
  • thread/read or thread/resume later restores enough thread state for the conversation to continue and for dynamic tool definitions to remain visible
  • the client-side dynamic tool handler binding is not reliably reattached for that conversation
  • automation_update then fails with a missing-handler error even though the schema is present
  • full Desktop restart rebuilds the handler registration and fixes the affected thread

Expected behavior:

  • Completed heartbeat target threads should not be repeatedly resumed and unsubscribed while Desktop is idle.
  • More than four linked heartbeat targets should not trigger sustained resume/unsubscribe churn.
  • If a thread is unsubscribed for renderer memory cleanup and later resumed/read again, dynamic tool handlers such as codex_app.automation_update should be reattached consistently.
  • The model should not be left in a state where it can see a dynamic tool schema but cannot invoke the handler.

Potentially useful diagnostic distinction:

  • This does not look like RRULE parsing. The affected schedules included ordinary daily and weekly/hourly RRULEs that the UI can represent.
  • This does not look like a missing target thread. The thread was readable/resumable.
  • This does not look like a global app-server handler outage. Other threads in the same Desktop process could call automation_update successfully.
  • This does look like per-conversation client handler drift after inactive unsubscribe/resume churn.