diff --git a/CHANGELOG.md b/CHANGELOG.md index ac09a3611..7ed9b1c35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Agents/Compaction continuity: expand staged-summary merge instructions to preserve active task status, batch progress, latest user request, and follow-up commitments so compaction handoffs retain in-flight work context. (#8903) thanks @joetomasone. +- Gateway/status self version reporting: make Gateway self version in `openclaw status` prefer runtime `VERSION` (while preserving explicit `OPENCLAW_VERSION` override), preventing stale post-upgrade app version output. (#32655) thanks @liuxiaopai-ai. - Memory/QMD index isolation: set `QMD_CONFIG_DIR` alongside `XDG_CONFIG_HOME` so QMD config state stays per-agent despite upstream XDG handling bugs, preventing cross-agent collection indexing and excess disk/CPU usage. (#27028) thanks @HenryLoenwind. - LINE/auth boundary hardening synthesis: enforce strict LINE webhook authn/z boundary semantics across pairing-store account scoping, DM/group allowlist separation, fail-closed webhook auth/runtime behavior, and replay/duplication controls (including in-flight replay reservation and post-success dedupe marking). (from #26701, #26683, #25978, #17593, #16619, #31990, #26047, #30584, #18777) Thanks @bmendonca3, @davidahmann, @harshang03, @haosenwang1018, @liuxiaopai-ai, @coygeek, and @Takhoffman. - LINE/media download synthesis: fix file-media download handling and M4A audio classification across overlapping LINE regressions. (from #26386, #27761, #27787, #29509, #29755, #29776, #29785, #32240) Thanks @kevinWangSheng, @loiie45e, @carrotRakko, @Sid-Qin, @codeafridi, and @bmendonca3. diff --git a/src/gateway/server.auth.default-token.suite.ts b/src/gateway/server.auth.default-token.suite.ts index 8cc20f57a..98bbbbe60 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 1ecbb330c..f15687961 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 a6e5863b2..a644cd001 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 44e2a26c3..8465466ef 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 028aac69b..6156ddd8e 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 2e974b6e1..806928103 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