{"repo":"NousResearch/hermes-agent","public_id":"47653","display_label":"Telegram CJK rich message rendering fixes","summary":"These issues and pull requests address garbled or overlapping CJK character rendering in Telegram rich messages, including fallback handling, opt-out configuration options, and separate gating for draft previews.","subsystem":"gateway","counts":{"members":6,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":2,"attempt_prs":3},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":50222},"resolution":{"state":"resolved","by":"pr-50222"},"freshness":{"state":"verified","last_verified_at":"2026-09-15T11:24:54+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.3333333333333333},"fix_edges":[{"issue_number":47653,"pr_number":50222,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":47653,"pr_number":111831,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":47653,"title":"Bug: Mac Telegram Rich Messages render CJK characters as garbled overlapping symbols","url":null,"state":"closed"},{"kind":"pr","number":47681,"title":"fix(telegram): avoid rich messages for CJK text","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#50222","number":50222},{"kind":"pr","value":"#47653","number":47653}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":50222,"title":"fix(telegram): avoid rich messages for CJK text (#47653)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["plugins/platforms/telegram/adapter.py","tests/gateway/test_telegram_rich_messages.py"],"fix_hunks":[{"file":"plugins/platforms/telegram/adapter.py","hunks":["@@ -1048,6 +1048,16 @@ def _bot_supports_rich(self) -> bool:\n         r\"int|prod|sqrt|lim|infty|begin\\{(?:equation|align|matrix|cases)\\}))\",\n         re.IGNORECASE | re.DOTALL,\n     )\n+    _RICH_CJK_RE = re.compile(\n+        \"[\"\n+        \"\\u3040-\\u30ff\"  # Hiragana, Katakana\n+        \"\\u3400-\\u4dbf\"  # CJK Extension A\n+        \"\\u4e00-\\u9fff\"  # CJK Unified Ideographs\n+        \"\\uac00-\\ud7af\"  # Hangul syllables\n+        \"\\uf900-\\ufaff\"  # CJK Compatibility Ideographs\n+        \"\\U00020000-\\U000323af\"  # CJK extensions and compatibility supplement\n+        \"]\"\n+    )\n \n     def _has_telegram_desktop_details_math_crash_shape(self, content: str) -> bool:\n         \"\"\"Return True for rich-message details+math content that crashes TDesktop.","@@ -1065,6 +1075,16 @@ def _has_telegram_desktop_details_math_crash_shape(self, content: str) -> bool:\n                 return True\n         return False\n \n+    def _has_telegram_desktop_cjk_rich_garble_shape(self, content: str) -> bool:\n+        \"\"\"Return True for CJK content that current TDesktop rich drafts garble.\n+\n+        Telegram Mac/Desktop Bot API 10.1 rich-message rendering currently\n+        leaves overlapping draft/overlay glyph artifacts for CJK text (#47653).\n+        The legacy MarkdownV2 path renders the same text cleanly, so skip rich\n+        delivery up front until affected clients age out.\n+        \"\"\"\n+        return bool(content and self._RICH_CJK_RE.search(content))\n+\n     def _needs_rich_rendering(self, content: str) -> bool:\n         \"\"\"Return True for markdown constructs that the legacy path degrades.\n ","@@ -1103,6 +1123,7 @@ def _rich_eligible(self, content: str) -> bool:\n             and content.strip()\n             and self._needs_rich_rendering(content)\n             and not self._has_telegram_desktop_details_math_crash_shape(content)\n+            and not self._has_telegram_desktop_cjk_rich_garble_shape(content)\n             and self._content_fits_rich_limits(content)\n             and self._bot_supports_rich()\n         )","@@ -1424,6 +1445,7 @@ def _should_attempt_rich_draft(self, content: str) -> bool:\n             and content\n             and content.strip()\n             and not self._has_telegram_desktop_details_math_crash_shape(content)\n+            and not self._has_telegram_desktop_cjk_rich_garble_shape(content)\n             and self._content_fits_rich_limits(content)\n             and self._bot_supports_rich()\n         )"]},{"file":"tests/gateway/test_telegram_rich_messages.py","hunks":["@@ -24,6 +24,8 @@\n # Content exercising rich-only constructs: a heading, a real Markdown table,\n # and a task list. Pipes / brackets must survive untouched into the payload.\n RICH_CONTENT = \"## Results\\n\\n| Case | Status |\\n|---|---|\\n| rich | ✅ |\\n\\n- [x] table renders\"\n+CJK_RICH_CONTENT = \"## 持仓\\n\\n| 项目 | 状态 |\\n|---|---|\\n| 早盘 | 正常 |\"\n+ASTRAL_CJK_RICH_CONTENT = \"## Rare Han\\n\\n| glyph | status |\\n|---|---|\\n| \\U00030000 | ok |\"\n DANGEROUS_DETAILS_MATH = (\n     \"<details><summary>Complex proof</summary>\\n\\n\"\n     \"$$\\\\sum_{i=1}^{n} i = \\\\frac{n(n+1)}{2}$$\\n\\n\"","@@ -159,6 +161,28 @@ async def test_math_outside_details_still_uses_rich_send():\n     bot.send_message.assert_not_called()\n \n \n[redacted:email]\n+async def test_cjk_rich_content_skips_rich_send_to_avoid_tdesktop_garble():\n+    adapter = _make_adapter()\n+\n+    result = await adapter.send(\"12345\", CJK_RICH_CONTENT)\n+\n+    assert result.success is True\n+    adapter._bot.do_api_request.assert_not_called()\n+    adapter._bot.send_message.assert_awaited_once()\n+\n+\n[redacted:email]\n+async def test_astral_cjk_rich_content_skips_rich_send_to_avoid_tdesktop_garble():\n+    adapter = _make_adapter()\n+\n+    result = await adapter.send(\"12345\", ASTRAL_CJK_RICH_CONTENT)\n+\n+    assert result.success is True\n+    adapter._bot.do_api_request.assert_not_called()\n+    adapter._bot.send_message.assert_awaited_once()\n+\n+\n @pytest.mark.asyncio\n async def test_rich_messages_opt_out_uses_legacy_send_path():\n     adapter = _make_adapter(extra={\"rich_messages\": False})","@@ -281,13 +305,15 @@ async def test_oversized_content_skips_rich_and_chunks():\n async def test_rich_limit_is_characters_not_bytes():\n     \"\"\"Telegram's rich limit is UTF-8 characters, not encoded bytes.\"\"\"\n     adapter = _make_adapter()\n-    # Rich-eligible (table) so the content takes the rich path; the CJK body\n-    # is 20k chars / 60k UTF-8 bytes — over the byte count, under the char cap.\n-    cjk = \"| a | b |\\n|---|---|\\n\" + \"测\" * 20000  # 20k chars, ~60k UTF-8 bytes\n-    assert len(cjk.encode(\"utf-8\")) > TelegramAdapter.RICH_MESSAGE_MAX_BYTES\n-    assert len(cjk) <= TelegramAdapter.RICH_MESSAGE_MAX_CHARS\n+    # Rich-eligible (table) so the content takes the rich path; the accented\n+    # body is 20k chars / 40k UTF-8 bytes — over the byte count, under the\n+    # character cap. CJK is intentionally avoided here because affected\n+    # Telegram Desktop clients render CJK rich drafts incorrectly.\n+    accented = \"| a | b |\\n|---|---|\\n\" + \"é\" * 20000\n+    assert len(accented.encode(\"utf-8\")) > TelegramAdapter.RICH_MESSAGE_MAX_BYTES\n+    assert len(accented) <= TelegramAdapter.RICH_MESSAGE_MAX_CHARS\n \n-    result = await adapter.send(\"12345\", cjk)\n+    result = await adapter.send(\"12345\", accented)\n \n     assert result.success is True\n     bot = adapter._bot","@@ -528,6 +554,18 @@ async def test_rich_draft_happy_path_sends_raw_markdown():\n     adapter._bot.send_message_draft.assert_not_called()\n \n \n[redacted:email]\n+async def test_cjk_rich_content_skips_rich_draft_to_avoid_tdesktop_garble():\n+    adapter = _make_adapter()\n+    adapter._bot.do_api_request = AsyncMock(return_value=True)\n+\n+    result = await adapter.send_draft(\"12345\", draft_id=7, content=CJK_RICH_CONTENT)\n+\n+    assert result.success is True\n+    adapter._bot.do_api_request.assert_not_called()\n+    adapter._bot.send_message_draft.assert_awaited_once()\n+\n+\n @pytest.mark.asyncio\n async def test_rich_draft_capability_failure_falls_back_and_latches_off():\n     adapter = _make_adapter()","@@ -673,6 +711,19 @@ async def test_finalize_edit_plain_content_stays_legacy():\n     adapter._bot.edit_message_text.assert_awaited()\n \n \n[redacted:email]\n+async def test_finalize_edit_cjk_rich_content_stays_legacy_to_avoid_tdesktop_garble():\n+    adapter = _make_adapter()\n+\n+    result = await adapter.edit_message(\n+        \"12345\", \"555\", CJK_RICH_CONTENT, finalize=True,\n+    )\n+\n+    assert result.success is True\n+    adapter._bot.do_api_request.assert_not_called()\n+    adapter._bot.edit_message_text.assert_awaited_once()\n+\n+\n @pytest.mark.asyncio\n async def test_finalize_edit_rich_capability_error_falls_back_to_legacy():\n     \"\"\"A capability error on the rich edit latches rich off and falls back to\n"]}]},{"kind":"pr","number":62448,"title":"feat(telegram): add config option to disable CJK rich-message guard","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"7e42fc341a","number":0},{"kind":"pr","value":"#111831","number":111831},{"kind":"pr","value":"#56155","number":56155},{"kind":"pr","value":"#47653","number":47653}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":85197,"title":"fix(telegram): make CJK rich-message gate opt-out via extra.rich_cjk","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"7e42fc341a","number":0},{"kind":"pr","value":"#111831","number":111831},{"kind":"pr","value":"#56155","number":56155},{"kind":"pr","value":"#47653","number":47653}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":111831,"title":"fix(telegram): CJK content can opt in to native rich messages (salvage #56155)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cli-config.yaml.example","contributors/emails/[redacted:email]","hermes_cli/config_defaults.py","plugins/platforms/telegram/adapter.py","tests/gateway/test_telegram_rich_messages.py","website/docs/user-guide/messaging/telegram.md","website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/telegram.md"]}],"trust":{"default_tier":"verified","includes_suggested":false}}