fix(update): pin post-core plugin compatibility to the downgraded core version (#87914) (#87952)

* fix(update): pin post-core plugin compatibility to the downgraded core version (#87914)

* fix(update): force plugin compatibility repair on rollback

* style(update): clarify downgrade compatibility note

* fix(plugins): resolve compatible prerelease plugin downgrades

* fix(plugins): honor host gates during npm downgrade repair

* fix(plugins): keep prerelease downgrade fallback on channel

---------

Co-authored-by: Gio Della-Libera <giodl73@gmail.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Mukunda Rao Katta
2026-06-02 04:13:26 -07:00
committed by GitHub
parent 30b9e123b8
commit 2d61521bd3
6 changed files with 487 additions and 17 deletions
+73 -11
View File
@@ -88,6 +88,7 @@ import { runGatewayUpdate, type UpdateRunResult } from "../../infra/update-runne
import { normalizePluginsConfig, resolveEffectiveEnableState } from "../../plugins/config-state.js";
import {
loadInstalledPluginIndexInstallRecords,
writePersistedInstalledPluginIndexInstallRecords,
withoutPluginInstallRecords,
withPluginInstallRecords,
} from "../../plugins/installed-plugin-index-records.js";
@@ -2716,6 +2717,35 @@ export function resolvePostCoreUpdateChildStdio(
return platform === "win32" ? "pipe" : "inherit";
}
function preparePostCorePluginInstallRecordsForFreshProcess(params: {
records: Record<string, PluginInstallRecord>;
targetVersion: string | null;
}): Record<string, PluginInstallRecord> {
if (!params.targetVersion) {
return params.records;
}
const runtimeComparison = compareSemverStrings(VERSION, params.targetVersion);
if (runtimeComparison === null || runtimeComparison <= 0) {
return params.records;
}
let changed = false;
const next: Record<string, PluginInstallRecord> = {};
for (const [pluginId, record] of Object.entries(params.records)) {
const installedVersion = record.resolvedVersion ?? record.version;
const comparison = installedVersion
? compareSemverStrings(installedVersion, params.targetVersion)
: null;
if (record.source !== "npm" || comparison === null || comparison <= 0) {
next[pluginId] = record;
continue;
}
const { resolvedSpec: _resolvedSpec, resolvedVersion: _resolvedVersion, ...rest } = record;
next[pluginId] = rest;
changed = true;
}
return changed ? next : params.records;
}
async function continuePostCoreUpdateInFreshProcess(params: {
root: string;
channel: "stable" | "beta" | "dev";
@@ -2750,8 +2780,16 @@ async function continuePostCoreUpdateInFreshProcess(params: {
const sourceConfigPath = path.join(resultDir, "source-config.json");
const postCoreHostVersion = await readPackageVersion(params.root);
const pluginInstallRecords = preparePostCorePluginInstallRecordsForFreshProcess({
records: params.pluginInstallRecords,
targetVersion: postCoreHostVersion,
});
try {
await writePostCorePluginInstallRecordsFile(installRecordsPath, params.pluginInstallRecords);
if (pluginInstallRecords && pluginInstallRecords !== params.pluginInstallRecords) {
await writePersistedInstalledPluginIndexInstallRecords(pluginInstallRecords);
}
await writePostCorePluginInstallRecordsFile(installRecordsPath, pluginInstallRecords);
await writePostCoreSourceConfigFile(sourceConfigPath, params.preUpdateConfig);
const childStdio = resolvePostCoreUpdateChildStdio();
const child = spawn(params.nodeRunner ?? resolveNodeRunner(), argv, {
@@ -3520,16 +3558,40 @@ async function updateCommandInternal(opts: UpdateCommandOptions): Promise<void>
: undefined,
);
postUpdateConfigSnapshot = restoredConfig.snapshot;
postCorePluginUpdate = await runPostCorePluginUpdate({
root: postUpdateRoot,
channel,
configSnapshot: postUpdateConfigSnapshot,
configChanged: restoredConfig.changed,
restoredAuthoredChannels: restoredConfig.authoredChannels,
opts,
timeoutMs: updateStepTimeoutMs,
pluginInstallRecords: preUpdatePluginInstallRecords,
});
// Current-process post-core convergence still reports the pre-update
// VERSION. During downgrades, pin compatibility checks to the installed
// target so incompatible newer plugins are disabled before restart.
const postUpdateInstalledVersion = await readPackageVersion(postUpdateRoot);
const versionComparison =
postUpdateInstalledVersion && VERSION
? compareSemverStrings(VERSION, postUpdateInstalledVersion)
: null;
const compatibilityDowngradeTarget =
versionComparison != null && versionComparison > 0 ? postUpdateInstalledVersion : null;
const previousCompatibilityHostVersion = process.env.OPENCLAW_COMPATIBILITY_HOST_VERSION;
if (compatibilityDowngradeTarget) {
process.env.OPENCLAW_COMPATIBILITY_HOST_VERSION = compatibilityDowngradeTarget;
}
try {
postCorePluginUpdate = await runPostCorePluginUpdate({
root: postUpdateRoot,
channel,
configSnapshot: postUpdateConfigSnapshot,
configChanged: restoredConfig.changed,
restoredAuthoredChannels: restoredConfig.authoredChannels,
opts,
timeoutMs: updateStepTimeoutMs,
pluginInstallRecords: preUpdatePluginInstallRecords,
});
} finally {
if (compatibilityDowngradeTarget) {
if (previousCompatibilityHostVersion === undefined) {
delete process.env.OPENCLAW_COMPATIBILITY_HOST_VERSION;
} else {
process.env.OPENCLAW_COMPATIBILITY_HOST_VERSION = previousCompatibilityHostVersion;
}
}
}
}
const resultWithPostUpdate: UpdateRunResult = postCorePluginUpdate