Skip to content

Commit 7eb423e

Browse files
committed
fix(gateway): preserve probed server version
1 parent 18a514e commit 7eb423e

9 files changed

Lines changed: 159 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai
3333
- Providers/Ollama: default unknown-capabilities models to tool-capable so discovered native Ollama models can use tools when `/api/show` omits capabilities. (#84055) Thanks @dutifulbob.
3434
- Installer/Windows: launch `install.ps1` onboarding as an attached child process so fresh native Windows installs do not freeze visibly at `Starting setup...` or corrupt the wizard's terminal rendering.
3535
- CLI/update: keep restart health checks working across one-version CLI/Gateway protocol skew and use the managed Gateway service Node for all follow-up commands even when the package root is unchanged, so `openclaw update` no longer silently switches the gateway to a different Node binary when multiple Node installations are present. Thanks @amknight.
36+
- CLI/gateway: include the running Gateway version in `gateway status` JSON output, preserving existing server metadata while falling back to status RPC data for read probes. Fixes #56222. Thanks @galiniliev.
3637
- Memory/search: close local embedding providers when active-memory searches time out so pending local model loads and embedding contexts are aborted and released. (#83858) Thanks @brokemac79.
3738
- CLI/nodes: request pending node surface approval scopes before `openclaw nodes approve` so exec-capable node approval can use admin-scoped Gateway credentials instead of failing with `missing scope: operator.admin`. (#84392) Thanks @joshavant.
3839

docs/cli/gateway.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ openclaw gateway status --require-rpc
299299
- `gateway status` resolves configured auth SecretRefs for probe auth when possible.
300300
- If a required auth SecretRef is unresolved in this command path, `gateway status --json` reports `rpc.authWarning` when probe connectivity/auth fails; pass `--token`/`--password` explicitly or resolve the secret source first.
301301
- If the probe succeeds, unresolved auth-ref warnings are suppressed to avoid false positives.
302+
- When probing is enabled, JSON output includes `gateway.version` when the running Gateway reports it; `--require-rpc` can fall back to the `status.runtimeVersion` RPC payload if the follow-up handshake probe cannot provide version metadata.
302303
- Use `--require-rpc` in scripts and automation when a listening service is not enough and you need read-scope RPC calls to be healthy too.
303304
- `--deep` adds a best-effort scan for extra launchd/systemd/schtasks installs. When multiple gateway-like services are detected, human output prints cleanup hints and warns that most setups should run one gateway per machine.
304305
- `--deep` also reports a recent Gateway supervisor restart handoff when the service process exited cleanly for an external supervisor restart.

src/cli/daemon-cli.coverage.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,13 +244,14 @@ describe("daemon-cli coverage", () => {
244244
expect(inspectPortUsage).toHaveBeenCalledWith(19001);
245245

246246
const parsed = parseFirstJsonRuntimeLine<{
247-
gateway?: { port?: number; portSource?: string; probeUrl?: string };
247+
gateway?: { port?: number; portSource?: string; probeUrl?: string; version?: string | null };
248248
config?: { mismatch?: boolean };
249249
rpc?: { url?: string; ok?: boolean };
250250
}>();
251251
expect(parsed.gateway?.port).toBe(19001);
252252
expect(parsed.gateway?.portSource).toBe("service args");
253253
expect(parsed.gateway?.probeUrl).toBe("ws://127.0.0.1:19001");
254+
expect(parsed.gateway?.version).toBeNull();
254255
expect(parsed.config?.mismatch).toBe(true);
255256
expect(parsed.rpc?.url).toBe("ws://127.0.0.1:19001");
256257
expect(parsed.rpc?.ok).toBe(true);

src/cli/daemon-cli/probe.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ describe("probeGatewayStatus", () => {
110110
}
111111
expect(result.server?.version).toBe("2026.5.6");
112112
expect(result.server?.connId).toBe("conn-1");
113+
expect(result.version).toBe("2026.5.6");
113114
});
114115

115116
it("uses a real status RPC when requireRpc is enabled", async () => {
@@ -243,6 +244,69 @@ describe("probeGatewayStatus", () => {
243244
});
244245
});
245246

247+
it("uses status.runtimeVersion when read-probe handshake metadata is unavailable", async () => {
248+
callGatewayMock.mockReset();
249+
probeGatewayMock.mockReset();
250+
callGatewayMock.mockResolvedValueOnce({ runtimeVersion: "2026.4.24", status: "ok" });
251+
probeGatewayMock.mockRejectedValueOnce(new Error("probe timed out after status"));
252+
253+
const result = await probeGatewayStatus({
254+
url: "ws://127.0.0.1:19191",
255+
token: "temp-token",
256+
timeoutMs: 5_000,
257+
requireRpc: true,
258+
});
259+
260+
expect(result).toEqual({
261+
ok: true,
262+
kind: "read",
263+
capability: "read_only",
264+
auth: undefined,
265+
version: "2026.4.24",
266+
});
267+
});
268+
269+
it("prefers read-probe server metadata over status.runtimeVersion", async () => {
270+
callGatewayMock.mockReset();
271+
probeGatewayMock.mockReset();
272+
callGatewayMock.mockResolvedValueOnce({ runtimeVersion: "2026.4.23", status: "ok" });
273+
probeGatewayMock.mockResolvedValueOnce({
274+
ok: true,
275+
auth: {
276+
role: "operator",
277+
scopes: ["operator.read"],
278+
capability: "read_only",
279+
},
280+
server: {
281+
version: "2026.4.24",
282+
connId: "conn-1",
283+
},
284+
});
285+
286+
const result = await probeGatewayStatus({
287+
url: "ws://127.0.0.1:19191",
288+
token: "temp-token",
289+
timeoutMs: 5_000,
290+
requireRpc: true,
291+
});
292+
293+
expect(result).toEqual({
294+
ok: true,
295+
kind: "read",
296+
capability: "read_only",
297+
auth: {
298+
role: "operator",
299+
scopes: ["operator.read"],
300+
capability: "read_only",
301+
},
302+
server: {
303+
version: "2026.4.24",
304+
connId: "conn-1",
305+
},
306+
version: "2026.4.24",
307+
});
308+
});
309+
246310
it("surfaces probe close details when the handshake fails", async () => {
247311
callGatewayMock.mockReset();
248312
probeGatewayMock.mockReset();

src/cli/daemon-cli/probe.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ function resolveGatewayStatusProbeDetails(result: GatewayStatusProbeResult) {
3434
return "authProbe" in result ? result.authProbe : result;
3535
}
3636

37+
function readRuntimeVersionFromStatusPayload(payload: unknown): string | null {
38+
if (!payload || typeof payload !== "object") {
39+
return null;
40+
}
41+
const runtimeVersion = (payload as { runtimeVersion?: unknown }).runtimeVersion;
42+
return typeof runtimeVersion === "string" && runtimeVersion.trim().length > 0
43+
? runtimeVersion.trim()
44+
: null;
45+
}
46+
3747
export async function probeGatewayStatus(opts: {
3848
url: string;
3949
token?: string;
@@ -48,6 +58,7 @@ export async function probeGatewayStatus(opts: {
4858
}) {
4959
const kind = (opts.requireRpc ? "read" : "connect") satisfies GatewayStatusProbeKind;
5060
try {
61+
let statusRuntimeVersion: string | null = null;
5162
const result = await withProgress<GatewayStatusProbeResult>(
5263
{
5364
label: "Checking gateway status...",
@@ -71,7 +82,7 @@ export async function probeGatewayStatus(opts: {
7182
};
7283
if (opts.requireRpc) {
7384
const { callGateway } = await import("../../gateway/call.js");
74-
await callGateway({
85+
const statusPayload = await callGateway({
7586
url: opts.url,
7687
token: opts.token,
7788
password: opts.password,
@@ -81,6 +92,7 @@ export async function probeGatewayStatus(opts: {
8192
timeoutMs: opts.timeoutMs,
8293
...(opts.configPath ? { configPath: opts.configPath } : {}),
8394
});
95+
statusRuntimeVersion = readRuntimeVersionFromStatusPayload(statusPayload);
8496
const authProbe = await probeGateway(probeOpts).catch(() => null);
8597
return { ok: true as const, authProbe };
8698
}
@@ -91,6 +103,7 @@ export async function probeGatewayStatus(opts: {
91103
const auth = probeDetails?.auth;
92104
const server = probeDetails?.server;
93105
const serverSummary = server ? { server } : {};
106+
const version = server?.version ?? ("authProbe" in result ? statusRuntimeVersion : null);
94107
if (result.ok) {
95108
return {
96109
ok: true,
@@ -103,6 +116,7 @@ export async function probeGatewayStatus(opts: {
103116
: auth?.capability,
104117
auth,
105118
...serverSummary,
119+
...(version != null ? { version } : {}),
106120
} as const;
107121
}
108122
return {
@@ -111,6 +125,7 @@ export async function probeGatewayStatus(opts: {
111125
capability: auth?.capability,
112126
auth,
113127
...serverSummary,
128+
...(version != null ? { version } : {}),
114129
error: resolveProbeFailureMessage(result),
115130
} as const;
116131
} catch (err) {

src/cli/daemon-cli/status.gather.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const callGatewayStatusProbe = vi.fn<
1717
url?: string;
1818
error?: string | null;
1919
server?: { version?: string | null; connId?: string | null };
20+
version?: string | null;
2021
}>
2122
>(async (_opts?: unknown) => ({
2223
ok: true,
@@ -272,6 +273,7 @@ describe("gatherDaemonStatus", () => {
272273
expect(probeInput.token).toBe("daemon-token");
273274
expect(status.gateway?.probeUrl).toBe("wss://127.0.0.1:19001");
274275
expect(status.gateway?.tlsEnabled).toBe(true);
276+
expect(status.gateway?.version).toBe("2026.5.6");
275277
expect(status.rpc?.url).toBe("wss://127.0.0.1:19001");
276278
expect(status.rpc?.ok).toBe(true);
277279
expect(status.rpc?.server).toEqual({ version: "2026.5.6", connId: "conn-1" });
@@ -282,6 +284,25 @@ describe("gatherDaemonStatus", () => {
282284
expect(inspectGatewayRestart).not.toHaveBeenCalled();
283285
});
284286

287+
it("falls back to probe version when server metadata is unavailable", async () => {
288+
callGatewayStatusProbe.mockResolvedValueOnce({
289+
ok: true,
290+
url: "ws://127.0.0.1:19001",
291+
error: null,
292+
version: "2026.5.7",
293+
});
294+
295+
const status = await gatherDaemonStatus({
296+
rpc: {},
297+
probe: true,
298+
deep: false,
299+
});
300+
301+
expect(status.gateway?.version).toBe("2026.5.7");
302+
expect(status.rpc?.version).toBe("2026.5.7");
303+
expect(status.rpc?.server).toBeUndefined();
304+
});
305+
285306
it("forwards requireRpc and configPath to the daemon probe", async () => {
286307
await gatherDaemonStatus({
287308
rpc: {},

src/cli/daemon-cli/status.gather.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ type GatewayStatusSummary = {
6060
portSource: "service args" | "env/config";
6161
probeUrl: string;
6262
probeNote?: string;
63+
version?: string | null;
6364
};
6465

6566
type PortStatusSummary = {
@@ -314,6 +315,7 @@ export type DaemonStatus = {
314315
version?: string | null;
315316
connId?: string | null;
316317
};
318+
version?: string | null;
317319
error?: string;
318320
url?: string;
319321
authWarning?: string;
@@ -622,6 +624,11 @@ export async function gatherDaemonStatus(
622624
)
623625
.catch(() => undefined)
624626
: undefined;
627+
const gatewayVersion = opts.probe
628+
? ((rpc && "server" in rpc ? rpc.server?.version : undefined) ??
629+
(rpc && "version" in rpc ? rpc.version : undefined) ??
630+
null)
631+
: undefined;
625632

626633
let lastError: string | undefined;
627634
if (loaded && runtime?.status === "running" && portStatus && portStatus.status !== "busy") {
@@ -647,7 +654,14 @@ export async function gatherDaemonStatus(
647654
daemon: daemonConfigSummary,
648655
...(configMismatch ? { mismatch: true } : {}),
649656
},
650-
gateway,
657+
gateway: {
658+
...gateway,
659+
...(opts.probe
660+
? {
661+
version: gatewayVersion,
662+
}
663+
: {}),
664+
},
651665
port: portStatus,
652666
...(portCliStatus ? { portCli: portCliStatus } : {}),
653667
...(establishedClients ? { connections: establishedClients } : {}),

src/cli/daemon-cli/status.print.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,44 @@ describe("printDaemonStatus", () => {
321321
);
322322
});
323323

324+
it("prints gateway version from gathered gateway status when probe server metadata is absent", () => {
325+
printDaemonStatus(
326+
{
327+
cli: {
328+
version: "2026.4.23",
329+
entrypoint: "/usr/local/bin/openclaw",
330+
},
331+
service: {
332+
label: "LaunchAgent",
333+
loaded: true,
334+
loadedText: "loaded",
335+
notLoadedText: "not loaded",
336+
runtime: { status: "running", pid: 8000 },
337+
},
338+
gateway: {
339+
bindMode: "loopback",
340+
bindHost: "127.0.0.1",
341+
port: 18789,
342+
portSource: "env/config",
343+
probeUrl: "ws://127.0.0.1:18789",
344+
version: "2026.5.7",
345+
},
346+
rpc: {
347+
ok: true,
348+
kind: "read",
349+
capability: "read_only",
350+
url: "ws://127.0.0.1:18789",
351+
version: "2026.5.7",
352+
},
353+
extraServices: [],
354+
},
355+
{ json: false },
356+
);
357+
358+
expectMockLineContains(runtime.log, "Gateway version: 2026.5.7");
359+
expectMockLineContains(runtime.error, "this OpenClaw command is version 2026.4.23");
360+
});
361+
324362
it("prints restart handoff diagnostics when deep status gathered one", () => {
325363
printDaemonStatus(
326364
{

src/cli/daemon-cli/status.print.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean })
220220
spacer();
221221
}
222222

223-
const gatewayVersion = rpc?.server?.version?.trim();
223+
const gatewayVersion = rpc?.server?.version?.trim() || status.gateway?.version?.trim();
224224
const cliVersionLine = formatCliVersionLine(status.cli);
225225
if (gatewayVersion) {
226226
if (cliVersionLine) {

0 commit comments

Comments
 (0)