Regression: Playwright MCP stdio processes still leak after #16895 fix — 213 orphaned pairs, 13.6 GB RSS

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

Regression: Playwright MCP stdio processes still leak after #16895 fix — 213 orphaned pairs, 13.6 GB RSS

Environment

| Field | Value |
|-------|-------|
| Codex CLI | codex-cli 0.120.0 |
| Codex App | 26.409.20454 |
| Subscription | ChatGPT Pro |
| Platform | macOS 26.4.1 (25E253), Apple M3 Pro, 36 GB RAM |
| MCP server | @playwright/mcp@latest (stdio transport) |

Summary

Issue #16895 (stdio MCP child processes not cleaned up) was closed on April 9 as fixed. As of April 14 on codex-cli 0.120.0, the bug is still present — specifically for @playwright/mcp configured as a global stdio MCP server in config.toml.

After a normal day of use in Codex Desktop involving multi-agent / subagent workflows, 213 leaked npm exec @playwright/mcp@latest + node playwright-mcp process pairs accumulated under the codex app-server process tree, consuming 13.6 GB of RSS and contributing to Activity Monitor reporting ~24 GB total for Codex.

This is the same root cause as #17574 (xcodebuild/chrome-devtools MCP leak) and #12491 (original 37 GB / 1300+ zombie report). The lifecycle fix from #16895 did not fully resolve the problem — subagent thread teardown still does not reap MCP child processes.

Forensic data

Process tree before cleanup
codex app-server (PID 4938, 993.8 MB physical footprint)
├── 213 × npm exec @playwright/mcp@latest  (~35 MB each = 7.4 GB)
│   └── each spawns: node playwright-mcp   (~30 MB each = 6.2 GB)
└── (total leaked MCP processes: 426, total leaked RSS: 13.6 GB)
Memory breakdown (physical footprint via vmmap)

| Process | PIDs | Physical Footprint |
|---------|------|--------------------|
| Codex main (Electron) | 4892 | 177.0 MB |
| codex app-server (Rust) | 4938 | 993.8 MB |
| Renderer #1 | 4950 | 560.2 MB |
| Renderer #2 | 5676 | 134.5 MB |
| Renderer #3 | 5729 | 134.7 MB |
| GPU process | 4895 | 514.6 MB |
| Network service | 4897 | 16.3 MB |
| Tracing service | 61658 | 51.3 MB |
| 213 × npm exec playwright | various | 7,550 MB (7.4 GB) |
| 213 × node playwright-mcp | various | 6,304 MB (6.2 GB) |
| Grand total | 433 processes | ~15.3 GB RSS |

app-server malloc analysis (PID 4938)
DefaultMallocZone: 179.3 MB resident + 792.5 MB swapped
                   5,023,995 allocations, 880.6 MB allocated
                   335.8 MB stack across 61 threads

The app-server itself has ~5M live allocations and 792 MB swapped out — likely holding session state / MCP connection metadata for all 213 leaked connections.

Reproduction context
  • config.toml has @playwright/mcp@latest as a global stdio MCP server
  • Normal usage pattern: Codex Desktop with multi-agent workflows (max_threads = 20, max_depth = 2)
  • 927 total threads in state_5.sqlite, 476 spawn edges
  • Heavy subagent use: one parent thread spawned 58 children, another 37
  • Each subagent session appears to spawnsession_initmcp_manager_init → spawn new MCP processes
  • When the subagent session closes, the MCP processes are not terminated
Log evidence from logs_2.sqlite

The codex_core::spawn and codex_mcp::mcp_connection_manager modules show the pattern:

  1. thread_spawn creates a new subagent session
  2. session_initmcp_manager_init starts fresh MCP connections (including Playwright)
  3. Session completes and thread is marked closed in thread_spawn_edges (status = "open" — note: even completed edges show status "open", suggesting the edge status itself may not be updated)
  4. The spawned npm exec @playwright/mcp@latest and its node playwright-mcp child remain alive indefinitely
