refactor(config): track runtime config revisions

This commit is contained in:
Peter Steinberger
2026-04-27 12:51:49 +01:00
parent 047c03cc88
commit 94a9d3f0be
10 changed files with 264 additions and 114 deletions

View File

@@ -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";

View File

@@ -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.

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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();

View File

@@ -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)) {

View File

@@ -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,

View File

@@ -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",

View File

@@ -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;