diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aa2f91cdbf..629cb667604 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -160,6 +160,7 @@ Docs: https://docs.openclaw.ai - Dreaming/memory-core: use the ingestion day, not the source file day, for daily recall dedupe so repeat sweeps of the same daily note can increment `dailyCount` across days instead of stalling at `1`. (#67091) Thanks @Bartok9. - Node-host/tools.exec: let approval binding distinguish known native binaries from mutable shell payload files, while still fail-closing unknown or racy file probes so absolute-path node-host commands like `/usr/bin/whoami` no longer get rejected as unsafe interpreter/runtime commands. (#66731) Thanks @tmimmanuel. - Codex/gateway: fix gateway crash when the codex-acp subprocess terminates abruptly; an unhandled EPIPE on the child stdin stream now routes through graceful client shutdown, rejecting pending requests instead of propagating as an uncaught exception that crashes the entire gateway daemon and all connected channels. Fixes #67886. (#67947) thanks @openperf +- Slack/streaming: resolve native streaming recipient teams from the inbound user when available, with a monitor-team fallback, so DM and shared-workspace streams target the right recipient more reliably. ## 2026.4.14 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 445cf768241..6ea4d14a7d0 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 } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { createSlackTurnDeliveryTracker, isSlackStreamingEnabled, + resolveSlackStreamRecipientTeamId, resolveSlackStreamingThreadHint, shouldEnableSlackPreviewStreaming, shouldInitializeSlackDraftStream, @@ -20,6 +21,79 @@ describe("slack native streaming defaults", () => { }); }); +describe("slack native streaming recipient team", () => { + it("resolves the recipient team through users.info", async () => { + const usersInfo = vi.fn(async () => ({ + user: { team_id: "T_LOOKUP" }, + })); + + expect( + await resolveSlackStreamRecipientTeamId({ + client: { + users: { + info: usersInfo, + }, + } as never, + token: "xoxb-test", + userId: "U_REMOTE", + fallbackTeamId: "T_LOCAL", + }), + ).toBe("T_LOOKUP"); + expect(usersInfo).toHaveBeenCalledWith({ + token: "xoxb-test", + user: "U_REMOTE", + }); + }); + + it("falls back to profile.team when users.info omits user.team_id", async () => { + expect( + await resolveSlackStreamRecipientTeamId({ + client: { + users: { + info: vi.fn(async () => ({ + user: { profile: { team: "T_PROFILE" } }, + })), + }, + } as never, + token: "xoxb-test", + userId: "U_REMOTE", + fallbackTeamId: "T_LOCAL", + }), + ).toBe("T_PROFILE"); + }); + + it("falls back to the monitor team when users.info cannot resolve a team", async () => { + expect( + await resolveSlackStreamRecipientTeamId({ + client: { + users: { + info: vi.fn(async () => { + throw new Error("user_not_found"); + }), + }, + } as never, + token: "xoxb-test", + userId: "U_REMOTE", + fallbackTeamId: "T_LOCAL", + }), + ).toBe("T_LOCAL"); + }); + + it("falls back to the monitor team when no user id is available", async () => { + expect( + await resolveSlackStreamRecipientTeamId({ + client: { + users: { + info: vi.fn(), + }, + } as never, + token: "xoxb-test", + fallbackTeamId: "T_LOCAL", + }), + ).toBe("T_LOCAL"); + }); +}); + describe("slack turn delivery tracker", () => { it("treats repeated text payloads on the same thread as duplicates", () => { const tracker = createSlackTurnDeliveryTracker(); diff --git a/extensions/slack/src/monitor/message-handler/dispatch.ts b/extensions/slack/src/monitor/message-handler/dispatch.ts index 28fc3aa7f71..74783f81db7 100644 --- a/extensions/slack/src/monitor/message-handler/dispatch.ts +++ b/extensions/slack/src/monitor/message-handler/dispatch.ts @@ -182,6 +182,29 @@ function shouldUseStreaming(params: { return true; } +export async function resolveSlackStreamRecipientTeamId(params: { + client: Pick; + token: string; + userId?: PreparedSlackMessage["message"]["user"]; + fallbackTeamId?: string; +}): Promise { + if (params.userId) { + try { + const info = await params.client.users.info({ + token: params.token, + user: params.userId, + }); + const teamId = info.user?.team_id ?? info.user?.profile?.team; + if (teamId) { + return teamId; + } + } catch (err) { + logVerbose(`slack-stream: users.info team lookup failed (${String(err)})`); + } + } + return params.fallbackTeamId; +} + export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessage) { const { ctx, account, message, route } = prepared; const cfg = ctx.cfg; @@ -490,7 +513,12 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag channel: message.channel, threadTs: streamThreadTs, text, - teamId: ctx.teamId, + teamId: await resolveSlackStreamRecipientTeamId({ + client: ctx.app.client, + token: ctx.botToken, + userId: message.user, + fallbackTeamId: ctx.teamId, + }), userId: message.user, }); observedReplyDelivery = true;