[Linux Desktop 0.147.0] Completed v2 subagents retain full stdio MCP runtimes

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

Environment

  • Codex Desktop on Linux
  • Bundled / local CLI: codex-cli 0.147.0
  • Platform: Linux 6.8.0-137-generic x86_64 (Ubuntu 24.04)
  • Authentication: ChatGPT subscription
  • MCP configuration at the time of reproduction: 8 enabled stdio servers plus 4 Streamable HTTP servers
  • Long-lived Desktop app-server
  • Multi-agent v2 / spawn_agent workflows

What issue are you seeing?

Completed subagents can leave their entire stdio MCP runtime resident under the live Desktop app-server. The processes are not zombies and are not reparented to PID 1: every retained MCP launcher/server is still a direct descendant of the same healthy app-server.

On one real workload, after a subagent-heavy task:

  • 15 instances each of context7, github, gitee, playwright, and codex-security remained.
  • The 15 start clusters aligned across unrelated servers, so the growth unit was one complete MCP stack, not one crashing third-party server.
  • A sixth stdio MCP was unusually expensive because of its own development logging, amplifying the Codex lifecycle bug but not causing the stack duplication.
  • System used memory grew to about 29 GiB; the Desktop cgroup accounted for about 23.9 GiB RSS.
  • Sending SIGTERM only to the stale MCP descendants reduced system used memory to about 11 GiB and the Desktop cgroup to about 5.4 GiB. The live task then recreated one healthy current stack, demonstrating that the old stacks were not required for the current task.

Local logs correlate each new process batch with:

spawn_agent / followup_task
  -> thread_spawn
  -> session_init
  -> session_init.mcp_manager_init
  -> mcp.runtime.refresh
  -> start_server_task

This is related to #12491 and #17832, but the narrower failure mode here is completed v2 subagents retaining live runtimes while their logical agent identity remains resumable. It is not a request to mark the spawn edge closed, and it is not only the already-fixed manager-replacement leak from #18881.

Steps to reproduce

  1. Configure several global stdio MCP servers.
  2. Start Codex Desktop and keep the same app-server alive.
  3. Run a task that creates subagents with spawn_agent; let them finish normally.
  4. Repeat across several tasks and/or use follow-up/resume flows.
  5. After every child reports a terminal status, count the app-server descendants by MCP command.
  6. Observe complete, timestamp-aligned MCP stacks remaining after the child work has completed and memory increasing monotonically.

The problem becomes especially visible with a high-memory stdio server, but it also reproduces as equal instance counts across small, unrelated servers.

Current-main code observation

The current v2 residency path can unload Completed, Errored, or Interrupted threads, preserving their durable logical identity so they can be reloaded on later delivery. However, that unload path is currently driven by a later residency-slot reservation under capacity pressure. The completion watcher delivers the child result to the parent and returns without proactively releasing the now-idle resident thread/runtime.

This can leave completed children holding stdio MCP processes indefinitely when no later spawn forces eviction. Across multiple Desktop tasks, those resident stacks accumulate under the same long-lived app-server.

Recent lazy startup work reduces how many MCPs a child starts, but any MCP actually started by the child still needs an idle release path.

Expected behavior

After a v2 subagent reaches a terminal status, its result has been delivered, its active turn is cleared, and its mailbox is empty:

  1. Its resident execution runtime (including stdio MCP children) should be unloaded within a bounded period.
  2. Its logical agent id/path and open spawn edge should remain durable so followup_task / send_input can transparently reload it.
  3. Active turns and pending mailbox input must never be unloaded.
  4. Repeating completed subagent runs should return MCP process counts and RSS near the idle baseline instead of growing monotonically.

A regression test should use a PID-recording stdio MCP fixture, verify that the process exits after terminal idle, then send a follow-up and verify that the same logical agent reloads and can start/use MCP again.

Related issues / changes

  • #12491 — broad Desktop MCP child cleanup tracking; latest maintainer guidance asks distinct bugs to be filed separately
  • #17832 — Playwright stdio processes retained after subagents
  • #17574 — subagent MCP helper process trees
  • #30408 — closed/archived threads retaining MCP processes
  • #33700 — persisted subagent state / rehydration observations (logical edge lifetime is related but should not be conflated with runtime residency)
  • #18881 / #19753 — explicit manager/client shutdown fixes
  • #26632 and #26997 — durable v2 unload/reload behavior
  • #38217 — lazy startup of required cached MCP servers for subagents

I am preparing a minimal patch with tests. I understand external contributions are invitation-only; if maintainers agree that proactively releasing terminal-idle v2 runtimes is the right layer, please explicitly invite the PR and I will open it against main.

View original on GitHub ↗

2 Comments

github-actions[bot] contributor · 15 days ago

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

  • #37870
  • #37426

Powered by Codex Action

BranchManager69 · 11 days ago

Additional Linux 0.147.0 reproduction: retained v2 MCP runtimes exhausted the app-server FD table

