From 2c87c5b1ba05016005fe8e25c8852438871ece06 Mon Sep 17 00:00:00 2001 From: SidQin-cyber Date: Thu, 5 Mar 2026 12:29:57 +0800 Subject: [PATCH] fix(gateway): pass actual version to Control UI client instead of "dev" The GatewayClient, CLI WS client, and browser Control UI all sent "dev" as their clientVersion during handshake, making it impossible to distinguish builds in gateway logs and health snapshots. - GatewayClient and CLI WS client now use the resolved VERSION constant - Control UI reads serverVersion from the bootstrap endpoint and forwards it when connecting - Bootstrap contract extended with serverVersion field Closes #35209 --- src/gateway/call.ts | 3 ++- src/gateway/client.ts | 4 +++- src/gateway/control-ui-contract.ts | 1 + src/gateway/control-ui.ts | 2 ++ ui/src/ui/app-gateway.ts | 2 ++ ui/src/ui/app-lifecycle.ts | 1 + ui/src/ui/app.ts | 1 + ui/src/ui/controllers/control-ui-bootstrap.test.ts | 5 +++++ ui/src/ui/controllers/control-ui-bootstrap.ts | 2 ++ ui/src/ui/gateway.ts | 2 +- 10 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/gateway/call.ts b/src/gateway/call.ts index d52ffcc6d08..ba1e079e455 100644 --- a/src/gateway/call.ts +++ b/src/gateway/call.ts @@ -17,6 +17,7 @@ import { type GatewayClientMode, type GatewayClientName, } from "../utils/message-channel.js"; +import { VERSION } from "../version.js"; import { GatewayClient } from "./client.js"; import { resolveGatewayCredentialsFromConfig } from "./credentials.js"; import { @@ -628,7 +629,7 @@ async function executeGatewayRequestWithScopes(params: { instanceId: opts.instanceId ?? randomUUID(), clientName: opts.clientName ?? GATEWAY_CLIENT_NAMES.CLI, clientDisplayName: opts.clientDisplayName, - clientVersion: opts.clientVersion ?? "dev", + clientVersion: opts.clientVersion ?? VERSION, platform: opts.platform, mode: opts.mode ?? GATEWAY_CLIENT_MODES.CLI, role: "operator", diff --git a/src/gateway/client.ts b/src/gateway/client.ts index a887c757df1..bcf5edd72fb 100644 --- a/src/gateway/client.ts +++ b/src/gateway/client.ts @@ -13,6 +13,7 @@ import { } from "../infra/device-identity.js"; import { clearDevicePairing } from "../infra/device-pairing.js"; import { normalizeFingerprint } from "../infra/tls/fingerprint.js"; +import { VERSION } from "../version.js"; import { rawDataToString } from "../infra/ws.js"; import { logDebug, logError } from "../logger.js"; import { @@ -21,6 +22,7 @@ import { type GatewayClientMode, type GatewayClientName, } from "../utils/message-channel.js"; +import { VERSION } from "../version.js"; import { buildDeviceAuthPayloadV3 } from "./device-auth.js"; import { isSecureWebSocketUrl } from "./net.js"; import { @@ -302,7 +304,7 @@ export class GatewayClient { client: { id: this.opts.clientName ?? GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT, displayName: this.opts.clientDisplayName, - version: this.opts.clientVersion ?? "dev", + version: this.opts.clientVersion ?? VERSION, platform, deviceFamily: this.opts.deviceFamily, mode: this.opts.mode ?? GATEWAY_CLIENT_MODES.BACKEND, diff --git a/src/gateway/control-ui-contract.ts b/src/gateway/control-ui-contract.ts index 654835e0424..b53eca81db5 100644 --- a/src/gateway/control-ui-contract.ts +++ b/src/gateway/control-ui-contract.ts @@ -5,4 +5,5 @@ export type ControlUiBootstrapConfig = { assistantName: string; assistantAvatar: string; assistantAgentId: string; + serverVersion?: string; }; diff --git a/src/gateway/control-ui.ts b/src/gateway/control-ui.ts index 6075e8281a5..de8b36d47d6 100644 --- a/src/gateway/control-ui.ts +++ b/src/gateway/control-ui.ts @@ -25,6 +25,7 @@ import { normalizeControlUiBasePath, resolveAssistantAvatarUrl, } from "./control-ui-shared.js"; +import { resolveRuntimeServiceVersion } from "../version.js"; const ROOT_PREFIX = "/"; const CONTROL_UI_ASSETS_MISSING_MESSAGE = @@ -350,6 +351,7 @@ export function handleControlUiHttpRequest( assistantName: identity.name, assistantAvatar: avatarValue ?? identity.avatar, assistantAgentId: identity.agentId, + serverVersion: resolveRuntimeServiceVersion(process.env), } satisfies ControlUiBootstrapConfig); return true; } diff --git a/ui/src/ui/app-gateway.ts b/ui/src/ui/app-gateway.ts index aa324c32b4c..e61e3e8b658 100644 --- a/ui/src/ui/app-gateway.ts +++ b/ui/src/ui/app-gateway.ts @@ -69,6 +69,7 @@ type GatewayHost = { assistantName: string; assistantAvatar: string | null; assistantAgentId: string | null; + serverVersion: string | null; sessionKey: string; chatRunId: string | null; refreshSessionsAfterChat: Set; @@ -150,6 +151,7 @@ export function connectGateway(host: GatewayHost) { token: host.settings.token.trim() ? host.settings.token : undefined, password: host.password.trim() ? host.password : undefined, clientName: "openclaw-control-ui", + clientVersion: host.serverVersion ?? undefined, mode: "webchat", instanceId: host.clientInstanceId, onHello: (hello) => { diff --git a/ui/src/ui/app-lifecycle.ts b/ui/src/ui/app-lifecycle.ts index 36527c161fc..cd603b96aa5 100644 --- a/ui/src/ui/app-lifecycle.ts +++ b/ui/src/ui/app-lifecycle.ts @@ -27,6 +27,7 @@ type LifecycleHost = { assistantName: string; assistantAvatar: string | null; assistantAgentId: string | null; + serverVersion: string | null; chatHasAutoScrolled: boolean; chatManualRefreshInFlight: boolean; chatLoading: boolean; diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 3b50922bdfc..2576196e2e5 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -135,6 +135,7 @@ export class OpenClawApp extends LitElement { @state() assistantName = bootAssistantIdentity.name; @state() assistantAvatar = bootAssistantIdentity.avatar; @state() assistantAgentId = bootAssistantIdentity.agentId ?? null; + @state() serverVersion: string | null = null; @state() sessionKey = this.settings.sessionKey; @state() chatLoading = false; diff --git a/ui/src/ui/controllers/control-ui-bootstrap.test.ts b/ui/src/ui/controllers/control-ui-bootstrap.test.ts index 29e66fab854..fbe0750ac27 100644 --- a/ui/src/ui/controllers/control-ui-bootstrap.test.ts +++ b/ui/src/ui/controllers/control-ui-bootstrap.test.ts @@ -13,6 +13,7 @@ describe("loadControlUiBootstrapConfig", () => { assistantName: "Ops", assistantAvatar: "O", assistantAgentId: "main", + serverVersion: "2026.3.2", }), }); vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch); @@ -22,6 +23,7 @@ describe("loadControlUiBootstrapConfig", () => { assistantName: "Assistant", assistantAvatar: null, assistantAgentId: null, + serverVersion: null, }; await loadControlUiBootstrapConfig(state); @@ -33,6 +35,7 @@ describe("loadControlUiBootstrapConfig", () => { expect(state.assistantName).toBe("Ops"); expect(state.assistantAvatar).toBe("O"); expect(state.assistantAgentId).toBe("main"); + expect(state.serverVersion).toBe("2026.3.2"); vi.unstubAllGlobals(); }); @@ -46,6 +49,7 @@ describe("loadControlUiBootstrapConfig", () => { assistantName: "Assistant", assistantAvatar: null, assistantAgentId: null, + serverVersion: null, }; await loadControlUiBootstrapConfig(state); @@ -68,6 +72,7 @@ describe("loadControlUiBootstrapConfig", () => { assistantName: "Assistant", assistantAvatar: null, assistantAgentId: null, + serverVersion: null, }; await loadControlUiBootstrapConfig(state); diff --git a/ui/src/ui/controllers/control-ui-bootstrap.ts b/ui/src/ui/controllers/control-ui-bootstrap.ts index a996e1265d3..6542fe1a9ba 100644 --- a/ui/src/ui/controllers/control-ui-bootstrap.ts +++ b/ui/src/ui/controllers/control-ui-bootstrap.ts @@ -10,6 +10,7 @@ export type ControlUiBootstrapState = { assistantName: string; assistantAvatar: string | null; assistantAgentId: string | null; + serverVersion: string | null; }; export async function loadControlUiBootstrapConfig(state: ControlUiBootstrapState) { @@ -43,6 +44,7 @@ export async function loadControlUiBootstrapConfig(state: ControlUiBootstrapStat state.assistantName = normalized.name; state.assistantAvatar = normalized.avatar; state.assistantAgentId = normalized.agentId ?? null; + state.serverVersion = parsed.serverVersion ?? null; } catch { // Ignore bootstrap failures; UI will update identity after connecting. } diff --git a/ui/src/ui/gateway.ts b/ui/src/ui/gateway.ts index 5d0c4e73f2f..d8fd305ae3e 100644 --- a/ui/src/ui/gateway.ts +++ b/ui/src/ui/gateway.ts @@ -233,7 +233,7 @@ export class GatewayBrowserClient { maxProtocol: 3, client: { id: this.opts.clientName ?? GATEWAY_CLIENT_NAMES.CONTROL_UI, - version: this.opts.clientVersion ?? "dev", + version: this.opts.clientVersion ?? "control-ui", platform: this.opts.platform ?? navigator.platform ?? "web", mode: this.opts.mode ?? GATEWAY_CLIENT_MODES.WEBCHAT, instanceId: this.opts.instanceId,