Skip to content

Codex for macOS accumulates Computer Use / MCP helper processes and unreaped zombie children, causing HID lag and WindowServer/TCC stalls #25744

Open

Description

@quasa0

Codex macOS Resource Leak / Unreaped Child Processes Report

GitHub Issue Draft

Title

Codex for macOS accumulates Computer Use / MCP helper processes and unreaped zombie children, causing HID lag and WindowServer/TCC stalls

Summary

On macOS, a long-running Codex session accumulated a large number of Codex-owned helper processes related to Computer Use / MCP tooling. The machine became visibly laggy. Inspection showed:

  • Dozens of live SkyComputerUseClient mcp processes.
  • Dozens of live node_repl processes.
  • Dozens of live codex app-server --listen stdio:// helper processes.
  • More than 100 zombie child processes whose parent was the main Codex GUI process.
  • macOS spin reports for Codex showing slow HID event handling, with WindowServer blocked in the TCC event-monitor permission path.

Manual cleanup of stale helper processes reduced live Codex-related memory use by roughly 3 GB, but the zombie processes could not be cleared without restarting Codex because the main Codex process did not reap them.

Related Issues

This may share root cause with MCP stdio lifecycle leaks such as #17832, but this report adds a separate Computer Use case with SkyComputerUseClient accumulation, Codex-owned zombie children, and macOS WindowServer/TCC HID stalls. It does not look like the SkyComputerUseClient code-signing crash in #21200 or the simple unexpected-exit case in #18549.

Environment

Sanitized system details:

  • App: Codex for macOS
  • Codex version: 26.527.31326
  • Codex bundle version: 3390
  • Codex bundle ID: com.openai.codex
  • Chromium base: 148.0.7778.179
  • Computer Use app bundle ID: com.openai.sky.CUAService
  • Computer Use app version: 1.0
  • Computer Use build: 799
  • Computer Use client bundle ID: com.openai.sky.CUAService.cli
  • Computer Use client version/build: 1.0 (799)
  • OS: macOS 26.5 build 25F5042g
  • Architecture: Apple Silicon / arm64
  • CPU: Apple M-series Pro class, 14 logical CPUs
  • Memory: 24 GB
  • Thermal state: no thermal or performance warning recorded during sampling

Privacy redactions applied:

  • User home path replaced with <HOME>.
  • Host/device name redacted.
  • Project/workspace paths omitted.
  • Unrelated app/process names omitted unless directly relevant.
  • Raw spin report should not be attached unless separately redacted; it contains a full process inventory.

Observed Behavior

The main Codex process stayed alive for approximately 16 hours. During that period, helper processes accumulated rather than being cleaned up.

Initial process counts observed:

SkyComputerUseClient       count=50   cpu=18.0%   mem=4.7%   rss=1448.1 MB
node_repl                  count=47   cpu=0.0%    mem=0.0%   rss=405.3 MB
codex app-server stdio     count=47   cpu=3.2%    mem=6.3%   rss=1715.4 MB
Codex app/server/renderers count=several, additional RSS/CPU

Aggregate Codex-related footprint before cleanup:

codex_related count=143-144
codex_related RSS ~= 4.8-4.9 GB
codex_related CPU ~= 28-70% depending on sample

Zombie processes:

zombies=126
parent=<codex-main-pid> zombies=126

After killing stale helper processes, the zombie count increased slightly rather than disappearing:

zombies=128
parent=<codex-main-pid> zombies=128

That strongly suggests the main Codex process is not reaping exited child processes.

System Symptoms

The machine was visibly laggy. System-level sampling showed:

Load Avg: 8.12 6.57 6.23
Processes: 980 total
Threads: 6301
CPU usage: 13.70% user, 22.98% sys, 63.30% idle
PhysMem: 22 GB used, 1.3 GB unused
Compressor: ~5.5 GB
Memory pressure: not critical
Thermal warnings: none

The strongest live CPU consumers during the bad state included:

trustd        ~90-100% CPU in multiple samples
syspolicyd    ~20-60% CPU in multiple samples
WindowServer  ~40-50% CPU in multiple samples
Codex GPU     ~8-40% CPU depending on sample
Codex renderer/app server also periodically high

After helper cleanup, trustd and syspolicyd dropped back to low single-digit CPU in later samples. This suggests the large helper-process population may have been amplifying code-signing/trust/TCC work.

macOS Spin Report Evidence

macOS generated at least two Codex-related spin reports during the session.

Recent spin report summary, sanitized:

Date/Time:        2026-05-31 17:51:29 local time
OS Version:       macOS 26.5 (Build 25F5042g)
Reason:           Slow response to HID event
Command:          WindowServer
Affected Process: Codex [<codex-main-pid>]
Event:            spin
Duration:         2.98s
Duration Sampled: 2.77s
Steps:            277 (10ms sampling interval)
Report threshold: 0.5s
Active CPUs:      14
Memory size:      24 GB
Memory pressure:  average 40%, highest 42%
Available memory: average 14.26 GB, lowest 13.75 GB
HID Events:       LeftMouseDown*, LeftMouseUp

The heaviest stack for the target process was in WindowServer waiting on TCC event-monitor access checks:

WindowServer
  SLXServer
  server_loop
  CGXRunOneServicesPass
  post_port_data
  rendezvousHandler
  CGXHandleMessageWithResponseBuffer
  _XGetEventShmem
  WSEventSourceCreateGraphShmemForAuditToken
  WSAuditTokenCanMonitorEvents
  WSAuditTokenHoldsCachedProcessRight
  audit_token_check_tcc_access
  TCCAccessPreflightWithAuditToken
  TCCAccessRequest
  tccd_send_message
  xpc_connection_send_message_with_reply_sync
  dispatch_mach_send_with_result_and_wait_for_reply
  mach_msg2_trap

The slow HID event line:

CGXGetNextEvent  1-274  3000.000ms  WindowServer thread

There was another earlier Codex spin report the same day:

Affected Process: Codex
Event: spin
Duration: 1.28s
WindowServer stack again included kTCCServiceListenEvent / TCC preflight path

Nearby TCC logs also showed repeated privacy preflights for screen/event access by a non-Codex screen/window utility. That may have amplified the symptom, but it does not explain the Codex-owned process leak or the Codex-owned zombies.

Cleanup Experiment

The session was kept alive by not killing:

  • The main Codex GUI process.
  • The main Codex codex app-server --analytics-default-enabled process.

Stale helpers were terminated carefully:

Killed 40 stale SkyComputerUseClient mcp processes.
Killed 80 stale node_repl / codex app-server --listen stdio:// helper processes.
Killed 1 stale SkyComputerUseService process.
Killed several old orphaned Codex crash/keyboard helper processes from previous app launches.

After cleanup:

codex_related count ~= 15-18
codex_related RSS ~= 1.8-1.9 GB
SkyComputerUseClient herd gone
node_repl / stdio helper herd gone
trustd / syspolicyd CPU substantially lower
zombies still present under main Codex process

A single fresh helper triad later respawned:

node_repl
SkyComputerUseClient mcp
codex app-server --listen stdio://

That may be the active/current session helper set. The bug is not that one helper set exists; the bug is that many old helper sets accumulated and were not cleaned up.

Expected Behavior

Codex should:

  • Keep at most the necessary active helper processes for currently active tools/sessions.
  • Tear down stale SkyComputerUseClient, node_repl, and stdio codex app-server helpers after a tool/session completes.
  • Reap exited child processes so zombie children do not accumulate.
  • Avoid triggering system-wide TCC/trust/code-signing churn due to stale helper population.
  • Avoid causing WindowServer HID stalls when Computer Use/event-monitor permissions are involved.

Actual Behavior

Codex accumulated:

  • Approximately 50 Computer Use client processes.
  • Approximately 47 node_repl processes.
  • Approximately 47 stdio codex app-server children.
  • 126+ zombie children owned by the main Codex process.

The system became laggy and macOS generated Codex spin reports involving WindowServer/TCC event-monitor checks.

Repro / Diagnostic Commands

These are safe commands to run locally. Redact output before posting publicly.

Count Codex-related helpers:

ps -axo pid,ppid,pgid,stat,%cpu,%mem,rss,etime,args |
  awk '/Codex|SkyComputerUse|node_repl|codex app-server/ {print}'

Aggregate Codex-related footprint:

ps -axo pid,ppid,pgid,stat,%cpu,%mem,rss,etime,args |
  awk '/Codex|SkyComputerUse|node_repl|codex app-server/ {
    cpu += $5; mem += $6; rss += $7; n++
  }
  END {
    printf "codex_related count=%d cpu=%.1f mem%%=%.1f rssMB=%.1f\n",
      n, cpu, mem, rss/1024
  }'

Count zombie children by parent:

ps -axo stat,ppid |
  awk '$1 ~ /^Z/ { z++; p[$2]++ }
  END {
    print "zombies=" z+0
    for (ppid in p) print "parent", ppid, "zombies", p[ppid]
  }'

Inspect top CPU processes:

ps -axo pid,ppid,%cpu,%mem,rss,etime,args |
  sort -nrk3 |
  head -30

Sanitize paths before sharing:

sed -E 's#/Users/[^/]+#<HOME>#g; s#[[:alnum:]_.-]+_2026-[0-9-]+-[0-9]+_[^./]+\\.spin#<redacted-spin-file>.spin#g'

Workaround

The clean workaround is to quit and reopen Codex. That should clear:

  • Live stale helpers.
  • Zombie processes owned by the old Codex parent.

If keeping the current session alive is required, stale helper processes can be killed manually, but this does not clear zombies because the main Codex process still fails to reap them. Killing children may actually increase the zombie count until Codex is restarted.

Suggested Fix Area

Likely areas to inspect:

  • Lifecycle ownership for Computer Use MCP client processes.
  • Lifecycle ownership for per-session node_repl processes.
  • Lifecycle ownership for stdio codex app-server --listen stdio:// child processes.
  • Child-process reaping in the main Codex macOS process.
  • Cleanup path when a thread/tool session ends, errors, times out, or is superseded.
  • Interaction between Computer Use/event-monitoring permissions and TCC preflight calls.

Severity

High for long-running desktop sessions:

  • Causes visible system lag.
  • Accumulates multiple GB of resident memory.
  • Creates unreaped zombie children.
  • Triggers macOS spin reports.
  • Involves WindowServer/TCC HID event stalls, so user input latency is affected.

Notes For Maintainers

I can provide raw spin reports privately if needed, but they contain a full process inventory and local paths. The issue body above intentionally omits those details while preserving the relevant stack, counts, versions, and cleanup experiment.

