diff --git a/src/gateway/server.auth.default-token.suite.ts b/src/gateway/server.auth.default-token.suite.ts index 8cc20f57aa3..98bbbbe6010 100644 --- a/src/gateway/server.auth.default-token.suite.ts +++ b/src/gateway/server.auth.default-token.suite.ts @@ -112,7 +112,8 @@ export function registerDefaultAuthTokenSuite(): void { ws.close(); }); - test("connect (req) handshake resolves server version from env precedence", async () => { + test("connect (req) handshake resolves server version from runtime precedence", async () => { + const { VERSION } = await import("../version.js"); for (const testCase of [ { env: { @@ -120,7 +121,7 @@ export function registerDefaultAuthTokenSuite(): void { OPENCLAW_SERVICE_VERSION: "2.4.6-service", npm_package_version: "1.0.0-package", }, - expectedVersion: "2.4.6-service", + expectedVersion: VERSION, }, { env: { @@ -136,7 +137,7 @@ export function registerDefaultAuthTokenSuite(): void { OPENCLAW_SERVICE_VERSION: "\t", npm_package_version: "1.0.0-package", }, - expectedVersion: "1.0.0-package", + expectedVersion: VERSION, }, ]) { await withRuntimeVersionEnv(testCase.env, async () => diff --git a/src/gateway/server/ws-connection/message-handler.ts b/src/gateway/server/ws-connection/message-handler.ts index 1ecbb330c7c..f1568796192 100644 --- a/src/gateway/server/ws-connection/message-handler.ts +++ b/src/gateway/server/ws-connection/message-handler.ts @@ -1032,7 +1032,7 @@ export function attachGatewayWsMessageHandler(params: { type: "hello-ok", protocol: PROTOCOL_VERSION, server: { - version: resolveRuntimeServiceVersion(process.env, "dev"), + version: resolveRuntimeServiceVersion(process.env), connId, }, features: { methods: gatewayMethods, events }, diff --git a/src/infra/system-presence.ts b/src/infra/system-presence.ts index a6e5863b236..a644cd001de 100644 --- a/src/infra/system-presence.ts +++ b/src/infra/system-presence.ts @@ -51,7 +51,7 @@ function resolvePrimaryIPv4(): string | undefined { function initSelfPresence() { const host = os.hostname(); const ip = resolvePrimaryIPv4() ?? undefined; - const version = resolveRuntimeServiceVersion(process.env, "unknown"); + const version = resolveRuntimeServiceVersion(process.env); const modelIdentifier = (() => { const p = os.platform(); if (p === "darwin") { diff --git a/src/infra/system-presence.version.test.ts b/src/infra/system-presence.version.test.ts index 44e2a26c3f8..8465466ef9c 100644 --- a/src/infra/system-presence.version.test.ts +++ b/src/infra/system-presence.version.test.ts @@ -13,20 +13,21 @@ async function withPresenceModule( } describe("system-presence version fallback", () => { - it("uses OPENCLAW_SERVICE_VERSION when OPENCLAW_VERSION is not set", async () => { + it("uses runtime VERSION when OPENCLAW_VERSION is not set", async () => { await withPresenceModule( { OPENCLAW_SERVICE_VERSION: "2.4.6-service", npm_package_version: "1.0.0-package", }, - ({ listSystemPresence }) => { + async ({ listSystemPresence }) => { + const { VERSION } = await import("../version.js"); const selfEntry = listSystemPresence().find((entry) => entry.reason === "self"); - expect(selfEntry?.version).toBe("2.4.6-service"); + expect(selfEntry?.version).toBe(VERSION); }, ); }); - it("prefers OPENCLAW_VERSION over OPENCLAW_SERVICE_VERSION", async () => { + it("prefers OPENCLAW_VERSION over runtime VERSION", async () => { await withPresenceModule( { OPENCLAW_VERSION: "9.9.9-cli", @@ -40,16 +41,17 @@ describe("system-presence version fallback", () => { ); }); - it("uses npm_package_version when OPENCLAW_VERSION and OPENCLAW_SERVICE_VERSION are blank", async () => { + it("uses runtime VERSION when OPENCLAW_VERSION and OPENCLAW_SERVICE_VERSION are blank", async () => { await withPresenceModule( { OPENCLAW_VERSION: " ", OPENCLAW_SERVICE_VERSION: "\t", npm_package_version: "1.0.0-package", }, - ({ listSystemPresence }) => { + async ({ listSystemPresence }) => { + const { VERSION } = await import("../version.js"); const selfEntry = listSystemPresence().find((entry) => entry.reason === "self"); - expect(selfEntry?.version).toBe("1.0.0-package"); + expect(selfEntry?.version).toBe(VERSION); }, ); }); diff --git a/src/version.test.ts b/src/version.test.ts index 028aac69be8..6156ddd8e60 100644 --- a/src/version.test.ts +++ b/src/version.test.ts @@ -4,10 +4,12 @@ import path from "node:path"; import { pathToFileURL } from "node:url"; import { describe, expect, it } from "vitest"; import { + VERSION, readVersionFromBuildInfoForModuleUrl, readVersionFromPackageJsonForModuleUrl, resolveBinaryVersion, resolveRuntimeServiceVersion, + resolveUsableRuntimeVersion, resolveVersionFromModuleUrl, } from "./version.js"; @@ -141,14 +143,24 @@ describe("version resolution", () => { ).toBe("9.9.9"); }); - it("uses service and package fallbacks and ignores blank env values", () => { + it("normalizes runtime version candidate for fallback handling", () => { + expect(resolveUsableRuntimeVersion(undefined)).toBeUndefined(); + expect(resolveUsableRuntimeVersion("")).toBeUndefined(); + expect(resolveUsableRuntimeVersion(" \t ")).toBeUndefined(); + expect(resolveUsableRuntimeVersion("0.0.0")).toBeUndefined(); + expect(resolveUsableRuntimeVersion(" 0.0.0 ")).toBeUndefined(); + expect(resolveUsableRuntimeVersion("2026.3.2")).toBe("2026.3.2"); + expect(resolveUsableRuntimeVersion(" 2026.3.2 ")).toBe("2026.3.2"); + }); + + it("prefers runtime VERSION over service/package markers and ignores blank env values", () => { expect( resolveRuntimeServiceVersion({ OPENCLAW_VERSION: " ", OPENCLAW_SERVICE_VERSION: " 2.0.0 ", npm_package_version: "1.0.0", }), - ).toBe("2.0.0"); + ).toBe(VERSION); expect( resolveRuntimeServiceVersion({ @@ -156,7 +168,7 @@ describe("version resolution", () => { OPENCLAW_SERVICE_VERSION: "\t", npm_package_version: " 1.0.0-package ", }), - ).toBe("1.0.0-package"); + ).toBe(VERSION); expect( resolveRuntimeServiceVersion( @@ -167,6 +179,6 @@ describe("version resolution", () => { }, "fallback", ), - ).toBe("fallback"); + ).toBe(VERSION); }); }); diff --git a/src/version.ts b/src/version.ts index 2e974b6e1db..806928103cd 100644 --- a/src/version.ts +++ b/src/version.ts @@ -90,13 +90,28 @@ export type RuntimeVersionEnv = { [key: string]: string | undefined; }; +export const RUNTIME_SERVICE_VERSION_FALLBACK = "unknown"; + +export function resolveUsableRuntimeVersion(version: string | undefined): string | undefined { + const trimmed = version?.trim(); + // "0.0.0" is the resolver's hard fallback when module metadata cannot be read. + // Prefer explicit service/package markers in that edge case. + if (!trimmed || trimmed === "0.0.0") { + return undefined; + } + return trimmed; +} + export function resolveRuntimeServiceVersion( env: RuntimeVersionEnv = process.env as RuntimeVersionEnv, - fallback = "dev", + fallback = RUNTIME_SERVICE_VERSION_FALLBACK, ): string { + const runtimeVersion = resolveUsableRuntimeVersion(VERSION); + return ( firstNonEmpty( env["OPENCLAW_VERSION"], + runtimeVersion, env["OPENCLAW_SERVICE_VERSION"], env["npm_package_version"], ) ?? fallback