From 8686f060d936f21fc2e284a5812fc1c0791c3caf Mon Sep 17 00:00:00 2001 From: nanobot-agent Date: Tue, 24 Feb 2026 12:43:21 +0000 Subject: [PATCH 1/3] fix(slack): add post-processing to fix mrkdwn conversion edge cases The slackify_markdown library misses several patterns that LLMs commonly produce, causing raw Markdown symbols (**bold**, ##headers) to appear in Slack messages. Add _fixup_mrkdwn() post-processor that: - Converts leftover **bold** patterns (e.g. **Status:**OK where closing ** is adjacent to non-space chars) - Fixes & over-escaping in bare URLs - Protects code blocks from false-positive fixups Co-authored-by: Cursor --- nanobot/channels/slack.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/nanobot/channels/slack.py b/nanobot/channels/slack.py index 906593b..e8175a3 100644 --- a/nanobot/channels/slack.py +++ b/nanobot/channels/slack.py @@ -229,6 +229,9 @@ class SlackChannel(BaseChannel): return re.sub(rf"<@{re.escape(self._bot_user_id)}>\s*", "", text).strip() _TABLE_RE = re.compile(r"(?m)^\|.*\|$(?:\n\|[\s:|-]*\|$)(?:\n\|.*\|$)*") + _CODE_FENCE_RE = re.compile(r"```[\s\S]*?```") + _LEFTOVER_BOLD_RE = re.compile(r"\*\*(.+?)\*\*") + _BARE_URL_RE = re.compile(r"(? str: @@ -236,7 +239,39 @@ class SlackChannel(BaseChannel): if not text: return "" text = cls._TABLE_RE.sub(cls._convert_table, text) - return slackify_markdown(text) + text = slackify_markdown(text) + text = cls._fixup_mrkdwn(text) + return text + + @classmethod + def _fixup_mrkdwn(cls, text: str) -> str: + """Fix markdown artifacts that slackify_markdown misses. + + Handles: leftover ``**bold**``, ``&`` in bare URLs, and + collapsed paragraph spacing. + """ + # Protect code blocks from further processing + code_blocks: list[str] = [] + + def _save_code(m: re.Match) -> str: + code_blocks.append(m.group(0)) + return f"\x00CB{len(code_blocks) - 1}\x00" + + text = cls._CODE_FENCE_RE.sub(_save_code, text) + + # Fix leftover **bold** the library didn't convert (e.g. **key:**val) + text = cls._LEFTOVER_BOLD_RE.sub(r"*\1*", text) + + # Fix & in bare URLs that the library over-escaped + text = cls._BARE_URL_RE.sub( + lambda m: m.group(0).replace("&", "&"), text + ) + + # Restore code blocks + for i, block in enumerate(code_blocks): + text = text.replace(f"\x00CB{i}\x00", block) + + return text @staticmethod def _convert_table(match: re.Match) -> str: From 81b669b36e4541b6f6f2101b71e5880643c350e8 Mon Sep 17 00:00:00 2001 From: nanobot-agent Date: Tue, 24 Feb 2026 12:44:17 +0000 Subject: [PATCH 2/3] fix(slack): post-process slackify_markdown output to catch leftover artifacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The slackify_markdown library (markdown-it) fails to convert **bold** when the closing ** is immediately followed by non-space text (e.g. **Status:**OK). This is a very common LLM output pattern that results in raw ** showing up in Slack messages. Add _fixup_mrkdwn() post-processor that: - Converts leftover **bold** → *bold* (Slack mrkdwn) - Converts leftover ## headers → *bold* (safety net) - Fixes over-escaped & in bare URLs - Protects code fences and inline code from being mangled Co-authored-by: Cursor --- nanobot/channels/slack.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/nanobot/channels/slack.py b/nanobot/channels/slack.py index e8175a3..87ba020 100644 --- a/nanobot/channels/slack.py +++ b/nanobot/channels/slack.py @@ -230,7 +230,9 @@ class SlackChannel(BaseChannel): _TABLE_RE = re.compile(r"(?m)^\|.*\|$(?:\n\|[\s:|-]*\|$)(?:\n\|.*\|$)*") _CODE_FENCE_RE = re.compile(r"```[\s\S]*?```") + _INLINE_CODE_RE = re.compile(r"`[^`]+`") _LEFTOVER_BOLD_RE = re.compile(r"\*\*(.+?)\*\*") + _LEFTOVER_HEADER_RE = re.compile(r"^#{1,6}\s+(.+)$", re.MULTILINE) _BARE_URL_RE = re.compile(r"(? str: """Fix markdown artifacts that slackify_markdown misses. - Handles: leftover ``**bold**``, ``&`` in bare URLs, and - collapsed paragraph spacing. + The slackify_markdown library uses markdown-it which requires certain + boundary conditions for emphasis. Patterns like ``**key:**value`` + (closing ``**`` immediately followed by non-space) are not recognised + as bold and pass through as literal asterisks. This method catches + those leftovers, stray ``## headers``, and over-escaped ``&`` in + bare URLs, while leaving code spans untouched. """ # Protect code blocks from further processing code_blocks: list[str] = [] @@ -258,9 +264,10 @@ class SlackChannel(BaseChannel): return f"\x00CB{len(code_blocks) - 1}\x00" text = cls._CODE_FENCE_RE.sub(_save_code, text) + text = cls._INLINE_CODE_RE.sub(_save_code, text) - # Fix leftover **bold** the library didn't convert (e.g. **key:**val) text = cls._LEFTOVER_BOLD_RE.sub(r"*\1*", text) + text = cls._LEFTOVER_HEADER_RE.sub(r"*\1*", text) # Fix & in bare URLs that the library over-escaped text = cls._BARE_URL_RE.sub( From 96e1730af55ef8b61ac7fdcdde556df2127d33da Mon Sep 17 00:00:00 2001 From: Re-bin Date: Tue, 24 Feb 2026 16:20:28 +0000 Subject: [PATCH 3/3] style: simplify _fixup_mrkdwn and trim docstring in SlackChannel --- nanobot/channels/slack.py | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/nanobot/channels/slack.py b/nanobot/channels/slack.py index 87ba020..57bfbcb 100644 --- a/nanobot/channels/slack.py +++ b/nanobot/channels/slack.py @@ -241,22 +241,11 @@ class SlackChannel(BaseChannel): if not text: return "" text = cls._TABLE_RE.sub(cls._convert_table, text) - text = slackify_markdown(text) - text = cls._fixup_mrkdwn(text) - return text + return cls._fixup_mrkdwn(slackify_markdown(text)) @classmethod def _fixup_mrkdwn(cls, text: str) -> str: - """Fix markdown artifacts that slackify_markdown misses. - - The slackify_markdown library uses markdown-it which requires certain - boundary conditions for emphasis. Patterns like ``**key:**value`` - (closing ``**`` immediately followed by non-space) are not recognised - as bold and pass through as literal asterisks. This method catches - those leftovers, stray ``## headers``, and over-escaped ``&`` in - bare URLs, while leaving code spans untouched. - """ - # Protect code blocks from further processing + """Fix markdown artifacts that slackify_markdown misses.""" code_blocks: list[str] = [] def _save_code(m: re.Match) -> str: @@ -265,19 +254,12 @@ class SlackChannel(BaseChannel): text = cls._CODE_FENCE_RE.sub(_save_code, text) text = cls._INLINE_CODE_RE.sub(_save_code, text) - text = cls._LEFTOVER_BOLD_RE.sub(r"*\1*", text) text = cls._LEFTOVER_HEADER_RE.sub(r"*\1*", text) + text = cls._BARE_URL_RE.sub(lambda m: m.group(0).replace("&", "&"), text) - # Fix & in bare URLs that the library over-escaped - text = cls._BARE_URL_RE.sub( - lambda m: m.group(0).replace("&", "&"), text - ) - - # Restore code blocks for i, block in enumerate(code_blocks): text = text.replace(f"\x00CB{i}\x00", block) - return text @staticmethod