mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:50:43 +00:00
fix: roll back plugin index for update channel writes
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) },
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user