Repository navigation
Description
Summary
One V2 managed service can retain a full configured MCP subprocess set for every active or cached Location. A live 0.0.0-next-15839 snapshot showed approximately ten repeated MCP process groups under one healthy service, with about five configured MCP children per group. The direct children accounted for roughly 2.2 GiB RSS even though the MCP processes were idle at the sample boundary.
This is separate from process-global event fan-out, but the costs multiply in the same many-TUI workflow: each TUI commonly owns a different Location, so one service accumulates both more Location graphs and more MCP subprocess sets over time.
Reproduction
- Configure several local MCP servers at user scope.
- Start one managed V2 service.
- Open TUIs across approximately ten distinct Locations.
- Inspect the managed service process tree after Location acquisition.
- Leave Locations idle and verify whether their MCP subprocess groups are torn down after the configured Location cache TTL.
Expected Behavior
- Project-specific MCP declarations remain Location-isolated.
- Identical user-global MCP declarations do not require one eager subprocess per Location unless the server protocol or effective configuration requires isolation.
- MCP servers that are not used by a Location can start lazily.
- Evicting an idle Location deterministically stops every MCP subprocess owned by that Location.
- Resource use has a documented scaling model for many concurrently open Locations.
Actual Behavior
The managed service had approximately ten start-time clusters containing the same configured MCP process shapes, for approximately 50 persistent MCP children. These clusters aligned with Location acquisition rather than tool invocation. The implementation currently makes MCP a Location node and depends on Location-scoped config, integrations, forms, credentials, and events.
The service remained semantically healthy, but it was under broader runtime pressure: its health request took 1.78 seconds, and the managed server plus connected OpenCode clients accounted for about 13 GiB RSS and 457% CPU in the same snapshot. No process was restarted or terminated during measurement.
Investigation Constraints
This should not be solved by making all MCP state process-global. Effective MCP configuration, credentials, integration ownership, forms, events, and tool registration can differ by Location. The ownership key needs to reflect those semantics.
Candidate directions to measure:
- Start configured MCP servers only when tools, prompts, resources, or instructions are first requested.
- Share a process only when the complete effective declaration and credential/placement semantics are identical.
- Separate a shareable transport/process owner from Location-specific registration and event projection.
- Verify the 60-minute Location idle TTL closes MCP scopes and child processes under real TUI usage.
Acceptance Criteria
- A test opens multiple Locations with the same global MCP declaration and asserts the intended subprocess count.
- A distinct project-scoped declaration remains isolated.
- Concurrent first use shares one startup per ownership key.
- Location eviction stops unshared children and releases shared ownership without interrupting remaining Locations.
- A many-Location benchmark records child count, RSS, startup time, and teardown time.
- Diagnostics report MCP ownership and lifecycle without command arguments, credentials, paths, prompts, or tool payloads.
Related
- 2.0: Scope event streams and bound event payloads #36441 tracks process-global event delivery and oversized payload amplification.
- core: cold VCS location acquisition blocked the service for 53 seconds #37794 tracks cold Location acquisition latency.
Activity
mradwankhalil commentedon Oct 14, 2026
Additional 2.0.28 source/local-prototype context for the per-location retention problem: our desktop integration can retain about ten loaded chat folders, each with its own plugin/service graph; the local ledger records roughly 3.6 GB backend working set with the real plugin set. The aggregate is not proof of a leak inside any one MCP child.
We prototyped an optional loaded-location ceiling, reclaiming the least recently active idle surplus locations and never reclaiming locations with a running execution. We also limited expired/surplus disposal to two locations at a time; an unbounded same-sweep teardown burst amplifies event-loop starvation. This is a policy mitigation rather than proof that client-presence-based release is wrong. Our 24-hour idle TTL is workstation-specific, not a proposed universal default.
Before upstreaming that policy, it needs design agreement on retained locations, background shells/pending interactions, client presence, and the capacity guarantee when every location is active (the prototype is a soft ceiling, not a hard admission cap). Linking the local investigation here rather than opening a duplicate cached-location-memory issue. Relevant source:
packages/core/src/location-activity.ts; the regression fixture exercises surplus eviction, active-execution protection, and bounded disposal.liucyoung commentedon Oct 18, 2026
Additional real-world data point confirming this is not isolated: on Windows, a single managed V2 service instance (OpenCode desktop, version 2.0.32) running ~14 concurrent sessions accumulated ~280 MCP-related OS processes / ~10.9 GB RSS before cleanup. Process tree tracing (
Get-CimInstance Win32_Process) shows every spawn cluster's direct parent is the same single serve PID, consistent with the "one Location graph per cached location" model described in the issue body rather than any per-session process isolation.Timing data from
~/.local/share/opencode/log/opencode.log(mcp connectedlines, grouped by run ID):All three (and every other active run) exhibit the same near-constant resync cadence of the entire configured MCP subprocess set, regardless of whether any tool call actually occurred in that window - i.e. it aligns with a Location cache TTL/rebuild timer rather than with tool invocation, matching the "clusters aligned with Location acquisition rather than tool invocation" observation already reported above.
Mitigations in place locally while waiting for an upstream fix: (1) disabling unused MCP servers in
opencode.jsoncuts the per-Location set from 6 to 3; (2) an idle-session cleanup script (DELETE /api/session/{id}after a configured inactivity threshold) so that stale Locations - and by extension their cached MCP process groups - get explicitly evicted instead of accumulating for hours; neither of these addresses the root cause described here, both are just papering over it until #46210/#37844 lands a proper lazy-start/LRU-reclaim behavior for identical global-scope MCP declarations across Locations.Workstation: Windows x86_64, OpenCode desktop v2.0.32 (opencode-cli.exe serve --service), no Docker involved, purely local stdio MCP servers (node/cmd/python/uvx wrappers). Happy to share more process-tree dumps or log excerpts if useful for the loaded-location-ceiling work mentioned in the last maintainer comment here.
mlimarenko commentedon Oct 28, 2026
Linking the recent controlled evidence from #51003 here so it remains attached to the shared per-location issue:
90a138be0680de018235428c6fe2af79480b435c, real direct stdio fixtures behind 16 locations gave 0 processes initially, 48 after HTTP agent/MCP discovery, 0 after advancing the test clock 61 seconds and waiting for teardown, 0 after repeated metadata requests, 1 after one actual tool call, and 0 after server-scope disposal.#53046 remains open and is not part of released 2.2.0. It reduces discovery-only residency, not the initial discovery peak, package-launcher cost or caches already on disk; it does not pool servers genuinely used across locations. The fixture check did not measure real npx/uvx chains, RSS, cache sizes or wall-clock expiry. An initial grouped polling test was inconsistent and remains undiagnosed, as recorded in the linked comment.
The separate reports of same-directory accumulation, reload survivors or children surviving a killed parent need their own lifecycle evidence; these per-location counts do not establish those causes.
opencodejr commentedon Oct 28, 2026
Thanks for the detailed write-up. I tried this on
@opencode/cli@2.2.0(the snapshot0.0.0-next-15839isn't installable from npm).Setup: three user-scope local stdio MCP servers (a minimal idle server), one managed service (
opencode service start), thenopencode mcp listfrom five separate project directories.What I saw:
experimental.location_idle_timeoutset to 30 seconds, the idle sweep evicted the Locations and stopped every MCP child, with none orphaned. Eviction teardown appears to work.Why I'm not treating it as a bug: this follows the current design. MCP is built as a Location node (
makeLocationNodeinpackages/core/src/mcp/index.ts), and a local server'scwddefaults to the Location directory (packages/core/src/mcp/client.ts), so identical global declarations aren't yet identical processes across Locations. Lazy start, sharing by ownership key, a documented scaling model and lifecycle diagnostics would be design and performance improvements to the current behaviour, so this might be better tracked as a feature or enhancement.If you've seen MCP children survive past the Location idle TTL in real TUI use, the config and timings would be the most useful thing to add. That would be a real lifecycle bug.
yethranayeh commentedon Nov 7, 2026
Reproduced on 2.2.6
Two locations stay loaded with no client attached and both are pinned by sessions from Oct 28 that the service still reports as
runningin/api/session/active, withtime.busyset at service start.The agent checked and said:
I don't know how I ended up wtih this kind of case where my clients for these directories were closed but they still require a permission request somehow.
Apparently, this is the root cause from source for my case, per my agent's reply: