From 8ce7cc8aae5b11d45993e82d2e0db0c7fbe52fa2 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 10:35:10 -0700 Subject: [PATCH] perf(slack): cache stream recipient team lookup --- .../dispatch.streaming.test.ts | 27 +++++++++- .../src/monitor/message-handler/dispatch.ts | 50 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/extensions/slack/src/monitor/message-handler/dispatch.streaming.test.ts b/extensions/slack/src/monitor/message-handler/dispatch.streaming.test.ts index 86666f5d3cc..5c3897d6f13 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.streaming.test.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.streaming.test.ts @@ -1,7 +1,8 @@ -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { createSlackTurnDeliveryTracker, isSlackStreamingEnabled, + resetSlackStreamRecipientTeamCacheForTests, resolveSlackDisableBlockStreaming, resolveSlackStreamRecipientTeamId, resolveSlackStreamingThreadHint, @@ -9,6 +10,10 @@ import { shouldInitializeSlackDraftStream, } from "./dispatch.js"; +afterEach(() => { + resetSlackStreamRecipientTeamCacheForTests(); +}); + describe("slack native streaming defaults", () => { it("is enabled for partial mode when native streaming is on", () => { expect(isSlackStreamingEnabled({ mode: "partial", nativeStreaming: true })).toBe(true); @@ -93,6 +98,26 @@ describe("slack native streaming recipient team", () => { }), ).toBe("T_LOCAL"); }); + + it("caches resolved user teams for repeated stream starts", async () => { + const usersInfo = vi.fn(async () => ({ + user: { team_id: "T_LOOKUP" }, + })); + const params = { + client: { + users: { + info: usersInfo, + }, + } as never, + token: "xoxb-test", + userId: "U_REMOTE", + fallbackTeamId: "T_LOCAL", + }; + + await expect(resolveSlackStreamRecipientTeamId(params)).resolves.toBe("T_LOOKUP"); + await expect(resolveSlackStreamRecipientTeamId(params)).resolves.toBe("T_LOOKUP"); + expect(usersInfo).toHaveBeenCalledTimes(1); + }); }); describe("slack turn delivery tracker", () => { diff --git a/extensions/slack/src/monitor/message-handler/dispatch.ts b/extensions/slack/src/monitor/message-handler/dispatch.ts index 1e1db60f8e2..dc82dfa6595 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.ts @@ -171,6 +171,9 @@ type SlackTurnDeliveryAttempt = { textOverride?: string; }; +const SLACK_STREAM_RECIPIENT_TEAM_CACHE_MAX = 2000; +const slackStreamRecipientTeamCache = new Map(); + function buildSlackTurnDeliveryKey(params: SlackTurnDeliveryAttempt): string | null { const reply = resolveSendableOutboundReplyParts(params.payload, { text: params.textOverride, @@ -189,6 +192,48 @@ function buildSlackTurnDeliveryKey(params: SlackTurnDeliveryAttempt): string | n }); } +function readSlackStreamRecipientTeamCache(params: { + fallbackTeamId?: string; + userId?: string; +}): string | undefined { + if (!params.fallbackTeamId || !params.userId) { + return undefined; + } + const cacheKey = `${params.fallbackTeamId}:${params.userId}`; + const cached = slackStreamRecipientTeamCache.get(cacheKey); + if (!cached) { + return undefined; + } + slackStreamRecipientTeamCache.delete(cacheKey); + slackStreamRecipientTeamCache.set(cacheKey, cached); + return cached; +} + +function rememberSlackStreamRecipientTeam(params: { + fallbackTeamId?: string; + userId?: string; + teamId: string; +}): void { + if (!params.fallbackTeamId || !params.userId) { + return; + } + const cacheKey = `${params.fallbackTeamId}:${params.userId}`; + if (slackStreamRecipientTeamCache.has(cacheKey)) { + slackStreamRecipientTeamCache.delete(cacheKey); + } + slackStreamRecipientTeamCache.set(cacheKey, params.teamId); + if (slackStreamRecipientTeamCache.size > SLACK_STREAM_RECIPIENT_TEAM_CACHE_MAX) { + const oldest = slackStreamRecipientTeamCache.keys().next().value; + if (oldest) { + slackStreamRecipientTeamCache.delete(oldest); + } + } +} + +export function resetSlackStreamRecipientTeamCacheForTests(): void { + slackStreamRecipientTeamCache.clear(); +} + export function createSlackTurnDeliveryTracker() { const deliveredKeys = new Set(); return { @@ -225,6 +270,10 @@ export async function resolveSlackStreamRecipientTeamId(params: { userId?: PreparedSlackMessage["message"]["user"]; fallbackTeamId?: string; }): Promise { + const cachedTeamId = readSlackStreamRecipientTeamCache(params); + if (cachedTeamId) { + return cachedTeamId; + } if (params.userId) { try { const info = await params.client.users.info({ @@ -233,6 +282,7 @@ export async function resolveSlackStreamRecipientTeamId(params: { }); const teamId = info.user?.team_id ?? info.user?.profile?.team; if (teamId) { + rememberSlackStreamRecipientTeam({ ...params, teamId }); return teamId; } } catch (err) {