From 8cd86541ca8b9a2f59a884a5ec0d112de95319b6 Mon Sep 17 00:00:00 2001 From: Alessandro Pogliaghi Date: Thu, 2 Apr 2026 10:52:31 +0100 Subject: [PATCH] fix(cloud): avoid recursive SSE failures --- packages/agent/src/server/agent-server.ts | 23 ++++--- .../agent/src/server/question-relay.test.ts | 65 +++++++++++++++++++ 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/packages/agent/src/server/agent-server.ts b/packages/agent/src/server/agent-server.ts index 76f537fe6..bafd1fdf1 100644 --- a/packages/agent/src/server/agent-server.ts +++ b/packages/agent/src/server/agent-server.ts @@ -169,6 +169,12 @@ export class AgentServer { private initializationPromise: Promise | null = null; private pendingEvents: Record[] = []; + private detachSseController(controller: SseController): void { + if (this.session?.sseController === controller) { + this.session.sseController = null; + } + } + private emitConsoleLog = ( level: LogLevel, _scope: string, @@ -250,18 +256,15 @@ export class AgentServer { controller.enqueue( new TextEncoder().encode(`data: ${JSON.stringify(data)}\n\n`), ); - } catch (error) { - this.logger.debug( - "SSE send failed (stream may be closed)", - error, - ); + } catch { + this.detachSseController(sseController); } }, close: () => { try { controller.close(); - } catch (error) { - this.logger.debug("SSE close failed (already closed)", error); + } catch { + this.detachSseController(sseController); } }, }; @@ -1577,6 +1580,10 @@ Important: } private sendSseEvent(controller: SseController, data: unknown): void { - controller.send(data); + try { + controller.send(data); + } catch { + this.detachSseController(controller); + } } } diff --git a/packages/agent/src/server/question-relay.test.ts b/packages/agent/src/server/question-relay.test.ts index b4d95dfc4..bdac3891f 100644 --- a/packages/agent/src/server/question-relay.test.ts +++ b/packages/agent/src/server/question-relay.test.ts @@ -235,6 +235,71 @@ describe("Question relay", () => { expect(result.outcome.outcome).toBe("selected"); }); + + it("keeps auto-approving permissions after SSE send failures", async () => { + const appendRawLine = vi.fn(); + const brokenSseController = { + send: vi.fn(() => { + throw new Error("stream closed"); + }), + close: vi.fn(), + }; + + const cloudPermissionServer = server as TestableAgentServer & { + emitConsoleLog: ( + level: "debug" | "info" | "warn" | "error", + scope: string, + message: string, + data?: unknown, + ) => void; + logger: { debug: (message: string, data?: unknown) => void }; + session: { + payload: typeof TEST_PAYLOAD; + sseController: typeof brokenSseController | null; + logWriter: { + appendRawLine: (runId: string, line: string) => void; + }; + }; + }; + + cloudPermissionServer.session = { + payload: TEST_PAYLOAD, + sseController: brokenSseController, + logWriter: { + appendRawLine, + }, + }; + cloudPermissionServer.logger = { + debug: (message: string, data?: unknown) => { + cloudPermissionServer.emitConsoleLog( + "debug", + "agent", + message, + data, + ); + }, + }; + + const client = cloudPermissionServer.createCloudClient(TEST_PAYLOAD); + + const firstResult = await client.requestPermission({ + options: ALLOW_OPTIONS, + toolCall: { _meta: { codeToolKind: "bash" } }, + }); + + expect(firstResult.outcome.outcome).toBe("selected"); + expect(brokenSseController.send).toHaveBeenCalledTimes(1); + expect(cloudPermissionServer.session.sseController).toBeNull(); + + const secondResult = await client.requestPermission({ + options: ALLOW_OPTIONS, + toolCall: { _meta: { codeToolKind: "bash" } }, + }); + + expect(secondResult.outcome.outcome).toBe("selected"); + expect(brokenSseController.send).toHaveBeenCalledTimes(1); + expect(appendRawLine).toHaveBeenCalledTimes(2); + }); }); });