fix: roll back plugin index for update channel writes

This commit is contained in:
Shakker
2026-04-26 01:24:56 +01:00
parent 41282fcb13
commit e7c131d6de
7 changed files with 78 additions and 67 deletions

View File

@@ -92,6 +92,7 @@ Docs: https://docs.openclaw.ai
image models keep their `input: ["text", "image"]` capability. Fixes #33185.
Thanks @Kobe9312 and @vincentkoc.
- Plugins/install: restore the previous plugin index records if a concurrent config write conflict interrupts install, update, or uninstall metadata commits. Thanks @shakkernerd.
- Plugins/update: restore previous plugin index records if core update or channel setup hits a concurrent config write conflict after plugin metadata changes. Thanks @shakkernerd.
- Sessions: keep embedded runtime context out of the visible user prompt by
sending it as a hidden next-turn custom message, and teach doctor to repair
affected 2026.4.24 transcripts with duplicated prompt-rewrite branches.

View File

@@ -2,16 +2,19 @@ import { replaceConfigFile } from "../config/config.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { PluginInstallRecord } from "../config/types.plugins.js";
import {
loadInstalledPluginIndexInstallRecords,
PLUGIN_INSTALLS_CONFIG_PATH,
writePersistedInstalledPluginIndexInstallRecords,
} from "../plugins/installed-plugin-index-records.js";
export async function commitPluginInstallRecordsWithConfig(params: {
previousInstallRecords: Record<string, PluginInstallRecord>;
previousInstallRecords?: Record<string, PluginInstallRecord>;
nextInstallRecords: Record<string, PluginInstallRecord>;
nextConfig: OpenClawConfig;
baseHash?: string;
}): Promise<void> {
const previousInstallRecords =
params.previousInstallRecords ?? (await loadInstalledPluginIndexInstallRecords());
await writePersistedInstalledPluginIndexInstallRecords(params.nextInstallRecords);
try {
await replaceConfigFile({
@@ -21,7 +24,7 @@ export async function commitPluginInstallRecordsWithConfig(params: {
});
} catch (error) {
try {
await writePersistedInstalledPluginIndexInstallRecords(params.previousInstallRecords);
await writePersistedInstalledPluginIndexInstallRecords(previousInstallRecords);
} catch (rollbackError) {
throw new Error(
"Failed to commit plugin install records and could not restore the previous plugin index",

View File

@@ -44,9 +44,7 @@ import {
import { runGatewayUpdate, type UpdateRunResult } from "../../infra/update-runner.js";
import {
loadInstalledPluginIndexInstallRecords,
PLUGIN_INSTALLS_CONFIG_PATH,
withoutPluginInstallRecords,
writePersistedInstalledPluginIndexInstallRecords,
withPluginInstallRecords,
} from "../../plugins/installed-plugin-index-records.js";
import { syncPluginsForUpdateChannel, updateNpmInstalledPlugins } from "../../plugins/update.js";
@@ -64,6 +62,7 @@ import {
terminateStaleGatewayPids,
waitForGatewayHealthyRestart,
} from "../daemon-cli/restart-health.js";
import { commitPluginInstallRecordsWithConfig } from "../plugins-install-record-commit.js";
import { listPersistedBundledPluginLocationBridges } from "../plugins-location-bridges.js";
import { refreshPluginRegistryAfterConfigMutation } from "../plugins-registry-refresh.js";
import { createUpdateProgress, printResult } from "./progress.js";
@@ -628,17 +627,19 @@ async function updatePluginsAfterCoreUpdate(params: {
pluginConfig = npmResult.config;
if (syncResult.changed || npmResult.changed) {
await writePersistedInstalledPluginIndexInstallRecords(pluginConfig.plugins?.installs ?? {});
const nextInstallRecords = pluginConfig.plugins?.installs ?? {};
const nextConfig = withoutPluginInstallRecords(pluginConfig);
await replaceConfigFile({
await commitPluginInstallRecordsWithConfig({
previousInstallRecords: pluginInstallRecords,
nextInstallRecords,
nextConfig,
baseHash: params.configSnapshot.hash,
writeOptions: { unsetPaths: [Array.from(PLUGIN_INSTALLS_CONFIG_PATH)] },
});
await refreshPluginRegistryAfterConfigMutation({
config: nextConfig,
reason: "source-changed",
workspaceDir: params.root,
installRecords: nextInstallRecords,
logger: pluginLogger,
});
}

View File

@@ -5,13 +5,10 @@ import { moveSingleAccountChannelSectionToDefaultAccount } from "../../channels/
import type { ChannelSetupPlugin } from "../../channels/plugins/setup-wizard-types.js";
import type { ChannelPlugin } from "../../channels/plugins/types.plugin.js";
import type { ChannelId, ChannelSetupInput } from "../../channels/plugins/types.public.js";
import { commitPluginInstallRecordsWithConfig } from "../../cli/plugins-install-record-commit.js";
import { refreshPluginRegistryAfterConfigMutation } from "../../cli/plugins-registry-refresh.js";
import { replaceConfigFile, type OpenClawConfig } from "../../config/config.js";
import {
PLUGIN_INSTALLS_CONFIG_PATH,
withoutPluginInstallRecords,
writePersistedInstalledPluginIndexInstallRecords,
} from "../../plugins/installed-plugin-index-records.js";
import { withoutPluginInstallRecords } from "../../plugins/installed-plugin-index-records.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js";
@@ -251,19 +248,22 @@ export async function channelsAddCommand(
? withoutPluginInstallRecords(nextConfig)
: nextConfig;
if (shouldMovePluginInstalls) {
await writePersistedInstalledPluginIndexInstallRecords(nextConfig.plugins?.installs ?? {});
await commitPluginInstallRecordsWithConfig({
nextInstallRecords: nextConfig.plugins?.installs ?? {},
nextConfig: writtenConfig,
...(baseHash !== undefined ? { baseHash } : {}),
});
} else {
await replaceConfigFile({
nextConfig: writtenConfig,
...(baseHash !== undefined ? { baseHash } : {}),
});
}
await replaceConfigFile({
nextConfig: writtenConfig,
...(baseHash !== undefined ? { baseHash } : {}),
...(shouldMovePluginInstalls
? { writeOptions: { unsetPaths: [Array.from(PLUGIN_INSTALLS_CONFIG_PATH)] } }
: {}),
});
if (shouldMovePluginInstalls) {
await refreshPluginRegistryAfterConfigMutation({
config: writtenConfig,
reason: "source-changed",
installRecords: nextConfig.plugins?.installs ?? {},
logger: { warn: (message) => runtime.log(message) },
});
}
@@ -402,19 +402,22 @@ export async function channelsAddCommand(
? withoutPluginInstallRecords(nextConfig)
: nextConfig;
if (shouldMovePluginInstalls) {
await writePersistedInstalledPluginIndexInstallRecords(nextConfig.plugins?.installs ?? {});
await commitPluginInstallRecordsWithConfig({
nextInstallRecords: nextConfig.plugins?.installs ?? {},
nextConfig: writtenConfig,
...(baseHash !== undefined ? { baseHash } : {}),
});
} else {
await replaceConfigFile({
nextConfig: writtenConfig,
...(baseHash !== undefined ? { baseHash } : {}),
});
}
await replaceConfigFile({
nextConfig: writtenConfig,
...(baseHash !== undefined ? { baseHash } : {}),
...(shouldMovePluginInstalls
? { writeOptions: { unsetPaths: [Array.from(PLUGIN_INSTALLS_CONFIG_PATH)] } }
: {}),
});
if (shouldMovePluginInstalls || pluginRegistrySourceChanged) {
await refreshPluginRegistryAfterConfigMutation({
config: writtenConfig,
reason: "source-changed",
...(shouldMovePluginInstalls ? { installRecords: nextConfig.plugins?.installs ?? {} } : {}),
logger: { warn: (message) => runtime.log(message) },
});
}

View File

@@ -10,6 +10,7 @@ import type {
ChannelCapabilitiesDisplayLine,
ChannelPlugin,
} from "../../channels/plugins/types.public.js";
import { commitPluginInstallRecordsWithConfig } from "../../cli/plugins-install-record-commit.js";
import { refreshPluginRegistryAfterConfigMutation } from "../../cli/plugins-registry-refresh.js";
import {
readConfigFileSnapshot,
@@ -18,11 +19,7 @@ import {
} from "../../config/config.js";
import { danger } from "../../globals.js";
import { formatErrorMessage } from "../../infra/errors.js";
import {
PLUGIN_INSTALLS_CONFIG_PATH,
withoutPluginInstallRecords,
writePersistedInstalledPluginIndexInstallRecords,
} from "../../plugins/installed-plugin-index-records.js";
import { withoutPluginInstallRecords } from "../../plugins/installed-plugin-index-records.js";
import { defaultRuntime, type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
import {
normalizeLowercaseStringOrEmpty,
@@ -259,21 +256,25 @@ export async function channelsCapabilitiesCommand(
const shouldMovePluginInstalls = Boolean(
cfg.plugins?.installs && Object.keys(cfg.plugins.installs).length > 0,
);
const nextInstallRecords = cfg.plugins?.installs ?? {};
if (shouldMovePluginInstalls) {
await writePersistedInstalledPluginIndexInstallRecords(cfg.plugins?.installs ?? {});
cfg = withoutPluginInstallRecords(cfg);
await commitPluginInstallRecordsWithConfig({
nextInstallRecords,
nextConfig: cfg,
baseHash: (await sourceSnapshotPromise)?.hash,
});
} else {
await replaceConfigFile({
nextConfig: cfg,
baseHash: (await sourceSnapshotPromise)?.hash,
});
}
await replaceConfigFile({
nextConfig: cfg,
baseHash: (await sourceSnapshotPromise)?.hash,
...(shouldMovePluginInstalls
? { writeOptions: { unsetPaths: [Array.from(PLUGIN_INSTALLS_CONFIG_PATH)] } }
: {}),
});
if (shouldMovePluginInstalls || resolved.pluginInstalled) {
await refreshPluginRegistryAfterConfigMutation({
config: cfg,
reason: "source-changed",
...(shouldMovePluginInstalls ? { installRecords: nextInstallRecords } : {}),
logger: { warn: (message) => runtime.log(message) },
});
}

View File

@@ -4,13 +4,10 @@ import {
listChannelPlugins,
normalizeChannelId,
} from "../../channels/plugins/index.js";
import { commitPluginInstallRecordsWithConfig } from "../../cli/plugins-install-record-commit.js";
import { refreshPluginRegistryAfterConfigMutation } from "../../cli/plugins-registry-refresh.js";
import { replaceConfigFile, type OpenClawConfig } from "../../config/config.js";
import {
PLUGIN_INSTALLS_CONFIG_PATH,
withoutPluginInstallRecords,
writePersistedInstalledPluginIndexInstallRecords,
} from "../../plugins/installed-plugin-index-records.js";
import { withoutPluginInstallRecords } from "../../plugins/installed-plugin-index-records.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
@@ -180,21 +177,25 @@ export async function channelsRemoveCommand(
const shouldMovePluginInstalls = Boolean(
next.plugins?.installs && Object.keys(next.plugins.installs).length > 0,
);
const nextInstallRecords = next.plugins?.installs ?? {};
if (shouldMovePluginInstalls) {
await writePersistedInstalledPluginIndexInstallRecords(next.plugins?.installs ?? {});
next = withoutPluginInstallRecords(next);
await commitPluginInstallRecordsWithConfig({
nextInstallRecords,
nextConfig: next,
...(baseHash !== undefined ? { baseHash } : {}),
});
} else {
await replaceConfigFile({
nextConfig: next,
...(baseHash !== undefined ? { baseHash } : {}),
});
}
await replaceConfigFile({
nextConfig: next,
...(baseHash !== undefined ? { baseHash } : {}),
...(shouldMovePluginInstalls
? { writeOptions: { unsetPaths: [Array.from(PLUGIN_INSTALLS_CONFIG_PATH)] } }
: {}),
});
if (shouldMovePluginInstalls || resolvedPluginState?.pluginInstalled) {
await refreshPluginRegistryAfterConfigMutation({
config: next,
reason: "source-changed",
...(shouldMovePluginInstalls ? { installRecords: nextInstallRecords } : {}),
logger: { warn: (message) => runtime.log(message) },
});
}

View File

@@ -5,15 +5,12 @@ import type {
} from "../../channels/plugins/types.adapters.js";
import { resolveCommandConfigWithSecrets } from "../../cli/command-config-resolution.js";
import { getChannelsCommandSecretTargetIds } from "../../cli/command-secret-targets.js";
import { commitPluginInstallRecordsWithConfig } from "../../cli/plugins-install-record-commit.js";
import { refreshPluginRegistryAfterConfigMutation } from "../../cli/plugins-registry-refresh.js";
import { loadConfig, readConfigFileSnapshot, replaceConfigFile } from "../../config/config.js";
import { danger } from "../../globals.js";
import { resolveMessageChannelSelection } from "../../infra/outbound/channel-selection.js";
import {
PLUGIN_INSTALLS_CONFIG_PATH,
withoutPluginInstallRecords,
writePersistedInstalledPluginIndexInstallRecords,
} from "../../plugins/installed-plugin-index-records.js";
import { withoutPluginInstallRecords } from "../../plugins/installed-plugin-index-records.js";
import { type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
import {
normalizeLowercaseStringOrEmpty,
@@ -148,21 +145,25 @@ export async function channelsResolveCommand(opts: ChannelsResolveOptions, runti
const shouldMovePluginInstalls = Boolean(
cfg.plugins?.installs && Object.keys(cfg.plugins.installs).length > 0,
);
const nextInstallRecords = cfg.plugins?.installs ?? {};
if (shouldMovePluginInstalls) {
await writePersistedInstalledPluginIndexInstallRecords(cfg.plugins?.installs ?? {});
cfg = withoutPluginInstallRecords(cfg);
await commitPluginInstallRecordsWithConfig({
nextInstallRecords,
nextConfig: cfg,
baseHash: (await sourceSnapshotPromise)?.hash,
});
} else {
await replaceConfigFile({
nextConfig: cfg,
baseHash: (await sourceSnapshotPromise)?.hash,
});
}
await replaceConfigFile({
nextConfig: cfg,
baseHash: (await sourceSnapshotPromise)?.hash,
...(shouldMovePluginInstalls
? { writeOptions: { unsetPaths: [Array.from(PLUGIN_INSTALLS_CONFIG_PATH)] } }
: {}),
});
if (shouldMovePluginInstalls || resolvedExplicit.pluginInstalled) {
await refreshPluginRegistryAfterConfigMutation({
config: cfg,
reason: "source-changed",
...(shouldMovePluginInstalls ? { installRecords: nextInstallRecords } : {}),
logger: { warn: (message) => runtime.log(message) },
});
}