openai / openai/codex

[app][macOS][26.810.52044] Unbounded SkyComputerUseService respawn loop crashes the app in ~2 min — onServiceConnectionLost has no failure counter, backoff, or circuit breaker

Open
#38,841 4 comments 6 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

app bug computer-use performance
Dominant language
Rust
Stars
125k
Forks
19.5k
PR merge metrics
PR metrics pending

Description

[app][macOS][26.810.52044] Unbounded SkyComputerUseService respawn loop crashes the app in ~2 min — onServiceConnectionLost has no failure counter, backoff, or circuit breaker

Summary

On macOS, the ChatGPT/Codex desktop app enters an unbounded respawn loop for the Computer Use PiP helper (SkyComputerUseService) whenever the PiP bootstrap handshake fails. Each iteration leaks a live 75 MB helper process and permanently blocks a main-app thread, so the app reaches the V8 heap limit and aborts roughly 2 minutes after launch — with no user interaction and without Computer Use ever being invoked.

The native (ObjC/C++) side of the handshake is implemented correctly: it has a 15 s Apple Event timeout, a 5-attempt cap, and exponential backoff. The defect is in the JavaScript layer above it, where the connection-lost callback is a no-op that only nulls the cached PID. I have traced this to a specific callback in app.asar and include a proposed patch below.

This appears to be the same underlying defect behind several existing reports (see Related); this issue adds the root cause and a specific code location.


Environment

App ChatGPT.app 26.810.52044 (build 6662), bundle id com.openai.codex
Framework Codex Framework 151.0.7922.137
macOS 26.3 (25D125)
Hardware Mac16,10 (Mac mini, Apple Silicon), 16 GB RAM
Computer Use helper 26.812.1000717, at ~/.codex/computer-use/Codex Computer Use.app
Install source https://persistent.oaistatic.com/codex-app-prod/ChatGPT.dmg (verified: the downloaded DMG is byte-identical in version/build to the installed app)

Reported crashing builds in the community thread: 6570, 6644, 6662.


Reproduction

  1. Launch ChatGPT.app.
  2. Do nothing. Do not open a project, do not use Computer Use, do not interact at all.
  3. The app aborts after ~70–150 s.

100% reproducible on this machine across 5+ consecutive launches.


Observed behaviour

Helper process explosion

SkyComputerUseService instances, all children of the main app process, none reaped:

Time since launch Live helper processes Combined RSS
12 s 36 ~2.7 GB
25 s 79 ~5.9 GB
30 s 105 ~7.9 GB
~40 s (manually killed) 199 12.5 GB

Spawn rate ≈ 5/s. Each helper is ~75 MB and stays alive and healthy — sampling one shows a normal, idle NSApplicationMain run loop parked in mach_msg2_trap. They are never asked to exit and never reaped.

Main-process thread exhaustion
Time since launch Threads in main process
5 s 2
60 s 209
120 s 309 → crash

Growth is linear and monotonic; threads are never released.

Crash report
Exception Type:  EXC_CRASH (SIGABRT)
Termination Reason: Namespace SIGNAL, Code 6, Abort trap: 6
Dispatch Thread Soft Limit Reached: 64 (too many dispatch threads blocked in synchronous operations)
Triggered by Thread: 9  V8Worker

Thread 9 Crashed:
0  libsystem_kernel.dylib  __pthread_kill
1  libsystem_pthread.dylib pthread_kill
2  libsystem_c.dylib       abort
3  Codex Framework         node::OOMErrorHandler(char const*, v8::OOMDetails const&)

Thread-name histogram at crash time (321 threads total):

187  computer-use
 64  com.apple.root.user-initiated-qos
 16  ThreadPoolForegroundWorker
  ...

Every one of the 187 computer-use threads is parked identically:

libsystem_kernel.dylib  __ulock_wait
libdispatch.dylib       _dlock_wait
libdispatch.dylib       _dispatch_group_wait_slow
AE                      AESendMessage
node.napi.armv8.node    ffi_call_SYSV
sky.node                Napi::details::CallbackData<...>::Wrapper
Codex Framework         napi_is_detached_arraybuffer

and the 64 QoS threads (which is exactly the dispatch soft limit) are parked in:

AE                      AESendMessage
Foundation              -[NSAppleEventDescriptor sendEventWithOptions:timeout:error:]
sky.node                -[RemoteHostedPIPContentService sendBootstrapToServiceWithProcessIdentifier:rendezvousPort:attempt:]
Log evidence

log stream --predicate 'process == "ChatGPT"' during startup:

sky.node  RemoteHostedPIPContent started private endpoint host
xpc       [0x...] failed to do a bootstrap look-up: xpc_error=[3: No such process]
appleevents  AESendMessage(SkCu,PiPB {SkCu,PiPB target='kpid'[pid=32651  returnID=24465}...
appleevents  AESendMessage(SkCu,PiPB {SkCu,PiPB target='kpid'[pid=32700  returnID=-4047}...
appleevents  AESendMessage(SkCu,PiPB {SkCu,PiPB target='kpid'[pid=32705  returnID=22616}...
appleevents  AESendMessage(SkCu,PiPB {SkCu,PiPB target='kpid'[pid=32709  returnID=...}...

The target PID differs on every send — confirming a fresh helper is spawned per attempt rather than the existing one being reused.


Root cause

The native retry logic is correct

Disassembly of -[RemoteHostedPIPContentService sendBootstrapToServiceWithProcessIdentifier:rendezvousPort:attempt:] in Contents/Resources/native/sky.node (symbols are not stripped; function at 0xd474):

Event constructioneventClass = 'SkCu', eventID = 'PiPB':

d504  mov   w2, #0x4375
d508  movk  w2, #0x536b, lsl #16        ; 'SkCu'
d50c  mov   w3, #0x5042
d510  movk  w3, #0x5069, lsl #16        ; 'PiPB'
d520  bl    appleEventWithEventClass:eventID:targetDescriptor:returnID:transactionID:

Send with a bounded 15 s timeout (options = 0x23 = kAEWaitReply | kAECanInteract):

d5d4  fmov  d0, #15.00000000            ; timeout = 15 s
d5e0  mov   w2, #0x23                   ; kAEWaitReply | kAECanInteract
d5e4  bl    sendEventWithOptions:timeout:error:

Attempt cap of 5 (attempt arrives in x4, spilled to [sp,#0x10]):

d494  str   x4, [sp, #0x10]             ; save attempt
...
d73c  ldr   x10, [sp, #0x10]
d740  sub   x8, x10, #0x5
d744  cmn   x8, #0x7
d748  b.hi  0xd7b4                      ; attempt in 0..4 -> retry; attempt >= 5 -> give up

Exponential backoff, 50 ms << attempt (0xF4240 = 1e6 ns/ms):

d7b4  mov   w8, #0x32                   ; 50
d7b8  lsl   x8, x8, x10                 ; 50 << attempt
d7bc  mov   w9, #0x4240
d7c0  movk  w9, #0xf, lsl #16           ; 1_000_000
d7c4  mul   x1, x8, x9                  ; -> nanoseconds
d7d0  bl    dispatch_time
d7e0  bl    dispatch_get_global_queue

So the native layer retries at 50/100/200/400/800 ms (5 attempts, ~1.55 s total) and then correctly gives up, logging and dispatching the failure callback to the main queue.

The JavaScript layer discards that failure

From Contents/Resources/app.asar (minified; identifiers are the shipped mangled names):

te = () => {
  if (!(D || v || !n || !t()) && (
    oe(),
    v = ko({
      controlTooltips: c(),
      onServiceConnectionLost: () => { b = null }     // <-- the entire failure handler
    }),
    v
  )) {
    b != null && Ao(b), se(), B(), ce(), le(), me()
    for (let e of _) e()
  }
}

where ko(...)startRemoteHostedPIPContentHost(tooltips, onServiceConnectionLost), Ao(pid)connectRemoteHostedPIPContentHost(pid), and the spawn path is:

async function Eo(path, { addon: t, electronAppPath: n, platform: r, resourcesPath: i } = {}) {
  return r !== `darwin` || path.trim() === ``
    ? null
    : (t ?? so({ electronAppPath: n, resourcesPath: i })).spawnComputerUseService(path)
}

onServiceConnectionLost only nulls the cached PID. There is no failure counter, no delay, no ceiling, and no disable path. Once b is null, the next invocation of te() — triggered by ordinary UI/state changes — spawns another helper immediately, and the previously spawned helper is never terminated.

Searching the entire 267 MB app.asar for any restart-throttling mechanism:

identifier occurrences
maxRestart 0
restartCount 0
consecutiveFailure 0
circuitBreak 0

No global restart circuit breaker exists.

Why the handshake fails in the first place

Not fully determined. The XPC bootstrap look-up fails with [3: No such process] immediately before the app falls back to the Apple Event path, and the spawned helper — while alive and idle in a normal run loop — never answers SkCu/PiPB.

Note that com.openai.codex has Apple Events authorization entries for com.apple.Terminal, com.apple.finder, com.apple.systemevents, com.google.Chrome and others, but no entry targeting com.openai.sky.CUAService, while other clients on this machine (python, node, other apps) do have that pairing. tccd logs during startup show no AppleEvents authorization request for that target, so this is an observation rather than a confirmed cause.

Regardless of why the handshake fails, the unbounded respawn is an independent defect and is what turns a recoverable failure into a crash.


Proposed fix

Give onServiceConnectionLost a failure counter, backoff, and circuit breaker, and reap the previous helper before spawning a new one:

let cuFailCount = 0
let cuDisabled = false

onServiceConnectionLost: () => {
  b = null
  if (cuDisabled) return
  if (++cuFailCount >= 3) {
    cuDisabled = true                                  // stop trying; surface a UI notice
    log.warn('Computer Use host unavailable; disabling for this session')
    return
  }
  setTimeout(te, Math.min(1000 * 2 ** cuFailCount, 30_000))
}

and reset cuFailCount = 0 on a successful connect.

Additionally:

  1. Terminate the previous helper before spawning a replacement. spawnComputerUseService should either reuse a live helper (computerUseServiceProcessMatchesExecutablePath already exists for this check) or kill the stale one. Today, helpers accumulate without bound and are never reaped — this is also the substance of #25744 and the zombie-process reports.
  2. Bound in-flight bootstraps. With a 15 s Apple Event timeout and a ~200 ms spawn cadence, up to ~75 bootstraps can be in flight simultaneously, each holding a blocked thread. The 64-thread dispatch soft limit is hit well before any of them time out.
  3. Do not run the bootstrap on com.apple.root.user-initiated-qos. Synchronously blocking global-queue threads on AESendMessage is what exhausts the dispatch pool.

Workaround (verified)

Making the helper bundle non-executable stops the loop entirely:

chmod 000 "$HOME/.codex/computer-use/Codex Computer Use.app"

Result — measured over a continuous 14.5 h run:

before after
Helper processes 199 and climbing 0
Main-process threads 309 @ 2 min 63, flat
App memory 12.5 GB and climbing 347 MB, flat
Stability crash @ ~2 min no crash in 14.5 h

Computer Use is unavailable, but every other feature works normally. Reverse with chmod 755 on the same path.

Deleting or renaming the helper bundle does not work — the app re-provisions an identical copy on next launch.


Possibly related: idle renderer memory growth

Likely a separate defect, noted here because it appears in the same reports. On a clean launch with nothing opened, the app starts 7 renderers. Four of them (renderer-client-id 7–10, created within 36 ms of each other and identical in size) grow steadily while completely idle:

renderer at launch after 14.5 h growth
#5 (main UI) 304 MB 346 MB +14%
#6 182 MB 263 MB +45%
#7 102 MB 1428 MB +1300%
#8 101 MB 1426 MB +1300%
#9 102 MB 1425 MB +1300%
#10 100 MB 1424 MB +1300%

Measured rate ≈ 320 MB/h per renderer (16 MB per 3 min, sampled). Growth is in app-specific tag 14 (JS heap): 1145 MB → 1373 MB, with region count rising 4385 → 4754.

Notably these four processes accumulated only 2.5–9.3 seconds of CPU time over 14.5 hours, so whatever is allocating is doing almost no work. Total app footprint reached 7.67 GB; a restart brings it back to 1.45 GB. On a 16 GB machine this drove swap to 7.74 GB of 8 GB.

The renderers that are actually in use are fine — the leak is in the four idle pre-warmed ones.


Related issues


Contact

Happy to provide the full .ips crash reports, log stream captures, disassembly, or to run any additional diagnostics on request — just ask here.


Notes on this machine's configuration

For completeness: this machine uses a customized ~/.codex/config.toml (custom openai_base_url pointing at a local router, custom model catalog, sandbox_mode = "danger-full-access", 6 MCP servers), and ~/.codex is a symlink to an external volume. The failure path above is entirely within the Computer Use PiP bootstrap and is independent of model routing; the identical crash signature has been reported by users on stock configurations.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the minified callback in Contents/Resources/app.asar and trace ko/startRemoteHostedPIPContentHost, te(), and spawnComputerUseService. Inspect sky.node's failure callback and the existing computerUseServiceProcessMatchesExecutablePath check. Done means failed bootstraps are bounded, stale helpers are reaped or reused, and the app no longer accumulates processes and blocked threads during handshake failure.

Written by the indexing model from the issue text.

Assessment

Tech stack
electron, javascript, macos
Domain
desktop, performance
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.