From 8711f6108a218cc12c5964be10efec034dbe1ebd Mon Sep 17 00:00:00 2001 From: sunlit-deng Date: Fri, 10 Jul 2026 13:43:17 +0800 Subject: [PATCH] fix(config): cap legacy toolsBySender deprecation warning cache with shared dedupe helper (#101696) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(config): use shared dedupe cache for legacy toolsBySender warnings Replace unbounded Set with createDedupeCache({ ttlMs: 0, maxSize: 4096 }) from src/infra/dedupe.ts. The shared helper provides check() with touch-on-read and max-size pruning — no custom LRU stack needed. Tests verify eviction and hot-key retention through the public resolveToolsBySender API without a testing-only cache export. * test(config): tighten legacy warning cache proof --------- Co-authored-by: Peter Steinberger --- src/config/group-policy.test.ts | 40 ++++++++++++++++++++++++++++++++- src/config/group-policy.ts | 11 ++++++--- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/config/group-policy.test.ts b/src/config/group-policy.test.ts index 18f9465c3320..2f8531c65e41 100644 --- a/src/config/group-policy.test.ts +++ b/src/config/group-policy.test.ts @@ -1,5 +1,5 @@ // Verifies group-policy normalization and runtime resolution. -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "./config.js"; import { resolveChannelGroupPolicy, @@ -412,4 +412,42 @@ describe("resolveToolsBySender", () => { expect(String(warningMessage)).toContain(`toolsBySender key "${legacyKey}"`); expect(warningMeta?.code).toBe("OPENCLAW_TOOLS_BY_SENDER_UNTYPED_KEY"); }); + + describe("legacy key warning dedupe cache", () => { + let resolveToolsBySenderFn: typeof resolveToolsBySender; + + const resolveFreshConfig = (legacyKey: string) => { + resolveToolsBySenderFn({ + toolsBySender: { [legacyKey]: { allow: ["read"] }, "*": { deny: ["exec"] } }, + senderId: "some-id", + }); + }; + + beforeEach(async () => { + vi.resetModules(); + const mod = await import("./group-policy.js"); + resolveToolsBySenderFn = mod.resolveToolsBySender; + }); + + it("refreshes recent keys across config snapshots and re-warns evicted keys", () => { + const warningSpy = vi.spyOn(process, "emitWarning").mockImplementation(() => undefined); + + for (let i = 0; i < 4096; i++) { + resolveFreshConfig(`legacy-key-${i}`); + } + expect(warningSpy).toHaveBeenCalledTimes(4096); + + resolveFreshConfig("legacy-key-0"); + expect(warningSpy).toHaveBeenCalledTimes(4096); + + resolveFreshConfig("overflow-key"); + expect(warningSpy).toHaveBeenCalledTimes(4097); + + resolveFreshConfig("legacy-key-0"); + expect(warningSpy).toHaveBeenCalledTimes(4097); + + resolveFreshConfig("legacy-key-1"); + expect(warningSpy).toHaveBeenCalledTimes(4098); + }); + }); }); diff --git a/src/config/group-policy.ts b/src/config/group-policy.ts index a776322be163..1a6f5b017bd2 100644 --- a/src/config/group-policy.ts +++ b/src/config/group-policy.ts @@ -4,6 +4,7 @@ import { normalizeOptionalString, } from "@openclaw/normalization-core/string-coerce"; import type { ChannelId } from "../channels/plugins/channel-id.types.js"; +import { createDedupeCache } from "../infra/dedupe.js"; import { resolveAccountEntry } from "../routing/account-lookup.js"; import { normalizeAccountId } from "../routing/session-key.js"; import { normalizeMessageChannel } from "../utils/message-channel-core.js"; @@ -72,7 +73,12 @@ type CompiledSenderPolicy = { wildcard?: GroupToolPolicyConfig; }; -const warnedLegacyToolsBySenderKeys = new Set(); +const MAX_WARNED_LEGACY_TOOLS_BY_SENDER_KEYS = 4096; +// Warning state spans fresh config snapshots; bounding it means evicted legacy keys can re-warn. +const warnedLegacyToolsBySenderKeys = createDedupeCache({ + ttlMs: 0, + maxSize: MAX_WARNED_LEGACY_TOOLS_BY_SENDER_KEYS, +}); const compiledToolsBySenderCache = new WeakMap< GroupToolPolicyBySenderConfig, CompiledSenderPolicy @@ -137,10 +143,9 @@ function normalizeLegacySenderKey(value: string): string { function warnLegacyToolsBySenderKey(rawKey: string) { const trimmed = rawKey.trim(); - if (!trimmed || warnedLegacyToolsBySenderKeys.has(trimmed)) { + if (!trimmed || warnedLegacyToolsBySenderKeys.check(trimmed)) { return; } - warnedLegacyToolsBySenderKeys.add(trimmed); process.emitWarning( `toolsBySender key "${trimmed}" is deprecated. Use explicit prefixes (channel:, id:, e164:, username:, name:). Legacy unprefixed keys are matched as id only.`, {