Skip to content

Fail loudly when a DO restart kills an in-flight MCP tool call - #1489

Draft
RhysSullivan wants to merge 1 commit into
mainfrom
fail-loudly-on-do-restart
Draft

Fail loudly when a DO restart kills an in-flight MCP tool call#1489
RhysSullivan wants to merge 1 commit into
mainfrom
fail-loudly-on-do-restart

Conversation

@RhysSullivan

Copy link
Copy Markdown
Collaborator

When a deploy resets a Durable Object mid tool call, the running execution dies
with the isolate. The client's POST SSE stream closes cleanly, so the MCP client
neither errors nor reconnects — it hangs to its own timeout (~90s observed) with
no server-side exception and no exported span. The failure is invisible in
observability: production traces carry Durable Object reset because its code was updated only on session-establishment spans, and every mcp.host.tool.execute
span is complete, because killed executions never export a span at all.

Reproduced with overlapping deploys: a single deploy did not trigger it (30/30
clean), rapid successive deploy bursts did (4/4).

What this does

On every runtime start, scan for POST streams that still carry persisted
in-flight request ids. The transport writes that key when a request is
dispatched and deletes it the moment the final response is written, so a
surviving entry at start means "dispatched, never answered" — after a restart
that can only be work the reset destroyed. Each one gets a JSON-RPC error
appended to its event log at the next sequence number, and the request-id key
dropped. A reconnecting client replays from last-event-id and gets a real
error in seconds instead of hanging.

The client half of the fix — the SSE priming event that makes the MCP SDK
willing to reconnect a dropped POST stream at all — already ships in
patches/agents@0.17.3.patch. Without it the error written here would sit in
storage unread. Both halves are required; measured on a deployed worker:
unfixed hung 91.5s, priming alone hung 90.8s, priming + reaper failed in 6.3s.

Retry safety — the main judgment call

The prototype's message said "please retry". This ships something different, on
purpose. execute runs arbitrary user code, which may already have charged a
card, sent mail, or written to a third party before the isolate died. The server
cannot know how far it got — the only evidence was in the dead isolate — so it
cannot honestly promise a retry is safe, and an LLM client will act on whatever
the message implies.

The error therefore states what is known and hands the decision back:

Execution was interrupted by a server restart and its outcome is unknown. Any
side effects it had already performed have been applied. Do not retry
automatically — verify the current state first, then re-run only if it is safe.

with data: { reason: "server_restart", retrySafe: false } carrying the same
fact machine-readably. A test pins that the message never says "please retry".
Making retry genuinely safe needs idempotency keys or durable execution; this
change does not pretend to have them.

Scope and edge cases

  • Converts a silent hang into a prompt visible error. It does not recover
    the lost work — durable execution is separate and out of scope.
  • Observability: the reap ends its own McpSessionDO.restart_reap span as a
    failure, which the OTEL tracer renders as status ERROR with a recorded
    exception, so a deploy that ate N tool calls becomes queryable rather than an
    absence. Pinned with a recording tracer; no changes to shared telemetry wiring.
  • Batched requests: every request id on a stream is reaped, not just the first —
    the transport only closes a stream once all of them have responded, so missing
    one still hangs the client.
  • Standalone GET stream (_GET_stream): explicitly skipped. It carries no POST
    request ids so it cannot look orphaned today, but the skip is explicit so a
    future SDK change cannot make the reaper cancel a live listen stream.
  • Failure ordering: the request-id key is dropped only after its errors are
    durably stored, so a reset during the reap leaves the stream reapable on the
    next start rather than silently forgotten.
  • Reaper failures are logged and swallowed; a session still starts.

SDK seam

getOpenStreamRequestIds is @internal in agents. Rather than repeat the
prototype's raw storage-prefix manipulation, all SDK-shaped access is behind one
structurally-typed interface in restart-reaper.ts with a runtime shape check.
Nothing else in the module knows a storage key, prefix, or the seq zero-padding.
An agents upgrade that moves this surface breaks in exactly one place, and if
the method disappears the reaper stands down instead of throwing on every cold
start.

Verification

  • bun run typecheck — 43/43 tasks pass
  • bun run lint — 0 warnings, 0 errors
  • bun run format:check — all files correctly formatted
  • bun run test — 37/37 tasks pass
  • 18 new tests (10 unit + 8 DO-level). Confirmed non-vacuous: removing the
    reaper call makes the reaping tests fail with empty replays, while the
    no-false-positive test still passes.

@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare preview

Console https://executor-preview-pr-1489.executor-e2e.workers.dev
MCP https://executor-preview-pr-1489.executor-e2e.workers.dev/mcp
Deployed commit 888bba6

Sign-in is Cloudflare Access (one-time PIN to an allowed email). The preview has its own database and encryption key; it is destroyed when this PR closes.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
executor-marketing 888bba6 Commit Preview URL

Branch Preview URL
Jul 28 2026, 11:49 PM

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
executor-cloud 888bba6 Jul 28 2026, 11:50 PM

@pkg-pr-new

pkg-pr-new Bot commented Jul 28, 2026

Copy link
Copy Markdown

Open in StackBlitz

@executor-js/cli

npm i https://pkg.pr.new/@executor-js/cli@1489

@executor-js/config

npm i https://pkg.pr.new/@executor-js/config@1489

@executor-js/execution

npm i https://pkg.pr.new/@executor-js/execution@1489

@executor-js/sdk

npm i https://pkg.pr.new/@executor-js/sdk@1489

@executor-js/codemode-core

npm i https://pkg.pr.new/@executor-js/codemode-core@1489

@executor-js/runtime-quickjs

npm i https://pkg.pr.new/@executor-js/runtime-quickjs@1489

@executor-js/plugin-file-secrets

npm i https://pkg.pr.new/@executor-js/plugin-file-secrets@1489

@executor-js/plugin-graphql

npm i https://pkg.pr.new/@executor-js/plugin-graphql@1489

@executor-js/plugin-keychain

npm i https://pkg.pr.new/@executor-js/plugin-keychain@1489

@executor-js/plugin-mcp

npm i https://pkg.pr.new/@executor-js/plugin-mcp@1489

@executor-js/plugin-onepassword

npm i https://pkg.pr.new/@executor-js/plugin-onepassword@1489

@executor-js/plugin-openapi

npm i https://pkg.pr.new/@executor-js/plugin-openapi@1489

executor

npm i https://pkg.pr.new/executor@1489

commit: 888bba6

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.

1 participant