fix(update): avoid duplicate configured plugin installs (#122161)

Make updater doctor phases explicit so only post-plugin finalization marks post-core convergence, and strip ambient convergence state from fresh child environments.

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Vincent Koc
2026-08-12 01:56:40 +08:00
committed by GitHub
parent d9766c3d57
commit 0e52f4c4ce
5 changed files with 42 additions and 14 deletions
@@ -20,7 +20,9 @@ import {
stripGatewayServiceMarkerEnv,
} from "./update-command-service.js";
export function withUpdateFinalizationEnv<T>(run: () => Promise<T>): Promise<T> {
type UpdateDoctorPhase = "pre-plugin" | "post-plugin";
export async function withPrePluginUpdateDoctorEnv<T>(run: () => Promise<T>): Promise<T> {
const previousUpdateInProgress = process.env.OPENCLAW_UPDATE_IN_PROGRESS;
const previousDeferConfiguredPluginInstallRepair =
process.env[UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV];
@@ -30,8 +32,10 @@ export function withUpdateFinalizationEnv<T>(run: () => Promise<T>): Promise<T>
process.env.OPENCLAW_UPDATE_IN_PROGRESS = "1";
process.env[UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV] = "1";
process.env[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV] = "1";
process.env[UPDATE_POST_CORE_CONVERGENCE_ENV] = "1";
return run().finally(() => {
delete process.env[UPDATE_POST_CORE_CONVERGENCE_ENV];
try {
return await run();
} finally {
if (previousUpdateInProgress === undefined) {
delete process.env.OPENCLAW_UPDATE_IN_PROGRESS;
} else {
@@ -54,7 +58,7 @@ export function withUpdateFinalizationEnv<T>(run: () => Promise<T>): Promise<T>
} else {
process.env[UPDATE_POST_CORE_CONVERGENCE_ENV] = previousPostCoreConvergence;
}
});
}
}
async function withNormalConfigValidation<T>(run: () => Promise<T>): Promise<T> {
@@ -91,6 +95,7 @@ function createPostPluginDoctorExecutionFailure(
}
export async function runUpdateFinalizationDoctorInFreshProcess(params: {
phase: UpdateDoctorPhase;
root: string;
yes: boolean;
json: boolean;
@@ -110,17 +115,19 @@ export async function runUpdateFinalizationDoctorInFreshProcess(params: {
"--no-workspace-suggestions",
...(params.yes ? ["--yes"] : []),
];
const baseEnv = stripGatewayServiceMarkerEnv(disableUpdatedPackageCompileCacheEnv(process.env));
delete baseEnv[UPDATE_POST_CORE_CONVERGENCE_ENV];
const result = await runExec(params.nodeRunner ?? resolveNodeRunner(), args, {
cwd: params.root,
timeoutMs: params.timeoutMs,
maxBuffer: 4 * 1024 * 1024,
logOutput: false,
baseEnv: stripGatewayServiceMarkerEnv(disableUpdatedPackageCompileCacheEnv(process.env)),
baseEnv,
env: {
OPENCLAW_UPDATE_IN_PROGRESS: "1",
[UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR_ENV]: "1",
[UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE_ENV]: "1",
[UPDATE_POST_CORE_CONVERGENCE_ENV]: "1",
...(params.phase === "post-plugin" ? { [UPDATE_POST_CORE_CONVERGENCE_ENV]: "1" } : {}),
},
});
if (!params.json) {
@@ -186,7 +193,11 @@ async function applyFreshPostPluginDoctor(params: {
}
let pluginUpdate = params.pluginUpdate;
try {
await runUpdateFinalizationDoctorInFreshProcess({ ...params, entryPath });
await runUpdateFinalizationDoctorInFreshProcess({
...params,
entryPath,
phase: "post-plugin",
});
} catch (err) {
pluginUpdate = createPostPluginDoctorExecutionFailure(params.pluginUpdate, String(err));
}
@@ -76,7 +76,7 @@ import {
} from "./update-command-config.js";
import {
completePostCorePluginUpdate,
withUpdateFinalizationEnv,
withPrePluginUpdateDoctorEnv,
} from "./update-command-fresh-doctor.js";
import {
updatePluginsAfterCoreUpdate,
@@ -207,7 +207,7 @@ export async function updateFinalizeCommand(opts: UpdateFinalizeOptions): Promis
}
const completedPluginUpdate = await withPluginLifecycleLease({}, async () => {
const initialPluginUpdate = await withUpdateFinalizationEnv(async () => {
const initialPluginUpdate = await withPrePluginUpdateDoctorEnv(async () => {
await createUpdateConfigSnapshot();
await doctorCommand(defaultRuntime, {
nonInteractive: true,
@@ -77,6 +77,7 @@ async function resumePostCoreUpdateUnlocked(params: ResumePostCoreUpdateParams):
});
await createUpdateConfigSnapshot();
await runUpdateFinalizationDoctorInFreshProcess({
phase: "pre-plugin",
root: params.root,
yes: params.opts.yes === true,
json: params.opts.json === true,