refactor: canonicalize cache mechanics (#118262)

* refactor: adopt shared map pruning

* refactor: share config-scoped ttl cache

* refactor: share backup link cache

* refactor: centralize native relay retention cap

* chore: remove obsolete bundled channel lint suppression

* chore: shrink max-lines baseline

* refactor: remove cache pruning wrappers
This commit is contained in:
Peter Steinberger
2026-08-02 19:33:13 -07:00
committed by GitHub
parent 4f9af40078
commit d347cd1097
87 changed files with 306 additions and 724 deletions
-1
View File
@@ -532,7 +532,6 @@ src/auto-reply/reply/session.ts
src/auto-reply/status.test.ts
src/channels/message/ingress-queue.ts
src/channels/plugins/bundled.shape-guard.test.ts
src/channels/plugins/bundled.ts
src/channels/plugins/message-actions.security.test.ts
src/channels/plugins/outbound/interactive.test.ts
src/channels/plugins/read-only.test.ts
@@ -25,6 +25,7 @@ import {
freezeDiagnosticTraceContext,
type DiagnosticTraceContext,
} from "../infra/diagnostic-trace-context.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import type { SessionState } from "../logging/diagnostic-session-state.js";
import { redactToolDetail } from "../logging/redact.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
@@ -118,13 +119,7 @@ export function rememberPendingTerminalPresentation(params: {
toolParams: structuredClone(params.toolParams),
toolCallOrdinal: params.toolCallOrdinal,
});
while (pendingTerminalPresentationByToolCall.size > MAX_PENDING_TERMINAL_PRESENTATIONS) {
const oldestKey = pendingTerminalPresentationByToolCall.keys().next().value;
if (!oldestKey) {
break;
}
pendingTerminalPresentationByToolCall.delete(oldestKey);
}
pruneMapToMaxSize(pendingTerminalPresentationByToolCall, MAX_PENDING_TERMINAL_PRESENTATIONS);
}
/** Finalizes a trusted terminal summary after harness result middleware. */
@@ -581,12 +576,7 @@ export function shouldEmitLoopWarning(
return false;
}
state.toolLoopWarningBuckets.set(warningKey, bucket);
if (state.toolLoopWarningBuckets.size > MAX_LOOP_WARNING_KEYS) {
const oldest = state.toolLoopWarningBuckets.keys().next().value;
if (oldest) {
state.toolLoopWarningBuckets.delete(oldest);
}
}
pruneMapToMaxSize(state.toolLoopWarningBuckets, MAX_LOOP_WARNING_KEYS);
return true;
}
@@ -12,6 +12,7 @@ import {
createChildDiagnosticTraceContext,
freezeDiagnosticTraceContext,
} from "../infra/diagnostic-trace-context.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { copyPluginToolMeta, getPluginToolMeta } from "../plugins/tools.js";
import {
buildToolContentPrivateData,
@@ -188,12 +189,7 @@ export function recordAdjustedParamsForToolCall(
}
const adjustedParamsKey = buildAdjustedParamsKey({ runId, toolCallId });
adjustedParamsByToolCallId.set(adjustedParamsKey, cloneResult.value);
if (adjustedParamsByToolCallId.size > MAX_TRACKED_ADJUSTED_PARAMS) {
const oldest = adjustedParamsByToolCallId.keys().next().value;
if (oldest) {
adjustedParamsByToolCallId.delete(oldest);
}
}
pruneMapToMaxSize(adjustedParamsByToolCallId, MAX_TRACKED_ADJUSTED_PARAMS);
}
function cloneParamsForAdjustedReplay(
+3 -12
View File
@@ -3,6 +3,7 @@
* Reuses unchanged bootstrap file arrays while refreshing each turn so edits
* become visible to long-lived agent sessions.
*/
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { loadWorkspaceBootstrapFiles, type WorkspaceBootstrapFile } from "./workspace.js";
type BootstrapSnapshot = {
@@ -33,22 +34,12 @@ function bootstrapFilesEqual(
});
}
function pruneOldestBootstrapSnapshots(): void {
while (cache.size > MAX_BOOTSTRAP_SNAPSHOTS) {
const oldestKey = cache.keys().next().value;
if (typeof oldestKey !== "string") {
return;
}
cache.delete(oldestKey);
}
}
/** Load bootstrap files for a session, reusing the prior snapshot when content is unchanged. */
export async function getOrLoadBootstrapFiles(params: {
workspaceDir: string;
sessionKey: string;
}): Promise<WorkspaceBootstrapFile[]> {
pruneOldestBootstrapSnapshots();
pruneMapToMaxSize(cache, MAX_BOOTSTRAP_SNAPSHOTS);
const existing = cache.get(params.sessionKey);
// Refresh per turn so long-lived sessions pick up edits; loadWorkspaceBootstrapFiles
// handles unchanged file content through its guarded inode/mtime cache.
@@ -64,7 +55,7 @@ export async function getOrLoadBootstrapFiles(params: {
}
cache.set(params.sessionKey, { workspaceDir: params.workspaceDir, files });
pruneOldestBootstrapSnapshots();
pruneMapToMaxSize(cache, MAX_BOOTSTRAP_SNAPSHOTS);
return files;
}
@@ -8,6 +8,7 @@ import {
} from "@openclaw/ai/internal/shared";
import { stableStringify } from "@openclaw/normalization-core";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import type { NormalizedUsage } from "../usage.js";
type PromptCacheChangeCode =
@@ -169,10 +170,7 @@ function setTracker(key: string, tracker: PromptCacheTracker): void {
if (trackers.has(key)) {
trackers.delete(key);
} else if (trackers.size >= MAX_TRACKERS) {
const oldestKey = trackers.keys().next().value;
if (typeof oldestKey === "string") {
trackers.delete(oldestKey);
}
pruneMapToMaxSize(trackers, MAX_TRACKERS - 1);
}
trackers.set(key, tracker);
}
@@ -8,6 +8,7 @@ import type {
ContextEngineRuntimeContext,
ContextEngineSessionTarget,
} from "../../../context-engine/types.js";
import { pruneMapToMaxSize } from "../../../infra/map-size.js";
import { drainPluginNextTurnInjectionContext } from "../../../plugins/host-hook-state.js";
import { buildPluginAgentTurnPrepareContext } from "../../../plugins/host-hooks.js";
import type {
@@ -72,10 +73,7 @@ function rememberDrainedInjections(
if (promptBuildDrainCache.has(runId)) {
promptBuildDrainCache.delete(runId);
} else if (promptBuildDrainCache.size >= PROMPT_BUILD_DRAIN_CACHE_MAX) {
const oldest = promptBuildDrainCache.keys().next().value;
if (oldest !== undefined) {
promptBuildDrainCache.delete(oldest);
}
pruneMapToMaxSize(promptBuildDrainCache, PROMPT_BUILD_DRAIN_CACHE_MAX - 1);
}
promptBuildDrainCache.set(runId, injections);
}
@@ -1,4 +1,5 @@
/** Process-local prompt projection state owned by an embedded session lifecycle. */
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import { resolveGlobalSingleton } from "../../shared/global-singleton.js";
import type { AgentMessage } from "../runtime/index.js";
@@ -56,13 +57,7 @@ export function getEmbeddedSessionPromptState(sessionId: string): EmbeddedSessio
}
const created = createSessionPromptState();
sessionPromptStates.set(sessionId, created);
while (sessionPromptStates.size > MAX_SESSION_PROMPT_STATES) {
const oldest = sessionPromptStates.keys().next().value;
if (typeof oldest !== "string") {
break;
}
sessionPromptStates.delete(oldest);
}
pruneMapToMaxSize(sessionPromptStates, MAX_SESSION_PROMPT_STATES);
return created;
}
@@ -6,6 +6,7 @@ import {
import { stripAnsi } from "../../../packages/terminal-core/src/ansi.js";
import { isApprovalNotFoundError } from "../../infra/approval-errors.js";
import { toErrorObject } from "../../infra/errors.js";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { PluginApprovalResolutions } from "../../plugins/types.js";
import {
@@ -18,7 +19,10 @@ import {
nativeHookRelayParamsWereRewritten,
normalizeNativeHookToolName,
} from "./native-hook-relay-codec.js";
import { nativeHookRelayState } from "./native-hook-relay-state.js";
import {
MAX_NATIVE_HOOK_RELAY_INVOCATIONS,
nativeHookRelayState,
} from "./native-hook-relay-state.js";
import type {
JsonValue,
NativeHookRelayDeferredApprovalOutcome,
@@ -38,7 +42,6 @@ export type NativeHookRelayDeferredToolApprovalRequester = typeof requestDeferre
const DEFAULT_PERMISSION_TIMEOUT_MS = 120_000;
const PERMISSION_ALLOW_ALWAYS_TTL_MS = 30 * 60 * 1000;
const MAX_NATIVE_HOOK_RELAY_INVOCATIONS = 200;
const MAX_PERMISSION_FALLBACK_KEYS = 200;
const MAX_PERMISSION_FALLBACK_KEY_CHARS = 240;
const MAX_PERMISSION_FINGERPRINT_SORT_KEYS = 200;
@@ -445,13 +448,7 @@ function rememberNativeHookRelayPermissionAllowAlways(key: string, now = Date.no
return;
}
permissionAllowAlwaysApprovals.set(key, { expiresAtMs });
while (permissionAllowAlwaysApprovals.size > MAX_PERMISSION_ALLOW_ALWAYS_ENTRIES) {
const oldestKey = permissionAllowAlwaysApprovals.keys().next().value;
if (typeof oldestKey !== "string") {
break;
}
permissionAllowAlwaysApprovals.delete(oldestKey);
}
pruneMapToMaxSize(permissionAllowAlwaysApprovals, MAX_PERMISSION_ALLOW_ALWAYS_ENTRIES);
}
export function pruneNativeHookRelayPermissionAllowAlways(now = Date.now()): void {
@@ -1,6 +1,7 @@
import type { NativeHookRelaySharedState } from "./native-hook-relay-types.js";
const NATIVE_HOOK_RELAY_STATE_SYMBOL = Symbol.for("openclaw.nativeHookRelay.state");
export const MAX_NATIVE_HOOK_RELAY_INVOCATIONS = 200;
function getNativeHookRelaySharedState(): NativeHookRelaySharedState {
const globalRecord = globalThis as typeof globalThis & {
+4 -2
View File
@@ -42,7 +42,10 @@ import {
setNativeHookRelayPermissionApprovalRequesterForTests as setNativeHookRelayPermissionApprovalRequesterForTestsImpl,
} from "./native-hook-relay-permissions.js";
import type { NativeHookRelayDeferredToolApprovalRequester } from "./native-hook-relay-permissions.js";
import { nativeHookRelayState } from "./native-hook-relay-state.js";
import {
MAX_NATIVE_HOOK_RELAY_INVOCATIONS,
nativeHookRelayState,
} from "./native-hook-relay-state.js";
import type {
ActiveNativeHookRelayRegistration,
ActiveNativeHookRelayRegistrationHandle,
@@ -75,7 +78,6 @@ export type {
} from "./native-hook-relay-types.js";
const DEFAULT_RELAY_TTL_MS = 30 * 60 * 1000;
const MAX_NATIVE_HOOK_RELAY_INVOCATIONS = 200;
const log = createSubsystemLogger("agents/harness/native-hook-relay");
const { relays, relayBridges, invocations } = nativeHookRelayState;
+2 -7
View File
@@ -5,6 +5,7 @@ import {
resolveAgentModelPrimaryValue,
} from "../config/model-input.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { normalizePluginsConfig } from "../plugins/config-state.js";
import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js";
import { resolvePluginControlPlaneFingerprint } from "../plugins/plugin-control-plane-context.js";
@@ -158,13 +159,7 @@ export function resolveModelCandidateChain(
const candidates = resolveFallbackCandidatesUncached(params);
if (cacheKey) {
fallbackCandidateCache.set(cacheKey, candidates.map(cloneModelCandidate));
while (fallbackCandidateCache.size > MAX_FALLBACK_CANDIDATE_CACHE_ENTRIES) {
const oldest = fallbackCandidateCache.keys().next();
if (oldest.done) {
break;
}
fallbackCandidateCache.delete(oldest.value);
}
pruneMapToMaxSize(fallbackCandidateCache, MAX_FALLBACK_CANDIDATE_CACHE_ENTRIES);
}
return candidates;
}
+3 -13
View File
@@ -1,4 +1,5 @@
import path from "node:path";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { runCommandWithTimeout } from "../process/exec.js";
import { parseGitUrl } from "./utils/git.js";
@@ -18,18 +19,6 @@ function escapeProjectKeyForAnnotation(value: string): string {
.replaceAll("\n", "%0a");
}
function setBounded<K, V>(map: Map<K, V>, key: K, value: V, limit: number): void {
map.delete(key);
map.set(key, value);
while (map.size > limit) {
const oldest = map.keys().next().value as K | undefined;
if (oldest === undefined) {
break;
}
map.delete(oldest);
}
}
async function resolveUncachedProjectKey(repoRoot: string): Promise<string> {
try {
const result = await runCommandWithTimeout(
@@ -64,6 +53,7 @@ export function resolveProjectKey(repoRoot: string): Promise<string> {
return cached;
}
const pending = resolveUncachedProjectKey(canonicalRoot);
setBounded(projectKeyByRepoRoot, canonicalRoot, pending, MAX_PROJECT_KEY_CACHE_ENTRIES);
projectKeyByRepoRoot.set(canonicalRoot, pending);
pruneMapToMaxSize(projectKeyByRepoRoot, MAX_PROJECT_KEY_CACHE_ENTRIES);
return pending;
}
+2 -7
View File
@@ -3,6 +3,7 @@ import { HEARTBEAT_TRANSCRIPT_PROMPT } from "../auto-reply/heartbeat.js";
import { HEARTBEAT_TOKEN } from "../auto-reply/tokens.js";
import { normalizeAgentPlanSteps } from "../channels/streaming.js";
import type { AgentEventPayload } from "../infra/agent-events.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { redactToolPayloadText } from "../logging/redact.js";
import {
buildAgentRunTerminalOutcomeFromLifecycleEvent,
@@ -167,13 +168,7 @@ function rememberItemStatus(
}
state.itemStatuses.delete(itemId);
state.itemStatuses.set(itemId, status);
while (state.itemStatuses.size > limit) {
const oldest = state.itemStatuses.keys().next().value;
if (oldest === undefined) {
break;
}
state.itemStatuses.delete(oldest);
}
pruneMapToMaxSize(state.itemStatuses, limit);
return true;
}
+2 -6
View File
@@ -8,6 +8,7 @@ import fs from "node:fs";
import path from "node:path";
import { AnsiSequenceStripper } from "../../packages/terminal-core/src/ansi-sequences.js";
import { stripAnsiForStreamChunk } from "../../packages/terminal-core/src/ansi.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import {
killProcessTree as killProcessTreeGracefully,
type KillProcessTreeOptions,
@@ -133,12 +134,7 @@ function resolveWindowsGitBashUsrBin(shellPath: string): string | undefined {
fs.existsSync(usrBin)
? usrBin
: undefined;
if (windowsGitBashUsrBinCache.size >= WINDOWS_GIT_BASH_CACHE_LIMIT) {
const oldestKey = windowsGitBashUsrBinCache.keys().next().value;
if (oldestKey) {
windowsGitBashUsrBinCache.delete(oldestKey);
}
}
pruneMapToMaxSize(windowsGitBashUsrBinCache, WINDOWS_GIT_BASH_CACHE_LIMIT - 1);
windowsGitBashUsrBinCache.set(cacheKey, resolved);
return resolved;
}
+2 -7
View File
@@ -28,6 +28,7 @@ import {
} from "../channels/plugins/native-approval-prompt.js";
import type { SubagentDelegationMode } from "../config/types.agent-defaults.js";
import type { MemoryCitationsMode } from "../config/types.memory.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import {
buildMemoryPromptSection,
type PreparedMemoryPromptSection,
@@ -159,13 +160,7 @@ function cacheStablePromptPrefix(key: string, build: () => string): string {
const value = build();
stablePromptPrefixCache.set(key, { value });
while (stablePromptPrefixCache.size > SYSTEM_PROMPT_STABLE_PREFIX_CACHE_LIMIT) {
const oldestKey = stablePromptPrefixCache.keys().next().value;
if (oldestKey === undefined) {
break;
}
stablePromptPrefixCache.delete(oldestKey);
}
pruneMapToMaxSize(stablePromptPrefixCache, SYSTEM_PROMPT_STABLE_PREFIX_CACHE_LIMIT);
return value;
}
+2 -7
View File
@@ -1,3 +1,4 @@
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { generateSecureToken } from "../infra/secure-random.js";
import { getPluginToolMeta, type PluginToolMcpMeta } from "../plugins/tools.js";
import type { HookContext } from "./agent-tools.before-tool-call.js";
@@ -153,13 +154,7 @@ function rememberReusableCatalog(key: string | undefined, catalog: ToolSearchCat
reusableCatalogSnapshots.delete(key);
}
reusableCatalogSnapshots.set(key, { entries: catalog.entries, fingerprint });
while (reusableCatalogSnapshots.size > MAX_REUSABLE_CATALOG_SNAPSHOTS) {
const oldestKey = reusableCatalogSnapshots.keys().next().value;
if (!oldestKey) {
break;
}
reusableCatalogSnapshots.delete(oldestKey);
}
pruneMapToMaxSize(reusableCatalogSnapshots, MAX_REUSABLE_CATALOG_SNAPSHOTS);
}
function classifyTool(tool: CatalogTool): {
+2 -6
View File
@@ -9,6 +9,7 @@ import {
MAX_TIMER_TIMEOUT_SECONDS,
resolveExpiresAtMsFromDurationMs,
} from "@openclaw/normalization-core/number-coercion";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
export type CacheEntry<T> = {
value: T;
expiresAt: number;
@@ -72,12 +73,7 @@ export function writeCache<T>(
if (expiresAt === undefined) {
return;
}
if (cache.size >= DEFAULT_CACHE_MAX_ENTRIES) {
const oldest = cache.keys().next();
if (!oldest.done) {
cache.delete(oldest.value);
}
}
pruneMapToMaxSize(cache, DEFAULT_CACHE_MAX_ENTRIES - 1);
cache.set(key, {
value,
expiresAt,
+2 -7
View File
@@ -12,6 +12,7 @@ import {
import { isAllowedToolCallName } from "../agents/tool-call-shared.js";
import type { AgentEventPayload } from "../infra/agent-events.js";
import type { TrustedToolExecutionEvent } from "../infra/diagnostic-events.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { parseAgentSessionKey } from "../routing/session-key.js";
import type {
@@ -81,13 +82,7 @@ function rememberRunProvenance(
): void {
runProvenance.delete(runId);
runProvenance.set(runId, provenance);
while (runProvenance.size > MAX_TRACKED_RUN_PROVENANCE) {
const oldestRunId = runProvenance.keys().next().value;
if (oldestRunId === undefined) {
break;
}
runProvenance.delete(oldestRunId);
}
pruneMapToMaxSize(runProvenance, MAX_TRACKED_RUN_PROVENANCE);
}
function resolveProvenance(
+2 -7
View File
@@ -1,6 +1,7 @@
import { resolveAgentIdentity } from "../../agents/identity.js";
import { deriveContextPromptTokens, type NormalizedUsage } from "../../agents/usage.js";
import type { OpenClawConfig } from "../../config/config.js";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import type { PluginHookReplyUsageState } from "../../plugins/hook-types.js";
import { estimateUsageCost, resolveModelCostConfig } from "../../utils/usage-format.js";
@@ -113,13 +114,7 @@ function prune(now: number): void {
}
// This handoff is best-effort metadata for an optional hook. Bound bursts so
// completed runs cannot retain one full snapshot each for the whole TTL.
while (store.size > MAX_REPLY_USAGE_STATE_ENTRIES) {
const oldest = store.keys().next();
if (oldest.done) {
return;
}
store.delete(oldest.value);
}
pruneMapToMaxSize(store, MAX_REPLY_USAGE_STATE_ENTRIES);
}
export function recordReplyUsageState(
+2 -7
View File
@@ -3,6 +3,7 @@ import {
resolveExpiresAtMsFromDurationMs,
} from "@openclaw/normalization-core/number-coercion";
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import { normalizeAccountId } from "../../routing/account-id.js";
import { outboundMessageIdentities } from "./outbound-echo-state.js";
@@ -63,13 +64,7 @@ export function recordOutboundMessageIdentity(identity: OutboundMessageIdentity)
pruneExpiredEntries(nowMs);
for (const key of keys) {
outboundMessageIdentities.delete(key);
while (outboundMessageIdentities.size >= OUTBOUND_MESSAGE_IDENTITY_MAX_ENTRIES) {
const oldest = outboundMessageIdentities.keys().next();
if (oldest.done) {
break;
}
outboundMessageIdentities.delete(oldest.value);
}
pruneMapToMaxSize(outboundMessageIdentities, OUTBOUND_MESSAGE_IDENTITY_MAX_ENTRIES - 1);
outboundMessageIdentities.set(key, expiresAt);
}
}
+13 -36
View File
@@ -8,6 +8,7 @@ import path from "node:path";
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { extractErrorCode, formatErrorMessage } from "../../infra/errors.js";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import { isPathInside } from "../../infra/path-guards.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import type {
@@ -113,23 +114,6 @@ const bundledChannelLoadContextsByRoot = new Map<string, BundledChannelLoadConte
const bundledChannelBoundaryRoots = new Map<string, string>();
const sourceBundledEntryLoaderCache: PluginModuleLoaderCache = new Map();
function rememberBoundedBundledChannelValue<TKey, TValue>(
cache: Map<TKey, TValue>,
key: TKey,
value: TValue,
maxSize: number,
): TValue {
cache.delete(key);
cache.set(key, value);
if (cache.size > maxSize) {
const oldestKey = cache.keys().next().value;
if (oldestKey !== undefined) {
cache.delete(oldestKey);
}
}
return value;
}
function isSourceModulePath(modulePath: string): boolean {
return /\.(?:c|m)?tsx?$/iu.test(modulePath);
}
@@ -201,12 +185,10 @@ function resolveBundledChannelBoundaryRoot(params: {
].join("\0");
const cached = bundledChannelBoundaryRoots.get(cacheKey);
if (cached) {
return rememberBoundedBundledChannelValue(
bundledChannelBoundaryRoots,
cacheKey,
cached,
MAX_BUNDLED_CHANNEL_BOUNDARY_ROOTS,
);
bundledChannelBoundaryRoots.delete(cacheKey);
bundledChannelBoundaryRoots.set(cacheKey, cached);
pruneMapToMaxSize(bundledChannelBoundaryRoots, MAX_BUNDLED_CHANNEL_BOUNDARY_ROOTS);
return cached;
}
const canonicalModulePath = resolveCanonicalPathOrAbsolute(params.modulePath);
const sourceRoot = path.resolve(params.packageRoot, "extensions", params.metadata.dirName);
@@ -222,12 +204,9 @@ function resolveBundledChannelBoundaryRoot(params: {
.map(resolveCanonicalPathOrAbsolute)
.find((root) => isPathInside(root, canonicalModulePath)) ??
resolveCanonicalPathOrAbsolute(sourceRoot);
return rememberBoundedBundledChannelValue(
bundledChannelBoundaryRoots,
cacheKey,
boundaryRoot,
MAX_BUNDLED_CHANNEL_BOUNDARY_ROOTS,
);
bundledChannelBoundaryRoots.set(cacheKey, boundaryRoot);
pruneMapToMaxSize(bundledChannelBoundaryRoots, MAX_BUNDLED_CHANNEL_BOUNDARY_ROOTS);
return boundaryRoot;
}
function resolveGeneratedBundledChannelModulePath(params: {
@@ -406,12 +385,11 @@ function resolveActiveBundledChannelLoadScope(env: NodeJS.ProcessEnv = process.e
loadContext: BundledChannelLoadContext;
} {
const rootScope = resolveBundledChannelRootScope(env);
const loadContext = rememberBoundedBundledChannelValue(
bundledChannelLoadContextsByRoot,
rootScope.cacheKey,
bundledChannelLoadContextsByRoot.get(rootScope.cacheKey) ?? createBundledChannelLoadContext(),
MAX_BUNDLED_CHANNEL_LOAD_CONTEXTS,
);
const loadContext =
bundledChannelLoadContextsByRoot.get(rootScope.cacheKey) ?? createBundledChannelLoadContext();
bundledChannelLoadContextsByRoot.delete(rootScope.cacheKey);
bundledChannelLoadContextsByRoot.set(rootScope.cacheKey, loadContext);
pruneMapToMaxSize(bundledChannelLoadContextsByRoot, MAX_BUNDLED_CHANNEL_LOAD_CONTEXTS);
return {
rootScope,
loadContext,
@@ -779,4 +757,3 @@ export function setBundledChannelRuntime(id: ChannelId, runtime: PluginRuntime):
}
setter(runtime);
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
+5 -9
View File
@@ -13,6 +13,7 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/ag
import { resolveRuntimeConfigCacheKey } from "../../config/runtime-snapshot.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import { isBlockedObjectKey } from "../../infra/prototype-keys.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import {
@@ -105,15 +106,10 @@ function rememberReadOnlyChannelPluginResolution(
readOnlyChannelPluginResolutionCache.delete(key);
}
readOnlyChannelPluginResolutionCache.set(key, cloneReadOnlyChannelPluginResolution(resolution));
while (
readOnlyChannelPluginResolutionCache.size > MAX_READ_ONLY_CHANNEL_PLUGIN_RESOLUTION_CACHE_SIZE
) {
const oldestKey = readOnlyChannelPluginResolutionCache.keys().next().value;
if (!oldestKey) {
break;
}
readOnlyChannelPluginResolutionCache.delete(oldestKey);
}
pruneMapToMaxSize(
readOnlyChannelPluginResolutionCache,
MAX_READ_ONLY_CHANNEL_PLUGIN_RESOLUTION_CACHE_SIZE,
);
}
function resolveReadOnlyChannelPluginResolutionCacheKey(params: {
+2 -6
View File
@@ -2,6 +2,7 @@
import crypto from "node:crypto";
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import { CHANNEL_IDS } from "../channels/ids.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { parseConfigPathArrayIndex } from "../shared/path-array-index.js";
import { GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA } from "./bundled-channel-config-metadata.generated.js";
import { computeBaseConfigSchemaResponse } from "./schema-base.js";
@@ -506,12 +507,7 @@ function buildMergedSchemaCacheKey(params: {
}
function setMergedSchemaCache(key: string, value: ConfigSchemaResponse): void {
if (mergedSchemaCache.size >= MERGED_SCHEMA_CACHE_MAX) {
const oldest = mergedSchemaCache.keys().next();
if (!oldest.done) {
mergedSchemaCache.delete(oldest.value);
}
}
pruneMapToMaxSize(mergedSchemaCache, MERGED_SCHEMA_CACHE_MAX - 1);
mergedSchemaCache.set(key, value);
}
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
import { asOptionalRecord as asRecord } from "@openclaw/normalization-core/record-coerce";
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
import { executeSqliteQueryTakeFirstSync } from "../../infra/kysely-sync.js";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import { extractAssistantVisibleText } from "../../shared/chat-message-content.js";
import {
openOpenClawAgentDatabase,
@@ -122,12 +123,7 @@ function loadSessionBranchSummaries(
);
sessionBranchCache.delete(cacheKey);
sessionBranchCache.set(cacheKey, { ...watermark, branches });
if (sessionBranchCache.size > SESSION_BRANCH_CACHE_MAX_ENTRIES) {
const oldestKey = sessionBranchCache.keys().next().value;
if (oldestKey !== undefined) {
sessionBranchCache.delete(oldestKey);
}
}
pruneMapToMaxSize(sessionBranchCache, SESSION_BRANCH_CACHE_MAX_ENTRIES);
return cloneSessionBranchSummaries(branches);
}
@@ -4,6 +4,7 @@ import {
executeSqliteQueryTakeFirstSync,
getNodeSqliteKysely,
} from "../../infra/kysely-sync.js";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import type { DB as OpenClawAgentKyselyDatabase } from "../../state/openclaw-agent-db.generated.js";
import type { OpenClawAgentDatabase } from "../../state/openclaw-agent-db.js";
import type { TranscriptEvent } from "./session-accessor.sqlite-contract.js";
@@ -121,13 +122,7 @@ function readTranscriptGeneration(projection: ResetWindowProjection): string | u
function cacheResetMessageWindow(key: string, entry: ResetMessageWindowCacheEntry): void {
resetMessageWindowCache.delete(key);
resetMessageWindowCache.set(key, entry);
while (resetMessageWindowCache.size > MAX_RESET_MESSAGE_WINDOW_CACHE) {
const oldest = resetMessageWindowCache.keys().next().value;
if (typeof oldest !== "string") {
break;
}
resetMessageWindowCache.delete(oldest);
}
pruneMapToMaxSize(resetMessageWindowCache, MAX_RESET_MESSAGE_WINDOW_CACHE);
}
function findLatestResetMessageWindow(
+3 -14
View File
@@ -3,6 +3,7 @@ import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { writeTextAtomic } from "../../infra/json-files.js";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import type { SessionEntry, SessionSkillPromptRef, SessionSkillSnapshot } from "./types.js";
const PROMPT_BLOB_DIR = "skills-prompts";
@@ -69,13 +70,7 @@ function buildPromptRef(prompt: string): SessionSkillPromptRef {
};
promptRefCache.set(prompt, ref);
// Bounded process cache avoids rehashing repeated prompt snapshots without becoming store state.
while (promptRefCache.size > PROMPT_REF_CACHE_MAX_ENTRIES) {
const oldest = promptRefCache.keys().next().value;
if (typeof oldest !== "string") {
break;
}
promptRefCache.delete(oldest);
}
pruneMapToMaxSize(promptRefCache, PROMPT_REF_CACHE_MAX_ENTRIES);
return ref;
}
@@ -88,13 +83,7 @@ function shouldStorePromptAsBlob(prompt: string): boolean {
function rememberValidPromptBlob(blobPath: string, stat: fs.Stats, prompt: string): void {
validPromptBlobCache.set(blobPath, { mtimeMs: stat.mtimeMs, size: stat.size, prompt });
while (validPromptBlobCache.size > VALID_PROMPT_BLOB_CACHE_MAX_ENTRIES) {
const oldest = validPromptBlobCache.keys().next().value;
if (typeof oldest !== "string") {
break;
}
validPromptBlobCache.delete(oldest);
}
pruneMapToMaxSize(validPromptBlobCache, VALID_PROMPT_BLOB_CACHE_MAX_ENTRIES);
}
function readValidPromptBlob(storePath: string, ref: SessionSkillPromptRef): string | null {
+3 -8
View File
@@ -1,6 +1,7 @@
/** Computes at/every/cron schedule timestamps with bounded Croner caching. */
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { Cron } from "croner";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { parseAbsoluteTimeMs } from "./parse.js";
import { coerceFiniteScheduleNumber } from "./schedule-number.js";
import type { CronSchedule } from "./types.js";
@@ -27,14 +28,8 @@ function resolveCachedCron(expr: string, timezone: string): Cron {
cronEvalCache.set(key, cached);
return cached;
}
if (cronEvalCache.size >= CRON_EVAL_CACHE_MAX) {
// Expression parsing is expensive enough to cache, but cron jobs can be
// edited dynamically; keep the cache bounded and LRU-like.
const oldest = cronEvalCache.keys().next().value;
if (oldest) {
cronEvalCache.delete(oldest);
}
}
// Expression parsing is expensive, so retain the most recently promoted entries.
pruneMapToMaxSize(cronEvalCache, CRON_EVAL_CACHE_MAX - 1);
const next = new Cron(expr, { timezone, catch: false });
cronEvalCache.set(key, next);
return next;
+3 -8
View File
@@ -1,6 +1,7 @@
/** Scheduling state and next-run computation for cron jobs. */
import crypto from "node:crypto";
import { expectDefined } from "@openclaw/normalization-core";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import { isCronJobActive } from "../active-jobs.js";
import { parseAbsoluteTimeMs } from "../parse.js";
import {
@@ -99,14 +100,8 @@ function resolveStableCronOffsetMs(jobId: string, staggerMs: number) {
}
const digest = crypto.createHash("sha256").update(jobId).digest();
const offset = digest.readUInt32BE(0) % staggerMs;
if (staggerOffsetCache.size >= STAGGER_OFFSET_CACHE_MAX) {
// The offset is deterministic, so the cache can evict oldest entries
// without changing scheduling semantics for future lookups.
const first = staggerOffsetCache.keys().next();
if (!first.done) {
staggerOffsetCache.delete(first.value);
}
}
// The offset is deterministic, so FIFO eviction does not change future scheduling semantics.
pruneMapToMaxSize(staggerOffsetCache, STAGGER_OFFSET_CACHE_MAX - 1);
staggerOffsetCache.set(cacheKey, offset);
return offset;
}
+2 -6
View File
@@ -3,6 +3,7 @@ import fs from "node:fs";
import path from "node:path";
import type { DatabaseSync } from "node:sqlite";
import { expandHomePrefix } from "../infra/home-dir.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { openNodeSqliteDatabase } from "../infra/node-sqlite.js";
import {
openOpenClawStateDatabase,
@@ -50,12 +51,7 @@ function noteCronJobsStoreCommit(storeKey: string): void {
// polling SQLite or discarding the current scheduler's transient run state.
cronStoreRevisions.delete(storeKey);
cronStoreRevisions.set(storeKey, ++nextCronStoreRevision);
if (cronStoreRevisions.size > MAX_TRACKED_CRON_STORE_REVISIONS) {
const oldestStoreKey = cronStoreRevisions.keys().next().value;
if (oldestStoreKey !== undefined) {
cronStoreRevisions.delete(oldestStoreKey);
}
}
pruneMapToMaxSize(cronStoreRevisions, MAX_TRACKED_CRON_STORE_REVISIONS);
}
function resolveDefaultCronDir(env: NodeJS.ProcessEnv): string {
+2 -10
View File
@@ -41,6 +41,7 @@ import type { AnyAgentTool } from "../agents/tools/common.js";
import { ensureAgentWorkspace } from "../agents/workspace.js";
import { parseDurationMs } from "../cli/parse-duration.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import type { PluginRegistry } from "../plugins/registry-types.js";
import { withPluginRuntimeRegistryScope } from "../plugins/runtime/gateway-request-scope.js";
import { getPluginToolMeta } from "../plugins/tools.js";
@@ -343,15 +344,6 @@ function createCronCodeModeRunner(deps: CronTriggerEvaluatorDeps) {
// concurrent cold evaluations for one job single-flight.
const runtimeCache = new Map<string, TriggerRuntimeCacheEntry>();
const trimRuntimeCache = () => {
while (runtimeCache.size > MAX_CACHED_TRIGGER_RUNTIMES) {
const oldestJobId = runtimeCache.keys().next().value;
if (oldestJobId === undefined) {
return;
}
runtimeCache.delete(oldestJobId);
}
};
const resolveCachedRuntime = async (request: {
runtimeConfig: OpenClawConfig;
jobId: string;
@@ -404,7 +396,7 @@ function createCronCodeModeRunner(deps: CronTriggerEvaluatorDeps) {
};
runtimeCache.delete(request.jobId);
runtimeCache.set(request.jobId, entry);
trimRuntimeCache();
pruneMapToMaxSize(runtimeCache, MAX_CACHED_TRIGGER_RUNTIMES);
// Failed preparations evict themselves so the next tick retries cold.
void promise.catch(() => {
if (runtimeCache.get(request.jobId) === entry) {
+2 -13
View File
@@ -12,6 +12,7 @@ import {
ArchiveSecurityError,
extractArchive,
} from "../infra/archive.js";
import { createBackupLinkCache } from "../infra/backup-volatile-stat-cache.js";
import { formatErrorMessage as errorMessage } from "../infra/errors.js";
import { root as fsSafeRoot } from "../infra/fs-safe.js";
import {
@@ -49,18 +50,6 @@ const RESTORE_VERIFY_TIMEOUT_MS = 60_000;
const RESTORE_VERIFY_POLL_MS = 1_000;
const RESTORE_EXTRACT_TIMEOUT_MS = 30 * 60_000;
type BackupLinkCacheKey = `${number}:${number}`;
class BackupLinkCache extends Map<BackupLinkCacheKey, string> {
override get(_key: BackupLinkCacheKey): undefined {
return undefined;
}
override set(_key: BackupLinkCacheKey, _value: string): this {
return this;
}
}
type FleetBackupManifest = {
schemaVersion: 1;
kind: "openclaw-fleet-cell-backup";
@@ -316,7 +305,7 @@ export async function backupFleetCell(params: {
gzip: true,
portable: true,
preservePaths: true,
linkCache: new BackupLinkCache(),
linkCache: createBackupLinkCache(),
filter,
onWriteEntry: (entry) => {
entry.path = remapArchivePath(entry.path, manifestPath, dataTarget, authTarget);
+2 -7
View File
@@ -6,6 +6,7 @@ import {
readOpenedLocalAgentAvatarDataUrl,
type OpenedLocalAgentAvatarFile,
} from "../agents/identity-avatar-file.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
type AvatarDataUrlCacheEntry = {
ctimeMs: number;
@@ -59,13 +60,7 @@ export function createGatewayAvatarDataUrlCache(params?: {
size: opened.stat.size,
dataUrl,
});
while (entries.size > maxEntries) {
const oldestPath = entries.keys().next().value;
if (oldestPath === undefined) {
break;
}
entries.delete(oldestPath);
}
pruneMapToMaxSize(entries, maxEntries);
return dataUrl;
},
};
+2 -4
View File
@@ -1,5 +1,6 @@
// Control-plane rate limiting bounds write-side RPC attempts per device/IP and
// caps bucket growth against unique-key memory pressure.
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { normalizeControlPlaneIdentityPart } from "./control-plane-identity.js";
import type { GatewayClient } from "./server-methods/types.js";
@@ -53,10 +54,7 @@ export function consumeControlPlaneWriteBudget(params: {
!controlPlaneBuckets.has(key) &&
controlPlaneBuckets.size >= CONTROL_PLANE_BUCKET_MAX_ENTRIES
) {
const oldest = controlPlaneBuckets.keys().next().value;
if (oldest !== undefined) {
controlPlaneBuckets.delete(oldest);
}
pruneMapToMaxSize(controlPlaneBuckets, CONTROL_PLANE_BUCKET_MAX_ENTRIES - 1);
}
controlPlaneBuckets.set(key, {
count: 1,
+2 -7
View File
@@ -1,3 +1,4 @@
import { pruneMapToMaxSize } from "../infra/map-size.js";
import type { ControlUiGitHubPreview } from "./control-ui-contract.js";
// Same-origin GitHub metadata adapter for Control UI link previews.
import {
@@ -325,12 +326,6 @@ export function loadControlUiGitHubPreview(
}),
};
previewCache.set(key, entry);
while (previewCache.size > CACHE_LIMIT) {
const oldestKey = previewCache.keys().next().value as string | undefined;
if (!oldestKey) {
break;
}
previewCache.delete(oldestKey);
}
pruneMapToMaxSize(previewCache, CACHE_LIMIT);
return entry.promise;
}
@@ -1,4 +1,5 @@
import { runGit } from "../agents/worktrees/git.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import {
gitOutput,
resolveBranchLanding,
@@ -45,13 +46,7 @@ function createLocalGitCache<T>() {
const entry = { expiresAt: Date.now() + LOCAL_GIT_CACHE_MS, promise: load() };
entries.delete(key);
entries.set(key, entry);
while (entries.size > LOCAL_GIT_CACHE_LIMIT) {
const oldestKey = entries.keys().next().value;
if (oldestKey === undefined) {
break;
}
entries.delete(oldestKey);
}
pruneMapToMaxSize(entries, LOCAL_GIT_CACHE_LIMIT);
return entry.promise;
};
}
+2 -7
View File
@@ -5,6 +5,7 @@ import nodePath from "node:path";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { runGit } from "../agents/worktrees/git.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js";
import type {
ControlUiSessionBranch,
@@ -669,12 +670,6 @@ async function cachedBranchPullRequests(
);
branchCache.delete(key);
branchCache.set(key, entry);
while (branchCache.size > CACHE_LIMIT) {
const oldestKey = branchCache.keys().next().value as string | undefined;
if (!oldestKey) {
break;
}
branchCache.delete(oldestKey);
}
pruneMapToMaxSize(branchCache, CACHE_LIMIT);
return promise;
}
+2 -7
View File
@@ -3,6 +3,7 @@ import fs from "node:fs";
import type { IncomingMessage, ServerResponse } from "node:http";
import path from "node:path";
import { brotliCompress, constants as zlibConstants, gzip } from "node:zlib";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { getOrCreatePromise } from "../shared/lazy-promise.js";
const CONTROL_UI_IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
@@ -303,13 +304,7 @@ function cachedCompressedControlUiHtml(
() => compressControlUiBody(Buffer.from(body), encoding),
{ cacheRejections: false },
);
while (controlUiHtmlCompressionCache.size > CONTROL_UI_HTML_COMPRESSION_CACHE_MAX_ENTRIES) {
const oldestKey = controlUiHtmlCompressionCache.keys().next().value;
if (oldestKey === undefined) {
break;
}
controlUiHtmlCompressionCache.delete(oldestKey);
}
pruneMapToMaxSize(controlUiHtmlCompressionCache, CONTROL_UI_HTML_COMPRESSION_CACHE_MAX_ENTRIES);
return compression;
}
+2 -7
View File
@@ -1,5 +1,6 @@
import { resolveCronTriggerMinIntervalMs } from "../config/cron-limits.js";
import type { CronJob, CronJobState } from "../cron/types.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import type { ProcessSupervisor } from "../process/supervisor/index.js";
import {
CronStreamJobOwner,
@@ -140,13 +141,7 @@ export function createCronStreamWatchers(params: {
snapshot.coalescedBatches,
),
});
while (retiredCounterSeeds.size > MAX_RETIRED_COUNTER_SEEDS) {
const oldest = retiredCounterSeeds.keys().next().value;
if (oldest === undefined) {
break;
}
retiredCounterSeeds.delete(oldest);
}
pruneMapToMaxSize(retiredCounterSeeds, MAX_RETIRED_COUNTER_SEEDS);
};
const createOwner = (job: CronStreamJob): CronStreamJobOwner => {
+5 -10
View File
@@ -16,6 +16,7 @@ import type { ReplyMediaAttachment } from "../auto-reply/reply-payload.js";
import { getRuntimeConfig } from "../config/config.js";
import { resolveStateDir } from "../config/paths.js";
import { openLocalFileSafely, readLocalFileSafely } from "../infra/fs-safe.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { assertLocalMediaAllowed, resolveLocalMediaRoots } from "../media/local-media-access.js";
import { resolveLocalMediaPath } from "../media/local-media-path.js";
import { probePlaybackMediaFileDescriptor } from "../media/media-probe.js";
@@ -831,16 +832,10 @@ function setCachedSessionManagedOutgoingAttachmentIndex(
index,
},
);
while (
sessionManagedOutgoingAttachmentIndexCache.size >
MAX_SESSION_MANAGED_OUTGOING_ATTACHMENT_INDEX_CACHE_ENTRIES
) {
const oldestKey = sessionManagedOutgoingAttachmentIndexCache.keys().next().value;
if (!oldestKey) {
break;
}
sessionManagedOutgoingAttachmentIndexCache.delete(oldestKey);
}
pruneMapToMaxSize(
sessionManagedOutgoingAttachmentIndexCache,
MAX_SESSION_MANAGED_OUTGOING_ATTACHMENT_INDEX_CACHE_ENTRIES,
);
}
function sameManagedOutgoingAttachmentTranscriptStat(
+25 -1
View File
@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { setPluginToolMeta } from "../plugins/tools.js";
import {
@@ -44,6 +44,10 @@ beforeEach(() => {
);
});
afterEach(() => {
vi.useRealTimers();
});
describe("resolveMcpLoopbackScopedTools", () => {
it("keeps the full session scope without a grant allowlist", () => {
const scoped = resolveMcpLoopbackScopedTools(scopeParams());
@@ -159,6 +163,26 @@ describe("resolveMcpLoopbackScopedTools", () => {
});
describe("McpLoopbackToolCache", () => {
it("expires at the ttl boundary and partitions rows by config identity", () => {
vi.useFakeTimers();
const cache = new McpLoopbackToolCache();
const cfgA = {} as OpenClawConfig;
const cfgB = {} as OpenClawConfig;
const paramsA = scopeParams({ cfg: cfgA });
cache.resolve(paramsA);
cache.resolve(paramsA);
expect(resolveGatewayScopedTools).toHaveBeenCalledTimes(1);
vi.advanceTimersByTime(30_000);
cache.resolve(paramsA);
expect(resolveGatewayScopedTools).toHaveBeenCalledTimes(2);
cache.resolve(scopeParams({ cfg: cfgB }));
cache.resolve(paramsA);
expect(resolveGatewayScopedTools).toHaveBeenCalledTimes(3);
});
it("does not share cache rows across different grant allowlists", () => {
const cache = new McpLoopbackToolCache();
const cfg = {} as OpenClawConfig;
+5 -23
View File
@@ -3,6 +3,7 @@
import { applyEmbeddedAttemptToolsAllow } from "../agents/embedded-agent-runner/run/attempt-tool-construction-plan.js";
import { normalizeToolName } from "../agents/tool-policy.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { DirectoryCache } from "../infra/outbound/directory-cache.js";
import { getPluginToolMeta } from "../plugins/tools.js";
import type { McpLoopbackRequestContext } from "./mcp-grant-store.js";
import {
@@ -28,8 +29,6 @@ type CachedScopedTools = {
workspaceDir: string | undefined;
tools: McpLoopbackTool[];
toolSchema: McpToolSchemaEntry[];
configRef: OpenClawConfig;
time: number;
};
type McpLoopbackScopeParams = Omit<McpLoopbackRequestContext, "senderIsOwner"> & {
@@ -161,7 +160,7 @@ function applyPolicyToolsAllow(
/** Short-lived cache for loopback tool lists keyed by session/channel context. */
export class McpLoopbackToolCache {
#entries = new Map<string, CachedScopedTools>();
#entries = new DirectoryCache<CachedScopedTools>(TOOL_CACHE_TTL_MS, TOOL_CACHE_MAX_ENTRIES);
resolve(params: McpLoopbackScopeParams): CachedScopedTools {
// Callers differing only in capabilities must not share cached tool lists.
@@ -229,16 +228,8 @@ export class McpLoopbackToolCache {
? "non-owner"
: "unknown-owner",
].join("\u0000");
const now = Date.now();
for (const [key, entry] of this.#entries) {
if (now - entry.time >= TOOL_CACHE_TTL_MS) {
this.#entries.delete(key);
}
}
const cached = this.#entries.get(cacheKey);
// Config object identity is part of the cache contract so explicit gateway
// reloads invalidate tool scope and schema without filesystem polling.
if (cached && cached.configRef === params.cfg && now - cached.time < TOOL_CACHE_TTL_MS) {
const cached = this.#entries.get(cacheKey, params.cfg);
if (cached) {
return cached;
}
@@ -248,17 +239,8 @@ export class McpLoopbackToolCache {
workspaceDir: next.workspaceDir,
tools: next.tools,
toolSchema: buildMcpToolSchema(next.tools),
configRef: params.cfg,
time: now,
};
this.#entries.set(cacheKey, nextEntry);
while (this.#entries.size > TOOL_CACHE_MAX_ENTRIES) {
const oldestKey = this.#entries.keys().next().value;
if (oldestKey === undefined) {
break;
}
this.#entries.delete(oldestKey);
}
this.#entries.set(cacheKey, nextEntry, params.cfg);
return nextEntry;
}
}
+4 -12
View File
@@ -1,6 +1,7 @@
import { randomBytes } from "node:crypto";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import type { InternalChannelThreadingToolContext } from "../channels/threading-tool-context-internal.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { normalizeAgentId } from "../routing/session-key.js";
import {
isDeliverableMessageChannel,
@@ -88,13 +89,6 @@ function copyToolContext(
};
}
function evictOldestCapability(): void {
const oldest = capabilitiesByToken.keys().next().value;
if (typeof oldest === "string") {
capabilitiesByToken.delete(oldest);
}
}
function sweepExpiredMessageActionTurnCapabilities(nowMs: number = Date.now()): number {
let removed = 0;
for (const [token, capability] of capabilitiesByToken) {
@@ -131,11 +125,9 @@ export function mintMessageActionTurnCapability(params: {
}
const nowMs = params.nowMs ?? Date.now();
sweepExpiredMessageActionTurnCapabilities(nowMs);
while (capabilitiesByToken.size >= MAX_ACTIVE_CAPABILITIES) {
// A bounded fail-closed store prevents abandoned long-running turns from
// growing process memory without creating a second persistent state path.
evictOldestCapability();
}
// A bounded fail-closed store prevents abandoned long-running turns from
// growing process memory without creating a second persistent state path.
pruneMapToMaxSize(capabilitiesByToken, MAX_ACTIVE_CAPABILITIES - 1);
const token = randomBytes(32).toString("base64url");
capabilitiesByToken.set(token, {
agentId,
+2 -7
View File
@@ -8,6 +8,7 @@ import net from "node:net";
import os from "node:os";
import type { GatewayNodePairingConfig } from "../config/types.gateway.js";
import { normalizeDevicePublicKeyBase64Url } from "../infra/device-identity.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { getOrCreatePromise } from "../shared/lazy-promise.js";
import { isLoopbackAddress, isPrivateOrLoopbackAddress, isTrustedProxyAddress } from "./net.js";
import {
@@ -171,13 +172,7 @@ function pruneCooldowns(nowMs: number) {
cooldownExpiryByKey.delete(key);
}
}
while (cooldownExpiryByKey.size > MAX_COOLDOWN_ENTRIES) {
const oldest = cooldownExpiryByKey.keys().next().value;
if (oldest === undefined) {
break;
}
cooldownExpiryByKey.delete(oldest);
}
pruneMapToMaxSize(cooldownExpiryByKey, MAX_COOLDOWN_ENTRIES);
}
/**
+2 -11
View File
@@ -18,6 +18,7 @@ import type { CliDeps } from "../cli/deps.types.js";
import { agentCommandFromIngress } from "../commands/agent.js";
import type { GatewayHttpResponsesConfig } from "../config/types.gateway.js";
import { emitAgentEvent, onAgentEvent } from "../infra/agent-events.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { logWarn } from "../logger.js";
import { renderFileContextBlock } from "../media/file-context.js";
import {
@@ -176,16 +177,6 @@ function pruneExpiredResponseSessions(now: number) {
}
}
function evictOverflowResponseSessions() {
while (responseSessionMap.size > MAX_RESPONSE_SESSION_ENTRIES) {
const oldestKey = responseSessionMap.keys().next().value;
if (!oldestKey) {
return;
}
responseSessionMap.delete(oldestKey);
}
}
function storeResponseSession(
responseId: string,
sessionKey: string,
@@ -196,7 +187,7 @@ function storeResponseSession(
responseSessionMap.delete(responseId);
responseSessionMap.set(responseId, { ...scope, sessionKey, ts: now });
pruneExpiredResponseSessions(now);
evictOverflowResponseSessions();
pruneMapToMaxSize(responseSessionMap, MAX_RESPONSE_SESSION_ENTRIES);
}
function lookupResponseSession(
+2 -7
View File
@@ -2,6 +2,7 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { fileTypeFromBuffer } from "file-type";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { readRemoteMediaBuffer } from "../media/fetch.js";
import {
createImageProcessor,
@@ -130,13 +131,7 @@ function rememberIcon(
): PluginIconCacheEntry {
cache.delete(cacheKey);
cache.set(cacheKey, entry);
while (cache.size > PLUGIN_ICON_CACHE_MAX_ENTRIES) {
const oldest = cache.keys().next();
if (oldest.done) {
break;
}
cache.delete(oldest.value);
}
pruneMapToMaxSize(cache, PLUGIN_ICON_CACHE_MAX_ENTRIES);
return entry;
}
+2 -2
View File
@@ -479,7 +479,7 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
}
const { preserveRestartAttempts = false, preserveManualStop = false } = optsValue;
const cfg = getRuntimeConfig();
resetDirectoryCache({ channel: channelId, accountId });
resetDirectoryCache({ cfg, channel: channelId, accountId });
const store = getStore(channelId);
const accountIds = accountId
? [accountId]
@@ -795,7 +795,7 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
? setRuntimeFromTaskStatus(channelId, id, next, abort.signal)
: getRuntime(channelId, id),
invalidateDirectoryCache: () =>
resetDirectoryCache({ channel: channelId, accountId: id }),
resetDirectoryCache({ cfg, channel: channelId, accountId: id }),
...(channelRuntimeForTask ? { channelRuntime: channelRuntimeForTask } : {}),
}),
).finally(recordDuration);
+2 -11
View File
@@ -9,6 +9,7 @@ import {
} from "../agents/worktrees/service.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { sweepStaleRunContexts } from "../infra/agent-run-registry.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { pruneOrphanedDeliveryQueueMedia } from "../infra/outbound/delivery-queue-media-spool.js";
import { cleanOldMedia } from "../media/store.js";
import { createLazyPromiseLoader } from "../shared/lazy-promise.js";
@@ -239,17 +240,7 @@ export function startGatewayMaintenanceTimers(params: {
}
}
if (params.agentRunSeq.size > AGENT_RUN_SEQ_MAX) {
const excess = params.agentRunSeq.size - AGENT_RUN_SEQ_MAX;
let removed = 0;
for (const runId of params.agentRunSeq.keys()) {
params.agentRunSeq.delete(runId);
removed += 1;
if (removed >= excess) {
break;
}
}
}
pruneMapToMaxSize(params.agentRunSeq, AGENT_RUN_SEQ_MAX);
for (const [runId, entry] of params.chatAbortControllers) {
if (entry.projectSessionTerminalPending === true) {
@@ -6,6 +6,7 @@ import { hashRuntimeConfigValue } from "../../config/runtime-snapshot.js";
import type { SessionEntry } from "../../config/sessions.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import { normalizeAgentId } from "../../routing/session-key.js";
import type { GatewayModelCatalogSnapshot } from "../server-model-catalog.types.js";
import { listAgentsForGateway } from "../session-utils.js";
@@ -81,13 +82,7 @@ function setChatStartupMetadataMemo(
): void {
memo.metadataByKey.delete(key);
memo.metadataByKey.set(key, value);
if (memo.metadataByKey.size <= CHAT_STARTUP_METADATA_CACHE_MAX_ENTRIES) {
return;
}
const oldestKey = memo.metadataByKey.keys().next().value;
if (oldestKey) {
memo.metadataByKey.delete(oldestKey);
}
pruneMapToMaxSize(memo.metadataByKey, CHAT_STARTUP_METADATA_CACHE_MAX_ENTRIES);
}
function resolveChatStartupMetadataMemoKey(params: {
+2 -7
View File
@@ -6,6 +6,7 @@ import {
missingScopeErrorShape,
validateNodeInvokeParams,
} from "../../../packages/gateway-protocol/src/index.js";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import {
isAdminOnlyNodeInvokeCommand,
isBrowserProxyNodeInvokeCommand,
@@ -77,13 +78,7 @@ function emitTalkPttNodeEvent(params: {
const sessionId = `node:${params.nodeId}:talk:${captureId}`;
const seq = (talkPttEventSeqBySessionId.get(sessionId) ?? 0) + 1;
talkPttEventSeqBySessionId.set(sessionId, seq);
while (talkPttEventSeqBySessionId.size > 2048) {
const oldest = talkPttEventSeqBySessionId.keys().next().value;
if (oldest === undefined) {
break;
}
talkPttEventSeqBySessionId.delete(oldest);
}
pruneMapToMaxSize(talkPttEventSeqBySessionId, 2048);
const type =
params.command === "talk.ptt.start"
@@ -14,6 +14,7 @@ import {
validateSessionsCatalogReadParams,
} from "../../../packages/gateway-protocol/src/index.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import { getPluginRegistryRuntime } from "../../plugins/registry-runtime-binding.js";
import type { PluginRegistry } from "../../plugins/registry-types.js";
import { getActivePluginRegistry } from "../../plugins/runtime.js";
@@ -462,13 +463,7 @@ export const sessionCatalogHandlers: GatewayRequestHandlers = {
// out-of-phase clients but expires before the UI's 5s fast follow, so changed rows surface there.
// Expired and rejected work is removed; retaining it would mask provider recovery or new sessions.
cache.set(listKey, entry);
while (cache.size > SESSION_CATALOG_LIST_CACHE_MAX_ENTRIES) {
const oldest = cache.keys().next();
if (oldest.done) {
break;
}
cache.delete(oldest.value);
}
pruneMapToMaxSize(cache, SESSION_CATALOG_LIST_CACHE_MAX_ENTRIES);
try {
const result = await operation;
if (cache.get(listKey) === entry) {
@@ -1,3 +1,4 @@
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import { listSystemPresence } from "../../infra/system-presence.js";
import { resolveGlobalSingleton } from "../../shared/global-singleton.js";
@@ -154,8 +155,6 @@ export function updateTypingConnections(params: {
}
typingConnections.delete(params.key);
typingConnections.set(params.key, connections);
if (typingConnections.size > MAX_TYPING_THROTTLE_KEYS) {
typingConnections.delete(typingConnections.keys().next().value ?? "");
}
pruneMapToMaxSize(typingConnections, MAX_TYPING_THROTTLE_KEYS);
return true;
}
+2 -7
View File
@@ -22,6 +22,7 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/ag
import { resolveToCwd as resolveSessionToolPathToCwd } from "../../agents/sessions/tools/path-utils.js";
import { runGit } from "../../agents/worktrees/git.js";
import { FsSafeError } from "../../infra/fs-safe.js";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import { normalizeAgentId, parseAgentSessionKey } from "../../routing/session-key.js";
import {
readSessionTranscriptVisibleMessageDelta,
@@ -131,13 +132,7 @@ function readTouchedFilesCache(key: string): TouchedFilesCacheEntry | undefined
function writeTouchedFilesCache(key: string, entry: TouchedFilesCacheEntry): void {
touchedFilesCache.delete(key);
touchedFilesCache.set(key, entry);
while (touchedFilesCache.size > TOUCHED_FILES_CACHE_LIMIT) {
const oldestKey = touchedFilesCache.keys().next().value;
if (oldestKey === undefined) {
break;
}
touchedFilesCache.delete(oldestKey);
}
pruneMapToMaxSize(touchedFilesCache, TOUCHED_FILES_CACHE_LIMIT);
}
function sessionFilesError(type: string, message: string, details?: Record<string, unknown>) {
+3 -22
View File
@@ -16,6 +16,7 @@ import { buildRuntimeCompatibleMcpToolInventory } from "../../agents/tools-effec
import type { SessionToolOverrides } from "../../config/sessions/types.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { toErrorObject } from "../../infra/errors.js";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import { logDebug, logWarn } from "../../logger.js";
import { stringifyRouteThreadId } from "../../plugin-sdk/channel-route.js";
import { sessionDeliveryOrigin } from "../../utils/delivery-context.shared.js";
@@ -130,16 +131,6 @@ function buildToolsEffectiveCacheKey(params: {
});
}
function trimToolsEffectiveCache(): void {
while (toolsEffectiveCache.size > TOOLS_EFFECTIVE_CACHE_LIMIT) {
const oldest = toolsEffectiveCache.keys().next().value;
if (typeof oldest !== "string") {
return;
}
toolsEffectiveCache.delete(oldest);
}
}
function buildMcpConfigSummaryCacheKey(params: {
context: TrustedToolsEffectiveContext;
workspaceDir: string;
@@ -153,16 +144,6 @@ function buildMcpConfigSummaryCacheKey(params: {
});
}
function trimMcpConfigSummaryCache(): void {
while (mcpConfigSummaryCache.size > MCP_CONFIG_SUMMARY_CACHE_LIMIT) {
const oldest = mcpConfigSummaryCache.keys().next().value;
if (typeof oldest !== "string") {
return;
}
mcpConfigSummaryCache.delete(oldest);
}
}
function resolveCachedSessionMcpConfigSummary(params: {
context: TrustedToolsEffectiveContext;
workspaceDir: string;
@@ -178,14 +159,14 @@ function resolveCachedSessionMcpConfigSummary(params: {
...(params.context.toolOverrides ? { toolOverrides: params.context.toolOverrides } : {}),
});
mcpConfigSummaryCache.set(key, summary);
trimMcpConfigSummaryCache();
pruneMapToMaxSize(mcpConfigSummaryCache, MCP_CONFIG_SUMMARY_CACHE_LIMIT);
return summary;
}
function cacheToolsEffectiveResult(key: string, value: BaseToolsEffectiveResolution): void {
toolsEffectiveCache.delete(key);
toolsEffectiveCache.set(key, { value, createdAtMs: nowForToolsEffectiveCache() });
trimToolsEffectiveCache();
pruneMapToMaxSize(toolsEffectiveCache, TOOLS_EFFECTIVE_CACHE_LIMIT);
}
// Base inventory resolution is pure CPU work, but it can still fan through
+4 -21
View File
@@ -15,6 +15,7 @@ import {
resolveEventSessionRoutingPolicy,
scopedHeartbeatWakeOptionsForPolicy,
} from "../infra/event-session-routing.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import type { PromptImageOrderEntry } from "../media/prompt-image-order.js";
import { runWithGatewayIndependentRootWorkContinuation } from "../process/gateway-work-admission.js";
import { resolveAgentHarnessSessionContextError } from "../sessions/agent-harness-session-key.js";
@@ -172,13 +173,7 @@ function shouldDropDuplicateVoiceTranscript(params: {
break;
}
}
while (recentVoiceTranscripts.size > MAX_RECENT_VOICE_TRANSCRIPTS) {
const oldestKey = recentVoiceTranscripts.keys().next().value;
if (oldestKey === undefined) {
break;
}
recentVoiceTranscripts.delete(oldestKey);
}
pruneMapToMaxSize(recentVoiceTranscripts, MAX_RECENT_VOICE_TRANSCRIPTS);
}
return false;
@@ -328,13 +323,7 @@ function shouldDropDuplicateExecFinished(params: {
break;
}
}
while (recentExecFinishedRuns.size > MAX_RECENT_EXEC_FINISHED_RUNS) {
const oldestKey = recentExecFinishedRuns.keys().next().value;
if (oldestKey === undefined) {
break;
}
recentExecFinishedRuns.delete(oldestKey);
}
pruneMapToMaxSize(recentExecFinishedRuns, MAX_RECENT_EXEC_FINISHED_RUNS);
}
return false;
@@ -356,13 +345,7 @@ function pruneBoundedTimestampMap(
return;
}
}
while (map.size > params.maxEntries) {
const oldestKey = map.keys().next().value;
if (oldestKey === undefined) {
return;
}
map.delete(oldestKey);
}
pruneMapToMaxSize(map, params.maxEntries);
}
function compactExecEventOutput(raw: string) {
+2 -4
View File
@@ -9,6 +9,7 @@ import { getRuntimeConfig } from "../config/io.js";
import type { SessionEntry } from "../config/sessions.js";
import type { OpenClawConfig } from "../config/types.js";
import { getAgentRunContext } from "../infra/agent-run-registry.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import {
normalizeAgentId,
parseAgentSessionKey,
@@ -48,10 +49,7 @@ function setResolvedSessionKeyCache(
!resolvedSessionKeyByRunId.has(cacheKey) &&
resolvedSessionKeyByRunId.size >= RUN_LOOKUP_CACHE_LIMIT
) {
const oldest = resolvedSessionKeyByRunId.keys().next().value;
if (oldest) {
resolvedSessionKeyByRunId.delete(oldest);
}
pruneMapToMaxSize(resolvedSessionKeyByRunId, RUN_LOOKUP_CACHE_LIMIT - 1);
}
let expiresAt: number | null = null;
if (sessionKey === null) {
+2 -7
View File
@@ -1,6 +1,7 @@
// Hook request handler validates hook tokens, applies mappings, dedupes requests, and dispatches wake or agent work.
import { createHash } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import type { createSubsystemLogger } from "../../logging/subsystem.js";
import { resolveHookExternalContentSource as resolveHookExternalContentSourceFromSession } from "../../security/external-content.js";
import { safeEqualSecret } from "../../security/secret-equal.js";
@@ -121,13 +122,7 @@ export function createHooksRequestHandler(
hookReplayCache.delete(key);
}
}
while (hookReplayCache.size > DEDUPE_MAX) {
const oldestKey = hookReplayCache.keys().next().value;
if (!oldestKey) {
break;
}
hookReplayCache.delete(oldestKey);
}
pruneMapToMaxSize(hookReplayCache, DEDUPE_MAX);
};
const buildHookReplayCacheKey = (params: HookReplayScope): string | undefined => {
@@ -1,5 +1,6 @@
// Rate limiter for noisy websocket handshake auth logs.
import { resolveIntegerOption } from "@openclaw/normalization-core/number-coercion";
import { pruneMapToMaxSize } from "../../../infra/map-size.js";
/** Decision returned for a handshake auth log attempt. */
type HandshakeAuthLogDecision = {
@@ -27,7 +28,7 @@ export class HandshakeAuthLogLimiter {
register(key: string, nowMs = Date.now()): HandshakeAuthLogDecision {
const entry = this.entries.get(key);
if (!entry) {
this.pruneIfNeeded();
pruneMapToMaxSize(this.entries, this.maxEntries - 1);
this.entries.set(key, {
lastLoggedAtMs: nowMs,
suppressedSinceLastLog: 0,
@@ -45,16 +46,6 @@ export class HandshakeAuthLogLimiter {
entry.suppressedSinceLastLog = 0;
return { shouldLog: true, suppressedSinceLastLog };
}
private pruneIfNeeded(): void {
if (this.entries.size < this.maxEntries) {
return;
}
const oldestKey = this.entries.keys().next().value;
if (oldestKey !== undefined) {
this.entries.delete(oldestKey);
}
}
}
/** Build the limiter key from auth failure context. */
+3 -14
View File
@@ -22,6 +22,7 @@ import {
import type { SessionEntry } from "../config/sessions/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { AgentEventPayload } from "../infra/agent-events.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { redactToolPayloadText } from "../logging/redact.js";
import { normalizeAgentId } from "../routing/session-key.js";
import type {
@@ -102,13 +103,7 @@ export function rememberSessionObserverRevisionFloor(
floors.delete(sessionKey);
floors.set(sessionKey, candidate);
}
while (floors.size > MAX_REVISION_FLOORS) {
const oldest = floors.keys().next().value;
if (oldest === undefined) {
break;
}
floors.delete(oldest);
}
pruneMapToMaxSize(floors, MAX_REVISION_FLOORS);
}
export function rememberSessionObserverDormantRun(
@@ -159,13 +154,7 @@ export function markSessionObserverRunSuperseded(
): void {
runs.delete(runId);
runs.set(runId, observedAt);
while (runs.size > MAX_SUPERSEDED_RUNS) {
const oldest = runs.keys().next().value;
if (oldest === undefined) {
break;
}
runs.delete(oldest);
}
pruneMapToMaxSize(runs, MAX_SUPERSEDED_RUNS);
}
export function createDormantSessionObserverRun(
+2 -3
View File
@@ -19,6 +19,7 @@ import {
import { hasErrnoCode } from "../infra/errors.js";
import { readFileWindowFully } from "../infra/file-read.js";
import { resolveRequiredHomeDir } from "../infra/home-dir.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { emitSessionTranscriptUpdate } from "../sessions/transcript-events.js";
type ArchiveFileReason = SessionArchiveReason;
@@ -212,9 +213,7 @@ async function listResetArchiveCandidatesForTranscriptAsync(
dirSize: dirStat.size,
archives: boundedArchives,
});
if (resetArchiveDiscoveryCache.size > MAX_RESET_ARCHIVE_DISCOVERY_CACHE_ENTRIES) {
resetArchiveDiscoveryCache.delete(resetArchiveDiscoveryCache.keys().next().value ?? "");
}
pruneMapToMaxSize(resetArchiveDiscoveryCache, MAX_RESET_ARCHIVE_DISCOVERY_CACHE_ENTRIES);
return boundedArchives;
}
@@ -7,6 +7,7 @@ import {
type SessionTranscriptMessageEvent,
type SessionTranscriptReadScope,
} from "../config/sessions/session-accessor.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { hasInterSessionUserProvenance } from "../sessions/input-provenance.js";
import {
extractMessageRole,
@@ -44,13 +45,7 @@ function sqliteTitleFieldCacheKey(target: ResolvedTranscriptReadTarget): string
function setSqliteTitleFieldCache(key: string, entry: SqliteTitleFieldCacheEntry): void {
sqliteTitleFieldCache.delete(key);
sqliteTitleFieldCache.set(key, entry);
if (sqliteTitleFieldCache.size <= SQLITE_TITLE_FIELD_CACHE_MAX_ENTRIES) {
return;
}
const oldestKey = sqliteTitleFieldCache.keys().next().value;
if (oldestKey !== undefined) {
sqliteTitleFieldCache.delete(oldestKey);
}
pruneMapToMaxSize(sqliteTitleFieldCache, SQLITE_TITLE_FIELD_CACHE_MAX_ENTRIES);
}
function readSqliteTitleProbeRange(
+2 -7
View File
@@ -11,6 +11,7 @@ import {
import { stripInboundMetadata } from "../auto-reply/reply/strip-inbound-meta.js";
import { isTerminalSessionStatus, type SessionEntry } from "../config/sessions.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { resolveNonNegativeNumber } from "../shared/number-coercion.js";
import { truncateUtf16Safe } from "../utils.js";
import {
@@ -289,13 +290,7 @@ function rememberSingleRowChildSessionCandidateCacheEntry(
singleRowChildSessionCandidateCache.delete(storePath);
}
singleRowChildSessionCandidateCache.set(storePath, entry);
if (singleRowChildSessionCandidateCache.size <= SINGLE_ROW_CONTEXT_CACHE_MAX_ENTRIES) {
return;
}
const oldestKey = singleRowChildSessionCandidateCache.keys().next().value;
if (oldestKey) {
singleRowChildSessionCandidateCache.delete(oldestKey);
}
pruneMapToMaxSize(singleRowChildSessionCandidateCache, SINGLE_ROW_CONTEXT_CACHE_MAX_ENTRIES);
}
function buildStoreChildSessionCandidateIndex(
+2 -3
View File
@@ -20,6 +20,7 @@ import { streamSessionTranscriptLines } from "../config/sessions/transcript-stre
import { selectSessionTranscriptActiveEntries } from "../config/sessions/transcript-tree.js";
import { readFileWindowFully } from "../infra/file-read.js";
import { jsonUtf8Bytes } from "../infra/json-utf8-bytes.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { extractAssistantVisibleText } from "../shared/chat-message-content.js";
import { truncateUtf16Safe } from "../utils.js";
import { estimateStringChars, estimateTokensFromChars } from "../utils/cjk-chars.js";
@@ -566,9 +567,7 @@ async function readSessionTranscriptIndex(
if (opts.cache !== "skip") {
transcriptIndexes.delete(filePath);
transcriptIndexes.set(filePath, cached);
if (transcriptIndexes.size > MAX_TRANSCRIPT_INDEXES) {
transcriptIndexes.delete(transcriptIndexes.keys().next().value ?? "");
}
pruneMapToMaxSize(transcriptIndexes, MAX_TRANSCRIPT_INDEXES);
}
}
let index: SessionTranscriptIndex;
+2 -7
View File
@@ -31,6 +31,7 @@ import {
requestDevicePairing,
verifyDeviceToken,
} from "../infra/device-pairing.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { captureAuthenticatedNodePairingState } from "../infra/node-pairing-state.js";
import {
approveNodePairing,
@@ -254,13 +255,7 @@ function createChallengeStore() {
challenges.delete(oldest[0]);
}
}
while (challenges.size >= MAX_PENDING_CHALLENGES) {
const oldest = challenges.keys().next().value;
if (typeof oldest !== "string") {
break;
}
challenges.delete(oldest);
}
pruneMapToMaxSize(challenges, MAX_PENDING_CHALLENGES - 1);
const nonce = randomBytes(24).toString("base64url");
const expiresAtMs = current + CHALLENGE_TTL_MS;
challenges.set(nonce, { clientKey, expiresAtMs });
@@ -18,6 +18,7 @@ import {
registerAgentRunContext,
releaseAgentRunContext,
} from "../../infra/agent-run-registry.js";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import type { WorkerConnectionIdentity } from "./connection-identity.js";
import {
createWorkerLiveTrajectoryRecorder,
@@ -722,12 +723,7 @@ export function createWorkerLiveEventReceiver(options: WorkerLiveEventReceiverOp
// Refresh recency first so a re-fenced environment keeps its newest stale-owner epoch.
fencedEnvironmentEpochs.delete(environmentId);
fencedEnvironmentEpochs.set(environmentId, fencedEpoch);
if (fencedEnvironmentEpochs.size > MAX_FENCED_ENVIRONMENTS) {
const oldestEnvironmentId = fencedEnvironmentEpochs.keys().next().value;
if (oldestEnvironmentId) {
fencedEnvironmentEpochs.delete(oldestEnvironmentId);
}
}
pruneMapToMaxSize(fencedEnvironmentEpochs, MAX_FENCED_ENVIRONMENTS);
}
};
+5 -14
View File
@@ -32,7 +32,10 @@ import {
import { removePreparedBackupArchive, writeArchiveStreamToFile } from "./backup-create-stream.js";
import { writeTarArchiveWithRetry } from "./backup-tar-retry.js";
import { isVolatileBackupPath } from "./backup-volatile-filter.js";
import { createBackupVolatileStatCache } from "./backup-volatile-stat-cache.js";
import {
createBackupLinkCache,
createBackupVolatileStatCache,
} from "./backup-volatile-stat-cache.js";
import { formatErrorMessage } from "./errors.js";
import { sameFileIdentity } from "./fs-safe-advanced.js";
import { writeJson } from "./json-files.js";
@@ -48,18 +51,6 @@ import { withLegacyAuditMigrationLease } from "./state-migrations.audit-coordina
const loadTarRuntime = createLazyRuntimeModule(() => import("tar"));
type BackupLinkCacheKey = `${number}:${number}`;
class BackupLinkCache extends Map<BackupLinkCacheKey, string> {
override get(_key: BackupLinkCacheKey): undefined {
return undefined;
}
override set(_key: BackupLinkCacheKey, _value: string): this {
return this;
}
}
export type BackupCreateOptions = {
output?: string;
dryRun?: boolean;
@@ -945,7 +936,7 @@ export async function createBackupArchive(
gzip: true,
portable: true,
preservePaths: true,
linkCache: new BackupLinkCache(),
linkCache: createBackupLinkCache(),
statCache: createBackupVolatileStatCache(volatilePlan),
filter: tarFilter,
onWriteEntry: (entry) => {
+17
View File
@@ -2,6 +2,7 @@ import type { Stats } from "node:fs";
import { isVolatileBackupPath } from "./backup-volatile-filter.js";
type VolatileFilterPlan = Parameters<typeof isVolatileBackupPath>[1];
type BackupLinkCacheKey = `${number}:${number}`;
const VOLATILE_BACKUP_SYNTHETIC_STAT = {
isBlockDevice: () => false,
@@ -31,8 +32,24 @@ class BackupVolatileStatCache extends Map<string, Stats> {
}
}
// node-tar emits hardlink entries when this cache returns an earlier inode path.
// Suppressing both reads and writes keeps every backup entry independently restorable.
class BackupLinkCache extends Map<BackupLinkCacheKey, string> {
override get(_key: BackupLinkCacheKey): undefined {
return undefined;
}
override set(_key: BackupLinkCacheKey, _value: string): this {
return this;
}
}
export function createBackupVolatileStatCache(
volatilePlan: VolatileFilterPlan,
): Map<string, Stats> {
return new BackupVolatileStatCache(volatilePlan);
}
export function createBackupLinkCache(): Map<BackupLinkCacheKey, string> {
return new BackupLinkCache();
}
+2 -6
View File
@@ -21,6 +21,7 @@ import {
signEd25519Payload,
verifyEd25519Signature,
} from "./ed25519-signature.js";
import { pruneMapToMaxSize } from "./map-size.js";
export type { DeviceIdentity } from "./device-identity-store.js";
@@ -157,12 +158,7 @@ export function loadOrCreateProcessDeviceIdentity(
return cached;
}
const identity = loadOrCreateDeviceIdentityOwned(resolvedOptions);
if (processDeviceIdentities.size >= MAX_PROCESS_DEVICE_IDENTITIES) {
const oldestKey = processDeviceIdentities.keys().next().value;
if (oldestKey !== undefined) {
processDeviceIdentities.delete(oldestKey);
}
}
pruneMapToMaxSize(processDeviceIdentities, MAX_PROCESS_DEVICE_IDENTITIES - 1);
processDeviceIdentities.set(cacheKey, identity);
return identity;
});
+2 -7
View File
@@ -3,6 +3,7 @@ import fs from "node:fs";
import path from "node:path";
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import { expandHomePrefix } from "./home-dir.js";
import { pruneMapToMaxSize } from "./map-size.js";
function isDriveLessWindowsRootedPath(value: string): boolean {
return process.platform === "win32" && /^:[\\/]/.test(value);
@@ -127,13 +128,7 @@ function cacheExecutablePath(key: string, resolved: string | undefined): void {
expiresAt: Date.now() + EXECUTABLE_PATH_CACHE_TTL_MS,
resolved: resolved ?? null,
});
while (executablePathCache.size > EXECUTABLE_PATH_CACHE_MAX_ENTRIES) {
const oldest = executablePathCache.keys().next();
if (oldest.done) {
break;
}
executablePathCache.delete(oldest.value);
}
pruneMapToMaxSize(executablePathCache, EXECUTABLE_PATH_CACHE_MAX_ENTRIES);
}
function executablePathCacheKey(
+2 -6
View File
@@ -2,6 +2,7 @@
import type { DatabaseSync, SQLInputValue, StatementSync } from "node:sqlite";
import type { Compilable, CompiledQuery, Kysely, QueryResult } from "kysely";
import { InsertQueryNode, Kysely as KyselyInstance, SqliteDialect } from "kysely";
import { pruneMapToMaxSize } from "./map-size.js";
// Sync query helpers execute compiled Kysely SQL against node:sqlite without
// going through Kysely's async driver path.
@@ -201,12 +202,7 @@ function executeWithCachedStatement<Result>(
statement = db.prepare(sql);
if (!cached && cache.candidates.delete(sql)) {
cache.statements.set(sql, statement);
if (cache.statements.size > statementCacheCapacity) {
const oldestSql = cache.statements.keys().next().value;
if (oldestSql !== undefined) {
cache.statements.delete(oldestSql);
}
}
pruneMapToMaxSize(cache.statements, statementCacheCapacity);
} else if (!cached) {
// Admit only on second use so variable placeholder counts cannot fill
// the native statement cache with one-shot SQL strings.
@@ -11,6 +11,7 @@ import type { PluginChannelRegistration } from "../../plugins/registry-types.js"
import type { PluginRegistry } from "../../plugins/registry.js";
import { getActivePluginRegistry, getActivePluginRegistryVersion } from "../../plugins/runtime.js";
import type { DeliverableMessageChannel } from "../../utils/message-channel.js";
import { pruneMapToMaxSize } from "../map-size.js";
const MAX_BOOTSTRAP_CONFIG_GENERATIONS = 64;
let bootstrapRegistryGeneration: string | undefined;
@@ -40,12 +41,7 @@ function resolveBootstrapRegistries(
}
// Agent-scoped configs may interleave within one registry generation. Keep a
// bounded LRU so one caller cannot evict another on every delivery attempt.
if (bootstrapRegistriesByConfig.size >= MAX_BOOTSTRAP_CONFIG_GENERATIONS) {
const oldestConfigKey = bootstrapRegistriesByConfig.keys().next().value;
if (oldestConfigKey !== undefined) {
bootstrapRegistriesByConfig.delete(oldestConfigKey);
}
}
pruneMapToMaxSize(bootstrapRegistriesByConfig, MAX_BOOTSTRAP_CONFIG_GENERATIONS - 1);
const registries = new Map<DeliverableMessageChannel, PluginRegistry | null>();
bootstrapRegistriesByConfig.set(configKey, registries);
return registries;
+12 -6
View File
@@ -1,6 +1,6 @@
// Covers directory cache key dimensions, TTL expiration, config invalidation,
// recency refresh, bounded eviction, and matching clears.
import { describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import { DirectoryCache, buildDirectoryCacheKey } from "./directory-cache.js";
@@ -34,7 +34,11 @@ describe("buildDirectoryCacheKey", () => {
});
describe("DirectoryCache", () => {
it("expires entries after ttl and resets when config ref changes", () => {
afterEach(() => {
vi.useRealTimers();
});
it("expires entries at ttl and partitions entries by config identity", () => {
vi.useFakeTimers();
const cache = new DirectoryCache<string>(1_000);
const cfgA = {} as OpenClawConfig;
@@ -43,18 +47,18 @@ describe("DirectoryCache", () => {
cache.set("a", "first", cfgA);
expect(cache.get("a", cfgA)).toBe("first");
vi.advanceTimersByTime(1_001);
vi.advanceTimersByTime(1_000);
expect(cache.get("a", cfgA)).toBeUndefined();
cache.set("b", "second", cfgA);
expect(cache.get("b", cfgB)).toBeUndefined();
vi.useRealTimers();
expect(cache.get("b", cfgA)).toBe("second");
});
it("evicts least-recent entries, refreshes insertion order, and clears matches", () => {
const cache = new DirectoryCache<string>(60_000, 2);
const cfg = {} as OpenClawConfig;
const otherCfg = {} as OpenClawConfig;
cache.set("a", "A", cfg);
cache.set("b", "B", cfg);
@@ -65,8 +69,10 @@ describe("DirectoryCache", () => {
expect(cache.get("b", cfg)).toBeUndefined();
expect(cache.get("c", cfg)).toBe("C");
cache.clearMatching((key) => key.startsWith("c"));
cache.set("c", "other-C", otherCfg);
cache.clearMatching((key) => key.startsWith("c"), cfg);
expect(cache.get("c", cfg)).toBeUndefined();
expect(cache.get("c", otherCfg)).toBe("other-C");
cache.clear(cfg);
expect(cache.get("a", cfg)).toBeUndefined();
+33 -40
View File
@@ -1,8 +1,8 @@
// Directory cache stores short-lived channel directory lookups and invalidates
// them on config-object changes or resolver signature updates.
// Directory cache stores short-lived projections partitioned by config identity.
import { resolveNonNegativeIntegerOption } from "@openclaw/normalization-core/number-coercion";
import type { ChannelDirectoryEntryKind, ChannelId } from "../../channels/plugins/types.public.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { pruneMapToMaxSize } from "../map-size.js";
type CacheEntry<T> = {
value: T;
@@ -33,8 +33,7 @@ export function buildDirectoryCacheKey(key: DirectoryCacheKey): string {
* Small TTL cache for channel directory lookups tied to a config object reference.
*/
export class DirectoryCache<T> {
private readonly cache = new Map<string, CacheEntry<T>>();
private lastConfigRef: OpenClawConfig | null = null;
private cachesByConfig = new WeakMap<OpenClawConfig, Map<string, CacheEntry<T>>>();
private readonly ttlMs: number;
private readonly maxSize: number;
@@ -44,12 +43,12 @@ export class DirectoryCache<T> {
}
/**
* Returns a cached value after applying config, TTL, and capacity invalidation.
* Returns a cached value after applying config scoping, TTL, and capacity invalidation.
*/
get(key: string, cfg: OpenClawConfig): T | undefined {
this.resetIfConfigChanged(cfg);
this.pruneExpired(Date.now());
const entry = this.cache.get(key);
const cache = this.cacheForConfig(cfg);
this.pruneExpired(cache, Date.now());
const entry = cache.get(key);
if (!entry) {
return undefined;
}
@@ -60,64 +59,58 @@ export class DirectoryCache<T> {
* Stores a value and refreshes its recency for bounded-size eviction.
*/
set(key: string, value: T, cfg: OpenClawConfig): void {
this.resetIfConfigChanged(cfg);
const cache = this.cacheForConfig(cfg);
const now = Date.now();
this.pruneExpired(now);
this.pruneExpired(cache, now);
// Refresh insertion order so active keys are less likely to be evicted.
if (this.cache.has(key)) {
this.cache.delete(key);
}
this.cache.set(key, { value, fetchedAt: now });
this.evictToMaxSize();
cache.delete(key);
cache.set(key, { value, fetchedAt: now });
pruneMapToMaxSize(cache, this.maxSize);
}
/**
* Clears matching entries without disturbing unrelated cached lookups.
*/
clearMatching(match: (key: string) => boolean): void {
for (const key of this.cache.keys()) {
clearMatching(match: (key: string) => boolean, cfg: OpenClawConfig): void {
const cache = this.cachesByConfig.get(cfg);
if (!cache) {
return;
}
for (const key of cache.keys()) {
if (match(key)) {
this.cache.delete(key);
cache.delete(key);
}
}
}
/**
* Drops all cached entries and optionally adopts the current config reference.
* Drops one config scope or all cached entries.
*/
clear(cfg?: OpenClawConfig): void {
this.cache.clear();
if (cfg) {
this.lastConfigRef = cfg;
this.cachesByConfig.delete(cfg);
} else {
this.cachesByConfig = new WeakMap();
}
}
private resetIfConfigChanged(cfg: OpenClawConfig): void {
// Directory availability can change with config snapshots; ref changes must not leak stale entries.
if (this.lastConfigRef && this.lastConfigRef !== cfg) {
this.cache.clear();
private cacheForConfig(cfg: OpenClawConfig): Map<string, CacheEntry<T>> {
let cache = this.cachesByConfig.get(cfg);
if (!cache) {
cache = new Map();
this.cachesByConfig.set(cfg, cache);
}
this.lastConfigRef = cfg;
return cache;
}
private pruneExpired(now: number): void {
private pruneExpired(cache: Map<string, CacheEntry<T>>, now: number): void {
if (this.ttlMs <= 0) {
return;
}
for (const [cacheKey, entry] of this.cache.entries()) {
if (now - entry.fetchedAt > this.ttlMs) {
this.cache.delete(cacheKey);
for (const [cacheKey, entry] of cache) {
if (now - entry.fetchedAt >= this.ttlMs) {
cache.delete(cacheKey);
}
}
}
private evictToMaxSize(): void {
while (this.cache.size > this.maxSize) {
const oldestKey = this.cache.keys().next().value;
if (typeof oldestKey !== "string") {
break;
}
this.cache.delete(oldestKey);
}
}
}
+7 -3
View File
@@ -74,8 +74,12 @@ const CACHE_TTL_MS = 30 * 60 * 1000;
const directoryCache = new DirectoryCache<ChannelDirectoryEntry[]>(CACHE_TTL_MS);
/** Clears cached directory entries for all channels or one channel/account scope. */
export function resetDirectoryCache(params?: { channel?: ChannelId; accountId?: string | null }) {
if (!params?.channel) {
export function resetDirectoryCache(params?: {
cfg: OpenClawConfig;
channel: ChannelId;
accountId?: string | null;
}) {
if (!params) {
directoryCache.clear();
return;
}
@@ -89,7 +93,7 @@ export function resetDirectoryCache(params?: { channel?: ChannelId; accountId?:
return true;
}
return key.startsWith(`${channelKey}:${accountKey}:`);
});
}, params.cfg);
}
function normalizeQuery(value: string): string {
+2 -6
View File
@@ -1,3 +1,4 @@
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { escapeRegExp } from "../shared/regexp.js";
const MIN_SECRET_VALUE_LENGTH = 6;
@@ -18,12 +19,7 @@ function registerOneSecretValue(value: string): void {
return;
}
registeredValues.set(value, true);
if (registeredValues.size > MAX_SECRET_VALUES) {
const oldest = registeredValues.keys().next().value;
if (oldest !== undefined) {
registeredValues.delete(oldest);
}
}
pruneMapToMaxSize(registeredValues, MAX_SECRET_VALUES);
rebuildProbe();
}
+2 -4
View File
@@ -5,6 +5,7 @@ import { uniqueStrings } from "@openclaw/normalization-core/string-normalization
import { providerSupportsCapability } from "../../packages/media-understanding-common/src/provider-supports.js";
import { resolveRuntimeConfigCacheKey } from "../config/runtime-snapshot.js";
import type { OpenClawConfig } from "../config/types.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { buildMediaUnderstandingManifestMetadataRegistry } from "./manifest-metadata.js";
import {
normalizeMediaExecutionProviderId,
@@ -37,10 +38,7 @@ function cacheConfigRegistry(
!configRegistryCache.has(key) &&
configRegistryCache.size >= MAX_CONFIG_REGISTRY_CACHE_ENTRIES
) {
const oldestKey = configRegistryCache.keys().next().value;
if (oldestKey) {
configRegistryCache.delete(oldestKey);
}
pruneMapToMaxSize(configRegistryCache, MAX_CONFIG_REGISTRY_CACHE_ENTRIES - 1);
}
configRegistryCache.set(key, registry);
return registry;
+7 -14
View File
@@ -6,6 +6,7 @@ import { maxBytesForKind, type MediaKind } from "@openclaw/media-core/constants"
import { extensionForMime, normalizeMimeType } from "@openclaw/media-core/mime";
import { fileStore } from "../infra/file-store.js";
import { openLocalFileSafely } from "../infra/fs-safe.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { withTempWorkspace } from "../infra/private-temp-workspace.js";
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
import { getOrCreatePromise } from "../shared/lazy-promise.js";
@@ -223,18 +224,6 @@ function playbackSourceIdentityMatches(
);
}
function setBoundedMapEntry<K, V>(map: Map<K, V>, key: K, value: V, maxEntries: number): void {
map.delete(key);
while (map.size >= maxEntries) {
const oldestKey = map.keys().next().value as K | undefined;
if (oldestKey === undefined) {
break;
}
map.delete(oldestKey);
}
map.set(key, value);
}
function readPlaybackInspection(cacheKey: string): PlaybackInspection | undefined {
const inspection = playbackInspections.get(cacheKey);
if (inspection) {
@@ -244,7 +233,9 @@ function readPlaybackInspection(cacheKey: string): PlaybackInspection | undefine
}
function cachePlaybackInspection(cacheKey: string, inspection: PlaybackInspection): void {
setBoundedMapEntry(playbackInspections, cacheKey, inspection, MAX_PLAYBACK_ENTRIES.inspections);
playbackInspections.delete(cacheKey);
playbackInspections.set(cacheKey, inspection);
pruneMapToMaxSize(playbackInspections, MAX_PLAYBACK_ENTRIES.inspections);
}
function playbackInspectionCacheKey(params: {
@@ -697,7 +688,9 @@ export async function resolvePlaybackTranscode(
},
() => {
playbackJobs.delete(operationKey);
setBoundedMapEntry(playbackFailures, operationKey, Date.now(), MAX_PLAYBACK_ENTRIES.failures);
playbackFailures.delete(operationKey);
playbackFailures.set(operationKey, Date.now());
pruneMapToMaxSize(playbackFailures, MAX_PLAYBACK_ENTRIES.failures);
},
);
return { kind: "preparing" };
+2 -7
View File
@@ -8,6 +8,7 @@ import {
type ExecApprovalPendingReplyParams,
type ExecApprovalReplyDecision,
} from "../infra/exec-approval-reply.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import type { PluginApprovalRequest } from "../infra/plugin-approvals.js";
/**
* @deprecated Compatibility subpath for shipped approval reaction helpers.
@@ -590,13 +591,7 @@ export function createApprovalReactionTargetStore<TTarget>(params: {
memory.delete(key);
}
}
while (memory.size > params.maxEntries) {
const oldestKey = memory.keys().next().value;
if (!oldestKey) {
return;
}
memory.delete(oldestKey);
}
pruneMapToMaxSize(memory, params.maxEntries);
};
return {
+2 -6
View File
@@ -18,6 +18,7 @@ import { normalizeConfiguredProviderCatalogModelId } from "../agents/model-ref-s
import { resolveProviderRequestCapabilities } from "../agents/provider-attribution.js";
import type { ModelDefinitionConfig } from "../config/types.models.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import type { ProviderPlugin } from "../plugins/types.js";
import type { ModelProviderConfig } from "./provider-model-shared.js";
@@ -89,12 +90,7 @@ export async function getCachedLiveCatalogValue<T>(params: {
if (expiresAt !== undefined) {
// Auth-scoped live provider catalogs can vary by token; keep this
// process-local cache bounded so discovery cannot grow without limit.
if (liveCatalogCache.size >= LIVE_CATALOG_CACHE_MAX_ENTRIES) {
const oldestKey = liveCatalogCache.keys().next();
if (!oldestKey.done) {
liveCatalogCache.delete(oldestKey.value);
}
}
pruneMapToMaxSize(liveCatalogCache, LIVE_CATALOG_CACHE_MAX_ENTRIES - 1);
liveCatalogCache.set(key, {
expiresAt,
value,
+2 -11
View File
@@ -10,6 +10,7 @@ import {
} from "../channels/plugins/setup-contract.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { tryReadJsonSync } from "../infra/json-files.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import type { PluginCandidate } from "./discovery.js";
import { hashJson } from "./installed-plugin-index-hash.js";
import type { InstalledPluginIndex, InstalledPluginIndexRecord } from "./installed-plugin-index.js";
@@ -104,21 +105,11 @@ function rememberInstalledPackageMetadata(
): InstalledPackageMetadata {
if (key) {
installedPackageMetadataCache.set(key, metadata);
trimBoundedCache(installedPackageMetadataCache, MAX_INSTALLED_PACKAGE_METADATA_CACHE_ENTRIES);
pruneMapToMaxSize(installedPackageMetadataCache, MAX_INSTALLED_PACKAGE_METADATA_CACHE_ENTRIES);
}
return metadata;
}
function trimBoundedCache<Value>(cache: Map<string, Value>, maxEntries: number): void {
while (cache.size > maxEntries) {
const oldest = cache.keys().next().value;
if (oldest === undefined) {
break;
}
cache.delete(oldest);
}
}
function buildInstalledPackageMetadataCacheKey(
record: InstalledPluginIndexRecord,
): string | undefined {
+3 -12
View File
@@ -1,5 +1,6 @@
// Defines lifecycle-owned cache primitives for plugin metadata.
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
/** Result shape for cache lookups that need to distinguish a miss from cached `undefined`. */
type PluginLruCacheResult<T> = { hit: true; value: T } | { hit: false };
@@ -28,7 +29,7 @@ export class PluginLruCache<T> {
typeof value === "number"
? normalizeMaxEntries(value, this.#defaultMaxEntries)
: this.#defaultMaxEntries;
this.#evictOldestEntries();
pruneMapToMaxSize(this.#entries, this.#maxEntries);
}
clear(): void {
@@ -58,17 +59,7 @@ export class PluginLruCache<T> {
this.#entries.delete(cacheKey);
}
this.#entries.set(cacheKey, value);
this.#evictOldestEntries();
}
#evictOldestEntries(): void {
while (this.#entries.size > this.#maxEntries) {
const oldestEntry = this.#entries.keys().next();
if (oldestEntry.done) {
break;
}
this.#entries.delete(oldestEntry.value);
}
pruneMapToMaxSize(this.#entries, this.#maxEntries);
}
}
+5 -4
View File
@@ -1,6 +1,7 @@
/** Caches plugin tool descriptors by plugin source, contract names, and runtime context. */
import type { AnyAgentTool } from "../agents/tools/common.js";
import { resolveRuntimeConfigCacheKey } from "../config/runtime-snapshot.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import type { JsonObject, ToolDescriptor } from "../tools/types.js";
import type { PluginLoadOptions } from "./loader.js";
import { registerPluginMetadataProcessMemoLifecycleClear } from "./plugin-metadata-lifecycle.js";
@@ -186,10 +187,10 @@ export function writeCachedPluginToolDescriptors(params: {
!pluginToolDescriptorCacheState.descriptors.has(params.cacheKey) &&
pluginToolDescriptorCacheState.descriptors.size >= PLUGIN_TOOL_DESCRIPTOR_CACHE_LIMIT
) {
const oldestKey = pluginToolDescriptorCacheState.descriptors.keys().next().value;
if (oldestKey !== undefined) {
pluginToolDescriptorCacheState.descriptors.delete(oldestKey);
}
pruneMapToMaxSize(
pluginToolDescriptorCacheState.descriptors,
PLUGIN_TOOL_DESCRIPTOR_CACHE_LIMIT - 1,
);
}
pluginToolDescriptorCacheState.descriptors.set(params.cacheKey, [...params.descriptors]);
}
+2 -7
View File
@@ -1,5 +1,6 @@
// Routing account id helpers normalize account identifiers for route matching.
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { isBlockedObjectKey } from "../infra/prototype-keys.js";
export const DEFAULT_ACCOUNT_ID = "default";
@@ -66,13 +67,7 @@ export function normalizeOptionalAccountId(value: string | undefined | null): st
function setNormalizeCache<T>(cache: Map<string, T>, key: string, value: T): void {
cache.set(key, value);
if (cache.size <= ACCOUNT_ID_CACHE_MAX) {
return;
}
// Bounded FIFO-ish cache avoids unbounded growth from user/channel input
// while keeping hot account ids cheap during routing.
const oldest = cache.keys().next();
if (!oldest.done) {
cache.delete(oldest.value);
}
pruneMapToMaxSize(cache, ACCOUNT_ID_CACHE_MAX);
}
+2 -6
View File
@@ -1,5 +1,6 @@
// Performs lightweight safe-regex checks for user-supplied patterns.
import { expectDefined } from "@openclaw/normalization-core";
import { pruneMapToMaxSize } from "../infra/map-size.js";
type QuantifierRead = {
consumed: number;
minRepeat: number;
@@ -353,12 +354,7 @@ export function compileSafeRegexDetailed(source: string, flags = ""): SafeRegexC
}
safeRegexCache.set(cacheKey, result);
if (safeRegexCache.size > SAFE_REGEX_CACHE_MAX) {
const oldestKey = safeRegexCache.keys().next().value;
if (oldestKey) {
safeRegexCache.delete(oldestKey);
}
}
pruneMapToMaxSize(safeRegexCache, SAFE_REGEX_CACHE_MAX);
return result;
}
+2 -7
View File
@@ -4,6 +4,7 @@ import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "@openclaw/normalization-core/string-coerce";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import { escapeRegExp } from "../shared/regexp.js";
export type ParsedAgentSessionKey = {
@@ -142,13 +143,7 @@ function writeNormalizedSessionKeyCache(raw: string, normalized: string): void {
return;
}
normalizedSessionKeyCache.set(raw, normalized);
while (normalizedSessionKeyCache.size > NORMALIZED_SESSION_KEY_CACHE_MAX_ENTRIES) {
const oldest = normalizedSessionKeyCache.keys().next().value;
if (oldest === undefined) {
return;
}
normalizedSessionKeyCache.delete(oldest);
}
pruneMapToMaxSize(normalizedSessionKeyCache, NORMALIZED_SESSION_KEY_CACHE_MAX_ENTRIES);
}
function mayContainCasePreservingPeer(raw: string): boolean {
+2 -6
View File
@@ -3,6 +3,7 @@ import crypto from "node:crypto";
import { stableStringify } from "@openclaw/normalization-core";
import { redactConfigObject } from "../../config/redact-snapshot.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import { matchesSkillFilter } from "../discovery/filter.js";
import { buildWorkspaceSkillSnapshot } from "../loading/workspace.js";
import { WORKSPACE_SKILLS_PROMPT_FORMAT_VERSION } from "../types.js";
@@ -45,12 +46,7 @@ function fingerprintSkillSnapshotConfig(config: OpenClawConfig): string {
function cacheResolvedSkills(cacheKey: string, snapshot: SkillSnapshot): SkillSnapshot {
resolvedSkillsCache.set(cacheKey, snapshot.resolvedSkills);
if (resolvedSkillsCache.size > RESOLVED_SKILLS_CACHE_MAX) {
const oldest = resolvedSkillsCache.keys().next().value;
if (oldest !== undefined) {
resolvedSkillsCache.delete(oldest);
}
}
pruneMapToMaxSize(resolvedSkillsCache, RESOLVED_SKILLS_CACHE_MAX);
return snapshot;
}
+3 -12
View File
@@ -4,6 +4,7 @@ import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import { hasErrnoCode } from "../../infra/errors.js";
import { pruneMapToMaxSize } from "../../infra/map-size.js";
import { isPathInside } from "../../security/scan-paths.js";
import { formatScanEvidence, LITERAL_SECRET_SKILL_CONTENT_RULE } from "./scan-evidence.js";
@@ -114,22 +115,12 @@ function getCachedFileScanResult(params: {
}
function setCachedFileScanResult(filePath: string, entry: FileScanCacheEntry): void {
if (FILE_SCAN_CACHE.size >= FILE_SCAN_CACHE_MAX) {
const oldest = FILE_SCAN_CACHE.keys().next();
if (!oldest.done) {
FILE_SCAN_CACHE.delete(oldest.value);
}
}
pruneMapToMaxSize(FILE_SCAN_CACHE, FILE_SCAN_CACHE_MAX - 1);
FILE_SCAN_CACHE.set(filePath, entry);
}
function setCachedDirEntries(dirPath: string, entry: DirEntryCacheEntry): void {
if (DIR_ENTRY_CACHE.size >= DIR_ENTRY_CACHE_MAX) {
const oldest = DIR_ENTRY_CACHE.keys().next();
if (!oldest.done) {
DIR_ENTRY_CACHE.delete(oldest.value);
}
}
pruneMapToMaxSize(DIR_ENTRY_CACHE, DIR_ENTRY_CACHE_MAX - 1);
DIR_ENTRY_CACHE.set(dirPath, entry);
}
+2 -6
View File
@@ -17,6 +17,7 @@ import { resolveStateDir } from "../config/paths.js";
import type { ModelProviderConfig } from "../config/types.models.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { tryReadJsonSync } from "../infra/json-files.js";
import { pruneMapToMaxSize } from "../infra/map-size.js";
import {
modelCatalogPricingFingerprint,
resolveCatalogModelPricing,
@@ -293,12 +294,7 @@ function loadModelsJsonCostIndex(options?: {
normalizedEntries: null,
rawEntries: null,
};
if (modelsJsonCostCacheByAgentDir.size >= MODELS_JSON_COST_CACHE_LIMIT) {
const oldestAgentDir = modelsJsonCostCacheByAgentDir.keys().next().value;
if (oldestAgentDir !== undefined) {
modelsJsonCostCacheByAgentDir.delete(oldestAgentDir);
}
}
pruneMapToMaxSize(modelsJsonCostCacheByAgentDir, MODELS_JSON_COST_CACHE_LIMIT - 1);
modelsJsonCostCacheByAgentDir.set(agentDir, modelsJsonCostCache);
}