From 9bc703213bb3d7dbeebc8629e747c22e64bc0511 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 11:56:53 +0100 Subject: [PATCH] fix(control-ui): preserve loopback client version labels --- CHANGELOG.md | 1 + ui/src/ui/app-gateway.node.test.ts | 27 ++++++++++++++++++++ ui/src/ui/app-gateway.ts | 40 +++++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a06e6735f7d..3afaf64919b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Control UI/Gateway: preserve WebChat client version labels across localhost, 127.0.0.1, and IPv6 loopback aliases on the same port, avoiding misleading `vcontrol-ui` connection logs while investigating duplicate-message reports. Refs #72753 and #72742. Thanks @LumenFromTheFuture and @allesgutefy. - Memory-core: run one-shot memory CLI commands through transient builtin and QMD managers so `memory index`, `memory status --index`, and `memory search` no longer start long-lived file watchers that can hit macOS `EMFILE` limits. Fixes #59101; carries forward #49851. Thanks @mbear469210-coder and @maoyuanxue. - Memory-core: re-resolve the active runtime config whenever `memory_search` or `memory_get` executes, so provider changes made by `config.patch` stop leaving stale embedding backends behind in existing tool instances. Fixes #61098. Thanks @BradGroux and @Linux2010. - WebChat: keep bare `/new` and `/reset` startup instructions out of visible chat history while preserving `/reset ` as user-visible transcript text. Fixes #72369. Thanks @collynes and @haishmg. diff --git a/ui/src/ui/app-gateway.node.test.ts b/ui/src/ui/app-gateway.node.test.ts index 54a6989018b..99ad789c8fd 100644 --- a/ui/src/ui/app-gateway.node.test.ts +++ b/ui/src/ui/app-gateway.node.test.ts @@ -1025,6 +1025,33 @@ describe("resolveControlUiClientVersion", () => { ).toBe("2026.3.7"); }); + it("returns serverVersion for loopback aliases on the same port", () => { + expect( + resolveControlUiClientVersion({ + gatewayUrl: "ws://127.0.0.1:18789", + serverVersion: "2026.4.24", + pageUrl: "http://localhost:18789/chat", + }), + ).toBe("2026.4.24"); + expect( + resolveControlUiClientVersion({ + gatewayUrl: "ws://[::1]:18789", + serverVersion: "2026.4.24", + pageUrl: "http://127.0.0.1:18789/chat", + }), + ).toBe("2026.4.24"); + }); + + it("omits serverVersion for loopback aliases on different ports", () => { + expect( + resolveControlUiClientVersion({ + gatewayUrl: "ws://127.0.0.1:18789", + serverVersion: "2026.4.24", + pageUrl: "http://localhost:19889/chat", + }), + ).toBeUndefined(); + }); + it("omits serverVersion for cross-origin targets", () => { expect( resolveControlUiClientVersion({ diff --git a/ui/src/ui/app-gateway.ts b/ui/src/ui/app-gateway.ts index 65214fc79b2..978a66ecb55 100644 --- a/ui/src/ui/app-gateway.ts +++ b/ui/src/ui/app-gateway.ts @@ -265,7 +265,7 @@ export function resolveControlUiClientVersion(params: { const page = new URL(pageUrl); const gateway = new URL(params.gatewayUrl, page); const allowedProtocols = new Set(["ws:", "wss:", "http:", "https:"]); - if (!allowedProtocols.has(gateway.protocol) || gateway.host !== page.host) { + if (!allowedProtocols.has(gateway.protocol) || !isSameControlUiVersionEndpoint(page, gateway)) { return undefined; } return serverVersion; @@ -274,6 +274,44 @@ export function resolveControlUiClientVersion(params: { } } +function isSameControlUiVersionEndpoint(page: URL, gateway: URL): boolean { + if (gateway.host === page.host) { + return true; + } + return ( + isLoopbackHostname(page.hostname) && + isLoopbackHostname(gateway.hostname) && + resolveUrlEffectivePort(page) === resolveUrlEffectivePort(gateway) + ); +} + +function isLoopbackHostname(hostname: string): boolean { + const normalized = hostname.trim().toLowerCase().replace(/^\[/, "").replace(/\]$/, ""); + return ( + normalized === "localhost" || + normalized === "::1" || + normalized === "0:0:0:0:0:0:0:1" || + normalized === "127.0.0.1" || + normalized.startsWith("127.") + ); +} + +function resolveUrlEffectivePort(url: URL): string { + if (url.port) { + return url.port; + } + switch (url.protocol) { + case "http:": + case "ws:": + return "80"; + case "https:": + case "wss:": + return "443"; + default: + return ""; + } +} + function normalizeSessionKeyForDefaults( value: string | undefined, defaults: SessionDefaultsSnapshot,