perf(routing): cache route and mention regex resolution

This commit is contained in:
Peter Steinberger
2026-03-02 23:00:28 +00:00
parent a81704e622
commit ba5ae5b4f1
2 changed files with 93 additions and 12 deletions

View File

@@ -21,6 +21,8 @@ function deriveMentionPatterns(identity?: { name?: string; emoji?: string }) {
}
const BACKSPACE_CHAR = "\u0008";
const mentionRegexCompileCache = new Map<string, RegExp[]>();
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 {

View File

@@ -204,6 +204,16 @@ type EvaluatedBindingsCache = {
const evaluatedBindingsCacheByCfg = new WeakMap<OpenClawConfig, EvaluatedBindingsCache>();
const MAX_EVALUATED_BINDINGS_CACHE_KEYS = 2000;
const resolvedRouteCacheByCfg = new WeakMap<
OpenClawConfig,
{
bindingsRef: OpenClawConfig["bindings"];
agentsRef: OpenClawConfig["agents"];
sessionRef: OpenClawConfig["session"];
byKey: Map<string, ResolvedAgentRoute>;
}
>();
const MAX_RESOLVED_ROUTE_CACHE_KEYS = 4000;
type EvaluatedBindingsIndex = {
byPeer: Map<string, EvaluatedBinding[]>;
@@ -411,6 +421,33 @@ function normalizeBindingMatch(
};
}
function resolveRouteCacheForConfig(cfg: OpenClawConfig): Map<string, ResolvedAgentRoute> {
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<string, ResolvedAgentRoute>();
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,