Skip to content

Commit 26a2069

Browse files
committed
fix(infra): register no-op error handler on detached gateway spawn child
1 parent 36a91ac commit 26a2069

2 files changed

Lines changed: 28 additions & 4 deletions

File tree

src/infra/process-respawn.test.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ describe("respawnGatewayProcessForUpdate", () => {
302302
"gateway",
303303
"run",
304304
];
305-
spawnMock.mockReturnValue({ pid: 5151, unref: vi.fn(), kill: vi.fn() });
305+
spawnMock.mockReturnValue({ pid: 5151, unref: vi.fn(), kill: vi.fn(), on: vi.fn() });
306306

307307
const result = respawnGatewayProcessForUpdate();
308308

@@ -329,7 +329,7 @@ describe("respawnGatewayProcessForUpdate", () => {
329329
"gateway",
330330
"run",
331331
];
332-
spawnMock.mockReturnValue({ pid: 7171, unref: vi.fn(), kill: vi.fn() });
332+
spawnMock.mockReturnValue({ pid: 7171, unref: vi.fn(), kill: vi.fn(), on: vi.fn() });
333333

334334
const result = respawnGatewayProcessForUpdate();
335335

@@ -352,7 +352,7 @@ describe("respawnGatewayProcessForUpdate", () => {
352352
const entry =
353353
"/app/node_modules/.pnpm/@[email protected]/node_modules/@anthropic/sdk/dist/index.js";
354354
process.argv = ["/usr/local/bin/node", entry, "gateway", "run"];
355-
spawnMock.mockReturnValue({ pid: 8181, unref: vi.fn(), kill: vi.fn() });
355+
spawnMock.mockReturnValue({ pid: 8181, unref: vi.fn(), kill: vi.fn(), on: vi.fn() });
356356

357357
respawnGatewayProcessForUpdate();
358358

@@ -369,7 +369,7 @@ describe("respawnGatewayProcessForUpdate", () => {
369369
process.env.XPC_SERVICE_NAME = "ai.openclaw.mac";
370370
process.execArgv = [];
371371
process.argv = ["/usr/local/bin/node", "/repo/dist/index.js", "gateway", "run"];
372-
spawnMock.mockReturnValue({ pid: 6161, unref: vi.fn(), kill: vi.fn() });
372+
spawnMock.mockReturnValue({ pid: 6161, unref: vi.fn(), kill: vi.fn(), on: vi.fn() });
373373

374374
const result = respawnGatewayProcessForUpdate();
375375

@@ -413,4 +413,27 @@ describe("respawnGatewayProcessForUpdate", () => {
413413
expect(result.mode).toBe("failed");
414414
expect(result.detail).toContain("spawn failed");
415415
});
416+
417+
it("registers a no-op error listener on the detached child to prevent unhandled async errors", () => {
418+
delete process.env.OPENCLAW_NO_RESPAWN;
419+
clearSupervisorHints();
420+
setPlatform("linux");
421+
422+
const onMock = vi.fn();
423+
spawnMock.mockReturnValue({
424+
pid: 9091,
425+
unref: vi.fn(),
426+
kill: vi.fn(),
427+
on: onMock,
428+
});
429+
430+
const result = respawnGatewayProcessForUpdate();
431+
432+
expect(result.mode).toBe("spawned");
433+
expect(onMock).toHaveBeenCalledWith("error", expect.any(Function));
434+
435+
const errorHandler = onMock.mock.calls[0]?.[1] as (...args: unknown[]) => void;
436+
expect(() => errorHandler(new Error("ENOMEM"))).not.toThrow();
437+
expect(() => errorHandler(new Error("spawn EBADF"))).not.toThrow();
438+
});
416439
});

src/infra/process-respawn.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ function spawnDetachedGatewayProcess(opts: GatewayRespawnOptions = {}): {
5353
detached: true,
5454
stdio: "inherit",
5555
});
56+
child.on("error", () => {});
5657
child.unref();
5758
return { child, pid: child.pid ?? undefined };
5859
}

0 commit comments

Comments
 (0)