mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 08:10:42 +00:00
Summary: - The PR changes Gateway reload planning, CLI plugin install-index writes, plugin runtime/cache cleanup, docs, changelog, and tests so plugin enable/disable hot reloads while install/update/uninstall stay restart-backed. - Reproducibility: yes. The earlier blocker has a source-level reproduction: run an external plugin install/up ... watches config and only the managed plugin index changes; the PR now tests that path and queues a restart. ClawSweeper fixups: - Included follow-up commit: fix: hot reload plugin management changes - Included follow-up commit: fix(clawsweeper): address review for automerge-openclaw-openclaw-7597… - Ran the ClawSweeper repair loop before final review. Validation: - ClawSweeper review passed for head860594f722. - Required merge gates passed before the squash merge. Prepared head SHA:860594f722Review: https://github.com/openclaw/openclaw/pull/75976#issuecomment-4363168379 Co-authored-by: Peter Steinberger <steipete@gmail.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
144 lines
4.9 KiB
TypeScript
144 lines
4.9 KiB
TypeScript
import { isDeepStrictEqual } from "node:util";
|
|
import { replaceConfigFile } from "../config/config.js";
|
|
import type { ConfigWriteOptions } from "../config/io.js";
|
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
|
import {
|
|
loadInstalledPluginIndexInstallRecords,
|
|
PLUGIN_INSTALLS_CONFIG_PATH,
|
|
withoutPluginInstallRecords,
|
|
writePersistedInstalledPluginIndexInstallRecords,
|
|
} from "../plugins/installed-plugin-index-records.js";
|
|
|
|
function mergeUnsetPaths(
|
|
left?: ConfigWriteOptions["unsetPaths"],
|
|
right?: ConfigWriteOptions["unsetPaths"],
|
|
): ConfigWriteOptions["unsetPaths"] | undefined {
|
|
const merged = [...(left ?? []), ...(right ?? [])];
|
|
return merged.length > 0 ? merged : undefined;
|
|
}
|
|
|
|
type ConfigCommit = (config: OpenClawConfig, writeOptions?: ConfigWriteOptions) => Promise<void>;
|
|
const PLUGIN_SOURCE_CHANGED_RESTART_REASON = "plugin source changed";
|
|
|
|
async function commitPluginInstallRecordsWithWriter(params: {
|
|
previousInstallRecords?: Record<string, PluginInstallRecord>;
|
|
nextInstallRecords: Record<string, PluginInstallRecord>;
|
|
nextConfig: OpenClawConfig;
|
|
writeOptions?: ConfigWriteOptions;
|
|
commit: ConfigCommit;
|
|
}): Promise<void> {
|
|
const previousInstallRecords =
|
|
params.previousInstallRecords ?? (await loadInstalledPluginIndexInstallRecords());
|
|
await writePersistedInstalledPluginIndexInstallRecords(params.nextInstallRecords);
|
|
try {
|
|
const installRecordsChanged = !isDeepStrictEqual(
|
|
previousInstallRecords,
|
|
params.nextInstallRecords,
|
|
);
|
|
await params.commit(params.nextConfig, {
|
|
...params.writeOptions,
|
|
...(installRecordsChanged && params.writeOptions?.afterWrite === undefined
|
|
? { afterWrite: { mode: "restart", reason: PLUGIN_SOURCE_CHANGED_RESTART_REASON } }
|
|
: {}),
|
|
unsetPaths: mergeUnsetPaths(params.writeOptions?.unsetPaths, [
|
|
Array.from(PLUGIN_INSTALLS_CONFIG_PATH),
|
|
]),
|
|
});
|
|
} catch (error) {
|
|
try {
|
|
await writePersistedInstalledPluginIndexInstallRecords(previousInstallRecords);
|
|
} catch (rollbackError) {
|
|
throw new Error(
|
|
"Failed to commit plugin install records and could not restore the previous plugin index",
|
|
{ cause: rollbackError },
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function commitPluginInstallRecordsWithConfig(params: {
|
|
previousInstallRecords?: Record<string, PluginInstallRecord>;
|
|
nextInstallRecords: Record<string, PluginInstallRecord>;
|
|
nextConfig: OpenClawConfig;
|
|
baseHash?: string;
|
|
writeOptions?: ConfigWriteOptions;
|
|
}): Promise<void> {
|
|
await commitPluginInstallRecordsWithWriter({
|
|
...params,
|
|
commit: async (nextConfig, writeOptions) => {
|
|
await replaceConfigFile({
|
|
nextConfig,
|
|
...(params.baseHash !== undefined ? { baseHash: params.baseHash } : {}),
|
|
...(writeOptions ? { writeOptions } : {}),
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function commitConfigWriteWithPendingPluginInstalls(params: {
|
|
nextConfig: OpenClawConfig;
|
|
writeOptions?: ConfigWriteOptions;
|
|
commit: ConfigCommit;
|
|
}): Promise<{
|
|
config: OpenClawConfig;
|
|
installRecords: Record<string, PluginInstallRecord>;
|
|
movedInstallRecords: boolean;
|
|
}> {
|
|
const pendingInstallRecords = params.nextConfig.plugins?.installs ?? {};
|
|
if (Object.keys(pendingInstallRecords).length === 0) {
|
|
if (params.writeOptions) {
|
|
await params.commit(params.nextConfig, params.writeOptions);
|
|
} else {
|
|
await params.commit(params.nextConfig);
|
|
}
|
|
return {
|
|
config: params.nextConfig,
|
|
installRecords: {},
|
|
movedInstallRecords: false,
|
|
};
|
|
}
|
|
|
|
const previousInstallRecords = await loadInstalledPluginIndexInstallRecords();
|
|
const nextInstallRecords = {
|
|
...previousInstallRecords,
|
|
...pendingInstallRecords,
|
|
};
|
|
const strippedConfig = withoutPluginInstallRecords(params.nextConfig);
|
|
await commitPluginInstallRecordsWithWriter({
|
|
previousInstallRecords,
|
|
nextInstallRecords,
|
|
nextConfig: strippedConfig,
|
|
...(params.writeOptions ? { writeOptions: params.writeOptions } : {}),
|
|
commit: params.commit,
|
|
});
|
|
return {
|
|
config: strippedConfig,
|
|
installRecords: nextInstallRecords,
|
|
movedInstallRecords: true,
|
|
};
|
|
}
|
|
|
|
export async function commitConfigWithPendingPluginInstalls(params: {
|
|
nextConfig: OpenClawConfig;
|
|
baseHash?: string;
|
|
writeOptions?: ConfigWriteOptions;
|
|
}): Promise<{
|
|
config: OpenClawConfig;
|
|
installRecords: Record<string, PluginInstallRecord>;
|
|
movedInstallRecords: boolean;
|
|
}> {
|
|
return await commitConfigWriteWithPendingPluginInstalls({
|
|
nextConfig: params.nextConfig,
|
|
...(params.writeOptions ? { writeOptions: params.writeOptions } : {}),
|
|
commit: async (nextConfig, writeOptions) => {
|
|
await replaceConfigFile({
|
|
nextConfig,
|
|
...(params.baseHash !== undefined ? { baseHash: params.baseHash } : {}),
|
|
...(writeOptions ? { writeOptions } : {}),
|
|
});
|
|
},
|
|
});
|
|
}
|