Immediate workaround
pkill -f "playwright-mcp"
pkill -f "npm exec @playwright/mcp"

This instantly freed ~13.6 GB. Playwright MCP respawns correctly for the next thread that needs it.

Expected behavior

  • MCP child processes should be terminated when their owning thread/session closes
  • Subagent sessions should either share the parent's MCP connections or reliably clean up their own
  • The codex app-server should not accumulate unbounded child processes over the course of normal use

Related issues

  • #16895 — closed as fixed April 9, but the bug persists (this report)
  • #17574 — same leak pattern with xcodebuildmcp and chrome-devtools-mcp (open)
  • #12491 — original report: 1300+ zombies, 37 GB leak (open)
  • #12333 — duplicate Serena MCP instances from multi-agent spawns (closed)

View original on GitHub ↗

16 Comments

github-actions[bot] contributor · 3 months ago

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

  • #17574
  • #17115
  • #16895
  • #16256

Powered by Codex Action

RedesignedRobot · 3 months ago

Root Cause Analysis

Verified across 4 independent passes of the codex-rs/ source on main.

Update (2026-04-19): fix landed on branch RedesignedRobot:codex:fix/mcp-subprocess-cleanup, rebased onto current main, cargo check -p codex-core -p codex-mcp clean. Shipped impl is a sync self.clients.clear() leaning on the Drop chain (equivalent effect, simpler than the async draft below). Also extends the same teardown to the submission_loop channel-close fallthrough, which previously drained only guardian state. The paths referenced below predate the codex-rs/core/src/codex.rs split: current locations are codex-rs/core/src/session/handlers.rs (shutdown at L910, submission_loop fallthrough at L1200) and codex-rs/codex-mcp/src/mcp_connection_manager.rs (impl block).

Summary

McpConnectionManager has no cleanup mechanism. handlers::shutdown() terminates shell processes but never touches MCP. Child processes accumulate because nothing initiates the drop chain to ProcessGroupGuard.

The drop chain that should fire but does not

handlers::shutdown()
  -> unified_exec_manager.terminate_all_processes()   // shell cleanup - present
  -> mcp_connection_manager.shutdown()                // MCP cleanup - missing (method does not exist)
    -> drop AsyncManagedClient
      -> drop ManagedClient (Arc<RmcpClient>)
        -> drop ClientState::Ready._process_group_guard
          -> ProcessGroupGuard::drop()
            -> terminate_process_group(pgid)          // SIGTERM, then SIGKILL after 2s

ProcessGroupGuard::drop() at rmcp_client.rs:385 works correctly. It is never invoked.

Verified gaps

1. handlers::shutdown() ignores MCPcodex.rs L5814

Calls unified_exec_manager.terminate_all_processes() for shell processes. Zero references to mcp_connection_manager.

2. McpConnectionManager has no cleanup codemcp_connection_manager.rs, 1824 lines

No Drop impl. No shutdown(). No close(). No terminate(). The clients: HashMap<String, AsyncManagedClient> holding Arc<RmcpClient> references is never cleared.

3. shutdown_live_agent() does not force MCP teardownagent/control.rs

Sends Op::Shutdown, calls remove_thread() (decrements Arc refcount), calls release_spawned_thread(). Does not touch MCP. If any reference to the Session survives, the drop chain stalls.

4. CancellationToken is startup-onlymcp_connection_manager.rs

Wraps start_server_task().or_cancel(&cancel_token). Only cancels in-progress connection attempts. Inert once a client reaches ClientState::Ready.

5. Each subagent creates its own MCP connectionscodex.rs, Session::new()

Each Session::new() calls McpConnectionManager::new(), spawning fresh child processes per subagent. Per-session scoping is correct. Per-session cleanup does not exist.

PR #17223 was incomplete

