diff --git a/src/auto-reply/reply/mentions.ts b/src/auto-reply/reply/mentions.ts index 4c8aca16998..ca20905efae 100644 --- a/src/auto-reply/reply/mentions.ts +++ b/src/auto-reply/reply/mentions.ts @@ -21,6 +21,8 @@ function deriveMentionPatterns(identity?: { name?: string; emoji?: string }) { } const BACKSPACE_CHAR = "\u0008"; +const mentionRegexCompileCache = new Map(); +const MAX_MENTION_REGEX_COMPILE_CACHE_KEYS = 512; export const CURRENT_MESSAGE_MARKER = "[Current message - respond to this]"; @@ -54,7 +56,15 @@ function resolveMentionPatterns(cfg: OpenClawConfig | undefined, agentId?: strin export function buildMentionRegexes(cfg: OpenClawConfig | undefined, agentId?: string): RegExp[] { const patterns = normalizeMentionPatterns(resolveMentionPatterns(cfg, agentId)); - return patterns + if (patterns.length === 0) { + return []; + } + const cacheKey = patterns.join("\u001f"); + const cached = mentionRegexCompileCache.get(cacheKey); + if (cached) { + return [...cached]; + } + const compiled = patterns .map((pattern) => { try { return new RegExp(pattern, "i"); @@ -63,6 +73,12 @@ export function buildMentionRegexes(cfg: OpenClawConfig | undefined, agentId?: s } }) .filter((value): value is RegExp => Boolean(value)); + mentionRegexCompileCache.set(cacheKey, compiled); + if (mentionRegexCompileCache.size > MAX_MENTION_REGEX_COMPILE_CACHE_KEYS) { + mentionRegexCompileCache.clear(); + mentionRegexCompileCache.set(cacheKey, compiled); + } + return [...compiled]; } export function normalizeMentionText(text: string): string { diff --git a/src/routing/resolve-route.ts b/src/routing/resolve-route.ts index 307315e6e18..e76be518419 100644 --- a/src/routing/resolve-route.ts +++ b/src/routing/resolve-route.ts @@ -204,6 +204,16 @@ type EvaluatedBindingsCache = { const evaluatedBindingsCacheByCfg = new WeakMap(); const MAX_EVALUATED_BINDINGS_CACHE_KEYS = 2000; +const resolvedRouteCacheByCfg = new WeakMap< + OpenClawConfig, + { + bindingsRef: OpenClawConfig["bindings"]; + agentsRef: OpenClawConfig["agents"]; + sessionRef: OpenClawConfig["session"]; + byKey: Map; + } +>(); +const MAX_RESOLVED_ROUTE_CACHE_KEYS = 4000; type EvaluatedBindingsIndex = { byPeer: Map; @@ -411,6 +421,33 @@ function normalizeBindingMatch( }; } +function resolveRouteCacheForConfig(cfg: OpenClawConfig): Map { + const existing = resolvedRouteCacheByCfg.get(cfg); + if ( + existing && + existing.bindingsRef === cfg.bindings && + existing.agentsRef === cfg.agents && + existing.sessionRef === cfg.session + ) { + return existing.byKey; + } + const byKey = new Map(); + resolvedRouteCacheByCfg.set(cfg, { + bindingsRef: cfg.bindings, + agentsRef: cfg.agents, + sessionRef: cfg.session, + byKey, + }); + return byKey; +} + +function formatRouteCachePeer(peer: RoutePeer | null): string { + if (!peer || !peer.id) { + return "-"; + } + return `${peer.kind}:${peer.id}`; +} + function hasGuildConstraint(match: NormalizedBindingMatch): boolean { return Boolean(match.guildId); } @@ -474,13 +511,40 @@ export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentR const teamId = normalizeId(input.teamId); const memberRoleIds = input.memberRoleIds ?? []; const memberRoleIdSet = new Set(memberRoleIds); + const dmScope = input.cfg.session?.dmScope ?? "main"; + const identityLinks = input.cfg.session?.identityLinks; + const shouldLogDebug = shouldLogVerbose(); + const parentPeer = input.parentPeer + ? { + kind: normalizeChatType(input.parentPeer.kind) ?? input.parentPeer.kind, + id: normalizeId(input.parentPeer.id), + } + : null; + + const routeCache = + !shouldLogDebug && !identityLinks ? resolveRouteCacheForConfig(input.cfg) : null; + const routeCacheKey = routeCache + ? [ + channel, + accountId, + formatRouteCachePeer(peer), + formatRouteCachePeer(parentPeer), + guildId || "-", + teamId || "-", + memberRoleIds.length > 0 ? memberRoleIds.toSorted().join(",") : "-", + dmScope, + ].join("\t") + : ""; + if (routeCache && routeCacheKey) { + const cachedRoute = routeCache.get(routeCacheKey); + if (cachedRoute) { + return { ...cachedRoute }; + } + } const bindings = getEvaluatedBindingsForChannelAccount(input.cfg, channel, accountId); const bindingsIndex = getEvaluatedBindingIndexForChannelAccount(input.cfg, channel, accountId); - const dmScope = input.cfg.session?.dmScope ?? "main"; - const identityLinks = input.cfg.session?.identityLinks; - const choose = (agentId: string, matchedBy: ResolvedAgentRoute["matchedBy"]) => { const resolvedAgentId = pickFirstExistingAgentId(input.cfg, agentId); const sessionKey = buildAgentSessionKey({ @@ -495,7 +559,7 @@ export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentR agentId: resolvedAgentId, mainKey: DEFAULT_MAIN_KEY, }).toLowerCase(); - return { + const route = { agentId: resolvedAgentId, channel, accountId, @@ -503,9 +567,16 @@ export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentR mainSessionKey, matchedBy, }; + if (routeCache && routeCacheKey) { + routeCache.set(routeCacheKey, route); + if (routeCache.size > MAX_RESOLVED_ROUTE_CACHE_KEYS) { + routeCache.clear(); + routeCache.set(routeCacheKey, route); + } + } + return route; }; - const shouldLogDebug = shouldLogVerbose(); const formatPeer = (value?: RoutePeer | null) => value?.kind && value?.id ? `${value.kind}:${value.id}` : "none"; const formatNormalizedPeer = (value: NormalizedPeerConstraint) => { @@ -529,12 +600,6 @@ export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentR } } // Thread parent inheritance: if peer (thread) didn't match, check parent peer binding - const parentPeer = input.parentPeer - ? { - kind: normalizeChatType(input.parentPeer.kind) ?? input.parentPeer.kind, - id: normalizeId(input.parentPeer.id), - } - : null; const baseScope = { guildId, teamId,