fix(config): cap legacy toolsBySender deprecation warning cache with shared dedupe helper (#101696)

* 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 <steipete@gmail.com>
This commit is contained in:
sunlit-deng
2026-07-10 13:43:17 +08:00
committed by GitHub
parent 2cd831a0e3
commit 8711f6108a
2 changed files with 47 additions and 4 deletions
+39 -1
View File
@@ -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);
});
});
});
+8 -3
View File
@@ -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<string>();
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.`,
{