fix(mcp): answer a tool call whose response never arrives - #690
Merged
ducnmm merged 1 commit intoAug 19, 2026
Merged
Conversation
Collaborator
Style Guide AuditAll 1 file(s) pass the style guide audit. |
The bridge had no deadline of its own for a request. Its only timer watches for silence on the SSE stream, and the relayer's 3s keepalive keeps that stream looking healthy, so a reply lost while the connection is still up trips nothing: no watchdog, therefore no reconnect, therefore no replay. The request sat in `inFlight` forever and the caller could only report a bare timeout with nothing to act on. Stamp each in-flight entry with `startedAt` and sweep for expired ones. The stamp is never refreshed, not even by a replay, since a reconnect loop would otherwise keep pushing the deadline out and reintroduce the same indefinite wait. The sweep period is capped at 5s so the deadline is honoured closely rather than drifting by a quarter of itself. Expired calls close out through the existing `failRequest` path, which already adds the id to `closedOutIds` so a late genuine reply is dropped rather than becoming a second response for that id. `failRequest` gains optional wording because its "relayer unavailable" text would be wrong here: the relayer is fine, one reply just went missing, and saying otherwise sends whoever debugs it next in the wrong direction. The default of 240s clears the slowest server-side tool deadline (`analyze` at 180s) so a healthy request is never called orphaned while the relayer is still working on it. `MEMWAL_MCP_CALL_TIMEOUT_MS` overrides it. Also log the in-flight ids and methods at reconnect rather than just the count. The reported `replayCount: 0` could not be reproduced from the code — all three `inFlight.delete` sites require an actual response frame, and `inFlight.clear()` only runs on an account switch — so the next occurrence needs to show whether the map was empty or the entry was removed early. Refs WALM-328
HoangDucBach
force-pushed
the
wyner/walm-328-issue-sse-bridge-idle-close-orphans-in-flight-remember-calls
branch
from
August 18, 2026 23:55
b82f5ae to
4d6c87d
Compare
Collaborator
Author
Fixed |
ducnmm
approved these changes
Aug 19, 2026
ducnmm
left a comment
Collaborator
There was a problem hiding this comment.
LGTM! The per-call deadline handling, sweeper with unref/clear, late response suppression in closedOutIds, and regression test in orphaned-call.test.mjs look solid.
This was referenced Aug 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ticket
WALM-328 — the MCP bridge leaves a tool call hanging when its response is lost.
What changed?
All in
packages/mcp/src/bridge.ts.startedAtand a sweeper closes out the expired ones. The stamp is never refreshed, not even by a replay, or a reconnect loop would keep pushing the deadline out and reintroduce the same hang.failRequest, which already adds the id toclosedOutIdsso a late genuine reply is dropped instead of becoming a second response for that id.failRequestgains optional wording: its "relayer unavailable" text would be wrong here, since the relayer is fine and only one reply went missing.MEMWAL_MCP_CALL_TIMEOUT_MS, derived in code from the slowest server-side tool deadline (analyze) plus headroom, so it moves with that tool instead of being pinned. The sweep period is capped at 5s so a long deadline is honoured closely.Why is this needed?
The bridge had no deadline of its own. Its only timer watches for silence on the SSE stream, and the relayer's 3s keepalive keeps that stream looking healthy — so a reply lost while the connection is still up trips nothing: no watchdog, therefore no reconnect, therefore no replay. The request sat in
inFlightforever and the caller could only report a bare timeout.Two of the three options in the issue were already implemented: the sidecar keepalive (
services/server/scripts/mcp/index.ts:164-184) and in-flight replay (bridge.ts:832-857). The third, an explicit orphaned-call error, is what this adds.Scope
The bridge's own timers. No transport, protocol or relayer change.
Out of scope
notifications/progress, which the MCP SDK honours viaresetTimeoutOnProgressand would stop the client giving up at its own 60s default. The protocol's proper answer for long calls; worth its own ticket.inFlight.setruns before the send branch, so a call the relayer already executed is re-POSTed verbatim, and no idempotency key crosses the bridge.How was this tested?
New
test/orphaned-call.test.mjsreproduces the exact gap: the mock relayer keeps heartbeating throughout, answersinitialize, then swallows thememwal_rememberreply. It asserts the call is closed out with a retryable error, thatgetSseGetCount() === 1so the watchdog demonstrably did not rescue it, and that a late genuine reply is dropped rather than delivered twice.Verified the test earns its place: with the sweeper disabled it fails with
timed out waiting for message, the reported symptom. Suite is 19/19,tscclean.One caveat worth stating: a single unattributed failure appeared in one run and did not reproduce across 18 further runs on this branch or 8 on
origin/dev. Most likely machine load, since these tests spawn child processes on real timers, but it is unexplained.How can the reviewer verify it?
Risks and dependencies
SLOWEST_SERVER_TOOL_MSduplicates a value from another package.analyze's 180s lives inservices/server/scripts/mcp/tools/, which this package cannot import from. If that tool is granted longer and this is not raised with it, the bridge will start declaring healthy requests orphaned. The constant carries that warning.Author checklist