Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save galligan/7fdeeed33b3e282dc3afa9fcde7be470 to your computer and use it in GitHub Desktop.

Select an option

Save galligan/7fdeeed33b3e282dc3afa9fcde7be470 to your computer and use it in GitHub Desktop.
ChatGPT for macOS 26.810.x — Computer Use spawn storm: field report (2 Macs, measurements, what we tried, build comparison)

ChatGPT for macOS 26.810.x — Computer Use spawn storm: field report

⚠️ UPDATE 2026-08-20 — root cause found; two corrections to this document

The trigger is a symlinked codex home. The spawn loop needs both an uncapped retry loop in the app and ~/.codex (or CODEX_HOME) reached through a symlink. See openai/codex#39732, independently replicated here on a known-bad build: symlinked → 73 helpers in 27s; resolved real path → 1 helper, flat for 903s. Same binary, same machine, one variable.

Correction 1: the "watch the Computer Use runtime version" signal proposed below is wrong. The runtime moved to 26.817.1000761 and builds still stormed — the defect is in the Electron main process, not the helper.

Correction 2: downgrading is no longer the recommended fix. Canonicalizing the codex home path fixes it on current builds. This machine now runs 26.818.22352 with Computer History enabled — 1 helper, flat, zero crashes.

Full write-up of the investigation, including every wrong turn: https://gist.github.com/galligan/c8a64ec15b89e11ac0421b1b117056c0

The measurements below remain accurate as recorded; treat the recommendations as superseded.

ChatGPT for macOS 26.810.x — Computer Use spawn storm: field report

Independent investigation across two Macs, 2026-08-14 → 2026-08-16. Written for the ChatGPT/Codex desktop team as a record of what we observed and what we tried, with the measurements behind each claim.

Upstream thread: openai/codex#38455

Everything below was measured on our own machines unless explicitly attributed to another reporter. Where we are inferring rather than measuring, we say so.


Summary

On 26.810.x, the app spawns SkyComputerUseService helper processes in an unbounded retry loop from launch, while idle and without Computer Use being invoked. Each retry also creates a Node worker thread and a V8 isolate inside the main app process. At roughly 600 concurrent helpers — about 4 minutes after launch — the app fatally aborts while creating another isolate.

The behavior is independent of user configuration. We were unable to stop it from outside the app binary; the only state in which we could keep working was on a build predating the regression.


Environment

Machine A Machine B
Model Mac Studio, Apple Silicon Mac mini, Apple Silicon
macOS 26.6 (25G70), later 27.0 beta (26A5406e) 27.0 beta (26A5406e)
App at onset 26.810.41047 (build 6570) 26.810.41047 (build 6570)
Codex Framework 151.0.7922.137 151.0.7922.137
Embedded codex-cli 0.148.0-alpha.9 0.148.0-alpha.9
Computer Use runtime 26.812.1000717 not captured before rollback

Machine B hit the same failure independently the same day (2026-08-14), and the same rollback resolved it. The two onset installs are not merely the same version string — sky.node on Machine B is 1210752 bytes hashing 1e468ee72cc1e402…, byte-identical to Machine A's 41047. Machine B's Computer Use runtime version was lost when the rollback re-provisioned it, so that one cell is genuinely unknown rather than assumed to match.

Scope of Machine B's evidence. We did not re-measure the spawn storm on Machine B. The app had already crashed and was not running when we started, and reproducing would have cost another ~600 spawned processes for information Machine A had already established. Machine B was identified by version match plus crash-signature match, then rolled back and monitored. Its value here is as an independent confirmation of the crash signature, the rollback, and the Sparkle re-enable below — not as a second measurement of spawn rate.

macOS version is not a variable. Machine A crossed from 26.6 to 27.0 beta mid-investigation: the first three crash reports are stamped macOS 26.6, the remainder macOS 27.0, with no change in spawn rate, accumulation pattern, or crash threshold. We have not seen 27.0 reported elsewhere in the thread.


What we measured

  • Spawn rate ~2/sec from launch while idle, accelerating to ~6/sec as the count grows.
  • Helpers stay alive and accumulate — process start times are evenly spread across the window. This is live accumulation, not churn and not zombie reaping failure.
  • App thread count grows ~1:1 with spawns (74 → 169 threads as helpers went 6 → 111).
  • Crash at ~600–682 concurrent helpers, ~4–4.5 minutes after launch. 7 of 7 crashes in one afternoon fell in that band; the tightness of that clustering is what first suggested a hard resource limit rather than a heuristic.

