From ce7d75c65462570f4d25648165009dd761f307b7 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:13:10 -0700 Subject: [PATCH] Answer not found for requests that land mid-destroy on an MCP session destroy() wipes the session's storage and the SDK's tables before it aborts the isolate. A request landing in between passed the condemned-marker check, since the marker was wiped too, and then trusted the in-memory session meta. It restored the runtime against the dropped tables and failed with "no such table: cf_agents_mcp_servers" as an unhandled 500. Once destroy starts, the cached meta no longer vouches for the session. --- .changeset/mcp-destroy-stale-session-meta.md | 5 +++++ .../src/mcp/agent-session-durable-object.ts | 13 +++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 .changeset/mcp-destroy-stale-session-meta.md diff --git a/.changeset/mcp-destroy-stale-session-meta.md b/.changeset/mcp-destroy-stale-session-meta.md new file mode 100644 index 0000000000..c772e43219 --- /dev/null +++ b/.changeset/mcp-destroy-stale-session-meta.md @@ -0,0 +1,5 @@ +--- +"@executor-js/cloud": patch +--- + +An MCP request that reaches a session while the session is being torn down now gets the "Session not found" protocol error instead of a 500. diff --git a/packages/hosts/cloudflare/src/mcp/agent-session-durable-object.ts b/packages/hosts/cloudflare/src/mcp/agent-session-durable-object.ts index 805dd14d16..aecab75fdc 100644 --- a/packages/hosts/cloudflare/src/mcp/agent-session-durable-object.ts +++ b/packages/hosts/cloudflare/src/mcp/agent-session-durable-object.ts @@ -356,6 +356,15 @@ export abstract class McpAgentSessionDOBase< * second time. */ private disposingRuntime: Promise | null = null; + /** + * Set once `destroy()` begins. The SDK's teardown drops its tables and wipes + * storage (the condemned marker included) before it aborts the isolate, and + * requests still land on this instance in between. From this point the + * in-memory session meta no longer describes stored state, so it must not + * vouch for the session: a request that trusted it would restore the runtime + * against dropped tables and fail as an unhandled error. + */ + private destroyStarted = false; private onStartPromise: Promise | null = null; private lastActivityMs = 0; private resolvedSessionName: string | undefined = undefined; @@ -650,6 +659,9 @@ export abstract class McpAgentSessionDOBase< private loadSessionMeta(): Effect.Effect { return Effect.promise(async () => { + // See `destroyStarted`: once teardown begins, the cached meta outlives + // the storage it mirrors. + if (this.destroyStarted) return null; if (this.sessionMeta) return this.sessionMeta; const stored = await this.ctx.storage.get(SESSION_META_KEY); // Backfill `resource` for sessions persisted before scoped toolkits added @@ -1784,6 +1796,7 @@ export abstract class McpAgentSessionDOBase< } override async destroy(): Promise { + this.destroyStarted = true; await this.cleanup(); await super.destroy(); }