From 1103fcd2fd7efe42d5f440592313daffe3150901 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Thu, 7 May 2026 17:29:53 +0200 Subject: [PATCH] fix: catch errors in controller message handler --- .../src/__tests__/controller-session.test.ts | 53 +++++++++++++++++++ apps/agent/src/controller-session.ts | 11 +++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/apps/agent/src/__tests__/controller-session.test.ts b/apps/agent/src/__tests__/controller-session.test.ts index c6dea013..3745c1c7 100644 --- a/apps/agent/src/__tests__/controller-session.test.ts +++ b/apps/agent/src/__tests__/controller-session.test.ts @@ -95,3 +95,56 @@ test("closes the websocket when an outbound send throws", async () => { session.close(); } }); + +test("continues processing inbound messages after a volume command fails", async () => { + const outboundMessages: string[] = []; + const session = createControllerSession( + fromPartial({ + send: (message: string) => { + outboundMessages.push(message); + }, + }), + ); + + try { + session.onMessage( + createControllerMessage("volume.command", { + commandId: "command-1", + command: { + name: "filesystem.browse", + path: "/path/that/does/not/exist", + }, + }), + ); + session.onMessage(createControllerMessage("heartbeat.ping", { sentAt: 123 })); + + await waitForExpect(() => { + const parsedMessages = outboundMessages.map((message) => parseAgentMessage(message)); + const volumeResult = parsedMessages.find( + (message) => message?.success && message.data.type === "volume.commandResult", + ); + const heartbeatPong = parsedMessages.find( + (message) => message?.success && message.data.type === "heartbeat.pong", + ); + + expect(volumeResult?.success).toBe(true); + if (!volumeResult || !volumeResult.success || volumeResult.data.type !== "volume.commandResult") { + return; + } + + expect(volumeResult.data.payload).toEqual({ + commandId: "command-1", + status: "error", + error: "ENOENT: no such file or directory, scandir '/path/that/does/not/exist'", + }); + expect(heartbeatPong?.success).toBe(true); + if (!heartbeatPong || !heartbeatPong.success || heartbeatPong.data.type !== "heartbeat.pong") { + return; + } + + expect(heartbeatPong.data.payload).toEqual({ sentAt: 123 }); + }); + } finally { + session.close(); + } +}); diff --git a/apps/agent/src/controller-session.ts b/apps/agent/src/controller-session.ts index d8f82e24..07331166 100644 --- a/apps/agent/src/controller-session.ts +++ b/apps/agent/src/controller-session.ts @@ -120,7 +120,16 @@ export const createControllerSession = (ws: WebSocket): ControllerSession => { return; } - yield* handleControllerCommand(commandContext, parsed.data); + const commandEffect: Effect.Effect = handleControllerCommand( + commandContext, + parsed.data, + ); + + yield* commandEffect.pipe( + Effect.catchAll((error) => + Effect.sync(() => logger.error(`Failed to handle controller message: ${toMessage(error)}`)), + ), + ); }), ), );