Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
85 changes: 4 additions & 81 deletions bin/lib/runtime-recovery.js
Original file line number Diff line number Diff line change
@@ -1,84 +1,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//
// Thin re-export shim — the implementation lives in src/lib/runtime-recovery.ts,
// compiled to dist/lib/runtime-recovery.js.

const onboardSession = require("./onboard-session");

function stripAnsi(text) {
// eslint-disable-next-line no-control-regex
return String(text || "").replace(/\x1b\[[0-9;]*m/g, "");
}

function parseLiveSandboxNames(listOutput = "") {
const clean = stripAnsi(listOutput);
const names = new Set();
for (const rawLine of clean.split("\n")) {
const line = rawLine.trim();
if (!line) continue;
if (/^(NAME|No sandboxes found\.?$)/i.test(line)) continue;
if (/^Error:/i.test(line)) continue;
const cols = line.split(/\s+/);
if (cols[0]) {
names.add(cols[0]);
}
}
return names;
}

function classifySandboxLookup(output = "") {
const clean = stripAnsi(output).trim();
if (!clean) {
return { state: "missing", reason: "empty" };
}
if (/sandbox not found|status:\s*NotFound/i.test(clean)) {
return { state: "missing", reason: "not_found" };
}
if (
/transport error|client error|Connection reset by peer|Connection refused|No active gateway|Gateway: .*Error/i.test(
clean,
)
) {
return { state: "unavailable", reason: "gateway_unavailable" };
}
return { state: "present", reason: "ok" };
}

function classifyGatewayStatus(output = "") {
const clean = stripAnsi(output).trim();
if (!clean) {
return { state: "inactive", reason: "empty" };
}
if (/Connected/i.test(clean)) {
return { state: "connected", reason: "ok" };
}
if (
/No active gateway|transport error|client error|Connection reset by peer|Connection refused|Gateway: .*Error/i.test(
clean,
)
) {
return { state: "unavailable", reason: "gateway_unavailable" };
}
return { state: "inactive", reason: "not_connected" };
}

function shouldAttemptGatewayRecovery({
sandboxState = "missing",
gatewayState = "inactive",
} = {}) {
return sandboxState === "unavailable" && gatewayState !== "connected";
}

function getRecoveryCommand() {
const session = onboardSession.loadSession();
if (session && session.resumable !== false) {
return "nemoclaw onboard --resume";
}
return "nemoclaw onboard";
}

module.exports = {
classifyGatewayStatus,
classifySandboxLookup,
getRecoveryCommand,
parseLiveSandboxNames,
shouldAttemptGatewayRecovery,
};
module.exports = require("../../dist/lib/runtime-recovery");
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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check whether dist target exists without building =="
if [ -f dist/lib/runtime-recovery.js ]; then
  echo "FOUND: dist/lib/runtime-recovery.js"
else
  echo "MISSING: dist/lib/runtime-recovery.js"
fi

echo
echo "== Find references to runtime-recovery from bin/scripts =="
rg -n -C2 --type=js '\bruntime-recovery\b' bin scripts || true

echo
echo "== Inspect package scripts for enforced build-before-cli/test =="
rg -n '"(build|build:cli|test|test:cli|pretest|prepare|prepack|postinstall)"\s*:' package.json || true

Repository: NVIDIA/NemoClaw

Length of output: 1447


🏁 Script executed:

#!/bin/bash
# Get full package.json to see all scripts
cat -n package.json | head -50

echo "=== Check for build:all or other build targets ==="
rg -n '"(build|build:all|compile|prebuild|postinstall)"\s*:' package.json

echo "=== Check if dist/ is version controlled or generated ==="
git ls-files dist/ 2>/dev/null | head -20 || echo "dist/ not in git or git command failed"

echo "=== Check git ignore for dist/ ==="
cat .gitignore | grep -E 'dist|build' || echo "No dist/build in .gitignore"

echo "=== Check tsconfig files ==="
fd -t f 'tsconfig.*\.json$' -x basename {}

Repository: NVIDIA/NemoClaw

Length of output: 2384


🏁 Script executed:

#!/bin/bash
# Check the actual source file to understand the pattern
echo "=== Check src/lib/runtime-recovery.ts ==="
if [ -f src/lib/runtime-recovery.ts ]; then
  head -20 src/lib/runtime-recovery.ts
else
  echo "File not found, searching for it:"
  fd 'runtime-recovery' --type f
fi

echo
echo "=== Check other shims in bin/lib for pattern ==="
ls -la bin/lib/ | grep -E '\.js$' || true

echo
echo "=== Check content of other shim files ==="
for shim in bin/lib/*.js; do
  if [ -f "$shim" ]; then
    echo "--- $shim ---"
    head -10 "$shim"
  fi
done

Repository: NVIDIA/NemoClaw

Length of output: 7681


🏁 Script executed:

#!/bin/bash
# Verify tsconfig.src.json outputs to dist/
echo "=== tsconfig.src.json ==="
cat tsconfig.src.json

echo
echo "=== All files in bin/lib that require dist ==="
rg -n 'require.*dist' bin/lib/

echo
echo "=== Check if dist/ gets created during dev setup ==="
ls -la dist/ 2>/dev/null | head -5 || echo "dist/ directory does not exist"

Repository: NVIDIA/NemoClaw

Length of output: 1245


Multiple critical dependencies on missing dist/ directory — CLI and test flows broken in fresh checkout.

The shim in line 7 (bin/lib/runtime-recovery.js) and related shims (bin/lib/preflight.js, bin/lib/onboard.js) require compiled modules from dist/lib/ that do not exist without an explicit build step. Additionally:

  • dist/lib/ is excluded from package.json "files" field (only "nemoclaw/dist/" is included), so npm install will not provide these files
  • The prepare hook does not invoke build:cli, leaving no automatic build step
  • test and CLI execution will fail with MODULE_NOT_FOUND on fresh checkout or after npm install

Affected imports:

  • bin/lib/runtime-recovery.js:7dist/lib/runtime-recovery
  • bin/lib/preflight.js:7dist/lib/preflight
  • bin/lib/onboard.js:59-63dist/lib/gateway-state, validation, url-utils, build-context, dashboard

Either add dist/ to the "files" field and build:cli to prepare, or convert these modules back to CommonJS sources in bin/lib/ (preferred per guidelines: "bin/lib/**/*.js modules are expected to remain CommonJS").

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/lib/runtime-recovery.js` at line 7, The bin shims (e.g.,
bin/lib/runtime-recovery.js, bin/lib/preflight.js, bin/lib/onboard.js) currently
require compiled modules from dist/lib/ which are missing on fresh checkouts;
fix by converting these shims back to true CommonJS sources that directly export
the runtime logic instead of proxying to dist—restore or reimplement the
CommonJS exports for runtime-recovery, preflight and onboard (and the dependent
modules gateway-state, validation, url-utils, build-context, dashboard) inside
bin/lib so module.exports contains the implementation (or requires the original
source CommonJS files) and no longer points at ../../dist; alternatively, if you
prefer keeping compiled artifacts, update package.json to include dist/ in
"files" and add the build:cli step to the prepare hook so dist gets produced on
install—choose the CommonJS conversion (preferred) and ensure the exported
identifiers and require paths used by bin/* match the original API.

24 changes: 15 additions & 9 deletions test/runtime-recovery.test.js → src/lib/runtime-recovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@

import { describe, expect, it } from "vitest";

// Import from compiled dist/ for correct coverage attribution.
import {
classifyGatewayStatus,
classifySandboxLookup,
parseLiveSandboxNames,
shouldAttemptGatewayRecovery,
} from "../bin/lib/runtime-recovery";
} from "../../dist/lib/runtime-recovery";
Comment on lines +6 to +12
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check test-related scripts for guaranteed build-before-test =="
rg -n '"(build|build:cli|test|test:cli|pretest|prepare)"\s*:' package.json || true

echo
echo "== Check CI workflows for build step before vitest/test commands =="
fd -HI -t f -i 'yml' .github/workflows -x rg -n -C2 '(vitest|npm run test|pnpm test|yarn test|build:cli|npm run build|pnpm build|yarn build)' {}

Repository: NVIDIA/NemoClaw

Length of output: 608


🏁 Script executed:

cat -n package.json

Repository: NVIDIA/NemoClaw

Length of output: 2418


🏁 Script executed:

find .github/workflows -type f -name "*.yml" -o -name "*.yaml" 2>/dev/null | head -20

Repository: NVIDIA/NemoClaw

Length of output: 481


🏁 Script executed:

cat -n src/lib/runtime-recovery.test.ts | head -40

Repository: NVIDIA/NemoClaw

Length of output: 1694


🏁 Script executed:

cat -n .github/workflows/pr.yaml | head -80

Repository: NVIDIA/NemoClaw

Length of output: 963


🏁 Script executed:

cat -n .github/workflows/main.yaml | head -80

Repository: NVIDIA/NemoClaw

Length of output: 1097


🏁 Script executed:

find . -maxdepth 1 -name "vitest.config.*" -o -name "vite.config.*" 2>/dev/null | xargs cat -n 2>/dev/null || echo "No vitest/vite config found"

Repository: NVIDIA/NemoClaw

Length of output: 1326


🏁 Script executed:

ls -la dist/lib/runtime-recovery.* 2>/dev/null || echo "dist/lib/runtime-recovery files not found"

Repository: NVIDIA/NemoClaw

Length of output: 100


🏁 Script executed:

cat -n .github/actions/basic-checks/action.yaml

Repository: NVIDIA/NemoClaw

Length of output: 1733


🏁 Script executed:

find . -name "runtime-recovery.ts" -not -path "*/node_modules/*" 2>/dev/null

Repository: NVIDIA/NemoClaw

Length of output: 88


🏁 Script executed:

cat -n src/lib/runtime-recovery.ts | head -50

Repository: NVIDIA/NemoClaw

Length of output: 1996


Either import directly from source TypeScript or add a pretest build hook.

The test script in package.json does not include a build step, so running npm test locally without first running npm run build:cli will fail since dist/lib/runtime-recovery.ts is not built. While the CI workflows (pr.yaml, main.yaml) enforce builds before testing via the basic-checks action, local development can run tests against non-existent or stale dist files. Either add a pretest hook ("pretest": "npm run build:cli") to guarantee build ordering, or import directly from the TypeScript source (src/lib/runtime-recovery.ts) for unit tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/runtime-recovery.test.ts` around lines 6 - 12, The test imports
runtime helpers (classifyGatewayStatus, classifySandboxLookup,
parseLiveSandboxNames, shouldAttemptGatewayRecovery) from the compiled dist
which can be missing locally; either update the test imports to point at the
TypeScript source module (change the import path to the src/lib runtime-recovery
module) so tests run against source, or add a pretest npm hook to package.json
(e.g. "pretest": "npm run build:cli") to ensure dist/lib/runtime-recovery is
built before running tests.


describe("runtime recovery helpers", () => {
it("parses live sandbox names from openshell sandbox list output", () => {
Expand All @@ -29,6 +30,15 @@ describe("runtime recovery helpers", () => {
expect(Array.from(parseLiveSandboxNames("No sandboxes found."))).toEqual([]);
});

it("skips error lines", () => {
expect(Array.from(parseLiveSandboxNames("Error: something went wrong"))).toEqual([]);
});

it("handles empty input", () => {
expect(Array.from(parseLiveSandboxNames(""))).toEqual([]);
expect(Array.from(parseLiveSandboxNames())).toEqual([]);
});

it("classifies missing sandbox lookups", () => {
expect(
classifySandboxLookup('Error: × status: NotFound, message: "sandbox not found"').state,
Expand All @@ -52,14 +62,7 @@ describe("runtime recovery helpers", () => {
it("classifies successful sandbox lookups as present", () => {
expect(
classifySandboxLookup(
[
"Sandbox:",
"",
" Id: abc",
" Name: my-assistant",
" Namespace: openshell",
" Phase: Ready",
].join("\n"),
["Sandbox:", "", " Id: abc", " Name: my-assistant", " Phase: Ready"].join("\n"),
).state,
).toBe("present");
});
Expand All @@ -68,6 +71,9 @@ describe("runtime recovery helpers", () => {
expect(classifyGatewayStatus("Gateway: nemoclaw\nStatus: Connected").state).toBe("connected");
expect(classifyGatewayStatus("Error: × No active gateway").state).toBe("unavailable");
expect(classifyGatewayStatus("").state).toBe("inactive");
expect(classifyGatewayStatus("Gateway: nemoclaw\nStatus: Disconnected").state).toBe(
"inactive",
);
});

it("only attempts gateway recovery when sandbox access is unavailable and gateway is down", () => {
Expand Down
90 changes: 90 additions & 0 deletions src/lib/runtime-recovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

/**
* Runtime recovery helpers — classify sandbox/gateway state from CLI
* output and determine recovery strategy.
*/

// onboard-session is CJS — keep as require
// eslint-disable-next-line @typescript-eslint/no-require-imports
const onboardSession = require("../../bin/lib/onboard-session");

// eslint-disable-next-line no-control-regex
const ANSI_RE = /\x1b\[[0-9;]*m/g;

function stripAnsi(text: unknown): string {
return String(text || "").replace(ANSI_RE, "");
}

export interface StateClassification {
state: string;
reason: string;
}

export function parseLiveSandboxNames(listOutput = ""): Set<string> {
const clean = stripAnsi(listOutput);
const names = new Set<string>();
for (const rawLine of clean.split("\n")) {
const line = rawLine.trim();
if (!line) continue;
if (/^(NAME|No sandboxes found\.?$)/i.test(line)) continue;
if (/^Error:/i.test(line)) continue;
const cols = line.split(/\s+/);
if (cols[0]) {
names.add(cols[0]);
}
}
return names;
}

export function classifySandboxLookup(output = ""): StateClassification {
const clean = stripAnsi(output).trim();
if (!clean) {
return { state: "missing", reason: "empty" };
}
if (/sandbox not found|status:\s*NotFound/i.test(clean)) {
return { state: "missing", reason: "not_found" };
}
if (
/transport error|client error|Connection reset by peer|Connection refused|No active gateway|Gateway: .*Error/i.test(
clean,
)
) {
return { state: "unavailable", reason: "gateway_unavailable" };
}
return { state: "present", reason: "ok" };
}

export function classifyGatewayStatus(output = ""): StateClassification {
const clean = stripAnsi(output).trim();
if (!clean) {
return { state: "inactive", reason: "empty" };
}
if (/\bConnected\b/i.test(clean) && !/\bDisconnected\b/i.test(clean)) {
return { state: "connected", reason: "ok" };
}
if (
/No active gateway|transport error|client error|Connection reset by peer|Connection refused|Gateway: .*Error/i.test(
clean,
)
) {
return { state: "unavailable", reason: "gateway_unavailable" };
}
return { state: "inactive", reason: "not_connected" };
}

export function shouldAttemptGatewayRecovery({
sandboxState = "missing",
gatewayState = "inactive",
} = {}): boolean {
return sandboxState === "unavailable" && gatewayState !== "connected";
}

export function getRecoveryCommand(): string {
const session = onboardSession.loadSession();
if (session && session.resumable !== false) {
return "nemoclaw onboard --resume";
}
return "nemoclaw onboard";
}
Loading