From 326d0b315d8f23236f15b7edde459f923640d846 Mon Sep 17 00:00:00 2001 From: Dongni Yang Date: Tue, 28 Jul 2026 11:14:35 +0800 Subject: [PATCH 01/11] fix(cli): route sessions delete to the native Hermes command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sessions delete always refused on a Hermes sandbox: deleteSandboxSession validated an OpenClaw canonical session key and called the OpenClaw gateway admin RPC, which #7588 refuses for non-OpenClaw agents. Its sibling verbs already dispatch by agent kind — sessions list through the passthrough and sessions export through its own Hermes branch (#5526) — leaving delete as the one actionable verb with no route, even though Hermes ships a native `hermes sessions delete --yes` over its own session store. Route Hermes delete to that native command, mirroring sessions export. A Hermes branch at the top of deleteSandboxSession, before OpenClaw key validation, runs the native delete in-sandbox and takes a native Hermes session id as-is. The OpenClaw-only --agent (other than the hermes no-op alias), --keep-transcript, --json, and --verbose flags are refused rather than silently ignored. The native id is refused when it could be parsed as a flag. reset stays refused: Hermes has no native reset, and the refusal hint now also lists the available delete. Closes #7642 Signed-off-by: Dongni Yang --- docs/reference/commands.mdx | 10 ++ src/commands/sandbox/sessions/delete.ts | 5 + .../actions/sandbox/sessions/delete.test.ts | 98 ++++++++++++++++++- src/lib/actions/sandbox/sessions/delete.ts | 66 +++++++++++++ .../actions/sandbox/sessions/gateway-rpc.ts | 1 + test/sandbox-sessions-admin-agent-cli.test.ts | 68 ++++++++----- 6 files changed, 225 insertions(+), 23 deletions(-) diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index 261d5e4b8ff..6d01644a2c7 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -2474,6 +2474,16 @@ $$nemoclaw my-assistant sessions list $$nemoclaw my-assistant sessions list --source cli --limit 20 ``` +### `$$nemoclaw sessions delete ` + +Invoke `hermes sessions delete --yes` inside the sandbox to remove a session from the Hermes store. +Pass a native Hermes session id from `sessions list` (for example `20260727_130357_cb2b61`). +`--agent` accepts only `hermes` as a no-op alias and rejects any other value; the OpenClaw-only `--keep-transcript`, `--json`, and `--verbose` flags are not supported on a Hermes sandbox. + +```bash +$$nemoclaw my-assistant sessions delete 20260727_130357_cb2b61 +``` + diff --git a/src/commands/sandbox/sessions/delete.ts b/src/commands/sandbox/sessions/delete.ts index cad4b753a9c..81169875136 100644 --- a/src/commands/sandbox/sessions/delete.ts +++ b/src/commands/sandbox/sessions/delete.ts @@ -26,6 +26,11 @@ export default class SandboxSessionsDeleteCommand extends NemoClawCommand { "", "Pass --keep-transcript to retain the on-disk `.jsonl` after the", "session entry is removed.", + "", + "On a Hermes sandbox this routes to the native `hermes sessions delete ", + "--yes` and takes a native Hermes session id from `sessions list`. It accepts", + "--agent hermes as a no-op alias and refuses every other --agent value plus", + "--keep-transcript, --json, and --verbose.", ].join("\n"); static usage = [" [--agent ] [--keep-transcript] [--json] [--verbose]"]; static examples = [ diff --git a/src/lib/actions/sandbox/sessions/delete.test.ts b/src/lib/actions/sandbox/sessions/delete.test.ts index b83e553e6fe..eccf71d7712 100644 --- a/src/lib/actions/sandbox/sessions/delete.test.ts +++ b/src/lib/actions/sandbox/sessions/delete.test.ts @@ -11,12 +11,24 @@ vi.mock("./gateway-rpc", () => ({ callOpenclawGateway: vi.fn(), })); +vi.mock("../../../state/registry", () => ({ + getSandbox: vi.fn(() => null), +})); + +vi.mock("../exec", () => ({ + execSandbox: vi.fn(async () => undefined), +})); + +import * as registry from "../../../state/registry"; +import { execSandbox } from "../exec"; import { ensureLiveSandboxOrExit } from "../gateway-state"; -import { callOpenclawGateway } from "./gateway-rpc"; import { deleteSandboxSession } from "./delete"; +import { callOpenclawGateway } from "./gateway-rpc"; const ensureMock = ensureLiveSandboxOrExit as unknown as ReturnType; const gatewayMock = callOpenclawGateway as unknown as ReturnType; +const getSandboxMock = registry.getSandbox as unknown as ReturnType; +const execSandboxMock = execSandbox as unknown as ReturnType; function successResult(key: string, extra: { removedTranscript?: boolean; entry?: unknown } = {}) { const payload = { ok: true as const, key, ...extra }; @@ -35,6 +47,10 @@ let consoleLogSpy: ReturnType; beforeEach(() => { ensureMock.mockClear(); gatewayMock.mockReset(); + getSandboxMock.mockReset(); + getSandboxMock.mockReturnValue(null); + execSandboxMock.mockReset(); + execSandboxMock.mockResolvedValue(undefined); processExitSpy = vi.spyOn(process, "exit").mockImplementation((code?: number | string | null) => { throw new Error(`process.exit:${code ?? 0}`); }); @@ -163,3 +179,83 @@ describe("deleteSandboxSession", () => { expect(result.removedTranscript).toBe(false); }); }); + +describe("deleteSandboxSession (hermes sandbox)", () => { + beforeEach(() => { + getSandboxMock.mockReturnValue({ name: "sb-h", agent: "hermes" }); + }); + + it("routes to the native hermes sessions delete without the OpenClaw gateway (#7642)", async () => { + const result = await deleteSandboxSession("sb-h", { + key: "20260727_130357_cb2b61", + }); + + expect(gatewayMock).not.toHaveBeenCalled(); + expect(ensureMock).toHaveBeenCalledWith("sb-h", { allowNonReadyPhase: true }); + expect(execSandboxMock).toHaveBeenCalledWith("sb-h", [ + "hermes", + "sessions", + "delete", + "20260727_130357_cb2b61", + "--yes", + ]); + expect(result.key).toBe("20260727_130357_cb2b61"); + }); + + it("passes the native hermes session id through without OpenClaw canonicalization (#7642)", async () => { + await deleteSandboxSession("sb-h", { key: "20260727_121145_238595" }); + + expect(execSandboxMock.mock.calls[0]?.[1]).toContain("20260727_121145_238595"); + expect(execSandboxMock.mock.calls[0]?.[1]?.join(" ")).not.toContain("agent:"); + }); + + it("rejects the OpenClaw-only --agent flag on a hermes sandbox (#7642)", async () => { + await expect( + deleteSandboxSession("sb-h", { key: "20260727_130357_cb2b61", agent: "research" }), + ).rejects.toThrow(/process\.exit:1/); + + expect(execSandboxMock).not.toHaveBeenCalled(); + expect(consoleErrorSpy.mock.calls.flat().join("\n")).toMatch( + /--agent.*OpenClaw-only.*not supported on a Hermes sandbox/, + ); + }); + + it("rejects the OpenClaw-only --keep-transcript flag on a hermes sandbox (#7642)", async () => { + await expect( + deleteSandboxSession("sb-h", { key: "20260727_130357_cb2b61", keepTranscript: true }), + ).rejects.toThrow(/process\.exit:1/); + + expect(execSandboxMock).not.toHaveBeenCalled(); + expect(consoleErrorSpy.mock.calls.flat().join("\n")).toMatch( + /--keep-transcript.*OpenClaw-only.*not supported on a Hermes sandbox/, + ); + }); + + it("accepts --agent hermes as a no-op alias and still routes to the native command (#7642)", async () => { + await deleteSandboxSession("sb-h", { key: "20260727_130357_cb2b61", agent: "hermes" }); + + expect(execSandboxMock).toHaveBeenCalledWith("sb-h", [ + "hermes", + "sessions", + "delete", + "20260727_130357_cb2b61", + "--yes", + ]); + }); + + it("rejects the OpenClaw-only --json result output on a hermes sandbox (#7642)", async () => { + await expect( + deleteSandboxSession("sb-h", { key: "20260727_130357_cb2b61", json: true }), + ).rejects.toThrow(/process\.exit:1/); + + expect(execSandboxMock).not.toHaveBeenCalled(); + expect(consoleErrorSpy.mock.calls.flat().join("\n")).toMatch(/--json.*OpenClaw-only/); + }); + + it("rejects a hermes session id that could be parsed as a flag (#7642)", async () => { + await expect(deleteSandboxSession("sb-h", { key: "--yes" })).rejects.toThrow(/process\.exit:1/); + + expect(execSandboxMock).not.toHaveBeenCalled(); + expect(consoleErrorSpy.mock.calls.flat().join("\n")).toMatch(/session id/i); + }); +}); diff --git a/src/lib/actions/sandbox/sessions/delete.ts b/src/lib/actions/sandbox/sessions/delete.ts index 758fc03b5ef..fc1bfb98d2a 100644 --- a/src/lib/actions/sandbox/sessions/delete.ts +++ b/src/lib/actions/sandbox/sessions/delete.ts @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import * as registry from "../../../state/registry"; +import { execSandbox } from "../exec"; import { ensureLiveSandboxOrExit } from "../gateway-state"; import { callOpenclawGateway } from "./gateway-rpc"; import { @@ -37,6 +39,20 @@ export async function deleteSandboxSession( sandboxName: string, opts: SessionsDeleteOptions, ): Promise { + // Route by the sandbox's registered agent before OpenClaw key validation, + // the same dispatch `sessions export` uses (#5526). Hermes ships a native + // `hermes sessions delete` over its own SQLite store and neither exposes the + // OpenClaw gateway admin RPC nor accepts OpenClaw canonical `agent::` + // session keys. + // + // Trust boundary: `registry.getSandbox()` reads the host-side, user-owned + // sandbox registry; a sandbox process cannot change this agent selection. A + // sandbox with no registry entry, or with an `agent` other than `hermes`, + // keeps the OpenClaw path below. + if (registry.getSandbox(sandboxName)?.agent === "hermes") { + return deleteHermesSession(sandboxName, opts); + } + const requestedAgent = opts.agent ? validateAgentId(opts.agent) : null; const rawKey = validateSessionKey(opts.key); const keyAgent = parseAgentIdFromSessionKey(rawKey); @@ -97,3 +113,53 @@ export async function deleteSandboxSession( return { key: payload.key, removedTranscript, entry: payload.entry }; } + +// The `--agent hermes` no-op alias mirrors `sessions export`; every other +// OpenClaw-only flag is refused rather than silently ignored. +function rejectOpenClawOnlyDeleteOptions(opts: SessionsDeleteOptions): void { + if (opts.agent && opts.agent !== "hermes") { + console.error( + ` Refusing to delete: --agent ${opts.agent} is OpenClaw-only and is not supported on a Hermes sandbox. Pass --agent hermes or omit the flag.`, + ); + process.exit(1); + } + if (opts.keepTranscript === true) { + console.error( + " Refusing to delete: --keep-transcript is OpenClaw-only and is not supported on a Hermes sandbox. Hermes removes the session entry directly; omit the flag.", + ); + process.exit(1); + } + if (opts.json || opts.verbose) { + console.error( + " Refusing to delete: --json and --verbose print the OpenClaw gateway result and are OpenClaw-only; a Hermes sandbox streams the native command output. Omit the flags.", + ); + process.exit(1); + } +} + +// Accept the native Hermes id as-is but refuse a value that could be parsed as +// a flag by the in-sandbox command; the id is passed as its own argv element, +// so a leading dash or embedded whitespace is the only injection surface. +function validateHermesSessionId(rawKey: string): string { + const sessionId = rawKey.trim(); + if (sessionId === "" || sessionId.startsWith("-") || /\s/.test(sessionId)) { + console.error( + ` Refusing to delete: '${rawKey}' is not a valid Hermes session id. Pass a native id from \`sessions list\` (for example 20260727_130357_cb2b61).`, + ); + process.exit(1); + } + return sessionId; +} + +async function deleteHermesSession( + sandboxName: string, + opts: SessionsDeleteOptions, +): Promise { + rejectOpenClawOnlyDeleteOptions(opts); + const sessionId = validateHermesSessionId(opts.key); + + await ensureLiveSandboxOrExit(sandboxName, { allowNonReadyPhase: true }); + await execSandbox(sandboxName, ["hermes", "sessions", "delete", sessionId, "--yes"]); + + return { key: sessionId, removedTranscript: false }; +} diff --git a/src/lib/actions/sandbox/sessions/gateway-rpc.ts b/src/lib/actions/sandbox/sessions/gateway-rpc.ts index 65d689663e9..cab2701b2a6 100644 --- a/src/lib/actions/sandbox/sessions/gateway-rpc.ts +++ b/src/lib/actions/sandbox/sessions/gateway-rpc.ts @@ -186,6 +186,7 @@ function refuseUnsupportedSandboxAgent( console.error( ` Export a Hermes session with: ${cliName} ${sandboxName} sessions export `, ); + console.error(` Delete a Hermes session with: ${cliName} ${sandboxName} sessions delete `); } process.exit(1); } diff --git a/test/sandbox-sessions-admin-agent-cli.test.ts b/test/sandbox-sessions-admin-agent-cli.test.ts index faa8f6814b4..c2b7dba3473 100644 --- a/test/sandbox-sessions-admin-agent-cli.test.ts +++ b/test/sandbox-sessions-admin-agent-cli.test.ts @@ -39,30 +39,54 @@ function gatewayRpcCalls(logFile: string): string[] { } describe("sandbox sessions admin RPCs on a non-OpenClaw agent (#7587)", () => { - for (const verb of ["reset", "delete"] as const) { - it(`refuses \`sessions ${verb}\` on a hermes sandbox instead of dispatching the OpenClaw gateway RPC`, () => { - const home = fs.mkdtempSync(path.join(os.tmpdir(), `nemoclaw-cli-sessions-${verb}-hermes-`)); - try { - writeSandboxRegistry(home, "alpha", { agent: "hermes" }); - const openshellLog = path.join(home, "openshell-calls.log"); - const localBin = buildStubOpenshell(home, openshellLog); + it("refuses `sessions reset` on a hermes sandbox instead of dispatching the OpenClaw gateway RPC", () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-sessions-reset-hermes-")); + try { + writeSandboxRegistry(home, "alpha", { agent: "hermes" }); + const openshellLog = path.join(home, "openshell-calls.log"); + const localBin = buildStubOpenshell(home, openshellLog); - const result = runWithEnv(`alpha sessions ${verb} agent:main:main 2>&1`, { - HOME: home, - PATH: `${localBin}:${process.env.PATH || ""}`, - }); + const result = runWithEnv("alpha sessions reset agent:main:main 2>&1", { + HOME: home, + PATH: `${localBin}:${process.env.PATH || ""}`, + }); - expect(result.code).toBe(1); - expect(result.out).toContain(`Refusing to invoke 'sessions.${verb}' for sandbox 'alpha'`); - expect(result.out).toContain("it uses the 'hermes' agent"); - expect(result.out).toContain("alpha sessions list"); - expect(result.out).not.toContain("OPENCLAW_GATEWAY_TOKEN"); - expect(gatewayRpcCalls(openshellLog)).toEqual([]); - } finally { - fs.rmSync(home, { recursive: true, force: true }); - } - }); - } + expect(result.code).toBe(1); + expect(result.out).toContain("Refusing to invoke 'sessions.reset' for sandbox 'alpha'"); + expect(result.out).toContain("it uses the 'hermes' agent"); + expect(result.out).toContain("alpha sessions list"); + expect(result.out).not.toContain("OPENCLAW_GATEWAY_TOKEN"); + expect(gatewayRpcCalls(openshellLog)).toEqual([]); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + } + }); + + it("routes `sessions delete` on a hermes sandbox to the native command, not the OpenClaw gateway RPC (#7642)", () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-sessions-delete-hermes-")); + try { + writeSandboxRegistry(home, "alpha", { agent: "hermes" }); + const openshellLog = path.join(home, "openshell-calls.log"); + const localBin = buildStubOpenshell(home, openshellLog); + + const result = runWithEnv("alpha sessions delete 20260727_130357_cb2b61 2>&1", { + HOME: home, + PATH: `${localBin}:${process.env.PATH || ""}`, + }); + + expect(result.code).toBe(0); + expect(result.out).not.toContain("Refusing to invoke"); + expect(result.out).not.toContain("OPENCLAW_GATEWAY_TOKEN"); + expect(gatewayRpcCalls(openshellLog)).toEqual([]); + const nativeDeleteCalls = fs + .readFileSync(openshellLog, "utf8") + .split("\n") + .filter((line) => line.includes("hermes sessions delete 20260727_130357_cb2b61 --yes")); + expect(nativeDeleteCalls.length).toBe(1); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + } + }); it("still dispatches the gateway RPC when the registry records no agent", () => { const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-sessions-reset-default-")); From 034288e975f8da3b719db58396c534ea89815d24 Mon Sep 17 00:00:00 2001 From: Dongni Yang Date: Tue, 28 Jul 2026 11:31:59 +0800 Subject: [PATCH 02/11] fix(cli): make the Hermes delete branch a terminal contract execSandbox ends by calling process.exit with the native command's exit code, so deleteHermesSession never returned in production; the previous `return { key, removedTranscript: false }` was reachable only under the test's resolving execSandbox mock. Type the branch as Promise and end it after execSandbox, and model execSandbox's process-exit in the tests so the routing assertions no longer depend on an unreachable return value. Refs #7642 Signed-off-by: Dongni Yang --- .../actions/sandbox/sessions/delete.test.ts | 20 +++++++++++++------ src/lib/actions/sandbox/sessions/delete.ts | 8 +++++--- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/lib/actions/sandbox/sessions/delete.test.ts b/src/lib/actions/sandbox/sessions/delete.test.ts index eccf71d7712..c6e6a295b07 100644 --- a/src/lib/actions/sandbox/sessions/delete.test.ts +++ b/src/lib/actions/sandbox/sessions/delete.test.ts @@ -183,12 +183,17 @@ describe("deleteSandboxSession", () => { describe("deleteSandboxSession (hermes sandbox)", () => { beforeEach(() => { getSandboxMock.mockReturnValue({ name: "sb-h", agent: "hermes" }); + // execSandbox streams the native output and exits the process with its + // code; model that terminal behavior so the routing never returns a value. + execSandboxMock.mockImplementation(async () => { + process.exit(0); + }); }); it("routes to the native hermes sessions delete without the OpenClaw gateway (#7642)", async () => { - const result = await deleteSandboxSession("sb-h", { - key: "20260727_130357_cb2b61", - }); + await expect(deleteSandboxSession("sb-h", { key: "20260727_130357_cb2b61" })).rejects.toThrow( + /process\.exit:0/, + ); expect(gatewayMock).not.toHaveBeenCalled(); expect(ensureMock).toHaveBeenCalledWith("sb-h", { allowNonReadyPhase: true }); @@ -199,11 +204,12 @@ describe("deleteSandboxSession (hermes sandbox)", () => { "20260727_130357_cb2b61", "--yes", ]); - expect(result.key).toBe("20260727_130357_cb2b61"); }); it("passes the native hermes session id through without OpenClaw canonicalization (#7642)", async () => { - await deleteSandboxSession("sb-h", { key: "20260727_121145_238595" }); + await expect(deleteSandboxSession("sb-h", { key: "20260727_121145_238595" })).rejects.toThrow( + /process\.exit:0/, + ); expect(execSandboxMock.mock.calls[0]?.[1]).toContain("20260727_121145_238595"); expect(execSandboxMock.mock.calls[0]?.[1]?.join(" ")).not.toContain("agent:"); @@ -232,7 +238,9 @@ describe("deleteSandboxSession (hermes sandbox)", () => { }); it("accepts --agent hermes as a no-op alias and still routes to the native command (#7642)", async () => { - await deleteSandboxSession("sb-h", { key: "20260727_130357_cb2b61", agent: "hermes" }); + await expect( + deleteSandboxSession("sb-h", { key: "20260727_130357_cb2b61", agent: "hermes" }), + ).rejects.toThrow(/process\.exit:0/); expect(execSandboxMock).toHaveBeenCalledWith("sb-h", [ "hermes", diff --git a/src/lib/actions/sandbox/sessions/delete.ts b/src/lib/actions/sandbox/sessions/delete.ts index fc1bfb98d2a..010a4dae1ae 100644 --- a/src/lib/actions/sandbox/sessions/delete.ts +++ b/src/lib/actions/sandbox/sessions/delete.ts @@ -154,12 +154,14 @@ function validateHermesSessionId(rawKey: string): string { async function deleteHermesSession( sandboxName: string, opts: SessionsDeleteOptions, -): Promise { +): Promise { rejectOpenClawOnlyDeleteOptions(opts); const sessionId = validateHermesSessionId(opts.key); await ensureLiveSandboxOrExit(sandboxName, { allowNonReadyPhase: true }); + // execSandbox streams the native command output and exits the process with + // its exit code, so control never returns here and there is no NemoClaw-side + // result envelope to build (unlike the OpenClaw gateway path above). await execSandbox(sandboxName, ["hermes", "sessions", "delete", sessionId, "--yes"]); - - return { key: sessionId, removedTranscript: false }; + throw new Error("unreachable: execSandbox terminates the process"); } From 17fe863146c8f66a2f9f7c99c3ac2635020bee41 Mon Sep 17 00:00:00 2001 From: Dongni Yang Date: Tue, 28 Jul 2026 11:51:52 +0800 Subject: [PATCH 03/11] test(cli): cover every invalid Hermes session id branch validateHermesSessionId rejects empty, leading-dash, and whitespace-containing ids, but only the leading-dash branch was exercised. Parameterize the invalid-id test over a leading dash, an empty string, whitespace-only, and embedded whitespace so a future change cannot weaken the empty or whitespace branch without a regression failure. Refs #7642 Signed-off-by: Dongni Yang --- src/lib/actions/sandbox/sessions/delete.test.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/lib/actions/sandbox/sessions/delete.test.ts b/src/lib/actions/sandbox/sessions/delete.test.ts index c6e6a295b07..99d2a8883d3 100644 --- a/src/lib/actions/sandbox/sessions/delete.test.ts +++ b/src/lib/actions/sandbox/sessions/delete.test.ts @@ -260,8 +260,13 @@ describe("deleteSandboxSession (hermes sandbox)", () => { expect(consoleErrorSpy.mock.calls.flat().join("\n")).toMatch(/--json.*OpenClaw-only/); }); - it("rejects a hermes session id that could be parsed as a flag (#7642)", async () => { - await expect(deleteSandboxSession("sb-h", { key: "--yes" })).rejects.toThrow(/process\.exit:1/); + it.each([ + ["a leading dash that could parse as a flag", "--yes"], + ["an empty string", ""], + ["only whitespace", " "], + ["embedded whitespace", "2026 0727"], + ])("rejects an invalid hermes session id (%s) (#7642)", async (_case, key) => { + await expect(deleteSandboxSession("sb-h", { key })).rejects.toThrow(/process\.exit:1/); expect(execSandboxMock).not.toHaveBeenCalled(); expect(consoleErrorSpy.mock.calls.flat().join("\n")).toMatch(/session id/i); From 7909d2504015a32b0ca6c31ac3e0ef8f10c1b5cf Mon Sep 17 00:00:00 2001 From: Dongni Yang Date: Tue, 28 Jul 2026 12:11:50 +0800 Subject: [PATCH 04/11] test(cli): cover Hermes rejection of --verbose alongside --json rejectOpenClawOnlyDeleteOptions refuses --json or --verbose on a Hermes sandbox, but only the --json case was exercised. Parameterize the rejection test over both flags so a regression cannot let --verbose reach the native command. Refs #7642 Signed-off-by: Dongni Yang --- src/lib/actions/sandbox/sessions/delete.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/lib/actions/sandbox/sessions/delete.test.ts b/src/lib/actions/sandbox/sessions/delete.test.ts index 99d2a8883d3..a484df12647 100644 --- a/src/lib/actions/sandbox/sessions/delete.test.ts +++ b/src/lib/actions/sandbox/sessions/delete.test.ts @@ -251,13 +251,18 @@ describe("deleteSandboxSession (hermes sandbox)", () => { ]); }); - it("rejects the OpenClaw-only --json result output on a hermes sandbox (#7642)", async () => { + it.each([ + ["json", { json: true }], + ["verbose", { verbose: true }], + ])("rejects the OpenClaw-only --%s result output on a hermes sandbox (#7642)", async (_flag, extra) => { await expect( - deleteSandboxSession("sb-h", { key: "20260727_130357_cb2b61", json: true }), + deleteSandboxSession("sb-h", { key: "20260727_130357_cb2b61", ...extra }), ).rejects.toThrow(/process\.exit:1/); expect(execSandboxMock).not.toHaveBeenCalled(); - expect(consoleErrorSpy.mock.calls.flat().join("\n")).toMatch(/--json.*OpenClaw-only/); + expect(consoleErrorSpy.mock.calls.flat().join("\n")).toMatch( + /--json and --verbose.*OpenClaw-only/, + ); }); it.each([ From 2873383d413de10299aa8ce797932428bca7adc3 Mon Sep 17 00:00:00 2001 From: Dongni Yang Date: Tue, 28 Jul 2026 12:22:13 +0800 Subject: [PATCH 05/11] test(cli): cover native Hermes delete failure through the public CLI The Hermes delete integration test covered only a successful native delete. Add a stub-OpenShell case where `hermes sessions delete` exits nonzero and assert the public command returns that exit code and makes no gateway RPC, pinning execSandbox's exit-code propagation as the terminal contract for the Hermes branch. Refs #7642 Signed-off-by: Dongni Yang --- test/sandbox-sessions-admin-agent-cli.test.ts | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/test/sandbox-sessions-admin-agent-cli.test.ts b/test/sandbox-sessions-admin-agent-cli.test.ts index c2b7dba3473..3857a1c0a61 100644 --- a/test/sandbox-sessions-admin-agent-cli.test.ts +++ b/test/sandbox-sessions-admin-agent-cli.test.ts @@ -8,7 +8,7 @@ import { describe, expect, it } from "vitest"; import { runWithEnv, writeSandboxRegistry } from "./cli/helpers"; -function buildStubOpenshell(home: string, logFile: string): string { +function buildStubOpenshell(home: string, logFile: string, nativeDeleteExit = 0): string { const localBin = path.join(home, "bin"); fs.mkdirSync(localBin, { recursive: true }); fs.writeFileSync( @@ -23,6 +23,7 @@ function buildStubOpenshell(home: string, logFile: string): string { ' *"sandbox exec --name alpha -- bash -lc"*)', ` printf '%s\\n' '{"ok":true,"key":"agent:main:main","entry":null}'`, " exit 0 ;;", + ` *"hermes sessions delete"*) exit ${nativeDeleteExit} ;;`, " *) exit 0 ;;", "esac", ].join("\n"), @@ -88,6 +89,33 @@ describe("sandbox sessions admin RPCs on a non-OpenClaw agent (#7587)", () => { } }); + it("propagates a nonzero native hermes delete exit code and makes no gateway RPC (#7642)", () => { + const home = fs.mkdtempSync( + path.join(os.tmpdir(), "nemoclaw-cli-sessions-delete-hermes-fail-"), + ); + try { + writeSandboxRegistry(home, "alpha", { agent: "hermes" }); + const openshellLog = path.join(home, "openshell-calls.log"); + const localBin = buildStubOpenshell(home, openshellLog, 3); + + const result = runWithEnv("alpha sessions delete 20260727_130357_cb2b61 2>&1", { + HOME: home, + PATH: `${localBin}:${process.env.PATH || ""}`, + }); + + expect(result.code).toBe(3); + expect(result.out).not.toContain("Refusing to invoke"); + expect(gatewayRpcCalls(openshellLog)).toEqual([]); + const nativeDeleteCalls = fs + .readFileSync(openshellLog, "utf8") + .split("\n") + .filter((line) => line.includes("hermes sessions delete 20260727_130357_cb2b61 --yes")); + expect(nativeDeleteCalls.length).toBe(1); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + } + }); + it("still dispatches the gateway RPC when the registry records no agent", () => { const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-sessions-reset-default-")); try { From 30c01c51015de2528ad855ab798e524ff3407c34 Mon Sep 17 00:00:00 2001 From: Dongni Yang Date: Tue, 28 Jul 2026 12:27:46 +0800 Subject: [PATCH 06/11] test(cli): assert the Hermes delete failure path leaks no gateway token The native-failure test checked the exit code, refusal text, and RPC log but not the public output, so a regression that printed OPENCLAW_GATEWAY_TOKEN on the Hermes failure path could pass unnoticed. Assert its absence, matching the success test. Refs #7642 Signed-off-by: Dongni Yang --- test/sandbox-sessions-admin-agent-cli.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/sandbox-sessions-admin-agent-cli.test.ts b/test/sandbox-sessions-admin-agent-cli.test.ts index 3857a1c0a61..30225f1244f 100644 --- a/test/sandbox-sessions-admin-agent-cli.test.ts +++ b/test/sandbox-sessions-admin-agent-cli.test.ts @@ -105,6 +105,7 @@ describe("sandbox sessions admin RPCs on a non-OpenClaw agent (#7587)", () => { expect(result.code).toBe(3); expect(result.out).not.toContain("Refusing to invoke"); + expect(result.out).not.toContain("OPENCLAW_GATEWAY_TOKEN"); expect(gatewayRpcCalls(openshellLog)).toEqual([]); const nativeDeleteCalls = fs .readFileSync(openshellLog, "utf8") From 9ae0cfb5591e218087883cc4d46c88c9430fc21c Mon Sep 17 00:00:00 2001 From: Senthil Ravichandran Date: Tue, 28 Jul 2026 09:25:37 -0700 Subject: [PATCH 07/11] refactor(cli): reuse sessions agent routing Refs #7642 Signed-off-by: Senthil Ravichandran --- src/lib/actions/sandbox/sessions/delete.test.ts | 16 ++++++---------- src/lib/actions/sandbox/sessions/delete.ts | 13 ++++++------- src/lib/actions/sandbox/sessions/gateway-rpc.ts | 11 +++++++++++ 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/lib/actions/sandbox/sessions/delete.test.ts b/src/lib/actions/sandbox/sessions/delete.test.ts index a484df12647..5b71744b9a2 100644 --- a/src/lib/actions/sandbox/sessions/delete.test.ts +++ b/src/lib/actions/sandbox/sessions/delete.test.ts @@ -9,25 +9,21 @@ vi.mock("../gateway-state", () => ({ vi.mock("./gateway-rpc", () => ({ callOpenclawGateway: vi.fn(), -})); - -vi.mock("../../../state/registry", () => ({ - getSandbox: vi.fn(() => null), + sandboxUsesHermesAgent: vi.fn(() => false), })); vi.mock("../exec", () => ({ execSandbox: vi.fn(async () => undefined), })); -import * as registry from "../../../state/registry"; import { execSandbox } from "../exec"; import { ensureLiveSandboxOrExit } from "../gateway-state"; import { deleteSandboxSession } from "./delete"; -import { callOpenclawGateway } from "./gateway-rpc"; +import { callOpenclawGateway, sandboxUsesHermesAgent } from "./gateway-rpc"; const ensureMock = ensureLiveSandboxOrExit as unknown as ReturnType; const gatewayMock = callOpenclawGateway as unknown as ReturnType; -const getSandboxMock = registry.getSandbox as unknown as ReturnType; +const hermesAgentMock = sandboxUsesHermesAgent as unknown as ReturnType; const execSandboxMock = execSandbox as unknown as ReturnType; function successResult(key: string, extra: { removedTranscript?: boolean; entry?: unknown } = {}) { @@ -47,8 +43,8 @@ let consoleLogSpy: ReturnType; beforeEach(() => { ensureMock.mockClear(); gatewayMock.mockReset(); - getSandboxMock.mockReset(); - getSandboxMock.mockReturnValue(null); + hermesAgentMock.mockReset(); + hermesAgentMock.mockReturnValue(false); execSandboxMock.mockReset(); execSandboxMock.mockResolvedValue(undefined); processExitSpy = vi.spyOn(process, "exit").mockImplementation((code?: number | string | null) => { @@ -182,7 +178,7 @@ describe("deleteSandboxSession", () => { describe("deleteSandboxSession (hermes sandbox)", () => { beforeEach(() => { - getSandboxMock.mockReturnValue({ name: "sb-h", agent: "hermes" }); + hermesAgentMock.mockReturnValue(true); // execSandbox streams the native output and exits the process with its // code; model that terminal behavior so the routing never returns a value. execSandboxMock.mockImplementation(async () => { diff --git a/src/lib/actions/sandbox/sessions/delete.ts b/src/lib/actions/sandbox/sessions/delete.ts index 010a4dae1ae..b0d61ac5513 100644 --- a/src/lib/actions/sandbox/sessions/delete.ts +++ b/src/lib/actions/sandbox/sessions/delete.ts @@ -1,10 +1,9 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import * as registry from "../../../state/registry"; import { execSandbox } from "../exec"; import { ensureLiveSandboxOrExit } from "../gateway-state"; -import { callOpenclawGateway } from "./gateway-rpc"; +import { callOpenclawGateway, sandboxUsesHermesAgent } from "./gateway-rpc"; import { buildCanonicalSessionKey, DEFAULT_AGENT_ID, @@ -45,11 +44,11 @@ export async function deleteSandboxSession( // OpenClaw gateway admin RPC nor accepts OpenClaw canonical `agent::` // session keys. // - // Trust boundary: `registry.getSandbox()` reads the host-side, user-owned - // sandbox registry; a sandbox process cannot change this agent selection. A - // sandbox with no registry entry, or with an `agent` other than `hermes`, - // keeps the OpenClaw path below. - if (registry.getSandbox(sandboxName)?.agent === "hermes") { + // Trust boundary: the routing helper reads the host-side, user-owned sandbox + // registry; a sandbox process cannot change this agent selection. A sandbox + // with no registry entry, or with an `agent` other than `hermes`, keeps the + // OpenClaw path below. + if (sandboxUsesHermesAgent(sandboxName)) { return deleteHermesSession(sandboxName, opts); } diff --git a/src/lib/actions/sandbox/sessions/gateway-rpc.ts b/src/lib/actions/sandbox/sessions/gateway-rpc.ts index cab2701b2a6..794b51ec060 100644 --- a/src/lib/actions/sandbox/sessions/gateway-rpc.ts +++ b/src/lib/actions/sandbox/sessions/gateway-rpc.ts @@ -32,6 +32,17 @@ const OPENCLAW_AGENT_ID = "openclaw"; const RETRYABLE_PAIRING_FAILURE = /scope upgrade pending|pairing required|device is not approved/i; +/** + * Return whether the host registry assigns the sandbox to Hermes. + * + * A missing entry or agent keeps the historical OpenClaw command path. The + * gateway call validates that default more strictly before using OpenClaw + * credentials. + */ +export function sandboxUsesHermesAgent(sandboxName: string): boolean { + return registry.getSandbox(sandboxName)?.agent === "hermes"; +} + // Source-boundary note for this SDK-backed admin RPC wrapper: // - Invalid state: `openclaw gateway call` currently acts like a sandbox-origin // CLI client and can create/pending a new device pairing request while From 3c6f4fedbc6ec2126b20188b3710c1d133192091 Mon Sep 17 00:00:00 2001 From: Senthil Ravichandran Date: Tue, 28 Jul 2026 09:35:08 -0700 Subject: [PATCH 08/11] docs(cli): clarify sessions delete routing Refs #7642 Signed-off-by: Senthil Ravichandran --- src/commands/sandbox/sessions/delete.ts | 2 +- src/lib/actions/sandbox/sessions/delete.test.ts | 2 +- src/lib/actions/sandbox/sessions/delete.ts | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/commands/sandbox/sessions/delete.ts b/src/commands/sandbox/sessions/delete.ts index 81169875136..e7f8ca084c6 100644 --- a/src/commands/sandbox/sessions/delete.ts +++ b/src/commands/sandbox/sessions/delete.ts @@ -10,7 +10,7 @@ import { sandboxNameArg } from "../../../lib/sandbox/command-support"; export default class SandboxSessionsDeleteCommand extends NemoClawCommand { static id = "sandbox:sessions:delete"; static strict = true; - static summary = "Delete an OpenClaw conversation session via the gateway"; + static summary = "Delete a conversation session from the sandbox"; static description = [ "Remove a session entry (and, by default, its transcript) by invoking the", "OpenClaw gateway `sessions.delete` RPC from inside the sandbox. The gateway", diff --git a/src/lib/actions/sandbox/sessions/delete.test.ts b/src/lib/actions/sandbox/sessions/delete.test.ts index 5b71744b9a2..c6ce633e834 100644 --- a/src/lib/actions/sandbox/sessions/delete.test.ts +++ b/src/lib/actions/sandbox/sessions/delete.test.ts @@ -250,7 +250,7 @@ describe("deleteSandboxSession (hermes sandbox)", () => { it.each([ ["json", { json: true }], ["verbose", { verbose: true }], - ])("rejects the OpenClaw-only --%s result output on a hermes sandbox (#7642)", async (_flag, extra) => { + ])("rejects the OpenClaw-only --%s flag on a hermes sandbox (#7642)", async (_flag, extra) => { await expect( deleteSandboxSession("sb-h", { key: "20260727_130357_cb2b61", ...extra }), ).rejects.toThrow(/process\.exit:1/); diff --git a/src/lib/actions/sandbox/sessions/delete.ts b/src/lib/actions/sandbox/sessions/delete.ts index b0d61ac5513..a82a48a5cde 100644 --- a/src/lib/actions/sandbox/sessions/delete.ts +++ b/src/lib/actions/sandbox/sessions/delete.ts @@ -136,9 +136,8 @@ function rejectOpenClawOnlyDeleteOptions(opts: SessionsDeleteOptions): void { } } -// Accept the native Hermes id as-is but refuse a value that could be parsed as -// a flag by the in-sandbox command; the id is passed as its own argv element, -// so a leading dash or embedded whitespace is the only injection surface. +// Reject a leading dash so Hermes cannot parse the id as a flag. Reject +// whitespace because native Hermes ids contain none. function validateHermesSessionId(rawKey: string): string { const sessionId = rawKey.trim(); if (sessionId === "" || sessionId.startsWith("-") || /\s/.test(sessionId)) { From 4504e3393ea4b62a333c002159a3e8cbd5d0b677 Mon Sep 17 00:00:00 2001 From: Senthil Ravichandran Date: Tue, 28 Jul 2026 10:52:00 -0700 Subject: [PATCH 09/11] test(e2e): allow matrix workflow integration startup Signed-off-by: Senthil Ravichandran --- test/e2e/support/e2e-report-to-pr-workflow-boundary.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/support/e2e-report-to-pr-workflow-boundary.test.ts b/test/e2e/support/e2e-report-to-pr-workflow-boundary.test.ts index a52440b09e3..69360e6cb9e 100644 --- a/test/e2e/support/e2e-report-to-pr-workflow-boundary.test.ts +++ b/test/e2e/support/e2e-report-to-pr-workflow-boundary.test.ts @@ -904,7 +904,7 @@ it("carries the generated planner matrix through the workflow output and PR repo } finally { fs.rmSync(directory, { force: true, recursive: true }); } -}); +}, 30_000); it("builds controller target matrices only from trusted runner mappings (#7031)", () => { const target = "ubuntu-repo-cloud-langchain-deepagents-code"; From 239173e996a7d2faf5198cc24265debeb39fbf92 Mon Sep 17 00:00:00 2001 From: Senthil Ravichandran Date: Tue, 28 Jul 2026 11:02:21 -0700 Subject: [PATCH 10/11] test(e2e): allow controller workflow startup Signed-off-by: Senthil Ravichandran --- test/e2e/support/e2e-report-to-pr-workflow-boundary.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/support/e2e-report-to-pr-workflow-boundary.test.ts b/test/e2e/support/e2e-report-to-pr-workflow-boundary.test.ts index 69360e6cb9e..748c7230114 100644 --- a/test/e2e/support/e2e-report-to-pr-workflow-boundary.test.ts +++ b/test/e2e/support/e2e-report-to-pr-workflow-boundary.test.ts @@ -925,7 +925,7 @@ it("builds controller target matrices only from trusted runner mappings (#7031)" "::error::PR E2E target is not approved by the trusted controller", ); expect(rejected.workflowOutput).toBe(""); -}); +}, 30_000); it("binds controller matrix IDs and runners to the trusted target selector (#7031)", () => { const target = "ubuntu-repo-cloud-langchain-deepagents-code"; @@ -973,7 +973,7 @@ it("binds controller matrix IDs and runners to the trusted target selector (#703 "::error::E2E planner matrix does not match controller-selected targets", ); expect(runnerInjected.workflowOutput).toBe(""); -}); +}, 30_000); it("requires the report-to-pr job to check out the trusted workflow revision", () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "e2e-workflow-")); From 117044bbfc249064f5a77b64509aa80b03d7f1ad Mon Sep 17 00:00:00 2001 From: Senthil Ravichandran Date: Tue, 28 Jul 2026 11:25:57 -0700 Subject: [PATCH 11/11] fix(cli): reject agent flag for Hermes session delete Signed-off-by: Senthil Ravichandran --- docs/reference/commands.mdx | 2 +- src/commands/sandbox/sessions/delete.ts | 5 ++-- .../actions/sandbox/sessions/delete.test.ts | 15 +++++------ src/lib/actions/sandbox/sessions/delete.ts | 7 +++-- test/sandbox-sessions-admin-agent-cli.test.ts | 26 +++++++++++++++++++ 5 files changed, 38 insertions(+), 17 deletions(-) diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index a33172e7f81..414eabe345f 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -2492,7 +2492,7 @@ $$nemoclaw my-assistant sessions list --source cli --limit 20 Invoke `hermes sessions delete --yes` inside the sandbox to remove a session from the Hermes store. Pass a native Hermes session id from `sessions list` (for example `20260727_130357_cb2b61`). -`--agent` accepts only `hermes` as a no-op alias and rejects any other value; the OpenClaw-only `--keep-transcript`, `--json`, and `--verbose` flags are not supported on a Hermes sandbox. +The OpenClaw-only `--agent`, `--keep-transcript`, `--json`, and `--verbose` flags are not supported on a Hermes sandbox. ```bash $$nemoclaw my-assistant sessions delete 20260727_130357_cb2b61 diff --git a/src/commands/sandbox/sessions/delete.ts b/src/commands/sandbox/sessions/delete.ts index e7f8ca084c6..73d0531a84e 100644 --- a/src/commands/sandbox/sessions/delete.ts +++ b/src/commands/sandbox/sessions/delete.ts @@ -28,9 +28,8 @@ export default class SandboxSessionsDeleteCommand extends NemoClawCommand { "session entry is removed.", "", "On a Hermes sandbox this routes to the native `hermes sessions delete ", - "--yes` and takes a native Hermes session id from `sessions list`. It accepts", - "--agent hermes as a no-op alias and refuses every other --agent value plus", - "--keep-transcript, --json, and --verbose.", + "--yes` and takes a native Hermes session id from `sessions list`. It refuses", + "the OpenClaw-only --agent, --keep-transcript, --json, and --verbose flags.", ].join("\n"); static usage = [" [--agent ] [--keep-transcript] [--json] [--verbose]"]; static examples = [ diff --git a/src/lib/actions/sandbox/sessions/delete.test.ts b/src/lib/actions/sandbox/sessions/delete.test.ts index c6ce633e834..b6c12b50d73 100644 --- a/src/lib/actions/sandbox/sessions/delete.test.ts +++ b/src/lib/actions/sandbox/sessions/delete.test.ts @@ -233,18 +233,15 @@ describe("deleteSandboxSession (hermes sandbox)", () => { ); }); - it("accepts --agent hermes as a no-op alias and still routes to the native command (#7642)", async () => { + it("rejects --agent hermes instead of silently ignoring the OpenClaw-only flag (#7642)", async () => { await expect( deleteSandboxSession("sb-h", { key: "20260727_130357_cb2b61", agent: "hermes" }), - ).rejects.toThrow(/process\.exit:0/); + ).rejects.toThrow(/process\.exit:1/); - expect(execSandboxMock).toHaveBeenCalledWith("sb-h", [ - "hermes", - "sessions", - "delete", - "20260727_130357_cb2b61", - "--yes", - ]); + expect(execSandboxMock).not.toHaveBeenCalled(); + expect(consoleErrorSpy.mock.calls.flat().join("\n")).toMatch( + /--agent hermes.*OpenClaw-only.*not supported on a Hermes sandbox/, + ); }); it.each([ diff --git a/src/lib/actions/sandbox/sessions/delete.ts b/src/lib/actions/sandbox/sessions/delete.ts index a82a48a5cde..a1b0b6a432d 100644 --- a/src/lib/actions/sandbox/sessions/delete.ts +++ b/src/lib/actions/sandbox/sessions/delete.ts @@ -113,12 +113,11 @@ export async function deleteSandboxSession( return { key: payload.key, removedTranscript, entry: payload.entry }; } -// The `--agent hermes` no-op alias mirrors `sessions export`; every other -// OpenClaw-only flag is refused rather than silently ignored. +// OpenClaw-only flags are refused rather than silently ignored. function rejectOpenClawOnlyDeleteOptions(opts: SessionsDeleteOptions): void { - if (opts.agent && opts.agent !== "hermes") { + if (opts.agent) { console.error( - ` Refusing to delete: --agent ${opts.agent} is OpenClaw-only and is not supported on a Hermes sandbox. Pass --agent hermes or omit the flag.`, + ` Refusing to delete: --agent ${opts.agent} is OpenClaw-only and is not supported on a Hermes sandbox. Omit the flag.`, ); process.exit(1); } diff --git a/test/sandbox-sessions-admin-agent-cli.test.ts b/test/sandbox-sessions-admin-agent-cli.test.ts index 30225f1244f..70adc9ff2b3 100644 --- a/test/sandbox-sessions-admin-agent-cli.test.ts +++ b/test/sandbox-sessions-admin-agent-cli.test.ts @@ -89,6 +89,32 @@ describe("sandbox sessions admin RPCs on a non-OpenClaw agent (#7587)", () => { } }); + it("rejects `--agent hermes` without invoking native delete or the gateway RPC (#7642)", () => { + const home = fs.mkdtempSync( + path.join(os.tmpdir(), "nemoclaw-cli-sessions-delete-hermes-agent-"), + ); + try { + writeSandboxRegistry(home, "alpha", { agent: "hermes" }); + const openshellLog = path.join(home, "openshell-calls.log"); + const localBin = buildStubOpenshell(home, openshellLog); + + const result = runWithEnv( + "alpha sessions delete 20260727_130357_cb2b61 --agent hermes 2>&1", + { + HOME: home, + PATH: `${localBin}:${process.env.PATH || ""}`, + }, + ); + + expect(result.code).toBe(1); + expect(result.out).toContain("--agent hermes is OpenClaw-only"); + expect(result.out).not.toContain("OPENCLAW_GATEWAY_TOKEN"); + expect(fs.existsSync(openshellLog)).toBe(false); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + } + }); + it("propagates a nonzero native hermes delete exit code and makes no gateway RPC (#7642)", () => { const home = fs.mkdtempSync( path.join(os.tmpdir(), "nemoclaw-cli-sessions-delete-hermes-fail-"),