[Linux Desktop 0.147.0] Completed v2 subagents retain full stdio MCP runtimes
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_agentworkflows
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, andcodex-securityremained. - 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
- Configure several global stdio MCP servers.
- Start Codex Desktop and keep the same
app-serveralive. - Run a task that creates subagents with
spawn_agent; let them finish normally. - Repeat across several tasks and/or use follow-up/resume flows.
- After every child reports a terminal status, count the
app-serverdescendants by MCP command. - 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:
- Its resident execution runtime (including stdio MCP children) should be unloaded within a bounded period.
- Its logical agent id/path and open spawn edge should remain durable so
followup_task/send_inputcan transparently reload it. - Active turns and pending mailbox input must never be unloaded.
- 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.
2 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
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
EMFILEfailure 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
0.147.0/ tagrust-v0.147.0(be6e8eac)codex -c features.code_mode_host=true app-server --listen unix://RLIMIT_NOFILE: soft1024, hard1048576Exhausted-process snapshot
At
2026-08-17 01:44:05 UTC, the app-server held990 / 1024file 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:
nodeprocessesnpmMCP proxy processesThe 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 = 784descriptors, 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 24occurred at01:40:46 UTCwhile app-server tried to load a thread's MCP configuration. In the surrounding four-minute window, the log database contains 114No 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:
task_completeorturn_abortedevent between about 1 hour 45 minutes and 14 hours 22 minutes before the FD snapshot;The replacement app-server reproduced the same shape: a later read-only snapshot at roughly 2 hours 6 minutes uptime showed
665 / 1024descriptors 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
mainat commit1f41cc5d. The lifecycle boundary still appears to be:McpRuntime.UnloadingState::unloading_target).AgentControlscope).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:
ThreadManagerso 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_NOFILEeven 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.