From 6f91b937c1b578685ecf4055618145dbb0da01ed Mon Sep 17 00:00:00 2001 From: Syed Osama Ali Shah <86572800+Osamaali313@users.noreply.github.com> Date: Tue, 14 Jul 2026 20:54:17 +0300 Subject: [PATCH] fix(slack): don't double-emit trailing text on odd number of ``` fences (#1436) extractCodeBlocks split on "```" and treated every odd-indexed part as a closed code block. With an odd number of fences (>=3), Split returns an even number of parts, so the final segment -- which follows an unpaired opening fence and must stay literal -- landed on an odd index and was both emitted as a \x00CB\x00 placeholder (later restored as ```seg```) AND re-appended verbatim by the trailing len(parts)%2==0 block. The segment was therefore duplicated in the rendered Slack message (and blocks gained a spurious entry). Unclosed trailing fences are common in streamed LLM output. Exclude the final unpaired part from the paired-scan loop. Add a regression test covering the closed-block-then-unpaired-fence case. --- internal/channels/slack/format.go | 11 ++++++++++- internal/channels/slack/format_test.go | 8 ++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/internal/channels/slack/format.go b/internal/channels/slack/format.go index 41591ffe..b95389cb 100644 --- a/internal/channels/slack/format.go +++ b/internal/channels/slack/format.go @@ -113,8 +113,17 @@ func extractCodeBlocks(text string) (blocks []string, result string) { return nil, text } + // An odd number of ``` fences makes Split return an even number of parts; + // the final segment then follows an unpaired opening fence and must stay + // literal (it is re-appended verbatim below), so exclude it from the + // paired-scan loop instead of misclassifying it as a closed code block. + scanEnd := len(parts) + if scanEnd%2 == 0 { + scanEnd-- + } var sb strings.Builder - for i, part := range parts { + for i := 0; i < scanEnd; i++ { + part := parts[i] if i%2 == 1 { blocks = append(blocks, part) sb.WriteString(fmt.Sprintf("\x00CB%d\x00", len(blocks)-1)) diff --git a/internal/channels/slack/format_test.go b/internal/channels/slack/format_test.go index 273b4bdc..85234c51 100644 --- a/internal/channels/slack/format_test.go +++ b/internal/channels/slack/format_test.go @@ -318,6 +318,14 @@ func TestExtractCodeBlocks(t *testing.T) { input: "text\n```\ncode (no closing)", expectedCount: 0, }, + { + // A closed block followed by an unpaired opening fence: 3 fences -> + // 4 parts. Only the middle part is a real code block; the trailing + // segment must stay literal, not be counted (and re-emitted) as one. + name: "closed block then unpaired trailing fence", + input: "a```b```c```d", + expectedCount: 1, + }, { name: "empty code block", input: "```\n\n```",