diff --git a/src/commands/doctor/shared/release-configured-plugin-installs.test.ts b/src/commands/doctor/shared/release-configured-plugin-installs.test.ts index 2db87b5e384..a83221168ab 100644 --- a/src/commands/doctor/shared/release-configured-plugin-installs.test.ts +++ b/src/commands/doctor/shared/release-configured-plugin-installs.test.ts @@ -271,6 +271,46 @@ describe("configured plugin install release step", () => { expect(result.completed).toBe(true); }); + it("does not stamp config during update-time deferred install repair", async () => { + mocks.repairMissingPluginInstallsForIds.mockResolvedValue({ + changes: [ + 'Deferred missing configured plugin "codex" install repair until post-update doctor.', + ], + warnings: [], + }); + + const { maybeRunConfiguredPluginInstallReleaseStep } = + await import("./release-configured-plugin-installs.js"); + const result = await maybeRunConfiguredPluginInstallReleaseStep({ + cfg: { + agents: { + defaults: { + model: "openai/gpt-5.4", + agentRuntime: { id: "codex" }, + }, + }, + }, + currentVersion: "2026.5.2-beta.1", + touchedVersion: "2026.5.1", + env: { OPENCLAW_UPDATE_IN_PROGRESS: "1" }, + }); + + expect(mocks.repairMissingPluginInstallsForIds).toHaveBeenCalledWith( + expect.objectContaining({ + pluginIds: ["codex"], + env: { OPENCLAW_UPDATE_IN_PROGRESS: "1" }, + }), + ); + expect(result).toEqual({ + changes: [ + 'Deferred missing configured plugin "codex" install repair until post-update doctor.', + ], + warnings: [], + completed: false, + touchedConfig: false, + }); + }); + it("does not touch config when install repair warns", async () => { mocks.detectPluginAutoEnableCandidates.mockReturnValue([ { pluginId: "matrix", kind: "channel-configured", channelId: "matrix" }, diff --git a/src/commands/doctor/shared/release-configured-plugin-installs.ts b/src/commands/doctor/shared/release-configured-plugin-installs.ts index e755bc60666..88a13f98bf1 100644 --- a/src/commands/doctor/shared/release-configured-plugin-installs.ts +++ b/src/commands/doctor/shared/release-configured-plugin-installs.ts @@ -5,6 +5,7 @@ import { isChannelConfigured } from "../../../config/channel-configured.js"; import { detectPluginAutoEnableCandidates } from "../../../config/plugin-auto-enable.js"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import { compareOpenClawVersions } from "../../../config/version.js"; +import { isTruthyEnvValue } from "../../../infra/env.js"; import { resolveProviderInstallCatalogEntries } from "../../../plugins/provider-install-catalog.js"; import { resolveWebSearchInstallCatalogEntry } from "../../../plugins/web-search-install-catalog.js"; import { VERSION } from "../../../version.js"; @@ -12,6 +13,7 @@ import { repairMissingPluginInstallsForIds } from "./missing-configured-plugin-i import { asObjectRecord } from "./object.js"; export const CONFIGURED_PLUGIN_INSTALL_RELEASE_VERSION = "2026.5.2-beta.1"; +const UPDATE_IN_PROGRESS_ENV = "OPENCLAW_UPDATE_IN_PROGRESS"; const AGENT_HARNESS_RUNTIME_PLUGIN_IDS: Readonly> = { // Codex can be selected as a harness for OpenAI models without a plugin entry. @@ -343,9 +345,10 @@ export async function maybeRunConfiguredPluginInstallReleaseStep(params: { return { changes: [], warnings: [], completed: false, touchedConfig: false }; } const env = params.env ?? process.env; + const updateInProgress = isTruthyEnvValue(env[UPDATE_IN_PROGRESS_ENV]); const configured = collectReleaseConfiguredPluginIds({ cfg: params.cfg, env }); if (configured.pluginIds.length === 0 && configured.channelIds.length === 0) { - return { changes: [], warnings: [], completed: true, touchedConfig: true }; + return { changes: [], warnings: [], completed: true, touchedConfig: !updateInProgress }; } const repaired = await repairMissingPluginInstallsForIds({ cfg: params.cfg, @@ -354,7 +357,7 @@ export async function maybeRunConfiguredPluginInstallReleaseStep(params: { blockedPluginIds: collectBlockedPluginIds(params.cfg), env, }); - const completed = repaired.warnings.length === 0; + const completed = repaired.warnings.length === 0 && !updateInProgress; return { changes: repaired.changes, warnings: repaired.warnings,