Codex App: stale `heartbeat-thread-permissions-by-id` in global-state spins the main process at ~100% CPU on launch (UI frozen, persists across restarts)
What version of the Codex App are you using?
App 26.608.12217 (also reproduced on 26.602.x). Chromium framework 149.0.7827.54.
What subscription do you have?
Not relevant to this bug (reproduces regardless of plan).
What platform is your computer?
macOS 26.3 (25D125), Darwin 25.3.0, Apple M4 Pro (Mac16,8), arm64 (native).
What issue are you seeing?
After an auto-update, Codex Desktop fails to launch: the main/browser process pegs one CPU core at ~100% and the UI never becomes responsive (hard freeze, no window usable). Force-quitting and relaunching reproduces it every time. macOS flags it via cpu_resource.diag: 90s CPU over 91s wall (99%, single thread). On an M4 Pro — so this is a genuine busy-loop, not legitimate startup load.
Isolated root cause — a single key in ~/.codex/.codex-global-state.json:electron-persisted-atom-state → heartbeat-thread-permissions-by-id.
Removing only that key makes the app launch cleanly (settles to ~1–2% idle CPU) with everything else intact — all local + remote projects/sessions, automations, plugins, remote connections. Putting it back brings the spin straight back. Verified by isolation: I restored the full state and removed this one key → healthy; this was the only key that mattered.
Spin signature (sample of the main/browser process; symbol names approximate):
ChromeMain
-[NSApplication run] → CFRunLoop
__CFRunLoopDoSource0 → __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
uv_run → uv__run_check / uv__run_timers → node callback
v8::Promise::Resolver::Reject
<same anonymous JIT frame repeated ~17 levels> ← synchronous reject→retry, no backoff
The main thread spends ~100% of its time inside a CFRunLoop source0 callout running a JS promise that rejects and is immediately retried, never yielding → the run loop never returns → UI frozen. The heartbeat appears to fire against per-thread permission entries whose target (e.g. a stale/unreachable remote thread) fails, and the failure path is a tight synchronous retry with no backoff and no timeout.
What steps can reproduce the bug?
No deterministic from-scratch repro (depends on accumulated heartbeat-thread-permissions-by-id entries pointing at stale/unreachable threads — e.g. after using remote sessions, then an app update). On an affected install it is 100% deterministic:
- Launch Codex → main process immediately goes to ~100% CPU, UI frozen.
- Quit + relaunch → same (state persists).
- Remove
heartbeat-thread-permissions-by-idfrom~/.codex/.codex-global-state.json→ launches fine, everything else intact.
What is the expected behavior?
- Startup heartbeat / per-thread permission checks must be async + backed-off + time-bounded, never able to block the main thread.
- A failing heartbeat to a stale/unreachable thread should skip/timeout, not spin.
- Recovery should not require editing/nuking the whole
global-state— today the only workaround is destructive, and because the sidebar/session index is stored in the sameglobal-stateblob, naively resetting it makes the user lose their whole session list. The failing subsystem should be isolated from the session index.
Additional information
- In a related earlier startup-hang report (#25259) I had speculated leftover crashpad helper processes were involved — that turned out to be a red herring; with a clean process slate the spin still reproduced, and the actual cause is the
heartbeat-thread-permissions-by-idstate above (confirmed by single-key isolation). - Diagnose on an affected machine via
/Library/Logs/DiagnosticReports/Codex_*.cpu_resource.diagandsample <main-pid>(main thread stuck in__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0 → Promise::Resolver::Reject= this bug;mach_msg2_trap= healthy).
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