Skip to content

core: cached locations multiply MCP subprocesses #37844

Open

Description

@kitlangton

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

  1. Configure several local MCP servers at user scope.
  2. Start one managed V2 service.
  3. Open TUIs across approximately ten distinct Locations.
  4. Inspect the managed service process tree after Location acquisition.
  5. 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

Activity

  1. self-assigned this
    on Jul 20, 2026
  2. added
    perfIndicates a performance issue or need for optimization
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on Jul 20, 2026
  3. mradwankhalil commented on Oct 14, 2026

    @mradwankhalil
    Contributor

    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.

  4. liucyoung commented on Oct 18, 2026

    @liucyoung

    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 connected lines, grouped by run ID):

    run connections wall time interval
    A 753 ~8 h ~3.8 min between full resyncs
    B 704 ~7.5 h ~3.8 min
    C 332 ~3.5 h ~3.8 min

    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.json cuts 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.

  5. mlimarenko commented on Oct 28, 2026

    @mlimarenko
    Contributor

    Linking the recent controlled evidence from #51003 here so it remains attached to the shared per-location issue:

    #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.

  6. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the detailed write-up. I tried this on @opencode/cli@2.2.0 (the snapshot 0.0.0-next-15839 isn't installable from npm).

    Setup: three user-scope local stdio MCP servers (a minimal idle server), one managed service (opencode service start), then opencode mcp list from five separate project directories.

    What I saw:

    • Each Location acquired got its own set of three MCP children under the service process, started at acquisition rather than on tool use (12 children across 4 Locations). This matches your snapshot.
    • With experimental.location_idle_timeout set 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 (makeLocationNode in packages/core/src/mcp/index.ts), and a local server's cwd defaults 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.

  7. yethranayeh commented on Nov 7, 2026

    @yethranayeh

    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 running in /api/session/active, with time.busy set at service start.

    The agent checked and said:

    Each is waiting on an unanswered shell permission request, and the tool part is persisted as status: "running", executed: false.

    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:

    location-activity.ts treats a location with an active session as occupied and skips eviction. In the current log, 14 locations have location services evicted, and these two never do, while other locations in the same service evict normally.

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

Metadata

Metadata

Assignees

Labels

2.0bugSomething isn't workingcoreAnything pertaining to core functionality of the application (opencode server stuff)perfIndicates a performance issue or need for optimization

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions