Skip to content

fix(vscode-ide-companion): resolve stop() hang and fix keep-alive failure threshold (#28785) - #28789

Closed
Pranjulchaurasiya wants to merge 4 commits into
google-gemini:mainfrom
Pranjulchaurasiya:fix-ide-server-keepalive
Closed

Pranjulchaurasiya wants to merge 4 commits into
google-gemini:mainfrom
Pranjulchaurasiya:fix-ide-server-keepalive

Conversation

@Pranjulchaurasiya

@Pranjulchaurasiya Pranjulchaurasiya commented Aug 12, 2026 •

Copy link
Copy Markdown

Summary

This PR resolves two stability bugs in vscode-ide-companion (#28785):

  1. Fixes an issue where IdeServer.stop() hangs indefinitely when active streaming MCP sessions (GET /mcp) are open.
  2. Fixes a resource leak in the keep-alive ping loop where intermittent ping failures never triggered the 3-missed-pings threshold.

Details

  • IdeServer.stop() Hang: http.Server.close() waits for active connections to drain naturally. Because MCP transports maintain long-lived streaming HTTP responses, the close callback never fired.
    • Fix: stop() now iterates this.transports and calls .close() on each transport concurrently using Promise.allSettled, bounded by a Promise.race 1000ms timeout (preventing a stuck transport from blocking teardown). this.server.closeAllConnections() is invoked after server.close() to terminate remaining sockets, followed by an explicit this.transports = {} best-effort cleanup.
  • Keep-Alive Failure Threshold: missedPings was previously reset to 0 on every successful ping, allowing oscillating connections (success/fail/success/fail) to bypass the threshold of 3 missed pings indefinitely.
    • Fix: Removed the reset logic so cumulative failures count towards the limit, and added void transport.close() when missedPings >= 3.
  • Testing & Tooling: Added unit tests covering hung transport teardowns, multi-session shutdowns, and non-consecutive ping failure limits. Exposed an @internal getTransports() getter for type-safe test access and replaced fixed sleep delays with deterministic polling in tests.

Related Issues

Fixes #28785

How to Validate

  1. Navigate to the companion package:
cd packages/vscode-ide-companion
  1. Run type checks and linting:
npm run check-types
npm run lint
  1. Run the vitest test suite for ide-server:
npx vitest run src/ide-server.test.ts

Expected Result: All 17 tests pass, including the 4 new lifecycle tests (a, b, c, d).

Pre-Merge Checklist

  • Updated relevant documentation and README (if needed)
  • Added/updated tests (if needed)
  • Noted breaking changes (if any)
  • Validated on required platforms/methods:
    • MacOS
      • npm run
      • npx
      • Docker
      • Podman
      • Seatbelt
    • Windows
      • npm run
      • npx
      • Docker
    • Linux
      • npm run
      • npx
      • Docker

@Pranjulchaurasiya
Pranjulchaurasiya requested review from a team as code owners August 12, 2026 18:53
@google-cla

google-cla Bot commented Aug 12, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@github-actions github-actions Bot added the size/xl An extra large PR label Aug 12, 2026
@github-actions

github-actions Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

📊 PR Size: size/M

  • Lines changed: 200
  • Additions: +200
  • Deletions: -0
  • Files changed: 2

@github-actions

Copy link
Copy Markdown

🛑 Action Required: Evaluation Approval

Steering changes have been detected in this PR. To prevent regressions, a maintainer must approve the evaluation run before this PR can be merged.

Maintainers:

  1. Go to the Workflow Run Summary.
  2. Click the yellow 'Review deployments' button.
  3. Select the 'eval-gate' environment and click 'Approve'.

Once approved, the evaluation results will be posted here automatically.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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 two critical stability bugs in the vscode-ide-companion package: a server shutdown hang caused by active streaming connections and a resource leak in the keep-alive failure detection logic. Alongside these fixes, the PR includes a significant cleanup of the repository structure, removing various legacy configuration, documentation, and tooling files.

Highlights

  • IdeServer.stop() Hang Fix: Resolved an issue where IdeServer.stop() would hang indefinitely when active streaming MCP sessions were open by implementing a concurrent transport closure with a 1000ms timeout.
  • Keep-Alive Failure Threshold Fix: Corrected the keep-alive ping logic to ensure that cumulative failures trigger the 3-missed-pings threshold, preventing oscillating connections from bypassing the limit.
  • Repository Cleanup: Performed a comprehensive cleanup of the repository by removing obsolete configuration files, documentation, and tooling scripts.
Ignored Files
  • Ignored by pattern: .gemini/** (46)
    • .gemini/commands/code-guide.toml
    • .gemini/commands/core.toml
    • .gemini/commands/find-docs.toml
    • .gemini/commands/frontend.toml
    • .gemini/commands/full-context.toml
    • .gemini/commands/github/cleanup-back-to-main.toml
    • .gemini/commands/introspect.toml
    • .gemini/commands/oncall/pr-review.toml
    • .gemini/commands/prompt-suggest.toml
    • .gemini/commands/review-and-fix.toml
    • .gemini/commands/review-frontend.toml
    • .gemini/commands/strict-development-rules.md
    • .gemini/config.yaml
    • .gemini/settings.json
    • .gemini/skills/agent-tui/SKILL.md
    • .gemini/skills/async-pr-review/SKILL.md
    • .gemini/skills/async-pr-review/policy.toml
    • .gemini/skills/async-pr-review/scripts/async-review.sh
    • .gemini/skills/async-pr-review/scripts/check-async-review.sh
    • .gemini/skills/behavioral-evals/SKILL.md
    • .gemini/skills/behavioral-evals/assets/interactive_eval.ts.txt
    • .gemini/skills/behavioral-evals/assets/standard_eval.ts.txt
    • .gemini/skills/behavioral-evals/references/creating.md
    • .gemini/skills/behavioral-evals/references/fixing.md
    • .gemini/skills/behavioral-evals/references/promoting.md
    • .gemini/skills/behavioral-evals/references/running.md
    • .gemini/skills/ci/SKILL.md
    • .gemini/skills/ci/scripts/ci.mjs
    • .gemini/skills/code-reviewer/SKILL.md
    • .gemini/skills/docs-changelog/SKILL.md
    • .gemini/skills/docs-changelog/references/highlights_examples.md
    • .gemini/skills/docs-changelog/references/index_template.md
    • .gemini/skills/docs-changelog/references/latest_template.md
    • .gemini/skills/docs-changelog/references/preview_template.md
    • .gemini/skills/docs-writer/SKILL.md
    • .gemini/skills/docs-writer/quota-limit-style-guide.md
    • .gemini/skills/docs-writer/references/docs-auditing.md
    • .gemini/skills/github-issue-creator/SKILL.md
    • .gemini/skills/pr-address-comments/SKILL.md
    • .gemini/skills/pr-address-comments/scripts/fetch-pr-info.js
    • .gemini/skills/pr-creator/SKILL.md
    • .gemini/skills/review-duplication/SKILL.md
    • .gemini/skills/string-reviewer/SKILL.md
    • .gemini/skills/string-reviewer/references/settings.md
    • .gemini/skills/string-reviewer/references/word-list.md
    • .gemini/skills/tui-tester/SKILL.md
  • Ignored by pattern: .github/workflows/** (47)
    • .github/workflows/agent-session-drift-check.yml
    • .github/workflows/build-unsigned-mac-binaries.yml
    • .github/workflows/chained_e2e.yml
    • .github/workflows/ci.yml
    • .github/workflows/community-report.yml
    • .github/workflows/deflake.yml
    • .github/workflows/docs-audit.yml
    • .github/workflows/docs-page-action.yml
    • .github/workflows/docs-rebuild.yml
    • .github/workflows/eval-pr.yml
    • .github/workflows/eval.yml
    • .github/workflows/evals-nightly.yml
    • .github/workflows/gemini-automated-issue-dedup.yml
    • .github/workflows/gemini-automated-issue-triage.yml
    • .github/workflows/gemini-cli-bot-brain.yml
    • .github/workflows/gemini-cli-bot-pulse.yml
    • .github/workflows/gemini-lifecycle-manager.yml
    • .github/workflows/gemini-scheduled-issue-dedup.yml
    • .github/workflows/gemini-scheduled-issue-triage.yml
    • .github/workflows/gemini-scheduled-pr-triage.yml
    • .github/workflows/gemini-self-assign-issue.yml
    • .github/workflows/issue-opened-labeler.yml
    • .github/workflows/label-backlog-child-issues.yml
    • .github/workflows/label-workstream-rollup.yml
    • .github/workflows/links.yml
    • .github/workflows/memory-nightly.yml
    • .github/workflows/perf-nightly.yml
    • .github/workflows/pr-rate-limiter.yaml
    • .github/workflows/pr-size-labeler-batch-run.yml
    • .github/workflows/pr-size-labeler.yml
    • .github/workflows/release-change-tags.yml
    • .github/workflows/release-manual.yml
    • .github/workflows/release-nightly.yml
    • .github/workflows/release-notes.yml
    • .github/workflows/release-patch-0-from-comment.yml
    • .github/workflows/release-patch-1-create-pr.yml
    • .github/workflows/release-patch-2-trigger.yml
    • .github/workflows/release-patch-3-release.yml
    • .github/workflows/release-promote.yml
    • .github/workflows/release-rollback.yml
    • .github/workflows/release-sandbox.yml
    • .github/workflows/smoke-test.yml
    • .github/workflows/test-build-binary.yml
    • .github/workflows/tools-python-ci.yml
    • .github/workflows/trigger_e2e.yml
    • .github/workflows/unassign-inactive-assignees.yml
    • .github/workflows/verify-release.yml
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request contains a large number of deletions across various configuration, documentation, and script files, effectively cleaning up the repository by removing obsolete build configurations, unused scripts, and outdated documentation. As there were no review comments provided, I have no specific feedback to offer.

@Pranjulchaurasiya
Pranjulchaurasiya force-pushed the fix-ide-server-keepalive branch from 88da634 to 1bf2555 Compare August 12, 2026 18:59
@github-actions github-actions Bot added the size/m A medium sized PR label Aug 12, 2026
@gemini-cli gemini-cli Bot added the area/core Issues related to User Interface, OS Support, Core Functionality label Aug 12, 2026

@chiruu12 chiruu12 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reporter of #28785 here. Couple of things I don't think this covers.

The keep-alive interval can still outlive stop(). keepAlive lives in the connection closure and only gets cleared in two spots, the missedPings >= 3 branch and transport.onclose. Nothing stop() can reach holds it.

So when a transport doesn't finish closing inside the 1s race, which is the exact case this is fixing, onclose never runs, this.transports gets dropped, and that timer is now unreachable. It just keeps pinging. closeAllConnections() kills the sockets but not the timer.

Storing the intervals in a Map keyed by session id next to this.transports, and clearing them in stop() before the race, would handle it.

Separate thing: pulling out missedPings = 0 turns "three in a row" into "three ever". At 60s intervals a session that stays up for a few hours will hit three transient failures eventually and get closed for it. The log line stops matching what happened too.

If the point was to stop a flapping transport from dodging the threshold, counting consecutive misses plus a total, or a failure rate over some window, gets that without dropping the reset.

@Pranjulchaurasiya
Pranjulchaurasiya force-pushed the fix-ide-server-keepalive branch from d5eb3ef to 1bf2555 Compare August 13, 2026 06:43
@github-actions github-actions Bot added the size/l A large sized PR label Aug 13, 2026
@Pranjulchaurasiya

Copy link
Copy Markdown
Author

Thanks for the thorough review and both catches @chiruu12!

1. Keep-alive interval outliving stop(): You're right — if transport.close() doesn't resolve within the 1s race, onclose never fires and the interval keeps running against a dead connection. Added a keepAliveIntervals map keyed by session id, populated when the session initializes. stop() now explicitly clears every remaining interval in that map after the close race, regardless of whether onclose or the missed-pings branch ever ran — so a hung transport can no longer leave its timer orphaned.

2. missedPings = 0 removal turning "3 in a row" into "3 ever": Also right, and a good catch on the log line drifting from what actually happened. Split this into two counters: consecutiveMissedPings (resets on every successful ping, closes at 3-in-a-row — restores the original "flapping connection" intent) and totalMissedPings (never resets, closes at 10 lifetime failures — so a connection that's chronically-but-not-consecutively flaky still eventually gets closed instead of oscillating forever). The transport closes if either threshold is hit, and the log line now reports both numbers. The 10 for the lifetime threshold is a starting point, not something I have strong data behind — open to a different number or a windowed-rate approach if you think that's a better fit.

Added tests for both: one that simulates a hung transport.close() and asserts the interval is actually registered before stop() and actually cleared after (not just that stop() resolves), and one with an oscillating 5-failures-but-never-3-consecutive pattern confirming the transport stays open, versus 3 consecutive closing it. Full suite (45 tests across the package) and tsc/eslint are clean.

Pushed as 7367b481e.

@chiruu12

Copy link
Copy Markdown

Second pass on this.

The keepAliveIntervals map does not close the leak. It is only written inside onsessioninitialized, so a transport whose initialize never completes has a live 60s interval that sits in neither keepAliveIntervals nor transports. stop() has no handle on it and onclose never fires for it, so the interval outlives the server. That is the exact case the PR title says it fixes. Register the interval when you create it and delete it on close, then both paths are covered.

totalMissedPings >= 10 is a behavior change nobody asked for and I think it is wrong. There is no window on it, so it counts ten failures across the entire lifetime of a session. A session that stays up ten hours and hits one transient blip an hour gets closed even though it was healthy at every point. Consecutive-only was the correct threshold. If a cumulative cap is actually wanted it needs a time window and a way to turn it off, and it needs its own issue.

this.transports = {} immediately after the 1000ms race drops any transport that did not close in time, so nothing ever calls close() on it again. It happens to work because closeAllConnections() reaps the socket underneath, but that is load-bearing and unstated. Either say so in a comment or leave the entry in the map on the timeout path.

The tests poll with Date.now() on real timers and assert duration < 1500. That will flake on a loaded CI runner. Fake timers plus an explicit check that the promise resolved would be deterministic. it('a) ...') and it('b) ...') also do not match the naming in the rest of the file.

The CLA check is still red, so none of this can merge yet.

One more thing, and take it however you like. The ten-strike rule came straight out of the triage bot's effort analysis at the top of the issue. That is a first pass by an automated triager, not a spec, and here it was wrong: resetting on success is exactly what a consecutive-failure counter should do. The rest of this reads the same way, right shape and wrong details, which is the usual tell. Maybe let the model do a little less of the thinking on the next one.

@Pranjulchaurasiya

Copy link
Copy Markdown
Author

Thanks again for the sharp review @chiruu12!

Updated the PR addressing all 5 items:

  1. Set-Based keepAliveIntervals: Replaced the session-keyed object map with private keepAliveIntervals: Set<NodeJS.Timeout> = new Set(). Intervals are added to the Set immediately after setInterval(...) returns (before session initialization completes), ensuring stalled or pre-handshake sessions never leak interval handles on stop().
  2. Consecutive Missed Ping Restoration: Reverted the ping logic back to standard 3 consecutive missed pings by resetting missedPings = 0 on every successful ping resolution.
  3. Refined Teardown & Transport Isolation: Removed the forced this.transports = {} overwrite in stop(), relying on closeAllConnections() for underlying socket termination and start() initialization resetting this.transports = {} cleanly across server restarts.
  4. Deterministic Unit Testing: Updated test assertions to use vi.useFakeTimers() + vi.advanceTimersByTimeAsync(1000) and explicit promise resolution checks instead of wall-clock duration bounds. Standardized test descriptions across ide-server.test.ts.

All 44 tests in packages/vscode-ide-companion pass cleanly along with type check and linter.

@gemini-cli

gemini-cli Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Hi there! Thank you for your interest in contributing to Gemini CLI.

To ensure we maintain high code quality and focus on our prioritized roadmap, we only guarantee review and consideration of pull requests for issues that are explicitly labeled as 'help wanted'.

This PR will be closed in 7 days if it remains without that designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding.

@Pranjulchaurasiya

Copy link
Copy Markdown
Author

Thanks for the review and feedback. I’ve addressed the requested changes, but I’m closing this PR for now rather than continuing with it. I appreciate the detailed review and guidance.

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

Labels

area/core Issues related to User Interface, OS Support, Core Functionality size/l A large sized PR size/m A medium sized PR size/xl An extra large PR status/pr-nudge-sent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IdeServer.stop() never resolves while an MCP session is open

2 participants