diff --git a/packages/gateway-protocol/src/connect-error-details.test.ts b/packages/gateway-protocol/src/connect-error-details.test.ts index 1cdb3a3bd3f4..377fcee42cde 100644 --- a/packages/gateway-protocol/src/connect-error-details.test.ts +++ b/packages/gateway-protocol/src/connect-error-details.test.ts @@ -11,7 +11,7 @@ import { formatConnectPairingRequiredMessage, normalizePairingConnectRequestId, readConnectErrorDetailCode, - readControlUiBuildMismatchDetails, + readControlUiBuildMismatchId, readConnectErrorRecoveryAdvice, readConnectPairingRequiredMessage, readPairingConnectErrorDetails, @@ -44,19 +44,15 @@ describe("readConnectErrorDetailCode", () => { }); }); -describe("readControlUiBuildMismatchDetails", () => { +describe("readControlUiBuildMismatchId", () => { it("returns a bounded reload target", () => { expect( - readControlUiBuildMismatchDetails({ + readControlUiBuildMismatchId({ code: ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH, gatewayBuildId: "gateway-build", reloadRequired: true, }), - ).toEqual({ - code: ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH, - gatewayBuildId: "gateway-build", - reloadRequired: true, - }); + ).toBe("gateway-build"); }); it.each([ @@ -73,7 +69,7 @@ describe("readControlUiBuildMismatchDetails", () => { reloadRequired: false, }, ])("rejects malformed details", (details) => { - expect(readControlUiBuildMismatchDetails(details)).toBeNull(); + expect(readControlUiBuildMismatchId(details)).toBeNull(); }); }); diff --git a/packages/gateway-protocol/src/connect-error-details.ts b/packages/gateway-protocol/src/connect-error-details.ts index f9221ee6ceb7..04ba3b1cf0c1 100644 --- a/packages/gateway-protocol/src/connect-error-details.ts +++ b/packages/gateway-protocol/src/connect-error-details.ts @@ -55,12 +55,6 @@ export const ConnectErrorDetailCodes = { type ConnectErrorDetailCode = (typeof ConnectErrorDetailCodes)[keyof typeof ConnectErrorDetailCodes]; -export type ControlUiBuildMismatchDetails = { - code: typeof ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH; - gatewayBuildId: string; - reloadRequired: true; -}; - /** Pairing-specific reasons clients can display and use for reconnect policy. */ const ConnectPairingRequiredReasons = { NOT_PAIRED: "not-paired", @@ -237,15 +231,8 @@ export function readConnectErrorDetailCode(details: unknown): string | null { } /** Read the exact target artifact from an untrusted reload-required rejection. */ -export function readControlUiBuildMismatchDetails( - details: unknown, -): ControlUiBuildMismatchDetails | null { - if ( - readConnectErrorDetailCode(details) !== ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH || - !details || - typeof details !== "object" || - Array.isArray(details) - ) { +export function readControlUiBuildMismatchId(details: unknown): string | null { + if (readConnectErrorDetailCode(details) !== ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH) { return null; } const raw = details as { gatewayBuildId?: unknown; reloadRequired?: unknown }; @@ -253,11 +240,7 @@ export function readControlUiBuildMismatchDetails( if (!gatewayBuildId || gatewayBuildId.length > 96 || raw.reloadRequired !== true) { return null; } - return { - code: ConnectErrorDetailCodes.CONTROL_UI_BUILD_MISMATCH, - gatewayBuildId, - reloadRequired: true, - }; + return gatewayBuildId; } /** Extracts normalized retry advice from untrusted connect-error details. */ diff --git a/ui/src/api/gateway-connect-errors.ts b/ui/src/api/gateway-connect-errors.ts index a765f3afd430..354f82ef4b06 100644 --- a/ui/src/api/gateway-connect-errors.ts +++ b/ui/src/api/gateway-connect-errors.ts @@ -1,10 +1,8 @@ import { ConnectErrorDetailCodes, - GatewayProtocolRequestError, MIN_CLIENT_PROTOCOL_VERSION, PROTOCOL_VERSION, readConnectErrorDetailCode, - shouldPauseGatewayReconnect, } from "@openclaw/gateway-client/browser"; export function enrichProtocolMismatchDetails( @@ -30,22 +28,3 @@ export function resolveGatewayErrorDetailCode( ): string | null { return readConnectErrorDetailCode(error?.details); } - -export function isLegacyGatewayBuildIdSchemaError( - error: GatewayProtocolRequestError, - clientBuildId: string | undefined, -): boolean { - return Boolean( - clientBuildId && /invalid connect params.*unexpected property.*buildid/iu.test(error.message), - ); -} - -/** Token mismatch stays with its bounded retry owner; static failures pause. */ -export function isNonRecoverableConnectError(error: { details?: unknown } | undefined): boolean { - return error - ? shouldPauseGatewayReconnect({ - details: error.details, - protocolMismatchIsTerminal: true, - }) - : false; -} diff --git a/ui/src/api/gateway.ts b/ui/src/api/gateway.ts index ed318bc1e0ec..27f75422f6a6 100644 --- a/ui/src/api/gateway.ts +++ b/ui/src/api/gateway.ts @@ -27,6 +27,7 @@ import { PROTOCOL_VERSION, resolveGatewayStartupRetryAfterMs, resolveSafeTimeoutDelayMs, + shouldPauseGatewayReconnect, } from "@openclaw/gateway-client/browser"; export type { EventFrame as GatewayEventFrame } from "@openclaw/gateway-client/browser"; import type { @@ -54,8 +55,6 @@ import { generateUUID } from "../lib/uuid.ts"; import { createBrowserGatewaySocket } from "./gateway-browser-socket.ts"; import { enrichProtocolMismatchDetails, - isLegacyGatewayBuildIdSchemaError, - isNonRecoverableConnectError, resolveGatewayErrorDetailCode, } from "./gateway-connect-errors.ts"; @@ -288,7 +287,6 @@ export class GatewayBrowserClient { // Older shipped Gateways used a closed client schema. Downgrade once per // browser client; a document reload creates a fresh exact-identity attempt. private clientBuildIdCompatibilityDisabled = false; - private clientBuildIdRetryBudgetUsed = false; // Close/stop advances this generation before another socket can make stale hello work look active. private recovery = { value: "", resolved: false, generation: 0 }; private scopeUpgradeBinding: ScopeUpgradeBinding | null = null; @@ -380,7 +378,6 @@ export class GatewayBrowserClient { this.pendingDeviceTokenRetry = false; this.deviceTokenRetryBudgetUsed = false; this.clientBuildIdCompatibilityDisabled = false; - this.clientBuildIdRetryBudgetUsed = false; } get connected() { @@ -587,11 +584,11 @@ export class GatewayBrowserClient { const connectErrorCode = err instanceof GatewayRequestError ? resolveGatewayErrorDetailCode(err) : null; if ( - !this.clientBuildIdRetryBudgetUsed && - isLegacyGatewayBuildIdSchemaError(err, plan.params.client.buildId) + !this.clientBuildIdCompatibilityDisabled && + plan.params.client.buildId && + /invalid connect params.*unexpected property.*buildid/iu.test(err.message) ) { this.clientBuildIdCompatibilityDisabled = true; - this.clientBuildIdRetryBudgetUsed = true; this.client.resetReconnectBackoff(250); return { closeCode: CONNECT_FAILED_CLOSE_CODE, closeReason: "connect retry" }; } @@ -725,7 +722,10 @@ export class GatewayBrowserClient { const retry = connectErrorCode === ConnectErrorDetailCodes.AUTH_TOKEN_MISMATCH ? this.pendingDeviceTokenRetry - : !isNonRecoverableConnectError(connectError); + : !shouldPauseGatewayReconnect({ + details: connectError?.details, + protocolMismatchIsTerminal: true, + }); return { retry, notify: true, pendingError: error }; } diff --git a/ui/src/app/gateway-store.ts b/ui/src/app/gateway-store.ts index ccfa28dc24b3..7afc6245777c 100644 --- a/ui/src/app/gateway-store.ts +++ b/ui/src/app/gateway-store.ts @@ -1,4 +1,4 @@ -import { readControlUiBuildMismatchDetails } from "@openclaw/gateway-client/browser"; +import { readControlUiBuildMismatchId } from "@openclaw/gateway-client/browser"; import type { ControlUiBootstrapProfileHint } from "../../../src/gateway/control-ui-contract.js"; // Control UI module owns the application gateway store: the reactive // snapshot around GatewayBrowserClient consumed by the app shell. @@ -431,9 +431,9 @@ export function createApplicationGateway( return; } stopCanvasSurfaceLease(); - const buildMismatch = readControlUiBuildMismatchDetails(error?.details); - if (buildMismatch) { - void scheduleStaleChunkReload({ buildId: buildMismatch.gatewayBuildId }); + const mismatchedBuildId = readControlUiBuildMismatchId(error?.details); + if (mismatchedBuildId) { + void scheduleStaleChunkReload({ buildId: mismatchedBuildId }); } setSnapshot({ ...snapshot,