From 94a9d3f0bedb34067cabb08b407bb0a6aac8128b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 12:51:49 +0100 Subject: [PATCH] refactor(config): track runtime config revisions --- src/config/config.ts | 14 +- src/config/io.ts | 22 +- src/config/mutate.ts | 18 +- src/config/runtime-snapshot.test.ts | 26 +++ src/config/runtime-snapshot.ts | 75 +++++++ src/gateway/config-reload.test.ts | 9 + src/gateway/server-methods/config.ts | 188 +++++++++++------- .../server-methods/tools-effective.runtime.ts | 1 + .../server-methods/tools-effective.test.ts | 1 + src/gateway/server-methods/tools-effective.ts | 24 +-- 10 files changed, 264 insertions(+), 114 deletions(-) diff --git a/src/config/config.ts b/src/config/config.ts index 12962ab33ba..06aee542b22 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -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"; diff --git a/src/config/io.ts b/src/config/io.ts index 840ad337e0c..37937847c86 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -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. diff --git a/src/config/mutate.ts b/src/config/mutate.ts index dbb8edaad64..f81493a533d 100644 --- a/src/config/mutate.ts +++ b/src/config/mutate.ts @@ -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, diff --git a/src/config/runtime-snapshot.test.ts b/src/config/runtime-snapshot.test.ts index e82ff09ec21..02cb30f0723 100644 --- a/src/config/runtime-snapshot.test.ts +++ b/src/config/runtime-snapshot.test.ts @@ -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 { diff --git a/src/config/runtime-snapshot.ts b/src/config/runtime-snapshot.ts index 242d1aad539..1ca99f97bc6 100644 --- a/src/config/runtime-snapshot.ts +++ b/src/config/runtime-snapshot.ts @@ -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; diff --git a/src/gateway/config-reload.test.ts b/src/gateway/config-reload.test.ts index 1a26fa164f5..a1bec2ff86f 100644 --- a/src/gateway/config-reload.test.ts +++ b/src/gateway/config-reload.test.ts @@ -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(); diff --git a/src/gateway/server-methods/config.ts b/src/gateway/server-methods/config.ts index 4370a77e026..0336b1d0a63 100644 --- a/src/gateway/server-methods/config.ts +++ b/src/gateway/server-methods/config.ts @@ -63,6 +63,15 @@ type ConfigOpenCommand = { args: string[]; }; +type ConfigWriteSnapshot = Awaited>["snapshot"]; +type ConfigWriteOptions = Awaited< + ReturnType +>["writeOptions"]; + +function resolveGatewayConfigPath(snapshot?: Pick): string { + return snapshot?.path ?? createConfigIO().configPath; +} + function requireConfigBaseHash( params: unknown, snapshot: Awaited>, @@ -353,12 +362,12 @@ function resolveConfigRestartRequest(params: unknown): { function buildConfigRestartSentinelPayload(params: { kind: RestartSentinelPayload["kind"]; mode: string; + configPath: string; sessionKey: string | undefined; deliveryContext: ReturnType["deliveryContext"]; threadId: ReturnType["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; + context?: GatewayRequestContext; +}): Promise<{ + payload: RestartSentinelPayload; + sentinelPath: string | null; + restart: ReturnType | 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)) { diff --git a/src/gateway/server-methods/tools-effective.runtime.ts b/src/gateway/server-methods/tools-effective.runtime.ts index 6e56dbda4bf..6f5b3a54a5e 100644 --- a/src/gateway/server-methods/tools-effective.runtime.ts +++ b/src/gateway/server-methods/tools-effective.runtime.ts @@ -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, diff --git a/src/gateway/server-methods/tools-effective.test.ts b/src/gateway/server-methods/tools-effective.test.ts index 07be737f87f..86361f9319a 100644 --- a/src/gateway/server-methods/tools-effective.test.ts +++ b/src/gateway/server-methods/tools-effective.test.ts @@ -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", diff --git a/src/gateway/server-methods/tools-effective.ts b/src/gateway/server-methods/tools-effective.ts index 2bdf3e7b3ae..9fc50dd33c9 100644 --- a/src/gateway/server-methods/tools-effective.ts +++ b/src/gateway/server-methods/tools-effective.ts @@ -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(); 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(); }, setToolsEffectiveNowForTest(now: () => number) { nowForToolsEffectiveCache = now;