1053 lines · 44.1 KB · Text
RawDownload
diff --git a/node_modules/agents/.bun-tag-37e67b70862be2b2 b/.bun-tag-37e67b70862be2b2new file mode 100644index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391diff --git a/node_modules/agents/.bun-tag-61b2f1517ab5ced4 b/.bun-tag-61b2f1517ab5ced4new file mode 100644index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391diff --git a/node_modules/agents/.bun-tag-c0c639aa2299e502 b/.bun-tag-c0c639aa2299e502new file mode 100644index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391diff --git a/dist/agent-tool-types-CNyE1iz_.d.ts b/dist/agent-tool-types-CNyE1iz_.d.tsindex 571eececebd5a1eaf7f2fbf5278801c4d34728ba..317e526489fcd3fd477a50525da0df666253bf0b 100644--- a/dist/agent-tool-types-CNyE1iz_.d.ts+++ b/dist/agent-tool-types-CNyE1iz_.d.ts@@ -480,7 +480,10 @@ declare class DurableObjectEventStore implements EventStore {   private readonly seqByStream;   private readonly seqInit;   constructor(storage: DurableObjectStorage);-  storeEvent(streamId: StreamId, message: JSONRPCMessage): Promise<EventId>;+  /** Resolves `undefined` for a message too large for DO storage's 128 KiB+   *  per-value cap: the event is delivered live without a replay id rather+   *  than failing the send. */+  storeEvent(streamId: StreamId, message: JSONRPCMessage): Promise<EventId | undefined>;   getStreamIdForEventId(eventId: EventId): Promise<StreamId | undefined>;   replayEventsAfter(     lastEventId: EventId,diff --git a/dist/mcp/index.d.ts b/dist/mcp/index.d.tsindex c8fad448e8797b89690a99d93490d1363851b225..77f9fe3f6f2375eadc9f7a2974f0d202fe75b3bd 100644--- a/dist/mcp/index.d.ts+++ b/dist/mcp/index.d.ts@@ -29,6 +29,7 @@ import {   xt as MCPClientOAuthCallbackConfig,   zt as ElicitResult } from "../agent-tool-types-CNyE1iz_.js";+declare const MAX_SSE_AGE_MS = 1800000; export {   type ClearableEventStore,   type CreateMcpHandlerOptions,@@ -41,6 +42,7 @@ export {   type MCPConnectionResult,   type MCPDiscoverResult,   type MCPServerOptions,+  MAX_SSE_AGE_MS,   MCP_SERVER_ID_MAX_LENGTH,   McpAgent,   type McpAuthContext,diff --git a/dist/mcp/index.js b/dist/mcp/index.jsindex 1edcf0c8c9e67aa211ae515e7672cdf79912101e..cb04fc39d14db9f7fcdfa2613b1c5babd1219e17 100644--- a/dist/mcp/index.js+++ b/dist/mcp/index.js@@ -28,13 +28,17 @@ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/ const KEEPALIVE_INTERVAL_MS = 25e3; /** SSE comment frame the parser drops before any event dispatch. */ const KEEPALIVE_FRAME = ": keepalive\n\n";+// Max age is a stalled-client memory backstop, not session idleness. Keep it+// well above Executor's 5 minute session idle timeout so active clients rarely+// rotate.+const MAX_SSE_AGE_MS = 30 * 60 * 1000; /** * Start an SSE keepalive on `writer`. Returns a `clearInterval` handle * that the stream cleanup must invoke when the stream closes. */-function startKeepalive(writer, encoder) {+function startKeepalive(writeFrame, encoder) { 	const handle = setInterval(() => {-		writer.write(encoder.encode(KEEPALIVE_FRAME)).catch(() => clearInterval(handle));+		writeFrame(encoder.encode(KEEPALIVE_FRAME)); 	}, KEEPALIVE_INTERVAL_MS); 	return handle; }@@ -180,10 +184,15 @@ const createStreamingHttpHandler = (basePath, namespace, options = {}) => { 					}); 					return new Response(body, { status: 404 }); 				}-				const { readable, writable } = new TransformStream();-				const writer = writable.getWriter();-				const encoder = new TextEncoder();-				const existingHeaders = {};+					const { readable, writable } = new TransformStream();+					const writer = writable.getWriter();+					const encoder = new TextEncoder();+					const MAX_PENDING_SSE_BYTES = 8 * 1024 * 1024;+					let __pendingBytes = 0;+					let __writeChain = Promise.resolve();+					let __sseClosed = false;+					let keepAlive;+					const existingHeaders = {}; 				request.headers.forEach((value, key) => { 					existingHeaders[key] = value; 				});@@ -206,45 +215,99 @@ const createStreamingHttpHandler = (basePath, namespace, options = {}) => { 						jsonrpc: "2.0" 					}); 					return new Response(body, { status: 500 });-				}-				ws.accept();-				if (messages.every((msg) => isJSONRPCNotification(msg) || isJSONRPCResultResponse(msg))) {-					ws.close();-					return new Response(null, {+					}+					ws.accept();+					const __closeSse = () => {+						if (__sseClosed) return;+						__sseClosed = true;+						try {+							clearInterval(keepAlive);+						} catch {}+						try {+							ws.close(1013, "SSE client not draining");+						} catch {}+						writer.abort(new Error("SSE client not draining")).catch(() => {});+					};+					const __markSseClientClosed = () => {+						if (__sseClosed) return;+						__sseClosed = true;+						try {+							clearInterval(keepAlive);+						} catch {}+						writer.abort(new Error("SSE client disconnected")).catch(() => {});+					};+					writer.closed?.catch(() => {+						__markSseClientClosed();+					});+					request.signal.addEventListener("abort", __closeSse, { once: true });+					const __forwardSse = (frame) => {+						if (__sseClosed) return __writeChain;+						if (__pendingBytes + frame.byteLength > MAX_PENDING_SSE_BYTES) {+							__closeSse();+							return Promise.resolve();+						}+						__pendingBytes += frame.byteLength;+						__writeChain = __writeChain.then(() => writer.write(frame)).catch(() => {+							__closeSse();+						}).finally(() => {+							__pendingBytes -= frame.byteLength;+						});+						return __writeChain;+					};+					keepAlive = startKeepalive(__forwardSse, encoder);+					if (messages.every((msg) => isJSONRPCNotification(msg) || isJSONRPCResultResponse(msg))) {+						clearInterval(keepAlive);+						ws.close();+						return new Response(null, { 						headers: corsHeaders(request, options.corsOptions), 						status: 202-					});-				}-				const keepAlive = startKeepalive(writer, encoder);-				ws.addEventListener("message", (event) => {+						});+					}+					ws.addEventListener("message", (event) => { 					async function onMessage(event) { 						try {-							const data = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data);-							const message = JSON.parse(data);-							if (message.type !== "cf_mcp_agent_event") return;-							await writer.write(encoder.encode(message.event));-							if (message.close) {-								clearInterval(keepAlive);-								ws?.close();-								await writer.close().catch(() => {});-							}+								const data = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data);+								const message = JSON.parse(data);+								if (message.type !== "cf_mcp_agent_event") return;+								const writePromise = __forwardSse(encoder.encode(message.event));+								if (message.close) {+									clearInterval(keepAlive);+									await writePromise;+									await writer.close();+									if (!__sseClosed && !request.signal.aborted) {+										// workerd resolves writer.close() even when the client+										// canceled the POST response body, and request.signal does+										// not reliably fire for that cancellation, so a successful+										// close is NOT proof of delivery. Never ack POST-stream+										// deliveries: the DO keeps each response persisted for its+										// own Last-Event-ID recovery or an explicitly cursorless+										// fresh GET. A client that DID receive the result closes the+										// POST body reader without recovery, so the worst case of+										// this at-least-once choice is a duplicate on that fresh-GET+										// fallback, never a response injected into another cursor's+										// recovery stream.+										ws?.close(1000, "SSE response delivered");+									}+								} 						} catch (error) { 							console.error("Error forwarding message to SSE:", error); 						} 					} 					onMessage(event).catch(console.error); 				});-				ws.addEventListener("error", (error) => {-					async function onError(_error) {-						clearInterval(keepAlive);-						await writer.close().catch(() => {});+					ws.addEventListener("error", (error) => {+						async function onError(_error) {+							__sseClosed = true;+							clearInterval(keepAlive);+							await writer.close().catch(() => {}); 					} 					onError(error).catch(console.error); 				});-				ws.addEventListener("close", () => {-					async function onClose() {-						clearInterval(keepAlive);-						await writer.close().catch(() => {});+					ws.addEventListener("close", () => {+						async function onClose() {+							__sseClosed = true;+							clearInterval(keepAlive);+							await writer.close().catch(() => {}); 					} 					onClose().catch(console.error); 				});@@ -279,10 +342,16 @@ const createStreamingHttpHandler = (basePath, namespace, options = {}) => { 					id: null, 					jsonrpc: "2.0" 				}), { status: 400 });-				const { readable, writable } = new TransformStream();-				const writer = writable.getWriter();-				const encoder = new TextEncoder();-				const agent = await getAgentByName(namespace, `streamable-http:${sessionId}`, {+					const { readable, writable } = new TransformStream();+					const writer = writable.getWriter();+					const encoder = new TextEncoder();+					const __openedAt = Date.now();+					const MAX_PENDING_SSE_BYTES = 8 * 1024 * 1024;+					let __pendingBytes = 0;+					let __writeChain = Promise.resolve();+					let __sseClosed = false;+					let keepAlive;+					const agent = await getAgentByName(namespace, `streamable-http:${sessionId}`, { 					props: ctx.props, 					jurisdiction: options.jurisdiction 				});@@ -306,27 +375,116 @@ const createStreamingHttpHandler = (basePath, namespace, options = {}) => { 				if (!ws) { 					await writer.close(); 					return new Response("Failed to establish WS to DO", { status: 500 });-				}-				ws.accept();-				ws.addEventListener("message", (event) => {+					}+					ws.accept();+					const __abortSse = () => {+						if (__sseClosed) return;+						__sseClosed = true;+						try {+							clearInterval(keepAlive);+						} catch {}+						try {+							ws.close(1013, "SSE client not draining");+						} catch {}+						writer.abort(new Error("SSE client not draining")).catch(() => {});+					};+					writer.closed?.catch(() => {+						__abortSse();+					});+					request.signal.addEventListener("abort", __abortSse, { once: true });+					const __closeSseGracefully = () => {+						if (__sseClosed) return __writeChain;+						__sseClosed = true;+						try {+							clearInterval(keepAlive);+						} catch {}+						try {+							const finalFrame = encoder.encode(": max-age rotation, reconnect\n\n");+							__writeChain = __writeChain.then(() => writer.write(finalFrame)).catch(() => {}).then(() => writer.close()).catch(() => {+								writer.abort().catch(() => {});+							});+						} catch {+							__writeChain = writer.close().catch(() => {+								writer.abort().catch(() => {});+							});+						}+						try {+							ws.close(1000, "sse_max_age_rotation");+						} catch {}+						return __writeChain;+					};+					const __forwardSse = (frame) => {+						if (__sseClosed) return __writeChain;+						const ageMs = Date.now() - __openedAt;+						if (ageMs > MAX_SSE_AGE_MS) {+							console.log(JSON.stringify({+								event: "sse_max_age_close",+								sessionId,+								variant: "streamable-get",+								ageMs,+								pendingBytes: __pendingBytes+							}));+							__closeSseGracefully();+							return Promise.resolve();+						}+						if (__pendingBytes + frame.byteLength > MAX_PENDING_SSE_BYTES) {+							__abortSse();+							return Promise.resolve();+						}+						__pendingBytes += frame.byteLength;+						__writeChain = __writeChain.then(() => writer.write(frame)).catch(() => {+							__abortSse();+						}).finally(() => {+							__pendingBytes -= frame.byteLength;+						});+						return __writeChain;+					};+					keepAlive = startKeepalive(__forwardSse, encoder);+					ws.addEventListener("message", (event) => { 					try { 						async function onMessage(ev) {-							const data = typeof ev.data === "string" ? ev.data : new TextDecoder().decode(ev.data);-							const message = JSON.parse(data);-							if (message.type !== "cf_mcp_agent_event") return;-							await writer.write(encoder.encode(message.event));-						}+								const data = typeof ev.data === "string" ? ev.data : new TextDecoder().decode(ev.data);+								const message = JSON.parse(data);+								if (message.type !== "cf_mcp_agent_event") return;+								const writePromise = __forwardSse(encoder.encode(message.event));+								if (message.close) {+									clearInterval(keepAlive);+									await writePromise;+									await writer.close();+									if (!__sseClosed && !request.signal.aborted) {+										// Storage is cleared only on this ack, which fires only+										// after writer.close() resolved with the client still+										// attached; a replayed response enqueued into a dead GET+										// never acks and stays replayable. `ackStreamIds` lets a+										// replay-complete frame confirm several replayed streams+										// at once; a live final response carries its `streamId`.+										const ackStreamIds = Array.isArray(message.ackStreamIds) ? message.ackStreamIds : message.streamId ? [message.streamId] : [];+										for (const ackStreamId of ackStreamIds) try {+											ws.send(JSON.stringify({+												type: "cf_mcp_delivery_ack",+												eventId: message.eventId,+												streamId: ackStreamId+											}));+										} catch {}+										ws?.close(1000, "SSE response delivered");+									}+								}+							} 						onMessage(event).catch(console.error); 					} catch (e) { 						console.error("Error forwarding message to SSE:", e); 					}-				});-				ws.addEventListener("error", () => {-					writer.close().catch(() => {});-				});-				ws.addEventListener("close", () => {-					writer.close().catch(() => {});-				});+					});+					ws.addEventListener("error", () => {+						__sseClosed = true;+						clearInterval(keepAlive);+						writer.close().catch(() => {});+					});+					ws.addEventListener("close", () => {+						__sseClosed = true;+						clearInterval(keepAlive);+						writer.close().catch(() => {});+					}); 				return new Response(readable, { 					headers: { 						"Cache-Control": "no-cache",@@ -389,10 +547,16 @@ const createLegacySseHandler = (basePath, namespace, options = {}) => { 		const url = new URL(request.url); 		if (request.method === "GET" && basePattern.test(url)) { 			const sessionId = url.searchParams.get("sessionId") || namespace.newUniqueId().toString();-			const { readable, writable } = new TransformStream();-			const writer = writable.getWriter();-			const encoder = new TextEncoder();-			const endpointUrl = new URL(request.url);+				const { readable, writable } = new TransformStream();+				const writer = writable.getWriter();+				const encoder = new TextEncoder();+				const __openedAt = Date.now();+				const MAX_PENDING_SSE_BYTES = 8 * 1024 * 1024;+				let __pendingBytes = 0;+				let __writeChain = Promise.resolve();+				let __sseClosed = false;+				let keepAlive;+				const endpointUrl = new URL(request.url); 			endpointUrl.pathname = encodeURI(`${basePath}/message`); 			endpointUrl.searchParams.set("sessionId", sessionId); 			const endpointMessage = `event: endpoint\ndata: ${endpointUrl.pathname + endpointUrl.search + endpointUrl.hash}\n\n`;@@ -414,35 +578,94 @@ const createLegacySseHandler = (basePath, namespace, options = {}) => { 				console.error("Failed to establish WebSocket connection"); 				await writer.close(); 				return new Response("Failed to establish WebSocket connection", { status: 500 });-			}-			ws.accept();-			ws.addEventListener("message", (event) => {+				}+				ws.accept();+				const __abortSse = () => {+					__sseClosed = true;+					try {+						clearInterval(keepAlive);+					} catch {}+					try {+						ws.close(1013, "SSE client not draining");+					} catch {}+					writer.abort(new Error("SSE client not draining")).catch(() => {});+				};+				const __closeSseGracefully = () => {+					__sseClosed = true;+					try {+						clearInterval(keepAlive);+					} catch {}+					try {+						const finalFrame = encoder.encode(": max-age rotation, reconnect\n\n");+						__writeChain = __writeChain.then(() => writer.write(finalFrame)).catch(() => {}).then(() => writer.close()).catch(() => {+							writer.abort().catch(() => {});+						});+					} catch {+						__writeChain = writer.close().catch(() => {+							writer.abort().catch(() => {});+						});+					}+					try {+						ws.close(1000, "sse_max_age_rotation");+					} catch {}+					return __writeChain;+				};+				const __forwardSse = (frame) => {+					if (__sseClosed) return __writeChain;+					const ageMs = Date.now() - __openedAt;+					if (ageMs > MAX_SSE_AGE_MS) {+						console.log(JSON.stringify({+							event: "sse_max_age_close",+							sessionId,+							variant: "legacy-sse",+							ageMs,+							pendingBytes: __pendingBytes+						}));+						__closeSseGracefully();+						return Promise.resolve();+					}+					if (__pendingBytes + frame.byteLength > MAX_PENDING_SSE_BYTES) {+						__abortSse();+						return Promise.resolve();+					}+					__pendingBytes += frame.byteLength;+					__writeChain = __writeChain.then(() => writer.write(frame)).catch(() => {}).finally(() => {+						__pendingBytes -= frame.byteLength;+					});+					return __writeChain;+				};+				keepAlive = startKeepalive(__forwardSse, encoder);+				ws.addEventListener("message", (event) => { 				async function onMessage(event) { 					try { 						const message = JSON.parse(event.data);-						const result = JSONRPCMessageSchema.safeParse(message);-						if (!result.success) return;-						const messageText = `event: message\ndata: ${JSON.stringify(result.data)}\n\n`;-						await writer.write(encoder.encode(messageText));-					} catch (error) {+							const result = JSONRPCMessageSchema.safeParse(message);+							if (!result.success) return;+							const messageText = `event: message\ndata: ${JSON.stringify(result.data)}\n\n`;+							__forwardSse(encoder.encode(messageText));+						} catch (error) { 						console.error("Error forwarding message to SSE:", error); 					} 				} 				onMessage(event).catch(console.error); 			});-			ws.addEventListener("error", (error) => {-				async function onError(_error) {-					try {-						await writer.close();-					} catch (_e) {}+				ws.addEventListener("error", (error) => {+					async function onError(_error) {+						try {+							__sseClosed = true;+							clearInterval(keepAlive);+							await writer.close();+						} catch (_e) {} 				} 				onError(error).catch(console.error); 			});-			ws.addEventListener("close", () => {-				async function onClose() {-					try {-						await writer.close();-					} catch (error) {+				ws.addEventListener("close", () => {+					async function onClose() {+						try {+							__sseClosed = true;+							clearInterval(keepAlive);+							await writer.close();+						} catch (error) { 						console.error("Error closing SSE connection:", error);

Lines 1–500 of 1053.