Changed 3 files in codex-rs/app-server/. Fixed connection_closed() in codex_message_processor.rs — handles WebSocket client disconnect. Does not address subagent internal teardown. Subagents do not go through the WebSocket disconnect path.

Log evidence

From logs_2.sqlite on this machine:

  • MCP init events (current process): 3
  • MCP cleanup events (current process): 0
  • Subagent threads spawned: 521
  • thread_spawn_edges still status "open": 315
  • rmcp "RunningService dropped without explicit close()" warnings: 4 (only in dead processes)

Fix (as shipped on branch)

// 1. McpConnectionManager::shutdown — sync drain; Drop chain handles the rest
impl McpConnectionManager {
    pub fn shutdown(&mut self) {
        self.clients.clear();
    }
}

// 2. handlers::shutdown() — add after unified_exec_manager cleanup
sess.services.mcp_connection_manager.write().await.shutdown();

// 3. submission_loop channel-close fallthrough — mirror the same teardown
sess.services.unified_exec_manager.terminate_all_processes().await;
sess.services.mcp_connection_manager.write().await.shutdown();

The original draft used an async shutdown().await with explicit managed.client.close().await per client. The sync clients.clear() variant is equivalent: dropping AsyncManagedClient releases the Arc<RmcpClient>, which drops ProcessGroupGuard and triggers terminate_process_group. Fewer moving parts, no await needed.

Files

| File | Issue |
|------|-------|
| codex-rs/core/src/codex.rs L5814 | handlers::shutdown() missing MCP cleanup |
| codex-rs/codex-mcp/src/mcp_connection_manager.rs | No shutdown/Drop/close |
| codex-rs/core/src/agent/control.rs | shutdown_live_agent() does not trigger MCP teardown |
| codex-rs/rmcp-client/src/rmcp_client.rs L385 | ProcessGroupGuard::drop() works but is never reached |

RedesignedRobot · 3 months ago

Reference implementation: https://github.com/openai/codex/compare/main...RedesignedRobot:codex:fix/mcp-subprocess-cleanup

2 files, +23/-2. Covers both handlers::shutdown() (Op::Shutdown path) and the submission_loop channel-close fallthrough. cargo check -p codex-core -p codex-mcp clean.

Verified across 7 independent code review passes covering Drop semantics, call ordering, concurrency safety, and termination path coverage. Covers all primary termination paths. No deadlock risk in the Drop chain.

fortunexbt · 3 months ago

@RedesignedRobot this is the worst MCP lifecycle failure I have seen documented — 426 orphaned process pairs from 927 threads because subagent teardown never reaps MCP children. The forensic detail is excellent.

The operators I talk to hit this most often in unattended multi-agent runs where the process leak accumulates over hours until the host OOMs. The root cause you traced — no McpConnectionManager.shutdown() in handlers::shutdown() — explains why PR #17223 only fixed the WebSocket disconnect path while subagents kept leaking.

Is the bigger gap for you right now per-agent process isolation so leaks stay bounded, or runtime visibility that surfaces which subagents leaked their MCP processes before the host runs out of memory?

RedesignedRobot · 3 months ago

The immediate gap is the missing teardown call. handlers::shutdown() never invokes cleanup on McpConnectionManager. The reference implementation linked above addresses this directly. Once the drop chain fires correctly, the leak stops at source.

Process isolation is a secondary concern. Each subagent already gets its own McpConnectionManager via Session::new(), so the scoping is correct. The problem is purely that the cleanup path was never wired. Bounding leaks per-agent would be defense-in-depth, not a fix for the root cause.

Runtime observability would be the more useful follow-up. A process count metric emitted per-session at teardown (or periodically by the app-server) would catch regressions and make leaks visible before they reach OOM. The structured logging infrastructure is already there (logs_2.sqlite, codex_mcp::mcp_connection_manager target). It just needs a counter for active stdio child processes.

For unattended multi-agent runs specifically, a lightweight reaper in the app-server that periodically reconciles live MCP child PIDs against active thread_spawn_edges would catch any edge cases the explicit teardown misses (forced SIGTERM, caller timeouts, Arc reference leaks).

GoBeromsu · 3 months ago

Additional data point: CLI-only (no Codex App), tmux-based workflow with oh-my-codex

Environment

| Field | Value |
|-------|-------|
| Codex CLI | codex-cli 0.120.0 |
| Platform | macOS Darwin 25.4.0, Apple Silicon (arm64) |
| oh-my-codex | v0.12.6v0.13.0 |
| tmux | 3.6a |
| MCP servers | @playwright/mcp, xcodebuildmcp, notebooklm-mcp + 4 OMX servers |

Observed

After ~4 days of normal CLI usage (no Codex App involved), accumulated:

  • 112 orphaned OMX MCP server processes (cleaned by omx cleanup)
  • 41+ orphaned codex-native MCP processes (playwright-mcp, xcodebuildmcp, notebooklm-mcp) — required manual pkill
  • 12 stale detached tmux sessions left behind from previous codex launches
  • Total orphaned processes: ~153

Impact

New codex interactive sessions failed to start — immediately exited ([exited] in tmux). After killing all orphaned processes and stale tmux sessions, codex launched normally again.

Root cause confirmation

Same as this issue: each codex session spawns its own MCP server instances. When codex exits (especially via signal in tmux), MCP child processes are reparented to launchd (PID 1) and survive indefinitely. No process-group cleanup occurs.

Key difference from Codex App reports

This is purely CLI + tmux — no Electron, no app-server. The codex Rust binary itself (codex CLI) does not send SIGTERM to MCP children on session close. This confirms the leak is in codex-core, not just in the app-server lifecycle.

Workaround

oh-my-codex's omx cleanup handles OMX-owned MCP servers, but codex-native ones (playwright, xcodebuildmcp, etc.) require manual cleanup:

pkill -f "playwright-mcp|xcodebuildmcp|notebooklm-mcp"
RedesignedRobot · 3 months ago

Hey @jif-oai @etraut-openai, dropping a note in case it's useful.

Patch is ready on RedesignedRobot:codex:fix/mcp-subprocess-cleanup, rebased on main.

Fix: adds McpConnectionManager::shutdown() (drains the client map) and calls it from handlers::shutdown() alongside the existing shell teardown. Dropping the Arc<RmcpClient> refs lets ProcessGroupGuard::drop() fire and SIGTERM the MCP child process groups. This is the subagent shutdown path #17223 didn't cover.

Also covers the submission_loop channel-close fallthrough, which previously drained only guardian state and leaked the same processes on abnormal channel closure.

2 files, +23/-2, no behavior change on the happy path. cargo check -p codex-core -p codex-mcp clean.

Happy to open as a PR if you can invite me in.

Thanks.

gregoramon · 2 months ago

Additional data point: Codex.app desktop, version 0.125.0-alpha.3

Confirms the regression persists in the current alpha.

Environment

| Field | Value |
|---|---|
| Codex | codex-cli 0.125.0-alpha.3 (Codex.app desktop) |
| Platform | macOS 26.3.1, Apple Silicon |
| Codex.app uptime | ~33 hours (single launch) |
| MCP server | @playwright/mcp@latest |

Observed pattern

Six orphaned npm exec @playwright/mcp@latest processes, all with PPID = Codex app-server (PID 46443). They spawn in pairs ~1–2 minutes apart, on three distinct occasions over the 33h Codex.app uptime:

| PIDs | Pair started | Age |
|---|---|---|
| 47054, 48285 | Sun Apr 26 10:01:49 / 10:03:48 | 31h |
| 21593, 22586 | Sun Apr 26 20:20:47 / 20:22:18 | 21h |
| 83017, 84080 | Mon Apr 27 16:05:23 / 16:07:09 | ~1h (active) |