Representative sequence from one launch (15s sampling):

13:28:00  helpers=8     13:29:33  helpers=198
13:28:15  helpers=40    13:30:18  helpers=342
13:28:30  helpers=71    13:30:48  helpers=521
13:28:46  helpers=103   13:31:04  helpers=607
13:29:01  helpers=133   13:31:19  app crashed (682)

Crash signature

EXC_BREAKPOINT (SIGTRAP), in two of seven cases SIGABRT, always on a thread named computer-use. Symbolicated bottom of the crashing stack:

node::worker::Worker::Run()
  → node::NewIsolate()
    → v8::Isolate::Initialize()
      → EXC_BREAKPOINT

The fatal abort lands on isolate creation. This is a slightly different presentation from the node::OOMErrorHandler path reported by others in #38455, but the same resource-exhaustion family — consistent with the thread-count growth above, and with the launchservicesd 512-dispatch-thread exhaustion and kernel panics reported independently in #38760 and #38697.

The abort site varies; the thread does not. Both of Machine B's crashes (12:36:52 and 17:03:29 local, 2026-08-14) were EXC_CRASH (SIGABRT), and neither landed on NewIsolate. They aborted further up, inside the worker's message loop:

node::worker::Worker::Run()
  → node::SpinEventLoopInternal()
    → uv_run() → uv__io_poll() → uv__async_fork()
      → node::worker::MessagePort::OnMessage()
        → node::AsyncWrap::MakeCallback()
          → node::InternalMakeCallback()
            → v8::Function::Call()
              → SIGABRT

So across nine crashes on two machines we have three presentations — SIGTRAP on Isolate::Initialize, SIGABRT on Isolate::Initialize, and SIGABRT inside MessagePort::OnMessage — with node::worker::Worker::Run() at the bottom and a thread named computer-use in all nine. If you are triaging by exception type or by top frame you will split one bug into three. The thread name is the stable discriminator.


What we tried, and what happened

Each item was applied, then re-tested with a clean relaunch and process monitoring. None changed the spawn rate measurably. We are listing these mainly so the team can rule out user configuration as a factor.

Attempt Result
tccutil reset Accessibility for app + helper bundle IDs; granted the Accessibility prompt when it then appeared; re-added "Codex Computer Use" to Screen Recording No change. Our initial hypothesis was a stale TCC record after reinstall — one machine had shown the Computer History permission dialog and the other never did. Permissions turned out to be unrelated to the crash.
Computer Use toggled off in Settings No change
Computer History toggled off in Settings No change
[features] computer_use = false in ~/.codex/config.toml No change to spawning — though the app clearly read it: on next launch it removed its own [mcp_servers.computer-use] and [plugins."computer-use@openai-bundled"] entries. Worth noting as a UX inconsistency: the setting is honored in config bookkeeping but not in the spawner.
[desktop] computerUseAlwaysHidePictureInPicture = true No change. We tried this because it resolved the milder 26.730.61639 PiP respawn variant reported on the community forum on Aug 5.
Deleted ~/.codex/computer-use/ No change — the runtime was re-provisioned within seconds of launch and spawning resumed.
Clean room: killed every codex-family process system-wide (app, app-server, helpers, orphans), unloaded unrelated launchd agents, verified zero, relaunched No change — 4 → 101 helpers in 60 seconds. This is the result we'd most highlight: the loop requires no state or process outside the app binary.
Killed accumulated helpers on a 20s interval while the app ran This did prevent the crash. Over a 10.5-minute run the helper count stayed capped at 25–37, and the app's thread count plateaued around 105 then declined to ~90. The same machine died in ~4 minutes without it.

That last row may be the most useful diagnostic signal here: the worker threads are released when their child processes die. They are not orphaned. A fix that merely bounds the spawn loop — or that reaps on handshake failure — should reclaim them without further plumbing.

We ran that reaping loop as a stopgap for a while. It costs roughly 20–50% of one core in permanent churn, so it isn't something we'd want to keep running, and given the kernel-panic reports elsewhere we stopped relying on it.


Build comparison across the 26.810 line

Three builds shipped in ~36 hours (41047 Aug 13, 50856 Aug 14, 52044 Aug 15), then the cadence stopped. Comparing the first and last locally:

26.810.41047 26.810.52044
Contents/Resources/native/sky.node 1210752 bytes, 1e468ee72cc1e402… 1210752 bytes, b6248745c81c7013…
embedded codex-cli 0.148.0-alpha.9 0.148.0-alpha.9
Computer Use runtime 26.812.1000717 26.812.1000717

