fix: serialize config mutation writes

This commit is contained in:
Peter Steinberger
2026-05-13 06:45:35 +01:00
parent 488a3d8e52
commit 2e983e47df
8 changed files with 701 additions and 75 deletions

View File

@@ -1,5 +1,14 @@
import { isDeepStrictEqual } from "node:util";
import { replaceConfigFile } from "../config/config.js";
import {
replaceConfigFile,
resolveConfigWriteAfterWrite,
transformConfigFileWithRetry,
type ConfigMutationCommit,
type ConfigMutationResult,
type ConfigMutationContext,
type ConfigTransformResult,
type TransformConfigFileWithRetryParams,
} 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";
@@ -21,6 +30,19 @@ function mergeUnsetPaths(
type ConfigCommit = (config: OpenClawConfig, writeOptions?: ConfigWriteOptions) => Promise<void>;
const PLUGIN_SOURCE_CHANGED_RESTART_REASON = "plugin source changed";
function mergeAfterWrite(
writeOptions: ConfigWriteOptions | undefined,
afterWrite: ConfigWriteOptions["afterWrite"],
): ConfigWriteOptions | undefined {
if (afterWrite === undefined) {
return writeOptions;
}
return {
...writeOptions,
afterWrite,
};
}
async function commitPluginInstallRecordsWithWriter(params: {
previousInstallRecords?: Record<string, PluginInstallRecord>;
nextInstallRecords: Record<string, PluginInstallRecord>;
@@ -141,3 +163,53 @@ export async function commitConfigWithPendingPluginInstalls(params: {
},
});
}
export async function transformConfigWithPendingPluginInstalls<T = void>(
params: Omit<TransformConfigFileWithRetryParams<T>, "commit">,
): Promise<ConfigMutationResult<T>> {
const commit: ConfigMutationCommit = async ({ nextConfig, snapshot, baseHash, writeOptions }) => {
const requestedAfterWrite = params.afterWrite ?? params.writeOptions?.afterWrite;
const committed = await commitConfigWriteWithPendingPluginInstalls({
nextConfig,
...(writeOptions ? { writeOptions: mergeAfterWrite(writeOptions, params.afterWrite) } : {}),
commit: async (config, commitWriteOptions) => {
await replaceConfigFile({
nextConfig: config,
snapshot,
writeOptions: commitWriteOptions ?? {},
...(baseHash !== undefined ? { baseHash } : {}),
});
},
});
const afterWrite = resolveConfigWriteAfterWrite(
requestedAfterWrite ??
(committed.movedInstallRecords
? { mode: "restart", reason: PLUGIN_SOURCE_CHANGED_RESTART_REASON }
: undefined),
);
return {
config: committed.config,
afterWrite,
};
};
return await transformConfigFileWithRetry<T>({
...params,
commit,
});
}
export async function mutateConfigWithPendingPluginInstalls<T = void>(
params: Omit<TransformConfigFileWithRetryParams<T>, "commit" | "transform"> & {
mutate: (draft: OpenClawConfig, context: ConfigMutationContext) => Promise<T | void> | T | void;
},
): Promise<ConfigMutationResult<T>> {
return await transformConfigWithPendingPluginInstalls<T>({
...params,
transform: async (currentConfig, context): Promise<ConfigTransformResult<T>> => {
const draft = structuredClone(currentConfig) as OpenClawConfig;
const result = (await params.mutate(draft, context)) as T | undefined;
return { nextConfig: draft, result };
},
});
}