From 038e99969935ca92f2ca5a034d0ac278869a005c Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:58:35 +0000 Subject: [PATCH 1/2] fix(slack): Return thread reaction users Co-Authored-By: David Cramer --- packages/junior/src/chat/slack/channel.ts | 8 ++++ .../chat/slack/tools/channel-list-messages.ts | 33 ++++++++++----- .../src/chat/slack/tools/thread-read.ts | 28 ++++++++++--- .../integration/slack-thread-read.test.ts | 41 +++++++++++++++++++ 4 files changed, 94 insertions(+), 16 deletions(-) diff --git a/packages/junior/src/chat/slack/channel.ts b/packages/junior/src/chat/slack/channel.ts index a05ef1e723..d740968597 100644 --- a/packages/junior/src/chat/slack/channel.ts +++ b/packages/junior/src/chat/slack/channel.ts @@ -2,6 +2,12 @@ import { getSlackClient, withSlackRetries } from "@/chat/slack/client"; import type { SlackChannelId } from "@/chat/slack/ids"; import type { SlackMessageTs } from "@/chat/slack/timestamp"; +export interface SlackReaction { + name?: string; + count?: number; + users?: string[]; +} + export interface SlackChannelMessage { ts?: string; user?: string; @@ -11,6 +17,7 @@ export interface SlackChannelMessage { bot_id?: string; type?: string; attachments?: unknown[]; + reactions?: SlackReaction[]; } export interface SlackFileRef { @@ -32,6 +39,7 @@ export interface SlackThreadReply { type?: string; files?: SlackFileRef[]; attachments?: unknown[]; + reactions?: SlackReaction[]; } /** List channel history using Slack-native, pre-validated timestamp bounds. */ diff --git a/packages/junior/src/chat/slack/tools/channel-list-messages.ts b/packages/junior/src/chat/slack/tools/channel-list-messages.ts index 090a7a93ff..c3aa4d5d06 100644 --- a/packages/junior/src/chat/slack/tools/channel-list-messages.ts +++ b/packages/junior/src/chat/slack/tools/channel-list-messages.ts @@ -79,34 +79,45 @@ export function createSlackChannelListMessagesTool(context: SlackToolContext) { readOnlyHint: true, }, inputSchema: z.object({ - channel_id: slackChannelRefParam.optional(), + channel_id: slackChannelRefParam.nullable().optional(), limit: z.coerce .number() .int() .min(1) .max(1000) .describe("Maximum number of messages to return across pages.") + .nullable() .optional(), cursor: z .string() .min(1) - .describe("Optional cursor to continue from a prior call.") + .describe( + "Cursor from `next_cursor` in a prior result. Use null or omit it for the first page; never invent a cursor.", + ) + .nullable() .optional(), oldest: slackTimestampParam( "Oldest message timestamp (Slack ts) for range filtering.", - ).optional(), + ) + .nullable() + .optional(), latest: slackTimestampParam( "Latest message timestamp (Slack ts) for range filtering.", - ).optional(), + ) + .nullable() + .optional(), inclusive: booleanInput( "Whether oldest/latest bounds should be inclusive.", - ).optional(), + ) + .nullable() + .optional(), max_pages: z.coerce .number() .int() .min(1) .max(10) .describe("Maximum number of API pages to traverse in a single call.") + .nullable() .optional(), }), outputSchema: juniorToolOutputSchema, @@ -121,7 +132,7 @@ export function createSlackChannelListMessagesTool(context: SlackToolContext) { }) => { const target = await resolveOptionalSlackChannelRef({ field: "channel_id", - value: channel_id, + value: channel_id ?? undefined, defaultChannelId: context.destinationChannelId, teamId: context.teamId, }); @@ -141,7 +152,7 @@ export function createSlackChannelListMessagesTool(context: SlackToolContext) { const normalizedOldest = normalizeRangeTimestamp( "oldest", - oldest, + oldest ?? undefined, targetChannelId, ); if (!normalizedOldest.ok) { @@ -149,7 +160,7 @@ export function createSlackChannelListMessagesTool(context: SlackToolContext) { } const normalizedLatest = normalizeRangeTimestamp( "latest", - latest, + latest ?? undefined, targetChannelId, ); if (!normalizedLatest.ok) { @@ -160,11 +171,11 @@ export function createSlackChannelListMessagesTool(context: SlackToolContext) { listChannelMessages({ channelId: targetChannelId, limit: limit ?? 100, - cursor, + cursor: cursor ?? undefined, oldest: normalizedOldest.value, latest: normalizedLatest.value, - inclusive, - maxPages: max_pages, + inclusive: inclusive ?? undefined, + maxPages: max_pages ?? undefined, }); let result: Awaited> | undefined; diff --git a/packages/junior/src/chat/slack/tools/thread-read.ts b/packages/junior/src/chat/slack/tools/thread-read.ts index 7cb3c712dd..de9c9ef5da 100644 --- a/packages/junior/src/chat/slack/tools/thread-read.ts +++ b/packages/junior/src/chat/slack/tools/thread-read.ts @@ -69,6 +69,15 @@ function sanitizeMessage( subtype: msg.subtype, bot_id: msg.bot_id, type: msg.type, + ...(msg.reactions?.length + ? { + reactions: msg.reactions.map((reaction) => ({ + name: reaction.name, + count: reaction.count, + users: reaction.users, + })), + } + : undefined), ...(attachmentText ? { attachment_text: attachmentText } : undefined), ...(files.length ? { @@ -119,7 +128,7 @@ export function createSlackThreadReadTool( ) { return zodTool({ description: - "Read a Slack thread from a shared archive URL or explicit channel + timestamp. Works for the current conversation and public channels.", + "Read a Slack thread, including message reaction users, from a shared archive URL or explicit channel + timestamp. Works for the current conversation and public channels.", annotations: { destructiveHint: false, idempotentHint: true, @@ -127,17 +136,25 @@ export function createSlackThreadReadTool( readOnlyHint: true, }, inputSchema: z.object({ - url: z.string().min(1).describe("Slack message archive URL.").optional(), - channel_id: slackChannelRefParam.optional(), + url: z + .string() + .min(1) + .describe("Slack message archive URL.") + .nullable() + .optional(), + channel_id: slackChannelRefParam.nullable().optional(), ts: slackTimestampParam( "Slack message timestamp. May be the thread root or any message in the thread.", - ).optional(), + ) + .nullable() + .optional(), limit: z.coerce .number() .int() .min(1) .max(1000) .describe("Maximum number of thread messages to fetch.") + .nullable() .optional(), max_pages: z.coerce .number() @@ -145,6 +162,7 @@ export function createSlackThreadReadTool( .min(1) .max(10) .describe("Maximum number of Slack API pages to traverse.") + .nullable() .optional(), }), outputSchema: juniorToolOutputSchema, @@ -197,7 +215,7 @@ export function createSlackThreadReadTool( channelId, threadTs: lookupTs, limit: limit ?? 1000, - maxPages: max_pages, + maxPages: max_pages ?? undefined, }); let replies: SlackThreadReply[] | undefined; diff --git a/packages/junior/tests/integration/slack-thread-read.test.ts b/packages/junior/tests/integration/slack-thread-read.test.ts index 4557b25c34..298c65dbfe 100644 --- a/packages/junior/tests/integration/slack-thread-read.test.ts +++ b/packages/junior/tests/integration/slack-thread-read.test.ts @@ -120,6 +120,47 @@ describe("slackThreadRead", () => { expect(getCapturedSlackApiCalls("conversations.replies")).toHaveLength(1); }); + it("returns reaction users with the thread messages", async () => { + queueSlackApiResponse("conversations.replies", { + body: conversationsRepliesPage({ + threadTs: "1700000000.123456", + messages: [ + { + ts: "1700000000.123456", + thread_ts: "1700000000.123456", + user: "U1", + text: "root message", + reactions: [ + { + name: "raised_hands", + count: 2, + users: ["U2", "U3"], + }, + ], + }, + ], + }), + }); + + const result = await executeTool(createTool({}), { + channel_id: "C0AHB7N2JCR", + ts: "1700000000.123456", + url: null, + limit: null, + max_pages: null, + }); + + expect(result.messages[0]).toMatchObject({ + reactions: [ + { + name: "raised_hands", + count: 2, + users: ["U2", "U3"], + }, + ], + }); + }); + it("uses thread_ts from the URL when present", async () => { queueSlackApiResponse("conversations.replies", { body: conversationsRepliesPage({ From d07704e7e49e64b3a5e080cc0bbc07dce482270c Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 14:06:24 +0000 Subject: [PATCH 2/2] refactor(slack): Tighten reaction reader patch Co-Authored-By: David Cramer --- packages/junior/src/chat/slack/channel.ts | 13 ++-- .../integration/slack-channel-tools.test.ts | 3 + .../integration/slack-thread-read.test.ts | 64 +++++++------------ 3 files changed, 30 insertions(+), 50 deletions(-) diff --git a/packages/junior/src/chat/slack/channel.ts b/packages/junior/src/chat/slack/channel.ts index d740968597..6835d396a7 100644 --- a/packages/junior/src/chat/slack/channel.ts +++ b/packages/junior/src/chat/slack/channel.ts @@ -2,12 +2,6 @@ import { getSlackClient, withSlackRetries } from "@/chat/slack/client"; import type { SlackChannelId } from "@/chat/slack/ids"; import type { SlackMessageTs } from "@/chat/slack/timestamp"; -export interface SlackReaction { - name?: string; - count?: number; - users?: string[]; -} - export interface SlackChannelMessage { ts?: string; user?: string; @@ -17,7 +11,6 @@ export interface SlackChannelMessage { bot_id?: string; type?: string; attachments?: unknown[]; - reactions?: SlackReaction[]; } export interface SlackFileRef { @@ -39,7 +32,11 @@ export interface SlackThreadReply { type?: string; files?: SlackFileRef[]; attachments?: unknown[]; - reactions?: SlackReaction[]; + reactions?: Array<{ + name?: string; + count?: number; + users?: string[]; + }>; } /** List channel history using Slack-native, pre-validated timestamp bounds. */ diff --git a/packages/junior/tests/integration/slack-channel-tools.test.ts b/packages/junior/tests/integration/slack-channel-tools.test.ts index 3d9d299578..0f5958ffa9 100644 --- a/packages/junior/tests/integration/slack-channel-tools.test.ts +++ b/packages/junior/tests/integration/slack-channel-tools.test.ts @@ -193,9 +193,12 @@ describe("slack channel tools", () => { ); const result = await executeTool(tool, { + channel_id: null, limit: 150, + cursor: null, oldest: "1690000000.000", latest: "1710000000", + inclusive: null, max_pages: 3, }); diff --git a/packages/junior/tests/integration/slack-thread-read.test.ts b/packages/junior/tests/integration/slack-thread-read.test.ts index 298c65dbfe..ac4776a681 100644 --- a/packages/junior/tests/integration/slack-thread-read.test.ts +++ b/packages/junior/tests/integration/slack-thread-read.test.ts @@ -120,47 +120,6 @@ describe("slackThreadRead", () => { expect(getCapturedSlackApiCalls("conversations.replies")).toHaveLength(1); }); - it("returns reaction users with the thread messages", async () => { - queueSlackApiResponse("conversations.replies", { - body: conversationsRepliesPage({ - threadTs: "1700000000.123456", - messages: [ - { - ts: "1700000000.123456", - thread_ts: "1700000000.123456", - user: "U1", - text: "root message", - reactions: [ - { - name: "raised_hands", - count: 2, - users: ["U2", "U3"], - }, - ], - }, - ], - }), - }); - - const result = await executeTool(createTool({}), { - channel_id: "C0AHB7N2JCR", - ts: "1700000000.123456", - url: null, - limit: null, - max_pages: null, - }); - - expect(result.messages[0]).toMatchObject({ - reactions: [ - { - name: "raised_hands", - count: 2, - users: ["U2", "U3"], - }, - ], - }); - }); - it("uses thread_ts from the URL when present", async () => { queueSlackApiResponse("conversations.replies", { body: conversationsRepliesPage({ @@ -212,6 +171,13 @@ describe("slackThreadRead", () => { thread_ts: "1700000000.500000", user: "U1", text: "standalone message", + reactions: [ + { + name: "raised_hands", + count: 2, + users: ["U2", "U3"], + }, + ], }, ], }), @@ -221,13 +187,27 @@ describe("slackThreadRead", () => { const result = await executeTool(tool, { channel_id: "C0MANUAL", ts: "1700000000.500000", + url: null, + limit: null, + max_pages: null, }); expect(result).toMatchObject({ channel_id: "C0MANUAL", count: 1, + messages: [ + { + text: "standalone message", + reactions: [ + { + name: "raised_hands", + count: 2, + users: ["U2", "U3"], + }, + ], + }, + ], }); - expect(result.messages[0].text).toBe("standalone message"); }); it("allows reading a private channel when it matches the current channel", async () => {