The native module was rebuilt — same size, different hash — while the embedded CLI and the Computer Use runtime are unchanged. That matches every user report that 50856 and 52044 remain affected. As an outside observer, the Computer Use runtime version is the only field we found that would distinguish a real fix from a rebuild.


An operational note: the update pin does not persist

We rolled back and set:

defaults write com.openai.codex SUEnableAutomaticChecks -bool false
defaults write com.openai.codex SUAutomaticallyUpdate -bool false

Re-checking two days later: both keys had been reset to 1, SULastCheckTime showed a fresh check, and 26.810.52044 was fully downloaded (1.9 GB) and staged in ~/Library/Caches/com.openai.codex/org.sparkle-project.Sparkle/Installation/, ready to install on next quit. Only 39 hours of uninterrupted app uptime had kept it from landing.

We mention it because it interacts badly with the severity of this particular bug: users who believe they have pinned themselves to a safe build may be silently re-updated into one that, per #38760 and #38697, can panic the machine. The app rewriting its own preferences domain is presumably intentional; the interaction with a known-bad release is the concern.

This reproduced independently on Machine B, and faster. Both keys were written false at 17:15 local on 2026-08-14, immediately after that machine's rollback. Re-checking at 13:40 local on 2026-08-16:

Machine A Machine B
SUEnableAutomaticChecks reset to 1 reset to 1
SUAutomaticallyUpdate reset to 1 reset to 1
Time to reset ~2 days ≤23 h (SULastCheckTime = 2026-08-15 16:35:53 UTC)
Staged build 26.810.52044, 1.9 GB 26.810.52044, 1.9 GB
What held it off 39 h app uptime 26 h app uptime

Two details from Machine B worth having:

Only the app's own domain is rewritten. We set the keys in both com.openai.codex and com.openai.chat as a belt-and-braces measure. com.openai.codex — the actual CFBundleIdentifier of the shipping app — came back as 1/1. com.openai.chat was still 0/0, untouched. So this is the running app rewriting its own preferences, not an external agent or a stale domain, which narrows where a fix would go.

The staged payload is identical. Same version, same 1.9 GB, same Installation/<id>/<id>.810.52044.zip layout on both machines. Machine B was pinned and offline from the updater by intent, and Sparkle still fetched and unpacked a full copy of a build that the same document identifies as affected.

The practical shape of this for users: on both machines the only thing that kept a staged, known-affected build from landing was that the app never stopped running. We did not test whether an abnormal termination triggers the staged install the way a clean quit does — that is the open question we'd most like answered, because on 26.810 abnormal termination is the app's steady state.


Where we ended up

