diff --git a/CHANGELOG.md b/CHANGELOG.md index 96fce899305..da30874f1cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- CLI/update: keep the automatic post-update completion refresh on the core-command tree so it no longer stages bundled plugin runtime deps before the Gateway restart path, avoiding `.24` update hangs and 1006 disconnect cascades. Fixes #72665. Thanks @sakalaboator and @He-Pin. - Agents/Bedrock: stop heartbeat runs from persisting blank user transcript turns and repair existing blank user text messages before replay, preventing AWS Bedrock `ContentBlock` blank-text validation failures. Fixes #72640 and #72622. Thanks @goldzulu. - LM Studio: allow interactive onboarding to leave the API key blank for unauthenticated local servers, using local synthetic auth while clearing stale LM Studio auth profiles. Fixes #66937. Thanks @olamedia. - Process/Windows: decode command stdout and stderr from raw bytes with console-codepage awareness, while preserving valid UTF-8 output and multibyte characters split across chunks. Fixes #50519. Thanks @iready, @kevinten10, @zhangyongjie1997, @knightplat-blip, @heiqishi666, and @slepybear. diff --git a/docs/cli/update.md b/docs/cli/update.md index 07803ceeb70..4eeef6822a1 100644 --- a/docs/cli/update.md +++ b/docs/cli/update.md @@ -87,9 +87,10 @@ The Gateway core auto-updater (when enabled via config) reuses this same update For package-manager installs, `openclaw update` resolves the target package version before invoking the package manager. Even when the installed version already matches the target, the command refreshes the global package install, -then runs plugin sync, completion refresh, and restart work. This keeps packaged -sidecars and channel-owned plugin records aligned with the installed OpenClaw -build. +then runs plugin sync, a core-command completion refresh, and restart work. This +keeps packaged sidecars and channel-owned plugin records aligned with the +installed OpenClaw build while leaving full plugin-command completion rebuilds to +explicit `openclaw completion --write-state` runs. ## Git checkout flow diff --git a/src/cli/completion-cli.ts b/src/cli/completion-cli.ts index 2b21d53fdcc..e2f1980340e 100644 --- a/src/cli/completion-cli.ts +++ b/src/cli/completion-cli.ts @@ -10,6 +10,7 @@ import { } from "./completion-fish.js"; import { COMPLETION_SHELLS, + COMPLETION_SKIP_PLUGIN_COMMANDS_ENV, installCompletion, isCompletionShell, resolveCompletionCachePath, @@ -106,10 +107,12 @@ export function registerCompletionCli(program: Command) { // Eagerly register all subcommands except completion itself to build the full tree. await registerSubcommandsForCompletion(program); - const { registerPluginCliCommandsFromValidatedConfig } = await import("../plugins/cli.js"); - await registerPluginCliCommandsFromValidatedConfig(program, undefined, undefined, { - mode: "eager", - }); + if (process.env[COMPLETION_SKIP_PLUGIN_COMMANDS_ENV] !== "1") { + const { registerPluginCliCommandsFromValidatedConfig } = await import("../plugins/cli.js"); + await registerPluginCliCommandsFromValidatedConfig(program, undefined, undefined, { + mode: "eager", + }); + } if (options.writeState) { const writeShells = options.shell ? [shell] : [...COMPLETION_SHELLS]; diff --git a/src/cli/completion-cli.write-state.test.ts b/src/cli/completion-cli.write-state.test.ts index ed9951b7b17..d899b1c5e44 100644 --- a/src/cli/completion-cli.write-state.test.ts +++ b/src/cli/completion-cli.write-state.test.ts @@ -106,4 +106,35 @@ describe("completion-cli write-state", () => { await fs.rm(stateDir, { recursive: true, force: true }); await fs.rm(homeDir, { recursive: true, force: true }); }); + + it("can skip plugin command registration for update-triggered cache writes", async () => { + const [{ COMPLETION_SKIP_PLUGIN_COMMANDS_ENV }, { registerCompletionCli }] = await Promise.all([ + import("./completion-runtime.js"), + import("./completion-cli.js"), + ]); + const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-completion-state-")); + const homeDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-completion-home-")); + + process.env.OPENCLAW_STATE_DIR = stateDir; + process.env.HOME = homeDir; + process.env[COMPLETION_SKIP_PLUGIN_COMMANDS_ENV] = "1"; + + try { + const program = new Command(); + program.name("openclaw"); + registerCompletionCli(program); + + await program.parseAsync(["completion", "--write-state"], { from: "user" }); + + expect(registerSubCliByNameMock).toHaveBeenCalledWith(program, "qa"); + expect(registerPluginCliCommandsFromValidatedConfigMock).not.toHaveBeenCalled(); + expect(await fs.readdir(path.join(stateDir, "completions"))).toEqual( + expect.arrayContaining(["openclaw.bash", "openclaw.fish", "openclaw.ps1", "openclaw.zsh"]), + ); + } finally { + delete process.env[COMPLETION_SKIP_PLUGIN_COMMANDS_ENV]; + await fs.rm(stateDir, { recursive: true, force: true }); + await fs.rm(homeDir, { recursive: true, force: true }); + } + }); }); diff --git a/src/cli/completion-runtime.ts b/src/cli/completion-runtime.ts index 06bca656d8b..408eaffa478 100644 --- a/src/cli/completion-runtime.ts +++ b/src/cli/completion-runtime.ts @@ -10,6 +10,7 @@ import { pathExists } from "../utils.js"; export const COMPLETION_SHELLS = ["zsh", "bash", "powershell", "fish"] as const; export type CompletionShell = (typeof COMPLETION_SHELLS)[number]; +export const COMPLETION_SKIP_PLUGIN_COMMANDS_ENV = "OPENCLAW_COMPLETION_SKIP_PLUGIN_COMMANDS"; export function isCompletionShell(value: string): value is CompletionShell { return COMPLETION_SHELLS.includes(value as CompletionShell); diff --git a/src/cli/update-cli.test.ts b/src/cli/update-cli.test.ts index 849b286c4b1..45657e90470 100644 --- a/src/cli/update-cli.test.ts +++ b/src/cli/update-cli.test.ts @@ -540,7 +540,12 @@ describe("update-cli", () => { expect(spawnSync).toHaveBeenCalledWith( expect.any(String), [path.join(root, "openclaw.mjs"), "completion", "--write-state"], - expect.objectContaining({ timeout: 30_000 }), + expect.objectContaining({ + env: expect.objectContaining({ + OPENCLAW_COMPLETION_SKIP_PLUGIN_COMMANDS: "1", + }), + timeout: 30_000, + }), ); }); diff --git a/src/cli/update-cli/shared.ts b/src/cli/update-cli/shared.ts index c1ccd2de67b..3ab4104c8a1 100644 --- a/src/cli/update-cli/shared.ts +++ b/src/cli/update-cli/shared.ts @@ -22,6 +22,7 @@ import { defaultRuntime } from "../../runtime.js"; import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; import { theme } from "../../terminal/theme.js"; import { pathExists } from "../../utils.js"; +import { COMPLETION_SKIP_PLUGIN_COMMANDS_ENV } from "../completion-runtime.js"; export type UpdateCommandOptions = { json?: boolean; @@ -268,7 +269,10 @@ export async function tryWriteCompletionCache(root: string, jsonMode: boolean): const result = spawnSync(resolveNodeRunner(), [binPath, "completion", "--write-state"], { cwd: root, - env: process.env, + env: { + ...process.env, + [COMPLETION_SKIP_PLUGIN_COMMANDS_ENV]: "1", + }, encoding: "utf-8", timeout: COMPLETION_CACHE_WRITE_TIMEOUT_MS, });