From c6a68bfc0e7ef2ff1b10f5b2f8fadb70c7b85d68 Mon Sep 17 00:00:00 2001 From: "sentry[bot]" <39604003+sentry[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:56:39 +0000 Subject: [PATCH 1/2] fix(slack): prevent invalid_blocks from auth pause notice --- packages/junior/src/chat/slack/footer.ts | 17 ++++---------- .../junior/tests/unit/slack/footer.test.ts | 23 ++++++++++++++----- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/packages/junior/src/chat/slack/footer.ts b/packages/junior/src/chat/slack/footer.ts index 53709d2d3c..f50901281d 100644 --- a/packages/junior/src/chat/slack/footer.ts +++ b/packages/junior/src/chat/slack/footer.ts @@ -14,12 +14,6 @@ interface SlackPlainTextObject { type: "plain_text"; } -/** Slack-flavored Markdown block — accepts a standard Markdown subset and Slack renders it natively. */ -interface SlackMarkdownBlock { - text: string; - type: "markdown"; -} - interface SlackSectionBlock { text: SlackMrkdwnTextObject; type: "section"; @@ -30,10 +24,7 @@ interface SlackContextBlock { type: "context"; } -export type SlackMessageBlock = - | SlackMarkdownBlock - | SlackSectionBlock - | SlackContextBlock; +export type SlackMessageBlock = SlackSectionBlock | SlackContextBlock; interface SlackReplyFooterItem { label: string; @@ -90,7 +81,7 @@ export function buildSlackReplyFooter(args: { : undefined; } -/** Build Slack blocks for a reply chunk using the Slack-flavored markdown block for the body. */ +/** Build Slack blocks for a reply chunk using a mrkdwn section block for the body. */ export function buildSlackReplyBlocks( text: string, footer: SlackReplyFooter | undefined, @@ -101,8 +92,8 @@ export function buildSlackReplyBlocks( const blocks: SlackMessageBlock[] = [ { - type: "markdown", - text, + type: "section", + text: { type: "mrkdwn", text }, }, ]; diff --git a/packages/junior/tests/unit/slack/footer.test.ts b/packages/junior/tests/unit/slack/footer.test.ts index 6657dd4137..cc0f67451d 100644 --- a/packages/junior/tests/unit/slack/footer.test.ts +++ b/packages/junior/tests/unit/slack/footer.test.ts @@ -122,15 +122,15 @@ describe("buildSlackReplyFooter", () => { }); describe("buildSlackReplyBlocks", () => { - it("renders the reply body as a markdown block plus a context footer", () => { + it("renders the reply body as a mrkdwn section block plus a context footer", () => { const footer = buildSlackReplyFooter({ conversationId: "slack:C123:1700000000.000100", }); expect(buildSlackReplyBlocks("Hello world", footer)).toEqual([ { - type: "markdown", - text: "Hello world", + type: "section", + text: { type: "mrkdwn", text: "Hello world" }, }, { type: "context", @@ -144,11 +144,22 @@ describe("buildSlackReplyBlocks", () => { ]); }); - it("renders a markdown block without footer when footer is undefined", () => { + it("renders a mrkdwn section block without footer when footer is undefined", () => { expect(buildSlackReplyBlocks("Hello world", undefined)).toEqual([ { - type: "markdown", - text: "Hello world", + type: "section", + text: { type: "mrkdwn", text: "Hello world" }, + }, + ]); + }); + + it("renders a mrkdwn section block when text contains URLs", () => { + const text = + "<@U123> I need access to GitHub to continue.\n\n*Why:* check out and proceed\n\nI sent you a link."; + expect(buildSlackReplyBlocks(text, undefined)).toEqual([ + { + type: "section", + text: { type: "mrkdwn", text }, }, ]); }); From c50a6f903e2f4e30e03a1b7e595a545bace5ed7c Mon Sep 17 00:00:00 2001 From: "sentry[bot]" <39604003+sentry[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 00:54:13 +0000 Subject: [PATCH 2/2] fix(slack): scope invalid_blocks fix to pre-formatted mrkdwn replies --- .../junior/src/chat/providers/slack/resume.ts | 3 ++ .../junior/src/chat/providers/slack/turn.ts | 1 + packages/junior/src/chat/slack/footer.ts | 49 ++++++++++++++++--- packages/junior/src/chat/slack/reply.ts | 3 ++ .../junior/tests/unit/slack/footer.test.ts | 16 +++--- 5 files changed, 56 insertions(+), 16 deletions(-) diff --git a/packages/junior/src/chat/providers/slack/resume.ts b/packages/junior/src/chat/providers/slack/resume.ts index 927fa0d7a8..ab31531aba 100644 --- a/packages/junior/src/chat/providers/slack/resume.ts +++ b/packages/junior/src/chat/providers/slack/resume.ts @@ -95,10 +95,12 @@ async function postSlackMessageBestEffort( text: string, conversationId?: string, replyAttribution?: ReplyAttribution, + bodyFormat?: import("@/chat/slack/footer").SlackReplyBodyFormat, ): Promise { try { if (conversationId) { await sendSlackReply({ + bodyFormat, channelId, conversationId, replyAttribution, @@ -904,6 +906,7 @@ async function resumeSlackTurnInContext( ), runArgs.conversationId, runArgs.run?.dispatch?.replyAttribution, + "mrkdwn", ); } return true; diff --git a/packages/junior/src/chat/providers/slack/turn.ts b/packages/junior/src/chat/providers/slack/turn.ts index ea961dcccc..0c2115b92f 100644 --- a/packages/junior/src/chat/providers/slack/turn.ts +++ b/packages/junior/src/chat/providers/slack/turn.ts @@ -445,6 +445,7 @@ export function createSlackTurn(deps: SlackTurnDeps) { await beforeFirstResponsePost(); if (channelId && threadTs) { await sendSlackReply({ + bodyFormat: "mrkdwn", channelId, conversationId, replyAttribution: options.execution?.dispatch?.replyAttribution, diff --git a/packages/junior/src/chat/slack/footer.ts b/packages/junior/src/chat/slack/footer.ts index f50901281d..46af9eeef6 100644 --- a/packages/junior/src/chat/slack/footer.ts +++ b/packages/junior/src/chat/slack/footer.ts @@ -14,6 +14,12 @@ interface SlackPlainTextObject { type: "plain_text"; } +/** Slack-flavored Markdown block — accepts a standard Markdown subset and Slack renders it natively. */ +interface SlackMarkdownBlock { + text: string; + type: "markdown"; +} + interface SlackSectionBlock { text: SlackMrkdwnTextObject; type: "section"; @@ -24,7 +30,10 @@ interface SlackContextBlock { type: "context"; } -export type SlackMessageBlock = SlackSectionBlock | SlackContextBlock; +export type SlackMessageBlock = + | SlackMarkdownBlock + | SlackSectionBlock + | SlackContextBlock; interface SlackReplyFooterItem { label: string; @@ -37,6 +46,22 @@ export interface SlackReplyFooter { items: SlackReplyFooterItem[]; } +/** + * Controls which Slack block type is used for the reply body. + * + * - `"commonmark"` (default): wraps the text in a `{ type: "markdown" }` block, + * which Slack renders from standard Markdown (bold, links, tables, headers). + * Use this for normal agent replies where `normalizeSlackReplyMarkdown` has + * already formatted the output for CommonMark delivery. + * + * - `"mrkdwn"`: wraps the text in a `{ type: "section", text: { type: "mrkdwn" } }` + * block. Use this when the text is already pre-formatted as Slack mrkdwn + * (e.g. `<@user>` mentions, `` angle-bracket links, single-`*` emphasis) + * and must not be passed through the markdown-to-rich_text converter, which + * can mis-tag mrkdwn tokens and trigger `invalid_blocks`. + */ +export type SlackReplyBodyFormat = "commonmark" | "mrkdwn"; + /** Render compact reply attribution for the Slack footer. */ export function formatReplyAttribution(attribution: ReplyAttribution): string { return attribution.detail @@ -81,21 +106,29 @@ export function buildSlackReplyFooter(args: { : undefined; } -/** Build Slack blocks for a reply chunk using a mrkdwn section block for the body. */ +/** + * Build Slack blocks for a reply chunk. + * + * The body block format is controlled by `bodyFormat`: + * - `"commonmark"` (default): uses `{ type: "markdown" }` for CommonMark rendering. + * - `"mrkdwn"`: uses `{ type: "section", text: { type: "mrkdwn" } }` for + * pre-formatted mrkdwn text (e.g. auth-pause notices with `<@user>` mentions). + */ export function buildSlackReplyBlocks( text: string, footer: SlackReplyFooter | undefined, + bodyFormat: SlackReplyBodyFormat = "commonmark", ): SlackMessageBlock[] | undefined { if (!text.trim()) { return undefined; } - const blocks: SlackMessageBlock[] = [ - { - type: "section", - text: { type: "mrkdwn", text }, - }, - ]; + const bodyBlock: SlackMessageBlock = + bodyFormat === "mrkdwn" + ? { type: "section", text: { type: "mrkdwn", text } } + : { type: "markdown", text }; + + const blocks: SlackMessageBlock[] = [bodyBlock]; if (footer && (footer.attribution || footer.items.length > 0)) { const attributionElements: SlackPlainTextObject[] = footer.attribution diff --git a/packages/junior/src/chat/slack/reply.ts b/packages/junior/src/chat/slack/reply.ts index eaad1b1767..5d5ca26d31 100644 --- a/packages/junior/src/chat/slack/reply.ts +++ b/packages/junior/src/chat/slack/reply.ts @@ -8,6 +8,7 @@ import { buildSlackReplyBlocks, buildSlackReplyFooter, formatReplyAttribution, + type SlackReplyBodyFormat, } from "@/chat/slack/footer"; import { escapeSlackMrkdwnText } from "@/chat/slack/mrkdwn"; import { postSlackMessage } from "@/chat/slack/outbound"; @@ -20,6 +21,7 @@ import { splitSlackReplyText } from "@/chat/slack/output"; * context, and posts through the shared Slack outbound boundary. */ export async function sendSlackReply(args: { + bodyFormat?: SlackReplyBodyFormat; channelId: string; conversationId: string; replyAttribution?: ReplyAttribution; @@ -41,6 +43,7 @@ export async function sendSlackReply(args: { const blocks = buildSlackReplyBlocks( text, isFinalChunk ? footer : undefined, + args.bodyFormat, ); const fallbackText = isFinalChunk && args.replyAttribution diff --git a/packages/junior/tests/unit/slack/footer.test.ts b/packages/junior/tests/unit/slack/footer.test.ts index cc0f67451d..731a2f900b 100644 --- a/packages/junior/tests/unit/slack/footer.test.ts +++ b/packages/junior/tests/unit/slack/footer.test.ts @@ -122,15 +122,15 @@ describe("buildSlackReplyFooter", () => { }); describe("buildSlackReplyBlocks", () => { - it("renders the reply body as a mrkdwn section block plus a context footer", () => { + it("renders the reply body as a markdown block plus a context footer", () => { const footer = buildSlackReplyFooter({ conversationId: "slack:C123:1700000000.000100", }); expect(buildSlackReplyBlocks("Hello world", footer)).toEqual([ { - type: "section", - text: { type: "mrkdwn", text: "Hello world" }, + type: "markdown", + text: "Hello world", }, { type: "context", @@ -144,19 +144,19 @@ describe("buildSlackReplyBlocks", () => { ]); }); - it("renders a mrkdwn section block without footer when footer is undefined", () => { + it("renders a markdown block without footer when footer is undefined", () => { expect(buildSlackReplyBlocks("Hello world", undefined)).toEqual([ { - type: "section", - text: { type: "mrkdwn", text: "Hello world" }, + type: "markdown", + text: "Hello world", }, ]); }); - it("renders a mrkdwn section block when text contains URLs", () => { + it("renders a mrkdwn section block for pre-formatted mrkdwn text (auth-pause path)", () => { const text = "<@U123> I need access to GitHub to continue.\n\n*Why:* check out and proceed\n\nI sent you a link."; - expect(buildSlackReplyBlocks(text, undefined)).toEqual([ + expect(buildSlackReplyBlocks(text, undefined, "mrkdwn")).toEqual([ { type: "section", text: { type: "mrkdwn", text },