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
13 changes: 6 additions & 7 deletions docs/reference/commands-nemohermes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1393,23 +1393,22 @@ nemohermes my-assistant agents delete work --force --json

### `nemohermes <name> sessions`

List OpenClaw conversation sessions in the sandbox.
With no subcommand the in-sandbox CLI lists stored sessions for the configured default agent.
NemoClaw invokes `openclaw sessions` via `openshell sandbox exec` and forwards OpenClaw flags verbatim, but filters default list output so internal `nemoclaw-onboard-warmup-*` sessions created during onboarding are hidden from user-facing output.
List Hermes conversation sessions in the sandbox.
NemoClaw invokes `hermes sessions list` via `openshell sandbox exec`, forwards native Hermes flags such as `--source` and `--limit`, and streams the output unchanged.

```bash
nemohermes my-assistant sessions
nemohermes my-assistant sessions --all-agents --json
nemohermes my-assistant sessions --source cli --limit 20
```

### `nemohermes <name> sessions list`

Invoke `openclaw sessions list` inside the sandbox.
NemoClaw forwards every flag the in-sandbox CLI accepts (`--agent`, `--all-agents`, `--active`, `--limit`, `--json`, `--store`, `--verbose`) and filters the resulting default table or JSON so internal `nemoclaw-onboard-warmup-*` sessions are hidden.
Invoke `hermes sessions list` inside the sandbox.
NemoClaw forwards native Hermes flags such as `--source` and `--limit` and streams the output unchanged.

```bash
nemohermes my-assistant sessions list
nemohermes my-assistant sessions list --agent work --json
nemohermes my-assistant sessions list --source cli --limit 20
```

### `nemohermes <name> sessions reset <key>`
Expand Down
30 changes: 30 additions & 0 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1765,6 +1765,8 @@ Pass `-f` / `--file <agents.yaml>` to point at the manifest; `--yes` confirms th

### `$$nemoclaw <name> sessions`

<AgentOnly variant="openclaw">

List OpenClaw conversation sessions in the sandbox.
With no subcommand the in-sandbox CLI lists stored sessions for the configured default agent.
NemoClaw invokes `openclaw sessions` via `openshell sandbox exec` and forwards OpenClaw flags verbatim, but filters default list output so internal `nemoclaw-onboard-warmup-*` sessions created during onboarding are hidden from user-facing output.
Expand All @@ -1774,8 +1776,23 @@ $$nemoclaw my-assistant sessions
$$nemoclaw my-assistant sessions --all-agents --json
```

</AgentOnly>
<AgentOnly variant="hermes">

List Hermes conversation sessions in the sandbox.
NemoClaw invokes `hermes sessions list` via `openshell sandbox exec`, forwards native Hermes flags such as `--source` and `--limit`, and streams the output unchanged.

```bash
$$nemoclaw my-assistant sessions
$$nemoclaw my-assistant sessions --source cli --limit 20
```

</AgentOnly>

### `$$nemoclaw <name> sessions list`

<AgentOnly variant="openclaw">

Invoke `openclaw sessions list` inside the sandbox.
NemoClaw forwards every flag the in-sandbox CLI accepts (`--agent`, `--all-agents`, `--active`, `--limit`, `--json`, `--store`, `--verbose`) and filters the resulting default table or JSON so internal `nemoclaw-onboard-warmup-*` sessions are hidden.

Expand All @@ -1784,6 +1801,19 @@ $$nemoclaw my-assistant sessions list
$$nemoclaw my-assistant sessions list --agent work --json
```

</AgentOnly>
<AgentOnly variant="hermes">

Invoke `hermes sessions list` inside the sandbox.
NemoClaw forwards native Hermes flags such as `--source` and `--limit` and streams the output unchanged.

```bash
$$nemoclaw my-assistant sessions list
$$nemoclaw my-assistant sessions list --source cli --limit 20
```

</AgentOnly>

### `$$nemoclaw <name> sessions reset <key>`