Matches the root cause @RedesignedRobot traced: McpConnectionManager was replaced (3× here, presumably on session/reconnect) without draining its clients map, so the Arc<RmcpClient> refs that should trigger ProcessGroupGuard::drop() are never released. Codex.app itself never exited, so no shutdown path fires.

46443  Codex app-server (alive 33h)
├── 47054 @playwright/mcp    ← stale, 31h
├── 48285 @playwright/mcp    ← stale, 31h
├── 21593 @playwright/mcp    ← stale, 21h
├── 22586 @playwright/mcp    ← stale, 21h
├── 83017 @playwright/mcp    (current session)
└── 84080 @playwright/mcp    (current session)

The 31h-old pair had spawned \chrome-headless-shell\ children running 1d 3h, two of them holding 13.3% / 8.3% CPU steady.

Resource impact

On a 64 GB machine, after 33h of Codex.app uptime:

  • Compressor: 20 GB physical / ~47 GB logical
  • Swap: 2.1 GB used (3 GB allocated)
  • Memory pressure high enough to be visible in interactive workloads (Zoom, Chrome)

\kill <pids>\ on the stale orchestrators terminated cleanly — they were orphaned, not stuck.

etraut-openai contributor · 2 months ago

I have a pending PR that should address this. If you're so inclined, you could try building the PR from source and confirm that it does address the problem you're seeing.

nazordz · 2 months ago

Additional data point: still reproducible with @mui/mcp on macOS after the MCP shutdown fix.

Environment

  • Codex: 0.128.0
  • Platform: macOS, Apple Silicon
  • MCP server: @mui/mcp@latest configured globally via stdio/npx

Config:

[mcp_servers.mui-mcp]
command = "npx"
args = ["-y", "@mui/mcp@latest"]

Observed

After repeated Codex sessions, I found:

  • 59 npm exec @mui/mcp@latest parent processes
  • 59 matching node .../.npm/_npx/.../node_modules/.bin/mcp child processes
  • 58 of the npm exec @mui/mcp@latest parents had PPID 1, adopted by launchd
  • Total MUI MCP-related RSS was about 767 MB
  • CPU was mostly idle, but processes accumulated across sessions

Example stale process group:

PID 1428  PPID 1     PGID 1428  npm exec @mui/mcp@latest
PID 1994  PPID 1428  PGID 1428  node /Users/.../.npm/_npx/.../node_modules/.bin/mcp

Current live Codex session creates a fresh group:

PID 8934  PPID 51604  PGID 8934  codex
PID 8973  PPID 8934   PGID 8973  npm exec @mui/mcp@latest
PID 9518  PPID 8973   PGID 8973  node /Users/.../.npm/_npx/.../node_modules/.bin/mcp

Permission check

The stale processes are owned by my user. A harmless signal permission check succeeds outside Codex's restricted context:

kill -0 1428
kill -0 -1428

Both return success.

Codex log evidence

Codex logs contain repeated MCP cleanup failures:

WARN codex_rmcp_client::stdio_server_launcher: Failed to terminate MCP process group ... Operation not permitted (os error 1)
WARN codex_rmcp_client::rmcp_client: Failed to terminate MCP process group ... Operation not permitted (os error 1)

Interpretation

This looks like the stdio MCP cleanup leak still reproducing with @mui/mcp@latest on Codex 0.128.0. The processes are same-user and signalable from the host, but Codex cleanup appears to fail from its sandbox/restricted context, leaving MCP process groups orphaned under launchd.

mySebbe · 2 months ago

Additional Windows process-growth data point from Codex Desktop 26.506.3741.0.

This is not @playwright/mcp specifically, but it looks like the same class of browser automation lifecycle problem: long-lived Chrome/Playwright controlled browser profiles are not returning to a bounded baseline.

Observed live process footprint:

controlled_chrome_profile / remote-debugging-port=9444:
  chrome.exe processes: 74
  working set: ~10.4 GB
  oldest process: 2026-05-12 02:19:30
  newest process: 2026-05-12 13:27:48

playwright_chromiumdev_profile temp profile:
  chrome.exe processes: 20
  working set: ~1.6 GB
  oldest process: 2026-05-12 01:42:55
  newest process: 2026-05-12 02:11:16

CodexFabChromeProfile / remote-debugging-port=9333:
  chrome.exe processes: 7
  working set: ~0.47 GB

The largest group is the fallback/controlled Chrome profile, not the node_repl.exe MCP hosts. A local conservative cleanup is now managing stale node_repl.exe, but browser fallback Chrome processes remain the dominant footprint.

Cross-links where the routing/fallback side is discussed: #21868, #19365, #20417.

waypointmini-rgb · 1 month ago

Independent reproduction — same lifecycle bug still present on a much later Codex App build, with a different MCP server set, so this is not specific to @playwright/mcp or to the chrome-devtools-mcp / xcodebuildmcp pair in #17574.

Environment

| Field | Value |
|-------|-------|
| Codex App | 26.527.30818 (build 3370) |
| Codex CLI | codex-cli 0.135.0-alpha.1 |
| Bundle id | com.openai.codex |
| Subscription | ChatGPT Pro |
| Platform | macOS 26.5 |
| MCP servers configured | obsidian-mcp, notebooklm-mcp, @playwright/mcp@latest (stdio), SkyComputerUseClient mcp |

Forensic data — single live Codex App session, ~19h uptime

Single live codex app-server --analytics-default-enabled parent (PID 49055, age 19h14m) owns the entire leaked tree. Parent is alive and healthy; children are not orphans.

codex app-server --analytics-default-enabled (PID 49055, alive 19h14m)
├── 30 × obsidian-mcp
├── 24 × SkyComputerUseClient mcp
├── 19 × notebooklm-mcp
└── 13 × node npm exec @playwright/mcp@latest
   (54 MCP children rooted directly on the live app-server PID)

This is the exact pattern from #17574 — every subagent / computer-use session refresh runs session_init.mcp_manager_init, spawns a fresh MCP quartet, and the previous quartet is never reaped even though the new session is now wired to its own helpers. Ratio is roughly 1 quartet per session refresh; with computer-use refreshing about hourly, that's ~20 quartets accumulated in this window. Matches the per-session-init growth rate in #17574 exactly.

Why this is not the SDK-side stdin-EOF leak

A common confusion: the typescript-sdk side has a related leak class fixed by modelcontextprotocol/typescript-sdk#2003StdioServerTransport not exiting on stdin close. That fix only helps when the MCP server's parent has actually died and stdin EOF has been delivered.

Here the Codex App app-server parent is still alive and (I assume) still holding the stdio pipes open for each abandoned child. From the child's perspective stdin is not closed, so no SDK-side stdin-EOF / close listener can save us. The teardown receipt has to come from the app-server side — either explicit kill of the prior session's child set on session-init, or closing the pipes so the SDK-side EOF handler can take over.

Suggested triage

  • This bug class still ships on 26.527.30818, so #16895's fix isn't a full cure even in versions much later than the one in #17832.
  • Probably worth treating #17574 and this issue as the same root cause and re-opening lifecycle work that explicitly reaps the prior session's MCP set on session_init.mcp_manager_init — not relying on parent-process death to cascade cleanup.
  • For users hitting this today: a process-level reaper that walks codex app-server's child set and TERMs the staler quartets does work as a local mitigation, but is upstream-of-real-fix.

Happy to share further lsof / ps -axo pid,ppid,etime,command captures or a longer time series if it would help reproduce on the maintainer side.

nukk-pain · 7 days ago

Adding a current non-Playwright reproduction of the same subagent stdio MCP teardown failure.

Environment

  • Codex CLI: 0.144.3
  • Surface: CLI, multi-agent v2
  • Platform: macOS 26.5.1, Darwin arm64
  • MCPs: Codegraph plus OMO-provided stdio MCP servers
  • Per-process soft file descriptor limit: 256

Observed

Across local session JSONL logs, I found 54 distinct tool-call failures with:

Failed to create unified exec process: Too many open files (os error 24)

The failures affected 11 session logs over about 67 minutes. Once degraded, even pwd and git status --short could not start.

For one long-running root Codex process:

  • 17 spawn_agent calls in the root session log
  • 34 direct child processes still attached to the root
  • 16 Codegraph launcher children
  • 16 additional OMO MCP launcher children
  • 173 open file descriptors, including 99 pipes
  • the latest agent snapshot showed only 2 running and 9 completed agents

The direct child count tracks the accumulated subagent MCP stacks rather than only currently running agents. Completed subagents therefore leave stdio MCP processes/pipes attached to the long-lived root until it approaches the 256 FD limit.

Expected

Subagent completion should tear down its stdio MCP stack, or the parent should converge to a bounded baseline. A completed subagent should not leave enough pipes behind to prevent unrelated local commands from spawning.

Local mitigation

I reduced max_concurrent_threads_per_session from 1000 to 16. This limits impact but does not fix teardown. No private logs or repository data are attached.

Turnstyle · 3 days ago

I am seeing the same MCP lifecycle leak with:

  • Codex Desktop: 26.707.91948 (build 5440)
  • Embedded Codex CLI: 0.144.5
  • macOS: 26.5.2 (build 25F84)
  • Hardware: Apple silicon with 128 GiB RAM

After completed subagent work, approximately 170 MCP-related processes remained alive. Their summed RSS was approximately 16.5 GiB. I understand summed RSS is an upper bound because shared pages may be counted repeatedly; nevertheless, MCP process counts and memory use increase with repeated subagent fan-out and do not return to the pre-fan-out baseline.

Observed sequence:

  1. A parent Codex task spawns several subagents.
  2. Each subagent initializes an MCP helper bundle.
  3. The subagents finish and return their results.
  4. Their MCP helpers remain alive under the long-lived Codex/app-server process tree.
  5. The helpers appear mostly idle but continue accumulating.

Expected behavior: when a subagent completes or is closed, Codex should close its MCP stdin connections and terminate its owned MCP process group, or safely reuse a bounded shared connection pool.

I can provide sanitized before/after measurements and process-tree evidence if useful. I will exclude credentials, environment variables, private filesystem paths, and proprietary repository information.

EthanSK · 2 days ago

Current-build corroboration: Desktop 26.715.31925 / CLI 0.145.0-alpha.18

This remains reproducible on a newer unified ChatGPT/Codex macOS build. This snapshot was gathered read-only; I did not terminate helpers, edit SQLite state, change configuration, or restart the app.

Environment

  • ChatGPT/Codex App: 26.715.31925 (build 5551)
  • Bundled Codex CLI: 0.145.0-alpha.18
  • Platform: macOS 26.5.2 (25F84), Darwin 25.5.0 arm64 arm
  • Physical RAM: 36 GiB
  • Enabled local stdio helpers relevant to this snapshot:
  • global @playwright/mcp@latest
  • project-scoped nx mcp
  • built-in node_repl

Live process and memory evidence

A single persistent Desktop app-server owned repeated helper roots:

18 x npm exec @playwright/mcp@latest
15 x npm exec nx mcp
19 x node_repl

The direct roots alone summed to approximately:

Playwright launchers: 605.9 MiB RSS
Nx launchers:         468.6 MiB RSS
Node REPL hosts:      136.7 MiB RSS
Total direct roots:  1,211.2 MiB RSS

Walking the complete descendant trees for these three helper families found 121 processes using approximately 3.63 GiB summed RSS. Summed RSS is an upper bound because shared pages can be counted more than once, but the process multiplier is unambiguous.

The persistent app-server itself was approximately 818 MiB RSS in the same investigation. macOS reported 8.18 GiB swap in use out of 9 GiB allocated. Current memory pressure had recovered to 61% free, so this was not a claim that the host was actively OOM at capture time.

