Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion bin/nemoclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,19 @@ if (invokedAs === "nemo-deepagents") {
process.env.NEMOCLAW_INVOKED_AS = "nemo-deepagents";
}

require("../dist/nemoclaw");
const { log } = require("../dist/lib/cli/logger");

function reportTopLevelCliError(error) {
let message = "Command failed without an error message.";
try {
const candidate = error instanceof Error ? error.message : String(error);
if (candidate.trim()) message = candidate.trim();
} catch {
// Keep the fallback message when an unusual thrown value cannot be stringified.
}
log.error(`Error: ${message}`);
process.exitCode = 1;
}

const { mainPromise } = require("../dist/nemoclaw");
Promise.resolve(mainPromise).catch(reportTopLevelCliError);
Comment on lines +26 to +27

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For Node.js CommonJS entrypoints, does a synchronous require("../dist/nemoclaw") throw before a later Promise.resolve(mainPromise).catch(...) can handle the failure?

💡 Result:

Yes, a synchronous require call will throw an exception immediately if it fails [1][2], and this happens before any later code—including any Promise-based error handling—can execute [3]. Because require is a strictly synchronous operation [1], it operates within the main execution flow of the Node.js event loop [1]. If the module cannot be resolved, fails to load, or encounters an issue such as ERR_REQUIRE_ASYNC_MODULE (when attempting to load an ES module with top-level await) [4][5], require throws an exception immediately [1][2]. Because this throw is synchronous, it is not a Promise rejection [3]. Consequently: 1. A.catch method attached to a promise will not catch it, as.catch only handles rejected promises, not synchronous exceptions [3][6]. 2. If you have code structured like: require("../dist/nemoclaw"); Promise.resolve(mainPromise).catch(...); The require call will throw before the Promise is even created or the.catch handler is attached [3]. Unless the require call is wrapped in a try...catch block, the exception will bubble up as an uncaught exception, which may crash the Node.js process [3][2]. To handle such failures, you must use a standard try...catch block around the require call [3][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target launcher ---'
cat -n bin/nemoclaw.js

printf '%s\n' '--- error handler and related tests ---'
rg -n -C 5 'reportTopLevelCliError|top-level-error|mainPromise' bin test package.json

Repository: NVIDIA/NemoClaw

Length of output: 5724


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- top-level error contract test ---'
cat -n test/package-contract/cli/top-level-error.test.ts

printf '%s\n' '--- launcher references and build/package configuration ---'
rg -n -C 4 'bin/nemoclaw|dist/nemoclaw|top-level errors|package-contract' package.json package-lock.json bin test --glob '!**/node_modules/**' || true

printf '%s\n' '--- synchronous CommonJS load probe ---'
node - <<'JS'
try {
  require("./__definitely_missing_nemoclaw_entrypoint__");
  console.log("unexpected: require returned");
} catch (error) {
  console.log(JSON.stringify({
    name: error && error.name,
    code: error && error.code,
    caughtSynchronously: true,
  }));
}
let handlerAttached = false;
try {
  const value = require("./__definitely_missing_nemoclaw_entrypoint__");
  Promise.resolve(value).catch(() => { handlerAttached = true; });
} catch (error) {
  console.log(JSON.stringify({
    code: error && error.code,
    promiseCatchAttached: handlerAttached,
    uncaughtPathAvoided: false,
  }));
}
JS

Repository: NVIDIA/NemoClaw

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- focused probe output ---'
node - <<'JS'
let catchAttached = false;
try {
  const value = require("./__definitely_missing_nemoclaw_entrypoint__");
  Promise.resolve(value).catch(() => { catchAttached = true; });
} catch (error) {
  console.log(JSON.stringify({
    name: error.name,
    code: error.code,
    catchAttached,
    reachesFollowingStatement: false,
  }));
}
JS

printf '%s\n' '--- tracked build entrypoints ---'
git ls-files 'dist/nemoclaw*' 'src/**' 'bin/nemoclaw.js' | head -80

printf '%s\n' '--- logger implementation references ---'
rg -n -C 3 'export .*log|const log|log\.error' src bin test/package-contract/cli --glob '*.ts' --glob '*.js' --glob '*.cts'

Repository: NVIDIA/NemoClaw

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -eu

LOG=/tmp/coderabbit-shell-logs/shell-output-yVCIts
sed -n '1,35p' "$LOG"

Repository: NVIDIA/NemoClaw

Length of output: 1373


Catch synchronous startup failures at the executable boundary.

require("../dist/nemoclaw") executes before the promise rejection handler is registered. If the module or a dependency throws, reportTopLevelCliError is bypassed. Wrap the require() in try/catch and add this failure mode to test/package-contract/cli/top-level-error.test.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bin/nemoclaw.js` around lines 26 - 27, Update the executable boundary around
the require of dist/nemoclaw to catch synchronous module-loading failures and
route them through reportTopLevelCliError, while preserving the existing
mainPromise rejection handling. Extend the top-level error coverage in
test/package-contract/cli/top-level-error.test.ts to verify startup failures are
reported through the same handler.

8 changes: 7 additions & 1 deletion docs/manage-sandboxes/backup-restore.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ It preserves OpenShell credential placeholders so rebuild can reattach the host-
If NemoClaw cannot sanitize a copied configuration or environment file, it omits that file from the snapshot.
If it cannot remove the unsafe file, snapshot creation returns an error.
It deletes the incomplete backup when cleanup succeeds and reports when the backup remains.
This sanitization uses an isolated `python3` helper on POSIX hosts to keep reads, replacements, and removals anchored to opened directory descriptors.
Snapshot sanitization on POSIX hosts requires Python 3 from a trusted absolute path.
NemoClaw checks fixed installation paths and the directory that contains the Node.js executable.
It does not resolve the helper through `PATH`, which can contain user-controlled entries.
If no trusted interpreter is available, snapshot creation removes the incomplete backup when possible and tells you to install Python 3 before rerunning the command.
Install Python 3 through your operating-system package manager or Homebrew, then rerun `$$nemoclaw <name> snapshot create`.
NemoClaw runs the helper in isolated mode.
The helper keeps reads, replacements, and removals anchored to opened directory descriptors.
If a copied file or parent directory changes identity during the operation, snapshot creation fails closed instead of following the changed path.

<AgentOnly variant="hermes">
Expand Down
14 changes: 14 additions & 0 deletions docs/reference/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,20 @@ Upgrade NemoClaw to a version that supports your OpenShell release, or install a
For fresh installs, NemoClaw passes the blueprint range to `install-openshell.sh` and resolves a compatible published OpenShell release before downloading.
If GitHub release metadata is unavailable, the script uses its bundled fallback pin and the post-install gate still enforces the configured range.

### Review Sandbox Creation Diagnostics

When onboarding fails during `openshell sandbox create`, NemoClaw prints the redacted failure and exits with a nonzero status.
Expected CLI failures show the actionable message without internal Node.js stack frames.
The `Diagnostics saved:` line identifies the exact bundle path under the active NemoClaw state root.
The default path is `~/.nemoclaw/onboard-failures/`; a custom gateway port uses `~/.nemoclaw/gateways/<port>/onboard-failures/`.

Open `summary.txt` in the printed directory first.
It records the retained output path under `create_output` and shows the final error lines under `failure_excerpt`.
The `sandbox-create-output.log` file retains a bounded tail of the sandbox creation output.
The bundle also retains any available OpenShell gateway or virtual machine console logs.
This keeps the failing gateway or image-build step available on Linux and macOS, even when the gateway log or console output path is unavailable.
Correct the failure reported in the bundle before you rerun `$$nemoclaw onboard --resume`.

<AgentOnly variant="openclaw">

### Sandbox build fails during OpenClaw plugin install
Expand Down
8 changes: 4 additions & 4 deletions nemoclaw/src/shared/snapshot-sanitizer-boundary.cts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export function setSnapshotSanitizerPythonPathForTest(
snapshotSanitizerPythonPathForTest = pythonPath;
}

function snapshotSanitizerPythonPath(): string | null {
export function resolveSnapshotSanitizerPythonPath(): string | null {
if (process.env.VITEST === "true" && snapshotSanitizerPythonPathForTest !== undefined) {
return snapshotSanitizerPythonPathForTest;
}
Expand Down Expand Up @@ -719,7 +719,7 @@ export function scanDescriptorSnapshot(
targetName?: string,
): DescriptorSnapshotScan | null {
const mode = targetName === undefined ? "scan-tree" : "scan-file";
const pythonPath = snapshotSanitizerPythonPath();
const pythonPath = resolveSnapshotSanitizerPythonPath();
if (pythonPath === null) return null;
const result = spawnSync(
pythonPath,
Expand Down Expand Up @@ -751,7 +751,7 @@ export function applyDescriptorSnapshotActions(
actions: readonly SnapshotSanitizationAction[],
): boolean {
if (actions.length === 0) return true;
const pythonPath = snapshotSanitizerPythonPath();
const pythonPath = resolveSnapshotSanitizerPythonPath();
if (pythonPath === null) return false;
const result = spawnSync(
pythonPath,
Expand All @@ -774,7 +774,7 @@ export function installDescriptorSnapshotFile(
content: string,
): boolean {
if (!isSafeRelativePath(targetName) || targetName.includes("/")) return false;
const pythonPath = snapshotSanitizerPythonPath();
const pythonPath = resolveSnapshotSanitizerPythonPath();
if (pythonPath === null) return false;
const result = spawnSync(
pythonPath,
Expand Down
1 change: 1 addition & 0 deletions scripts/checks/export-managed-image-failure-diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const EXPORTED_DIAGNOSTIC_FILES = new Set([
"openshell-gateway-relevant.log",
"openshell-gateway-tail.log",
"rootfs-console.log",
"sandbox-create-output.log",
"summary.txt",
]);

Expand Down
16 changes: 15 additions & 1 deletion src/lib/onboard/created-sandbox-failure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ describe("reportSandboxCreateFailure", () => {
).toThrow(ExitSignal);
expect(deps.printCreateFailureDiagnostics).toHaveBeenCalledWith("alpha", {
backupPath: "/tmp/backup",
createOutput: "boom",
});
expect(deps.printRecoveryHints).toHaveBeenCalledWith("boom", {
createArgs: ["sandbox", "create", "alpha"],
Expand All @@ -92,7 +93,7 @@ describe("reportSandboxCreateFailure", () => {
expect(withOutput.classifyCreateFailure).toHaveBeenCalledWith(
"failed with Authorization: Bearer secr********",
);
expect(withOutput.error).toHaveBeenCalledWith("failed with Authorization: Bearer secr********");
expect(withOutput.error).toHaveBeenCalledWith("failed with Authorization: Bearer <REDACTED>");
expect(withOutput.error).not.toHaveBeenCalledWith(
"failed with Authorization: Bearer secret-token",
);
Expand All @@ -119,6 +120,7 @@ describe("reportSandboxCreateFailure", () => {
"github ghp_abcdefghijklmnopqrstuvwxyz1234567890",
"openai sk-abcdefghijklmnopqrstuvwxyz1234567890",
"aws AKIAABCDEFGHIJKLMNOP", // gitleaks:allow
"telegram path /bot-full-redaction-only/",
].join("\n");

expect(() => reportSandboxCreateFailure(createFailureOptions({ createOutput }), deps)).toThrow(
Expand All @@ -132,13 +134,25 @@ describe("reportSandboxCreateFailure", () => {
expect(echoed).not.toContain("ghp_abcdefghijklmnopqrstuvwxyz1234567890");
expect(echoed).not.toContain("sk-abcdefghijklmnopqrstuvwxyz1234567890");
expect(echoed).not.toContain("AKIAABCDEFGHIJKLMNOP"); // gitleaks:allow
expect(echoed).not.toContain("bot-full-redaction-only");
expect(echoed).toContain("/bot<REDACTED>/");
const hinted = (deps.printRecoveryHints as ReturnType<typeof vi.fn>).mock.calls
.map((call) => String(call[0]))
.join("\n");
expect(hinted).not.toContain("secret-token");
expect(hinted).not.toContain("ghp_abcdefghijklmnopqrstuvwxyz1234567890");
expect(hinted).not.toContain("sk-abcdefghijklmnopqrstuvwxyz1234567890");
expect(hinted).not.toContain("AKIAABCDEFGHIJKLMNOP"); // gitleaks:allow
const diagnosticOutput = String(
(deps.printCreateFailureDiagnostics as ReturnType<typeof vi.fn>).mock.calls[0]?.[1]
?.createOutput,
);
expect(diagnosticOutput).toContain("<REDACTED>");
expect(diagnosticOutput).not.toContain("secret-token");
expect(diagnosticOutput).not.toContain("ghp_abcdefghijklmnopqrstuvwxyz1234567890");
expect(diagnosticOutput).not.toContain("sk-abcdefghijklmnopqrstuvwxyz1234567890");
expect(diagnosticOutput).not.toContain("AKIAABCDEFGHIJKLMNOP"); // gitleaks:allow
expect(diagnosticOutput).not.toContain("bot-full-redaction-only");
});

it("falls back to exit code 1 when the create status is zero", () => {
Expand Down
16 changes: 13 additions & 3 deletions src/lib/onboard/created-sandbox-failure.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { redact } from "../security/redact";
import { redact, redactFull } from "../security/redact";
import type { CreatedSandboxReadinessResult } from "./sandbox-readiness-tracing";

/** Remove credential material before sandbox create failures reach a diagnostic sink. */
export function redactSandboxCreateFailureOutput(output: string): string {
return redact(redactFull(output));
}

export type SandboxCreateFailureReportOptions = {
sandboxName: string;
/** Non-zero exit status from the create stream. */
Expand All @@ -18,7 +23,10 @@ export type SandboxCreateFailureReportOptions = {

export type SandboxCreateFailureReportDeps = {
classifyCreateFailure(output: string): { kind: string };
printCreateFailureDiagnostics(sandboxName: string, options: { backupPath: string | null }): void;
printCreateFailureDiagnostics(
sandboxName: string,
options: { backupPath: string | null; createOutput: string },
): void;
printRecoveryHints(output: string, options: { createArgs: readonly string[] }): void;
warn(message: string): void;
error(message: string): void;
Expand All @@ -36,6 +44,7 @@ export function reportSandboxCreateFailure(
deps: SandboxCreateFailureReportDeps,
): void {
const redactedCreateOutput = redact(options.createOutput);
const fullyRedactedCreateOutput = redactSandboxCreateFailureOutput(options.createOutput);
const failure = deps.classifyCreateFailure(redactedCreateOutput);
if (failure.kind === "sandbox_create_incomplete") {
// The sandbox was created in the gateway but the create stream exited
Expand All @@ -52,10 +61,11 @@ export function reportSandboxCreateFailure(
deps.error(` Sandbox creation failed (exit ${options.createStatus}).`);
if (options.createOutput) {
deps.error("");
deps.error(redactedCreateOutput);
deps.error(fullyRedactedCreateOutput);
}
deps.printCreateFailureDiagnostics(options.sandboxName, {
backupPath: options.restoreBackupPath,
createOutput: fullyRedactedCreateOutput,
});
deps.error(" Try: openshell sandbox list # check gateway state");
deps.printRecoveryHints(redactedCreateOutput, { createArgs: options.createArgs });
Expand Down
87 changes: 79 additions & 8 deletions src/lib/onboard/sandbox-create-failure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ import path from "node:path";
import { GATEWAY_PORT } from "../core/ports";
import { rejectSymlinksOnPath } from "../state/config-io";
import { nemoclawStateRoot } from "../state/state-root";
import { redactSandboxCreateFailureOutput } from "./created-sandbox-failure";

const ANSI_RE = /\x1B(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\)|[@-_])/g;
const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
const MAX_RELEVANT_LOG_LINES = 120;
const MAX_GATEWAY_TAIL_LINES = 240;
const MAX_CREATE_OUTPUT_LINES = 240;
const MAX_CREATE_OUTPUT_CHARS = 32_000;
const MAX_FAILURE_EXCERPT_LINES = 8;
const TRUNCATED_OUTPUT_MARKER = "[diagnostic truncated; showing final output]";

export type SandboxCreateFailureDiagnostics = {
dir: string;
Expand All @@ -22,13 +27,16 @@ export type SandboxCreateFailureDiagnostics = {
consoleOutput: string | null;
copiedConsoleOutput: string | null;
gatewayTailPath: string | null;
createOutputPath: string | null;
backupPath: string | null;
summaryLines: string[];
};

export type SandboxCreateFailureDiagnosticOptions = {
homeDir?: string;
gatewayLogPath?: string | null;
homebrewPrefix?: string | null;
createOutput?: string | null;
backupPath?: string | null;
now?: Date;
};
Expand All @@ -45,8 +53,8 @@ function timestampForPath(now: Date): string {
return now.toISOString().replace(/[:.]/g, "-");
}

function gatewayLogCandidates(homeDir: string): string[] {
return [
function gatewayLogCandidates(homeDir: string, homebrewPrefix?: string | null): string[] {
const candidates = [
path.join(
homeDir,
".local",
Expand All @@ -57,17 +65,59 @@ function gatewayLogCandidates(homeDir: string): string[] {
),
path.join(homeDir, ".local", "state", "openshell", "openshell-gateway.log"),
];
const homebrewPrefixes = [
homebrewPrefix,
process.env.HOMEBREW_PREFIX,
"/opt/homebrew",
"/usr/local",
].filter((prefix): prefix is string => Boolean(prefix) && path.isAbsolute(prefix as string));
for (const prefix of new Set(homebrewPrefixes)) {
candidates.push(
path.join(prefix, "var", "log", "openshell", "openshell-gateway.err.log"),
path.join(prefix, "var", "log", "openshell", "openshell-gateway.out.log"),
);
}
return candidates;
}

function latestGatewayLogPath(candidates: string[]): string | null {
let latest: { path: string; modified: number } | null = null;
for (const candidate of candidates) {
try {
const stat = fs.statSync(candidate);
if (!stat.isFile()) continue;
if (latest === null || stat.mtimeMs > latest.modified) {
latest = { path: candidate, modified: stat.mtimeMs };
}
} catch {
// Continue to the next known log location.
}
}
return latest?.path ?? null;
}

function readLogLines(filePath: string): string[] | null {
try {
if (!fs.existsSync(filePath)) return null;
return stripAnsi(fs.readFileSync(filePath, "utf-8")).split(/\r?\n/);
return redactSandboxCreateFailureOutput(stripAnsi(fs.readFileSync(filePath, "utf-8"))).split(
/\r?\n/,
);
} catch {
return null;
}
}

function createOutputTail(value: string | null | undefined): string[] {
const redacted = redactSandboxCreateFailureOutput(stripAnsi(value ?? "")).trim();
if (!redacted) return [];
const wasTruncated = redacted.length > MAX_CREATE_OUTPUT_CHARS;
const lines = (wasTruncated ? redacted.slice(-MAX_CREATE_OUTPUT_CHARS) : redacted)
.split(/\r?\n/)
.filter((line) => line.trim());
if (!wasTruncated) return lines.slice(-MAX_CREATE_OUTPUT_LINES);
return [TRUNCATED_OUTPUT_MARKER, ...lines.slice(-(MAX_CREATE_OUTPUT_LINES - 1))];
}

function extractField(line: string, field: string): string | null {
const match = line.match(new RegExp(`${field}=([^\\s]+)`));
return match?.[1] ?? null;
Expand Down Expand Up @@ -113,7 +163,9 @@ function filterRelevantLines(
if (!line.trim()) return false;
if (line.includes(`sandbox_name=${sandboxName}`)) return true;
if (sandboxId && line.includes(`sandbox_id=${sandboxId}`)) return true;
return /ERROR krun|VmCreate|ProcessExited|console_output=|state_dir=/.test(line);
return /\bERROR\b|failed to (?:build|solve)|VmCreate|ProcessExited|console_output=|state_dir=/i.test(
line,
);
});
return relevant.slice(-MAX_RELEVANT_LOG_LINES);
}
Expand Down Expand Up @@ -172,8 +224,7 @@ export function collectSandboxCreateFailureDiagnostics(

const gatewayLogPath =
options.gatewayLogPath ??
gatewayLogCandidates(homeDir).find((candidate) => fs.existsSync(candidate)) ??
null;
latestGatewayLogPath(gatewayLogCandidates(homeDir, options.homebrewPrefix));
const rawLines = gatewayLogPath ? readLogLines(gatewayLogPath) : null;
const block = rawLines ? findLatestSandboxBlock(rawLines, sandboxName) : [];
const sandboxId = getLatestSandboxId(block, sandboxName);
Expand All @@ -192,6 +243,13 @@ export function collectSandboxCreateFailureDiagnostics(
);
const stateEntries = listStateDir(stateDir);
const backupPath = options.backupPath ?? null;
const createOutputLines = createOutputTail(options.createOutput);
const createOutputPath =
createOutputLines.length > 0 ? path.join(dir, "sandbox-create-output.log") : null;

if (createOutputPath) {
fs.writeFileSync(createOutputPath, `${createOutputLines.join("\n")}\n`, { mode: 0o600 });
}

if (relevantLines.length > 0) {
fs.writeFileSync(
Expand All @@ -213,11 +271,23 @@ export function collectSandboxCreateFailureDiagnostics(
`sandbox_id=${sandboxId ?? "unknown"}`,
`gateway_log=${gatewayLogPath ?? "not-found"}`,
`gateway_tail=${gatewayTailPath ?? "not-written"}`,
`create_output=${createOutputPath ?? "not-written"}`,
`state_dir=${stateDir ?? "unknown"}`,
`console_output=${consoleOutput ?? "unknown"}`,
`copied_console_output=${copiedConsoleOutput ?? "not-copied"}`,
`backup_path=${backupPath ?? "none"}`,
];
const failureExcerpt = (
createOutputLines.length > 0
? createOutputLines
: relevantLines.length > 0
? relevantLines
: gatewayTailLines
).slice(-MAX_FAILURE_EXCERPT_LINES);
if (failureExcerpt.length > 0) {
summaryLines.push("failure_excerpt:");
summaryLines.push(...failureExcerpt.map((line) => ` ${line}`));
}
if (stateEntries.length > 0) {
summaryLines.push("state_dir_entries:");
summaryLines.push(...stateEntries.map((entry) => ` ${entry}`));
Expand All @@ -234,8 +304,9 @@ export function collectSandboxCreateFailureDiagnostics(
consoleOutput,
copiedConsoleOutput,
gatewayTailPath,
createOutputPath,
backupPath,
summaryLines: relevantLines.length > 0 ? relevantLines.slice(-8) : gatewayTailLines.slice(-8),
summaryLines: failureExcerpt,
};
}

Expand All @@ -248,7 +319,7 @@ export function printSandboxCreateFailureDiagnostics(

console.error(` Diagnostics saved: ${diagnostics.dir}`);
if (diagnostics.summaryLines.length > 0) {
console.error(" Recent OpenShell gateway failure:");
console.error(" Recent sandbox creation failure:");
for (const line of diagnostics.summaryLines) {
console.error(` ${line}`);
}
Expand Down
1 change: 1 addition & 0 deletions src/lib/onboard/sandbox-gpu-create-run-attempt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ export function createSandboxGpuCreateAttemptRunner(
await runtimePatch.rollbackManagedStartupAfterCreateFailure();
printSandboxCreateFailureDiagnostics(input.sandboxName, {
backupPath: input.restoreBackupPath,
createOutput: createResult.output,
});
if (compatibility) runtimePatch.printReadinessFailureIfEnabled();
else {
Expand Down
Loading
Loading