From 8d887765c1d3583ca3f5e42f0d0bb268ec5ede55 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Sun, 21 Jun 2026 18:03:48 +0000 Subject: [PATCH] fix(agent-ui): keep app alive on GPU-process crash in GPU-less envs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A GPU-process crash routed through the child-process-gone safety net was treated as fatal, killing the whole Electron app in GPU-less environments (Windows Sandbox, headless VMs). GPU crashes are recoverable — Chromium relaunches the GPU process and falls back to software rendering — so log them and let Chromium recover instead of exiting. Other child-process crashes stay fatal. Closes #1800 --- src/gaia/apps/webui/main-safety-net.cjs | 11 +++++- tests/electron/test_main_error_handling.js | 39 ++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/gaia/apps/webui/main-safety-net.cjs b/src/gaia/apps/webui/main-safety-net.cjs index 911c0b91d..b98785980 100644 --- a/src/gaia/apps/webui/main-safety-net.cjs +++ b/src/gaia/apps/webui/main-safety-net.cjs @@ -134,11 +134,20 @@ function installSafetyNet({ logPath, dialogModule, appModule, homedirFn }) { appModule.on("child-process-gone", (_event, details) => { const reason = details && details.reason; + const type = details && details.type; // Ignore expected terminations during shutdown so the crash dialog // doesn't flash on a clean quit. if (reason === "clean-exit" || reason === "killed") return; + // A GPU-process crash is recoverable: Chromium relaunches the GPU process + // and, after repeated failures, falls back to software rendering. Treating + // it as fatal killed the whole app in GPU-less environments (Windows + // Sandbox, headless VMs). Log and let Chromium recover instead. + if (type === "GPU") { + appendLog(logPath, `[${new Date().toISOString()}] GPU_CRASH reason=${reason} (non-fatal; Chromium will fall back to software rendering)`); + return; + } fatal(new Error( - `child-process-gone: type=${details && details.type} reason=${reason}` + `child-process-gone: type=${type} reason=${reason}` )); }); diff --git a/tests/electron/test_main_error_handling.js b/tests/electron/test_main_error_handling.js index e458b0c24..c47d4eb69 100644 --- a/tests/electron/test_main_error_handling.js +++ b/tests/electron/test_main_error_handling.js @@ -289,6 +289,45 @@ describe("installSafetyNet", () => { exitSpy.mockRestore(); }); + // ── Test 9b: GPU child-process crash is non-fatal (issue #1800) ──────────── + // GPU crashes are recoverable — Chromium falls back to software rendering. + // The app must NOT exit, so GPU-less envs (Windows Sandbox/VM) keep running. + + test("GPU child-process-gone does not call fatal/exit", () => { + const { installSafetyNet } = require(SAFETY_NET_PATH); + const dialog = mockDialog(); + const app = mockApp(false); + const exitSpy = jest.spyOn(process, "exit").mockImplementation(() => {}); + + installSafetyNet({ logPath, dialogModule: dialog, appModule: app }); + app.emit("child-process-gone", {}, { type: "GPU", reason: "crashed" }); + + expect(exitSpy).not.toHaveBeenCalled(); + expect(dialog.showErrorBox).not.toHaveBeenCalled(); + expect(dialog.showMessageBoxSync).not.toHaveBeenCalled(); + // The crash is still recorded for forensics. + expect(fs.readFileSync(logPath, "utf8")).toContain("GPU_CRASH"); + + exitSpy.mockRestore(); + }); + + // ── Test 9c: non-GPU child-process crash stays fatal ────────────────────── + // A utility/plugin child dying unexpectedly is still treated as fatal. + + test("non-GPU child-process-gone calls fatal/exit", () => { + const { installSafetyNet } = require(SAFETY_NET_PATH); + const dialog = mockDialog(); + const app = mockApp(false); + const exitSpy = jest.spyOn(process, "exit").mockImplementation(() => {}); + + installSafetyNet({ logPath, dialogModule: dialog, appModule: app }); + app.emit("child-process-gone", {}, { type: "Utility", reason: "crashed" }); + + expect(exitSpy).toHaveBeenCalled(); + + exitSpy.mockRestore(); + }); + // ── Test 10: fatal handler writes to log before showing dialog ───────────── // If dialog.showErrorBox itself crashes, the log must already have the entry.