Skip to content

Fail WebMCP invocations fast with tool_page_unloaded when their page is replaced - #498

Merged
rgarcia merged 4 commits into
mainfrom
hypeship/webmcp-fail-fast-on-reload
Nov 1, 2026
Merged

rgarcia merged 4 commits into
mainfrom
hypeship/webmcp-fail-fast-on-reload

Conversation

@rgarcia

@rgarcia rgarcia commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

When the document that owns a pending WebMCP invocation is reloaded or navigated by anything other than the tool itself, POST /webmcp/invoke waits out the full timeout_sec (up to 120s) before returning outcome_unknown, even though no result can ever arrive. This fails those invocations fast with a distinct 410 tool_page_unloaded, and marks both invoke failures that may have had side effects as non-retryable for SDKs.

Chromium only answers a pending invocation across a navigation when the tool started that navigation (WebMCPHandler::DidFinishNavigation checks GetScriptToolInvocationId()): cross-origin gets an immediate Error, same-origin gets a result from the new document around its DOMContentLoaded. A reload or navigation from Playwright, the user, or unrelated page script never produces WebMCP.toolResponded. webmcpclient dropped the frame's tools on a document change but kept the invocation waiting.

Changes

Fail fast (lib/webmcpclient)

  • Mark a waiting invocation when its frame commits a new document (EventDocumentChanged).
  • When that replacement document finishes loading (Page.frameStoppedLoading on the same session and frame), wait 500ms more, then fail the invocation with ErrToolPageUnloaded (keeps invocation_id).
  • Waiting for the replacement load rather than a fixed delay after commit keeps tool-initiated navigations working: their result can arrive seconds after commit on slow pages, but always before the load finishes.

Renderer crashes (lib/webmcpclient)

  • Chrome keeps a crashed tab open, so its tools stayed in discovery. Invoking one hung for the full timeout_sec (the WebMCP.invokeTool command to a dead renderer never answers), and an invocation pending at crash time also waited out the timeout.
  • On Inspector.targetCrashed, drop that session's tools and fail its pending invocations with ErrToolPageUnloaded after the same 500ms grace. The session stays enabled, so tools registered after the tab is reloaded or navigated are picked up.
  • A recovery reload right after the crash keeps the crash deadline.

API (cmd/api/api/webmcp.go, openapi.yaml, regenerated lib/oapi/oapi.go)

  • ErrToolPageUnloaded → 410 with code: "tool_page_unloaded", message, and invocation_id.
  • 410 tool_page_unloaded and the existing 504 outcome_unknown both set X-Should-Retry: false. Kernel SDKs retry 408/409/429/5xx by default unless that header says otherwise, so a default-configured client could re-invoke a tool after a 504 even though it may already have run. 410 isn't retried by the SDKs even without the header. Both headers are declared in the spec.
  • 410 reports a known cause, not a known outcome: the tool may have had side effects before its page went away, so callers must not retry it automatically.
  • REPL help (webmcp.invokeTool) and docs/repl.md mention the new code.

Deploy order

kernel/kernel#5165 must deploy first. Until metro-api allows the new code, its /browser/kernel passthrough rewrites 410 tool_page_unloaded to session_gone. That PR also adds the header and 410 passthrough on the control-plane path. Docs: kernel/docs#738.

Tests

  • New regression fixture reloadWithoutResult in the fake CDP server replays the event sequence observed from Chromium for an external reload (frameStartedLoading → frameNavigated → frameStoppedLoading, no toolResponded). TestInvocationFailsFastWhenToolDocumentReloadsWithoutResult waited the full 10s before this change and returns ErrToolPageUnloaded in about 1s after.
  • The existing navigate-then-respond fixture now emits frameStoppedLoading before the late result, covering the grace window.
  • TestInvocationFailsFastWhenToolRendererCrashes (crash while pending, tools removed, stale ref returns ErrToolNotFound) and TestInvocationFailsFastWhenCrashedToolPageReloads (crash, then a recovery commit that never finishes loading) both waited the full 10s before the fix.
  • Handler tests check status, X-Should-Retry, and body for both the 504 and the 410.
  • go vet clean; go test -race passes for all non-e2e packages except lib/devtoolsproxy TestUpstreamManagerDetectsChromiumAndRestart, which fails the same way on main in my environment (temp dir cleanup with local Chromium) and doesn't depend on this change.
  • Drove a webmcpclient.Manager with the fail-fast change against a production Chromium 155 browser over CDP (20s timeout):
scenario before after
external reload / Page.navigate / page-script reload 20.0s outcome_unknown 1.9–2.3s, invocation failed
tool reloads its own page Completed with output Completed with output
tool navigates cross-origin Chromium Error Chromium Error
no navigation 20s timeout 20s timeout

Renderer crash via CDP Page.crash (15s timeout):

scenario before after
invocation pending when the tab crashes 15.0s outcome_unknown 1.7s tool_page_unloaded
tools listed after the crash crashed tab's tools none
invoke a crashed tab's tool ref 15.0s outcome_unknown immediate 404
navigate the crashed tab, register, invoke ×3 ~0.02s each ~0.02s each

Not included

  • A dispatched field on failures.
  • A reload or crash in the short window between WebMCP.invokeTool dispatch and its response isn't tracked and still waits for the timeout.

Reviewed with sol-loop: 5 rounds with openai-codex/gpt-6.1-sol on extra-high reasoning. Sol verdict: approved.

@rgarcia rgarcia changed the title Fail WebMCP invocations fast when their document is replaced without a result Fail WebMCP invocations fast with tool_page_unloaded when their page is replaced Nov 1, 2026
@rgarcia
rgarcia requested a review from tnsardesai November 1, 2026 19:29
@rgarcia

rgarcia commented Nov 1, 2026

Copy link
Copy Markdown
Contributor Author

Picked tnsardesai to review these related PRs (kernel/kernel#5165, kernel/docs#738): touched the changed code in webmcpclient; nothing pending

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2938c84. Configure here.

}
c.removeSessionToolsLocked(message.SessionID)
c.stateMu.Unlock()
c.signalStateChanged()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Crash handler never receives CDP events

High Severity

The new Inspector.targetCrashed path never runs against a real browser because nothing in webmcpclient or browsersurface sends Inspector.enable. Chromium only delivers that event after the domain is enabled; cdpmonitor already does this on its own sessions. Fake CDP tests inject the event directly, so they pass while production renderer crashes still wait out the full timeout instead of returning tool_page_unloaded.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2938c84. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not an issue: Chromium sends Inspector.targetCrashed to every session attached to the target whether or not Inspector.enable was called. InspectorHandler::TargetCrashed() emits the event unconditionally; Enable() only re-reports a crash that happened before enable.

Checked against a production browser (Chromium 155): attached a flattened session, sent only Page.navigate and Page.crash, never Inspector.enable, and received Inspector.targetCrashed on that session. The end-to-end run in the PR description used the same unmodified webmcpclient connection, which is why the crash case dropped the tools and failed in 1.7s instead of 15s.

@tnsardesai tnsardesai 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.

LGTM

@rgarcia
rgarcia merged commit 6295002 into main Nov 1, 2026
12 checks passed
@rgarcia
rgarcia deleted the hypeship/webmcp-fail-fast-on-reload branch November 1, 2026 22:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants