{"repo":"NousResearch/hermes-agent","public_id":"26157","display_label":"Telegram DM streaming transport default fix","summary":"Multiple fixes and reports addressing a regression where draft/native streaming was incorrectly selected as the default transport for Telegram DMs, causing visible content flicker and bubble collapse. Changes restore the edit-based streaming transport as the default.","subsystem":"gateway","counts":{"members":5,"open_issues":1,"open_issues_distinct":1,"open_prs":1,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":28476,"merged_at":"2026-05-19T04:51:39+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-05-19T04:51:39+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.33599999999999997},"fix_edges":[{"issue_number":26157,"pr_number":28476,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-05-19T04:51:39+00:00"}],"members":[{"kind":"issue","number":26157,"title":"ux(telegram): sendMessageDraft (native draft streaming) causes visible content flicker/rollback in DM responses","url":null,"state":"closed"},{"kind":"issue","number":54817,"title":"Regression: Telegram draft streaming is auto-selected again and causes bubble collapse/flicker","url":null,"state":"open"},{"kind":"pr","number":26357,"title":"fix(telegram): default DM streaming back to edit transport","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#28476","number":28476}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":28476,"title":"fix(telegram): default streaming transport back to edit (closes #26157)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/config.py","gateway/run.py","gateway/stream_consumer.py","tests/gateway/test_config.py","tests/gateway/test_stream_consumer_draft.py","website/docs/user-guide/messaging/telegram.md"],"fix_hunks":[{"file":"gateway/config.py","hunks":["@@ -358,12 +358,13 @@ class StreamingConfig:\n     # Transport selection:\n     #   \"auto\"  — prefer native streaming-draft updates when the platform\n     #             supports them (Telegram sendMessageDraft, Bot API 9.5+);\n-    #             fall back to edit-based when not.  Recommended.\n+    #             fall back to edit-based when not.\n     #   \"draft\" — explicitly request native drafts; falls back to edit when\n     #             the platform/chat doesn't support them.\n-    #   \"edit\"  — progressive editMessageText only (legacy behaviour).\n+    #   \"edit\"  — progressive editMessageText only (legacy/default\n+    #             behaviour).\n     #   \"off\"   — disable streaming entirely.\n-    transport: str = \"auto\"\n+    transport: str = \"edit\"\n     edit_interval: float = DEFAULT_STREAMING_EDIT_INTERVAL\n     buffer_threshold: int = DEFAULT_STREAMING_BUFFER_THRESHOLD\n     cursor: str = DEFAULT_STREAMING_CURSOR","@@ -392,7 +393,7 @@ def from_dict(cls, data: Dict[str, Any]) -> \"StreamingConfig\":\n             return cls()\n         return cls(\n             enabled=_coerce_bool(data.get(\"enabled\"), False),\n-            transport=data.get(\"transport\", \"auto\"),\n+            transport=data.get(\"transport\", \"edit\"),\n             edit_interval=_coerce_float(\n                 data.get(\"edit_interval\"), DEFAULT_STREAMING_EDIT_INTERVAL,\n             ),"]},{"file":"gateway/run.py","hunks":["@@ -14809,7 +14809,7 @@ def _run_still_current() -> bool:\n                         cursor=_effective_cursor,\n                         buffer_only=_buffer_only,\n                         fresh_final_after_seconds=_fresh_final_secs,\n-                        transport=_scfg.transport or \"auto\",\n+                        transport=_scfg.transport or \"edit\",\n                         chat_type=getattr(source, \"chat_type\", \"\") or \"\",\n                     )\n                     _stream_consumer = GatewayStreamConsumer(","@@ -15632,7 +15632,7 @@ def run_sync():\n                             cursor=_effective_cursor,\n                             buffer_only=_buffer_only,\n                             fresh_final_after_seconds=_fresh_final_secs,\n-                            transport=_scfg.transport or \"auto\",\n+                            transport=_scfg.transport or \"edit\",\n                             chat_type=getattr(source, \"chat_type\", \"\") or \"\",\n                         )\n                         _stream_consumer = GatewayStreamConsumer("]},{"file":"gateway/stream_consumer.py","hunks":["@@ -65,9 +65,9 @@ class StreamConsumerConfig:\n     #             when the adapter + chat supports it; fall back to edit.\n     #   \"draft\" — explicitly request native draft streaming; fall back to\n     #             edit when unsupported.\n-    #   \"edit\"  — progressive editMessageText (legacy behavior).\n+    #   \"edit\"  — progressive editMessageText (legacy/default behavior).\n     #   \"off\"   — handled by the gateway before the consumer is even built.\n-    transport: str = \"auto\"\n+    transport: str = \"edit\"\n     # Hint for the consumer about the originating chat type (e.g. \"dm\",\n     # \"group\", \"supergroup\", \"forum\").  Used to gate native draft streaming,\n     # which is platform-specific (Telegram drafts are DM-only).","@@ -846,7 +846,7 @@ def _resolve_draft_streaming(self) -> bool:\n         the chat type (e.g. Telegram drafts are DM-only) and platform-version\n         gates (e.g. python-telegram-bot 22.6+).\n         \"\"\"\n-        transport = (self.cfg.transport or \"auto\").lower()\n+        transport = (self.cfg.transport or \"edit\").lower()\n         if transport == \"edit\":\n             return False\n         # \"off\" is filtered upstream by the gateway; treat as edit defensively."]},{"file":"tests/gateway/test_config.py","hunks":["@@ -164,6 +164,10 @@ def test_from_dict_coerces_quoted_false_notify(self):\n \n \n class TestStreamingConfig:\n+    def test_defaults_to_edit_transport(self):\n+        restored = StreamingConfig.from_dict({\"enabled\": \"true\"})\n+        assert restored.transport == \"edit\"\n+\n     def test_from_dict_coerces_quoted_false_enabled(self):\n         restored = StreamingConfig.from_dict({\"enabled\": \"false\"})\n         assert restored.enabled is False"]},{"file":"tests/gateway/test_stream_consumer_draft.py","hunks":["@@ -80,6 +80,11 @@ async def _send_draft(*, chat_id, draft_id, content, metadata=None):\n class TestDraftTransportSelection:\n     \"\"\"Verify _resolve_draft_streaming picks the right transport.\"\"\"\n \n+    def test_default_transport_stays_on_edit(self):\n+        adapter = _make_draft_capable_adapter()\n+        consumer = GatewayStreamConsumer(adapter, \"12345\", StreamConsumerConfig(chat_type=\"dm\"))\n+        assert consumer._resolve_draft_streaming() is False\n+\n     def test_auto_dm_with_draft_capable_adapter_picks_draft(self):\n         adapter = _make_draft_capable_adapter()\n         cfg = StreamConsumerConfig(transport=\"auto\", chat_type=\"dm\")"]},{"file":"website/docs/user-guide/messaging/telegram.md","hunks":["@@ -611,7 +611,7 @@ To find a topic's `thread_id`, open the topic in Telegram Web or Desktop and loo\n \n - **Bot API 9.4 (Feb 2026):** Private Chat Topics — bots can create forum topics in 1-on-1 DM chats via `createForumTopic`. Hermes uses this for two distinct features: operator-curated [Private Chat Topics](#private-chat-topics-bot-api-94) (config-driven, fixed topic list) and user-driven [Multi-session DM mode](#multi-session-dm-mode-topic) (activated by `/topic`, unlimited user-created topics).\n - **Privacy policy:** Telegram now requires bots to have a privacy policy. Set one via BotFather with `/setprivacy_policy`, or Telegram may auto-generate a placeholder. This is particularly important if your bot is public-facing.\n-- **Bot API 9.5 (Mar 2026): Native streaming via `sendMessageDraft`.** Hermes uses Telegram's native streaming-draft API to render an animated preview of the agent's reply as tokens arrive in private chats. Drops the per-edit jitter you used to see with the legacy `editMessageText` polling path on slow models.\n+- **Bot API 9.5 (Mar 2026): Native streaming via `sendMessageDraft`.** Hermes supports Telegram's native streaming-draft API as an opt-in transport for private chats. The default remains the legacy `editMessageText` path because draft previews can visibly collapse and re-render on some Telegram clients.\n \n ### Streaming transport (`gateway.streaming.transport`)\n ","@@ -619,9 +619,9 @@ When streaming is enabled (`gateway.streaming.enabled: true`), Hermes picks one\n \n | Value | Behaviour |\n |---|---|\n-| `auto` (default) | Native draft streaming on supported chats (currently Telegram DMs); legacy edit-based path otherwise. Falls back gracefully if a draft frame fails. |\n+| `auto` | Native draft streaming on supported chats (currently Telegram DMs); legacy edit-based path otherwise. Falls back gracefully if a draft frame fails. |\n | `draft` | Force native drafts. Logs a downgrade and falls back to edit if the chat doesn't support drafts (e.g. groups/topics). |\n-| `edit` | Legacy progressive `editMessageText` polling for every chat type. |\n+| `edit` (default) | Legacy progressive `editMessageText` polling for every chat type. |\n | `off` | Disable streaming entirely (final reply only, no progressive updates). |\n \n In `~/.hermes/config.yaml`:","@@ -630,10 +630,12 @@ In `~/.hermes/config.yaml`:\n gateway:\n   streaming:\n     enabled: true\n-    transport: auto    # auto | draft | edit | off\n+    transport: edit    # edit | auto | draft | off\n ```\n \n-**What you'll see in DMs with `auto` (default)** — when the agent generates a reply, Telegram shows an animated draft preview that updates token-by-token. When the reply finishes, it's delivered as a regular message and the draft preview clears naturally on the client. Drafts have no message id, so the final answer is what stays in your chat history.\n+**What you'll see in DMs with `edit` (default)** — the gateway sends a normal preview message and progressively updates it via `editMessageText`, avoiding Telegram's draft-preview collapse/rollback effect.\n+\n+**What you'll see in DMs with `auto` or `draft`** — Telegram shows an animated draft preview that updates token-by-token. When the reply finishes, it's delivered as a regular message and the draft preview clears naturally on the client. Drafts have no message id, so the final answer is what stays in your chat history.\n \n **What about groups, supergroups, forum topics?** Telegram restricts `sendMessageDraft` to private chats (DMs). The gateway transparently falls back to the edit-based path for everything else — same UX as before.\n \n"]}]},{"kind":"pr","number":54883,"title":"fix(gateway): keep streaming transport default on edit","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}