Activity

  1. added
    bugSomething isn't working
    appIssues related to the Codex desktop app
    mcpIssues related to the use of model context protocol (MCP) servers
    app-serverIssues involving app server protocol or interfaces
    on Jun 2, 2026
  2. quasa0 commented on Jun 2, 2026

    @quasa0
    Author

    Reviewed the suggested duplicates. I do not think this is an exact duplicate.

    Closest is #25719 because it also involves macOS syspolicyd / trustd pressure around Codex Desktop / Computer Use. This issue adds separate lifecycle evidence: accumulated SkyComputerUseClient processes, duplicate node_repl / codex app-server --listen stdio:// helpers, 126+ Codex-owned zombie children, and WindowServer/TCC HID spin reports.

    The Windows/Linux MCP issues (#25697, #24347, #25742) look related at the lifecycle class level, but this report is specifically macOS Codex Desktop + Computer Use + unreaped children. #25667 is also adjacent, but focuses on code_sign_clone temp dirs after quit.

  3. johnsilvavlogs commented on Jun 7, 2026

    @johnsilvavlogs

    Adding current-build evidence from another macOS machine. This still reproduces on the current Sparkle appcast build and looks like the same lifecycle/perf class as this issue, with one extra signal: repeated AppServerConnection initialize/reconnect failures while Computer Use MCP helpers fan out during ordinary tool discovery.

    Environment:

    • Codex Desktop: 26.602.40724, bundle build 3593
    • Appcast top item verified as the same build: 26.602.40724 / 3593, published Fri, 05 Jun 2026 17:00:22 +0000
    • Global CLI: codex-cli 0.135.0
    • Computer Use plugin: computer-use@openai-bundled 1.0.809
    • Computer Use helper bundle: com.openai.sky.CUAService, version/build 1.0 (809)
    • macOS: 26.3 build 25D125, arm64
    • Hardware: Apple Silicon MacBook Pro, M1 Max, 64 GB memory

    Negative checks:

    • Codex app is current; not a stale install.
    • Computer Use plugin is present and enabled in ~/.codex/config.toml.
    • SKY_CUA_SERVICE_PATH points at <HOME>/.codex/plugins/cache/openai-bundled/computer-use/1.0.809/Codex Computer Use.app.
    • Computer Use helper code signature verifies cleanly.
    • state_5.sqlite and logs_2.sqlite both pass pragma integrity_check.
    • Memory pressure was low: 0 swap used, no pageouts/swapouts in the sampled window.
    • Current system TCC DB has Screen Recording and Accessibility allowed for both com.openai.codex and com.openai.sky.CUAService.

    Current process inventory, without restarting/killing Codex:

    main codex app-server:
      PID 55044, parent Codex GUI PID 54986, ~36% CPU, ~614 MB RSS, uptime ~1h15m
    
    Codex-owned helper fan-out:
      7 x SkyComputerUseClient mcp
      7 x node_repl
      7 x codex app-server --listen stdio://
      14 x node ./mcp/server.{cjs,mjs}
    

    The oldest Computer Use MCP/helper family was ~71 minutes old; two more were ~35-36 minutes old; newer helper families appeared during this diagnostic even though no Computer Use desktop action was invoked. That suggests the Computer Use MCP process is being started as part of tool listing/session setup, not only as part of actual Computer Use actions.

    High-signal log evidence:

    ~/Library/Logs/com.openai.codex/2026/06/07/codex-desktop-478b4760-97d6-4bdf-b1dc-7dc5f6179de3-54986-t0-i1-004733-0.log
    
    L13129-L13135:
      Initialize handshake still pending
      connectionError={"code":"connection-failed","message":"Codex app-server initialize handshake timed out"}
      reconnectAttempt=151
      app_server_connection.fail cause=initialize_handshake_timeout
      app_server_connection.state_changed cause=transport_closed
      initialize_handshake_result outcome=failure
    
    L13583-L13588:
      same failure again at reconnectAttempt=152
    

    Earlier in the same long-running app session:

    ~/Library/Logs/com.openai.codex/2026/06/06/codex-desktop-30fe6ed6-63f9-405c-aa0b-80661ad44790-49672-t0-i1-000000-1.log
    
    L6836-L6842:
      screen recording reconcile failed kind=PermissionRequired state=Unsupported
      error_message="No shareable content available: Content unavailable: The user declined TCCs for application, window, display capture"
      followed immediately by reconnectAttempt=101 and initialize_handshake_timeout / transport_closed
    
    L8235-L8240:
      IAB_LIFECYCLE route window is not live
      browser use route window missing for cursor/active state
    

    There is also a Computer Use MCP protocol-compat warning in the SQLite logs:

    2026-06-06 15:40:20 codex_mcp::connection_manager
      Failed to list resources for MCP server 'computer-use':
      Mcp error: -32601: Method not found: Unknown method: resources/list
    
    2026-06-06 15:40:20 codex_mcp::connection_manager
      Failed to list resource templates for MCP server 'computer-use':
      Mcp error: -32601: Method not found: Unknown method: resources/templates/list
    

    I think the user-visible symptom is "Computer Use feels flaky", but the local root-cause shape looks broader:

    1. Long-running Codex Desktop accumulates per-turn MCP/helper families.
    2. Computer Use MCP helpers are spawned during ordinary tool discovery, so unrelated work increases Computer Use helper count.
    3. AppServerConnection is stuck in repeated initialize handshake timeout / transport closed loops.
    4. Browser Use route state and screen-capture reconciliation also show lifecycle/TCC-adjacent failures, which makes desktop/browser-control surfaces feel unreliable even when the Computer Use plugin is installed and current.

    This does not look like the plugin-missing issues (#18258, #21579, #26429) on this machine. It looks closer to this issue plus the app-server/MCP timeout family.

  4. 0xdevalias commented on Jun 11, 2026

    @0xdevalias

    Feedback ID / Bug Report ID: 019eb57d-d318-78f2-8c45-76dea130046d


    A brief summary of what seems to be related issues that I just noticed due to my system being at its process limit, so failing to even run programs anymore:

    Codex 26.527.31326 (3390)
    macOS 26.3 (25D125), Darwin 25.3.0 arm64
    main Codex GUI PID 19100
    ~9,940 unreaped zombie children under PID 19100
    MYUSERNAME process count 10,581–10,656 / kern.maxprocperuid 10,666
    zsh fork failed: resource temporarily unavailable
    kill -CHLD did not cause Codex to reap children
    manual helper kills did not solve it
    updating/restarting to 26.608.12217 cleared zombies to 0
    

    The most important maintainer-facing points (according to Codex, GPT 5.5 (high)) are:

    • nearly 10,000 zombies were direct children of the main Codex GUI process;
    • the leak accumulated across an approximately 11-day app lifetime;
    • it exhausted kern.maxprocperuid and prevented all ordinary process spawning;
    • killing live helpers did not release the zombies;
    • SIGCHLD and SIGCONT+SIGCHLD did not trigger reaping;
    • old-parent exit immediately cleared the table;
    • restart cleanup does not yet prove the new version prevents recurrence.

    Summary of seemingly related issues by Codex (GPT 5.5, high) while reviewing this:

    Other potentially useful/relevant threads for maybe more context:


    Full overview / summary of everything tried/explored as per Codex (GPT 5.5, high):

    Codex Desktop zombie-process accumulation and macOS process-table exhaustion

    Executive summary

    On June 11, 2026, a long-running Codex Desktop process on macOS had accumulated
    approximately 9,940 unreaped zombie children. The zombies consumed nearly all
    of the per-user process-table allowance:

    user processes:       10,581-10,656
    kern.maxprocperuid:   10,666
    Codex-owned zombies:  ~9,936-9,949
    

    This directly caused ordinary process creation to fail:

    zsh: fork failed: resource temporarily unavailable
    

    The evidence establishes that:

    1. The main Codex GUI process was the parent of almost all zombies.
    2. The zombies had accumulated throughout an approximately 11-day app lifetime.
    3. Killing dozens of live Codex helper processes recovered only a small number
      of process slots and did not remove the zombies.
    4. Sending SIGCHLD, including after SIGCONT, did not make Codex reap them.
    5. Updating Codex caused the old main process to exit; immediately afterward,
      the zombie count was zero.

    The evidence does not establish:

    • which executable each zombie had originally been, because macOS displayed
      the exited children only as <defunct>;
    • that the newer Codex release fixed the underlying leak, because the zero
      count was measured immediately after restart with almost no subsequent use;
    • that the live node_repl, Computer Use, or stdio app-server helpers were
      necessarily the zombie source, although their accumulation makes them
      plausible lifecycle-related contributors.

    This is best treated as a severe additional reproduction of
    openai/codex#25744, with a
    stronger demonstrated impact: complete per-user process-table exhaustion.

    Environment

    Public-safe environment details:

    Codex Desktop: 26.527.31326 (bundle build 3390)
    Updated to:    26.608.12217, released June 9, 2026
    macOS:         26.3 (25D125)
    Darwin:        25.3.0 arm64
    Architecture:  Apple Silicon
    

    Redactions used below:

    <USER>             local account name
    <HOST>             host/device name
    <HOME>             /Users/<USER>
    <OLD_CODEX_PID>    19100
    <NEW_CODEX_PID>    72552
    

    PIDs are retained where useful because they are ephemeral and essential for
    understanding parent-child relationships.

    Timeline and observations

    1. Initial process-limit diagnosis

    The first diagnostic set checked OS details, launchd/kernel limits, process
    counts by user, and process counts by executable.

    Command:

    sw_vers
    uname -a
    
    echo "launchctl limits:"
    launchctl limit maxproc
    launchctl limit maxfiles
    
    echo "kernel process/file limits:"
    sysctl kern.maxproc kern.maxprocperuid kern.maxfiles kern.maxfilesperproc
    
    echo "current process counts:"
    ps -axo user= | sort | uniq -c | sort -nr | head -20
    
    echo "top process names:"
    ps -axo comm= | sort | uniq -c | sort -nr | head -30

    Relevant output:

    ProductVersion: 26.3
    BuildVersion:   25D125
    Darwin 25.3.0 ... arm64
    
    maxproc                10666  16000
    kern.maxproc:          16000
    kern.maxprocperuid:    10666
    
    10656 <USER>
    
    9927 <defunct>
      75 /Applications/Codex.app/Contents/Resources/codex
      73 /Applications/Codex.app/Contents/Resources/node_repl
    

    Interpretation:

    • The account was only about 10 processes below kern.maxprocperuid.
    • Approximately 93% of the account's process-table entries were zombies.
    • This was process-table exhaustion, not primarily a low configured limit.

    2. Identification of the zombie parent

    Command:

    ps -axo ppid= | sort | uniq -c | sort -nr | head -20

    Relevant output:

    9946 19100
    

    The parent was then identified:

    ps -o pid,ppid,user,stat,etime,command -p 19100

    Output:

    PID    PPID  USER    STAT  ELAPSED       COMMAND
    19100  1     <USER>  S/R   10-23:27:xx  /Applications/Codex.app/Contents/MacOS/Codex
    

    Direct-child sampling:

    ps -axo pid,ppid,stat,etime,command \
      | awk '$2 == 19100 { print }' \
      | head -50

    Representative output:

    115  19100  Z       11:38:55     <defunct>
    140  19100  Z    02-04:25:15     <defunct>
    142  19100  Z       06:15:55     <defunct>
    152  19100  Z    10-21:47:02     <defunct>
    155  19100  Z    10-21:47:02     <defunct>
    ...
    

    The oldest sampled zombies were almost as old as the Codex process itself.
    This indicates gradual accumulation across the long-running app session rather
    than a single recent burst.

    At this point even a simple diagnostic pipeline failed:

    zsh: fork failed: resource temporarily unavailable
    [1] broken pipe
    

    3. Killing live helper processes did not solve the limit

    Dozens of live Codex-related backend/helper processes were manually terminated.
    The follow-up checks were:

    launchctl limit maxproc
    sysctl kern.maxprocperuid kern.maxproc
    
    ps -axo user= | sort | uniq -c | sort -nr | head -10
    ps -axo comm= | sort | uniq -c | sort -nr | head -20
    
    ps -axo stat= \
      | awk '$1 ~ /^Z/ { n++ } END { print n+0 }'
    
    ps -axo ppid=,stat= \
      | awk '$1 == 19100 && $2 ~ /^Z/ { n++ } END { print n+0 }'

    Relevant output:

    <USER> processes:         10581
    total <defunct>:          9949
    zombies under PID 19100: 9936
    

    Live helper counts had dropped substantially:

    13 node_repl
    13 SkyComputerUseClient
     7 SkyComputerUseService
    

    Interpretation:

    • Terminating live helpers recovered only approximately 75 process slots.
    • The account remained approximately 85 processes below the limit.
    • Zombie counts increased while live helper counts fell.
    • Existing zombies could be released only by the parent calling
      wait()/waitpid(), or by the parent exiting.

    The top-zombie-parent check made the concentration explicit:

    ps -axo pid=,ppid=,stat=,command= \
      | awk '$3 ~ /^Z/ { z[$2]++ } END { for (p in z) print z[p], p }' \
      | sort -nr \
      | head -20 \
      | while read count ppid; do
          printf "%6s zombies  PPID=%s  " "$count" "$ppid"
          ps -o pid=,ppid=,stat=,etime=,command= -p "$ppid" 2>/dev/null
        done

    Relevant output:

    9936 zombies  PPID=19100  ... /Applications/Codex.app/Contents/MacOS/Codex
       1 zombie   PPID=<node_repl PID> ...
       1 zombie   PPID=<node_repl PID> ...
    

    The few one-zombie node_repl parents were negligible compared with the
    approximately 9,936 zombies directly parented by the main GUI process.

    4. SIGCHLD did not trigger reaping

    Commands:

    kill -CHLD 19100
    
    ps -axo ppid=,stat= \
      | awk '$1 == 19100 && $2 ~ /^Z/ { n++ } END { print n+0 }'

    Output:

    9940
    

    Retry after continuing the process:

    kill -CONT 19100
    kill -CHLD 19100
    
    ps -axo ppid=,stat= \
      | awk '$1 == 19100 && $2 ~ /^Z/ { n++ } END { print n+0 }'

    Output:

    9942
    

    Interpretation:

    • Codex did not run a successful catch-up reap in response to SIGCHLD.
    • The count continued to rise slightly during diagnosis.
    • There was no reliable way to clear the existing zombies without making the
      main process exit or injecting reaping behavior into it.

    5. Final pre-restart snapshot

    The saved codex-zombie-process-snapshot.txt was captured later in the same
    bad state.

    Relevant output:

    maxproc:                 10666 soft / 16000 hard
    kern.maxprocperuid:      10666
    <USER> process count:    10614
    total <defunct>:         9962
    Codex parent PID:        19100
    Codex elapsed time:      10-23:53:52
    zombies under Codex:     9949
    

    The snapshot also showed live Codex-related helpers:

    13 node_repl
    13 SkyComputerUseClient
     8 SkyComputerUseService
    13 Codex renderer processes
    

    This is useful supporting context, but the zombie command names had already
    collapsed to <defunct>, so it cannot prove that any of these helper classes
    were the exited children represented by the zombie entries.

    6. Main-process sample

    Command:

    sample 19100 10 \
      -file ~/Desktop/codex-main-zombie-parent-sample.txt

    Sample metadata:

    Process:                  Codex [19100]
    Version:                  26.527.31326 (3390)
    Launch Time:              2026-05-31 16:39:52 +1000
    Sample Time:              2026-06-11 16:28:21 +1000
    Physical footprint:       10.6 GB
    Physical footprint peak:  11.3 GB
    

    The sampled main thread and many workers were predominantly waiting in
    event-loop primitives such as mach_msg2_trap, kevent, and uv_run.
    No waitpid, wait4, SIGCHLD, child-exit, or explicit reaping stack was
    captured.

    This sample is not proof of the missing code path: a 10-second statistical
    sample cannot prove that a function never runs. It does show that no active
    reaping loop was observed while nearly 10,000 zombies were pending.

    The 10.6 GB process footprint is independently notable, but zombies themselves
    do not account for that memory. Memory retention and child reaping should not
    be conflated without more profiling.

    7. Saved unified-log exports

    Two log exports were saved:

    codex-spawn-limit-logs.txt   202,255 bytes
    codex-eagain-maxproc-logs.txt 1,124,680 bytes
    

    The commands used were:

    log show --last 24h --style compact --predicate '
      process CONTAINS[c] "Codex" OR
      process CONTAINS[c] "codex" OR
      process CONTAINS[c] "node_repl" OR
      eventMessage CONTAINS[c] "posix_spawn" OR
      eventMessage CONTAINS[c] "fork failed" OR
      eventMessage CONTAINS[c] "resource temporarily unavailable" OR
      eventMessage CONTAINS[c] "EAGAIN" OR
      eventMessage CONTAINS[c] "maxproc" OR
      eventMessage CONTAINS[c] "spawn"
    ' > ~/Desktop/codex-spawn-limit-logs.txt

    and:

    log show --last 24h --style compact --predicate '
      process CONTAINS[c] "Codex" OR
      eventMessage CONTAINS[c] "EAGAIN" OR
      eventMessage CONTAINS[c] "resource temporarily unavailable" OR
      eventMessage CONTAINS[c] "maxproc"
    ' > ~/Desktop/codex-eagain-maxproc-logs.txt

    Review result:

    direct matches for:
      EAGAIN
      resource temporarily unavailable
      posix_spawn
      fork failed
      failed to spawn
      maxproc
    
    result: 0
    

    The exports contain thousands of ordinary Codex/macOS framework messages, but
    no direct spawn-failure line matching the predicates above.

    This negative result matters:

    • it does not weaken the process-table diagnosis, which was directly proven by
      ps, kern.maxprocperuid, and the shell's fork failure;
    • it means these log exports should not be cited as evidence of a specific
      Codex spawn exception;
    • the broad export contains unrelated device names, identifiers, local paths,
      account-service activity, and other private system context, so it should not
      be attached publicly without heavy redaction.

    8. Session-preservation note

    codex-session-restore.txt was saved before restart to preserve a manual list
    of open Codex windows and thread URLs. It contains 12 thread identifiers and
    project/task titles.

    It explains why preserving the running app initially mattered, but it does not
    identify the leak. The raw file should not be posted because thread IDs,
    workspace topics, and project names are private.

    9. Update/restart recovery

    The Codex update changed the main process from PID 19100 to PID 72552.

    Commands:

    ps -o pid,ppid,user,stat,etime,cputime,command -p 19100
    
    pgrep -fl '/Applications/Codex.app/Contents/MacOS/Codex'
    
    NEWPID=72552
    ps -axo ppid=,stat= \
      | awk -v ppid="$NEWPID" \
        '$1 == ppid && $2 ~ /^Z/ { n++ } END { print n+0 }'

    Relevant output:

    old PID 19100: no process
    new Codex PID: 72552
    zombies under new PID: 0
    

    The post-restart attachment summarized in the shared chat additionally reported:

    <USER> processes: 645
    total zombies:    0
    

    Interpretation:

    • Exiting the old main process released the approximately 9,940 zombie entries.
    • The machine recovered approximately 10,000 per-user process slots.
    • This validates the immediate mechanism and workaround.
    • It does not establish that version 26.608.12217 fixed recurrence,
      because the replacement process had just started and had barely been used.

    Root-cause assessment

    Proven

    The main Codex GUI process failed to reap thousands of exited direct children.
    Those zombie entries accumulated over an approximately 11-day app lifetime and
    eventually exhausted kern.maxprocperuid.

    At the OS level, the missing invariant is:

    Every child spawned or adopted by the long-lived Codex GUI process must
    eventually have its exit status collected with wait/waitpid or equivalent.
    

    Strongly suggested, but not proven

    The broader Codex helper lifecycle likely contributes to the rate of child
    creation:

    • 75 codex resource processes and 73 node_repl processes were live before
      manual cleanup.
    • Later snapshots still showed repeated node_repl, Computer Use client, and
      Computer Use service processes.
    • Related users report repeated MCP, stdio app-server, Computer Use, and
      notification-helper fanout.

    However, the original executable identity of each zombie is unavailable.
    Therefore the public report should say these helper families are plausible
    sources or adjacent lifecycle symptoms, not confirmed identities of the
    approximately 9,940 zombies.

    Separate adjacent failure modes

    The following should remain conceptually separate unless further evidence links
    them:

    • live MCP/helper processes that remain running;
    • live helpers orphaned to launchd with PPID=1;
    • exited children left as zombies under a still-running Codex parent;
    • file-descriptor leakage causing EMFILE;
    • live git process spawn storms caused by watcher/retry loops.

    They may share ownership and teardown weaknesses, but they consume different OS
    resources and require different direct fixes.

    Reproduction and monitoring commands

    Compact diagnostic snapshot

    Run while the problem is present:

    {
      echo "== OS =="
      sw_vers
      uname -a
    
      echo
      echo "== process limits =="
      launchctl limit maxproc
      sysctl kern.maxproc kern.maxprocperuid
    
      echo
      echo "== process counts by user =="
      ps -axo user= | sort | uniq -c | sort -nr | head -10
    
      echo
      echo "== total zombie count =="
      ps -axo stat= \
        | awk '$1 ~ /^Z/ { n++ } END { print n+0 }'
    
      echo
      echo "== top zombie parents =="
      ps -axo pid=,ppid=,stat=,etime=,command= \
        | awk '$3 ~ /^Z/ { z[$2]++ } END { for (p in z) print z[p], p }' \
        | sort -nr \
        | head -20 \
        | while read count ppid; do
            printf "%6s zombies  PPID=%s  " "$count" "$ppid"
            ps -o pid=,ppid=,stat=,etime=,command= -p "$ppid" 2>/dev/null
          done
    
      echo
      echo "== Codex-related live processes =="
      ps -axo pid=,ppid=,pgid=,stat=,etime=,command= \
        | awk '/Codex|codex|node_repl|SkyComputerUse/ { print }'
    }

    Redact the username, host name, home paths, workspace paths, thread IDs, prompt
    payloads, and unrelated processes before posting.

    One-shot current Codex check

    CODEX_PID="$(
      pgrep -f '/Applications/Codex.app/Contents/MacOS/Codex' \
        | head -1
    )"
    
    echo "Codex PID: $CODEX_PID"
    
    ps -axo ppid=,stat= \
      | awk -v ppid="$CODEX_PID" \
        '$1 == ppid && $2 ~ /^Z/ { n++ } END {
          print "zombies under Codex:", n+0
        }'
    
    ps -axo stat= \
      | awk '$1 ~ /^Z/ { n++ } END {
          print "total zombies:", n+0
        }'
    
    ps -axo user= \
      | awk -v user="$USER" '$1 == user { n++ } END {
          print "user processes:", n+0
        }'

    Long-running recurrence monitor

    This version refreshes the PID each iteration, so it survives app updates and
    restarts:

    while true; do
      CODEX_PID="$(
        pgrep -f '/Applications/Codex.app/Contents/MacOS/Codex' \
          | head -1
      )"
    
      printf '\n== %s ==\n' "$(date)"
      echo "Codex PID: ${CODEX_PID:-not-running}"
    
      if [[ -n "$CODEX_PID" ]]; then
        ps -axo ppid=,stat= \
          | awk -v ppid="$CODEX_PID" \
            '$1 == ppid && $2 ~ /^Z/ { n++ } END {
              print "zombies under Codex:", n+0
            }'
      fi
    
      ps -axo stat= \
        | awk '$1 ~ /^Z/ { n++ } END {
          print "total zombies:", n+0
        }'
    
      ps -axo user= \
        | awk -v user="$USER" '$1 == user { n++ } END {
          print "user processes:", n+0
        }'
    
      sleep 60
    done

    Safer emergency recovery near the process limit

    When pipelines can no longer fork, use shell builtins:

    kill -TERM <CODEX_PID>

    If the process does not exit:

    kill -KILL <CODEX_PID>

    SIGCHLD was tested in this reproduction and did not recover the zombies.
    Raising maxproc would only postpone exhaustion and is not a real fix.

    Issue relevance assessment

    Directly relevant

    #25744: macOS Computer Use/MCP helpers and unreaped zombies

    URL: #25744

    Relevance: Direct and strongest match

    Why:

    • same macOS Codex Desktop surface;
    • same main-GUI-owned zombie failure;
    • same repeated Computer Use/MCP/node_repl lifecycle context;
    • same behavior where killing live helpers does not release zombies;
    • same restart workaround.

    This reproduction adds:

    • approximately 9,940 zombies rather than approximately 100;
    • direct proof of kern.maxprocperuid exhaustion;
    • shell-wide fork failure;
    • an unsuccessful SIGCHLD recovery experiment;
    • before/after restart counts;
    • a saved main-process sample and redaction analysis.

    Posting recommendation: Post the comprehensive overview here. This should
    be the canonical location.

    #12491: GUI MCP children not reaped after task completion

    URL: #12491

    Relevance: Direct historical precedent

    Why:

    • same child-lifecycle/reaping class;
    • macOS 26.3 build 25D125 is explicitly represented in the issue;
    • reports both zombies and persistent MCP/helper subtrees;
    • reports system slowdown and recovery after cleanup.

    Important difference:

    • the issue was closed on May 13, 2026 with a maintainer statement that the
      original reported bug was fixed;
    • later comments mix several potentially distinct causes.

    Posting recommendation: Do not add the full report to this closed issue.
    Link to it from the #25744 overview as prior evidence that a similar failure
    class existed and may have regressed or remained in another lifecycle path.

    Tangentially relevant

    #26293: SkyComputerUseClient turn-ended helpers remain as PPID=1 orphans

    URL: #26293

    Relevance: Adjacent and potentially related, but different observed state

    Similarity:

    • Codex Desktop on macOS;
    • Computer Use helper lifecycle;
    • child processes that outlive the event or owner;
    • accumulation during high-volume turns.

    Difference:

    Posting recommendation: Do not duplicate the full report. Add a short
    cross-link only if a fresh process capture independently confirms lingering
    SkyComputerUseClient turn-ended ... processes. Otherwise, mention #26293 in
    the canonical #25744 report as an adjacent lifecycle path.

    #26327: Windows helper-stack accumulation

    URL: #26327

    Relevance: Cross-platform lifecycle analogy

    Similarity:

    • repeated node_repl, MCP, and nested stdio app-server stacks;
    • long-running Desktop session;
    • restart restores a smaller baseline.

    Difference:

    • Windows rather than macOS;
    • live helper stacks rather than proven zombies;
    • the author retracted and closed the report because the causal split was not
      precise enough.

    Posting recommendation: Do not post there. A brief mention in #25744 is
    enough if cross-platform context is useful.

    #26984: MCP stdio pipe-FD and orphan leakage

    URL: #26984

    Relevance: Related ownership/teardown class, different exhausted resource

    Similarity:

    • child cleanup and process-group teardown concerns;
    • long-running Codex sessions;
    • eventual inability to open/spawn.

    Difference:

    • primary failure is EMFILE / file-descriptor exhaustion;
    • focuses on CLI-owned stdio MCP servers and npm/npx descendants;
    • this reproduction is EAGAIN-style process-table exhaustion caused by
      zombies under the Desktop GUI process;
    • no FD-count evidence was collected here.

    Posting recommendation: Do not post the overview there. Link it from
    #25744 only as a possible shared lifecycle class. Comment on #26984 only if a
    future reproduction also captures monotonically increasing Codex-owned pipe
    FDs with lsof.

    Likely not relevant

    #26904: WSL slow-filesystem git spawn storm

    URL: #26904

    Relevance: Likely unrelated root cause

    Why:

    • WSL/Linux and /mnt drvfs trigger;
    • live git processes spawned faster than they finish;
    • watcher ENOSPC and retry/backoff behavior;
    • OOM from active process growth;
    • no shared evidence of unreaped main-GUI child zombies.

    The only commonality is the eventual exhaustion caused by unbounded process
    lifecycle behavior.

    See Also

  5. 0xdevalias commented on Jun 11, 2026

    @0xdevalias

    I have now identified one concrete source of Codex-owned zombie bursts and filed the implementation-specific report as #27570:

    In a prospective process trace, opening/switching older threads caused the main Codex GUI process to spawn exactly 5,042 direct:

    git hash-object --no-filters -- <PATH>

    children. The zombie count reached exactly 5,042 before the children were eventually reaped.

    Inspection of the installed Desktop bundle identified an unbounded fallback in the review-summary Git worker:

    1. Desktop attempts one bulk git hash-object call.
    2. Three untracked nested Git repositories cause it to fail with fatal: Unable to hash (null).
    3. Desktop retries all 5,042 paths concurrently using an unbounded Promise.all.
    4. The fallback does not receive the outer request's abort signal.

    This provides a confirmed Git-based source for transient main-GUI-owned zombies. It may explain some or all of the approximately 9,940 long-lived zombies previously documented here, but that historical connection remains unproven because their executable identities were already lost.

  6. Nicolas0315 commented on Jun 22, 2026

    @Nicolas0315

    Adding a current verification snapshot from another macOS Codex.app session (2026-06-22 JST).

    Environment:

    • macOS 26.5.1 (25F80), Darwin 25.5.0 arm64
    • Codex.app: 26.616.51431 (build 4212), bundle id com.openai.codex
    • App bundled CLI: codex-cli 0.142.0-alpha.6
    • PATH CLI / managed app-server: codex-cli 0.141.0
    • app-server: running at ~/.codex/app-server-control/app-server-control.sock, parent process /Applications/Codex.app/Contents/Resources/codex app-server --analytics-default-enabled
    • MCP state: computer-use, event-stream, and node_repl enabled; onepassword disabled

    Current observation, without restarting or killing Codex:

    • SkyComputerUseClient event-stream mcp: 19 processes, oldest sampled process about 16h old
    • SkyComputerUseClient mcp: 19 processes, oldest sampled process about 16h old
    • node_repl: 21 processes, oldest sampled process about 16h old
    • Most sampled helpers are direct or nested descendants of the same long-lived app-server process.
    • A minimal node_repl reset and JS call currently succeeds, so this is not the immediate sandboxPolicy missing-field failure mode.

    Health checks:

    • codex doctor --json initially reported the app-server socket as stale/unreachable with a 2s probe timeout.
    • After codex app-server daemon version, a second doctor run reported app_server.status=ok.
    • Doctor still reports overallStatus=fail only because this noninteractive environment has TERM=dumb.
    • Auth, config, MCP, network, WebSocket, and SQLite integrity checks were OK.

    Repro / verification commands used:

    defaults read /Applications/Codex.app/Contents/Info CFBundleShortVersionString
    defaults read /Applications/Codex.app/Contents/Info CFBundleVersion
    /Applications/Codex.app/Contents/Resources/codex --version
    codex --version
    codex mcp list
    codex doctor --json
    codex app-server daemon version
    ps -axo pid=,ppid=,stat=,etime=,command= | rg 'SkyComputerUseClient|node_repl'

    Interpretation:
    This looks like the same helper lifecycle / fan-out class as this issue, not a new distinct issue. A useful fix invariant would be that old per-session Computer Use / node_repl helper families stay bounded or are reaped while the long-lived app-server remains alive.

  7. dysthymia commented on Jun 25, 2026

    @dysthymia

    Adding a fresh sanitized macOS Codex.app crash/stackshot data point that looks like the same lifecycle/resource-leak class, but with a more severe end state: WindowServer was terminated by watchdog after a Codex-owned node process storm.

    Environment:

    • Codex.app: 26.616.81150, bundle build 4306
    • Platform: Darwin 25.5.0 arm64 arm
    • macOS: 26.5.1 build 25F80
    • Hardware class: Apple Silicon desktop, 16 GB RAM
    • Raw report not attached because it contains local paths, full process inventory, display/peripheral details, and project/workspace identifiers.

    What happened:

    • The user-visible symptom looked like a system restart / black screen / session reset.
    • last reboot showed the machine did not actually reboot; uptime was still about 22 days.
    • The generated report was a WindowServer stackshot / bug type 409.
    • Termination was WATCHDOG, indicator monitoring timed out for service.
    • The specific watchdog detail was:
    unresponsive work processor(s): WindowServer main thread
    51 seconds since last successful checkin
    

    Most relevant stackshot numbers, redacted and aggregated:

    Codex jetsam coalition total RSS: 44.7 GiB
    Codex jetsam coalition process count: 1386
    node processes in that coalition: 1352
    node_repl processes in that coalition: 17
    Codex-named processes total RSS: 2.7 GiB
    WindowServer RSS: 1.19 GiB
    

    Largest processes in the Codex coalition:

    1147.9 MiB  Codex (Renderer)
    1091.8 MiB  node
     617.3 MiB  codex app-server
     578.7 MiB  Codex (Service)
     259.2 MiB  Codex
    

    This seems important because the biggest signal was not only Codex renderer memory. The stackshot had more than a thousand node children/related processes grouped into the same Codex jetsam coalition. That matches the helper-process accumulation pattern reported above, but the observed impact reached a system UI watchdog termination.

    Other context:

    • Other non-Codex memory users were present, but Codex's coalition was by far the largest by total RSS and process count.
    • The post-incident live system no longer had the same 1386-process state, but still had dozens of Codex-related helper/app-server/repl processes.
    • Memory pressure later recovered, so the stackshot is the useful evidence point.

    Expected behavior:

    • Codex should not accumulate hundreds or thousands of stale node, node_repl, or app-server helper processes across a long-running desktop session.
    • A long-running Codex session should not be able to push the graphical session into a WindowServer watchdog reset.
    • Stale helper processes should be bounded and cleaned up when their tool/session/thread is no longer active.

    Useful local diagnostic commands for future reports:

    ps -axo pid,ppid,rss,etime,command |
      awk 'BEGIN{IGNORECASE=1} /Codex|\/codex|cua_node|node_repl/ {print}'
    
    ps -axo command |
      awk 'BEGIN{IGNORECASE=1} /Codex|\/codex|cua_node|node_repl/ {n++} END{print n+0}'
    
    memory_pressure
    last reboot | head

    I intentionally omitted raw paths, usernames, project/workspace names, third-party project identifiers, display/peripheral names, and the raw stackshot process inventory.

  8. Nicolas0315 commented on Jun 28, 2026

    @Nicolas0315

    Adding a current helper-accumulation datapoint from the latest macOS Desktop build I have installed.

    Snapshot time: 2026-06-28 11:26 JST

    Environment:

    • Codex Desktop: 26.623.42026, build 4514
    • Codex CLI / app-server: 0.142.3
    • Platform: macOS arm64
    • MCP config: 13 configured servers, 4 disabled, 7 stdio, 6 streamable HTTP
    • Active local surfaces include Desktop plus IDE-extension app-server processes.

    Current sanitized helper counts:

    chrome_devtools_mcp: 59
    playwright_mcp: 30
    computer_use_mcp: 41
    computer_use_event_stream: 22
    node_repl: 22
    xcodebuildmcp: 40
    codex_app_server matched processes: 20
    

    Selected app-server FD counts from the same snapshot:

    standalone codex app-server --listen unix://: 258 lsof entries
    VS Code extension app-server: 96 lsof entries
    Cursor extension app-server samples: 25, 25, 27 lsof entries
    launchd maxfiles soft limit: 256
    

    This looks like the same lifecycle class as this issue, with one extra angle: in a multi-surface setup where Codex Desktop, CLI, VS Code, and Cursor are all present, the shared MCP/tool configuration appears to multiply the same helper families across surfaces. I did not kill or restart anything before collecting these numbers.

    Omitting raw process args, full local paths, and private project/tool details here.

  9. clairernovotny commented on Jun 29, 2026

    @clairernovotny

    Adding a narrower source-level repro for the app-server/MCP helper retention path. This is from a current local openai/codex checkout, with local paths sanitized.

    The live symptom was not just macOS FD pressure. Once the long-running app-server accumulated repeated MCP/helper runtime groups, new Codex unified exec process creation started failing with:

    Bad file descriptor (os error 9)
    dup of fd 255 failed
    failed to openpty
    

    At the same time, an already-live Node MCP runtime could still spawn /bin/pwd, so the failure was not a global OS inability to fork/exec. It pointed at Codex session/tool runtime lifecycle and unified exec descriptor handling under retained helper pressure.

    Concrete repro test

    I added an ignored regression test in:

    codex-rs/core/tests/suite/mcp_refresh_cleanup.rs
    

    Test name:

    shutdown_cleans_superseded_mcp_server_with_in_flight_call
    

    The test does this:

    1. Starts a stdio MCP server and records its PID via MCP_TEST_PID_FILE.
    2. Starts a long in-flight MCP sync call that holds the current MCP runtime.
    3. Refreshes MCP servers, creating a replacement MCP server and making the first runtime superseded.
    4. Calls fixture.codex.shutdown_and_wait().await.
    5. Asserts both the replacement MCP server and the superseded MCP server exit.

    Run command:

    cd ~/dev/codex/codex-rs
    RUST_MIN_STACK=16777216 RUST_BACKTRACE=1 cargo test -p codex-core \
      shutdown_cleans_superseded_mcp_server_with_in_flight_call \
      -- --ignored --nocapture

    Observed failure:

    test suite::mcp_refresh_cleanup::shutdown_cleans_superseded_mcp_server_with_in_flight_call ... FAILED
    
    Error: superseded MCP process <pid> survived session shutdown while an in-flight tool call held the old runtime: timed out waiting for process to exit
    

    Without RUST_MIN_STACK=16777216, this same scenario can abort earlier with a Tokio worker stack overflow during shutdown, so the larger stack is useful to reach the cleaner process-retention assertion.

    Repro patch

    Patch for the ignored repro test
    diff --git i/codex-rs/core/tests/suite/mcp_refresh_cleanup.rs w/codex-rs/core/tests/suite/mcp_refresh_cleanup.rs
    index 620b6c734c..7e12d1e3ec 100644
    --- i/codex-rs/core/tests/suite/mcp_refresh_cleanup.rs
    +++ w/codex-rs/core/tests/suite/mcp_refresh_cleanup.rs
    @@ -133,3 +133,131 @@ async fn refresh_keeps_superseded_mcp_server_alive_for_in_flight_calls() -> anyh
         fixture.codex.shutdown_and_wait().await?;
         wait_for_process_exit(&replacement_pid).await
     }
    +
    +#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    +#[ignore = "reproduces shutdown leaking a superseded MCP runtime that is held by an in-flight call"]
    +async fn shutdown_cleans_superseded_mcp_server_with_in_flight_call() -> anyhow::Result<()> {
    +    skip_if_no_network!(Ok(()));
    +
    +    let server = responses::start_mock_server().await;
    +    let temp_dir = tempfile::tempdir()?;
    +    let pid_file = temp_dir.path().join("mcp.pid");
    +    let pid_file_for_config = pid_file.clone();
    +    let command = stdio_server_bin()?;
    +    let fixture = test_codex()
    +        .with_config(move |config| {
    +            let mut servers = config.mcp_servers.get().clone();
    +            servers.insert(
    +                "refresh_cleanup".to_string(),
    +                McpServerConfig {
    +                    auth: Default::default(),
    +                    transport: McpServerTransportConfig::Stdio {
    +                        command,
    +                        args: Vec::new(),
    +                        env: Some(HashMap::from([(
    +                            "MCP_TEST_PID_FILE".to_string(),
    +                            pid_file_for_config.to_string_lossy().into_owned(),
    +                        )])),
    +                        env_vars: Vec::new(),
    +                        cwd: None,
    +                    },
    +                    environment_id: DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
    +                    enabled: true,
    +                    required: false,
    +                    supports_parallel_tool_calls: false,
    +                    disabled_reason: None,
    +                    startup_timeout_sec: Some(Duration::from_secs(10)),
    +                    tool_timeout_sec: None,
    +                    default_tools_approval_mode: None,
    +                    enabled_tools: None,
    +                    disabled_tools: None,
    +                    scopes: None,
    +                    oauth: None,
    +                    oauth_resource: None,
    +                    tools: HashMap::new(),
    +                },
    +            );
    +            config
    +                .mcp_servers
    +                .set(servers)
    +                .expect("test MCP servers should accept any configuration");
    +        })
    +        .build(&server)
    +        .await?;
    +    wait_for_mcp_server(&fixture.codex, "refresh_cleanup").await?;
    +
    +    let superseded_pid = wait_for_pid_file(&pid_file).await?;
    +    assert!(process_is_alive(&superseded_pid)?);
    +
    +    let barrier = serde_json::json!({
    +        "id": "mcp-refresh-shutdown-cleanup",
    +        "participants": 2,
    +        "timeout_ms": 1_000
    +    });
    +    let long_call = tokio::spawn({
    +        let codex = Arc::clone(&fixture.codex);
    +        let barrier = barrier.clone();
    +        async move {
    +            codex
    +                .call_mcp_tool(
    +                    "refresh_cleanup",
    +                    "sync",
    +                    Some(serde_json::json!({
    +                        "barrier": barrier,
    +                        "sleep_after_ms": 300_000
    +                    })),
    +                    /*meta*/ None,
    +                )
    +                .await
    +        }
    +    });
    +    fixture
    +        .codex
    +        .call_mcp_tool(
    +            "refresh_cleanup",
    +            "sync",
    +            Some(serde_json::json!({ "barrier": barrier })),
    +            /*meta*/ None,
    +        )
    +        .await?;
    +    fs::remove_file(&pid_file)?;
    +
    +    responses::mount_sse_once(
    +        &server,
    +        responses::sse(vec![
    +            responses::ev_response_created("resp-1"),
    +            responses::ev_assistant_message("msg-1", "done"),
    +            responses::ev_completed("resp-1"),
    +        ]),
    +    )
    +    .await;
    +    fixture
    +        .codex
    +        .set_openai_form_elicitation_support(/*supported*/ true)
    +        .await?;
    +    fixture.submit_turn("refresh MCP servers").await?;
    +
    +    let replacement_pid = wait_for_pid_file(&pid_file).await?;
    +    assert_ne!(replacement_pid, superseded_pid);
    +    assert!(process_is_alive(&superseded_pid)?);
    +    assert!(process_is_alive(&replacement_pid)?);
    +
    +    fixture.codex.shutdown_and_wait().await?;
    +
    +    let replacement_exit = wait_for_process_exit(&replacement_pid).await;
    +    let superseded_exit = wait_for_process_exit(&superseded_pid).await;
    +    if superseded_exit.is_err() {
    +        long_call.abort();
    +        let _ = long_call.await;
    +        let _ = wait_for_process_exit(&superseded_pid).await;
    +    } else {
    +        let _ = tokio::time::timeout(Duration::from_secs(1), long_call).await;
    +    }
    +
    +    replacement_exit?;
    +    superseded_exit.map_err(|err| {
    +        anyhow::anyhow!(
    +            "superseded MCP process {superseded_pid} survived session shutdown while an in-flight tool call held the old runtime: {err}"
    +        )
    +    })
    +}

    Suspected root cause path

    shutdown_session_runtime() currently shuts down:

    sess.services
        .latest_mcp_runtime()
        .manager_arc()
        .shutdown()
        .await;

    This repro shows that a superseded runtime can remain alive when an in-flight tool call still holds it. The latest runtime is shut down, but the old runtime held by the in-flight call is not reliably cancelled/shut down by session shutdown.

    That gives a concrete source for stale stdio MCP/helper processes surviving after the owning session/thread is closed or shut down. In a long-running Desktop/app-server process, repeated occurrences can leave old helper groups and their descriptors around until app-server eventually hits the soft FD boundary and unified exec starts failing.

    Useful fix invariant

    Session shutdown / thread close should cancel and shut down all MCP runtime snapshots associated with that session, not only the latest published runtime. Superseded runtimes held by in-flight tool calls need an owner-session shutdown/cancellation path so their stdio children cannot survive the session that created them.

  10. clairernovotny commented on Jun 29, 2026

    @clairernovotny

    Posting the repro test source directly, since the previous comment had it inside a collapsed <details> diff block.

    File:

    codex-rs/core/tests/suite/mcp_refresh_cleanup.rs
    

    Source to append after refresh_keeps_superseded_mcp_server_alive_for_in_flight_calls:

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    #[ignore = "reproduces shutdown leaking a superseded MCP runtime that is held by an in-flight call"]
    async fn shutdown_cleans_superseded_mcp_server_with_in_flight_call() -> anyhow::Result<()> {
        skip_if_no_network!(Ok(()));
    
        let server = responses::start_mock_server().await;
        let temp_dir = tempfile::tempdir()?;
        let pid_file = temp_dir.path().join("mcp.pid");
        let pid_file_for_config = pid_file.clone();
        let command = stdio_server_bin()?;
        let fixture = test_codex()
            .with_config(move |config| {
                let mut servers = config.mcp_servers.get().clone();
                servers.insert(
                    "refresh_cleanup".to_string(),
                    McpServerConfig {
                        auth: Default::default(),
                        transport: McpServerTransportConfig::Stdio {
                            command,
                            args: Vec::new(),
                            env: Some(HashMap::from([(
                                "MCP_TEST_PID_FILE".to_string(),
                                pid_file_for_config.to_string_lossy().into_owned(),
                            )])),
                            env_vars: Vec::new(),
                            cwd: None,
                        },
                        environment_id: DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(),
                        enabled: true,
                        required: false,
                        supports_parallel_tool_calls: false,
                        disabled_reason: None,
                        startup_timeout_sec: Some(Duration::from_secs(10)),
                        tool_timeout_sec: None,
                        default_tools_approval_mode: None,
                        enabled_tools: None,
                        disabled_tools: None,
                        scopes: None,
                        oauth: None,
                        oauth_resource: None,
                        tools: HashMap::new(),
                    },
                );
                config
                    .mcp_servers
                    .set(servers)
                    .expect("test MCP servers should accept any configuration");
            })
            .build(&server)
            .await?;
        wait_for_mcp_server(&fixture.codex, "refresh_cleanup").await?;
    
        let superseded_pid = wait_for_pid_file(&pid_file).await?;
        assert!(process_is_alive(&superseded_pid)?);
    
        let barrier = serde_json::json!({
            "id": "mcp-refresh-shutdown-cleanup",
            "participants": 2,
            "timeout_ms": 1_000
        });
        let long_call = tokio::spawn({
            let codex = Arc::clone(&fixture.codex);
            let barrier = barrier.clone();
            async move {
                codex
                    .call_mcp_tool(
                        "refresh_cleanup",
                        "sync",
                        Some(serde_json::json!({
                            "barrier": barrier,
                            "sleep_after_ms": 300_000
                        })),
                        /*meta*/ None,
                    )
                    .await
            }
        });
        fixture
            .codex
            .call_mcp_tool(
                "refresh_cleanup",
                "sync",
                Some(serde_json::json!({ "barrier": barrier })),
                /*meta*/ None,
            )
            .await?;
        fs::remove_file(&pid_file)?;
    
        responses::mount_sse_once(
            &server,
            responses::sse(vec![
                responses::ev_response_created("resp-1"),
                responses::ev_assistant_message("msg-1", "done"),
                responses::ev_completed("resp-1"),
            ]),
        )
        .await;
        fixture
            .codex
            .set_openai_form_elicitation_support(/*supported*/ true)
            .await?;
        fixture.submit_turn("refresh MCP servers").await?;
    
        let replacement_pid = wait_for_pid_file(&pid_file).await?;
        assert_ne!(replacement_pid, superseded_pid);
        assert!(process_is_alive(&superseded_pid)?);
        assert!(process_is_alive(&replacement_pid)?);
    
        fixture.codex.shutdown_and_wait().await?;
    
        let replacement_exit = wait_for_process_exit(&replacement_pid).await;
        let superseded_exit = wait_for_process_exit(&superseded_pid).await;
        if superseded_exit.is_err() {
            long_call.abort();
            let _ = long_call.await;
            let _ = wait_for_process_exit(&superseded_pid).await;
        } else {
            let _ = tokio::time::timeout(Duration::from_secs(1), long_call).await;
        }
    
        replacement_exit?;
        superseded_exit.map_err(|err| {
            anyhow::anyhow!(
                "superseded MCP process {superseded_pid} survived session shutdown while an in-flight tool call held the old runtime: {err}"
            )
        })
    }

    Run command:

    cd ~/dev/codex/codex-rs
    RUST_MIN_STACK=16777216 RUST_BACKTRACE=1 cargo test -p codex-core \
      shutdown_cleans_superseded_mcp_server_with_in_flight_call \
      -- --ignored --nocapture

    Observed failure:

    Error: superseded MCP process <pid> survived session shutdown while an in-flight tool call held the old runtime: timed out waiting for process to exit
    
  11. jamesjenkinsyates commented on Aug 3, 2026

    @jamesjenkinsyates

    Adding a macOS data point that matches this closely, plus a downstream consequence I haven't seen documented: the jetsam kills corrupt unrelated user data.

    Environment

    • ChatGPT.app 26.727.51351 (build 6119), codex-cli 0.146.0-alpha.9.2
    • MacBook Pro (Mac16,7), macOS 26.5.2 (25F84), 48 GB
    • 17 MCP servers configured; 16 enabled at the time of measurement, 11 of them stdio
      (process-spawning). The remaining 5 are url = servers and cost nothing locally.

    Growth — single app-server, no restart

    09:53  (1 min uptime)    16 MCP children      4.36 GB
    10:53  (1 hr)            24 children          5.18 GB
    11:24  (1h 31m)         202 processes        16.05 GB
    11:55  (2h)             235 processes        16.89 GB
    

    Roughly 10 GB/hour. Split at the 2h mark:

    ChatGPT.app's own binaries         :  29 procs   3.37 GB
    Processes it spawned, never reaped : 206 procs  13.53 GB   (80%)
    

    Per-server instance counts at 2h — one full stack per opened thread:

    mcp-server-circleci   32
    aws_api_mcp_server    32
    @toolbox-sdk/server   30
    @playwright/mcp       16
    node_repl             16
    chrome-devtools-mcp    9
    

    JetsamEvent reports from 27 Jul – 1 Aug show the endpoint: free memory 0.38–0.57 GB,
    compressor 20.8–23.4 GB, and 1,336 distinct node processes recorded across one boot.

    The part that seems under-reported

    Jetsam killing processes mid-write corrupted files with no connection to Codex:

    • Brave Browser crashed 9× in 6 minutes on 28 Jul. Its History database was rebuilt and
      all browsing history before 31 Jul was lost permanently — Chromium marks History with
      com.apple.metadata:com_apple_backup_excludeItem, so Time Machine holds no copy of it.
      Recovery was impossible.
    • A Docker Desktop auto-update running at 31 Jul 19:40 was interrupted (JetsamEvent at
      19:54), leaving a bundle macOS reported as "damaged and can't be opened" on every login.
    • Codex's own state was hit — codex doctor reports: rollout files are missing from the state DB; state DB rows point at missing or unusable rollout files.

    #21984 notes the user-visible failure mode is "not necessarily profile corruption". On this
    machine it demonstrably was, and the data loss was unrecoverable.

    Mitigation available today

    enabled = false per server in ~/.codex/config.toml is the only lever, and it only takes
    effect on a full app restart. Either way the accumulation rate is still governed by
    threads opened × stdio servers enabled, so it only changes the slope. A project-scoped
    process pool as proposed in #20883 would address the root cause.

  12. johson commented on Aug 5, 2026

    @johson

    I can reproduce this on macOS with the current Codex/ChatGPT desktop app.

    Immediately after restarting the app, the main desktop process started accumulating unreaped zombie children.

    Observed after approximately 4m40s:

    • Per-user process limit: 2666
    • User processes: 487
    • System processes: 668
    • Zombie processes: 169
    • All 169 zombies had the same Codex/ChatGPT main process as parent
    • Codex-related live processes: approximately 18
    • Codex-related RSS: approximately 984 MiB
    • Available memory: approximately 32%
    • File descriptor limit: 65536

    The zombie count increased at roughly 36 processes per minute. After longer use, the per-user process limit is exhausted and all local process creation begins failing with:

    parse_git_dirty: fork failed: resource temporarily unavailable
    omz_termsupport_cwd: fork failed: resource temporarily unavailable
    zsh: fork failed: resource temporarily unavailable
    

    Quitting and restarting Codex clears the zombies temporarily, but accumulation begins again immediately after restart.

    The zombies are direct children of the desktop main process, strongly indicating that exited subprocesses are not being waited on/reaped.

    Visible framework version:
    151.0.7922.71

    macOS version:
    13.3 (22E252)

    Codex App version shown in “About Codex”:
    26.730.61639

  13. scyuo commented on Aug 5, 2026

    @scyuo

    Additional sanitized reproduction on macOS:

    • The ChatGPT desktop main process accumulated a large number of direct child processes in zombie (Z / <defunct>) state during local command execution.
    • Eventually, new local commands failed with fork failed: resource temporarily unavailable.
    • Process inspection showed the zombies shared the same ChatGPT desktop main-process parent.
    • Terminating and reopening the desktop app cleared the zombies temporarily; the count began growing again during subsequent command execution.

    No raw process listing, local paths, usernames, project names, task content, or logs are attached because they may contain private information.

    This observation does not attribute the trigger to a specific MCP server or Computer Use feature; it is a process-reaping failure observed from normal local command execution.

  14. galligan commented on Aug 5, 2026

    @galligan

    Cross-linking a measured data point for the SkyComputerUseClient portion of this report.

    I posted per-client CPU measurements in #35659 for the event-stream mcp sub-variant — the same class as the "dozens of live SkyComputerUseClient mcp processes" noted here: #35659 (comment)

    Summary of what may be useful for triage:

    • Reproduced on Codex Desktop 26.727.51351 / CLI 0.146.0-alpha.9.2 / CU runtime 26.727.1000550 (newer than the builds in the linked reports).
    • 10 SkyComputerUseClient event-stream mcp clients against a single SkyComputerUseService, all live children of the app-server — zero PPID=1 orphans, which separates this path from SkyComputerUseClient processes spawned for turn-ended events remain alive as PPID=1 orphans #26293 / macOS Computer Use leaks SkyComputerUseClient turn-ended helpers #29157.
    • Each leaked client burns a near-constant ~1.0–1.33% of a core regardless of age, so aggregate CPU cost scales linearly with the leak count (~12% of a core at 10 clients).
    • Clean-boot accumulation rate ≈ 0.45 clients/hour. A prior boot reached 161 total app-server children over ~3d21h uptime and pushed a 128 GB machine into sustained memory pressure (swap file grown 3 GB → 4 GB, compressor 19 GB, ~1.1 GB free). Post-reboot: swap 0, compressor 2.4 GB — then accumulation restarted immediately.

    That last point supports the lifecycle/reaping framing in this issue rather than any single runaway process.

  15. TralafalgarV commented on Aug 6, 2026

    @TralafalgarV

    Additional data point: the zombie leak has a deterministic, OS-gated trigger — and it cannot be disabled from config

    I hit the same unbounded zombie accumulation described here, and was able to narrow it
    down to a single code path with a deterministic trigger. Two findings below are not yet
    covered in this thread and I think are directly actionable:

    1. The leak is driven by a bundled component whose minimum OS is higher than the host
      it was installed on
      , turning into an unbounded crash-retry loop.
    2. Disabling Computer Use from configuration does not stop it. computer-use is
      registered twice (as a plugin and as an MCP server); setting enabled = false on
      both, plus a full app restart, does not prevent the spawn loop.

    Environment

    Item Value
    ChatGPT.app 26.730.61639 (build 6234)
    Embedded Codex Framework / Chromium 151.0.7922.71
    codex-cli 0.147.0-alpha.1.2
    SkyComputerUseService 26.803.1000621
    OS macOS 14.2.1 (23C71), Darwin 23.2.0
    Hardware Apple M2, 16 GB
    kern.maxproc / kern.maxprocperuid 4000 / 2666

    Root cause: minimum-OS mismatch in a bundled component

    The bundled Computer Use service binary is built for a higher minimum OS than the host
    it was installed on
    :

    $ vtool -show-build "$HOME/.codex/computer-use/Codex Computer Use.app/Contents/MacOS/SkyComputerUseService"
     platform MACOS
        minos 14.4      <-- requires macOS >= 14.4
          sdk 26.1
    
    $ defaults read "$HOME/.codex/computer-use/Codex Computer Use.app/Contents/Info.plist" LSMinimumSystemVersion
    14.4
    
    $ sw_vers -productVersion
    14.2.1
    

    14.2.1 < 14.4, so the binary cannot load at all on this host. It dies before
    main(), inside dyld's Objective-C image mapping:

    Exception:   EXC_BAD_ACCESS (SIGBUS)
    Termination: Bus error: 10
    Faulting thread stack (top frames):
      libobjc.A.dylib    map_images_nolock
      libobjc.A.dylib    map_images
      dyld               dyld4::RuntimeState::setObjCNotifiers(...)
      dyld               dyld4::RuntimeLocks::withLoadersReadLock(...)
      dyld               dyld4::APIs::_dyld_objc_register_callbacks(...)
      libobjc.A.dylib    _objc_init
      libdispatch.dylib  _os_object_init
    

    All 15 crash reports in ~/Library/Logs/DiagnosticReports/ share an identical
    signature — this is a 100% deterministic launch failure, not a race condition:

    15  26.803.1000621  EXC_BAD_ACCESS  SIGBUS  Bus error: 10
    

    Note that this is why the crash is invisible in normal use: there is no error surfaced
    in the UI, and macOS throttles crash-report writing, so DiagnosticReports only retains
    sparse bursts rather than one report per failure.


    How that becomes an unbounded zombie leak

    The main ChatGPT GUI process respawns the service with no backoff and no retry cap,
    and never reaps the corpses:

    1. Main process spawns SkyComputerUseService
    2. It SIGBUSes immediately, pre-main() — guaranteed to fail on this OS
    3. Main process retries roughly every 1.7 s, indefinitely
    4. Main process never calls wait() / waitpid() → every attempt leaves a <defunct> child

    Crash-report timestamps confirm the retry interval within each burst:

    20:29:32  20:29:34  20:29:36  20:29:37
    20:51:04  20:51:06  20:51:08  20:51:09
    20:52:20  20:52:22  20:52:23  20:52:25  20:52:27
    

    Measured leak rate — four independent 20–30 s samples, all consistent:

     55 ->  73   (+18 / 30 s)
    348 -> 360   (+12 / 20 s)
    470 -> 488   (+18 / 30 s)
     31 ->  66   (+35 / 60 s, after a full app restart)
    => ~35-36 zombies/min
    

    Cross-check against the cumulative count: 687 zombies over 19 min 35 s = 35.1/min.

    At this rate the per-user process cap (2666) is reached in ~70 minutes, after which
    nothing on the system can fork — every shell and application fails with
    resource temporarily unavailable until ChatGPT is quit. Quitting the app causes launchd
    to reap the entire backlog immediately (867 zombies -> 24 in my case), confirming the
    main process is the sole owner.


    Finding 1: disabling Computer Use from config does NOT stop the spawn

    This is the part I think matters most, because it will mislead anyone trying to work
    around the issue. Two different binaries are involved, and the config flags only
    govern one of them:

    Binary Path Governed by config?
    SkyComputerUseClient .../SharedSupport/SkyComputerUseClient.app/Contents/MacOS/SkyComputerUseClient yes
    SkyComputerUseService ~/.codex/computer-use/Codex Computer Use.app/Contents/MacOS/SkyComputerUseService no

    computer-use is registered twice in ~/.codex/config.toml:

    [plugins."computer-use@openai-bundled"]
    enabled = false            # <- set by me
    
    [mcp_servers.computer-use]
    command = "./Codex Computer Use.app/Contents/SharedSupport/SkyComputerUseClient.app/Contents/MacOS/SkyComputerUseClient"
    args = ["mcp"]
    enabled = false            # <- set by me

    The MCP entry points at the Client. The binary that crash-loops is the Service,
    spawned directly by the main GUI process.

    What I verified, in order:

    1. Set enabled = false on [mcp_servers.computer-use] and [mcp_servers.node_repl].
      Confirmed SkyComputerUseClient and node_repl processes exited.
      Leak rate unchanged: 470 -> 488 over 30 s (36/min).
    2. Additionally set enabled = false on [plugins."computer-use@openai-bundled"].
      Leak rate unchanged (35/min) — but the app had not been restarted, so this test was
      inconclusive on its own.
    3. Fully restarted ChatGPT.app with all three disabled. New main PID, backlog reaped.
      Leak rate still 35/min, and high-frequency sampling confirms
      SkyComputerUseService is still being spawned ~33 times/min.

    So the Computer Use service spawn path ignores both the plugin flag and the MCP server
    flag
    . There appears to be no supported way to disable it from configuration. Related:
    .codex-global-state.json contains computer-use-bundled-plugin-auto-install-disabled
    (default false), and config.toml carries
    SKY_CUA_SERVICE_PATH = "$HOME/.codex/computer-use/Codex Computer Use.app".


    Finding 2: the missing reap is specific to this path, not app-wide

    Counting unique child PIDs of the main GUI process over a 20 s window (see
    methodology note below) apportions the leak precisely:

    Child process Launches Reaped?
    SkyComputerUseService ~33/min no — becomes <defunct>
    git ~72/min yes
    ps, sh ~36/min yes
    Codex Helper, codex app-server, bare-modifier-monitor long-lived n/a

    SkyComputerUseService at ~33 launches/min accounts for essentially the entire measured
    zombie rate of ~35/min. Meanwhile git, ps and sh are spawned at comparable or
    higher frequency by the same parent and are reaped correctly.

    This narrows the defect considerably: it is not a general "the main process doesn't
    reap children" problem. The Computer Use service spawn path specifically is missing its
    waitpid(). That should make it much easier to locate than the broad symptom suggests.


    Methodology note (to avoid a false positive I initially hit)

    When sampling ps in a loop, presence count is not launch count. A single
    gh pr list invocation that lives ~1.4 s appeared in 29 consecutive samples and briefly
    looked like a second high-rate leak source; it was one process, and it was reaped
    normally. Counting distinct PIDs instead of sample hits is what produced the
    apportionment table above. Worth doing if others try to reproduce this.


    Reproduction / verification commands

    # 1. Confirm the min-OS mismatch
    vtool -show-build "$HOME/.codex/computer-use/Codex Computer Use.app/Contents/MacOS/SkyComputerUseService"
    sw_vers -productVersion
    
    # 2. Confirm the deterministic crash loop
    ls -t ~/Library/Logs/DiagnosticReports/SkyComputerUseService-*.ips | head
    
    # 3. Measure the leak rate
    z(){ ps -axo state= | awk '$1 ~ /^Z/ {n++} END {print n+0}'; }
    a=$(z); sleep 30; b=$(z); echo "rate=$(( (b-a)*2 ))/min"
    
    # 4. Confirm the zombies' parent is the main GUI process
    ps -axo ppid,state= | awk '$2 ~ /^Z/ {print $1}' | sort | uniq -c | sort -rn | head
    
    # 5. Count actual launches by unique PID (replace <MAIN_PID>)
    P=<MAIN_PID>; out=/tmp/pids.txt; : > "$out"
    end=$((SECONDS+20))
    while [ $SECONDS -lt $end ]; do
      ps -axo pid=,ppid=,state=,command= | awk -v p="$P" '$2==p && $3 !~ /^Z/ {pid=$1; $1="";$2="";$3=""; print pid"\t"$0}' >> "$out"
    done
    sort -u "$out" | awk -F'\t' '{print $2}' | awk '{print $1}' | sort | uniq -c | sort -rn

    Suggested fixes, in priority order

    1. Gate the Computer Use component on LSMinimumSystemVersion before download and
      before launch.
      Shipping a minos 14.4 binary to a 14.2.1 host is the underlying
      packaging defect — it should never have been installed, let alone launched in a loop.
    2. Add waitpid() to the Computer Use service spawn path. Per Finding 2 this path is
      the outlier; sibling spawns in the same process are reaped correctly.
    3. Add exponential backoff and a retry cap. A pre-main() SIGBUS is not a transient
      error; retrying every 1.7 s forever cannot succeed.
    4. Make the plugin / MCP enabled = false flags actually gate the service spawn, or
      document a supported way to disable Computer Use. Right now there is no user-accessible
      mitigation short of updating the OS.
    5. Surface the failure. Today it is entirely silent — no error, no degraded-mode
      notice. The first user-visible symptom is the whole machine losing the ability to fork,
      roughly 70 minutes later.

    Items 1 and 3 each independently stop the bleeding on affected hosts. Item 2 is the
    actual fix. Item 4 would have let users work around it without an OS upgrade.


    Workaround that does work

    Updating the OS to satisfy the minos 14.4 requirement (in my case macOS Sonoma 14.8.8)
    removes the trigger, since the binary can then load and the crash loop ends. Note this
    does not fix the missing waitpid() — it only stops it from being hit ~36 times a minute.

    Until then, the only reliable mitigation is to quit ChatGPT when not actively using
    it
    ; the backlog is reaped by launchd the moment the main process exits.

  16. TralafalgarV commented on Aug 7, 2026

    @TralafalgarV

    Update: confirmed by controlled experiment — OS upgrade takes the leak from ~35/min to 0/min

    Following up on my earlier report with verification. I upgraded the host past the
    minos 14.4 requirement of the bundled SkyComputerUseService binary. No configuration
    was changed between the two states — only the OS version.

    Before After
    OS macOS 14.2.1 (Darwin 23.2.0) macOS 26.6, build 25G72 (Darwin 25.6.0)
    minos 14.4 satisfied? no (14.2.1 < 14.4) yes
    SkyComputerUseService SIGBUS every ~1.7 s, pre-main() runs normally as a stable long-lived process
    Zombie leak rate ~35/min 0/min (three consecutive 30 s samples, all 0)
    Accumulated zombies 905 0
    User process table usage 1224 / 2666 519 / 2666

    The last crash report is timestamped 2026-08-07 11:55:15, immediately before the
    upgrade; none have been produced since (~1 h 40 m of uptime at the time of writing).

    This closes the causal chain. The binary was simply unable to load on a host below its
    minos, and what produced the leak was the unbounded, no-backoff retry of a spawn that
    was guaranteed to fail
    , combined with the missing reap. Once the binary can load, the
    same spawn path — with the same configuration — behaves correctly and leaks nothing.

    Two things I want to stress, because the upgrade is a workaround and not a fix:

    1. The missing waitpid() on this path is still there. The upgrade only stops it from
      being hit ~36 times a minute. Any future spawn failure on that path will resume leaking
      at the same rate.
    2. There is still no user-accessible mitigation. Setting enabled = false on both
      [plugins."computer-use@openai-bundled"] and [mcp_servers.computer-use], followed by
      a full app restart, did not prevent the spawn loop — verified at ~33 spawns/min while
      both were disabled. Users below the required minos who cannot upgrade have no option
      other than quitting the app periodically.

    So I would still suggest gating the component on LSMinimumSystemVersion before download
    and before launch, adding backoff plus a retry cap, and adding the missing reap. Happy to
    provide raw crash reports or additional sampling data if useful.

  17. s0601737-boop commented on Aug 9, 2026

    @s0601737-boop

    Additional reproduction on ChatGPT/Codex Desktop for macOS.

    Environment:

    • ChatGPT/Codex Desktop: 26.730.61639
    • Embedded Codex CLI: 0.147.0-alpha.1.2
    • Architecture: arm64

    Isolation test:

    • The local codex-usage-tracker plugin was removed before testing; it no longer appeared in the embedded CLI plugin list.
    • Zombie processes increased from 129 to 141 within 20 seconds after removal.
    • All observed zombies had the same parent PID (81042), in the Codex Desktop process tree.
    • The standalone token overlay continued running independently (single process, no helper children).

    This makes the tracker plugin an unlikely root cause. Restarting Desktop clears the accumulated zombies temporarily, but the issue can recur during a long-running session.

    Please investigate Desktop child-process lifecycle/reaping for app-server and MCP helpers, including cleanup after a thread/tool ends or fails. No source code, prompts, or private files are attached.

  18. scyuo commented on Aug 10, 2026

    @scyuo

    Follow-up: Sonoma 14.8.9 is sufficient; still at 0 zombies after 23+ hours

    A lower-risk workaround than moving to Tahoe is now confirmed on the affected host.

    I upgraded the OS from macOS 14.2.1 to macOS Sonoma 14.8.9 (23J631), while keeping the ChatGPT/Codex Desktop configuration unchanged. The bundled SkyComputerUseService still declares minos 14.4, so 14.8.9 satisfies its loader requirement.

    Post-upgrade results:

    • ChatGPT/Codex Desktop: 26.803.41515
    • SkyComputerUseService: one stable, long-lived process
    • Zombie count immediately after upgrade: 0
    • Four consecutive samples over 45 seconds: 0 -> 0 -> 0 -> 0
    • Next-day verification after more than 23 hours of continuous uptime: 0 -> 0 -> 0 across another 30-second sample
    • The same service process remained alive for more than 23 hours
    • No new SkyComputerUseService crash reports were produced after the upgrade

    Before the upgrade, the host was below the binary's minimum OS version and the service failed before main() roughly every 1.7 seconds, leaking about 35 zombies per minute. After upgrading to Sonoma 14.8.9, the measured leak rate remains 0/min.

    For users affected specifically by this minos 14.4 mismatch, upgrading the OS is an effective temporary workaround. Sonoma 14.8.9 is directly confirmed; upgrading all the way to Tahoe is not required. Other macOS releases at or above 14.4 should satisfy the binary's declared minimum version, although they were not all individually tested.

    This remains a workaround, not the underlying fix: the spawn path should still gate on the binary's minimum OS version, add retry backoff/capping, and reap failed children correctly.

  19. asheppard47 commented on Aug 17, 2026

    @asheppard47

    Observed

    • ChatGPT.app 26.810.52044 (build 6662) on macOS 26.6.1 (build 25G76), Apple Silicon, 16 GiB RAM; main app uptime was 2 days 7 hours.
    • One long-lived app-server; 24 sleeping node_repl direct children totaling 157.2 MiB, oldest 12 hours 19 minutes; 3 stdio app-server helpers totaling 52.8 MiB; 1 SkyComputerUse process totaling 14.3 MiB; zero zombies.
    • 51 Chrome renderer processes totaled 2795.9 MiB while only 3 visible Chrome tabs were open. Swap was 14336 MiB total, with 12946.56 MiB used. The APFS container was 98.3% used, with 4.1 GB free.
    • Three detached CUA kernel processes had PPID 1, 0.0% CPU, and ages from 5 days 23 hours to 7 days 4 hours. No code_sign_clone directories were present.

    Inference

    This reproduces on an OS above the reported minos 14.4 threshold, so it is not the loader-crash subtype described in later comments.

    Could maintainers provide a supported cleanup command or lifecycle fix? A global pkill is unsafe because long-running tasks may still own workers.

  20. jonmc12 commented on Aug 19, 2026

    @jonmc12

    Corroborating reproduction on a current macOS desktop build

    I reproduced the same stale-helper accumulation and recovered the affected Codex task with scoped cleanup.

    Environment

    • ChatGPT/Codex Desktop: 26.814.41407 (bundle 6720)
    • macOS, Apple Silicon

    Observed

    • 108 SkyComputerUseClient processes
    • 129 node_repl processes
    • Many helpers were several days old and clearly predated the current app session
    • Aggregate helper footprint was approximately 3.6 GB RSS and 55% CPU
    • The affected task repeatedly reported:
      stream disconnected before completion: error sending request for url (https://chatgpt.com/backend-api/codex/responses)

    Scoped cleanup experiment

    I selected only helper processes older than one hour that predated the current app session, preserving the GUI, primary app-server, and current-session helpers. I sent SIGTERM only—no task/session files were removed.

    • Stale helpers terminated: 217
      • 114 node_repl
      • 103 SkyComputerUseClient
    • Reclaimed from the stale set: approximately 3.2 GB RSS and 60.2% CPU
    • Remaining current-session helpers: 24
    • Remaining helper footprint: approximately 413 MB RSS and 3.4% CPU
    • Remaining helpers older than one hour: 0

    After cleanup, the original affected task successfully completed a bounded readiness request and returned to idle. A long, tool-heavy run has not yet been used to prove complete resolution, so I am treating this as strong lifecycle/resource evidence rather than proof that every stream disconnect shares this cause.

    This appears to support session/tool-scoped lifecycle cleanup, retry/backoff, and child reaping rather than relying on global process termination.

  21. yulonglin commented on Aug 25, 2026

    @yulonglin

    Still reproducible on 26.818.31338. SkyComputerUseService respawned within minutes even after codex plugin remove computer-history@openai-bundled, deleting the notify turn-ended hook from ~/.codex/config.toml, and removing ~/.codex/computer-use — the app re-adds the hook and relaunches from the copy bundled inside ChatGPT.app. Note the AX polling charges its cost to the victim apps' main threads, so the service looks idle in Activity Monitor while causing system-wide typing lag; sample <victim app> during a stutter shows it. Only reliable mitigation we've found: a launchd agent that pkills SkyComputerUse|CUALockScreenGuardian every 30s.

  22. diegocantarero commented on Sep 12, 2026

    @diegocantarero

    Current reproduction on Codex Desktop 26.903.71938 (build 8576), sanitized: one main Desktop process and one SkyComputerUseService are running, but the same live session accumulated 57 SkyComputerUseClient processes and 38 node_repl processes. The clients were spawned in repeated timestamp clusters over roughly 24 minutes and remain parented by the active Codex process; they are not duplicate GUI instances. Aggregate sampled RSS was about 1.68 GiB for SkyComputerUseClient and 0.49 GiB for node_repl. I did not terminate them because multiple active tasks may own live tool sessions. During the same observation, read-only Computer Use acquisition succeeded for Activity Monitor, Finder, and Chrome, while earlier runs had intermittent -10005/noWindowsAvailable. This strengthens the lifecycle/leak hypothesis and suggests the app needs per-session ownership plus safe teardown/reaping after each MCP/Computer Use session ends. No raw logs, task IDs, account data, or private paths are included.

  23. yulonglin commented on Sep 12, 2026

    @yulonglin

    This has been so bad I've switched off computer use, disabled all computer use permissions and set up jobs to reap SkyComputerUse processes ever 30s, as mentioned above.

  24. giorgio-zamparelli commented on Sep 30, 2026

    @giorgio-zamparelli

    Adding a measured macOS case where one long-lived Computer Use helper with an active capture stream was enough to cause noticeable typing lag and skipped cursor frames. This did not require a large collection of live helpers.

    Environment:

    • macOS 26.6.2 (25G83), Apple M5 Max, 18 logical cores, 128 GB RAM.
    • Four displays: two 6144 x 3456 displays at 60 Hz, one 5120 x 2880 display at 60 Hz, and the built-in 3024 x 1964 display.
    • The desktop bundle is com.openai.codex; it displays the name ChatGPT on this installation. The version currently on disk is 26.924.22138 (11645). The app had been running since before the investigation, so that is not a verified running-app version.
    • The running SkyComputerUseService bundle was com.openai.sky.CUAService, version 26.923.1001242 (1001242), located in the Codex Computer Use helper bundle. It had been alive for about 30 hours.
    • Coding agents had mostly been moved to a different Mac. Orca remained open locally. Overall CPU had spare capacity, memory pressure was normal, and swap was zero.

    The helper's native sample showed com.openai.sky.CUAService.CaptureStream callbacks, CMCapture sample-buffer processing, and IOSurface work. The separate Orca Computer Use helper was idle. I have not established whether the OpenAI stream was started by Computer Use, Computer History, or foreground-app context. Those features share this helper, so process lifetime alone is not proof of an abandoned session.

    Controlled local comparison, with the desktop app and Orca left running:

    Measurement Before ending the helper First interval after Later steady interval
    WindowServer CPU, percent of one core 93.2% 28.0% 20.3%
    replayd CPU, percent of one core 5.9% 0.0% 0.0%
    Interval length 5.45 s 8.15 s 8.17 s

    The helper itself had been using about 16-19% of one CPU core. Ending that specific helper with SIGTERM released the capture stream. CPU die temperature subsequently fell from about 75 C to 51-53 C, GPU temperature from about 69 C to 49-51 C, fans from roughly 5,357 to 2,502 RPM, and thermal state from Fair to Nominal. These temperatures/fan readings are follow-up observations rather than identical-duration averages. The user immediately confirmed that input and cursor movement were substantially more responsive.

    Temporarily suspending and resuming the helper had produced a much smaller WindowServer change (about 92% to 81%), so pausing a process while retaining capture resources did not reproduce the benefit of fully releasing the stream. Pausing the performance monitor did not materially reduce WindowServer load.

    This is evidence that the active capture pipeline was a major contributor on this setup; it does not identify the exact native-service bug or establish that all WindowServer/input stalls have this cause. The high-resolution display configuration may amplify the cost.

    Useful fixes or diagnostics to investigate:

    1. Identify the owning feature and active consumers for each capture stream. Expose whether capture continues for Computer History, app context, or an active Computer Use task.
    2. Release capture streams and surfaces when no enabled feature needs them, and verify stop/pause/disable and app shutdown paths. If continuous history capture is intentional, consider its idle cadence and display-resolution cost.
    3. Test input latency and WindowServer/replayd load with multiple high-resolution displays, including when all coding work is remote and no local Computer Use task is executing.
    4. Make a supported capture stop/disable control sufficient, rather than requiring users to terminate helper processes.

    I am also preparing a Mac Performance Monitor diagnostic for this combination of display-service and capture-helper activity. That can point users toward the capture owner, but the underlying stream lifecycle/performance fix belongs here.

    Only summarized measurements are included here; no raw process inventory, screen images, or user activity history is attached.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    appIssues related to the Codex desktop appapp-serverIssues involving app server protocol or interfacesbugSomething isn't workingcomputer-usemcpIssues related to the use of model context protocol (MCP) serversperformance

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions