diff --git a/src/cli/nodes-cli/register.pairing.ts b/src/cli/nodes-cli/register.pairing.ts index df7a7941b2e3..aaea23ff46f4 100644 --- a/src/cli/nodes-cli/register.pairing.ts +++ b/src/cli/nodes-cli/register.pairing.ts @@ -211,13 +211,6 @@ export function registerNodesPairingCommands(nodes: Command) { .action(async (opts: NodesRpcOpts) => { await runNodesCommand("remove", async () => { const nodeId = await resolveCliNodeId(opts, normalizeOptionalString(opts.node) ?? ""); - if (!nodeId) { - defaultRuntime.error( - `--node is required. Run ${formatCliCommand("openclaw nodes pending")} to choose a node request.`, - ); - defaultRuntime.exit(1); - return; - } const result = await callNodesGatewayCli("node.pair.remove", opts, { nodeId }); if (opts.json) { defaultRuntime.writeJson(result); @@ -239,9 +232,9 @@ export function registerNodesPairingCommands(nodes: Command) { await runNodesCommand("rename", async () => { const nodeId = await resolveCliNodeId(opts, normalizeOptionalString(opts.node) ?? ""); const name = normalizeOptionalString(opts.name) ?? ""; - if (!nodeId || !name) { + if (!name) { defaultRuntime.error( - `--node and --name are required. Run ${formatCliCommand("openclaw nodes pending")} to choose a node, then rerun with --name .`, + `--name must not be empty. Run ${formatCliCommand("openclaw nodes list")} to see paired nodes, then rerun with --name .`, ); defaultRuntime.exit(1); return; diff --git a/src/cli/nodes-cli/register.status.ts b/src/cli/nodes-cli/register.status.ts index 192f9758f26c..ba5ae5877ab6 100644 --- a/src/cli/nodes-cli/register.status.ts +++ b/src/cli/nodes-cli/register.status.ts @@ -256,26 +256,17 @@ export function registerNodesStatusCommands(nodes: Command) { const tableWidth = getTerminalTableWidth(); const now = Date.now(); const nodesLocal = parseNodeList(result); - const lastConnectedById = - sinceMs !== undefined - ? new Map( - parsePairingList( - await callNodesGatewayCli("node.pair.list", opts, {}), - ).paired.map((entry) => [entry.nodeId, entry]), - ) - : null; const filtered = nodesLocal.filter((n) => { if (connectedOnly && !n.connected) { return false; } if (sinceMs !== undefined) { - const paired = lastConnectedById?.get(n.nodeId); - const lastConnectedAtMs = - typeof paired?.lastConnectedAtMs === "number" - ? paired.lastConnectedAtMs - : typeof n.connectedAtMs === "number" - ? n.connectedAtMs - : undefined; + // The gateway records lastConnectedAtMs on every node.list row + // (max of stored pairing history and live connection); joining a + // second pairing-scoped RPC re-derived that fact and made + // --last-connected fail for read-scoped callers. connectedAtMs + // covers gateways predating the recorded field. + const lastConnectedAtMs = n.lastConnectedAtMs ?? n.connectedAtMs; if (typeof lastConnectedAtMs !== "number") { return false; } @@ -532,7 +523,9 @@ export function registerNodesStatusCommands(nodes: Command) { const tableWidth = getTerminalTableWidth(); const now = Date.now(); const hasFilters = connectedOnly || sinceMs !== undefined; - const pendingRows = hasFilters ? [] : pending; + // Pending requests carry no connection state to filter on; hiding + // them under --connected printed "Pending: 0" while requests waited. + const pendingRows = pending; const effectiveNodes = hasFilters ? parseNodeList(await callNodesGatewayCli("node.list", opts, {})) : await tryReadNodeList(opts); diff --git a/src/cli/program.nodes-basic.e2e.test.ts b/src/cli/program.nodes-basic.e2e.test.ts index 793db88f078e..f2e0f4c877fa 100644 --- a/src/cli/program.nodes-basic.e2e.test.ts +++ b/src/cli/program.nodes-basic.e2e.test.ts @@ -358,25 +358,23 @@ describe("cli program (nodes basics)", () => { expect(output).toContain("Catalog Only"); }); - it("runs nodes status --last-connected and filters by age", async () => { + it("runs nodes status --last-connected using the recorded node.list fact", async () => { const now = Date.now(); + const methods: string[] = []; programGatewayCallMock.mockImplementation(async (...args: unknown[]) => { const opts = (args[0] ?? {}) as { method?: string }; + methods.push(opts.method ?? ""); if (opts.method === "node.list") { return { ts: now, nodes: [ - { nodeId: "n1", displayName: "One", connected: false }, - { nodeId: "n2", displayName: "Two", connected: false }, - ], - }; - } - if (opts.method === "node.pair.list") { - return { - pending: [], - paired: [ - { nodeId: "n1", lastConnectedAtMs: now - 1_000 }, - { nodeId: "n2", lastConnectedAtMs: now - 2 * 24 * 60 * 60 * 1000 }, + { nodeId: "n1", displayName: "One", connected: false, lastConnectedAtMs: now - 1_000 }, + { + nodeId: "n2", + displayName: "Two", + connected: false, + lastConnectedAtMs: now - 2 * 24 * 60 * 60 * 1000, + }, ], }; } @@ -384,7 +382,9 @@ describe("cli program (nodes basics)", () => { }); await runProgram(["nodes", "status", "--last-connected", "24h"]); - expectGatewayRequest("node.pair.list", {}); + // The gateway records lastConnectedAtMs on node.list rows; re-joining + // node.pair.list broke --last-connected for read-scoped callers. + expect(methods).not.toContain("node.pair.list"); const output = getRuntimeOutput(); expect(output).toContain("One"); expect(output).not.toContain("Two"); diff --git a/src/commands/status.node-mode.test.ts b/src/commands/status.node-mode.test.ts index 0d414abb09a1..fd600862089e 100644 --- a/src/commands/status.node-mode.test.ts +++ b/src/commands/status.node-mode.test.ts @@ -30,7 +30,7 @@ describe("resolveNodeOnlyGatewayInfo", () => { installed: true, loaded: true, externallyManaged: false, - runtimeShort: "running (pid 4321)", + runtime: { status: "running", pid: 4321 }, }, }), ).resolves.toEqual({ @@ -60,7 +60,6 @@ describe("resolveNodeOnlyGatewayInfo", () => { loaded: false, externallyManaged: false, runtime: { status: "stopped" }, - runtimeShort: "stopped", }, }), ).resolves.toBeNull(); @@ -76,7 +75,6 @@ describe("resolveNodeOnlyGatewayInfo", () => { installed: true, loaded: true, externallyManaged: false, - runtimeShort: "running (pid 4321)", }, }), ).resolves.toEqual({ diff --git a/src/commands/status.node-mode.ts b/src/commands/status.node-mode.ts index 4f4b2207947d..324799f109dc 100644 --- a/src/commands/status.node-mode.ts +++ b/src/commands/status.node-mode.ts @@ -14,7 +14,6 @@ type NodeOnlyServiceLike = { pid?: number; } | undefined; - runtimeShort?: string | null; }; export type NodeOnlyGatewayInfo = { @@ -51,10 +50,7 @@ function isNodeServiceActive(node: NodeOnlyServiceLike): boolean { if (node.loaded === true) { return true; } - if (hasRunningRuntime(node.runtime)) { - return true; - } - return typeof node.runtimeShort === "string" && node.runtimeShort.startsWith("running"); + return hasRunningRuntime(node.runtime); } /** Returns node-only gateway context when node is active and the local gateway is intentionally absent. */