From d6b011dfa9c9ea83550c6325cb3c565805cf3aad Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Wed, 13 May 2026 16:00:42 +0000 Subject: [PATCH 1/3] fix(onboard): clearer diagnostics when reusable gateway lacks GPU The "Existing gateway was started without GPU passthrough" suggestion contained the literal `` placeholder and assumed a sandbox was registered; when none was, the suggested `nemoclaw destroy --yes` had nothing to act on and looped the user through `nemoclaw uninstall` without saying so. Also fix the uninstall log line: when an `openshell` cleanup step returned non-zero (typically because the resource was already absent), the warning read `Destroyed gateway 'nemoclaw' skipped`, which contradicts itself. Use a state-form wording so the message reads truthfully. Refs #3456 Signed-off-by: Tinson Lai --- src/lib/actions/uninstall/run-plan.test.ts | 33 ++++++++++++++++ src/lib/actions/uninstall/run-plan.ts | 8 +++- src/lib/onboard.test.ts | 44 ++++++++++++++++++++++ src/lib/onboard.ts | 15 ++++++-- src/lib/onboard/gpu-recovery.ts | 20 ++++++++++ 5 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 src/lib/onboard.test.ts create mode 100644 src/lib/onboard/gpu-recovery.ts diff --git a/src/lib/actions/uninstall/run-plan.test.ts b/src/lib/actions/uninstall/run-plan.test.ts index 32ae0ae9611..87ec3565d7d 100644 --- a/src/lib/actions/uninstall/run-plan.test.ts +++ b/src/lib/actions/uninstall/run-plan.test.ts @@ -533,4 +533,37 @@ describe("uninstall run plan", () => { expect(warnings).toContain("Failed to disable /swapfile; skipping swap cleanup."); expect(logs).not.toContain("Swap file removed"); }); + + it("reports skipped openshell cleanup without the contradictory past-tense wording", () => { + const logs: string[] = []; + const warnings: string[] = []; + const result = runUninstallPlan( + { assumeYes: true, deleteModels: false, keepOpenShell: true }, + { + commandExists: () => true, + env: { HOME: "/tmp/nemoclaw-uninstall-test-skip-wording" } as NodeJS.ProcessEnv, + error: (line) => warnings.push(line), + existsSync: () => false, + isTty: false, + kill: () => true, + log: (line) => logs.push(line), + rmSync: vi.fn(), + run: (command, args) => { + if (command === "openshell") { + return { status: 1, stdout: "", stderr: "" }; + } + if (args[0] === "-c") return ok("/fake/bin/tool\n"); + if (args[0] === "-f") return ok(""); + return ok(); + }, + runDocker: () => ok(""), + }, + ); + + expect(result.exitCode).toBe(0); + expect(warnings).toContain("Skipped gateway 'nemoclaw' (already absent or unavailable)"); + expect(warnings).toContain("Skipped all OpenShell sandboxes (already absent or unavailable)"); + expect(warnings.every((line) => !/^Destroyed .+ skipped$/.test(line))).toBe(true); + expect(warnings.every((line) => !/^Deleted .+ skipped$/.test(line))).toBe(true); + }); }); diff --git a/src/lib/actions/uninstall/run-plan.ts b/src/lib/actions/uninstall/run-plan.ts index f8fd7fcccb0..6cc916e4f41 100644 --- a/src/lib/actions/uninstall/run-plan.ts +++ b/src/lib/actions/uninstall/run-plan.ts @@ -209,8 +209,12 @@ function confirm(options: UninstallRunOptions, runtime: UninstallRuntime): boole function runOptional(runtime: UninstallRuntime, description: string, command: string, args: string[]): void { const result = runtime.run(command, args, { env: runtime.env, stdio: "ignore" }); - if (result.status === 0) runtime.log(description); - else runtime.warn(`${description} skipped`); + if (result.status === 0) { + runtime.log(description); + return; + } + const target = description.replace(/^(Destroyed|Deleted|Stopped|Removed)\s+/i, ""); + runtime.warn(`Skipped ${target} (already absent or unavailable)`); } function stopHelperServices(paths: UninstallPaths, runtime: UninstallRuntime): void { diff --git a/src/lib/onboard.test.ts b/src/lib/onboard.test.ts new file mode 100644 index 00000000000..1a866451b68 --- /dev/null +++ b/src/lib/onboard.test.ts @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; + +import { gpuPassthroughRecoveryLines } from "./onboard/gpu-recovery"; + +describe("gpuPassthroughRecoveryLines", () => { + it("suggests uninstall when no sandboxes are registered (no actionable destroy target)", () => { + const lines = gpuPassthroughRecoveryLines([]); + expect(lines).toEqual([ + " Existing gateway was started without GPU passthrough.", + " No sandboxes are registered, so there is nothing for `nemoclaw destroy` to act on.", + " To enable GPU, clear the stale gateway state and re-onboard:", + " nemoclaw uninstall && nemoclaw onboard --gpu", + ]); + }); + + it("names the registered sandbox in the destroy command (singular form)", () => { + const lines = gpuPassthroughRecoveryLines(["my-assistant"]); + expect(lines).toEqual([ + " Existing gateway was started without GPU passthrough.", + " To enable GPU, destroy the registered sandbox (`my-assistant`) and re-onboard:", + " nemoclaw my-assistant destroy --yes", + " nemoclaw onboard --gpu", + ]); + }); + + it("lists every registered sandbox (plural form)", () => { + const lines = gpuPassthroughRecoveryLines(["alpha", "beta"]); + expect(lines).toEqual([ + " Existing gateway was started without GPU passthrough.", + " To enable GPU, destroy the registered sandboxes (`alpha`, `beta`) and re-onboard:", + " nemoclaw alpha destroy --yes", + " nemoclaw beta destroy --yes", + " nemoclaw onboard --gpu", + ]); + }); + + it("never emits the literal `` placeholder in any suggestion", () => { + expect(gpuPassthroughRecoveryLines([]).every((l) => !l.includes("nemoclaw destroy --yes"))).toBe(true); + expect(gpuPassthroughRecoveryLines(["x"]).every((l) => !l.includes("nemoclaw destroy --yes"))).toBe(true); + }); +}); diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 6629eb7d7fe..7fd994be038 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -353,6 +353,8 @@ const RESET = USE_COLOR ? "\x1b[0m" : ""; let OPENSHELL_BIN: string | null = null; const GATEWAY_NAME = "nemoclaw"; const BACK_TO_SELECTION = "__NEMOCLAW_BACK_TO_SELECTION__"; + +const { gpuPassthroughRecoveryLines }: typeof import("./onboard/gpu-recovery") = require("./onboard/gpu-recovery"); type HermesAuthMethod = "oauth" | "api_key"; const HERMES_AUTH_METHOD_OAUTH: HermesAuthMethod = "oauth"; const HERMES_AUTH_METHOD_API_KEY: HermesAuthMethod = "api_key"; @@ -10412,9 +10414,16 @@ async function onboard(opts: OnboardOptions = {}): Promise { const gpuOutput = String(gpuCheck.stdout || "").trim(); const gatewayHasGpu = gpuCheck.status === 0 && gpuOutput !== "null" && gpuOutput !== "[]"; if (!gatewayHasGpu) { - console.error(" Existing gateway was started without GPU passthrough."); - console.error(" To enable GPU, destroy the existing sandbox and gateway, then re-onboard:"); - console.error(` nemoclaw destroy --yes && nemoclaw onboard --gpu`); + const registeredNames = (() => { + try { + return registry.listSandboxes().sandboxes.map((s) => s.name).filter(Boolean); + } catch { + return []; + } + })(); + for (const line of gpuPassthroughRecoveryLines(registeredNames)) { + console.error(line); + } process.exit(1); } } diff --git a/src/lib/onboard/gpu-recovery.ts b/src/lib/onboard/gpu-recovery.ts new file mode 100644 index 00000000000..dfdcc098def --- /dev/null +++ b/src/lib/onboard/gpu-recovery.ts @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +export function gpuPassthroughRecoveryLines(registeredNames: readonly string[]): string[] { + const lines: string[] = [" Existing gateway was started without GPU passthrough."]; + if (registeredNames.length === 0) { + lines.push( + " No sandboxes are registered, so there is nothing for `nemoclaw destroy` to act on.", + ); + lines.push(" To enable GPU, clear the stale gateway state and re-onboard:"); + lines.push(" nemoclaw uninstall && nemoclaw onboard --gpu"); + return lines; + } + const plural = registeredNames.length === 1 ? "" : "es"; + const list = registeredNames.map((n) => `\`${n}\``).join(", "); + lines.push(` To enable GPU, destroy the registered sandbox${plural} (${list}) and re-onboard:`); + for (const name of registeredNames) lines.push(` nemoclaw ${name} destroy --yes`); + lines.push(" nemoclaw onboard --gpu"); + return lines; +} From 6cc0b14dab33e2674d5020e5a13f91ead10b3451 Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Wed, 13 May 2026 16:25:01 +0000 Subject: [PATCH 2/3] fix(onboard): drop from no-sandbox guidance and keep onboard.ts neutral CodeRabbit and Codex feedback on #3464: - gpu-recovery still emitted "nemoclaw destroy" in the no-sandbox branch. The whole point of the bug is that `` is not actionable when no sandbox is registered; rewrite the line so the placeholder is gone entirely and broaden the test to forbid any "" occurrence in the joined output. - Hoist the registry lookup + print loop into reportGpuPassthroughRecovery inside gpu-recovery.ts so the entrypoint diff in src/lib/onboard.ts is net-neutral against the budget check. Inject the lookup as a callback for testability and add a fallback case that hides registry-throw paths. Signed-off-by: Tinson Lai --- src/lib/onboard.test.ts | 28 ++++++++++++++++++++++++---- src/lib/onboard.ts | 13 ++----------- src/lib/onboard/gpu-recovery.ts | 26 +++++++++++++++++++++++--- 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/lib/onboard.test.ts b/src/lib/onboard.test.ts index 1a866451b68..cd8b5668ab1 100644 --- a/src/lib/onboard.test.ts +++ b/src/lib/onboard.test.ts @@ -3,14 +3,14 @@ import { describe, expect, it } from "vitest"; -import { gpuPassthroughRecoveryLines } from "./onboard/gpu-recovery"; +import { gpuPassthroughRecoveryLines, reportGpuPassthroughRecovery } from "./onboard/gpu-recovery"; describe("gpuPassthroughRecoveryLines", () => { it("suggests uninstall when no sandboxes are registered (no actionable destroy target)", () => { const lines = gpuPassthroughRecoveryLines([]); expect(lines).toEqual([ " Existing gateway was started without GPU passthrough.", - " No sandboxes are registered, so there is nothing for `nemoclaw destroy` to act on.", + " No sandboxes are registered, so there is nothing to destroy.", " To enable GPU, clear the stale gateway state and re-onboard:", " nemoclaw uninstall && nemoclaw onboard --gpu", ]); @@ -38,7 +38,27 @@ describe("gpuPassthroughRecoveryLines", () => { }); it("never emits the literal `` placeholder in any suggestion", () => { - expect(gpuPassthroughRecoveryLines([]).every((l) => !l.includes("nemoclaw destroy --yes"))).toBe(true); - expect(gpuPassthroughRecoveryLines(["x"]).every((l) => !l.includes("nemoclaw destroy --yes"))).toBe(true); + expect(gpuPassthroughRecoveryLines([]).join("\n")).not.toContain(""); + expect(gpuPassthroughRecoveryLines(["x"]).join("\n")).not.toContain(""); + expect(gpuPassthroughRecoveryLines(["alpha", "beta"]).join("\n")).not.toContain(""); + }); +}); + +describe("reportGpuPassthroughRecovery", () => { + it("routes the registered sandbox names through the printer", () => { + const printed: string[] = []; + reportGpuPassthroughRecovery((line) => printed.push(line), () => ["alpha"]); + expect(printed).toEqual(gpuPassthroughRecoveryLines(["alpha"])); + }); + + it("falls back to the no-sandbox guidance when the registry lookup throws", () => { + const printed: string[] = []; + reportGpuPassthroughRecovery( + (line) => printed.push(line), + () => { + throw new Error("registry unreachable"); + }, + ); + expect(printed).toEqual(gpuPassthroughRecoveryLines([])); }); }); diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 7fd994be038..fb6330976c8 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -354,7 +354,7 @@ let OPENSHELL_BIN: string | null = null; const GATEWAY_NAME = "nemoclaw"; const BACK_TO_SELECTION = "__NEMOCLAW_BACK_TO_SELECTION__"; -const { gpuPassthroughRecoveryLines }: typeof import("./onboard/gpu-recovery") = require("./onboard/gpu-recovery"); +const { reportGpuPassthroughRecovery }: typeof import("./onboard/gpu-recovery") = require("./onboard/gpu-recovery"); type HermesAuthMethod = "oauth" | "api_key"; const HERMES_AUTH_METHOD_OAUTH: HermesAuthMethod = "oauth"; const HERMES_AUTH_METHOD_API_KEY: HermesAuthMethod = "api_key"; @@ -10414,16 +10414,7 @@ async function onboard(opts: OnboardOptions = {}): Promise { const gpuOutput = String(gpuCheck.stdout || "").trim(); const gatewayHasGpu = gpuCheck.status === 0 && gpuOutput !== "null" && gpuOutput !== "[]"; if (!gatewayHasGpu) { - const registeredNames = (() => { - try { - return registry.listSandboxes().sandboxes.map((s) => s.name).filter(Boolean); - } catch { - return []; - } - })(); - for (const line of gpuPassthroughRecoveryLines(registeredNames)) { - console.error(line); - } + reportGpuPassthroughRecovery(); process.exit(1); } } diff --git a/src/lib/onboard/gpu-recovery.ts b/src/lib/onboard/gpu-recovery.ts index dfdcc098def..ef4c9b80385 100644 --- a/src/lib/onboard/gpu-recovery.ts +++ b/src/lib/onboard/gpu-recovery.ts @@ -4,9 +4,7 @@ export function gpuPassthroughRecoveryLines(registeredNames: readonly string[]): string[] { const lines: string[] = [" Existing gateway was started without GPU passthrough."]; if (registeredNames.length === 0) { - lines.push( - " No sandboxes are registered, so there is nothing for `nemoclaw destroy` to act on.", - ); + lines.push(" No sandboxes are registered, so there is nothing to destroy."); lines.push(" To enable GPU, clear the stale gateway state and re-onboard:"); lines.push(" nemoclaw uninstall && nemoclaw onboard --gpu"); return lines; @@ -18,3 +16,25 @@ export function gpuPassthroughRecoveryLines(registeredNames: readonly string[]): lines.push(" nemoclaw onboard --gpu"); return lines; } + +function defaultRegisteredSandboxNames(): readonly string[] { + try { + const registry = require("../state/registry") as typeof import("../state/registry"); + return registry.listSandboxes().sandboxes.map((s) => s.name).filter(Boolean); + } catch { + return []; + } +} + +export function reportGpuPassthroughRecovery( + emit: (line: string) => void = console.error, + listRegisteredSandboxes: () => readonly string[] = defaultRegisteredSandboxNames, +): void { + let names: readonly string[] = []; + try { + names = listRegisteredSandboxes(); + } catch { + names = []; + } + for (const line of gpuPassthroughRecoveryLines(names)) emit(line); +} From 3e48de4b2f8bcb583809689f9c5b9802f9829ea2 Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Wed, 13 May 2026 17:05:38 +0000 Subject: [PATCH 3/3] fix(onboard): point GPU-passthrough recovery at the actual loop fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review on #3464 caught three issues: 1) `nemoclaw destroy --yes` explicitly preserves the shared gateway (destroy.ts L207). The previous recovery snippet therefore destroyed the sandbox and then re-onboarded against the same gateway, which still lacks GPU passthrough — same wall, one round later. Append --cleanup-gateway to the last destroy command so the gateway actually goes away. 2) `nemoclaw uninstall` removes the global CLI via `npm uninstall -g nemoclaw` (run-plan.ts L451), so `nemoclaw uninstall && nemoclaw onboard --gpu` is not even executable — the second half runs against a CLI that no longer exists. Use the bare `openshell gateway destroy -g nemoclaw` command (the same one destroy.ts already prints as gatewayRemovalHint) so the CLI survives. 3) The registry catch collapsed read errors to `[]`, which then printed "No sandboxes are registered" — a false diagnosis. Switch the return type to `readonly string[] | null` and add a dedicated branch with a "Could not read the NemoClaw sandbox registry" message and a direct gateway-removal recipe. Signed-off-by: Tinson Lai --- src/lib/onboard.test.ts | 41 ++++++++++++++++++++++----------- src/lib/onboard/gpu-recovery.ts | 35 ++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 23 deletions(-) diff --git a/src/lib/onboard.test.ts b/src/lib/onboard.test.ts index cd8b5668ab1..42caf71b241 100644 --- a/src/lib/onboard.test.ts +++ b/src/lib/onboard.test.ts @@ -6,41 +6,55 @@ import { describe, expect, it } from "vitest"; import { gpuPassthroughRecoveryLines, reportGpuPassthroughRecovery } from "./onboard/gpu-recovery"; describe("gpuPassthroughRecoveryLines", () => { - it("suggests uninstall when no sandboxes are registered (no actionable destroy target)", () => { + it("clears the gateway directly when no sandboxes are registered (no NemoClaw uninstall, so the CLI survives)", () => { const lines = gpuPassthroughRecoveryLines([]); expect(lines).toEqual([ " Existing gateway was started without GPU passthrough.", " No sandboxes are registered, so there is nothing to destroy.", - " To enable GPU, clear the stale gateway state and re-onboard:", - " nemoclaw uninstall && nemoclaw onboard --gpu", + " To enable GPU, clear the stale gateway and re-onboard:", + " openshell gateway destroy -g nemoclaw", + " nemoclaw onboard --gpu", + ]); + }); + + it("falls back to a direct gateway-removal hint when the registry cannot be read", () => { + const lines = gpuPassthroughRecoveryLines(null); + expect(lines).toEqual([ + " Existing gateway was started without GPU passthrough.", + " Could not read the NemoClaw sandbox registry; cannot enumerate sandboxes.", + " To enable GPU, clear the stale gateway directly and re-onboard:", + " openshell gateway destroy -g nemoclaw", + " nemoclaw onboard --gpu", ]); }); - it("names the registered sandbox in the destroy command (singular form)", () => { + it("appends --cleanup-gateway to the single destroy command so the stale gateway is actually removed", () => { const lines = gpuPassthroughRecoveryLines(["my-assistant"]); expect(lines).toEqual([ " Existing gateway was started without GPU passthrough.", " To enable GPU, destroy the registered sandbox (`my-assistant`) and re-onboard:", - " nemoclaw my-assistant destroy --yes", + " nemoclaw my-assistant destroy --yes --cleanup-gateway", " nemoclaw onboard --gpu", ]); }); - it("lists every registered sandbox (plural form)", () => { + it("only puts --cleanup-gateway on the last destroy command when more than one sandbox is registered", () => { const lines = gpuPassthroughRecoveryLines(["alpha", "beta"]); expect(lines).toEqual([ " Existing gateway was started without GPU passthrough.", " To enable GPU, destroy the registered sandboxes (`alpha`, `beta`) and re-onboard:", " nemoclaw alpha destroy --yes", - " nemoclaw beta destroy --yes", + " nemoclaw beta destroy --yes --cleanup-gateway", " nemoclaw onboard --gpu", ]); }); - it("never emits the literal `` placeholder in any suggestion", () => { - expect(gpuPassthroughRecoveryLines([]).join("\n")).not.toContain(""); - expect(gpuPassthroughRecoveryLines(["x"]).join("\n")).not.toContain(""); - expect(gpuPassthroughRecoveryLines(["alpha", "beta"]).join("\n")).not.toContain(""); + it("never emits the literal `` placeholder or a `nemoclaw uninstall && nemoclaw onboard` chain in any branch", () => { + for (const names of [null, [], ["x"], ["alpha", "beta"]] as const) { + const joined = gpuPassthroughRecoveryLines(names).join("\n"); + expect(joined).not.toContain(""); + expect(joined).not.toContain("nemoclaw uninstall && nemoclaw onboard"); + } }); }); @@ -51,7 +65,7 @@ describe("reportGpuPassthroughRecovery", () => { expect(printed).toEqual(gpuPassthroughRecoveryLines(["alpha"])); }); - it("falls back to the no-sandbox guidance when the registry lookup throws", () => { + it("falls back to the registry-unreadable guidance when the lookup throws (does not collapse to 'no sandboxes')", () => { const printed: string[] = []; reportGpuPassthroughRecovery( (line) => printed.push(line), @@ -59,6 +73,7 @@ describe("reportGpuPassthroughRecovery", () => { throw new Error("registry unreachable"); }, ); - expect(printed).toEqual(gpuPassthroughRecoveryLines([])); + expect(printed).toEqual(gpuPassthroughRecoveryLines(null)); + expect(printed.join("\n")).toContain("Could not read the NemoClaw sandbox registry"); }); }); diff --git a/src/lib/onboard/gpu-recovery.ts b/src/lib/onboard/gpu-recovery.ts index ef4c9b80385..8b8a3b2794e 100644 --- a/src/lib/onboard/gpu-recovery.ts +++ b/src/lib/onboard/gpu-recovery.ts @@ -1,40 +1,55 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -export function gpuPassthroughRecoveryLines(registeredNames: readonly string[]): string[] { +const GATEWAY_REMOVAL_COMMAND = "openshell gateway destroy -g nemoclaw"; +const ONBOARD_GPU_COMMAND = "nemoclaw onboard --gpu"; + +export function gpuPassthroughRecoveryLines(registeredNames: readonly string[] | null): string[] { const lines: string[] = [" Existing gateway was started without GPU passthrough."]; + if (registeredNames === null) { + lines.push(" Could not read the NemoClaw sandbox registry; cannot enumerate sandboxes."); + lines.push(" To enable GPU, clear the stale gateway directly and re-onboard:"); + lines.push(` ${GATEWAY_REMOVAL_COMMAND}`); + lines.push(` ${ONBOARD_GPU_COMMAND}`); + return lines; + } if (registeredNames.length === 0) { lines.push(" No sandboxes are registered, so there is nothing to destroy."); - lines.push(" To enable GPU, clear the stale gateway state and re-onboard:"); - lines.push(" nemoclaw uninstall && nemoclaw onboard --gpu"); + lines.push(" To enable GPU, clear the stale gateway and re-onboard:"); + lines.push(` ${GATEWAY_REMOVAL_COMMAND}`); + lines.push(` ${ONBOARD_GPU_COMMAND}`); return lines; } const plural = registeredNames.length === 1 ? "" : "es"; const list = registeredNames.map((n) => `\`${n}\``).join(", "); lines.push(` To enable GPU, destroy the registered sandbox${plural} (${list}) and re-onboard:`); - for (const name of registeredNames) lines.push(` nemoclaw ${name} destroy --yes`); - lines.push(" nemoclaw onboard --gpu"); + registeredNames.forEach((name, index) => { + const isLast = index === registeredNames.length - 1; + const flags = isLast ? " --yes --cleanup-gateway" : " --yes"; + lines.push(` nemoclaw ${name} destroy${flags}`); + }); + lines.push(` ${ONBOARD_GPU_COMMAND}`); return lines; } -function defaultRegisteredSandboxNames(): readonly string[] { +function defaultRegisteredSandboxNames(): readonly string[] | null { try { const registry = require("../state/registry") as typeof import("../state/registry"); return registry.listSandboxes().sandboxes.map((s) => s.name).filter(Boolean); } catch { - return []; + return null; } } export function reportGpuPassthroughRecovery( emit: (line: string) => void = console.error, - listRegisteredSandboxes: () => readonly string[] = defaultRegisteredSandboxNames, + listRegisteredSandboxes: () => readonly string[] | null = defaultRegisteredSandboxNames, ): void { - let names: readonly string[] = []; + let names: readonly string[] | null; try { names = listRegisteredSandboxes(); } catch { - names = []; + names = null; } for (const line of gpuPassthroughRecoveryLines(names)) emit(line); }