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
62 changes: 62 additions & 0 deletions src/lib/registry-recovery-action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,68 @@ describe("recoverRegistryEntries (#2753 seed-time guard)", () => {
expect(result.sandboxes).toEqual([]);
});

it("preserves a persisted Hermes agent when the session re-seeds the same sandbox", async () => {
// A Hermes sandbox already in the registry must keep `agent: "hermes"`
// even when registry-recovery re-seeds from session metadata that has
// no agent field. Object.assign in updateSandbox would otherwise clobber
// the persisted agent to null, breaking rebuild-time agent resolution
// (state paths under /sandbox/.hermes-data versus /sandbox/.openclaw-data).
mockRegistryState.sandboxes["my-hermes"] = {
name: "my-hermes",
provider: "nvidia-prod",
model: "nvidia/nemotron-3-super-120b-a12b",
gpuEnabled: false,
policies: ["npm", "pypi"],
nimContainer: null,
agent: "hermes",
agentVersion: "2026.5.16",
};
vi.mocked(loadSession).mockReturnValue({
sandboxName: "my-hermes",
provider: "nvidia-prod",
model: "nvidia/nemotron-3-super-120b-a12b",
policyPresets: ["npm", "pypi"],
nimContainer: null,
agent: "hermes",
steps: {
sandbox: { status: "complete", startedAt: null, completedAt: null, error: null },
},
} as never);

await recoverRegistryEntries();

expect(mockRegistryState.sandboxes["my-hermes"]?.agent).toBe("hermes");
expect(mockRegistryState.sandboxes["my-hermes"]?.agentVersion).toBe("2026.5.16");
});

it("does not clobber a persisted agent when session metadata omits it", async () => {
// Defensive: even if a stale session has no `agent` field at all (older
// session format), recovery must not overwrite the persisted agent.
mockRegistryState.sandboxes["my-hermes"] = {
name: "my-hermes",
provider: "nvidia-prod",
model: "nvidia/nemotron-3-super-120b-a12b",
gpuEnabled: false,
policies: [],
nimContainer: null,
agent: "hermes",
};
vi.mocked(loadSession).mockReturnValue({
sandboxName: "my-hermes",
provider: "nvidia-prod",
model: "nvidia/nemotron-3-super-120b-a12b",
policyPresets: [],
nimContainer: null,
steps: {
sandbox: { status: "complete", startedAt: null, completedAt: null, error: null },
},
} as never);

await recoverRegistryEntries();

expect(mockRegistryState.sandboxes["my-hermes"]?.agent).toBe("hermes");
});

it("does not evict a registered sandbox even when its session step is incomplete (avoids false positives)", async () => {
// A user with a real registered sandbox alpha and a stale session that
// happens to record alpha with an incomplete sandbox step (e.g. a
Expand Down
13 changes: 11 additions & 2 deletions src/lib/registry-recovery-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function buildRecoveredSandboxEntry(
name: string,
metadata: RecoveredSandboxMetadata = {},
): SandboxEntry {
return {
const entry: SandboxEntry = {
name,
model: metadata.model || null,
provider: metadata.provider || null,
Expand All @@ -34,8 +34,16 @@ function buildRecoveredSandboxEntry(
? metadata.policyPresets
: [],
nimContainer: metadata.nimContainer || null,
agent: metadata.agent || null,
};
// Only assert `agent` when recovery actually knows it. Object.assign in
// updateSandbox would otherwise overwrite a persisted agent (e.g. "hermes")
// with null whenever the recovery seed has no source of truth — the live
// OpenShell gateway does not surface NemoClaw's agent type, and a session
// sandbox seed never set this field, so the existing entry must win.
if (metadata.agent !== undefined && metadata.agent !== null) {
entry.agent = metadata.agent;
}
return entry;
}

function upsertRecoveredSandbox(name: string, metadata: RecoveredSandboxMetadata = {}) {
Expand Down Expand Up @@ -109,6 +117,7 @@ function seedRecoveryMetadata(
provider: session.provider || null,
nimContainer: session.nimContainer || null,
policyPresets: session.policyPresets || null,
agent: session.agent || null,
}),
);
const sessionSandboxMissing = !current.sandboxes.some(
Expand Down
Loading