Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
2fc5c50
fix(uninstall): preserve failed llama.cpp cleanup state
prekshivyas Aug 19, 2026
edcb705
Merge remote-tracking branch 'origin/main' into codex/fix-9575-llama-…
prekshivyas Aug 19, 2026
6432af6
test(uninstall): preserve sibling model authority
prekshivyas Aug 20, 2026
e58cd1d
Merge remote-tracking branch 'origin/main' into codex/fix-9575-llama-…
prekshivyas Aug 20, 2026
b0c5291
test(inference): qualify vllm serving-port fixtures
prekshivyas Aug 20, 2026
870496b
test(uninstall): assert preserved llama ownership
prekshivyas Aug 20, 2026
78e300b
test(inference): qualify Spark onboarder fixtures
prekshivyas Aug 20, 2026
021e380
Merge branch 'main' into codex/fix-9575-llama-cpp-uninstall
prekshivyas Aug 20, 2026
3cc81e4
test(package): allow full CLI docs crawl
prekshivyas Aug 20, 2026
235370b
Merge branch 'codex/fix-9575-llama-cpp-uninstall' of https://github.c…
prekshivyas Aug 20, 2026
e2ede40
Merge remote-tracking branch 'origin/main' into codex/fix-9575-llama-…
prekshivyas Aug 20, 2026
e0501c8
fix(inference): lease llama bridge cleanup
prekshivyas Aug 20, 2026
c1bc00c
fix(ci): compile catalog for CPU delegation proof
prekshivyas Aug 20, 2026
af50bee
Merge remote-tracking branch 'origin/main' into codex/fix-9575-llama-…
prekshivyas Aug 20, 2026
6a7c509
test(ci): avoid shape-locking proof workflow
prekshivyas Aug 20, 2026
e808d7d
Merge remote-tracking branch 'origin/main' into codex/fix-9575-llama-…
prekshivyas Aug 20, 2026
fbfe522
fix(uninstall): preserve model stores after cleanup failure
senthilr-nv Aug 20, 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
3 changes: 3 additions & 0 deletions .github/workflows/podman-cpu-proof.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ jobs:
- name: Build shared sandbox-name contract
run: npm run build:policy-boundary

- name: Compile managed inference catalog
run: npm run catalog:compile

- name: Prepare system and app slice CPU settings without service delegation
shell: bash
run: node --experimental-strip-types scripts/checks/run-portable-cpu-delegation-proof.mts prepare
Expand Down
70 changes: 49 additions & 21 deletions src/lib/actions/uninstall/run-plan-local-model-profile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "../../../../test/support/uninstall-managed-gateway-test-support";

import {
loadManagedLlamaCppOwner,
managedLlamaCppStatePaths,
reserveManagedLlamaCppOwner,
} from "../../inference/llama-cpp/managed-state";
Expand Down Expand Up @@ -102,13 +103,7 @@ function publishManagedLlamaOwner(
}

