Skip to content

Commit 233dfe8

Browse files
committed
fix(mcp): log diagnostic instead of swallowing gateway event rejections
1 parent 14bf179 commit 233dfe8

2 files changed

Lines changed: 66 additions & 1 deletion

File tree

src/mcp/channel-bridge.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ type BridgeInternals = {
3434
event: string;
3535
payload?: Record<string, unknown>;
3636
}) => Promise<void>;
37+
dispatchGatewayEvent: (event: {
38+
event: string;
39+
payload?: Record<string, unknown>;
40+
}) => Promise<void>;
3741
handleSessionMessageEvent: (payload: {
3842
sessionKey: string;
3943
senderIsOwner?: boolean;
@@ -304,6 +308,52 @@ describe("OpenClawChannelBridge — pendingClaudePermissions / pendingApprovals
304308
}
305309
});
306310

311+
test("a rejected gateway event still emits exactly one diagnostic record with verbose off", async () => {
312+
const bridge = makeBridge(false);
313+
const writes: string[] = [];
314+
const writeSpy = vi
315+
.spyOn(process.stderr, "write")
316+
.mockImplementation((chunk: string | Uint8Array): boolean => {
317+
writes.push(String(chunk));
318+
return true;
319+
});
320+
vi.spyOn(bridge, "handleGatewayEvent").mockRejectedValue(new Error("handler boom"));
321+
try {
322+
await bridge.dispatchGatewayEvent({ event: "exec.approval.requested", payload: {} });
323+
324+
expect(writes).toHaveLength(1);
325+
expect(writes[0]).toBe("openclaw mcp: gateway event exec.approval.requested failed\n");
326+
expect(writes[0]).not.toContain("handler boom");
327+
} finally {
328+
writeSpy.mockRestore();
329+
await bridge.close();
330+
}
331+
});
332+
333+
test("a rejected gateway event includes error detail with verbose on", async () => {
334+
const bridge = makeBridge(true);
335+
const writes: string[] = [];
336+
const writeSpy = vi
337+
.spyOn(process.stderr, "write")
338+
.mockImplementation((chunk: string | Uint8Array): boolean => {
339+
writes.push(String(chunk));
340+
return true;
341+
});
342+
vi.spyOn(bridge, "handleGatewayEvent").mockRejectedValue(new Error("handler boom"));
343+
try {
344+
await bridge.dispatchGatewayEvent({ event: "exec.approval.requested", payload: {} });
345+
346+
expect(writes).toHaveLength(2);
347+
expect(writes[0]).toBe("openclaw mcp: gateway event exec.approval.requested failed\n");
348+
expect(writes[1]).toBe(
349+
"openclaw mcp: gateway event exec.approval.requested error: Error: handler boom\n",
350+
);
351+
} finally {
352+
writeSpy.mockRestore();
353+
await bridge.close();
354+
}
355+
});
356+
307357
test("sweeper interval is not started before any pending entry is added", async () => {
308358
const bridge = makeBridge();
309359
try {

src/mcp/channel-bridge.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export class OpenClawChannelBridge {
148148
scopes: [READ_SCOPE, WRITE_SCOPE, APPROVALS_SCOPE],
149149
requestTimeoutMs: 180_000,
150150
onEvent: (event) => {
151-
void this.handleGatewayEvent(event).catch(() => {});
151+
void this.dispatchGatewayEvent(event);
152152
},
153153
onHelloOk: () => {
154154
this.retryingInitialConnect = false;
@@ -521,6 +521,21 @@ export class OpenClawChannelBridge {
521521
}
522522
}
523523

524+
private async dispatchGatewayEvent(event: EventFrame): Promise<void> {
525+
try {
526+
await this.handleGatewayEvent(event);
527+
} catch (error) {
528+
// Always surface a single low-noise record so swallowed gateway event
529+
// failures remain observable; the spammy error detail stays behind --verbose.
530+
process.stderr.write(`openclaw mcp: gateway event ${event.event} failed\n`);
531+
if (this.verbose) {
532+
process.stderr.write(
533+
`openclaw mcp: gateway event ${event.event} error: ${String(error)}\n`,
534+
);
535+
}
536+
}
537+
}
538+
524539
private async handleGatewayEvent(event: EventFrame): Promise<void> {
525540
switch (event.event) {
526541
case "session.message":

0 commit comments

Comments
 (0)