From 352ef94d40fb86f6e00119aa000b3fefbd7bd69c Mon Sep 17 00:00:00 2001 From: RileyJJY <0668000974@xydigit.com> Date: Thu, 16 Jul 2026 23:18:38 +0800 Subject: [PATCH] fix(config): cap missing-provider group-policy warning cache with shared dedupe helper (#108806) --- src/config/runtime-group-policy.test.ts | 39 ++++++++++++++++++++++++- src/config/runtime-group-policy.ts | 12 ++++++-- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/config/runtime-group-policy.test.ts b/src/config/runtime-group-policy.test.ts index 355414595256..0a8118577026 100644 --- a/src/config/runtime-group-policy.test.ts +++ b/src/config/runtime-group-policy.test.ts @@ -1,5 +1,5 @@ // Covers runtime group-policy resolution from config and context. -import { beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { GROUP_POLICY_BLOCKED_LABEL, resolveAllowlistProviderRuntimeGroupPolicy, @@ -63,4 +63,41 @@ describe("warnMissingProviderGroupPolicyFallbackOnce", () => { expect(lines[0]).toContain("channels.runtime-policy-test is missing"); expect(lines[0]).toContain("room messages blocked"); }); + + describe("warning dedupe cache bounds", () => { + let warnOnceFn: typeof warnMissingProviderGroupPolicyFallbackOnce; + + // Fresh module instance so the module-level cache starts empty for this block. + beforeEach(async () => { + vi.resetModules(); + const mod = await import("./runtime-group-policy.js"); + warnOnceFn = mod.warnMissingProviderGroupPolicyFallbackOnce; + }); + + it("refreshes recent keys and re-warns evicted keys once the cap overflows", () => { + const lines: string[] = []; + const warnForAccount = (accountId: string) => + warnOnceFn({ + providerMissingFallbackApplied: true, + providerKey: "runtime-policy-evict-test", + accountId, + log: (message) => lines.push(message), + }); + + for (let i = 0; i < 4096; i++) { + warnForAccount(`account-${i}`); + } + expect(lines).toHaveLength(4096); + + // Recent duplicate stays deduped and refreshes its recency. + expect(warnForAccount("account-0")).toBe(false); + expect(lines).toHaveLength(4096); + + // Overflow evicts the oldest untouched key (account-1), not the refreshed one. + expect(warnForAccount("account-overflow")).toBe(true); + expect(warnForAccount("account-0")).toBe(false); + expect(warnForAccount("account-1")).toBe(true); + expect(lines).toHaveLength(4098); + }); + }); }); diff --git a/src/config/runtime-group-policy.ts b/src/config/runtime-group-policy.ts index 1f06d4d28978..8ba90220f58f 100644 --- a/src/config/runtime-group-policy.ts +++ b/src/config/runtime-group-policy.ts @@ -1,5 +1,6 @@ // Resolves runtime group-policy settings for channels and sessions. import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; +import { createDedupeCache } from "../infra/dedupe.js"; import type { GroupPolicy } from "./types.base.js"; type RuntimeGroupPolicyResolution = { @@ -90,7 +91,13 @@ export function resolveAllowlistProviderRuntimeGroupPolicy( }); } -const warnedMissingProviderGroupPolicy = new Set(); +const MAX_WARNED_MISSING_PROVIDER_GROUP_POLICY_KEYS = 4096; +// Warn-once keys accumulate per provider/account for the process lifetime; +// bounding them means evicted keys can re-warn instead of growing without limit. +const warnedMissingProviderGroupPolicy = createDedupeCache({ + ttlMs: 0, + maxSize: MAX_WARNED_MISSING_PROVIDER_GROUP_POLICY_KEYS, +}); /** * Log the missing-provider fail-closed fallback once per provider/account. @@ -107,10 +114,9 @@ export function warnMissingProviderGroupPolicyFallbackOnce(params: { return false; } const key = `${params.providerKey}:${params.accountId ?? "*"}`; - if (warnedMissingProviderGroupPolicy.has(key)) { + if (warnedMissingProviderGroupPolicy.check(key)) { return false; } - warnedMissingProviderGroupPolicy.add(key); const blockedLabel = normalizeOptionalString(params.blockedLabel) || "group messages"; params.log( `${params.providerKey}: channels.${params.providerKey} is missing; defaulting groupPolicy to "allowlist" (${blockedLabel} blocked until explicitly configured).`,