Rolled back to 26.727.51351 on both machines — the last build we could find that predates this bug class, and specifically earlier than 26.730.61639, where the PiP respawn behavior first appears publicly. (Some rollback advice in the thread points at 26.803.x; that line has its own crash-on-launch report in #38037, which is why we went further back.)

Result: 39+ hours uptime, one helper process, thread count flat at ~66, zero crashes. Computer Use functions normally there, and the app re-provisioned a version-matched runtime (26.727.1000550) on its own.

Machine B landed in the same place from an independent rollback: 26+ hours uptime, SkyComputerUseService flat at 1, thread count 62–73 across a 2-minute sampling window with no upward trend, zero crash reports since. It re-provisioned the identical runtime (26.727.1000550) unprompted, which is a small point in favor of that runtime version being the meaningful field to watch.

For the sampling window immediately after the swap (15s interval):

17:11:51  sky=0  threads=64      17:13:06  sky=1  threads=62
17:12:06  sky=1  threads=65      17:13:21  sky=1  threads=62
17:12:21  sky=1  threads=65      17:13:36  sky=1  threads=73
17:12:36  sky=1  threads=62      17:13:52  sky=1  threads=66
17:12:51  sky=1  threads=64

Compare against the 26.810 sequence in What we measured: same sampling interval, same machine class, 607 helpers by the four-minute mark there versus 1 here.

This is a holding position, not a resolution — it costs us everything shipped since late July, including Computer History.

Reproducing the rollback

For anyone else who needs to get to this state, 26.727.51351 is still served from OpenAI's own CDN and is notarized, so it installs without any Gatekeeper override:

https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-arm64-26.727.51351.zip
https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-x64-26.727.51351.zip

SHA-256, cross-checked against Homebrew's audited cask history (commit a1ac4f8212) and re-verified on download:

arm64  8f3fc87e634332fddc711e5221eb2af554f5f6ecb04e6a69b3d10e01f4f196c8
x64    94120e6ff4f0da1224283cf90bdc51f3c352c0538c617f68747c39fcf6ef8ce7

spctl -a -vv on the extracted bundle returns accepted, source=Notarized Developer ID, Developer ID Application: OpenAI OpCo, LLC (2DC432GLL2). Keep the outgoing bundle rather than deleting it — having 26.810.41047 on disk is what let us do the byte-level sky.node comparison above after the fact.


Separate, much milder issue on the older build

Worth flagging since it's adjacent and long-lived. On 26.727.x, one SkyComputerUseClient — plus one instance of every configured stdio MCP server — is stranded per idle conversation, at roughly 0.25–0.5/hour, each consuming ~1–1.3% of a core indefinitely.

Reading the open-source tree: MCP servers are spawned per conversation thread, and teardown runs via McpConnectionSet::shutdown on session shutdown. thread_lifecycle.rs does implement idle unloading (THREAD_UNLOADING_DELAY, 30 min; PRs #17223 and #17398) — but it is gated on has_subscribers == false. Our observation is that the desktop client holds one long-lived app-server connection and does not appear to unsubscribe idle conversations, so that path never runs. The supporting evidence is negative but clean: the unload path logs "has no subscribers and is idle; shutting down", and that string appears zero times across our entire ~/Library/Logs/com.openai.codex/2026/** history, while the accumulation is demonstrably live. Umbrella issue: #25744.

Killing the stranded children doesn't help — the still-live session respawns replacements 1:1, which is consistent with them being supervised rather than leaked.

Machine B: accumulation is conversation-driven, not time-driven

Machine B let us separate those two variables, because it ran 26 hours on 26.727.51351 with almost no interactive use. At the 26-hour mark:

SkyComputerUseService   1
SkyComputerUseClient    3     elapsed 26:07:30, 26:06:07, 26:04:19   (0.8–1.0% CPU each)
node_repl               3     the one remaining enabled stdio MCP server

Every stranded client's elapsed time puts its start within ~3 minutes of app launch, and the count did not move over the following 26 hours. So the per-hour framing above ("0.25–0.5/hour") is better read as a consequence of how often conversations get opened than as a clock-driven leak. An idle app does not accumulate; an app that opens conversations does. That is consistent with the thread_lifecycle.rs reading — the unit of leakage is a conversation thread that never loses its last subscriber — and it means the practical severity depends entirely on usage pattern, which may explain the spread in reports.

The stdio multiplier is real, and npx doubles it

Machine B's codex config had three enabled stdio MCP servers at onset (context7, firecrawl, node_repl; chrome-devtools and computer-use were present but enabled = false, and deepwiki is a remote url entry). Removing the two removable ones dropped the stranded MCP process count to exactly conversations × remaining stdio servers — 3 above — with the remote entry contributing nothing. The relationship is linear and the remote-vs-stdio distinction holds.

One wrinkle for anyone estimating blast radius: an MCP server configured as command = "npx" costs two processes per conversation, not one — the npm exec wrapper plus the actual server it execs:

npm exec @upstash/context7-mcp   ← wrapper, stays resident
node .../node_modules/.bin/context7-mcp

So a user with four npx-configured stdio servers strands eight processes per conversation plus the client, not four. Converting entries with an HTTP endpoint to remote url = form removes them from the multiplier entirely; that is the only user-side mitigation we found that doesn't involve restarting the app on a schedule. It reduces the per-conversation cost — it does not stop conversations from being stranded, and the SkyComputerUseClient count still grows.


Open questions we couldn't answer from outside

  1. Is the ~600 ceiling the launchservicesd 512-dispatch-thread limit surfacing as a V8 isolate-allocation failure, or two independent limits reached at similar times? The kernel-panic reports suggest the former.
  2. Does the retry path have any failure counter at all? From the outside the spawn rate never decays, which is what #38841 reports finding in the JS layer.
  3. Is there a supported way to pin a desktop version, for users who need to stay off a bad build? (#38022 requests this.) Today it requires fighting the updater.
  4. Does an abnormal termination install a staged Sparkle update, or only a clean quit? Both machines had a known-affected build staged and unlaunched purely because the app stayed up. If crashes do trigger installation, the pin is not merely fragile for this bug class — it is specifically defeated by it.
  5. Why does the abort site move between Isolate::Initialize and MessagePort::OnMessage while the thread name stays constant? If both are the same allocation failure surfacing at different points, the nine crashes are one bug; if not, the computer-use thread has a second failure mode worth separating.

Related issues

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment