Repository navigation
fix(vscode-ide-companion): make IdeServer.stop() resolve while MCP sessions are open - #29674
elberthc-byte wants to merge 4 commits into
Conversation
…ssions are open `stop()` awaited `http.Server.close()`, which only stops accepting new connections and waits for existing ones to drain. The CLI's StreamableHTTP client keeps a standalone `GET /mcp` SSE stream open for the life of the session, so the callback never fired, `stop()` never resolved, and the cleanup after the await (env collection, port file) never ran. `stop()` now: - detaches `server`/`transports` synchronously so repeated or concurrent calls are no-ops instead of racing on the same listener; - closes every MCP transport first (`Promise.allSettled`), which ends the SSE responses and fires `onclose` (clears keep-alive, evicts session); - calls `server.closeAllConnections()` alongside `server.close()` so idle keep-alive sockets cannot hold the callback; - runs the env-collection/port-file cleanup in `finally`. The keep-alive `missedPings >= 3` branch now closes the transport (as its log line already claimed) rather than only clearing the interval. Consecutive-miss semantics are unchanged. Fixes google-gemini#28785
|
📊 PR Size: size/L
|
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request addresses a regression where Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request refactors the IDE server shutdown process in packages/vscode-ide-companion to ensure active MCP sessions are closed, keep-alive intervals are cleared, and remaining connections are dropped promptly, making the stop() method idempotent and preventing hangs. It also adds comprehensive tests to verify these shutdown and keep-alive behaviors. The review feedback suggests handling potential promise rejections when calling transport.close() within the keep-alive interval to prevent unhandled rejections in the extension host.
There was a problem hiding this comment.
Code Review
This pull request refactors the shutdown and keep-alive mechanisms of the IDE server in packages/vscode-ide-companion to ensure active MCP sessions are properly closed and keep-alive intervals are cleared. It also adds comprehensive unit tests covering various shutdown scenarios and keep-alive behaviors. However, the current implementation of stop() introduces a race condition where concurrent calls can resolve instantly before the initial asynchronous shutdown and cleanup processes actually complete. It is recommended to manage the asynchronous shutdown state using an explicit state variable and await the active shutdown promise on concurrent calls.
- Catch and log a rejected `transport.close()` in the missed-pings branch instead of `void`-ing it, and clear the interval directly so a transport that cannot close is not retried every 60s. - Share the in-flight shutdown promise across concurrent `stop()` calls so a second caller resolves only after the listener, sockets and port file are actually gone, rather than instantly.
…icit status Per review: model the shutdown lifecycle with `status: 'idle' | 'stopping' | 'stopped'` instead of the presence of a promise. `stopping` joins the in-flight shutdown, `stopped` is a no-op, and `start()` resets to `idle` so a restarted server remains stoppable. Adds a restart test.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request improves the IDE server's shutdown sequence by introducing state tracking ('idle', 'stopping', 'stopped') and ensuring active MCP sessions are closed and keep-alive intervals are cleared. It also adds comprehensive tests covering various shutdown scenarios and keep-alive eviction. Feedback points out two critical race conditions during rapid stop-start-stop cycles: one affecting the port file and environment variables, and another affecting the status transition. To resolve these, it is recommended to capture the port file synchronously at the start of shutdown and use an explicit shutdown identifier to safely track the active shutdown operation.
…ycles A shutdown that finishes after a newer start() must not touch the new server's state. Each shutdown now captures the lifecycle generation (bumped by start()) and only marks the server stopped / clears the env collection if it is still the current one; the port file is captured synchronously so only the old file is unlinked.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request refactors the shutdown process of the IDE server in packages/vscode-ide-companion to safely handle active Model Context Protocol (MCP) sessions and concurrent shutdown requests. It introduces a state machine (status), a generation counter to prevent late-finishing shutdowns from clobbering restarted servers, and ensures active transports are closed and connections are drained promptly during shutdown. Additionally, comprehensive unit tests have been added to verify these behaviors, including keep-alive ping eviction and concurrent stop scenarios. I have no further feedback to provide as no review comments were submitted.
Summary
IdeServer.stop()never resolved while a Gemini CLI session was connected to the VS Code companion.stop()awaitedhttp.Server.close(), which only stops accepting new connections and waits for existing ones to drain — but the CLI'sStreamableHTTPClientTransportholds a standaloneGET /mcpSSE stream open for the life of the session, so nothing ever drained. Extensiondeactivate()blocked behind it, and the cleanup after theawait(environment-variable collection, port file) never ran, which can leave a stale port file forgetConnectionConfigFromFileto pick up on the next launch.This PR makes
stop()close the MCP sessions first and force-close remaining sockets, so it resolves promptly and always runs its cleanup. Two files, no API changes, no neweslint-disabledirectives.Details
stop()(packages/vscode-ide-companion/src/ide-server.ts)status: 'idle' | 'stopping' | 'stopped': a concurrentstop()whilestoppingjoins the in-flight promise and so resolves only once the listener, sockets and port file are actually gone;stop()when alreadystoppedis a no-op (previously a second call rejected withERR_SERVER_NOT_RUNNING).start()resets the status toidleand bumps a lifecyclegeneration; a shutdown that finishes after a newerstart()neither marks the new serverstopped, nor clears its env vars, nor unlinks its port file (the old port file path is captured synchronously), so overlapping stop/start/stop cycles are isolated.Promise.allSettled(transports.map(t => t.close())).StreamableHTTPServerTransport.close()ends the SSE responses and firesonclosesynchronously, which is where the keep-alive interval is cleared and the session evicted — so no timer can outlivestop(). There is no timeout race; every registered transport is closed before the server is.server.closeAllConnections()alongsideserver.close()so idle keep-alive sockets (not tied to any transport) cannot hold the close callback either. Requires Node ≥ 18.2; VS Code^1.99ships Node 20+ and@types/node20.x already declares it.finally.environmentVariableCollection.clear()andfs.unlink(portFile)now run even ifserver.close()reports an error.Keep-alive (
missedPings >= 3branch)The branch logged "Closing connection and cleaning up interval" but only cleared the interval, leaving the session in
this.transports. It now clears the interval and callstransport.close()(rejections caught and logged, so a transport that cannot close is neither an unhandled rejection nor retried every 60 s);oncloseevicts the session. ThemissedPings = 0reset on success is intentionally unchanged — "3 consecutive misses" is the sane reading of the threshold, and changing it is a policy decision outside this bug (see discussion on #28789).Why the first test attempts passed without the fix (for reviewers)
The
OpenFilesManagermock in this test file had nostate, so theGET /mcphandler threw while building the initialide/contextUpdatenotification, Express 5 destroyed the SSE socket, and the hang was masked. The mock now exposes a schema-validstate, and the new helperconnectMcpClient()waits for theide/contextUpdatenotification as the deterministic "SSE stream is live" signal before callingstop(). With that in place, 11 of the 12 new tests fail againstmainand pass with this change.Tests added (
ide-server.test.ts,describe('shutdown with active MCP sessions'))stop()resolves while a session holds an open SSE stream (the IdeServer.stop() never resolves while an MCP session is open #28785 regression test)stop()is idempotentstop()calls await the same in-flight shutdown (second caller stays pending until cleanup has run)stop()→start()(restart)stop()does not clobber a restarted server (only its own port file is unlinked; env vars untouched; new server still stoppable)stoppedprematurely)server.close()errorstransport.close()is logged and the interval stopsOut of scope (pre-existing, found during verification)
A single
McpServerisconnect()ed to every transport, so the SDK'sProtocol._transportis overwritten on each new session and responses route to the most recent one; with several CLIs connecting in parallel only the last completestools/list. Reproduces before and after this change — will file separately.Related Issues
Fixes #28785
Related to #28789, #29088 (earlier attempts, closed without merge)
How to Validate
Unit tests
To see the regression test fail:
git stashtheide-server.tshunk (keep the test file) and re-run —should resolve stop() while a session holds an open SSE streamtimes out, and 6 more in the samedescribefail.Manual, end-to-end (real
IdeClientfrom@google/gemini-cli-coreagainst a realIDEServer)A vite-node harness drove the production server with the production CLI client in child processes across 7 scenarios: S1 no sessions · S2 one connected client holding the
GET /mcpstream · S3 three clients · S4 in-flightopenDiffrequest duringstop()· S5 repeated and concurrentstop()· S6 restart afterstop()(no stale port file, fresh listener, client reconnects) · S7 keep-alive eviction (2 misses + success + 2 misses must not evict; 3 consecutive must). Results:main(unfixed)stop()hangs with ≥1 client; port file and env collection leaked; second concurrentstop()rejectsstop()resolves in 1–7 ms with 0, 1 and 3 clients and with a request in flight;Session closed: <id>logged per session; port file removed; env collection cleared; 0 unhandled rejectionsIn VS Code
geminiin the integrated terminal and let it connect (/ide status→ connected).$TMPDIR/gemini/ide/gemini-ide-server-<extension-host-pid>-<port>.jsonis left behind. Expected after: deactivation completes immediately and the port file is gone.CI parity: all
ci.ymlLint-job steps and both Test (Linux) shards + bundle + npx smoke test were run locally on Node 20 — 15 950 tests, 0 failures.Pre-Merge Checklist