diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index 4ca95a53561..9ed26c2e157 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -839,6 +839,8 @@ Use `--no-sandbox-gpu`, `--no-gpu`, or `NEMOCLAW_SANDBOX_GPU=0` when you want to List all registered sandboxes with their model, provider, and policy presets. Pass `--json` for machine-readable output that includes a `schemaVersion`, the default sandbox, recovery metadata, and the sandbox inventory. +Each sandbox row reports `activeSessionCount` as a nonnegative integer when the SSH-session probe is available and `null` when it is unavailable. +The row does not include the former derived `connected` boolean. Sandboxes with an active SSH session are marked with a `●` indicator so you can tell at a glance which sandbox you are already connected to in another terminal. When a sandbox has a recorded dashboard port, the output includes its local dashboard URL. @@ -1409,7 +1411,7 @@ When sandbox GPU passthrough is enabled, the `Sandbox GPU` line includes the las It reports `(CUDA verified)`, `(CUDA unverified)`, or `(last CUDA proof failed: Use `$$nemoclaw status` when you need one sandbox's live health and recovery guidance. Pass `--json` for machine-readable output with registered sandboxes, service state, inference routes, and health details. -For each listed sandbox, the text output includes the configured inference provider and model plus whether an active SSH session is connected. +For each listed sandbox, the text output includes the configured inference provider and model plus the number of active SSH sessions when the session probe is available. Host-service PID lookup honors `NEMOCLAW_SANDBOX_NAME`, then `NEMOCLAW_SANDBOX`, then `SANDBOX_NAME`, then the registry default. ```bash diff --git a/src/lib/actions/sandbox/status-flow.test.ts b/src/lib/actions/sandbox/status-flow.test.ts index e7336db919a..e5a449e55cd 100644 --- a/src/lib/actions/sandbox/status-flow.test.ts +++ b/src/lib/actions/sandbox/status-flow.test.ts @@ -133,8 +133,7 @@ describe("showSandboxStatus flow", () => { expect(output).toContain("Host GPU: yes"); expect(output).toContain("last CUDA proof failed: cuInit"); expect(output).toContain("CUDA initialization failed"); - expect(output).toContain("Connected:"); - expect(output).toContain("2 sessions"); + expect(output).toContain("SSH sessions: 2"); expect(output).toContain("Permissions: mutable default"); expect(output).toContain("Update:"); expect(output).toContain("Recovered NemoClaw gateway runtime via gateway reattach."); @@ -149,6 +148,27 @@ describe("showSandboxStatus flow", () => { expect(exitSpy).not.toHaveBeenCalled(); }); + it("reports zero SSH sessions as 'none' without connection-negative language (#7805)", async () => { + const harness = createStatusFlowHarness(); + harness.getActiveSandboxSessionsSpy.mockReturnValue({ detected: true, sessions: [] }); + + await expect(harness.showSandboxStatus("alpha")).resolves.toBeUndefined(); + + const output = harness.logSpy.mock.calls.map((call) => String(call[0])).join("\n"); + expect(output).toContain("SSH sessions: none"); + expect(output).not.toMatch(/^\s*Connected:/m); + }); + + it("omits SSH sessions when the active-session probe is unavailable (#7805)", async () => { + const harness = createStatusFlowHarness(); + harness.getActiveSandboxSessionsSpy.mockReturnValue({ detected: false, sessions: [] }); + + await expect(harness.showSandboxStatus("alpha")).resolves.toBeUndefined(); + + const output = harness.logSpy.mock.calls.map((call) => String(call[0])).join("\n"); + expect(output).not.toMatch(/^\s*(?:Connected|SSH sessions):/m); + }); + it("reports active baseline exclusions and their support impact (#7178)", async () => { const harness = createStatusFlowHarness({ sandboxEntry: { diff --git a/src/lib/actions/sandbox/status-text.ts b/src/lib/actions/sandbox/status-text.ts index 550128c7f64..72a5c75fe90 100644 --- a/src/lib/actions/sandbox/status-text.ts +++ b/src/lib/actions/sandbox/status-text.ts @@ -247,8 +247,8 @@ function printActiveSessions(sandboxName: string): void { const sessionResult = getActiveSandboxSessions(sandboxName, createSessionDeps(openshell)); if (!sessionResult.detected) return; const count = sessionResult.sessions.length; - const connected = count > 0 ? `${G}yes${R} (${count} session${count > 1 ? "s" : ""})` : "no"; - console.log(` Connected: ${connected}`); + const sessions = count > 0 ? `${G}${count}${R}` : "none"; + console.log(` SSH sessions: ${sessions}`); } catch { // Session detection is informational; an unavailable OpenShell client must // not suppress the primary sandbox and gateway health report. diff --git a/src/lib/inventory/index.test.ts b/src/lib/inventory/index.test.ts index 4f7f299e886..9660f9c882a 100644 --- a/src/lib/inventory/index.test.ts +++ b/src/lib/inventory/index.test.ts @@ -128,7 +128,6 @@ describe("inventory commands", () => { agent: "openclaw", isDefault: true, activeSessionCount: 1, - connected: true, }, ], }); @@ -1090,7 +1089,7 @@ describe("inventory commands", () => { expect(lines).toContain(" Inference: live-provider / live-model"); }); - it("emits a Connected line per sandbox when getActiveSessionCount is provided (#2604)", () => { + it("emits an SSH sessions line per sandbox when getActiveSessionCount is provided (#2604)", () => { const lines: string[] = []; showStatusCommand({ listSandboxes: () => ({ @@ -1106,11 +1105,11 @@ describe("inventory commands", () => { log: (message = "") => lines.push(message), }); - expect(lines).toContain(" Connected: yes (2 sessions)"); - expect(lines).toContain(" Connected: no"); + expect(lines).toContain(" SSH sessions: 2"); + expect(lines).toContain(" SSH sessions: none"); }); - it("renders `1 session` (singular) when the active count is exactly one (#2604)", () => { + it("renders the exact active count when exactly one session (#2604)", () => { const lines: string[] = []; showStatusCommand({ listSandboxes: () => ({ @@ -1123,10 +1122,10 @@ describe("inventory commands", () => { log: (message = "") => lines.push(message), }); - expect(lines).toContain(" Connected: yes (1 session)"); + expect(lines).toContain(" SSH sessions: 1"); }); - it("omits the Connected line when getActiveSessionCount returns null (probe unavailable)", () => { + it("omits the SSH sessions line when getActiveSessionCount returns null (probe unavailable)", () => { const lines: string[] = []; showStatusCommand({ listSandboxes: () => ({ @@ -1139,10 +1138,10 @@ describe("inventory commands", () => { log: (message = "") => lines.push(message), }); - expect(lines.some((l) => l.includes("Connected:"))).toBe(false); + expect(lines.some((l) => l.includes("SSH sessions:"))).toBe(false); }); - it("omits the Connected line when the dep is not wired", () => { + it("omits the SSH sessions line when the dep is not wired", () => { const lines: string[] = []; showStatusCommand({ listSandboxes: () => ({ @@ -1154,7 +1153,7 @@ describe("inventory commands", () => { log: (message = "") => lines.push(message), }); - expect(lines.some((l) => l.includes("Connected:"))).toBe(false); + expect(lines.some((l) => l.includes("SSH sessions:"))).toBe(false); }); it("emits a gateway-down diagnostic and sets process.exitCode when the gateway is unhealthy (#3386)", () => { diff --git a/src/lib/inventory/index.ts b/src/lib/inventory/index.ts index 1f753cb3d68..59d7f57a255 100644 --- a/src/lib/inventory/index.ts +++ b/src/lib/inventory/index.ts @@ -89,7 +89,6 @@ export interface SandboxInventoryRow { dashboardPort?: number | null; isDefault: boolean; activeSessionCount: number | null; - connected: boolean; // #5714: row recovered display-only from the live gateway. Its agent/GPU/ // inference state is unknown (the gateway sandbox list does not expose it), // so the renderer shows "unknown" rather than asserting OpenClaw/CPU defaults. @@ -130,7 +129,7 @@ export interface ShowStatusCommandDeps { getServiceStatuses?: (options: { sandboxName?: string }) => StatusServiceRow[]; /** * Active SSH-session count for a sandbox. When provided, `showStatusCommand` - * emits a `Connected:` line under each sandbox row. Returns null when the + * emits an `SSH sessions:` line under each sandbox row. Returns null when the * probe is not available (e.g. no openshell binary); the line is omitted in * that case. #2604. */ @@ -233,7 +232,6 @@ function buildSandboxInventoryRow( ...(sandbox.dashboardPort != null ? { dashboardPort: sandbox.dashboardPort } : {}), isDefault: sandbox.name === defaultSandbox, activeSessionCount, - connected: activeSessionCount !== null && activeSessionCount > 0, ...(sandbox.recoveredFromGateway ? { recoveredFromGateway: true } : {}), ...(sandbox.recoveredFromGateway ? { livePhase: sandbox.livePhase ?? null } : {}), }; @@ -346,13 +344,13 @@ export function renderSandboxInventoryText( ? "sandbox GPU" : "CPU sandbox"; const presets = sandbox.policies.length > 0 ? sandbox.policies.join(", ") : "none"; - const connected = sandbox.connected ? " ●" : ""; + const sessionDot = (sandbox.activeSessionCount ?? 0) > 0 ? " ●" : ""; const agent = sandbox.agent || "openclaw"; // #5714: for a gateway-recovered row, surface the trusted live PHASE // (e.g. Ready) from `openshell sandbox list` so `list` agrees with // `nemoclaw status`; normal registry rows have no live phase. const phase = sandbox.recoveredFromGateway ? ` phase: ${sandbox.livePhase || "unknown"}` : ""; - log(` ${sandbox.name}${def}${connected}`); + log(` ${sandbox.name}${def}${sessionDot}`); log( ` agent: ${agent} model: ${model} provider: ${provider} ${gpu}${phase} policies: ${presets}`, ); @@ -536,11 +534,10 @@ export function showStatusCommand(deps: ShowStatusCommandDeps): void { if (isDefault && liveModel && liveModel !== inference.model) { log(` (onboarded: ${inference.model || "unknown"})`); } - // #2604: surface the configured Inference (provider/model) and - // Connected (active-session count) as labeled fields. Bare - // `nemoclaw status` previously only had the model in parens above — - // users had to run `nemoclaw status` to see provider and - // connection state. + // #2604: surface the configured Inference (provider/model) and the + // SSH-session count as labeled fields. Bare `nemoclaw status` previously + // only had the model in parens above — users had to run + // `nemoclaw status` to see provider and session state. if (provider || model) { const parts = [provider, model].filter(Boolean).join(" / "); log(` Inference: ${parts}`); @@ -548,9 +545,7 @@ export function showStatusCommand(deps: ShowStatusCommandDeps): void { if (deps.getActiveSessionCount) { const count = deps.getActiveSessionCount(sb.name); if (count !== null) { - log( - ` Connected: ${count > 0 ? `yes (${count} session${count > 1 ? "s" : ""})` : "no"}`, - ); + log(` SSH sessions: ${count > 0 ? count : "none"}`); } } } diff --git a/test/cli/list-inference.test.ts b/test/cli/list-inference.test.ts index f9271ee89ec..9271a6c0cff 100644 --- a/test/cli/list-inference.test.ts +++ b/test/cli/list-inference.test.ts @@ -247,7 +247,6 @@ describe("CLI dispatch", () => { agent: "openclaw", isDefault: true, activeSessionCount: 1, - connected: true, hostGpuDetected: false, sandboxGpuEnabled: true, sandboxGpuMode: null,