mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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:
@@ -68,6 +68,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- **Updater plugin convergence:** keep pre-plugin doctor passes from installing configured plugins before the updater's plugin sweep, while preserving the final post-plugin migration pass and preventing ambient update-phase state from leaking into fresh doctor processes.
|
||||
- **Control UI browser tab identity:** keep selected tab styling, accessibility, focus, address, and page snapshot aligned across in-place navigation and tab reordering. Fixes #120745. Thanks @shakkernerd.
|
||||
- **Control UI staged attachments:** preserve unsent images, files, pasted images, and large pasted text across same-tab route and narrow split-pane remounts while keeping pane close, mismatched pane/session/Gateway remounts, application shutdown, and hard reload as cleanup boundaries. Fixes #121519. Thanks @shakkernerd.
|
||||
- **Control UI browser annotations:** keep marked screenshots and generated page context together in structured composer cards, preserve user-written drafts when annotations are removed or replaced, retain complete unsent annotation packages across same-tab route and active split-pane remounts, and offer bounded Undo without restoring removed context into another session. Fixes #120744. Thanks @shakkernerd.
|
||||
|
||||
@@ -2501,7 +2501,7 @@ describe("update-cli", () => {
|
||||
});
|
||||
|
||||
it("post-core resume mode skips the core update and only runs post-update tasks", async () => {
|
||||
await runPostCoreCommand({ restart: false });
|
||||
await runPostCoreCommand({ restart: false }, { OPENCLAW_UPDATE_POST_CORE_CONVERGENCE: "1" });
|
||||
|
||||
expect(runGatewayUpdate).not.toHaveBeenCalled();
|
||||
const installCall = (
|
||||
@@ -2524,6 +2524,21 @@ describe("update-cli", () => {
|
||||
([, args]) => args[0] === FRESH_POST_UPDATE_ENTRYPOINT && args[1] === "doctor",
|
||||
);
|
||||
expect(freshDoctorCall).toBeDefined();
|
||||
expect(freshDoctorCall?.[2]).toMatchObject({
|
||||
env: {
|
||||
OPENCLAW_UPDATE_IN_PROGRESS: "1",
|
||||
OPENCLAW_UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR: "1",
|
||||
OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE: "1",
|
||||
},
|
||||
});
|
||||
expect(
|
||||
(freshDoctorCall?.[2] as { env?: NodeJS.ProcessEnv } | undefined)?.env
|
||||
?.OPENCLAW_UPDATE_POST_CORE_CONVERGENCE,
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
(freshDoctorCall?.[2] as { baseEnv?: NodeJS.ProcessEnv } | undefined)?.baseEnv
|
||||
?.OPENCLAW_UPDATE_POST_CORE_CONVERGENCE,
|
||||
).toBeUndefined();
|
||||
expect(vi.mocked(runExec).mock.invocationCallOrder[0] ?? 0).toBeLessThan(
|
||||
syncPluginsForUpdateChannel.mock.invocationCallOrder[0] ?? 0,
|
||||
);
|
||||
@@ -7248,13 +7263,13 @@ describe("update-cli", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("updateFinalizeCommand runs doctor and plugin convergence with full update env", async () => {
|
||||
it("updateFinalizeCommand defers plugin installation during pre-plugin doctor", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
OPENCLAW_UPDATE_IN_PROGRESS: undefined,
|
||||
OPENCLAW_UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR: undefined,
|
||||
OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE: undefined,
|
||||
OPENCLAW_UPDATE_POST_CORE_CONVERGENCE: undefined,
|
||||
OPENCLAW_UPDATE_POST_CORE_CONVERGENCE: "1",
|
||||
},
|
||||
async () => {
|
||||
let doctorEnv: NodeJS.ProcessEnv | undefined;
|
||||
@@ -7274,11 +7289,11 @@ describe("update-cli", () => {
|
||||
expect(doctorEnv?.OPENCLAW_UPDATE_IN_PROGRESS).toBe("1");
|
||||
expect(doctorEnv?.OPENCLAW_UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR).toBe("1");
|
||||
expect(doctorEnv?.OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE).toBe("1");
|
||||
expect(doctorEnv?.OPENCLAW_UPDATE_POST_CORE_CONVERGENCE).toBe("1");
|
||||
expect(doctorEnv?.OPENCLAW_UPDATE_POST_CORE_CONVERGENCE).toBeUndefined();
|
||||
expect(process.env.OPENCLAW_UPDATE_IN_PROGRESS).toBeUndefined();
|
||||
expect(process.env.OPENCLAW_UPDATE_DEFER_CONFIGURED_PLUGIN_INSTALL_REPAIR).toBeUndefined();
|
||||
expect(process.env.OPENCLAW_UPDATE_PARENT_SUPPORTS_DOCTOR_CONFIG_WRITE).toBeUndefined();
|
||||
expect(process.env.OPENCLAW_UPDATE_POST_CORE_CONVERGENCE).toBeUndefined();
|
||||
expect(process.env.OPENCLAW_UPDATE_POST_CORE_CONVERGENCE).toBe("1");
|
||||
expect(doctorCommand).toHaveBeenCalledWith(defaultRuntime, {
|
||||
nonInteractive: true,
|
||||
repair: true,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user