diff --git a/scripts/openclaw-cross-os-release-checks.ts b/scripts/openclaw-cross-os-release-checks.ts index 4b6fe265fee..c667429927d 100644 --- a/scripts/openclaw-cross-os-release-checks.ts +++ b/scripts/openclaw-cross-os-release-checks.ts @@ -774,15 +774,35 @@ async function runUpgradeLane(params) { "--timeout", String(updateStepTimeoutSeconds()), ]; - const updateResult = await runOpenClaw({ - lane, - env: updateEnv, - args: updateArgs, - logPath: join(params.logsDir, "upgrade-update.log"), - timeoutMs: updateTimeoutMs(), - check: false, - }); + const updateLogPath = join(params.logsDir, "upgrade-update.log"); + let updateResult; + let usedWindowsPackagedUpgradeTimeoutFallback = false; + try { + updateResult = await runOpenClaw({ + lane, + env: updateEnv, + args: updateArgs, + logPath: updateLogPath, + timeoutMs: updateTimeoutMs(), + check: false, + }); + } catch (error) { + if (!isRecoverableWindowsPackagedUpgradeTimeoutError(error, process.platform)) { + throw error; + } + usedWindowsPackagedUpgradeTimeoutFallback = true; + appendFileSync( + updateLogPath, + `\n[release-checks] Windows baseline updater timed out after fetching candidate; falling back to direct candidate install: ${formatError(error)}\n`, + ); + updateResult = { + exitCode: 124, + stdout: "", + stderr: formatError(error), + }; + } const usedWindowsPackagedUpgradeFallback = + usedWindowsPackagedUpgradeTimeoutFallback || isRecoverableWindowsPackagedUpgradeSwapCleanupFailure(updateResult, process.platform); if (usedWindowsPackagedUpgradeFallback) { logLanePhase(lane, "update-fallback-install"); @@ -1362,6 +1382,22 @@ export function isRecoverableWindowsPackagedUpgradeSwapCleanupFailure( ); } +export function isRecoverableWindowsPackagedUpgradeTimeoutError( + error, + platform = process.platform, +) { + if (platform !== "win32") { + return false; + } + const message = error instanceof Error ? error.message : String(error); + return ( + /\bCommand timed out:/u.test(message) && + /[/\\]openclaw\.mjs update --tag http:\/\/127\.0\.0\.1:\d+\/openclaw-current\.tgz --yes --json --timeout \d+/u.test( + message, + ) + ); +} + export function shouldRunPackagedUpgradeStatusProbe({ platform = process.platform, usedWindowsPackagedUpgradeFallback, diff --git a/test/scripts/openclaw-cross-os-release-checks.test.ts b/test/scripts/openclaw-cross-os-release-checks.test.ts index ba536f51e0d..c197732a3ab 100644 --- a/test/scripts/openclaw-cross-os-release-checks.test.ts +++ b/test/scripts/openclaw-cross-os-release-checks.test.ts @@ -37,6 +37,7 @@ import { CROSS_OS_AGENT_TURN_TIMEOUT_SECONDS, isImmutableReleaseRef, isRecoverableWindowsPackagedUpgradeSwapCleanupFailure, + isRecoverableWindowsPackagedUpgradeTimeoutError, looksLikeReleaseVersionRef, normalizeRequestedRef, normalizeWindowsCommandShimPath, @@ -730,6 +731,21 @@ describe("scripts/openclaw-cross-os-release-checks", () => { ).toBe(true); }); + it("recognizes the shipped Windows updater packaged-upgrade timeout", () => { + const error = new Error( + "Command timed out: C:\\hostedtoolcache\\windows\\node\\24.15.0\\x64\\node.exe C:\\Users\\RUNNER~1\\AppData\\Local\\Temp\\openclaw-upgrade-q9DsA7\\prefix\\node_modules\\openclaw\\openclaw.mjs update --tag http://127.0.0.1:49951/openclaw-current.tgz --yes --json --timeout 1500", + ); + + expect(isRecoverableWindowsPackagedUpgradeTimeoutError(error, "win32")).toBe(true); + expect(isRecoverableWindowsPackagedUpgradeTimeoutError(error, "linux")).toBe(false); + expect( + isRecoverableWindowsPackagedUpgradeTimeoutError( + new Error("Command timed out: node openclaw.mjs update --tag openclaw@beta"), + "win32", + ), + ).toBe(false); + }); + it("skips the packaged upgrade status probe after the Windows fallback install", () => { expect( shouldRunPackagedUpgradeStatusProbe({