Skip to content

Commit 791c5d6

Browse files
authored
Merge branch 'main' into fix/9896-tui-status-config
2 parents 24de251 + 149a846 commit 791c5d6

65 files changed

Lines changed: 2914 additions & 1021 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/managed-images.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ on:
4343
- "scripts/**"
4444
- "src/lib/actions/sandbox/openshell-child-visible-credentials.v*.json"
4545
- "src/lib/actions/sandbox/mcp-bridge-*.ts"
46+
- "src/lib/actions/sandbox/rebuild-post-restore-phase.ts"
4647
- "src/lib/core/json-types.ts"
4748
- "src/lib/core/ports.ts"
4849
- "src/lib/messaging/**"

‎docs/inference/set-up-vllm.mdx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,7 @@ The host checks the token before `docker pull`, so a missing or empty token stop
570570

571571
Set `NEMOCLAW_VLLM_EXTRA_ARGS_JSON` to a JSON array of individual non-blank `vllm serve` tokens for operator-owned options and overrides.
572572
NemoClaw trims and validates the array before downloads, shell-quotes each token, and appends the tokens after the registry defaults.
573+
When the array overrides `--gpu-memory-utilization`, NemoClaw validates the last value and uses that effective value for GPU-memory preflight, matching the final `vllm serve` command.
573574