Archive a session and rebind its key to a fresh `sessionId` by invoking the OpenClaw gateway `sessions.reset` RPC inside the sandbox.
Expand Down
9 changes: 4 additions & 5 deletions src/commands/sandbox/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,13 @@ import { NemoClawCommand } from "../../lib/cli/nemoclaw-oclif-command";
export default class SandboxSessionsCommand extends NemoClawCommand {
static id = "sandbox:sessions";
static strict = false;
static summary = "List OpenClaw conversation sessions in a sandbox";
static summary = "List conversation sessions in a sandbox";
static description =
"Pass through to `openclaw sessions` in the sandbox. With no subcommand the in-sandbox CLI lists stored sessions for the configured default agent, hiding internal NemoClaw onboard warm-up sessions from default user-facing output. Additional OpenClaw flags are forwarded verbatim after the sandbox name.";
static usage = ["<name> [openclaw-sessions-flags...]"];
"Pass through to the sandbox agent's session-listing command (`openclaw sessions` for OpenClaw sandboxes, `hermes sessions list` for Hermes sandboxes). On OpenClaw sandboxes the in-sandbox CLI lists stored sessions for the configured default agent, and internal NemoClaw onboard warm-up sessions are hidden from default user-facing output; OpenClaw-specific flags are forwarded verbatim. Hermes sandboxes pass through their native output unchanged.";
static usage = ["<name> [sessions-flags...]"];
static examples = [
"<%= config.bin %> sandbox sessions alpha",
"<%= config.bin %> sandbox sessions alpha --all-agents",
"<%= config.bin %> sandbox sessions alpha --json",
"<%= config.bin %> sandbox sessions alpha --limit 20",
];

public async run(): Promise<void> {
Expand Down
8 changes: 4 additions & 4 deletions src/commands/sandbox/sessions/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import { NemoClawCommand } from "../../../lib/cli/nemoclaw-oclif-command";
export default class SandboxSessionsListCommand extends NemoClawCommand {
static id = "sandbox:sessions:list";
static strict = false;
static summary = "List OpenClaw conversation sessions in a sandbox";
static summary = "List conversation sessions in a sandbox";
static description =
"Pass through to `openclaw sessions list` in the sandbox, hiding internal NemoClaw onboard warm-up sessions from default user-facing output. All OpenClaw flags (--agent, --all-agents, --active, --limit, --json, --store, --verbose) are forwarded verbatim.";
static usage = ["<name> [openclaw-sessions-list-flags...]"];
"Pass through to the sandbox agent's `sessions list` command (`openclaw sessions list` for OpenClaw sandboxes, `hermes sessions list` for Hermes sandboxes). On OpenClaw sandboxes, internal NemoClaw onboard warm-up sessions are hidden from default user-facing output and OpenClaw flags (--agent, --all-agents, --active, --limit, --json, --store, --verbose) are forwarded verbatim. Hermes sandboxes pass through their native output unchanged.";
static usage = ["<name> [sessions-list-flags...]"];
static examples = [
"<%= config.bin %> sandbox sessions list alpha",
"<%= config.bin %> sandbox sessions list alpha --agent work --json",
"<%= config.bin %> sandbox sessions list alpha --limit 20",
];

public async run(): Promise<void> {
Expand Down
101 changes: 101 additions & 0 deletions src/lib/actions/sandbox/sessions/passthrough.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const captureMock = vi.hoisted(() => vi.fn());
const execMock = vi.hoisted(() => vi.fn(async () => {}));
const ensureLiveMock = vi.hoisted(() => vi.fn(async () => ({})));
const getSandboxMock = vi.hoisted(() => vi.fn(() => null as { agent?: string } | null));

vi.mock("../../../adapters/openshell/runtime", () => ({
captureOpenshell: captureMock,
Expand All @@ -15,11 +16,13 @@ vi.mock("../exec", async () => {
return { ...actual, execSandbox: execMock };
});
vi.mock("../gateway-state", () => ({ ensureLiveSandboxOrExit: ensureLiveMock }));
vi.mock("../../../state/registry", () => ({ getSandbox: getSandboxMock }));

import { WARMUP_SESSION_ID_PREFIX } from "../warmup-session";
import {
filterWarmupSessionsListJson,
filterWarmupSessionsListText,
printSessionsPassthroughHelp,
runSessionsPassthrough,
} from "./passthrough";

Expand Down Expand Up @@ -172,6 +175,42 @@ describe("filterWarmupSessionsListText", () => {
});
});

describe("printSessionsPassthroughHelp", () => {
let logSpy: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
});

afterEach(() => {
logSpy.mockRestore();
});

function capturedHelpText(): string {
return logSpy.mock.calls.map((call: unknown[]) => String(call[0] ?? "")).join("\n");
}

it("does not promise OpenClaw-only passthrough for the generic sessions command (#6247)", () => {
printSessionsPassthroughHelp();
const help = capturedHelpText();

expect(help).not.toMatch(/Pass-through to `openclaw sessions/i);
expect(help).toMatch(/openclaw/i);
expect(help).toMatch(/hermes sessions list/i);
// Warm-up filtering is documented as OpenClaw-specific, not universal.
expect(help).toMatch(/warm-up[^\n]*OpenClaw|OpenClaw[^\n]*warm-up/i);
});

it("scopes the list-verb help to per-agent binaries and OpenClaw-only filtering (#6247)", () => {
printSessionsPassthroughHelp("list");
const help = capturedHelpText();

expect(help).not.toMatch(/Pass-through to `openclaw sessions list/i);
expect(help).toMatch(/sessions list/);
expect(help).toMatch(/hermes/i);
});
});

describe("runSessionsPassthrough", () => {
let stdoutSpy: ReturnType<typeof vi.spyOn>;
let stderrSpy: ReturnType<typeof vi.spyOn>;
Expand All @@ -181,6 +220,8 @@ describe("runSessionsPassthrough", () => {
captureMock.mockReset();
execMock.mockClear();
ensureLiveMock.mockClear();
getSandboxMock.mockReset();
getSandboxMock.mockReturnValue(null);
stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
Expand Down Expand Up @@ -342,6 +383,66 @@ describe("runSessionsPassthrough", () => {
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("--limit"));
});

it("routes the bare command to `hermes sessions list` and skips warm-up filtering (#6247)", async () => {
getSandboxMock.mockReturnValue({ agent: "hermes" });

await runSessionsPassthrough("hermes", { extraArgs: [] });

expect(captureMock).not.toHaveBeenCalled();
expect(execMock).toHaveBeenCalledWith("hermes", ["hermes", "sessions", "list"]);
});

it("uses openclaw binary for openclaw-agent sandboxes (#6247)", async () => {
getSandboxMock.mockReturnValue({ agent: "openclaw" });
captureMock.mockReturnValueOnce({ status: 0, output: "Sessions listed: 0\n" });

await runSessionsPassthrough("alpha", { extraArgs: [] });

expect(execMock).not.toHaveBeenCalled();
expect(captureMock).toHaveBeenCalledWith(
["sandbox", "exec", "--name", "alpha", "--", "openclaw", "sessions"],
{ ignoreError: true, includeStreams: true, maxBuffer: 64 * 1024 * 1024 },
);
});

it("routes hermes `sessions list` with forwarded flags via execSandbox (#6247)", async () => {
getSandboxMock.mockReturnValue({ agent: "hermes" });

await runSessionsPassthrough("hermes", {
verb: "list",
extraArgs: ["--limit", "5"],
});

expect(captureMock).not.toHaveBeenCalled();
expect(execMock).toHaveBeenCalledWith("hermes", ["hermes", "sessions", "list", "--limit", "5"]);
});

it("defaults to the openclaw binary + filter path when the registry has no entry (#6247)", async () => {
getSandboxMock.mockReturnValue(null);
captureMock.mockReturnValueOnce({ status: 0, output: "Sessions listed: 0\n" });

await runSessionsPassthrough("alpha", { extraArgs: [] });

expect(execMock).not.toHaveBeenCalled();
expect(captureMock).toHaveBeenCalledWith(
["sandbox", "exec", "--name", "alpha", "--", "openclaw", "sessions"],
{ ignoreError: true, includeStreams: true, maxBuffer: 64 * 1024 * 1024 },
);
});

it("defaults to the openclaw binary for an unknown agent value (#6247)", async () => {
getSandboxMock.mockReturnValue({ agent: "custom-future-agent" });
captureMock.mockReturnValueOnce({ status: 0, output: "Sessions listed: 0\n" });

await runSessionsPassthrough("alpha", { extraArgs: [] });

expect(execMock).not.toHaveBeenCalled();
expect(captureMock).toHaveBeenCalledWith(
["sandbox", "exec", "--name", "alpha", "--", "openclaw", "sessions"],
{ ignoreError: true, includeStreams: true, maxBuffer: 64 * 1024 * 1024 },
);
});

it("prints captured output when OpenClaw exits non-zero", async () => {
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((
code?: string | number | null,
Expand Down
36 changes: 30 additions & 6 deletions src/lib/actions/sandbox/sessions/passthrough.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import { captureOpenshell } from "../../../adapters/openshell/runtime";
import { CLI_NAME } from "../../../cli/branding";
import * as registry from "../../../state/registry";
import { buildOpenshellExecArgs, computeExitCode, execSandbox } from "../exec";
import { ensureLiveSandboxOrExit } from "../gateway-state";
import { isWarmupSessionId, WARMUP_SESSION_ID_PREFIX } from "../warmup-session";
Expand All @@ -27,15 +28,26 @@ export function hasSessionsPassthroughHelpToken(args: readonly string[]): boolea

export function printSessionsPassthroughHelp(verb?: SessionsPassthroughVerb): void {
const usageSuffix = verb ? ` ${verb}` : "";
const flagsToken = verb ? `openclaw-sessions-${verb}-flags` : "openclaw-sessions-flags";
const hermesUsageSuffix = verb ? ` ${verb}` : " list";
const flagsToken = verb ? `sessions-${verb}-flags` : "sessions-flags";
console.log("");
console.log(` Usage: ${CLI_NAME} <name> sessions${usageSuffix} [${flagsToken}...]`);
console.log("");
console.log(
` Pass-through to \`openclaw sessions${usageSuffix} ...\` inside the sandbox via \`openshell sandbox exec\`.`,
` Pass-through to the sandbox agent's \`sessions${usageSuffix} ...\` command inside the sandbox`,
);
console.log(" Internal NemoClaw onboard warm-up sessions are hidden from default list output.");
console.log(" All flags accepted by the in-sandbox OpenClaw CLI are forwarded verbatim.");
console.log(" via `openshell sandbox exec` — `openclaw sessions ...` for OpenClaw sandboxes,");
console.log(
` \`hermes sessions${hermesUsageSuffix} ...\` for Hermes sandboxes; the in-sandbox binary is picked`,
);
console.log(" from the sandbox's agent.");
console.log(
" On OpenClaw sandboxes, internal NemoClaw onboard warm-up sessions are hidden from default",
);
console.log(
" list output and OpenClaw-specific flags are forwarded verbatim. Hermes sandboxes pass",
);
console.log(" through their native output unchanged.");
console.log("");
}

Expand Down Expand Up @@ -202,10 +214,22 @@ export async function runSessionsPassthrough(
{ verb, extraArgs = [] }: SessionsPassthroughOptions = {},
): Promise<void> {
await ensureLiveSandboxOrExit(sandboxName, { allowNonReadyPhase: true });
const command = ["openclaw", "sessions"];
// Hermes sandboxes ship the `hermes` binary in place of OpenClaw's
// `openclaw` binary, and `openclaw` does not exist inside them (#6247).
// Route the passthrough at the in-sandbox agent's own binary name and
// bypass the OpenClaw-specific warm-up filter for non-OpenClaw agents.
//
// Trust boundary: `registry.getSandbox()` reads the host-side, user-owned
// `~/.nemoclaw/sandboxes.json` registry (`REGISTRY_FILE`). Sandbox processes
// cannot access the host filesystem to change this agent selection; unknown
// or missing values deliberately default to `openclaw` below.
const sandboxAgent = registry.getSandbox(sandboxName)?.agent;
const inSandboxBinary = sandboxAgent === "hermes" ? "hermes" : "openclaw";
const command = [inSandboxBinary, "sessions"];
if (verb) command.push(verb);
else if (inSandboxBinary === "hermes") command.push("list");
for (const arg of extraArgs) command.push(arg);
if (isFilterableListPassthrough(verb)) {
if (isFilterableListPassthrough(verb) && inSandboxBinary === "openclaw") {
const result = captureOpenshell(buildOpenshellExecArgs(sandboxName, command), {
ignoreError: true,
includeStreams: true,
Expand Down
8 changes: 4 additions & 4 deletions src/lib/cli/public-display-sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ export const SANDBOX_SESSIONS_DISPLAY_LAYOUT: Record<string, readonly PublicDisp
{
group: "Sandbox Management",
order: 17,
flags: "[openclaw-sessions-flags...]",
description: "List OpenClaw conversation sessions in the sandbox",
flags: "[sessions-flags...]",
description: "List conversation sessions in the sandbox",
},
],
"sandbox:sessions:list": [
{
group: "Sandbox Management",
order: 17.1,
flags: "[openclaw-sessions-list-flags...]",
description: "List OpenClaw conversation sessions",
flags: "[sessions-list-flags...]",
description: "List conversation sessions",
},
],
"sandbox:sessions:reset": [
Expand Down
Loading