Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
8d028b8
feat(state): segregate host state root per gateway port
laitingsheng Jul 12, 2026
15bbe12
fix(state): keep gateway-port state root correct under real CLI runs
laitingsheng Jul 12, 2026
d560c34
Merge remote-tracking branch 'origin/main' into feat/3053-instance-st…
laitingsheng Jul 12, 2026
435c689
test(cli): prove port segregation in the gateway-conflict status test
laitingsheng Jul 12, 2026
e16b885
Merge remote-tracking branch 'origin/main' into feat/3053-instance-st…
laitingsheng Jul 13, 2026
4f56d39
refactor(state): pass GATEWAY_PORT explicitly to nemoclawStateRoot
laitingsheng Jul 13, 2026
5ed2008
fix(state): harden gateway-scoped lifecycle
prekshivyas Jul 13, 2026
618294b
test(state): use declarative symlink platform gate
prekshivyas Jul 13, 2026
9bac22d
fix(state): secure remaining port-scoped writers
prekshivyas Jul 13, 2026
a2fa5ba
test(state): close port-scoped consumer gaps
prekshivyas Jul 13, 2026
5f0a409
fix(state): report selected backup root
prekshivyas Jul 13, 2026
1abadcc
Merge remote-tracking branch 'origin/main' into codex/pr-6711-fixes
prekshivyas Jul 13, 2026
479c033
fix(state): reject symlinked model router roots
prekshivyas Jul 13, 2026
5fecf93
Merge remote-tracking branch 'origin/main' into codex/pr-6711-fixes
prekshivyas Jul 13, 2026
70c8d2d
test(e2e): assert port-scoped gateway registries
prekshivyas Jul 13, 2026
d2d7483
fix(state): close multi-gateway lifecycle gaps
prekshivyas Jul 13, 2026
3c5fb29
Merge branch 'main' of github.com:NVIDIA/NemoClaw into codex/pr-6711-…
prekshivyas Jul 13, 2026
4b11dd0
test(uninstall): model preservable state precisely
prekshivyas Jul 13, 2026
31a3b56
fix(uninstall): preserve legacy sibling gateways
prekshivyas Jul 13, 2026
741e816
fix(state): harden gateway migration and installer
prekshivyas Jul 13, 2026
00dba27
test(installer): keep symlink cases linear
prekshivyas Jul 13, 2026
1fda112
test(state): avoid external CLI in migration proof
prekshivyas Jul 13, 2026
17f9343
fix(state): close gateway migration safety gaps
prekshivyas Jul 13, 2026
91a5b01
fix(installer): fail closed on registry inspection
prekshivyas Jul 13, 2026
a4fe26f
test(state): harden cleanup regressions
prekshivyas Jul 13, 2026
b92e8ac
test(uninstall): use valid preserved registry fixture
prekshivyas Jul 13, 2026
d9532be
fix(uninstall): remove current gateway registration
prekshivyas Jul 13, 2026
895557d
Merge branch 'main' into feat/3053-instance-state-root
prekshivyas Jul 13, 2026
e66655c
test(uninstall): isolate gateway compatibility cases
prekshivyas Jul 13, 2026
7d00f98
docs(uninstall): clarify gateway selection
prekshivyas Jul 13, 2026
3c263c9
Merge remote-tracking branch 'origin/main' into codex/pr-6711-fixes
prekshivyas Jul 13, 2026
8a672bd
Merge remote-tracking branch 'origin/main' into codex/pr-6711-fixes
prekshivyas Jul 14, 2026
ada776f
fix(state): harden migration recovery paths
prekshivyas Jul 14, 2026
c1c4bf3
Merge remote-tracking branch 'origin/main' into codex/pr-6711-fixes
prekshivyas Jul 14, 2026
09682cc
fix(uninstall): migrate legacy state before planning
prekshivyas Jul 14, 2026
290da9b
Merge branch 'main' into feat/3053-instance-state-root
prekshivyas Jul 14, 2026
144ffa5
Merge remote-tracking branch 'origin/main' into feat/3053-instance-st…
laitingsheng Jul 14, 2026
1a6990e
docs: fix uninstall cleanup scope and split multi-sentence lines
laitingsheng Jul 14, 2026
bb4d2d0
Merge branch 'main' into feat/3053-instance-state-root
cv Jul 14, 2026
44d70ee
Merge remote-tracking branch 'origin/main' into feat/3053-instance-st…
laitingsheng Jul 15, 2026
354feda
docs: stop overstating final-gateway uninstall cleanup scope
laitingsheng Jul 15, 2026
0cba390
Merge remote-tracking branch 'origin/main' into feat/3053-instance-st…
laitingsheng Jul 15, 2026
71dab25
Merge branch 'main' into feat/3053-instance-state-root
cv Jul 15, 2026
eeeb935
Merge branch 'main' into feat/3053-instance-state-root
cv Jul 15, 2026
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
2 changes: 2 additions & 0 deletions docs/manage-sandboxes/lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,14 @@ When the default API port is already held by another sandbox, `$$nemoclaw onboar
<AgentOnly variant="openclaw,hermes">
If you intentionally run separate OpenShell gateways on the same host, set a different `NEMOCLAW_GATEWAY_PORT` before each onboarding run.
NemoClaw isolates the gateway name and local state by port so one port-specific gateway does not replace another.
A non-default `NEMOCLAW_GATEWAY_PORT` also gets its own host state root at `~/.nemoclaw/gateways/<port>/`, with a separate sandbox registry, credentials, and snapshots, so gateway-scoped state stays segregated while shared host-level files remain under `~/.nemoclaw/`. The default port keeps the shared `~/.nemoclaw/` location, and `$$nemoclaw uninstall` on one port leaves the other ports' state in place.
Gateway and dashboard cleanup is scoped by sandbox name and port.
A later onboarding run that uses a different `NEMOCLAW_GATEWAY_PORT` or `--control-ui-port` does not tear down the first sandbox's gateway or dashboard forward.
</AgentOnly>
<AgentOnly variant="deepagents">
If you intentionally run separate OpenShell gateways on the same host, set a different `NEMOCLAW_GATEWAY_PORT` before each onboarding run.
NemoClaw isolates the gateway name and local state by port so one port-specific gateway does not replace another.
A non-default `NEMOCLAW_GATEWAY_PORT` also gets its own host state root at `~/.nemoclaw/gateways/<port>/`, with a separate sandbox registry, credentials, and snapshots, so gateway-scoped state stays segregated while shared host-level files remain under `~/.nemoclaw/`. The default port keeps the shared `~/.nemoclaw/` location, and `nemo-deepagents uninstall` on one port leaves the other ports' state in place.
Gateway cleanup is scoped by sandbox name and port.
</AgentOnly>

Expand Down
3 changes: 2 additions & 1 deletion docs/reference/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,8 @@ NemoClaw keeps non-secret operator-facing state on the host rather than inside t

| Path | Purpose |
|---|---|
| `~/.nemoclaw/sandboxes.json` | Registered sandbox metadata, including the default sandbox selection. |
| `~/.nemoclaw/sandboxes.json` | Registered sandbox metadata for the default gateway port, including the default sandbox selection. |
| `~/.nemoclaw/gateways/<port>/` | Segregated host state root (its own registry, credentials, and snapshots) for a non-default `NEMOCLAW_GATEWAY_PORT`. The default gateway port uses the top-level `~/.nemoclaw/` location, so existing single-gateway hosts are unchanged. |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
<AgentOnly variant="openclaw">
| `~/.openclaw/openclaw.json` | Host OpenClaw configuration that NemoClaw snapshots or restores during migration flows. |
</AgentOnly>
Expand Down
87 changes: 87 additions & 0 deletions src/lib/actions/uninstall/run-plan-gateway-segregation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import fs from "node:fs";
import os from "node:os";
import path from "node:path";

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

import { type RunResult, runUninstallPlan } from "./run-plan";

function ok(stdout = ""): RunResult {
return { status: 0, stdout, stderr: "" };
}

describe("uninstall gateway-port segregation (#3053)", () => {
it("preserves the gateways/ subtree so uninstalling one environment leaves the others", () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-uninstall-gwpreserve-"));
try {
const stateDir = path.join(tmpHome, ".nemoclaw");
const otherEnv = path.join(stateDir, "gateways", "8091");
fs.mkdirSync(otherEnv, { recursive: true });
fs.writeFileSync(path.join(otherEnv, "sandboxes.json"), "[]");
fs.writeFileSync(path.join(stateDir, "sandboxes.json"), "[]");
const logs: string[] = [];
const result = runUninstallPlan(
{ assumeYes: true, deleteModels: false, keepOpenShell: true },
{
commandExists: () => false,
env: {
HOME: tmpHome,
NEMOCLAW_NON_INTERACTIVE: "",
NEMOCLAW_UNINSTALL_DESTROY_USER_DATA: "1",
} as NodeJS.ProcessEnv,
existsSync: (target) => target.startsWith(tmpHome) && fs.existsSync(target),
isTty: false,
log: (line) => logs.push(line),
run: vi.fn(() => ok()),
runDocker: () => ok(""),
},
);

expect(result.exitCode).toBe(0);
expect(fs.existsSync(path.join(otherEnv, "sandboxes.json"))).toBe(true);
expect(fs.existsSync(path.join(stateDir, "sandboxes.json"))).toBe(false);
expect(fs.existsSync(stateDir)).toBe(true);
} finally {
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});

it("keeps the host-shared /swapfile when other gateway-port environments remain", () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-uninstall-swap-"));
try {
const stateDir = path.join(tmpHome, ".nemoclaw");
fs.mkdirSync(path.join(stateDir, "gateways", "8091"), { recursive: true });
fs.writeFileSync(path.join(stateDir, "managed_swap"), "/swapfile");
const logs: string[] = [];
const runCalls: string[][] = [];
const result = runUninstallPlan(
{ assumeYes: true, deleteModels: false, keepOpenShell: true },
{
commandExists: (command) => command !== "docker" && command !== "pgrep",
env: { HOME: tmpHome, NEMOCLAW_NON_INTERACTIVE: "" } as NodeJS.ProcessEnv,
existsSync: (target) =>
target === "/swapfile" || (target.startsWith(tmpHome) && fs.existsSync(target)),
isTty: true,
log: (line) => logs.push(line),
rmSync: vi.fn(),
run: (_command, args) => {
runCalls.push(args);
return ok();
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
runDocker: () => ok(""),
},
);

expect(result.exitCode).toBe(0);
expect(logs).toContain(
"Other NemoClaw gateway-port environments remain; keeping the host-shared /swapfile.",
);
expect(runCalls.some((args) => args[0] === "swapoff")).toBe(false);
} finally {
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});
});
69 changes: 55 additions & 14 deletions src/lib/actions/uninstall/run-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,23 @@ import { dockerSpawnSync } from "../../adapters/docker/exec";
import { type AgentBranding, getAgentBranding } from "../../cli/branding";
import { isStdinTty, readLineFromStdin } from "../../core/stdin";
import { sleepMs } from "../../core/wait";
import {
defaultUninstallPaths,
NEMOCLAW_OLLAMA_MODELS,
NEMOCLAW_PROVIDERS,
type UninstallPaths,
} from "../../domain/uninstall/paths";
import {
gatewayDestroySkipMessage,
OPENSHELL_SANDBOXES_DELETE_SKIP_MESSAGE,
preservedRegistryUnrecoverableWarnings,
providerDeleteSkipMessage,
} from "../../domain/uninstall/messaging";
import {
defaultUninstallPaths,
NEMOCLAW_OLLAMA_MODELS,
NEMOCLAW_PROVIDERS,
type UninstallPaths,
} from "../../domain/uninstall/paths";
import { buildUninstallPlan, type UninstallPlan } from "../../domain/uninstall/plan";
import { stopHostGatewayProcesses } from "../../onboard/host-gateway-process";
import { isModelRouterCommandLineForPort } from "../../onboard/model-router-process";
import { stopStaleDashboardListeners } from "../../onboard/stale-gateway-cleanup";
import { GATEWAYS_SUBDIR } from "../../state/state-root";
import { stopOpenRouterRuntimeAdapter } from "./openrouter-runtime-adapter-cleanup";
import { classifyShimPath, type FileSystemDeps } from "./plan";

Expand Down Expand Up @@ -289,23 +290,38 @@ function printBye(runtime: UninstallRuntime): void {
runtime.log(branding.uninstallGoodbye);
}

function userDataDispositionLine(options: UninstallRunOptions, runtime: UninstallRuntime): string {
function stateDirDisplay(paths: UninstallPaths): string {
const sharedRoot = path.dirname(paths.managedSwapMarkerPath);
const home = path.dirname(sharedRoot);
return `~/${path.relative(home, paths.nemoclawStateDir)}`;
}

function userDataDispositionLine(
options: UninstallRunOptions,
runtime: UninstallRuntime,
paths: UninstallPaths,
): string {
const dir = stateDirDisplay(paths);
if (options.destroyUserData) {
return " · ~/.nemoclaw (removes rebuild-backups/, backups/, sandboxes.json: --destroy-user-data set)";
return ` · ${dir} (removes rebuild-backups/, backups/, sandboxes.json: --destroy-user-data set)`;
}
if (runtime.env.NEMOCLAW_UNINSTALL_DESTROY_USER_DATA === "1") {
return " · ~/.nemoclaw (removes rebuild-backups/, backups/, sandboxes.json: NEMOCLAW_UNINSTALL_DESTROY_USER_DATA=1)";
return ` · ${dir} (removes rebuild-backups/, backups/, sandboxes.json: NEMOCLAW_UNINSTALL_DESTROY_USER_DATA=1)`;
}
return " · ~/.nemoclaw (preserves rebuild-backups/, backups/, sandboxes.json by default)";
return ` · ${dir} (preserves rebuild-backups/, backups/, sandboxes.json by default)`;
}

function confirm(options: UninstallRunOptions, runtime: UninstallRuntime): boolean {
function confirm(
options: UninstallRunOptions,
runtime: UninstallRuntime,
paths: UninstallPaths,
): boolean {
const branding = runtimeBranding(runtime);
if (options.assumeYes) return true;
runtime.log("What will be removed:");
runtime.log(` · All OpenShell sandboxes, gateway, and ${branding.display} providers`);
runtime.log(" · Related Docker containers, images, and volumes");
runtime.log(userDataDispositionLine(options, runtime));
runtime.log(userDataDispositionLine(options, runtime, paths));
runtime.log(" · ~/.config/openshell ~/.config/nemoclaw");
runtime.log(` · Global ${branding.display} CLI (npm package: nemoclaw)`);
runtime.log(
Expand Down Expand Up @@ -793,6 +809,18 @@ function removeOllamaModels(options: UninstallRunOptions, runtime: UninstallRunt
}
}

function otherGatewayEnvironmentsRemain(paths: UninstallPaths, runtime: UninstallRuntime): boolean {
const sharedRoot = path.dirname(paths.managedSwapMarkerPath);
if (paths.nemoclawStateDir !== sharedRoot) return true;
const gatewaysDir = path.join(sharedRoot, "gateways");
if (!runtime.existsSync(gatewaysDir)) return false;
try {
return fs.readdirSync(gatewaysDir).length > 0;
} catch {
return false;
}
}

function removeManagedSwap(paths: UninstallPaths, runtime: UninstallRuntime): void {
if (!runtime.existsSync("/swapfile")) {
runtime.log("No /swapfile found; skipping swap cleanup.");
Expand All @@ -804,6 +832,12 @@ function removeManagedSwap(paths: UninstallPaths, runtime: UninstallRuntime): vo
);
return;
}
if (otherGatewayEnvironmentsRemain(paths, runtime)) {
runtime.log(
"Other NemoClaw gateway-port environments remain; keeping the host-shared /swapfile.",
);
return;
}
if (runtime.env.NEMOCLAW_NON_INTERACTIVE === "1" || !runtime.isTty) {
runtime.warn("Skipping swap cleanup in non-interactive mode (requires sudo).");
return;
Expand Down Expand Up @@ -943,7 +977,14 @@ function executePlan(
else
for (const target of paths.openshellInstallPaths)
removeFileWithOptionalSudo(target, runtime);
if (!removePathExcept(paths.nemoclawStateDir, preserveUnderStateDir, runtime)) ok = false;
if (
!removePathExcept(
paths.nemoclawStateDir,
[...preserveUnderStateDir, GATEWAYS_SUBDIR],
runtime,
)
)
ok = false;
removePath(paths.gatewayLocalStateDir, runtime);
removePath(paths.openshellConfigDir, runtime);
removePath(paths.nemoclawConfigDir, runtime);
Expand Down Expand Up @@ -980,7 +1021,7 @@ export function runUninstallPlan(
const runtime = buildRuntime(deps);
const { paths, plan } = buildRunPlan(options, { ...deps, env: runtime.env });
printBanner(runtime);
if (!confirm(options, runtime)) return { exitCode: 0, plan };
if (!confirm(options, runtime, paths)) return { exitCode: 0, plan };
const preserveUnderStateDir = resolvePreserveSet(paths, options, runtime);
const { ok } = executePlan(plan, paths, options, runtime, preserveUnderStateDir);
if (ok) {
Expand Down
5 changes: 3 additions & 2 deletions src/lib/credentials/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { isErrnoException } from "../core/errno";
import { createPromptActivityCleanup } from "../core/prompt-activity";
import { listMessagingCredentialMetadata } from "../messaging/channels";
import { rejectSymlinksOnPath } from "../state/config-io";
import { nemoclawStateRoot } from "../state/state-root";

const UNSAFE_HOME_PATHS = new Set(["/tmp", "/var/tmp", "/dev/shm", "/"]);

Expand Down Expand Up @@ -127,10 +128,10 @@ export function getCredsDir(): string {
const home = resolveHomeDir();
if (_cachedHome !== home) {
_cachedHome = home;
_credsDir = path.join(home, ".nemoclaw");
_credsDir = nemoclawStateRoot(home);
_legacyCredsFile = null;
}
return _credsDir || path.join(home, ".nemoclaw");
return _credsDir || nemoclawStateRoot(home);
}

/**
Expand Down
4 changes: 3 additions & 1 deletion src/lib/domain/uninstall/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import path from "node:path";

import { nemoclawStateRoot } from "../../state/state-root";

export const DEFAULT_GATEWAY_NAME = "nemoclaw";
export const NEMOCLAW_PROVIDERS = [
"nvidia-nim",
Expand Down Expand Up @@ -68,7 +70,7 @@ export function defaultUninstallPaths(options: UninstallPathOptions): UninstallP
binName,
path: path.join(options.home, ".local", "bin", binName),
})),
nemoclawStateDir: path.join(options.home, ".nemoclaw"),
nemoclawStateDir: nemoclawStateRoot(options.home),
gatewayLocalStateDir: path.join(options.home, ".local", "state", "nemoclaw"),
openshellConfigDir: path.join(options.home, ".config", "openshell"),
openshellInstallPaths: openshellInstallPathsForBinDirs(["/usr/local/bin", xdgBinHome]),
Expand Down
5 changes: 3 additions & 2 deletions src/lib/inference/local-adapter-lifecycle.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { spawn, type ChildProcess } from "node:child_process";
import { type ChildProcess, spawn } from "node:child_process";
import crypto from "node:crypto";
import fs from "node:fs";
import http from "node:http";
import os from "node:os";
import path from "node:path";

import { waitUntilAsync } from "../core/wait";
import { nemoclawStateRoot } from "../state/state-root";

export type JsonObject = Record<string, unknown>;

Expand All @@ -22,7 +23,7 @@ export type RunFn = (
options?: { ignoreError?: boolean; suppressOutput?: boolean },
) => unknown;

export const DEFAULT_LOCAL_ADAPTER_STATE_DIR = path.join(os.homedir(), ".nemoclaw");
export const DEFAULT_LOCAL_ADAPTER_STATE_DIR = nemoclawStateRoot(os.homedir());

export function ensureLocalAdapterStateDir(stateDir = DEFAULT_LOCAL_ADAPTER_STATE_DIR): void {
if (!fs.existsSync(stateDir)) {
Expand Down
3 changes: 2 additions & 1 deletion src/lib/inference/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { OLLAMA_PORT, OLLAMA_PROXY_PORT, VLLM_PORT } from "../core/ports";
import { sleepSeconds } from "../core/wait";
import { containerCanReachHostLoopback, isWsl } from "../platform";
import { type CaptureResult, runCapture, runCaptureEx, shellQuote } from "../runner";
import { nemoclawStateRoot } from "../state/state-root";
import { buildSubprocessEnv } from "../subprocess-env";
import { detectNvidiaPlatform } from "./nim";
import {
Expand Down Expand Up @@ -244,7 +245,7 @@ export interface LocalProviderHealthProbeOptions {
}

function defaultLoadOllamaProxyToken(): string | null {
const tokenPath = nodePath.join(os.homedir(), ".nemoclaw", "ollama-proxy-token");
const tokenPath = nodePath.join(nemoclawStateRoot(os.homedir()), "ollama-proxy-token");
try {
if (fs.existsSync(tokenPath)) {
const token = fs.readFileSync(tokenPath, "utf-8").trim();
Expand Down
4 changes: 2 additions & 2 deletions src/lib/onboard/docker-gpu-patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
dockerStart,
dockerStop,
} from "../adapters/docker";
import { nemoclawStateRoot } from "../state/state-root";
import { createDockerGpuDiagnosticRedactor } from "./docker-gpu-diagnostic-redaction";
import {
reconcileSupervisorReconnect,
Expand Down Expand Up @@ -1814,8 +1815,7 @@ export function collectDockerGpuPatchDiagnostics(
const d = depsWithDefaults(deps);
const now = d.now();
const dir = path.join(
d.homedir(),
".nemoclaw",
nemoclawStateRoot(d.homedir()),
"onboard-failures",
`${timestampForPath(now)}-${sanitizePathPart(sandboxName)}-docker-gpu-patch`,
);
Expand Down
5 changes: 3 additions & 2 deletions src/lib/onboard/model-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ROOT, run, runCapture } from "../runner";
import { hashCredential } from "../security/credential-hash";
import type { Session } from "../state/onboard-session";
import * as onboardSession from "../state/onboard-session";
import { nemoclawStateRoot } from "../state/state-root";
import { buildSubprocessEnv } from "../subprocess-env";
import { hydrateCredentialEnv } from "./credential-env";
import {
Expand All @@ -40,7 +41,7 @@ export {
const ROUTER_HEALTH_RETRIES = 15;
const ROUTER_HEALTH_INTERVAL_MS = 2000;
const MODEL_ROUTER_RELATIVE_DIR = path.join("nemoclaw-blueprint", "router", "llm-router");
const MODEL_ROUTER_VENV_DIR = path.join(os.homedir(), ".nemoclaw", "model-router-venv");
const MODEL_ROUTER_VENV_DIR = path.join(nemoclawStateRoot(os.homedir()), "model-router-venv");
export const DEFAULT_MODEL_ROUTER_CREDENTIAL_ENV = "NVIDIA_INFERENCE_API_KEY";

export type BlueprintRouterConfig = {
Expand Down Expand Up @@ -224,7 +225,7 @@ export async function startModelRouter(
blueprintDir,
routerCfg.pool_config_path || "router/pool-config.yaml",
);
const stateDir = path.join(deps.homeDir, ".nemoclaw", "state");
const stateDir = path.join(nemoclawStateRoot(deps.homeDir), "state");
const litellmConfigPath = path.join(stateDir, "litellm-proxy.yaml");

deps.mkdirSync(stateDir);
Expand Down
5 changes: 3 additions & 2 deletions src/lib/onboard/sandbox-create-failure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";

import { nemoclawStateRoot } from "../state/state-root";

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;
Expand Down Expand Up @@ -153,8 +155,7 @@ export function collectSandboxCreateFailureDiagnostics(
const homeDir = options.homeDir ?? os.homedir();
const now = options.now ?? new Date();
const dir = path.join(
homeDir,
".nemoclaw",
nemoclawStateRoot(homeDir),
"onboard-failures",
`${timestampForPath(now)}-${sanitizePathPart(sandboxName)}`,
);
Expand Down
Loading
Loading