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,