upstream passthrough: session recovery reconnects into #67's Bad Request, so a lost session is still permanent #80

Closed
opened 2026-08-17 06:18:17 +00:00 by coilyco-ops · 4 comments
Member

What

#79 added recovery for a lost upstream session: the next call replaces the session rather than failing forever. That half works. The reconnect then fails with #67's exact error, so the session is never actually replaced and the server stays dead.

Live on sirens-deep-owl-glass, from mcp-beaver's own logs:

{"msg":"tool call refused","tool":"browser_navigate","duration_ms":4,
 "outcome":"tool_error",
 "reason":"mcp-beaver: upstream MCP session is closed (reconnect also failed:
           reconnect upstream MCP \"http://127.0.0.1:8931/mcp\":
           sending \"notifications/initialized\": Bad Request)"}

And the first failure in the sequence, before the session was marked closed:

{"reason":"refresh upstream tools: calling \"tools/list\": sending \"tools/list\": Bad Request
           (reconnect also failed: reconnect upstream MCP \"http://127.0.0.1:8931/mcp\":
            sending \"notifications/initialized\": Bad Request)"}

sending "notifications/initialized": Bad Request is the string #67 reported and closed with 8cc94bd fix(upstream): reuse the one session for the drift check. It is still present on the reconnect path.

Not playwright-specific

Two independent upstreams, two different ports, same failure:

  • http://127.0.0.1:8931/mcp - playwright, browser_navigate, 5 failures
  • http://127.0.0.1:9113/mcp - bluesky, get_profile, 2 failures

Different upstream servers ruling out an upstream quirk. This is the passthrough's reconnect.

Likely shape

A fresh initialize returns a new Mcp-Session-Id. If the follow-up notifications/initialized is sent without that new id, or with the old closed one, a spec-compliant streamable-HTTP server answers 400 Bad Request - which is what both upstreams do. #67 fixed this for the drift check by reusing the one session; the reconnect path builds a new session and appears not to carry its id forward.

Not confirmed from source. duration_ms of 4 to 13 says it fails immediately rather than timing out.

Impact

The failure mode #79 was meant to remove is still there, just one layer down. A session dies once and every subsequent call fails until the pod rolls, because recovery cannot complete. Observed as 0 successes across 7 calls on the two affected servers while the same lane's other nine MCPs answered normally.

Worth naming

This took under a minute to diagnose because #78 landed. The reason string is in mcp-beaver's own log with the upstream URL and the failing JSON-RPC method in it. The previous instance of this class needed a live probe, a hand-built MCP handshake, and a guess. Same class of bug, an order of magnitude less work.

  • #79 - the recovery this lands inside, which is working as far as detection
  • #67 - where notifications/initialized: Bad Request was first reported and fixed for the drift check
  • #78 - the logging that made this readable
## What `#79` added recovery for a lost upstream session: the next call replaces the session rather than failing forever. That half works. The **reconnect** then fails with `#67`'s exact error, so the session is never actually replaced and the server stays dead. Live on `sirens-deep-owl-glass`, from mcp-beaver's own logs: ```json {"msg":"tool call refused","tool":"browser_navigate","duration_ms":4, "outcome":"tool_error", "reason":"mcp-beaver: upstream MCP session is closed (reconnect also failed: reconnect upstream MCP \"http://127.0.0.1:8931/mcp\": sending \"notifications/initialized\": Bad Request)"} ``` And the first failure in the sequence, before the session was marked closed: ```json {"reason":"refresh upstream tools: calling \"tools/list\": sending \"tools/list\": Bad Request (reconnect also failed: reconnect upstream MCP \"http://127.0.0.1:8931/mcp\": sending \"notifications/initialized\": Bad Request)"} ``` `sending "notifications/initialized": Bad Request` is the string `#67` reported and closed with `8cc94bd fix(upstream): reuse the one session for the drift check`. It is still present on the reconnect path. ## Not playwright-specific Two independent upstreams, two different ports, same failure: * `http://127.0.0.1:8931/mcp` - playwright, `browser_navigate`, 5 failures * `http://127.0.0.1:9113/mcp` - bluesky, `get_profile`, 2 failures Different upstream servers ruling out an upstream quirk. This is the passthrough's reconnect. ## Likely shape A fresh `initialize` returns a new `Mcp-Session-Id`. If the follow-up `notifications/initialized` is sent without that new id, or with the old closed one, a spec-compliant streamable-HTTP server answers `400 Bad Request` - which is what both upstreams do. `#67` fixed this for the drift check by reusing the one session; the reconnect path builds a new session and appears not to carry its id forward. Not confirmed from source. `duration_ms` of 4 to 13 says it fails immediately rather than timing out. ## Impact The failure mode `#79` was meant to remove is still there, just one layer down. A session dies once and every subsequent call fails until the pod rolls, because recovery cannot complete. Observed as 0 successes across 7 calls on the two affected servers while the same lane's other nine MCPs answered normally. ## Worth naming This took under a minute to diagnose because `#78` landed. The reason string is in mcp-beaver's own log with the upstream URL and the failing JSON-RPC method in it. The previous instance of this class needed a live probe, a hand-built MCP handshake, and a guess. Same class of bug, an order of magnitude less work. ## Related * `#79` - the recovery this lands inside, which is working as far as detection * `#67` - where `notifications/initialized: Bad Request` was first reported and fixed for the drift check * `#78` - the logging that made this readable
Author
Member

Ops confirmation, 2026-08-18. Still live on sirens-deep-owl-glass. Two findings change the shape of this, one of them a correction to the hypothesis above.

A rolling restart does not clear it

Worth stating plainly because it is the obvious first remedy and it does not work. I restarted the deployment and verified through Deep's own lane rather than from pod status:

kubectl rollout restart deployment/sirens-deep-owl-glass-playwright-mcp -n sirens-deep-owl-glass
deployment "sirens-deep-owl-glass-playwright-mcp" successfully rolled out

New pod 84d78456bf-djm8w, 2/2 Running, 0 restarts. A browser_navigate through the lane then failed with the identical error. Rollback status - none needed, a restart is not a spec change and the prior pod was already refusing every call.

The session does not survive 40 seconds on a fresh pod

This is the part that does not match "a session dies once and every subsequent call fails until the pod rolls." From the new pod's own log:

00:45:27  INFO  serving upstream proxy  mode=upstream server=playwright tools=8
00:46:07  WARN  tool call refused  reason=refresh upstream tools: calling "tools/list":
                sending "tools/list": Bad Request (reconnect also failed: ...)
00:46:21  WARN  tool call refused  reason=mcp-beaver: upstream MCP session is closed (...)

The initial handshake succeeds and snapshots all 8 tools, so initialize plus notifications/initialized is fine at that moment. Forty seconds later, with no traffic in between, tools/list on that same session returns 400. So the primary session is not dying after a long life, it is being dropped almost immediately, and only then does the reconnect path fail on top of it. Any fix aimed only at the reconnect leaves the underlying session loss in place.

Correction - the session id is not beaver's to carry

The Likely shape section above proposes that the reconnect builds a new session and does not carry its Mcp-Session-Id forward. Read from source at internal/mcpserver/upstream.go:112, beaver does not hand-roll that header at all:

func (p *proxyBackend) dial(ctx context.Context) (*mcp.ClientSession, error) {
	client := mcp.NewClient(&mcp.Implementation{Name: "mcp-beaver", Version: "0.1.0"}, nil)
	client.AddSendingMiddleware(p.telemetry.clientMiddleware)
	return client.Connect(ctx, &mcp.StreamableClientTransport{
		Endpoint:             p.endpoint,
		HTTPClient:           p.httpClient,
		DisableStandaloneSSE: true,
	}, nil)
}

reconnect calls this same dial, and session id handling lives inside the official Go SDK's StreamableClientTransport. If the id were being dropped between initialize and notifications/initialized that would be an SDK bug rather than a beaver one, and the initial dial at process start would fail the same way. It does not. Worth ruling out before anyone writes a fix against that theory.

A better candidate, marked as inference rather than proven - DisableStandaloneSSE: true on the line above. In streamable HTTP the client normally holds a standalone GET SSE stream, and a server is entitled to treat a session with no stream as collectable. That would explain a session going bad within 40 seconds of a clean handshake, on both upstreams, with no traffic. It is also the one transport option this code sets by hand. I have not confirmed it against either upstream's session bookkeeping, and the observation that would settle it is a packet or server-side log showing when the Node side discards the session.

Scope, from live inventory

Only upstream-passthrough pods with a co-located sidecar are affected. In sirens-deep-owl-glass all twelve MCPs run the same beaver image 9769d7c3, and the nine single-container guardfile servers are fine - forgejo.list_issue answered normally in the same agent turn that both browser_navigate calls failed. signoz-mcp is a third upstream-mode pod in that namespace and is recent enough that I have not exercised it, so treat it as untested rather than healthy.

Attribution is clean on the playwright side. The shared playwright-mcp service runs the identical image digest upstream-dd643326... with no beaver in front of it, reached over the tailnet, and answered a browser_tabs call for me while the lane copy was refusing everything. Same browser build, same day - the difference is the passthrough.

The deployed image is 9769d7c3, which is current main and already contains 8cc94bd from #67, so this is not a stale-image problem.

Filed from ops. No live change is available to me that clears this, the pod is already on the newest beaver, so this needs the engineer fix rather than another restart. Cross-referenced from coilyco-gaming/sirens-echo#897.

**Ops confirmation, 2026-08-18.** Still live on `sirens-deep-owl-glass`. Two findings change the shape of this, one of them a correction to the hypothesis above. ## A rolling restart does not clear it Worth stating plainly because it is the obvious first remedy and it does not work. I restarted the deployment and verified through Deep's own lane rather than from pod status: ``` kubectl rollout restart deployment/sirens-deep-owl-glass-playwright-mcp -n sirens-deep-owl-glass deployment "sirens-deep-owl-glass-playwright-mcp" successfully rolled out ``` New pod `84d78456bf-djm8w`, 2/2 Running, 0 restarts. A `browser_navigate` through the lane then failed with the identical error. Rollback status - none needed, a restart is not a spec change and the prior pod was already refusing every call. ## The session does not survive 40 seconds on a fresh pod This is the part that does not match "a session dies once and every subsequent call fails until the pod rolls." From the new pod's own log: ``` 00:45:27 INFO serving upstream proxy mode=upstream server=playwright tools=8 00:46:07 WARN tool call refused reason=refresh upstream tools: calling "tools/list": sending "tools/list": Bad Request (reconnect also failed: ...) 00:46:21 WARN tool call refused reason=mcp-beaver: upstream MCP session is closed (...) ``` The initial handshake **succeeds** and snapshots all 8 tools, so `initialize` plus `notifications/initialized` is fine at that moment. Forty seconds later, with no traffic in between, `tools/list` on that same session returns 400. So the primary session is not dying after a long life, it is being dropped almost immediately, and only then does the reconnect path fail on top of it. Any fix aimed only at the reconnect leaves the underlying session loss in place. ## Correction - the session id is not beaver's to carry The `Likely shape` section above proposes that the reconnect builds a new session and does not carry its `Mcp-Session-Id` forward. Read from source at `internal/mcpserver/upstream.go:112`, beaver does not hand-roll that header at all: ```go func (p *proxyBackend) dial(ctx context.Context) (*mcp.ClientSession, error) { client := mcp.NewClient(&mcp.Implementation{Name: "mcp-beaver", Version: "0.1.0"}, nil) client.AddSendingMiddleware(p.telemetry.clientMiddleware) return client.Connect(ctx, &mcp.StreamableClientTransport{ Endpoint: p.endpoint, HTTPClient: p.httpClient, DisableStandaloneSSE: true, }, nil) } ``` `reconnect` calls this same `dial`, and session id handling lives inside the official Go SDK's `StreamableClientTransport`. If the id were being dropped between `initialize` and `notifications/initialized` that would be an SDK bug rather than a beaver one, and the initial dial at process start would fail the same way. It does not. Worth ruling out before anyone writes a fix against that theory. **A better candidate, marked as inference rather than proven** - `DisableStandaloneSSE: true` on the line above. In streamable HTTP the client normally holds a standalone GET SSE stream, and a server is entitled to treat a session with no stream as collectable. That would explain a session going bad within 40 seconds of a clean handshake, on both upstreams, with no traffic. It is also the one transport option this code sets by hand. I have not confirmed it against either upstream's session bookkeeping, and the observation that would settle it is a packet or server-side log showing when the Node side discards the session. ## Scope, from live inventory Only upstream-passthrough pods with a co-located sidecar are affected. In `sirens-deep-owl-glass` all twelve MCPs run the same beaver image `9769d7c3`, and the nine single-container guardfile servers are fine - `forgejo.list_issue` answered normally in the same agent turn that both `browser_navigate` calls failed. `signoz-mcp` is a third upstream-mode pod in that namespace and is recent enough that I have not exercised it, so treat it as untested rather than healthy. **Attribution is clean on the playwright side.** The shared `playwright-mcp` service runs the identical image digest `upstream-dd643326...` with no beaver in front of it, reached over the tailnet, and answered a `browser_tabs` call for me while the lane copy was refusing everything. Same browser build, same day - the difference is the passthrough. The deployed image is `9769d7c3`, which is current `main` and already contains `8cc94bd` from `#67`, so this is not a stale-image problem. Filed from ops. No live change is available to me that clears this, the pod is already on the newest beaver, so this needs the engineer fix rather than another restart. Cross-referenced from `coilyco-gaming/sirens-echo#897`.
Author
Member

Engineer, 2026-08-19. Both hypotheses on this issue are refuted by measurement, and the upstream is not at fault. Recording that before anyone writes a fix against either.

Everything below was run against sirens-dowel's live broken pod and against the shared playwright-mcp service, which runs the identical image digest upstream-dd643326... and the same server build, Playwright 1.62.0-alpha-2026-06-29, protocol 2025-11-25.

Refuted: DisableStandaloneSSE

The comment above proposes that disabling the standalone GET stream lets a spec-compliant server collect the session. Tested directly with the same SDK (go-sdk v1.7.0) and the same transport options, one run per setting:

  • DisableStandaloneSSE: true - handshake, 50s idle, tools/list succeeds
  • DisableStandaloneSSE: false - handshake, 50s idle, tools/list succeeds

Extended to the interval that actually matters, a session with DisableStandaloneSSE: true survived a 14 minute idle against the shared service. The lane's session died inside 12.5 minutes (serving upstream proxy at 03:46:28, first tools/list 400 at 03:58:56). So the flag beaver sets by hand is not the variable.

Refuted: a one-session-at-a-time upstream

A second concurrent session dials cleanly while the first is open and both answer tools/list. So reconnect asking for a new session while a stale one exists is not itself the problem.

Also refuted: beaver's reconnect logic in isolation

Close-the-stale-session-then-redial on one shared http.Client, which is exactly what reconnect does, works: replacement session up, 23 tools. The code path is not wrong on its own.

The upstream is healthy, measured on the broken pod

This is the finding that moves it. Using deploy's own probe-mcp-beaver --direct, which bypasses beaver and speaks to the sidecar in the same pod:

-- direct to pod/sirens-dowel-playwright-mcp-57dc8fd5fb-788n4:8931, bypassing mcp-beaver
== initialize   server: {"name": "Playwright", "version": "1.62.0-alpha-2026-06-29"}
== tools/list   23 advertised: browser_close, browser_resize, ...

A fresh session initializes and lists all 23 tools right now, on the pod that was refusing every call. So the browser server accepts new sessions; beaver's replacement dial into the same endpoint does not. That is a passthrough defect, not an upstream one.

Two things this also rules out: the sidecar was sitting at 63Mi with 0m CPU, so it is not resource exhaustion, and the --allowed-hosts check answers 403 with a plain message, not the 400 seen here.

A clue worth chasing

My direct probe launched a browser (63Mi → 308Mi). Beaver's very next call changed failure mode: from an instant Bad Request at 3-40ms to context deadline exceeded at 60043ms, twice. So once a browser context exists, beaver's dial gets further and the call hangs instead. That suggests the 400 is tied to the upstream's state at dial time rather than to anything in the request beaver sends.

Disclosure for anyone reading that pod's logs or metrics after 01:54Z: the browser and the 60s timeouts are mine, not organic traffic.

Not a fix

coilyco-bridge/deploy@cef9a8e adds --shared-browser-context to the sirens-dowel sidecar, matching the shared service, which is the one deployment that stays up under the same idle. That is a lane-local workaround for the 2026-08-19 stream taken on Kai's call, and it trades away the per-turn context isolation the flag was omitted for. It does not fix this issue and the deploy comment says to revert it when this lands.

The defect to fix is still that a lost session is unrecoverable, and the question to answer first is why a dial that succeeds from a port-forward fails from inside the pod.

**Engineer, 2026-08-19.** Both hypotheses on this issue are refuted by measurement, and the upstream is not at fault. Recording that before anyone writes a fix against either. Everything below was run against `sirens-dowel`'s live broken pod and against the shared `playwright-mcp` service, which runs the **identical image digest** `upstream-dd643326...` and the same server build, `Playwright 1.62.0-alpha-2026-06-29`, protocol `2025-11-25`. ## Refuted: `DisableStandaloneSSE` The comment above proposes that disabling the standalone GET stream lets a spec-compliant server collect the session. Tested directly with the same SDK (`go-sdk v1.7.0`) and the same transport options, one run per setting: * `DisableStandaloneSSE: true` - handshake, 50s idle, `tools/list` **succeeds** * `DisableStandaloneSSE: false` - handshake, 50s idle, `tools/list` **succeeds** Extended to the interval that actually matters, a session with `DisableStandaloneSSE: true` **survived a 14 minute idle** against the shared service. The lane's session died inside 12.5 minutes (`serving upstream proxy` at 03:46:28, first `tools/list` 400 at 03:58:56). So the flag beaver sets by hand is not the variable. ## Refuted: a one-session-at-a-time upstream A second concurrent session dials cleanly while the first is open and both answer `tools/list`. So reconnect asking for a new session while a stale one exists is not itself the problem. ## Also refuted: beaver's reconnect logic in isolation Close-the-stale-session-then-redial on one shared `http.Client`, which is exactly what `reconnect` does, works: replacement session up, 23 tools. The code path is not wrong on its own. ## The upstream is healthy, measured on the broken pod This is the finding that moves it. Using `deploy`'s own `probe-mcp-beaver --direct`, which bypasses beaver and speaks to the sidecar in the same pod: ``` -- direct to pod/sirens-dowel-playwright-mcp-57dc8fd5fb-788n4:8931, bypassing mcp-beaver == initialize server: {"name": "Playwright", "version": "1.62.0-alpha-2026-06-29"} == tools/list 23 advertised: browser_close, browser_resize, ... ``` A **fresh session initializes and lists all 23 tools right now**, on the pod that was refusing every call. So the browser server accepts new sessions; beaver's replacement dial into the same endpoint does not. That is a passthrough defect, not an upstream one. Two things this also rules out: the sidecar was sitting at 63Mi with 0m CPU, so it is not resource exhaustion, and the `--allowed-hosts` check answers **403** with a plain message, not the 400 seen here. ## A clue worth chasing My direct probe launched a browser (63Mi → 308Mi). Beaver's very next call **changed failure mode**: from an instant `Bad Request` at 3-40ms to `context deadline exceeded` at 60043ms, twice. So once a browser context exists, beaver's dial gets further and the call hangs instead. That suggests the 400 is tied to the upstream's state at dial time rather than to anything in the request beaver sends. **Disclosure for anyone reading that pod's logs or metrics after 01:54Z: the browser and the 60s timeouts are mine, not organic traffic.** ## Not a fix `coilyco-bridge/deploy@cef9a8e` adds `--shared-browser-context` to the `sirens-dowel` sidecar, matching the shared service, which is the one deployment that stays up under the same idle. That is a lane-local workaround for the 2026-08-19 stream taken on Kai's call, and it trades away the per-turn context isolation the flag was omitted for. **It does not fix this issue** and the deploy comment says to revert it when this lands. The defect to fix is still that a lost session is unrecoverable, and the question to answer first is why a dial that succeeds from a port-forward fails from inside the pod.
Author
Member

Correcting my own comment above, and fixed in 94e4050.

I refuted DisableStandaloneSSE on the evidence that a session survives idle with the flag either way. That measurement is right and the conclusion drawn from it was wrong: I only ever exercised tools/list, and tools/list is not a tool call.

Same server, same SDK, one variable:

DisableStandaloneSSE=true    tools/call FAILED after 59.949s: context deadline exceeded
DisableStandaloneSSE=false   tools/call OK in 505ms

So the flag was the cause after all. Not by letting the session be collected, which is what the original hypothesis proposed and what my idle test correctly ruled out, but because this upstream answers a tools/call on the standalone stream, and a client holding no stream waits for an answer it cannot see.

Why every symptom said healthy

tools/list answers on the POST and works with no stream. That is the startup snapshot and the drift check, so the proxy connects, advertises all 8 tools, and reports serving upstream proxy while every real call hangs to the request budget. The pod is 2/2 with 0 restarts throughout.

The reported error also pointed away from it. Once a session died the next call reconnected, that dial failed too, and the reason surfaced was the reconnect's, not the hang underneath. Both hypotheses on this issue were built on that reconnect, and both are genuinely refuted - the session id lives in the SDK rather than here, and a second concurrent session dials cleanly. They were answers to the wrong question.

Why the tests did not catch it

Every upstream fixture in the suite serves JSONResponse: true, so it answers on the POST and structurally cannot express this failure. The new fixture answers a tools/call only on the standalone stream, and fails without the change with request terminated without response.

The POST-answering fixtures now decline the GET with 405, which the spec allows for a server offering no stream. Left open, httptest waits on that outstanding request forever at close, which is a second thing this flag was hiding.

Follow-through

The image builds from 94e4050. coilyco-bridge/deploy still carries --shared-browser-context on sirens-dowel's sidecar as a workaround from before this was understood; it bought nothing and should be reverted once every upstream-mode pod is on the new image.

Worth noting for whoever does that rollout: this affected every upstream-mode passthrough, not only playwright. bluesky was reported dead in the same shape on sirens-echo#897, and signoz-mcp runs the same mode and was never exercised.

**Correcting my own comment above, and fixed in `94e4050`.** I refuted `DisableStandaloneSSE` on the evidence that a session survives idle with the flag either way. That measurement is right and the conclusion drawn from it was wrong: **I only ever exercised `tools/list`, and `tools/list` is not a tool call.** Same server, same SDK, one variable: ``` DisableStandaloneSSE=true tools/call FAILED after 59.949s: context deadline exceeded DisableStandaloneSSE=false tools/call OK in 505ms ``` So the flag was the cause after all. Not by letting the session be collected, which is what the original hypothesis proposed and what my idle test correctly ruled out, but because **this upstream answers a `tools/call` on the standalone stream**, and a client holding no stream waits for an answer it cannot see. ## Why every symptom said healthy `tools/list` answers on the POST and works with no stream. That is the startup snapshot and the drift check, so the proxy connects, advertises all 8 tools, and reports `serving upstream proxy` while every real call hangs to the request budget. The pod is 2/2 with 0 restarts throughout. The reported error also pointed away from it. Once a session died the next call reconnected, that dial failed too, and the reason surfaced was the reconnect's, not the hang underneath. Both hypotheses on this issue were built on that reconnect, and both are genuinely refuted - the session id lives in the SDK rather than here, and a second concurrent session dials cleanly. They were answers to the wrong question. ## Why the tests did not catch it **Every upstream fixture in the suite serves `JSONResponse: true`**, so it answers on the POST and structurally cannot express this failure. The new fixture answers a `tools/call` only on the standalone stream, and fails without the change with `request terminated without response`. The POST-answering fixtures now decline the GET with 405, which the spec allows for a server offering no stream. Left open, `httptest` waits on that outstanding request forever at close, which is a second thing this flag was hiding. ## Follow-through The image builds from `94e4050`. `coilyco-bridge/deploy` still carries `--shared-browser-context` on `sirens-dowel`'s sidecar as a workaround from before this was understood; it bought nothing and should be reverted once every upstream-mode pod is on the new image. Worth noting for whoever does that rollout: this affected **every upstream-mode passthrough**, not only playwright. `bluesky` was reported dead in the same shape on `sirens-echo#897`, and `signoz-mcp` runs the same mode and was never exercised.
Author
Member

Reproduces on the fixed image, and the timing says it is the other half

94e4050 closed this, and the hang it measured is genuinely gone. The reconnect failure in this issue's title is not.

Running pod sirens-dowel-playwright-mcp, image mcp-beaver:94e40505b0c61763f37120e5cde604afc4e68631, started 02:50:23Z, 0 restarts. The commit landed 02:41Z and deploy rolled it out at 03:00Z, so both of these are after the fix:

04:04:41Z  browser_navigate  duration_ms=7  refresh upstream tools: calling "tools/list": Bad Request
                                            (reconnect also failed: ... "notifications/initialized": Bad Request)
04:12:00Z  browser_navigate  duration_ms=4  mcp-beaver: upstream MCP session is closed
                                            (reconnect also failed: ... "notifications/initialized": Bad Request)

Those are this issue's own two quoted blocks, including duration_ms: 4.

The duration is the tell. This issue's hang was 59.9s. These are 4ms and 7ms. That is consistent with 94e4050 fixing the hang while the recovery path underneath stays broken: the session still reaches closed, and notifications/initialized still returns #67's Bad Request.

The reasoning I take the fix to have made is that a call answered on the standalone stream stops hanging, so the session stops dying, so the broken reconnect is never reached. First half held, second did not.

Not the whole server: another client ran wait_for, snapshot, click, navigate, type against this same pod at 03:23:53 to 03:24:06, all ok. Split by trace id, every call carrying one is the Dowel lane's, and every one failed.

Filed the detail as #85, including two theories I built and discarded so nobody rebuilds them. Reopening this may be the better home, since this is the failure the title names.

Worth noting the standing: sirens-dowel has made 14 browser calls in 7 days with 14 failures and no success at any point, and that lane is live on a stream at 11:00 PT today.

## Reproduces on the fixed image, and the timing says it is the other half `94e4050` closed this, and the hang it measured is genuinely gone. The reconnect failure in this issue's title is not. Running pod `sirens-dowel-playwright-mcp`, image `mcp-beaver:94e40505b0c61763f37120e5cde604afc4e68631`, started 02:50:23Z, 0 restarts. The commit landed 02:41Z and deploy rolled it out at 03:00Z, so both of these are after the fix: ``` 04:04:41Z browser_navigate duration_ms=7 refresh upstream tools: calling "tools/list": Bad Request (reconnect also failed: ... "notifications/initialized": Bad Request) 04:12:00Z browser_navigate duration_ms=4 mcp-beaver: upstream MCP session is closed (reconnect also failed: ... "notifications/initialized": Bad Request) ``` **Those are this issue's own two quoted blocks, including `duration_ms: 4`.** **The duration is the tell.** This issue's hang was 59.9s. These are 4ms and 7ms. That is consistent with `94e4050` fixing the hang while the recovery path underneath stays broken: the session still reaches closed, and `notifications/initialized` still returns #67's `Bad Request`. The reasoning I take the fix to have made is that a call answered on the standalone stream stops hanging, so the session stops dying, so the broken reconnect is never reached. First half held, second did not. Not the whole server: another client ran `wait_for`, `snapshot`, `click`, `navigate`, `type` against this same pod at 03:23:53 to 03:24:06, all ok. Split by trace id, every call carrying one is the Dowel lane's, and every one failed. Filed the detail as #85, including two theories I built and discarded so nobody rebuilds them. **Reopening this may be the better home**, since this is the failure the title names. Worth noting the standing: `sirens-dowel` has made 14 browser calls in 7 days with 14 failures and no success at any point, and that lane is live on a stream at 11:00 PT today.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/mcp-beaver#80
No description provided.