From 98928388db03ef69cc003b69bed832ac4c70cff4 Mon Sep 17 00:00:00 2001 From: iot2edge Date: Mon, 27 Apr 2026 16:29:52 +0200 Subject: [PATCH] fix(cli): clarify completion cache timeout message after openclaw update When the post-update completion cache refresh times out (slow disk, large bundled plugin tree, Docker overlayfs), the user previously saw the opaque 'Completion cache update failed: Error: spawnSync /usr/bin/node ETIMEDOUT'. Detect ETIMEDOUT specifically, surface 'timed out after 30s', and append a manual refresh hint pointing at 'openclaw completion --write-state' so users know it's non-fatal and how to recover. Fixes #72842 --- src/cli/update-cli.test.ts | 25 +++++++++++++++++++++++++ src/cli/update-cli/shared.ts | 19 +++++++++++++++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/cli/update-cli.test.ts b/src/cli/update-cli.test.ts index dc1fa147cab..b664a303095 100644 --- a/src/cli/update-cli.test.ts +++ b/src/cli/update-cli.test.ts @@ -549,6 +549,31 @@ describe("update-cli", () => { ); }); + it("logs friendly hint with manual refresh command when completion cache write times out", async () => { + const root = createCaseDir("openclaw-completion-timeout-msg"); + pathExists.mockResolvedValue(true); + const timeoutErr = Object.assign(new Error("spawnSync /usr/bin/node ETIMEDOUT"), { + code: "ETIMEDOUT", + }); + vi.mocked(spawnSync).mockReturnValueOnce({ + pid: 0, + output: [], + stdout: "", + stderr: "", + status: null, + signal: null, + error: timeoutErr, + }); + vi.mocked(runtimeCapture.log).mockClear(); + + await updateCliShared.tryWriteCompletionCache(root, false); + + const logs = vi.mocked(runtimeCapture.log).mock.calls.map((call) => String(call[0])); + expect(logs.some((line) => line.includes("timed out after 30s"))).toBe(true); + expect(logs.some((line) => line.includes("openclaw completion --write-state"))).toBe(true); + expect(logs.some((line) => line.includes("Error: spawnSync"))).toBe(false); + }); + it("respawns into the updated package root before running post-update tasks", async () => { const { entrypoints } = setupUpdatedRootRefresh(); diff --git a/src/cli/update-cli/shared.ts b/src/cli/update-cli/shared.ts index 3ab4104c8a1..c96d85e2d0a 100644 --- a/src/cli/update-cli/shared.ts +++ b/src/cli/update-cli/shared.ts @@ -260,6 +260,8 @@ export async function resolveGlobalManager(params: { } const COMPLETION_CACHE_WRITE_TIMEOUT_MS = 30_000; +const COMPLETION_CACHE_MANUAL_REFRESH_HINT = + "Shell tab-completion may be stale; refresh manually with: openclaw completion --write-state"; export async function tryWriteCompletionCache(root: string, jsonMode: boolean): Promise { const binPath = path.join(root, "openclaw.mjs"); @@ -279,7 +281,16 @@ export async function tryWriteCompletionCache(root: string, jsonMode: boolean): if (result.error) { if (!jsonMode) { - defaultRuntime.log(theme.warn(`Completion cache update failed: ${String(result.error)}`)); + const err = result.error as NodeJS.ErrnoException; + const reason = + err.code === "ETIMEDOUT" + ? `timed out after ${COMPLETION_CACHE_WRITE_TIMEOUT_MS / 1000}s` + : String(result.error); + defaultRuntime.log( + theme.warn( + `Completion cache update failed: ${reason}. ${COMPLETION_CACHE_MANUAL_REFRESH_HINT}`, + ), + ); } return; } @@ -287,7 +298,11 @@ export async function tryWriteCompletionCache(root: string, jsonMode: boolean): if (result.status !== 0 && !jsonMode) { const stderr = (result.stderr ?? "").trim(); const detail = stderr ? ` (${stderr})` : ""; - defaultRuntime.log(theme.warn(`Completion cache update failed${detail}.`)); + defaultRuntime.log( + theme.warn( + `Completion cache update failed${detail}. ${COMPLETION_CACHE_MANUAL_REFRESH_HINT}`, + ), + ); } }