# NousResearch/hermes-agent · Telegram rich Markdown hint gating and config

## Open now

- Nothing open on GitHub.

These issues and PRs concern whether Telegram's rich Markdown formatting instructions and prompt hints are shown or applied when the rich_messages config option is disabled. Multiple approaches to gating or defaulting this behavior are represented.

## TL;DR

- 5 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 3 PR(s) closed without merging.
- A fix was merged to main in PR #64423. Verify it addresses your case.
- **Recommended:** read PR #64423 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-64423`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.40`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #57122: Telegram platform hint pushes rich markdown constructs even when rich_messages is disabled (default)

## Prior & competing PRs

- **#64423: fix(telegram): respect rich_messages opt-in for pipe tables and prompt hint (#53825, #57132 salvage)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/prompt_builder.py`, `agent/system_prompt.py`, `plugins/platforms/telegram/adapter.py`, `tests/agent/test_prompt_builder.py`, `tests/agent/test_system_prompt.py`, `tests/gateway/test_telegram_rich_messages.py`
  - fix hunks:
  - `agent/prompt_builder.py`

    ```diff
    @@ -659,19 +659,7 @@ def format_steer_marker(steer_text: str) -> str:
             "Standard Markdown is automatically converted to Telegram formatting. "
             "Supported: **bold**, *italic*, ~~strikethrough~~, ||spoiler||, "
             "`inline code`, ```code blocks```, [links](url), and ## headers. "
    -        "Telegram now supports rich Markdown, so lean into it: whenever it "
    -        "makes the answer clearer or easier to scan, actively reach for real "
    -        "Markdown tables (pipe `| col | col |` syntax), bullet and numbered "
    -        "lists, task lists (`- [ ]` / `- [x]`), headings, nested blockquotes, "
    -        "collapsible details, footnotes/references, math/formulas (`$...$`, "
    -        "`$$...$$`), underline, subscript/superscript, marked (highlighted) "
    -        "text, and anchors. Default to structured formatting over dense "
    -        "paragraphs for any comparison, set of steps, key/value summary, or "
    -        "tabular data. Prefer real Markdown tables and task lists over "
    -        "hand-built bullet substitutes when presenting structured data; these "
    -        "degrade gracefully (tables become readable bullet groups) when rich "
    -        "rendering is unavailable, but advanced constructs like math and "
    -        "collapsible details may render as plain source text in that case. "
    +        "Prefer bullet lists and labeled key:value pairs for structured data. "
             "You can send media files natively: to deliver a file to the user, "
             "include MEDIA:/absolute/path/to/file in your response. Images "
             "(.png, .jpg, .webp) appear as photos, audio (.ogg) sends as voice "
    ```

    ```diff
    @@ -865,6 +853,27 @@ def format_steer_marker(steer_text: str) -> str:
         ),
     }
     
    +# Telegram rich-messages extension — only injected when the user has opted in
    +# to ``platforms.telegram.extra.rich_messages: true``.  The base
    +# PLATFORM_HINTS["telegram"] covers MarkdownV2-compatible constructs; this
    +# extension adds the Bot API 10.1 rich-Markdown guidance (tables, task lists,
    +# collapsible details, math, etc.).
    +TELEGRAM_RICH_MESSAGES_HINT = (
    +    "Telegram now supports rich Markdown, so lean into it: whenever it "
    +    "makes the answer clearer or easier to scan, actively reach for real "
    +    "Markdown tables (pipe `| col | col |` syntax), bullet and numbered "
    +    "lists, task lists (`- [ ]` / `- [x]`), headings, nested blockquotes, "
    +    "collapsible details, footnotes/references, math/formulas (`$...$`, "
    +    "`$$...$$`), underline, subscript/superscript, marked (highlighted) "
    +    "text, and anchors. Default to structured formatting over dense "
    +    "paragraphs for any comparison, set of steps, key/value summary, or "
    +    "tabular data. Prefer real Markdown tables and task lists over "
    +    "hand-built bullet substitutes when presenting structured data; these "
    +    "degrade gracefully (tables become readable bullet groups) when rich "
    +    "rendering is unavailable, but advanced constructs like math and "
    +    "collapsible details may render as plain source text in that case. "
    +)
    +
     # ---------------------------------------------------------------------------
     # Environment hints — execution-environment awareness for the agent.
     # Unlike PLATFORM_HINTS (which describe the messaging channel), these describe
    ```
  - `agent/system_prompt.py`

    ```diff
    @@ -40,6 +40,7 @@
         SKILLS_GUIDANCE,
         STEER_CHANNEL_NOTE,
         TASK_COMPLETION_GUIDANCE,
    +    TELEGRAM_RICH_MESSAGES_HINT,
         TOOL_USE_ENFORCEMENT_GUIDANCE,
         TOOL_USE_ENFORCEMENT_MODELS,
         drain_truncation_warnings,
    ```

    ```diff
    @@ -429,6 +430,20 @@ def build_system_prompt_parts(agent: Any, system_message: Optional[str] = None)
             except Exception:
                 pass
     
    +    # For Telegram: append the rich-messages extension only when the user has
    +    # opted in to ``platforms.telegram.extra.rich_messages: true``.  The base
    +    # hint covers MarkdownV2-compatible constructs; the extension adds Bot API
    +    # 10.1 guidance (tables, task lists, math, collapsible details, etc.).
    +    if platform_key == "telegram" and _default_hint:
    +        try:
    +            from hermes_cli.config import load_config_readonly
    +            _cfg = load_config_readonly()
    +            _tg_extra = ((_cfg.get("platforms") or {}).get("telegram") or {}).get("extra") or {}
    +            if _tg_extra.get("rich_messages"):
    +                _default_hint = _default_hint.rstrip() + " " + TELEGRAM_RICH_MESSAGES_HINT
    +        except Exception:
    +            pass  # Config read failure — fall back to base hint only
    +
         _effective_hint = _resolve_platform_hint(agent, platform_key, _default_hint)
         if platform_key == "tui" and _effective_hint:
             _effective_hint = _tui_embedded_pane_clarifier(_effective_hint)
    ```
  - `plugins/platforms/telegram/adapter.py`

    ```diff
    @@ -1479,33 +1479,9 @@ def _needs_rich_rendering(self, content: str) -> bool:
                 return True
             return False
     
    -    def _content_is_pipe_table_primary(self, content: str) -> bool:
    -        """True when pipe tables are the only rich construct in *content*.
    -
    -        Tables are auto-routed to ``sendRichMessage`` even when the full
    -        ``rich_messages`` opt-in is off — MarkdownV2 has no table syntax and
    -        the legacy path rewrites them into bullet lists, which reads like a
    -        regression when users enable Telegram Topics and expect native tables.
    -        Task lists, ``<details>``, and block math still require the full opt-in.
    -        """
    -        if not content or not any(
    -            _TABLE_SEPARATOR_RE.match(line) for line in content.splitlines()
    -        ):
    -            return False
    -        if re.search(r"(?m)^\s*[-*]\s+\[[ xX]\]\s+", content):
    -            return False
    -        if re.search(r"(?m)^<details\b|^</details>|^<summary\b|^</summary>", content):
    -            return False
    -        if "$$" in content:
    -            return False
    -        return True
    -
    -    def _rich_delivery_enabled(self, content: str) -> bool:
    -        """Whether rich delivery is allowed for this payload."""
    -        return bool(
    -            getattr(self, "_rich_messages_enabled", True)
    -            or self._content_is_pipe_table_primary(content)
    -        )
    +    def _rich_delivery_enabled(self) -> bool:
    +        """Whether rich delivery is allowed (``rich_messages`` opt-in)."""
    +        return bool(getattr(self, "_rich_messages_enabled", True))
     
         def _rich_eligible(self, content: str) -> bool:
             """Capability/content eligibility for rich, ignoring ``expect_edits``.
    ```

    ```diff
    @@ -1517,7 +1493,7 @@ def _rich_eligible(self, content: str) -> bool:
             FINAL edit should still upgrade to rich when the content warrants it.
             """
             return bool(
    -            self._rich_delivery_enabled(content)
    +            self._rich_delivery_enabled()
                 and not getattr(self, "_rich_send_disabled", False)
                 and content
                 and content.strip()
    ```
  - `tests/agent/test_prompt_builder.py`

    ```diff
    @@ -1092,15 +1092,22 @@ def test_telegram_hint_encourages_rich_markdown(self):
             hint = PLATFORM_HINTS["telegram"]
             lowered = hint.lower()
             assert "Telegram has NO table syntax" not in hint
    -        assert "rich markdown" in lowered
    -        assert "table" in lowered
    -        assert "task list" in lowered
    -        assert "math" in lowered
    +        # Base hint covers MarkdownV2-compatible constructs.
    +        assert "MEDIA:" in hint
    +        # Rich-messages extension (TELEGRAM_RICH_MESSAGES_HINT) covers the
    +        # Bot API 10.1 guidance; it is injected conditionally in
    +        # system_prompt.py when rich_messages: true.
    +        from agent.prompt_builder import TELEGRAM_RICH_MESSAGES_HINT
    +        rich_lowered = TELEGRAM_RICH_MESSAGES_HINT.lower()
    +        assert "rich markdown" in rich_lowered
    +        assert "table" in rich_lowered
    +        assert "task list" in rich_lowered
    +        assert "math" in rich_lowered
             # Hint should proactively steer toward structured formatting, not just
             # permit it: bullet + numbered lists for scannable, structured output.
    -        assert "bullet" in lowered
    -        assert "numbered" in lowered
    -        # Local media delivery guidance must remain intact.
    +        assert "bullet" in rich_lowered
    +        assert "numbered" in rich_lowered
    +        # Local media delivery guidance must remain intact in the base hint.
             assert "include MEDIA:" in hint
     
         def test_platform_hints_mattermost(self):
    ```
  - `tests/agent/test_system_prompt.py`

    ```diff
    @@ -99,3 +99,46 @@ def test_absent_without_tools(self, monkeypatch, tmp_path):
             monkeypatch.setenv("TERMINAL_CWD", str(tmp_path))
             agent = _make_agent(valid_tool_names=[], platform="cli")
             assert "coding agent" not in _stable_prompt(agent)
    +
    +
    +class TestTelegramRichMessagesHint:
    +    """Verify that TELEGRAM_RICH_MESSAGES_HINT is conditionally included."""
    +
    +    def test_base_hint_without_rich_messages(self, monkeypatch):
    +        """When rich_messages is False (default), only the base hint is used."""
    +        agent = _make_agent(platform="telegram")
    +        # Mock config to return rich_messages: false (default)
    +        with patch("hermes_cli.config.load_config_readonly") as mock_cfg:
    +            mock_cfg.return_value = {
    +                "platforms": {"telegram": {"extra": {"rich_messages": False}}}
    +            }
    +            stable = _stable_prompt(agent)
    +        # Base hint should be present
    +        assert "Standard Markdown is automatically converted" in stable
    +        # Rich-messages extension should NOT be present
    +        assert "lean into it" not in stable
    +        assert "task lists" not in stable
    +
    +    def test_rich_hint_with_rich_messages_enabled(self, monkeypatch):
    +        """When rich_messages is True, the rich-messages extension is appended."""
    +        agent = _make_agent(platform="telegram")
    +        with patch("hermes_cli.config.load_config_readonly") as mock_cfg:
    +            mock_cfg.return_value = {
    +                "platforms": {"telegram": {"extra": {"rich_messages": True}}}
    +            }
    +            stable = _stable_prompt(agent)
    +        # Base hint should be present
    +        assert "Standard Markdown is automatically converted" in stable
    +        # Rich-messages extension should be present
    +        assert "lean into it" in stable
    +        assert "task lists" in stable
    +        assert "math/formulas" in stable
    +
    +    def test_base_hint_without_config(self, monkeypatch):
    +        """When config has no telegram section, only base hint is used."""
    +        agent = _make_agent(platform="telegram")
    +        with patch("hermes_cli.config.load_config_readonly") as mock_cfg:
    +            mock_cfg.return_value = {}
    +            stable = _stable_prompt(agent)
    +        assert "Standard Markdown is automatically converted" in stable
    +        assert "lean into it" not in stable
    ```
  - `tests/gateway/test_telegram_rich_messages.py`

    ```diff
    @@ -582,21 +582,20 @@ async def test_notification_opt_in_drops_disable_flag():
     
     
     @pytest.mark.asyncio
    -async def test_table_only_uses_rich_when_rich_messages_opt_out():
    -    """Pipe tables auto-route to sendRichMessage even without the full opt-in."""
    +async def test_table_only_uses_legacy_when_rich_messages_opt_out():
    +    """Pipe tables respect the rich_messages: false config and stay on legacy."""
         adapter = _make_adapter(extra={"rich_messages": False})
     
         result = await adapter.send("12345", TABLE_ONLY_CONTENT)
     
         assert result.success is True
    -    api_kwargs = _rich_api_kwargs(adapter)
    -    assert api_kwargs["rich_message"]["markdown"] == TABLE_ONLY_CONTENT
    -    adapter._bot.send_message.assert_not_called()
    +    adapter._bot.do_api_request.assert_not_called()
    +    adapter._bot.send_message.assert_awaited()
     
     
     @pytest.mark.asyncio
    -async def test_table_only_uses_rich_with_default_config():
    -    """Default config keeps task lists on legacy but upgrades bare tables."""
    +async def test_table_only_uses_legacy_with_default_config():
    +    """Default config (rich_messages unset → False) keeps tables on legacy path."""
         config = PlatformConfig(enabled=True, token="[redacted:secret-assignment]")
         adapter = TelegramAdapter(config)
         bot = MagicMock()
    ```

    ```diff
    @@ -608,13 +607,13 @@ async def test_table_only_uses_rich_with_default_config():
         result = await adapter.send("12345", TABLE_ONLY_CONTENT)
     
         assert result.success is True
    -    bot.do_api_request.assert_awaited_once()
    -    bot.send_message.assert_not_called()
    +    bot.do_api_request.assert_not_called()
    +    bot.send_message.assert_awaited()
     
     
     @pytest.mark.asyncio
    -async def test_dm_topic_resumed_send_uses_rich_for_table_without_reply_anchor():
    -    """Resumed/synthetic DM-topic sends route tables via direct_messages_topic_id."""
    +async def test_dm_topic_resumed_send_uses_legacy_for_table_when_opt_out():
    +    """Resumed DM-topic sends respect rich_messages: false for tables."""
         adapter = _make_adapter(extra={"rich_messages": False})
     
         result = await adapter.send(
    ```

    ```diff
    @@ -628,14 +627,13 @@ async def test_dm_topic_resumed_send_uses_rich_for_table_without_reply_anchor():
         )
     
         assert result.success is True
    -    api_kwargs = _rich_api_kwargs(adapter)
    -    assert api_kwargs["direct_messages_topic_id"] == 20189
    -    assert "reply_parameters" not in api_kwargs
    -    assert api_kwargs["rich_message"]["markdown"] == TABLE_ONLY_CONTENT
    +    adapter._bot.do_api_request.assert_not_called()
    +    adapter._bot.send_message.assert_awaited()
     
     
     @pytest.mark.asyncio
    -async def test_finalize_edit_rich_includes_forum_topic_routing():
    +async def test_finalize_edit_legacy_includes_forum_topic_routing():
    +    """With rich_messages: false, table edits use legacy path with topic routing."""
         adapter = _make_adapter(extra={"rich_messages": False})
     
         result = await adapter.edit_message(
    ```

    ```diff
    @@ -647,9 +645,8 @@ async def test_finalize_edit_rich_includes_forum_topic_routing():
         )
     
         assert result.success is True
    -    api_kwargs = _rich_edit_kwargs(adapter)
    -    assert api_kwargs["message_thread_id"] == 5
    -    assert api_kwargs["rich_message"]["markdown"] == TABLE_ONLY_CONTENT
    +    adapter._bot.do_api_request.assert_not_called()
    +    adapter._bot.edit_message_text.assert_awaited()
     
     
     @pytest.mark.asyncio
    ```
- **#57132: fix(agent): gate Telegram rich-Markdown hint on rich_messages config**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#64423`, `#53825`
- **#57148: fix(telegram): gate rich markdown prompt hint**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#64423`, `#57132`
- **#57210: fix(agent): remove rich Markdown instruction from Telegram platform hint**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#64423`, `#57122`, `#57132`

## Fix status

- Merged to main in PR #64423 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-14`.
- Subsystem: `gateway`
