mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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:
committed by
GitHub
parent
4f9af40078
commit
d347cd1097
@@ -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,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 & {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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): {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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 +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,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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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 +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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user