Repository navigation
fix(core): release location holders on shutdown - #59399
Conversation
There was a problem hiding this comment.
The change looks sound. I copied the new location-layer test onto the base and it fails there: the held location's MCP process outlives invalidate. With this PR it passes, along with the rest of location-layer.test.ts. The MCP test files pass apart from one descendant-termination test that fails the same way on the base. tsc for packages/core passes.
Public API: unchanged. close is added only to the internal Mcp.Service interface.
I found nothing blocking. A late startServer after close forks from the already-closed servers scope, so its cleanup runs straight away and nothing is left behind. Adding Mcp.node as a lifecycle dependency doesn't change when MCP starts, since instance.ts already builds it for every location.
bccd255 to
64263be
Compare
There was a problem hiding this comment.
I couldn't verify the fix end to end. The rewrite drops the earlier Mcp.close approach and its test, and I didn't run a manual serve with a held terminal socket. The core location and MCP-attach tests and the server tests pass, and tsc -b passes for packages/core and packages/server.
Public API: no endpoints, schemas or events change. One behaviour change: PTY and client MCP websockets now close with 1012 ("location shut down") when their location shuts down.
Concerns:
- Terminals end on every invalidation, not only reload. Details are inline.
- No test now covers the fixed behaviour. A small one would help: a held PTY or MCP socket is closed on invalidate, and the idle sweep skips a retained location.
| const socket = yield* Effect.orDie(ctx.request.upgrade) | ||
| // TODO: Integrate graceful-shutdown socket tracking before clients migrate to this route. | ||
| const outbox = yield* Queue.unbounded<Outbound>() | ||
| yield* closeOnShutdown(outbox) |
There was a problem hiding this comment.
The PTY service belongs to the location, so closing this socket on shutdown also ends the shell. The client can't reconnect to it either: the new location has no PTY with this ID, so it gets a 404. Besides opencode reload and the debug evict route, vcs.init (packages/server/src/handlers/vcs.ts) also invalidates the location. So "initialize git" in the app would now end every open terminal in that directory, which looks like a regression. Should those call sites skip or defer retained locations, the way the idle sweep now does?
There was a problem hiding this comment.
I looked at this again and kept it. The PTY service is location-owned, so keeping the shell on vcs.init or reload means keeping the stale location and its MCP set alive, which is the leak. Deferring vcs.init would leave the old project identity in place. In practice the TUI and app terminals go through the persistent PTY daemon, which is not tied to a location, so they never see this. Only direct /api/pty users get the 1012, and the idle sweep still skips a location with an open PTY or client MCP socket. Client MCP sockets reconnect to the new location on their own (now covered in client-mcp.test.ts).
64263be to
b557542
Compare
There was a problem hiding this comment.
The new reload test fails on the base and passes with this PR, 5/5 with --rerun-each 5. The core location tests pass, and tsc passes for packages/core and packages/server. Public API: no change from this round.
The earlier point about terminals ending on vcs.init and other explicit invalidations is now documented as intentional in the description, but the code is unchanged, so that thread stays open for a maintainer to decide. Nothing new to raise.
b557542 to
fe8ed14
Compare
fe8ed14 to
62e85f2
Compare
Part of #51003 and #52965. Related: #50363, #51249, #37844, #58795
Problem
invalidate(idle eviction,opencode reload,vcs.init, debug evict) drops a location from the cache, but theRcMaponly closes it once nobody holds it. An open PTY websocket or a client-hosted MCP socket holds it for as long as it stays open, so the whole old location keeps running: MCP servers, watchers, shells. The next request boots a fresh location with its own set, so you end up with duplicate MCP server sets.Running steps already handle this by checking
lifecycle.isClosed()at step boundaries. Sockets never did.Change
LocationLifecyclegetsawaitShutdownplusretain/isRetained. One server helper,holdLocation, does both for the PTY and client MCP sockets.connectClientMcpreconnects within 500ms, so evicting would just boot a fresh location (and its MCP servers) right away.vcs.init, debug evict) still end location PTYs on purpose. The PTY service belongs to the location, so keeping the shell means keeping the stale location, which is the leak this fixes. The TUI and app terminals use the persistent PTY daemon (/api/experimental/persistent-pty), which isn't tied to a location, so they're unaffected. Only direct/api/ptyusers see the 1012 close, and reconnecting to the old PTY ID gets a 404.Verification
packages/core:bun run typecheck,bun run typecheck:tsc,bun run script/test.tson the location, mcp, pty and browser-idle tests: 200 pass.packages/server:bun run typecheck,bun run typecheck:tsc,bun test: 117 pass.packages/clientbun test test/mcp.test.ts: 4 pass. Rootbun run checkpasses.dev, and passes 5/5 with--rerun-each 5:core/test/location-retain.test.ts: the idle sweep evicts an idle sibling but keeps a retained location, then evicts it once released.core/test/location-layer.test.ts: warns when a borrower still holds an invalidated location after 30s, stays quiet when it lets go on shutdown.server/test/pty-transport.test.ts: a held PTY socket gets 1012 on reload and the old shell process exits.server/test/client-mcp.test.ts: a realconnectClientMcpclient gets 1012 on reload, the old location closes, and the client reattaches to the new one.serve(isolated XDG dirs) with one stdio MCP server, a PTY socket and a client MCP socket on the same directory:dev, after reload: 2 MCP processes, old shell still alive, sockets still on the old locationVisual
Added by the opencode bot (AI-generated).
Related: #51003, #52965, #50363, #51249, #37844