The helper roots appeared in repeated timestamp-aligned clusters. Most clusters contained one Playwright launcher, one Nx launcher, and one Node REPL host, consistent with a fresh per-session MCP initialization rather than one shared project/runtime pool.

Local log evidence

The current logs_2.sqlite contains explicit task teardown events such as:

Agent loop exited
Shutting down Codex instance
clearing thread listener during thread-state teardown

Four such teardown sequences occurred between 19:35:08 and 19:37:09 local time. The helper clusters remained alive under the same app-server afterward.

The rmcp::transport::child_process target had no matching child-exit records during the current app-server lifetime. Its most recent exit records were from the previous app-server lifetime and showed Playwright, Nx MCP, and Node REPL exiting around a prior resume/restart boundary. This is consistent with full app-server termination cleaning the tree while ordinary task/subagent shutdown does not.

The logs also show the initialization path:

thread/resume
  -> thread_spawn
  -> session_init
  -> session_init.mcp_manager_init
  -> start_server_task

Persisted lifecycle evidence

The active state_5.sqlite.thread_spawn_edges table contained:

status=open:   164
status=closed:  36
parents with open children: 49
largest open-child count for one parent: 58

The 164 open child rows were not archived. This independently supports the stale subagent lifecycle problem tracked in #33700.

Crash evidence boundary

I found historical Crashpad sidecars, including renderer and browser process types, but no matching renderer crash in the two-hour unified-log window used for this live memory snapshot. I am reporting the process/RAM lifecycle leak here, not claiming that this specific snapshot ended in a fresh crash.

Expected behavior

  • Task/subagent shutdown should explicitly close each session's MCP clients and terminate the complete stdio process groups.
  • Completed or interrupted subagents should transition their thread_spawn_edges rows to closed.
  • Restoring or resuming tasks should not recreate stale helper generations.
  • Helper counts and memory should converge to a bounded baseline without requiring a full Desktop restart.
  • Diagnostics should expose the owning task/session and lifecycle state for every MCP root.

PR #19753 was merged as a shutdown cleanup fix, but this later build still reproduces the leak on the task/subagent teardown and restore paths.

Related: #33700, #30408, #32942.

I have not attached raw logs, SQLite databases, session transcripts, environment variables, task IDs, prompts, repository names, or personal filesystem paths because those artifacts may contain private information. I can provide narrower sanitized queries or process snapshots if a maintainer needs them.

philkunz · 1 day ago

Independent sanitized Linux reproduction of the Playwright MCP lifecycle leak.

Environment

  • codex-cli 0.144.5
  • Linux x86_64
  • Long-lived codex app-server --listen unix:// process, approximately 27 hours old
  • Global stdio server command observed as npm exec @playwright/mcp@latest

Process evidence

  • The live app-server had accumulated 61 direct npm exec @playwright/mcp@latest children.
  • Each launcher had the expected shell and playwright-mcp Node descendant.
  • Creation times were spread across the app-server lifetime, including launchers older than one day and multiple timestamp-aligned bursts during later activity. Earlier generations remained alive and mostly idle.
  • There were not 61 browser instances. Only five headless Chromium trees were running, each with a distinct ephemeral playwright_chromiumdev_profile-* user-data directory. This confirms separate MCP generations do not share one browser by default, while many MCP launchers may never have launched a browser.
  • The app-server itself later degraded into a high-CPU Tokio worker loop and would not stop on SIGTERM; this is cross-reported in #34142.
  • After forced app-server termination, the MCP launcher population disappeared, but the five Chromium trees survived as PID-1 orphans because SIGKILL bypassed normal cleanup.

The direct-parent relationship, spread of process ages, and retained earlier generations match the missing per-session/subagent MCP teardown described in this issue. No project paths, hostnames, account data, configuration contents, browser profile identifiers, or session data are included.