mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-03 18:20:25 +00:00
fix(config): reuse in-memory gateway write reloads
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user