From 9459057d7f570b8674d28a100490a106b348ff9c Mon Sep 17 00:00:00 2001
From: Teknium <127238744+teknium1@users.noreply.github.com>
Date: Sun, 14 Jun 2026 04:22:22 -0700
Subject: [PATCH] fix(telegram): guard rich details math crash (#46102)
---
gateway/platforms/telegram.py | 28 +++++++++
tests/gateway/test_telegram_rich_messages.py | 62 ++++++++++++++++++++
2 files changed, 90 insertions(+)
diff --git a/gateway/platforms/telegram.py b/gateway/platforms/telegram.py
index 314f7249e8a..381535ab8ac 100644
--- a/gateway/platforms/telegram.py
+++ b/gateway/platforms/telegram.py
@@ -953,6 +953,32 @@ class TelegramAdapter(BasePlatformAdapter):
"""
return inspect.iscoroutinefunction(getattr(self._bot, "do_api_request", None))
+ _RICH_DETAILS_RE = re.compile(r"]*>.*? ", re.IGNORECASE | re.DOTALL)
+ _RICH_MATH_IN_DETAILS_RE = re.compile(
+ r"(\$\$.*?\$\$|"
+ r"\\\[.*?\\\]|"
+ r"\\\(.*?\\\)|"
+ r"\\(?:sum|frac|alpha|beta|gamma|delta|theta|lambda|mu|pi|sigma|"
+ r"int|prod|sqrt|lim|infty|begin\{(?:equation|align|matrix|cases)\}))",
+ re.IGNORECASE | re.DOTALL,
+ )
+
+ def _has_telegram_desktop_details_math_crash_shape(self, content: str) -> bool:
+ """Return True for rich-message details+math content that crashes TDesktop.
+
+ Telegram Desktop 6.9.1 can crash while rendering Bot API 10.1 rich
+ messages containing math inside a collapsible details block
+ (telegramdesktop/tdesktop#30808). The Bot API accepts the payload, so
+ Hermes must skip rich delivery up front and use the legacy MarkdownV2
+ path until affected Desktop clients age out.
+ """
+ if not content:
+ return False
+ for details_block in self._RICH_DETAILS_RE.findall(content):
+ if self._RICH_MATH_IN_DETAILS_RE.search(details_block):
+ return True
+ return False
+
def _should_attempt_rich(
self, content: str, metadata: Optional[Dict[str, Any]] = None
) -> bool:
@@ -962,6 +988,7 @@ class TelegramAdapter(BasePlatformAdapter):
and not (metadata or {}).get("expect_edits")
and content
and content.strip()
+ and not self._has_telegram_desktop_details_math_crash_shape(content)
and self._content_fits_rich_limits(content)
and self._bot_supports_rich()
)
@@ -1194,6 +1221,7 @@ class TelegramAdapter(BasePlatformAdapter):
and not getattr(self, "_rich_draft_disabled", False)
and content
and content.strip()
+ and not self._has_telegram_desktop_details_math_crash_shape(content)
and self._content_fits_rich_limits(content)
and self._bot_supports_rich()
)
diff --git a/tests/gateway/test_telegram_rich_messages.py b/tests/gateway/test_telegram_rich_messages.py
index 54827111a75..d13dc43ef03 100644
--- a/tests/gateway/test_telegram_rich_messages.py
+++ b/tests/gateway/test_telegram_rich_messages.py
@@ -24,6 +24,12 @@ from telegram.error import BadRequest, NetworkError, TimedOut
# Content exercising rich-only constructs: a heading, a real Markdown table,
# and a task list. Pipes / brackets must survive untouched into the payload.
RICH_CONTENT = "## Results\n\n| Case | Status |\n|---|---|\n| rich | ✅ |\n\n- [x] table renders"
+DANGEROUS_DETAILS_MATH = (
+ "Complex proof
\n\n"
+ "$$\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}$$\n\n"
+ "And inline \\(\\alpha + \\beta\\)\n"
+ " "
+)
# PTB 22.6's real unknown-endpoint errors: do_api_request can raise
# EndPointNotFound for Bot API 404s, and the request layer can wrap that same
@@ -105,6 +111,48 @@ async def test_rich_happy_path_sends_raw_markdown():
adapter._bot.send_message.assert_not_called()
+@pytest.mark.asyncio
+async def test_details_with_math_skips_rich_send_to_avoid_tdesktop_crash():
+ adapter = _make_adapter()
+
+ result = await adapter.send("12345", DANGEROUS_DETAILS_MATH)
+
+ assert result.success is True
+ bot = adapter._bot
+ assert bot is not None
+ bot.do_api_request.assert_not_called()
+ bot.send_message.assert_awaited()
+
+
+@pytest.mark.asyncio
+async def test_details_without_math_still_uses_rich_send():
+ adapter = _make_adapter()
+
+ result = await adapter.send(
+ "12345",
+ "Notes
\nNo equations here.\n ",
+ )
+
+ assert result.success is True
+ bot = adapter._bot
+ assert bot is not None
+ bot.do_api_request.assert_awaited_once()
+ bot.send_message.assert_not_called()
+
+
+@pytest.mark.asyncio
+async def test_math_outside_details_still_uses_rich_send():
+ adapter = _make_adapter()
+
+ result = await adapter.send("12345", "Outside details: $$x^2 + y^2$$")
+
+ assert result.success is True
+ bot = adapter._bot
+ assert bot is not None
+ bot.do_api_request.assert_awaited_once()
+ bot.send_message.assert_not_called()
+
+
@pytest.mark.asyncio
async def test_rich_messages_opt_out_uses_legacy_send_path():
adapter = _make_adapter(extra={"rich_messages": False})
@@ -393,6 +441,20 @@ async def test_rich_gate_tolerates_minimal_bot_without_raw_endpoint():
# ── Streaming drafts: sendRichMessageDraft ─────────────────────────────
+@pytest.mark.asyncio
+async def test_details_with_math_skips_rich_draft_to_avoid_tdesktop_crash():
+ adapter = _make_adapter()
+ bot = adapter._bot
+ assert bot is not None
+ bot.do_api_request = AsyncMock(return_value=True)
+
+ result = await adapter.send_draft("12345", draft_id=7, content=DANGEROUS_DETAILS_MATH)
+
+ assert result.success is True
+ bot.do_api_request.assert_not_called()
+ bot.send_message_draft.assert_awaited_once()
+
+
@pytest.mark.asyncio
async def test_rich_draft_happy_path_sends_raw_markdown():
adapter = _make_adapter()