574575
```bash
575576
NEMOCLAW_PROVIDER=install-vllm \
@@ -579,6 +580,10 @@ NEMOCLAW_PROVIDER=install-vllm \
579580

580581
If the selected image does not support an argument, the managed container exits and NemoClaw prints the log tail.
581582

583+
When the selected managed recipe defines a GPU-memory utilization value, NemoClaw requires valid `nvidia-smi` total/free-memory telemetry for the Docker-selected GPU before large downloads.
584+
It repeats the same check with fresh telemetry immediately before launch so another workload cannot consume the required free GPU memory unnoticed during the download.
585+
If telemetry is unavailable, the Docker-selected device cannot be resolved, or free memory is below the effective utilization requirement, onboarding stops with a diagnostic and can be resumed after the GPU state is corrected.
586+
582587
To bound resource use while investigating long-context workflows on one DGX Spark or N1x host, select the Qwen profile.
583588
The following override disables async scheduling and lowers the context window, concurrent-sequence limit, and batch limit.
584589

‎docs/reference/commands.mdx‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,7 +1054,10 @@ Jetson/Tegra hosts that ship without `nvidia-smi` continue to be detected via th
10541054
Use `--no-gpu` to opt out when you want host-side inference providers only and do not need direct GPU access inside the sandbox.
10551055
Use `--gpu` to require GPU passthrough and fail fast if an NVIDIA GPU is not detected.
10561056
Use `--sandbox-gpu` or `--no-sandbox-gpu` to control only direct NVIDIA GPU access inside the sandbox.
1057-
Use `--sandbox-gpu --sandbox-gpu-device <device>` to pass a specific OpenShell GPU device selector to `openshell sandbox create`; device selectors require explicit sandbox GPU enablement.
1057+
Use `--sandbox-gpu --sandbox-gpu-device <device>` to select an NVIDIA GPU by index (`0`), GPU UUID (`GPU-...`), or full CDI device name (`nvidia.com/gpu=0`).
1058+
NemoClaw preserves the selection on resume.
1059+
For native Docker and Podman creation, NemoClaw passes the normalized CDI name through OpenShell driver config; compatibility routes use the equivalent container-runtime selector.
1060+
Device selection requires explicit sandbox GPU enablement.
10581061
On ordinary native Linux Docker-driver hosts, NemoClaw uses native OpenShell GPU injection by default and never broadens confinement automatically.
10591062

10601063
Portable onboarding requires native OpenShell GPU injection for every agent.
@@ -4893,7 +4896,7 @@ Set them before running `$$nemoclaw onboard`.
48934896
| `NEMOCLAW_VLLM_MODEL` | registry slug or Hugging Face model ID | Selects the model the managed-vLLM install path serves and remains authoritative during DGX Station installer setup. Slugs and full model IDs are case-insensitive. Recognized slugs: `qwen3.6-27b`, `qwen3.6-35b-a3b-nvfp4`, `muse-glimmer-30b`, `nemotron-3.5-lightning-30b`, `nemotron-3-nano-4b`, `deepseek-v4-flash`, `nemotron-3-ultra-550b-a55b`, `deepseek-r1-distill-70b`. The `muse-glimmer-30b` and `nemotron-3.5-lightning-30b` profiles are Experimental on DGX Spark and Linux x86_64 with a qualifying NVIDIA GPU. NemoClaw does not enable vision or DFlash speculative decoding for Muse Glimmer. Station Express selects `nemotron-3-ultra-550b-a55b`; a qualified reciprocal pair uses the distributed topology, while no qualifying pair retains the single-Station Ultra topology. Outside Station Express, unset uses the per-platform profile default. Gated models (for example, `deepseek-r1-distill-70b`) require `HF_TOKEN` or `HUGGING_FACE_HUB_TOKEN`. |
48944897
| `NEMOCLAW_DGX_STATION_PEER` | SSH host or `user@host` | Selects one exact, already-trusted DGX Station peer for Nemotron 3 Ultra pair qualification. The peer must match the reciprocal private `/30` rail and hardware checks; an explicit peer failure stops setup instead of falling back. NemoClaw does not enroll SSH trust or accept a port or SSH option in this value. When unset, DGX Station installer discovery checks only the two deterministic `/30` counterpart addresses. A peer cannot be combined with an explicit non-Ultra model; conflicting explicit selections fail before pair preparation. |
48954898
| `NEMOCLAW_DGX_STATION_SSH_BINDING` | opaque installer-managed token | Carries the qualified peer endpoint and host-key binding from DGX Station pair preparation into the current managed-vLLM install. The installer creates and clears this token; operators should not set or persist it. Missing, changed, or mismatched binding state fails before peer SSH or Docker work. |
4896-
| `NEMOCLAW_VLLM_EXTRA_ARGS_JSON` | JSON array of non-blank strings | Appends advanced operator-owned tokens to the managed `vllm serve` command after NemoClaw's registry defaults. Example: `["--max-num-seqs","2"]`. Malformed JSON, non-string tokens, or blank tokens fail before Docker work starts. |
4899+
| `NEMOCLAW_VLLM_EXTRA_ARGS_JSON` | JSON array of non-blank strings | Appends advanced operator-owned tokens to the managed `vllm serve` command after NemoClaw's registry defaults. Example: `["--max-num-seqs","2"]`. Malformed JSON, non-string tokens, blank tokens, or an invalid `--gpu-memory-utilization` override fail before Docker work starts. The last memory-utilization override also controls the early and immediate pre-launch GPU-memory checks. |
48974900
<AgentOnly variant="openclaw">
48984901
| `NEMOCLAW_MINIMAL_BOOTSTRAP` | `1` to enable | Skips default OpenClaw workspace-template seeding for new pristine workspaces. Existing files are not deleted; refer to [Understand Runtime Changes](../manage-sandboxes/configure-sandboxes/understand-runtime-changes). |
48994902
</AgentOnly>
@@ -5071,7 +5074,7 @@ Set them before running `$$nemoclaw onboard`.
50715074
| `NEMOCLAW_CPU` | percentage or Kubernetes CPU quantity | Overrides the selected profile's CPU size passed to OpenShell `--cpu`. Percentages resolve against detected capacity. |
50725075
| `NEMOCLAW_RAM` | percentage or Kubernetes memory quantity | Overrides the selected profile's memory size passed to OpenShell `--memory`. Percentages resolve against detected capacity. |
50735076
| `NEMOCLAW_SANDBOX_GPU` | `auto`, `1`, or `0` | Controls sandbox GPU passthrough during onboarding. `auto` enables GPU passthrough when an NVIDIA GPU is detected, `1` requires GPU passthrough, and `0` forces CPU-only sandbox creation. |
5074-
| `NEMOCLAW_SANDBOX_GPU_DEVICE` | OpenShell GPU device selector | Selects the GPU device passed with `openshell sandbox create --gpu-device`. Requires explicit sandbox GPU enablement with `NEMOCLAW_SANDBOX_GPU=1` (or `--sandbox-gpu` for CLI-driven onboarding); otherwise onboarding rejects the selector instead of treating it as an implicit opt-in. |
5077+
| `NEMOCLAW_SANDBOX_GPU_DEVICE` | NVIDIA GPU index, UUID, or CDI device name | Selects the GPU through OpenShell driver config on native Docker and Podman routes, or through the equivalent container-runtime selector on a compatibility route. Requires explicit sandbox GPU enablement with `NEMOCLAW_SANDBOX_GPU=1` (or `--sandbox-gpu` for CLI-driven onboarding); otherwise onboarding rejects the selector instead of treating it as an implicit opt-in. |
50755078
| `NEMOCLAW_SANDBOX_BASE_IMAGE_REFRESH` | `1`, `true`, `yes`, or `on` to enable | Bypasses recorded sandbox base-image resolution metadata during onboarding, recreation, and rebuild. NemoClaw reruns candidate resolution but can still use a compatible image from Docker's local image store. Versioned release candidates that exist locally but fail validation are refreshed from the registry once during normal resolution. This setting does not discard onboarding session state. |
50765079
| `NEMOCLAW_SANDBOX_BASE_LOCAL_BUILD` | unset or `auto` (default); `1`, `true`, `yes`, or `on` to enable; `0`, `false`, `no`, or `off` to disable | Controls whether base-image resolution may build a compatible image locally. The default allows builds during normal CLI runs and disables them when `NODE_ENV=test` or `VITEST=true`. When source inputs or a missing/incompatible release-version base require a fresh build, disabling local builds makes resolution fail instead of using an unproven image. |
50775080
| `NEMOCLAW_DOCKER_GPU_PATCH` | unset, `auto`, `fallback`, `1`, or `0`; other legacy nonzero values remain accepted through `v0.0.x` and will be removed in `v0.1.0` | Selects Linux Docker-driver GPU routing. Unset, `auto`, or `0` uses native OpenShell GPU injection on ordinary native Linux. `fallback` explicitly opts into one native attempt followed by one bounded compatibility retry when trusted host evidence identifies a GPU-routing failure. `1` and legacy nonzero values select the compatibility patch from the outset. Docker Desktop WSL and Jetson/Tegra use the compatibility path by default; Docker Desktop WSL ignores `0`, while Jetson/Tegra accepts `0` only as a troubleshooting override that bypasses device-group propagation. Portable onboarding requires native OpenShell GPU injection for every agent and does not use this setting. |

‎src/lib/actions/sandbox/destroy-flow.test.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,40 @@ describe("destroySandbox flow", () => {
446446
);
447447
});
448448

449+
it("releases lifecycle locks before exiting on Portable identity drift and permits retry", async () => {
450+
const harness = createDestroyHarness({ portableDestroyAuthority: true });
451+
const recordRevalidation = () => harness.events.push("portable-revalidate");
452+
const revalidateAtDeleteBoundary = vi.fn(recordRevalidation).mockImplementationOnce(() => {
453+
throw new Error("Portable receipt changed during destroy");
454+
});
455+
harness.portableDestroyRevalidateSpy.mockImplementation(() =>
456+
harness.events.at(-1) === "detach" ? revalidateAtDeleteBoundary() : recordRevalidation(),
457+
);
458+
exitSpy.mockImplementationOnce(((code?: number | string | null) => {
459+
harness.lifecycleLockEvents.push("process-exit");
460+
throw new Error(`process.exit(${code ?? 0})`);
461+
}) as never);
462+
463+
await expect(harness.destroySandbox("alpha", { yes: true })).rejects.toThrow("process.exit(1)");
464+
465+
expect(harness.lifecycleLockEvents).toEqual(["acquired", "released", "process-exit"]);
466+
expect(harness.events).not.toContain("delete");
467+
expect(harness.removeSandboxSpy).not.toHaveBeenCalled();
468+
expect(harness.retirePortableLifecycleReceiptSpy).not.toHaveBeenCalled();
469+
470+
await expect(harness.destroySandbox("alpha", { yes: true })).resolves.toBeUndefined();
471+
expect(harness.lifecycleLockEvents).toEqual([
472+
"acquired",
473+
"released",
474+
"process-exit",
475+
"acquired",
476+
"released",
477+
]);
478+
expect(harness.events.filter((event) => event === "delete")).toHaveLength(1);
479+
expect(harness.removeSandboxSpy).toHaveBeenCalledOnce();
480+
expect(harness.retirePortableLifecycleReceiptSpy).toHaveBeenCalledOnce();
481+
});
482+
449483
it("redacts credentials from an invalid schema-4 Portable receipt refusal and releases the lifecycle lock before retry (#9189)", async () => {
450484
const harness = createDestroyHarness({
451485
portableDestroyPrepareError: "portable receipt API_KEY=opaque-portable-secret is invalid",

‎src/lib/actions/sandbox/destroy.ts‎

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -464,14 +464,30 @@ export type { WipeSandboxStateDeps };
464464
// the wipe was extracted out of the destroy monolith (#5455 PRA-2).
465465
export { wipeSandboxState };
466466

467+
class SandboxDestroyExitRequest extends Error {
468+
constructor(readonly exitCode: number) {
469+
super(`Sandbox destroy requested exit ${String(exitCode)}`);
470+
this.name = "SandboxDestroyExitRequest";
471+
}
472+
}
473+
474+
function requestSandboxDestroyExit(exitCode: number): never {
475+
throw new SandboxDestroyExitRequest(exitCode);
476+
}
477+
467478
export async function destroySandbox(
468479
sandboxName: string,
469480
options: string[] | DestroySandboxOptions = {},
470481
): Promise<void> {
471-
return withMcpLifecycleLock(sandboxName, () => {
472-
assertSandboxDestroyCommandAvailable(sandboxName);
473-
return destroySandboxUnlocked(sandboxName, options);
474-
});
482+
try {
483+
return await withMcpLifecycleLock(sandboxName, () => {
484+
assertSandboxDestroyCommandAvailable(sandboxName);
485+
return destroySandboxUnlocked(sandboxName, options);
486+
});
487+
} catch (error) {
488+
if (error instanceof SandboxDestroyExitRequest) process.exit(error.exitCode);
489+
throw error;
490+
}
475491
}
476492

477493
async function destroySandboxUnlocked(
@@ -509,7 +525,7 @@ async function destroySandboxUnlocked(
509525
});
510526
const initialIdentity = portableContainerAuthority ? null : inspectContainerIdentity();
511527
if (initialIdentity === false) {
512-
process.exit(1);
528+
requestSandboxDestroyExit(1);
513529
}
514530

515531
const registeredSandbox = registry.getSandbox(sandboxName);
@@ -553,7 +569,7 @@ async function destroySandboxUnlocked(
553569
console.error(
554570
` The sandbox was not deleted. Resolve the reported cleanup preflight failure and retry destroy.`,
555571
);
556-
process.exit(1);
572+
requestSandboxDestroyExit(1);
557573
}
558574
}
559575
const abortPreparedCleanupOnError = <T>(operation: () => T): T => {
@@ -576,7 +592,7 @@ async function destroySandboxUnlocked(
576592
console.error(
577593
` Refusing to destroy sandbox '${sandboxName}': NemoClaw could not revalidate Portable container identity during preflight: ${redactDestroyError(error)}. NemoClaw removed no sandbox resources.`,
578594
);
579-
process.exit(1);
595+
requestSandboxDestroyExit(1);
580596
}
581597
} else {
582598
const preMutationIdentity = abortPreparedCleanupOnError(inspectContainerIdentity);
@@ -590,7 +606,7 @@ async function destroySandboxUnlocked(
590606
);
591607
}
592608
preparedManagedLlamaCppCleanup?.abort();
593-
process.exit(1);
609+
requestSandboxDestroyExit(1);
594610
}
595611
}
596612
const priorHttpsPinRouteId = abortPreparedCleanupOnError(() =>
@@ -675,7 +691,7 @@ async function destroySandboxUnlocked(
675691
}
676692
}
677693
preparedManagedLlamaCppCleanup?.abort();
678-
process.exit(destructiveResult.exitCode);
694+
requestSandboxDestroyExit(destructiveResult.exitCode);
679695
}
680696
const {
681697
detachOutcome,
@@ -738,7 +754,7 @@ async function destroySandboxUnlocked(
738754
);
739755
console.error(" The sandbox registry entry was preserved so exact cleanup can be retried.");
740756
preparedManagedLlamaCppCleanup?.abort();
741-
process.exit(1);
757+
requestSandboxDestroyExit(1);
742758
}
743759
if (stateVolumeCleanup.status === "not-owned") {
744760
console.warn(
@@ -770,7 +786,7 @@ async function destroySandboxUnlocked(
770786
` Managed llama.cpp cleanup failed for '${sandboxName}': ${managedLlamaCppCleanup.reason}`,
771787
);
772788
console.error(" The sandbox registry entry was preserved so exact cleanup can be retried.");
773-
process.exit(1);
789+
requestSandboxDestroyExit(1);
774790
}
775791
}
776792
// The sandbox's gateway was captured before the registry entry is removed —
@@ -814,7 +830,7 @@ async function destroySandboxUnlocked(
814830
emitProviderDetachResidualHint(sandboxName, detachOutcome.failures, (message) =>
815831
console.warn(` ${YW}⚠${R}${message}`),
816832
);
817-
process.exit(1);
833+
requestSandboxDestroyExit(1);
818834
}
819835
if (removed) {
820836
try {

0 commit comments

Comments
 (0)