function writeScopedGatewayState(home: string): void {
const stateDir = path.join(
home,
".local",
"state",
"nemoclaw",
"openshell-docker-gateway",
);
const stateDir = path.join(home, ".local", "state", "nemoclaw", "openshell-docker-gateway");
const jwtBundle = ensureDockerDriverGatewayJwtBundle(stateDir);
fs.writeFileSync(
path.join(stateDir, "openshell-gateway.toml"),
Expand Down Expand Up @@ -442,10 +437,12 @@ describe("uninstall local model profile cleanup", () => {
);

expect(result.exitCode).toBe(0);
run.mock.calls.filter(([command]) => command === "ollama").forEach(([command, , options]) => {
expect(command).toBe("ollama");
expect(options?.env?.OLLAMA_HOST).toBe("127.0.0.1:11434");
});
run.mock.calls
.filter(([command]) => command === "ollama")
.forEach(([command, , options]) => {
expect(command).toBe("ollama");
expect(options?.env?.OLLAMA_HOST).toBe("127.0.0.1:11434");
});
});

it("fails without deleting any Ollama model when inventory is malformed", () => {
Expand Down Expand Up @@ -620,36 +617,67 @@ describe("uninstall local model profile cleanup", () => {
}
});

it("preserves selected gateway authority when scoped cleanup leaves ownership state", () => {
it("continues unrelated uninstall after managed llama.cpp cleanup fails (#9575)", () => {
const tmpHome = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-uninstall-llama-fail-")),
);
const stateDir = publishManagedLlamaOwner(tmpHome, 8080, "selected-sandbox");
const siblingStateDir = publishManagedLlamaOwner(tmpHome, 9000, "sibling-sandbox");
const selectedPaths = managedLlamaCppStatePaths(tmpHome, 8080);
const siblingPaths = managedLlamaCppStatePaths(tmpHome, 9000);
const selectedOwnerBefore = loadManagedLlamaCppOwner(selectedPaths);
const siblingOwnerBefore = loadManagedLlamaCppOwner(siblingPaths);
writeScopedGatewayState(tmpHome);
const unrelatedState = path.join(tmpHome, ".nemoclaw", "unrelated-state.json");
const cacheDir = path.join(tmpHome, ".cache", "huggingface");
fs.mkdirSync(cacheDir, { recursive: true });
fs.writeFileSync(unrelatedState, "{}\n", { mode: 0o600 });
const errors: string[] = [];
const logs: string[] = [];
const runLocalModelRuntimeCleanup = vi.fn(() => ok());
const runHuggingFaceCacheDataCleanup = vi.fn(() => ok());
const runManagedLlamaCppRuntimeCleanup = vi.fn(() => ({
status: 1,
stdout: "",
stderr: "qualified endpoint changed",
}));
try {
const result = runUninstallPlan(
{ assumeYes: true, deleteModels: false, keepOpenShell: true },
{ assumeYes: true, deleteModels: true, keepOpenShell: true },
withProvenManagedGatewayProcess({
commandExists: (command) => command === "openshell",
env: { HOME: tmpHome } as NodeJS.ProcessEnv,
error: (message) => errors.push(message),
existsSync: fs.existsSync,
isPortFree: () => true,
isTty: false,
log: () => {},
run: vi.fn((command: string, args: string[]) =>
command === "openshell" && args[0] === "gateway" && args[1] === "list"
? ok(JSON.stringify([{ name: "nemoclaw" }, { name: "nemoclaw-9000" }]))
: ok(),
),
runManagedLlamaCppRuntimeCleanup: vi.fn(() => ok()),
log: (message) => logs.push(message),
run: vi.fn(okWithKnownGatewayList),
runHuggingFaceCacheDataCleanup,
runLocalModelRuntimeCleanup,
runManagedLlamaCppRuntimeCleanup,
}),
);

expect(result.exitCode).toBe(1);
expect(runManagedLlamaCppRuntimeCleanup).toHaveBeenCalledExactlyOnceWith(
"selected-sandbox",
8080,
);
expect(fs.existsSync(stateDir)).toBe(true);
expect(errors.join("\n")).toContain("returned without retiring its ownership state");
expect(fs.existsSync(path.join(stateDir, "owner.json"))).toBe(true);
expect(fs.existsSync(path.join(siblingStateDir, "owner.json"))).toBe(true);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
expect(loadManagedLlamaCppOwner(selectedPaths)).toEqual(selectedOwnerBefore);
expect(loadManagedLlamaCppOwner(siblingPaths)).toEqual(siblingOwnerBefore);
expect(fs.existsSync(unrelatedState)).toBe(false);
expect(fs.existsSync(cacheDir)).toBe(true);
expect(runHuggingFaceCacheDataCleanup).not.toHaveBeenCalled();
expect(runLocalModelRuntimeCleanup).not.toHaveBeenCalled();
expect(logs).toContain(
"Managed llama.cpp cleanup did not complete. NemoClaw kept model stores for retry.",
);
expect(logs.some((message) => message.endsWith("State and binaries"))).toBe(true);
expect(errors.join("\n")).toContain("continue unrelated uninstall steps");
} finally {
fs.rmSync(tmpHome, { recursive: true, force: true });
}
Expand Down
88 changes: 70 additions & 18 deletions src/lib/actions/uninstall/run-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,7 @@ function buildRuntime(deps: UninstallRunDeps): UninstallRuntime {
deps.runManagedLlamaCppRuntimeCleanup ??
((sandboxName, gatewayPort) => {
const result = cleanupManagedLlamaCppRuntimeForSandbox(sandboxName, {
env,
gatewayPort,
homeDir: env.HOME || os.homedir(),
});
Expand Down Expand Up @@ -2027,49 +2028,59 @@ function managedLlamaCppCleanupTargets(
}
}

interface ManagedLlamaCppCleanupOutcome {
readonly failedStateDirs: readonly string[];
readonly ok: boolean;
}

function removeManagedLlamaCppRuntimes(
runtime: UninstallRuntime,
scopedToSelectedGateway: boolean,
): boolean {
): ManagedLlamaCppCleanupOutcome {
const targets = managedLlamaCppCleanupTargets(runtime, scopedToSelectedGateway);
if (targets === null) return false;
if (targets === null) return { failedStateDirs: [], ok: false };
for (const target of targets) {
const result = runtime.runManagedLlamaCppRuntimeCleanup(target.sandboxName, target.gatewayPort);
for (const removed of splitNonEmptyLines(result.stdout)) runtime.log(`Removed ${removed}`);
if (result.status !== 0) {
runtime.error(
`Managed llama.cpp cleanup for sandbox '${target.sandboxName}' on gateway port ${String(target.gatewayPort)} did not complete: ${result.stderr.trim() || "unknown cleanup error"}. NemoClaw did not start the remaining uninstall steps.`,
`Managed llama.cpp cleanup for sandbox '${target.sandboxName}' on gateway port ${String(target.gatewayPort)} did not complete: ${result.stderr.trim() || "unknown cleanup error"}. NemoClaw will preserve its ownership state and continue unrelated uninstall steps.`,
);
return false;
return { failedStateDirs: [target.stateDir], ok: true };
}
if (fs.lstatSync(target.stateDir, { throwIfNoEntry: false }) !== undefined) {
runtime.error(
`Managed llama.cpp cleanup for sandbox '${target.sandboxName}' on gateway port ${String(target.gatewayPort)} returned without retiring its ownership state. NemoClaw did not start the remaining uninstall steps.`,
`Managed llama.cpp cleanup for sandbox '${target.sandboxName}' on gateway port ${String(target.gatewayPort)} returned without retiring its ownership state. NemoClaw will preserve its ownership state and continue unrelated uninstall steps.`,
);
return false;
return { failedStateDirs: [target.stateDir], ok: true };
}
}
return true;
return { failedStateDirs: [], ok: true };
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

function removeManagedModelRuntimes(
paths: UninstallPaths,
runtime: UninstallRuntime,
scopedToSelectedGateway: boolean,
): boolean {
if (!removeManagedLlamaCppRuntimes(runtime, scopedToSelectedGateway)) return false;
if (scopedToSelectedGateway) return true;
): ManagedLlamaCppCleanupOutcome {
const llama = removeManagedLlamaCppRuntimes(runtime, scopedToSelectedGateway);
if (!llama.ok || llama.failedStateDirs.length > 0) return llama;
if (scopedToSelectedGateway) return llama;
const sharedRoot = path.dirname(paths.managedSwapMarkerPath);
const hasDistributedReceipt = [
MANAGED_CLUSTER_VLLM_RUNTIME_RECEIPT_FILE,
DUAL_STATION_VLLM_RUNTIME_RECEIPT_FILE,
].some((name) => runtime.existsSync(path.join(sharedRoot, name)));
if (!removeManagedDistributedVllmRuntime(paths, runtime, !hasDistributedReceipt)) return false;
if (!removeHostLocalModelRuntimes(paths, runtime)) return false;
if (!removeManagedDistributedVllmRuntime(paths, runtime, !hasDistributedReceipt)) {
return { failedStateDirs: [], ok: false };
}
if (!removeHostLocalModelRuntimes(paths, runtime)) {
return { failedStateDirs: [], ok: false };
}
if (!hasDistributedReceipt) {
removePath(path.join(sharedRoot, MANAGED_VLLM_API_KEY_FILE), runtime);
}
if (!runtime.commandExists("docker")) return true;
if (!runtime.commandExists("docker")) return llama;
const inventory = runtime.runDocker(["ps", "-a", "--format", "{{.Names}}"], {
env: runtime.env,
timeout: 10_000,
Expand All @@ -2078,16 +2089,29 @@ function removeManagedModelRuntimes(
runtime.error(
"Docker could not inventory reserved managed inference container names. NemoClaw refused the remaining uninstall steps so it cannot report incomplete cleanup as success.",
);
return false;
return { failedStateDirs: [], ok: false };
}
const residual = splitNonEmptyLines(inventory.stdout).find((name) =>
MANAGED_INFERENCE_CONTAINER_NAME_PATTERN.test(name),
);
if (!residual) return true;
if (!residual) return llama;
runtime.error(
`Managed inference container '${residual}' remains after ownership-aware cleanup. NemoClaw refused the remaining uninstall steps; restore its ownership state or remove it after manual review, then retry.`,
);
return false;
return { failedStateDirs: [], ok: false };
}

function recordManagedModelCleanup(
paths: UninstallPaths,
runtime: UninstallRuntime,
scopedToSelectedGateway: boolean,
failedStateDirs: string[],
onPartialFailure: () => void,
): boolean {
const result = removeManagedModelRuntimes(paths, runtime, scopedToSelectedGateway);
failedStateDirs.push(...result.failedStateDirs);
if (result.failedStateDirs.length > 0) onPartialFailure();
return result.ok;
}

function removeDockerContainers(runtime: UninstallRuntime, gatewayName?: string): void {
Expand Down Expand Up @@ -2279,7 +2303,12 @@ function removeHostModelStores(
options: UninstallRunOptions,
runtime: UninstallRuntime,
scopedToSelectedGateway: boolean,
preserveForFailedLlamaCleanup: boolean,
): boolean {
if (preserveForFailedLlamaCleanup) {
runtime.log("Managed llama.cpp cleanup did not complete. NemoClaw kept model stores for retry.");
return true;
}
if (scopedToSelectedGateway) {
runtime.log(
"Sibling gateways remain; kept host-shared Ollama models and the Hugging Face model cache.",
Expand Down Expand Up @@ -2752,6 +2781,7 @@ function executePlan(
return { ok: false };
}
let ok = true;
const failedManagedLlamaStateDirs: string[] = [];
const branding = runtimeBranding(runtime);
const preserveSharedOpenShell =
options.keepOpenShell || externallySupervised || portableRuntimeCleanup;
Expand Down Expand Up @@ -2793,7 +2823,15 @@ function executePlan(
if (step.name === "Stopping services") {
if (
!portableRuntimeCleanup &&
!removeManagedModelRuntimes(paths, runtime, scopedToSelectedGateway)
!recordManagedModelCleanup(
paths,
runtime,
scopedToSelectedGateway,
failedManagedLlamaStateDirs,
() => {
ok = false;
},
)
) {
return { ok: false };
}
Expand Down Expand Up @@ -2914,7 +2952,15 @@ function executePlan(
if (action.kind === "delete-docker-volume") removeDockerVolume(action.name, runtime);
}
} else if (step.name === "Model stores") {
if (!removeHostModelStores(paths, options, runtime, scopedToSelectedGateway)) {
if (
!removeHostModelStores(
paths,
options,
runtime,
scopedToSelectedGateway,
failedManagedLlamaStateDirs.length > 0,
)
) {
ok = false;
}
} else if (step.name === "State and binaries") {
Expand Down Expand Up @@ -2957,6 +3003,12 @@ function executePlan(
[
...preserveUnderStateDir,
...portableStateEntries,
...failedManagedLlamaStateDirs.flatMap((stateDir) => {
const relative = path.relative(paths.nemoclawStateDir, stateDir);
return relative && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)
? [relative.split(path.sep)[0]!]
: [];
}),
...(selectedIsDefault
? [GATEWAYS_SUBDIR, path.basename(paths.managedSwapMarkerPath)]
: []),
Expand Down
8 changes: 8 additions & 0 deletions src/lib/inference/local-model-profile/cleanup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,17 +582,21 @@ describe("host-local model cleanup", () => {
const homeDir = temporaryHome();
const original = engineHarness({ authorityId: "docker:original" });
const changed = engineHarness({ authorityId: "docker:changed" });
const privateBridge = privateBridgeFixture();
createManagedState(homeDir, original.engine);

const result = cleanupLocalModelRuntimes({
homeDir,
engine: changed.engine,
privateBridge,
});

expect(result).toMatchObject({
ok: false,
reason: expect.stringContaining("endpoint"),
});
expect(privateBridge.stopTransaction).toHaveBeenCalledExactlyOnceWith(TRANSACTION_ID);
expect(privateBridge.assertStopped).toHaveBeenCalledExactlyOnceWith(TRANSACTION_ID);
expect(changed.capture).not.toHaveBeenCalled();
expect(fs.existsSync(managedLlamaCppStatePaths(homeDir).stateDir)).toBe(true);
});
Expand Down Expand Up @@ -621,13 +625,15 @@ describe("host-local model cleanup", () => {
it("does not race cleanup against a live lifecycle execution lease", () => {
const homeDir = temporaryHome();
const harness = engineHarness();
const privateBridge = privateBridgeFixture();
createManagedState(homeDir, harness.engine, { phase: "started" });
const store = createHostLocalCreateJournalStore(managedLlamaCppStatePaths(homeDir).stateDir);
const lease = store.acquireExecution(TRANSACTION_ID);
try {
const result = cleanupLocalModelRuntimes({
homeDir,
engine: harness.engine,
privateBridge,
});

expect(result).toMatchObject({
Expand All @@ -638,6 +644,8 @@ describe("host-local model cleanup", () => {
["rm", "--force", RUNTIME_ID],
expect.any(Number),
);
expect(privateBridge.stopTransaction).not.toHaveBeenCalled();
expect(privateBridge.assertStopped).not.toHaveBeenCalled();
expect(fs.existsSync(managedLlamaCppStatePaths(homeDir).stateDir)).toBe(true);
} finally {
store.releaseExecution(lease);
Expand Down
Loading
Loading