I independently reproduced this on Linux and traced the resulting EMFILE failure down to the app-server's retained per-thread MCP process sets. This connects the terminal-subagent residency problem in this issue to the descriptor-exhaustion symptom in #26984.

Environment

  • Codex CLI 0.147.0 / tag rust-v0.147.0 (be6e8eac)
  • Linux x86_64
  • Long-lived codex -c features.code_mode_host=true app-server --listen unix://
  • App-server RLIMIT_NOFILE: soft 1024, hard 1048576
  • Subagent-heavy multi-agent workload with configured and plugin-provided stdio MCP servers

Exhausted-process snapshot

At 2026-08-17 01:44:05 UTC, the app-server held 990 / 1024 file descriptors:

| Descriptor type | Count |
| --- | ---: |
| pipes | 593 |
| live pidfds | 198 |
| sockets | 57 |
| remaining files/event handles | 142 |

All 198 pidfds pointed to live processes; none pointed to an exited target. The live targets were:

  • 98 plugin MCP node processes
  • 49 configured MCP CLI processes
  • 49 npm MCP proxy processes
  • 1 code-mode host
  • 1 diagnostic shell

The 196 MCP children formed 49 copies of the same four direct-child roots. The parent held approximately three stdio pipe ends plus one pidfd per MCP child: 196 × 4 = 784 descriptors, about 79% of its entire FD table. This was not 990 programs, zombie handles, or machine-wide file-table exhaustion. The system file table was healthy at 30,418 allocated handles with a practically unbounded configured maximum.

The first recorded os error 24 occurred at 01:40:46 UTC while app-server tried to load a thread's MCP configuration. In the surrounding four-minute window, the log database contains 114 No file descriptors available (os error 24) records across MCP refresh, skill/plugin reads, thread-store writing, and session handling.

Thread/process correlation

During this app-server's approximately 27-hour lifetime, the local thread database recorded 232 new threads, 224 of them spawned child threads. This is not a claim that all 224 still had a runtime at the final snapshot; the live process census showed 49 retained MCP sets.

For six retained MCP process pairs sampled by start time:

  • five started one or two seconds after a recorded subagent-thread creation;
  • the corresponding five child rollouts had reached a terminal task_complete or turn_aborted event between about 1 hour 45 minutes and 14 hours 22 minutes before the FD snapshot;
  • one sampled pair did not have a matching retained thread row, so I cannot map every live set to an exact durable thread record after the fact.

The replacement app-server reproduced the same shape: a later read-only snapshot at roughly 2 hours 6 minutes uptime showed 665 / 1024 descriptors and 127 direct children. That later rate should not be treated as an ordinary-user baseline because this investigation and other concurrent chats themselves loaded additional sessions; it only shows that the per-session multiplication is repeatable.

Source trace

I traced the running tag and rechecked current main at commit 1f41cc5d. The lifecycle boundary still appears to be:

  1. Every session creates a thread-owned McpRuntime.
  2. App-server passes every initialized connection into listener attachment for each newly created thread (creation path, subscription path).
  3. The generic 30-minute unload clock exists only while a thread is both inactive and has no subscribers; otherwise there is no unloading target (UnloadingState::unloading_target).
  4. The completion watcher delivers the terminal child result and returns without proactively releasing the resident runtime (completion watcher).
  5. V2 residency already knows how to materialize, shut down, and remove a terminal idle child, but invokes that path while reserving a later residency slot under capacity pressure (residency eviction). The residency registry is scoped per root session tree, not globally across the shared app-server (AgentControl scope).

Actual session shutdown explicitly shuts down the MCP runtime (shutdown path). I also found successful process-shutdown events elsewhere in the same app-server lifetime. My read is therefore that the primary failure here is missing global eviction/proactive terminal-idle release, not evidence that every individual stdio teardown is broken.

The newer cached-tool lazy-start work reduces how many servers a subagent starts, but it does not release a runtime after that child actually starts an MCP server.

Proposed acceptance test

A regression test should:

  1. Spawn a v2 child with a PID-recording stdio MCP fixture and force that child to start/use the fixture.
  2. Wait for the child to reach a terminal state and for its result to be delivered to the parent, without spawning another child to create capacity pressure.
  3. Assert within a bounded interval that the child execution session is unloaded and the MCP PID has exited.
  4. Send a follow-up to the same logical agent and assert that it reloads, starts a new MCP process, and completes successfully.
  5. Repeat across multiple root session trees under one ThreadManager so the per-root residency cap cannot hide unbounded app-server-wide growth.

The smallest behavior-preserving fix appears to be proactive terminal-idle unloading using the existing residency teardown path while retaining the durable agent identity for reload. A short bounded grace period could avoid racing an immediate follow-up. Separately, a global app-server MCP/session resource budget would keep one process from approaching RLIMIT_NOFILE even when many roots remain open.

Would maintainers prefer the first change in the v2 completion/residency path, or should terminal subagent listener subscriptions be detached in app-server instead? I am happy to prepare a targeted PR and tests if invited under the repository's contribution policy.

Related: #30408, #26984, #37870.