fix(config): reuse in-memory gateway write reloads

This commit is contained in:
Peter Steinberger
2026-03-29 23:37:54 +01:00
parent 0e47ce58bc
commit a9984e2bf9
6 changed files with 176 additions and 6 deletions

View File

@@ -2,6 +2,7 @@ export {
clearConfigCache,
ConfigRuntimeRefreshError,
clearRuntimeConfigSnapshot,
registerConfigWriteListener,
createConfigIO,
getRuntimeConfigSnapshot,
getRuntimeConfigSourceSnapshot,
@@ -17,6 +18,7 @@ export {
setRuntimeConfigSnapshot,
writeConfigFile,
} from "./io.js";
export type { ConfigWriteNotification } from "./io.js";
export { migrateLegacyConfig } from "./legacy-migrate.js";
export * from "./paths.js";
export * from "./runtime-overrides.js";

View File

@@ -6,6 +6,7 @@ import {
getRuntimeConfigSourceSnapshot,
loadConfig,
projectConfigOntoRuntimeSourceSnapshot,
registerConfigWriteListener,
resetConfigRuntimeState,
setRuntimeConfigSnapshotRefreshHandler,
setRuntimeConfigSnapshot,
@@ -249,4 +250,35 @@ describe("runtime config snapshot writes", () => {
}
});
});
it("notifies in-process write listeners with the refreshed runtime snapshot", async () => {
await withTempHome("openclaw-config-runtime-write-listener-", async (home) => {
const configPath = path.join(home, ".openclaw", "openclaw.json");
await fs.mkdir(path.dirname(configPath), { recursive: true });
await fs.writeFile(configPath, `${JSON.stringify({ gateway: { port: 18789 } }, null, 2)}\n`);
const seen: Array<{ configPath: string; runtimeConfig: OpenClawConfig }> = [];
const unsubscribe = registerConfigWriteListener((event) => {
seen.push({
configPath: event.configPath,
runtimeConfig: event.runtimeConfig,
});
});
try {
expect(loadConfig().gateway?.port).toBe(18789);
await writeConfigFile({
...loadConfig(),
gateway: { port: 19003 },
});
expect(seen).toHaveLength(1);
expect(seen[0]?.configPath).toBe(configPath);
expect(seen[0]?.runtimeConfig.gateway?.port).toBe(19003);
} finally {
unsubscribe();
resetRuntimeConfigState();
}
});
});
});

View File

@@ -243,6 +243,12 @@ export type RuntimeConfigSnapshotRefreshHandler = {
clearOnRefreshFailure?: () => void;
};
export type ConfigWriteNotification = {
configPath: string;
sourceConfig: OpenClawConfig;
runtimeConfig: OpenClawConfig;
};
export class ConfigRuntimeRefreshError extends Error {
constructor(message: string, options?: { cause?: unknown }) {
super(message, options);
@@ -2076,11 +2082,31 @@ const AUTO_OWNER_DISPLAY_SECRET_PERSIST_WARNED = new Set<string>();
let runtimeConfigSnapshot: OpenClawConfig | null = null;
let runtimeConfigSourceSnapshot: OpenClawConfig | null = null;
let runtimeConfigSnapshotRefreshHandler: RuntimeConfigSnapshotRefreshHandler | null = null;
const configWriteListeners = new Set<(event: ConfigWriteNotification) => void>();
function notifyConfigWriteListeners(event: ConfigWriteNotification): void {
for (const listener of configWriteListeners) {
try {
listener(event);
} catch {
// Best-effort observer path only; successful writes must still complete.
}
}
}
export function clearConfigCache(): void {
// Compat shim: runtime snapshot is the only in-process cache now.
}
export function registerConfigWriteListener(
listener: (event: ConfigWriteNotification) => void,
): () => void {
configWriteListeners.add(listener);
return () => {
configWriteListeners.delete(listener);
};
}
export function setRuntimeConfigSnapshot(
config: OpenClawConfig,
sourceConfig?: OpenClawConfig,
@@ -2207,6 +2233,16 @@ export async function writeConfigFile(
envSnapshotForRestore: sameConfigPath ? options.envSnapshotForRestore : undefined,
unsetPaths: options.unsetPaths,
});
const notifyCommittedWrite = () => {
if (!runtimeConfigSnapshot) {
return;
}
notifyConfigWriteListeners({
configPath: io.configPath,
sourceConfig: nextCfg,
runtimeConfig: runtimeConfigSnapshot,
});
};
// Keep the last-known-good runtime snapshot active until the specialized refresh path
// succeeds, so concurrent readers do not observe unresolved SecretRefs mid-refresh.
const refreshHandler = runtimeConfigSnapshotRefreshHandler;
@@ -2214,6 +2250,7 @@ export async function writeConfigFile(
try {
const refreshed = await refreshHandler.refresh({ sourceConfig: nextCfg });
if (refreshed) {
notifyCommittedWrite();
return;
}
} catch (error) {
@@ -2234,12 +2271,15 @@ export async function writeConfigFile(
// subsequent writes still get secret-preservation merge-patch (hadBothSnapshots stays true).
const fresh = io.loadConfig();
setRuntimeConfigSnapshot(fresh, nextCfg);
notifyCommittedWrite();
return;
}
if (hadRuntimeSnapshot) {
const fresh = io.loadConfig();
setRuntimeConfigSnapshot(fresh);
notifyCommittedWrite();
return;
}
setRuntimeConfigSnapshot(io.loadConfig());
notifyCommittedWrite();
}