From 7eaabc0b3bd36edd1c7fa8ef9a880411f417bbf4 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 10:47:13 -0700 Subject: [PATCH] perf(slack): trim thread context allocation (cherry picked from commit 0caa419f76bdc3bd5eefc24edef1422da78a7d74) --- .../message-handler/prepare-content.ts | 78 ++++++++++----- .../message-handler/prepare-thread-context.ts | 97 +++++++++++++------ extensions/slack/src/monitor/thread.ts | 6 +- 3 files changed, 120 insertions(+), 61 deletions(-) diff --git a/extensions/slack/src/monitor/message-handler/prepare-content.ts b/extensions/slack/src/monitor/message-handler/prepare-content.ts index 52adc6017e3..36f0dc36698 100644 --- a/extensions/slack/src/monitor/message-handler/prepare-content.ts +++ b/extensions/slack/src/monitor/message-handler/prepare-content.ts @@ -13,6 +13,7 @@ type SlackResolvedMessageContent = { const SLACK_MENTION_RESOLUTION_CONCURRENCY = 4; const SLACK_MENTION_RESOLUTION_MAX_LOOKUPS_PER_MESSAGE = 20; +const SLACK_USER_MENTION_RE = /<@([A-Z0-9]+)(?:\|[^>]+)?>/gi; type SlackTextObject = { text?: unknown; @@ -54,7 +55,8 @@ function collectUniqueSlackMentionIds(texts: Array): string[ if (!text) { continue; } - for (const match of text.matchAll(/<@([A-Z0-9]+)(?:\|[^>]+)?>/gi)) { + SLACK_USER_MENTION_RE.lastIndex = 0; + for (const match of text.matchAll(SLACK_USER_MENTION_RE)) { const userId = match[1]; if (!userId || seen.has(userId)) { continue; @@ -73,7 +75,8 @@ function renderSlackUserMentions( if (!text || renderedMentions.size === 0) { return text; } - return text.replace(/<@([A-Z0-9]+)(?:\|[^>]+)?>/gi, (full, userId: string) => { + SLACK_USER_MENTION_RE.lastIndex = 0; + return text.replace(SLACK_USER_MENTION_RE, (full, userId: string) => { const rendered = renderedMentions.get(userId); return rendered ?? full; }); @@ -139,16 +142,19 @@ function renderSlackRichTextElements(elements: unknown): string { break; } case "rich_text_list": { - const listText = Array.isArray(element.elements) - ? element.elements - .map((child) => - child && typeof child === "object" - ? renderSlackRichTextElements((child as SlackRichTextElement).elements) - : "", - ) - .filter(Boolean) - .join("\n") - : ""; + const listParts: string[] = []; + if (Array.isArray(element.elements)) { + for (const child of element.elements) { + if (!child || typeof child !== "object") { + continue; + } + const rendered = renderSlackRichTextElements((child as SlackRichTextElement).elements); + if (rendered) { + listParts.push(rendered); + } + } + } + const listText = listParts.join("\n"); parts.push(listText); break; } @@ -174,7 +180,13 @@ function readSlackBlockText(block: unknown): string | undefined { return text; } if (Array.isArray(blockLike.fields)) { - const fields = blockLike.fields.map(readTextObject).filter(Boolean); + const fields: string[] = []; + for (const field of blockLike.fields) { + const fieldText = readTextObject(field); + if (fieldText) { + fields.push(fieldText); + } + } return fields.length > 0 ? fields.join("\n") : undefined; } return undefined; @@ -185,7 +197,13 @@ function readSlackBlockText(block: unknown): string | undefined { if (!Array.isArray(blockLike.elements)) { return undefined; } - const parts = blockLike.elements.map(readTextObject).filter(Boolean); + const parts: string[] = []; + for (const element of blockLike.elements) { + const text = readTextObject(element); + if (text) { + parts.push(text); + } + } return parts.length > 0 ? parts.join(" ") : undefined; } case "image": @@ -205,7 +223,13 @@ function resolveSlackBlocksText(blocks: unknown[] | undefined): string | undefin if (!blocks?.length) { return undefined; } - const parts = blocks.map(readSlackBlockText).filter(Boolean); + const parts: string[] = []; + for (const block of blocks) { + const text = readSlackBlockText(block); + if (text) { + parts.push(text); + } + } return parts.length > 0 ? parts.join("\n") : undefined; } @@ -302,17 +326,19 @@ export async function resolveSlackMessageContent(params: { : undefined; const fileOnlyPlaceholder = fileOnlyFallback ? `[Slack file: ${fileOnlyFallback}]` : undefined; - const botAttachmentText = - params.isBotMessage && !attachmentContent?.text - ? (params.message.attachments ?? []) - .map( - (attachment) => - normalizeOptionalString(attachment.text) ?? - normalizeOptionalString(attachment.fallback), - ) - .filter(Boolean) - .join("\n") - : undefined; + let botAttachmentText: string | undefined; + if (params.isBotMessage && !attachmentContent?.text) { + const botAttachmentTextParts: string[] = []; + for (const attachment of params.message.attachments ?? []) { + const text = + normalizeOptionalString(attachment.text) ?? normalizeOptionalString(attachment.fallback); + if (text) { + botAttachmentTextParts.push(text); + } + } + botAttachmentText = + botAttachmentTextParts.length > 0 ? botAttachmentTextParts.join("\n") : undefined; + } const blocksText = resolveSlackBlocksText(params.message.blocks); const primaryText = chooseSlackPrimaryText({ diff --git a/extensions/slack/src/monitor/message-handler/prepare-thread-context.ts b/extensions/slack/src/monitor/message-handler/prepare-thread-context.ts index 22bed6ef7d4..be2e418815c 100644 --- a/extensions/slack/src/monitor/message-handler/prepare-thread-context.ts +++ b/extensions/slack/src/monitor/message-handler/prepare-thread-context.ts @@ -1,4 +1,5 @@ import { formatInboundEnvelope } from "openclaw/plugin-sdk/channel-inbound"; +import { runTasksWithConcurrency } from "openclaw/plugin-sdk/concurrency-runtime"; import type { ContextVisibilityMode } from "openclaw/plugin-sdk/config-types"; import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; import { @@ -29,6 +30,8 @@ type SlackThreadContextData = { threadStarterMedia: SlackMediaResult[] | null; }; +const SLACK_THREAD_CONTEXT_USER_LOOKUP_CONCURRENCY = 4; + function isSlackThreadContextSenderAllowed(params: { allowFromLower: string[]; allowNameMatching: boolean; @@ -50,6 +53,38 @@ function isSlackThreadContextSenderAllowed(params: { }).allowed; } +async function resolveSlackThreadUserMap(params: { + ctx: SlackMonitorContext; + messages: SlackThreadStarter[]; +}): Promise> { + const uniqueUserIds: string[] = []; + const seen = new Set(); + for (const item of params.messages) { + if (!item.userId || seen.has(item.userId)) { + continue; + } + seen.add(item.userId); + uniqueUserIds.push(item.userId); + } + const userMap = new Map(); + if (uniqueUserIds.length === 0) { + return userMap; + } + const { results } = await runTasksWithConcurrency({ + tasks: uniqueUserIds.map((id) => async () => { + const user = await params.ctx.resolveUserName(id); + return user ? { id, user } : null; + }), + limit: SLACK_THREAD_CONTEXT_USER_LOOKUP_CONCURRENCY, + }); + for (const result of results) { + if (result) { + userMap.set(result.id, result.user); + } + } + return userMap; +} + export async function resolveSlackThreadContextData(params: { ctx: SlackMonitorContext; account: ResolvedSlackAccount; @@ -92,7 +127,7 @@ export async function resolveSlackThreadContextData(params: { const starter = params.threadStarter; const starterSenderName = - params.allowNameMatching && starter?.userId + params.allowNameMatching && params.allowFromLower.length > 0 && starter?.userId ? (await params.ctx.resolveUserName(starter.userId))?.name : undefined; const starterIsCurrentBot = Boolean( @@ -174,39 +209,37 @@ export async function resolveSlackThreadContextData(params: { const omittedCurrentBotHistoryCount = threadHistory.length - threadHistoryWithoutCurrentBot.length; - const uniqueUserIds = [ - ...new Set( - threadHistoryWithoutCurrentBot - .map((item) => item.userId) - .filter((id): id is string => Boolean(id)), - ), - ]; - const userMap = new Map(); - await Promise.all( - uniqueUserIds.map(async (id) => { - const user = await params.ctx.resolveUserName(id); - if (user) { - userMap.set(id, user); - } - }), - ); - + const userMapForFilter = + params.contextVisibilityMode !== "all" && + params.allowNameMatching && + params.allowFromLower.length > 0 + ? await resolveSlackThreadUserMap({ + ctx: params.ctx, + messages: threadHistoryWithoutCurrentBot, + }) + : new Map(); const { items: filteredThreadHistory, omitted: omittedHistoryCount } = - filterSupplementalContextItems({ - items: threadHistoryWithoutCurrentBot, - mode: params.contextVisibilityMode, - kind: "thread", - isSenderAllowed: (historyMsg) => { - const msgUser = historyMsg.userId ? userMap.get(historyMsg.userId) : null; - return isSlackThreadContextSenderAllowed({ - allowFromLower: params.allowFromLower, - allowNameMatching: params.allowNameMatching, - userId: historyMsg.userId, - userName: msgUser?.name, - botId: historyMsg.botId, + params.contextVisibilityMode === "all" + ? { items: threadHistoryWithoutCurrentBot, omitted: 0 } + : filterSupplementalContextItems({ + items: threadHistoryWithoutCurrentBot, + mode: params.contextVisibilityMode, + kind: "thread", + isSenderAllowed: (historyMsg) => { + const msgUser = historyMsg.userId ? userMapForFilter.get(historyMsg.userId) : null; + return isSlackThreadContextSenderAllowed({ + allowFromLower: params.allowFromLower, + allowNameMatching: params.allowNameMatching, + userId: historyMsg.userId, + userName: msgUser?.name, + botId: historyMsg.botId, + }); + }, }); - }, - }); + const userMap = await resolveSlackThreadUserMap({ + ctx: params.ctx, + messages: filteredThreadHistory, + }); if (omittedHistoryCount > 0 || omittedCurrentBotHistoryCount > 0) { logVerbose( `slack: omitted ${omittedHistoryCount + omittedCurrentBotHistoryCount} thread message(s) from context (mode=${params.contextVisibilityMode})`, diff --git a/extensions/slack/src/monitor/thread.ts b/extensions/slack/src/monitor/thread.ts index b2fdee74991..9425078cb35 100644 --- a/extensions/slack/src/monitor/thread.ts +++ b/extensions/slack/src/monitor/thread.ts @@ -163,9 +163,9 @@ export async function resolveSlackThreadHistory(params: { continue; } retained.push(msg); - if (retained.length > maxMessages) { - retained.shift(); - } + } + if (retained.length > maxMessages) { + retained.splice(0, retained.length - maxMessages); } const next = response.response_metadata?.next_cursor;