diff --git a/packages/loopover-miner/lib/chat-action-dispatch.js b/packages/loopover-miner/lib/chat-action-dispatch.js index 11b44bf251..346cf991c6 100644 --- a/packages/loopover-miner/lib/chat-action-dispatch.js +++ b/packages/loopover-miner/lib/chat-action-dispatch.js @@ -71,7 +71,17 @@ export async function dispatchChatAction(request, options = {}) { return { ok: false, status: "invalid_params", action }; } - const result = await registered.handler(request); + let result; + try { + result = await registered.handler(request); + } catch { + // A handler that throws fails closed with the module's typed result shape (#6989), consistent with the + // paramsValidator catch above. The thrown value is deliberately NOT echoed back: a handler wraps + // arbitrary action work (e.g. a network call), so its error could carry external detail -- the sibling + // fail-closed paths (sentry.js, pretooluse-hook.js) likewise swallow rather than surface it. A distinct + // "handler_error" status still lets a caller tell an execution failure from a params-validation failure. + return { ok: false, status: "handler_error", action }; + } return { ok: true, status: "dispatched", action, result }; } diff --git a/test/unit/miner-chat-action-dispatch.test.ts b/test/unit/miner-chat-action-dispatch.test.ts index f8df273c1b..a493ce63a7 100644 --- a/test/unit/miner-chat-action-dispatch.test.ts +++ b/test/unit/miner-chat-action-dispatch.test.ts @@ -147,4 +147,22 @@ describe("dispatchChatAction (#6519)", () => { const result = await dispatchChatAction({ action: "demo", params: {} }, { env: enabledEnv, registry }); expect(result).toEqual({ ok: false, status: "invalid_params", action: "demo", error: "boom" }); }); + + it("fails closed with handler_error when the handler throws, not an unhandled rejection (#6989)", async () => { + const registry = registryWith("demo", () => true, () => { + throw new Error("network down"); + }); + const result = await dispatchChatAction({ action: "demo", params: {} }, { env: enabledEnv, registry }); + // Typed result, distinct from invalid_params so a caller can tell an execution failure from a validation + // failure. The thrown value is deliberately not surfaced (it may carry external detail). + expect(result).toEqual({ ok: false, status: "handler_error", action: "demo" }); + }); + + it("fails closed the same way regardless of what the handler throws (#6989)", async () => { + const registry = registryWith("demo", () => true, () => { + throw "kaboom"; + }); + const result = await dispatchChatAction({ action: "demo", params: {} }, { env: enabledEnv, registry }); + expect(result).toEqual({ ok: false, status: "handler_error", action: "demo" }); + }); });