mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
refactor(config): track runtime config revisions
This commit is contained in:
@@ -5,6 +5,7 @@ export {
|
||||
registerConfigWriteListener,
|
||||
createConfigIO,
|
||||
getRuntimeConfig,
|
||||
getRuntimeConfigSnapshotMetadata,
|
||||
getRuntimeConfigSnapshot,
|
||||
getRuntimeConfigSourceSnapshot,
|
||||
projectConfigOntoRuntimeSourceSnapshot,
|
||||
@@ -21,13 +22,22 @@ export {
|
||||
recoverConfigFromJsonRootSuffix,
|
||||
resetConfigRuntimeState,
|
||||
resolveConfigSnapshotHash,
|
||||
resolveRuntimeConfigCacheKey,
|
||||
selectApplicableRuntimeConfig,
|
||||
setRuntimeConfigSnapshotRefreshHandler,
|
||||
setRuntimeConfigSnapshot,
|
||||
writeConfigFile,
|
||||
} from "./io.js";
|
||||
export { resolveConfigWriteAfterWrite, resolveConfigWriteFollowUp } from "./runtime-snapshot.js";
|
||||
export type { ConfigWriteAfterWrite, ConfigWriteFollowUp } from "./runtime-snapshot.js";
|
||||
export {
|
||||
hashRuntimeConfigValue,
|
||||
resolveConfigWriteAfterWrite,
|
||||
resolveConfigWriteFollowUp,
|
||||
} from "./runtime-snapshot.js";
|
||||
export type {
|
||||
ConfigWriteAfterWrite,
|
||||
ConfigWriteFollowUp,
|
||||
RuntimeConfigSnapshotMetadata,
|
||||
} from "./runtime-snapshot.js";
|
||||
export type { ConfigWriteNotification } from "./io.js";
|
||||
export { ConfigMutationConflictError, mutateConfigFile, replaceConfigFile } from "./mutate.js";
|
||||
export * from "./paths.js";
|
||||
|
||||
@@ -83,13 +83,16 @@ import {
|
||||
import { applyConfigOverrides } from "./runtime-overrides.js";
|
||||
import {
|
||||
clearRuntimeConfigSnapshot as clearRuntimeConfigSnapshotState,
|
||||
createRuntimeConfigWriteNotification,
|
||||
finalizeRuntimeSnapshotWrite,
|
||||
getRuntimeConfigSnapshotMetadata as getRuntimeConfigSnapshotMetadataState,
|
||||
getRuntimeConfigSnapshot as getRuntimeConfigSnapshotState,
|
||||
getRuntimeConfigSourceSnapshot as getRuntimeConfigSourceSnapshotState,
|
||||
loadPinnedRuntimeConfig,
|
||||
notifyRuntimeConfigWriteListeners,
|
||||
registerRuntimeConfigWriteListener,
|
||||
resetConfigRuntimeState as resetConfigRuntimeStateState,
|
||||
resolveRuntimeConfigCacheKey,
|
||||
selectApplicableRuntimeConfig,
|
||||
setRuntimeConfigSnapshot as setRuntimeConfigSnapshotState,
|
||||
getRuntimeConfigSnapshotRefreshHandler as getRuntimeConfigSnapshotRefreshHandlerState,
|
||||
@@ -107,9 +110,11 @@ import { shouldWarnOnTouchedVersion } from "./version.js";
|
||||
|
||||
export {
|
||||
clearRuntimeConfigSnapshotState as clearRuntimeConfigSnapshot,
|
||||
getRuntimeConfigSnapshotMetadataState as getRuntimeConfigSnapshotMetadata,
|
||||
getRuntimeConfigSnapshotState as getRuntimeConfigSnapshot,
|
||||
getRuntimeConfigSourceSnapshotState as getRuntimeConfigSourceSnapshot,
|
||||
resetConfigRuntimeStateState as resetConfigRuntimeState,
|
||||
resolveRuntimeConfigCacheKey,
|
||||
selectApplicableRuntimeConfig,
|
||||
setRuntimeConfigSnapshotState as setRuntimeConfigSnapshot,
|
||||
setRuntimeConfigSnapshotRefreshHandlerState as setRuntimeConfigSnapshotRefreshHandler,
|
||||
@@ -2371,14 +2376,15 @@ export async function writeConfigFile(
|
||||
if (!currentRuntimeConfig) {
|
||||
return;
|
||||
}
|
||||
notifyRuntimeConfigWriteListeners({
|
||||
configPath: io.configPath,
|
||||
sourceConfig: nextCfg,
|
||||
runtimeConfig: currentRuntimeConfig,
|
||||
persistedHash: writeResult.persistedHash,
|
||||
writtenAtMs: Date.now(),
|
||||
afterWrite: options.afterWrite,
|
||||
});
|
||||
notifyRuntimeConfigWriteListeners(
|
||||
createRuntimeConfigWriteNotification({
|
||||
configPath: io.configPath,
|
||||
sourceConfig: nextCfg,
|
||||
runtimeConfig: currentRuntimeConfig,
|
||||
persistedHash: writeResult.persistedHash,
|
||||
afterWrite: options.afterWrite,
|
||||
}),
|
||||
);
|
||||
};
|
||||
// Keep the last-known-good runtime snapshot active until the specialized refresh path
|
||||
// succeeds, so concurrent readers do not observe unresolved SecretRefs mid-refresh.
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "./io.js";
|
||||
import { applyUnsetPathsForWrite, resolveManagedUnsetPathsForWrite } from "./io.write-prepare.js";
|
||||
import {
|
||||
createRuntimeConfigWriteNotification,
|
||||
finalizeRuntimeSnapshotWrite,
|
||||
getRuntimeConfigSnapshot,
|
||||
getRuntimeConfigSnapshotRefreshHandler,
|
||||
@@ -194,14 +195,15 @@ async function tryWriteSingleTopLevelIncludeMutation(params: {
|
||||
if (!currentRuntimeConfig) {
|
||||
return;
|
||||
}
|
||||
notifyRuntimeConfigWriteListeners({
|
||||
configPath: params.snapshot.path,
|
||||
sourceConfig: refreshedSnapshot.sourceConfig,
|
||||
runtimeConfig: currentRuntimeConfig,
|
||||
persistedHash,
|
||||
writtenAtMs: Date.now(),
|
||||
afterWrite: params.afterWrite ?? params.writeOptions?.afterWrite,
|
||||
});
|
||||
notifyRuntimeConfigWriteListeners(
|
||||
createRuntimeConfigWriteNotification({
|
||||
configPath: params.snapshot.path,
|
||||
sourceConfig: refreshedSnapshot.sourceConfig,
|
||||
runtimeConfig: currentRuntimeConfig,
|
||||
persistedHash,
|
||||
afterWrite: params.afterWrite ?? params.writeOptions?.afterWrite,
|
||||
}),
|
||||
);
|
||||
};
|
||||
await finalizeRuntimeSnapshotWrite({
|
||||
nextSourceConfig: refreshedSnapshot.sourceConfig,
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
finalizeRuntimeSnapshotWrite,
|
||||
getRuntimeConfigSnapshotMetadata,
|
||||
getRuntimeConfigSourceSnapshot,
|
||||
getRuntimeConfigSnapshot,
|
||||
loadPinnedRuntimeConfig,
|
||||
notifyRuntimeConfigWriteListeners,
|
||||
registerRuntimeConfigWriteListener,
|
||||
resetConfigRuntimeState,
|
||||
resolveRuntimeConfigCacheKey,
|
||||
selectApplicableRuntimeConfig,
|
||||
setRuntimeConfigSnapshot,
|
||||
setRuntimeConfigSnapshotRefreshHandler,
|
||||
@@ -71,6 +73,26 @@ describe("runtime snapshot state", () => {
|
||||
expect(getRuntimeConfigSourceSnapshot()).toEqual(sourceConfig);
|
||||
});
|
||||
|
||||
it("tracks snapshot metadata and cache keys across runtime refreshes", () => {
|
||||
const firstConfig: OpenClawConfig = { gateway: { port: 18789 } };
|
||||
const secondConfig: OpenClawConfig = { gateway: { port: 19001 } };
|
||||
|
||||
setRuntimeConfigSnapshot(firstConfig);
|
||||
const firstMetadata = getRuntimeConfigSnapshotMetadata();
|
||||
expect(firstMetadata?.revision).toBe(1);
|
||||
expect(resolveRuntimeConfigCacheKey(firstConfig)).toBe(
|
||||
`runtime:${firstMetadata?.revision}:${firstMetadata?.fingerprint}`,
|
||||
);
|
||||
|
||||
setRuntimeConfigSnapshot(secondConfig);
|
||||
const secondMetadata = getRuntimeConfigSnapshotMetadata();
|
||||
expect(secondMetadata?.revision).toBe(2);
|
||||
expect(secondMetadata?.fingerprint).not.toBe(firstMetadata?.fingerprint);
|
||||
expect(resolveRuntimeConfigCacheKey(secondConfig)).toBe(
|
||||
`runtime:${secondMetadata?.revision}:${secondMetadata?.fingerprint}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("selects runtime config only when input still matches the runtime source", () => {
|
||||
const sourceConfig: OpenClawConfig = {
|
||||
models: {
|
||||
@@ -124,6 +146,7 @@ describe("runtime snapshot state", () => {
|
||||
resetRuntimeConfigState();
|
||||
expect(getRuntimeConfigSnapshot()).toBeNull();
|
||||
expect(getRuntimeConfigSourceSnapshot()).toBeNull();
|
||||
expect(getRuntimeConfigSnapshotMetadata()).toBeNull();
|
||||
});
|
||||
|
||||
it("refreshes both snapshots from disk after a write when source + runtime snapshots exist", async () => {
|
||||
@@ -285,6 +308,9 @@ describe("runtime snapshot state", () => {
|
||||
sourceConfig: { gateway: { port: 18789 } },
|
||||
runtimeConfig: { gateway: { port: 19003 } },
|
||||
persistedHash: "abc123",
|
||||
revision: 1,
|
||||
fingerprint: "runtime-fingerprint",
|
||||
sourceFingerprint: "source-fingerprint",
|
||||
writtenAtMs: 1,
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { OpenClawConfig } from "./types.js";
|
||||
|
||||
export type RuntimeConfigSnapshotRefreshParams = {
|
||||
@@ -65,12 +66,24 @@ export type RuntimeConfigWriteNotification = {
|
||||
sourceConfig: OpenClawConfig;
|
||||
runtimeConfig: OpenClawConfig;
|
||||
persistedHash: string;
|
||||
revision: number;
|
||||
fingerprint: string;
|
||||
sourceFingerprint: string | null;
|
||||
writtenAtMs: number;
|
||||
afterWrite?: ConfigWriteAfterWrite;
|
||||
};
|
||||
|
||||
export type RuntimeConfigSnapshotMetadata = {
|
||||
revision: number;
|
||||
fingerprint: string;
|
||||
sourceFingerprint: string | null;
|
||||
updatedAtMs: number;
|
||||
};
|
||||
|
||||
let runtimeConfigSnapshot: OpenClawConfig | null = null;
|
||||
let runtimeConfigSourceSnapshot: OpenClawConfig | null = null;
|
||||
let runtimeConfigSnapshotMetadata: RuntimeConfigSnapshotMetadata | null = null;
|
||||
let runtimeConfigSnapshotRevision = 0;
|
||||
let runtimeConfigSnapshotRefreshHandler: RuntimeConfigSnapshotRefreshHandler | null = null;
|
||||
const runtimeConfigWriteListeners = new Set<(event: RuntimeConfigWriteNotification) => void>();
|
||||
|
||||
@@ -99,17 +112,37 @@ function configSnapshotsMatch(left: OpenClawConfig, right: OpenClawConfig): bool
|
||||
}
|
||||
}
|
||||
|
||||
export function hashRuntimeConfigValue(value: OpenClawConfig): string {
|
||||
return createHash("sha256").update(stableConfigStringify(value)).digest("base64url");
|
||||
}
|
||||
|
||||
function createRuntimeConfigSnapshotMetadata(
|
||||
config: OpenClawConfig,
|
||||
sourceConfig?: OpenClawConfig,
|
||||
): RuntimeConfigSnapshotMetadata {
|
||||
runtimeConfigSnapshotRevision += 1;
|
||||
return {
|
||||
revision: runtimeConfigSnapshotRevision,
|
||||
fingerprint: hashRuntimeConfigValue(config),
|
||||
sourceFingerprint: sourceConfig ? hashRuntimeConfigValue(sourceConfig) : null,
|
||||
updatedAtMs: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
export function setRuntimeConfigSnapshot(
|
||||
config: OpenClawConfig,
|
||||
sourceConfig?: OpenClawConfig,
|
||||
): void {
|
||||
runtimeConfigSnapshot = config;
|
||||
runtimeConfigSourceSnapshot = sourceConfig ?? null;
|
||||
runtimeConfigSnapshotMetadata = createRuntimeConfigSnapshotMetadata(config, sourceConfig);
|
||||
}
|
||||
|
||||
export function resetConfigRuntimeState(): void {
|
||||
runtimeConfigSnapshot = null;
|
||||
runtimeConfigSourceSnapshot = null;
|
||||
runtimeConfigSnapshotMetadata = null;
|
||||
runtimeConfigSnapshotRevision = 0;
|
||||
}
|
||||
|
||||
export function clearRuntimeConfigSnapshot(): void {
|
||||
@@ -124,6 +157,48 @@ export function getRuntimeConfigSourceSnapshot(): OpenClawConfig | null {
|
||||
return runtimeConfigSourceSnapshot;
|
||||
}
|
||||
|
||||
export function getRuntimeConfigSnapshotMetadata(): RuntimeConfigSnapshotMetadata | null {
|
||||
return runtimeConfigSnapshotMetadata;
|
||||
}
|
||||
|
||||
export function resolveRuntimeConfigCacheKey(config: OpenClawConfig): string {
|
||||
const metadata = runtimeConfigSnapshotMetadata;
|
||||
if (metadata && config === runtimeConfigSnapshot) {
|
||||
return `runtime:${metadata.revision}:${metadata.fingerprint}`;
|
||||
}
|
||||
return `config:${hashRuntimeConfigValue(config)}`;
|
||||
}
|
||||
|
||||
export function createRuntimeConfigWriteNotification(params: {
|
||||
configPath: string;
|
||||
sourceConfig: OpenClawConfig;
|
||||
runtimeConfig: OpenClawConfig;
|
||||
persistedHash: string;
|
||||
writtenAtMs?: number;
|
||||
afterWrite?: ConfigWriteAfterWrite;
|
||||
}): RuntimeConfigWriteNotification {
|
||||
const metadata =
|
||||
params.runtimeConfig === runtimeConfigSnapshot && runtimeConfigSnapshotMetadata
|
||||
? runtimeConfigSnapshotMetadata
|
||||
: {
|
||||
revision: runtimeConfigSnapshotRevision,
|
||||
fingerprint: hashRuntimeConfigValue(params.runtimeConfig),
|
||||
sourceFingerprint: hashRuntimeConfigValue(params.sourceConfig),
|
||||
updatedAtMs: Date.now(),
|
||||
};
|
||||
return {
|
||||
configPath: params.configPath,
|
||||
sourceConfig: params.sourceConfig,
|
||||
runtimeConfig: params.runtimeConfig,
|
||||
persistedHash: params.persistedHash,
|
||||
revision: metadata.revision,
|
||||
fingerprint: metadata.fingerprint,
|
||||
sourceFingerprint: metadata.sourceFingerprint,
|
||||
writtenAtMs: params.writtenAtMs ?? Date.now(),
|
||||
afterWrite: params.afterWrite,
|
||||
};
|
||||
}
|
||||
|
||||
export function selectApplicableRuntimeConfig(params: {
|
||||
inputConfig?: OpenClawConfig;
|
||||
runtimeConfig?: OpenClawConfig | null;
|
||||
|
||||
@@ -519,6 +519,9 @@ function makeZeroDebounceHookWrite(persistedHash: string): ConfigWriteNotificati
|
||||
hooks: { enabled: true },
|
||||
},
|
||||
persistedHash,
|
||||
revision: 1,
|
||||
fingerprint: `runtime-${persistedHash}`,
|
||||
sourceFingerprint: `source-${persistedHash}`,
|
||||
writtenAtMs: Date.now(),
|
||||
};
|
||||
}
|
||||
@@ -1052,6 +1055,9 @@ describe("startGatewayConfigReloader", () => {
|
||||
},
|
||||
},
|
||||
persistedHash: "plugin-timestamps-1",
|
||||
revision: 1,
|
||||
fingerprint: "runtime-plugin-timestamps-1",
|
||||
sourceFingerprint: "source-plugin-timestamps-1",
|
||||
writtenAtMs: Date.now(),
|
||||
});
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
@@ -1106,6 +1112,9 @@ describe("startGatewayConfigReloader", () => {
|
||||
sourceConfig: nextSourceConfig,
|
||||
runtimeConfig: nextSourceConfig,
|
||||
persistedHash: "plugin-collision-1",
|
||||
revision: 1,
|
||||
fingerprint: "runtime-plugin-collision-1",
|
||||
sourceFingerprint: "source-plugin-collision-1",
|
||||
writtenAtMs: Date.now(),
|
||||
});
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
|
||||
@@ -63,6 +63,15 @@ type ConfigOpenCommand = {
|
||||
args: string[];
|
||||
};
|
||||
|
||||
type ConfigWriteSnapshot = Awaited<ReturnType<typeof readConfigFileSnapshotForWrite>>["snapshot"];
|
||||
type ConfigWriteOptions = Awaited<
|
||||
ReturnType<typeof readConfigFileSnapshotForWrite>
|
||||
>["writeOptions"];
|
||||
|
||||
function resolveGatewayConfigPath(snapshot?: Pick<ConfigWriteSnapshot, "path">): string {
|
||||
return snapshot?.path ?? createConfigIO().configPath;
|
||||
}
|
||||
|
||||
function requireConfigBaseHash(
|
||||
params: unknown,
|
||||
snapshot: Awaited<ReturnType<typeof readConfigFileSnapshot>>,
|
||||
@@ -353,12 +362,12 @@ function resolveConfigRestartRequest(params: unknown): {
|
||||
function buildConfigRestartSentinelPayload(params: {
|
||||
kind: RestartSentinelPayload["kind"];
|
||||
mode: string;
|
||||
configPath: string;
|
||||
sessionKey: string | undefined;
|
||||
deliveryContext: ReturnType<typeof extractDeliveryInfo>["deliveryContext"];
|
||||
threadId: ReturnType<typeof extractDeliveryInfo>["threadId"];
|
||||
note: string | undefined;
|
||||
}): RestartSentinelPayload {
|
||||
const configPath = createConfigIO().configPath;
|
||||
return {
|
||||
kind: params.kind,
|
||||
status: "ok",
|
||||
@@ -370,7 +379,7 @@ function buildConfigRestartSentinelPayload(params: {
|
||||
doctorHint: formatDoctorNonInteractiveHint(),
|
||||
stats: {
|
||||
mode: params.mode,
|
||||
root: configPath,
|
||||
root: params.configPath,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -393,6 +402,76 @@ function loadSchemaWithPlugins(): ConfigSchemaResponse {
|
||||
return loadGatewayRuntimeConfigSchema();
|
||||
}
|
||||
|
||||
async function commitGatewayConfigWrite(params: {
|
||||
snapshot: ConfigWriteSnapshot;
|
||||
writeOptions: ConfigWriteOptions;
|
||||
nextConfig: OpenClawConfig;
|
||||
context?: GatewayRequestContext;
|
||||
disconnectSharedAuthClients?: boolean;
|
||||
}): Promise<{ path: string; queueFollowUp: () => void }> {
|
||||
await replaceConfigFile({
|
||||
nextConfig: params.nextConfig,
|
||||
writeOptions: params.writeOptions,
|
||||
afterWrite: { mode: "auto" },
|
||||
});
|
||||
return {
|
||||
path: resolveGatewayConfigPath(params.snapshot),
|
||||
queueFollowUp: () => {
|
||||
queueSharedGatewayAuthGenerationRefresh(true, params.nextConfig, params.context);
|
||||
queueSharedGatewayAuthDisconnect(Boolean(params.disconnectSharedAuthClients), params.context);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveGatewayConfigRestartWriteResult(params: {
|
||||
requestParams: unknown;
|
||||
kind: RestartSentinelPayload["kind"];
|
||||
mode: "config.patch" | "config.apply";
|
||||
configPath: string;
|
||||
changedPaths: string[];
|
||||
nextConfig: OpenClawConfig;
|
||||
actor: ReturnType<typeof resolveControlPlaneActor>;
|
||||
context?: GatewayRequestContext;
|
||||
}): Promise<{
|
||||
payload: RestartSentinelPayload;
|
||||
sentinelPath: string | null;
|
||||
restart: ReturnType<typeof scheduleGatewaySigusr1Restart> | undefined;
|
||||
}> {
|
||||
const { sessionKey, note, restartDelayMs, deliveryContext, threadId } =
|
||||
resolveConfigRestartRequest(params.requestParams);
|
||||
const payload = buildConfigRestartSentinelPayload({
|
||||
kind: params.kind,
|
||||
mode: params.mode,
|
||||
configPath: params.configPath,
|
||||
sessionKey,
|
||||
deliveryContext,
|
||||
threadId,
|
||||
note,
|
||||
});
|
||||
const sentinelPath = await tryWriteRestartSentinelPayload(payload);
|
||||
const restart = shouldScheduleDirectConfigRestart({
|
||||
changedPaths: params.changedPaths,
|
||||
nextConfig: params.nextConfig,
|
||||
})
|
||||
? scheduleGatewaySigusr1Restart({
|
||||
delayMs: restartDelayMs,
|
||||
reason: params.mode,
|
||||
audit: {
|
||||
actor: params.actor.actor,
|
||||
deviceId: params.actor.deviceId,
|
||||
clientIp: params.actor.clientIp,
|
||||
changedPaths: params.changedPaths,
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
if (restart?.coalesced) {
|
||||
params.context?.logGateway?.warn(
|
||||
`${params.mode} restart coalesced ${formatControlPlaneActor(params.actor)} delayMs=${restart.delayMs}`,
|
||||
);
|
||||
}
|
||||
return { payload, sentinelPath, restart };
|
||||
}
|
||||
|
||||
export const configHandlers: GatewayRequestHandlers = {
|
||||
"config.get": async ({ params, respond }) => {
|
||||
if (!assertValidParams(params, validateConfigGetParams, "config.get", respond)) {
|
||||
@@ -456,21 +535,22 @@ export const configHandlers: GatewayRequestHandlers = {
|
||||
if (!(await ensureResolvableSecretRefsOrRespond({ config: parsed.config, respond }))) {
|
||||
return;
|
||||
}
|
||||
await replaceConfigFile({
|
||||
nextConfig: parsed.config,
|
||||
const writeResult = await commitGatewayConfigWrite({
|
||||
snapshot,
|
||||
writeOptions,
|
||||
afterWrite: { mode: "auto" },
|
||||
nextConfig: parsed.config,
|
||||
context,
|
||||
});
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
ok: true,
|
||||
path: createConfigIO().configPath,
|
||||
path: writeResult.path,
|
||||
config: redactConfigObject(parsed.config, parsed.schema.uiHints),
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
queueSharedGatewayAuthGenerationRefresh(true, parsed.config, context);
|
||||
writeResult.queueFollowUp();
|
||||
},
|
||||
"config.patch": async ({ params, respond, client, context }) => {
|
||||
if (!assertValidParams(params, validateConfigPatchParams, "config.patch", respond)) {
|
||||
@@ -563,7 +643,7 @@ export const configHandlers: GatewayRequestHandlers = {
|
||||
{
|
||||
ok: true,
|
||||
noop: true,
|
||||
path: createConfigIO().configPath,
|
||||
path: resolveGatewayConfigPath(snapshot),
|
||||
config: redactConfigObject(validated.config, schemaPatch.uiHints),
|
||||
},
|
||||
undefined,
|
||||
@@ -580,48 +660,29 @@ export const configHandlers: GatewayRequestHandlers = {
|
||||
snapshot.config,
|
||||
validated.config,
|
||||
);
|
||||
await replaceConfigFile({
|
||||
nextConfig: validated.config,
|
||||
const writeResult = await commitGatewayConfigWrite({
|
||||
snapshot,
|
||||
writeOptions,
|
||||
afterWrite: { mode: "auto" },
|
||||
nextConfig: validated.config,
|
||||
context,
|
||||
disconnectSharedAuthClients,
|
||||
});
|
||||
|
||||
const { sessionKey, note, restartDelayMs, deliveryContext, threadId } =
|
||||
resolveConfigRestartRequest(params);
|
||||
const payload = buildConfigRestartSentinelPayload({
|
||||
const { payload, sentinelPath, restart } = await resolveGatewayConfigRestartWriteResult({
|
||||
requestParams: params,
|
||||
kind: "config-patch",
|
||||
mode: "config.patch",
|
||||
sessionKey,
|
||||
deliveryContext,
|
||||
threadId,
|
||||
note,
|
||||
});
|
||||
const sentinelPath = await tryWriteRestartSentinelPayload(payload);
|
||||
const restart = shouldScheduleDirectConfigRestart({
|
||||
configPath: writeResult.path,
|
||||
changedPaths,
|
||||
nextConfig: validated.config,
|
||||
})
|
||||
? scheduleGatewaySigusr1Restart({
|
||||
delayMs: restartDelayMs,
|
||||
reason: "config.patch",
|
||||
audit: {
|
||||
actor: actor.actor,
|
||||
deviceId: actor.deviceId,
|
||||
clientIp: actor.clientIp,
|
||||
changedPaths,
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
if (restart?.coalesced) {
|
||||
context?.logGateway?.warn(
|
||||
`config.patch restart coalesced ${formatControlPlaneActor(actor)} delayMs=${restart.delayMs}`,
|
||||
);
|
||||
}
|
||||
actor,
|
||||
context,
|
||||
});
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
ok: true,
|
||||
path: createConfigIO().configPath,
|
||||
path: writeResult.path,
|
||||
config: redactConfigObject(validated.config, schemaPatch.uiHints),
|
||||
restart,
|
||||
sentinel: {
|
||||
@@ -631,8 +692,7 @@ export const configHandlers: GatewayRequestHandlers = {
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
queueSharedGatewayAuthGenerationRefresh(true, validated.config, context);
|
||||
queueSharedGatewayAuthDisconnect(disconnectSharedAuthClients, context);
|
||||
writeResult.queueFollowUp();
|
||||
},
|
||||
"config.apply": async ({ params, respond, client, context }) => {
|
||||
if (!assertValidParams(params, validateConfigApplyParams, "config.apply", respond)) {
|
||||
@@ -657,48 +717,29 @@ export const configHandlers: GatewayRequestHandlers = {
|
||||
// Compare before the write so we invalidate clients authenticated against the
|
||||
// previous shared secret immediately after the config update succeeds.
|
||||
const disconnectSharedAuthClients = didSharedGatewayAuthChange(snapshot.config, parsed.config);
|
||||
await replaceConfigFile({
|
||||
nextConfig: parsed.config,
|
||||
const writeResult = await commitGatewayConfigWrite({
|
||||
snapshot,
|
||||
writeOptions,
|
||||
afterWrite: { mode: "auto" },
|
||||
nextConfig: parsed.config,
|
||||
context,
|
||||
disconnectSharedAuthClients,
|
||||
});
|
||||
|
||||
const { sessionKey, note, restartDelayMs, deliveryContext, threadId } =
|
||||
resolveConfigRestartRequest(params);
|
||||
const payload = buildConfigRestartSentinelPayload({
|
||||
const { payload, sentinelPath, restart } = await resolveGatewayConfigRestartWriteResult({
|
||||
requestParams: params,
|
||||
kind: "config-apply",
|
||||
mode: "config.apply",
|
||||
sessionKey,
|
||||
deliveryContext,
|
||||
threadId,
|
||||
note,
|
||||
});
|
||||
const sentinelPath = await tryWriteRestartSentinelPayload(payload);
|
||||
const restart = shouldScheduleDirectConfigRestart({
|
||||
configPath: writeResult.path,
|
||||
changedPaths,
|
||||
nextConfig: parsed.config,
|
||||
})
|
||||
? scheduleGatewaySigusr1Restart({
|
||||
delayMs: restartDelayMs,
|
||||
reason: "config.apply",
|
||||
audit: {
|
||||
actor: actor.actor,
|
||||
deviceId: actor.deviceId,
|
||||
clientIp: actor.clientIp,
|
||||
changedPaths,
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
if (restart?.coalesced) {
|
||||
context?.logGateway?.warn(
|
||||
`config.apply restart coalesced ${formatControlPlaneActor(actor)} delayMs=${restart.delayMs}`,
|
||||
);
|
||||
}
|
||||
actor,
|
||||
context,
|
||||
});
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
ok: true,
|
||||
path: createConfigIO().configPath,
|
||||
path: writeResult.path,
|
||||
config: redactConfigObject(parsed.config, parsed.schema.uiHints),
|
||||
restart,
|
||||
sentinel: {
|
||||
@@ -708,8 +749,7 @@ export const configHandlers: GatewayRequestHandlers = {
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
queueSharedGatewayAuthGenerationRefresh(true, parsed.config, context);
|
||||
queueSharedGatewayAuthDisconnect(disconnectSharedAuthClients, context);
|
||||
writeResult.queueFollowUp();
|
||||
},
|
||||
"config.openFile": async ({ params, respond, context }) => {
|
||||
if (!assertValidParams(params, validateConfigGetParams, "config.openFile", respond)) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export { listAgentIds, resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||
export { resolveEffectiveToolInventory } from "../../agents/tools-effective-inventory.js";
|
||||
export { resolveReplyToMode } from "../../auto-reply/reply/reply-threading.js";
|
||||
export { resolveRuntimeConfigCacheKey } from "../../config/config.js";
|
||||
export {
|
||||
getActivePluginChannelRegistryVersion,
|
||||
getActivePluginRegistryVersion,
|
||||
|
||||
@@ -31,6 +31,7 @@ const runtimeMocks = vi.hoisted(() => ({
|
||||
})),
|
||||
getActivePluginChannelRegistryVersion: vi.fn(() => 1),
|
||||
getActivePluginRegistryVersion: vi.fn(() => 1),
|
||||
resolveRuntimeConfigCacheKey: vi.fn(() => "runtime:1:test"),
|
||||
resolveEffectiveToolInventory: vi.fn(() => ({
|
||||
agentId: "main",
|
||||
profile: "coding",
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
loadSessionEntry,
|
||||
resolveEffectiveToolInventory,
|
||||
resolveReplyToMode,
|
||||
resolveRuntimeConfigCacheKey,
|
||||
resolveSessionAgentId,
|
||||
resolveSessionModelRef,
|
||||
} from "./tools-effective.runtime.js";
|
||||
@@ -28,7 +29,6 @@ const TOOLS_EFFECTIVE_SLOW_LOG_MS = 250;
|
||||
const TOOLS_EFFECTIVE_CACHE_LIMIT = 128;
|
||||
|
||||
let nowForToolsEffectiveCache = () => Date.now();
|
||||
let configFingerprintCache = new WeakMap<OpenClawConfig, string>();
|
||||
|
||||
type TrustedToolsEffectiveContext = {
|
||||
cfg: OpenClawConfig;
|
||||
@@ -76,25 +76,6 @@ function resolveRequestedAgentIdOrRespondError(params: {
|
||||
return requestedAgentId;
|
||||
}
|
||||
|
||||
function hashCacheString(value: string): string {
|
||||
let hash = 5381;
|
||||
for (let i = 0; i < value.length; i += 1) {
|
||||
hash = (hash * 33) ^ value.charCodeAt(i);
|
||||
}
|
||||
return `${value.length}:${(hash >>> 0).toString(36)}`;
|
||||
}
|
||||
|
||||
function configFingerprint(cfg: OpenClawConfig): string {
|
||||
const existing = configFingerprintCache.get(cfg);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const serialized = JSON.stringify(cfg);
|
||||
const fingerprint = hashCacheString(serialized);
|
||||
configFingerprintCache.set(cfg, fingerprint);
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
function optionalCacheString(value: string | undefined | null): string {
|
||||
return value?.trim() ?? "";
|
||||
}
|
||||
@@ -106,7 +87,7 @@ function buildToolsEffectiveCacheKey(params: {
|
||||
const context = params.context;
|
||||
return JSON.stringify({
|
||||
v: 1,
|
||||
config: configFingerprint(context.cfg),
|
||||
config: resolveRuntimeConfigCacheKey(context.cfg),
|
||||
pluginRegistry: getActivePluginRegistryVersion(),
|
||||
channelRegistry: getActivePluginChannelRegistryVersion(),
|
||||
sessionKey: params.sessionKey,
|
||||
@@ -344,7 +325,6 @@ export const __testing = {
|
||||
resetToolsEffectiveCacheForTest() {
|
||||
toolsEffectiveCache.clear();
|
||||
toolsEffectiveInflight.clear();
|
||||
configFingerprintCache = new WeakMap<OpenClawConfig, string>();
|
||||
},
|
||||
setToolsEffectiveNowForTest(now: () => number) {
|
||||
nowForToolsEffectiveCache = now;
|
||||
|
||||
Reference in New Issue
Block a user