diff --git a/gateway/config.py b/gateway/config.py index bdf5ede52d7..94136291284 100644 --- a/gateway/config.py +++ b/gateway/config.py @@ -358,12 +358,13 @@ class StreamingConfig: # Transport selection: # "auto" — prefer native streaming-draft updates when the platform # supports them (Telegram sendMessageDraft, Bot API 9.5+); - # fall back to edit-based when not. Recommended. + # fall back to edit-based when not. # "draft" — explicitly request native drafts; falls back to edit when # the platform/chat doesn't support them. - # "edit" — progressive editMessageText only (legacy behaviour). + # "edit" — progressive editMessageText only (legacy/default + # behaviour). # "off" — disable streaming entirely. - transport: str = "auto" + transport: str = "edit" edit_interval: float = DEFAULT_STREAMING_EDIT_INTERVAL buffer_threshold: int = DEFAULT_STREAMING_BUFFER_THRESHOLD cursor: str = DEFAULT_STREAMING_CURSOR @@ -392,7 +393,7 @@ class StreamingConfig: return cls() return cls( enabled=_coerce_bool(data.get("enabled"), False), - transport=data.get("transport", "auto"), + transport=data.get("transport", "edit"), edit_interval=_coerce_float( data.get("edit_interval"), DEFAULT_STREAMING_EDIT_INTERVAL, ), diff --git a/gateway/run.py b/gateway/run.py index 7de7a111964..26a1903412e 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -14809,7 +14809,7 @@ class GatewayRunner: cursor=_effective_cursor, buffer_only=_buffer_only, fresh_final_after_seconds=_fresh_final_secs, - transport=_scfg.transport or "auto", + transport=_scfg.transport or "edit", chat_type=getattr(source, "chat_type", "") or "", ) _stream_consumer = GatewayStreamConsumer( @@ -15632,7 +15632,7 @@ class GatewayRunner: cursor=_effective_cursor, buffer_only=_buffer_only, fresh_final_after_seconds=_fresh_final_secs, - transport=_scfg.transport or "auto", + transport=_scfg.transport or "edit", chat_type=getattr(source, "chat_type", "") or "", ) _stream_consumer = GatewayStreamConsumer( diff --git a/gateway/stream_consumer.py b/gateway/stream_consumer.py index 3c761d528ab..d802bc00b13 100644 --- a/gateway/stream_consumer.py +++ b/gateway/stream_consumer.py @@ -65,9 +65,9 @@ class StreamConsumerConfig: # when the adapter + chat supports it; fall back to edit. # "draft" — explicitly request native draft streaming; fall back to # edit when unsupported. - # "edit" — progressive editMessageText (legacy behavior). + # "edit" — progressive editMessageText (legacy/default behavior). # "off" — handled by the gateway before the consumer is even built. - transport: str = "auto" + transport: str = "edit" # Hint for the consumer about the originating chat type (e.g. "dm", # "group", "supergroup", "forum"). Used to gate native draft streaming, # which is platform-specific (Telegram drafts are DM-only). @@ -846,7 +846,7 @@ class GatewayStreamConsumer: the chat type (e.g. Telegram drafts are DM-only) and platform-version gates (e.g. python-telegram-bot 22.6+). """ - transport = (self.cfg.transport or "auto").lower() + transport = (self.cfg.transport or "edit").lower() if transport == "edit": return False # "off" is filtered upstream by the gateway; treat as edit defensively. diff --git a/tests/gateway/test_config.py b/tests/gateway/test_config.py index cf197bd6f7f..fdd5e72f680 100644 --- a/tests/gateway/test_config.py +++ b/tests/gateway/test_config.py @@ -164,6 +164,10 @@ class TestSessionResetPolicy: class TestStreamingConfig: + def test_defaults_to_edit_transport(self): + restored = StreamingConfig.from_dict({"enabled": "true"}) + assert restored.transport == "edit" + def test_from_dict_coerces_quoted_false_enabled(self): restored = StreamingConfig.from_dict({"enabled": "false"}) assert restored.enabled is False diff --git a/tests/gateway/test_stream_consumer_draft.py b/tests/gateway/test_stream_consumer_draft.py index bab8e20fd35..23d12b03913 100644 --- a/tests/gateway/test_stream_consumer_draft.py +++ b/tests/gateway/test_stream_consumer_draft.py @@ -80,6 +80,11 @@ def _make_draft_capable_adapter( class TestDraftTransportSelection: """Verify _resolve_draft_streaming picks the right transport.""" + def test_default_transport_stays_on_edit(self): + adapter = _make_draft_capable_adapter() + consumer = GatewayStreamConsumer(adapter, "12345", StreamConsumerConfig(chat_type="dm")) + assert consumer._resolve_draft_streaming() is False + def test_auto_dm_with_draft_capable_adapter_picks_draft(self): adapter = _make_draft_capable_adapter() cfg = StreamConsumerConfig(transport="auto", chat_type="dm") diff --git a/website/docs/user-guide/messaging/telegram.md b/website/docs/user-guide/messaging/telegram.md index 95d9313c05e..056637e6238 100644 --- a/website/docs/user-guide/messaging/telegram.md +++ b/website/docs/user-guide/messaging/telegram.md @@ -611,7 +611,7 @@ To find a topic's `thread_id`, open the topic in Telegram Web or Desktop and loo - **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). - **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. -- **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. +- **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. ### Streaming transport (`gateway.streaming.transport`) @@ -619,9 +619,9 @@ When streaming is enabled (`gateway.streaming.enabled: true`), Hermes picks one | Value | Behaviour | |---|---| -| `auto` (default) | Native draft streaming on supported chats (currently Telegram DMs); legacy edit-based path otherwise. Falls back gracefully if a draft frame fails. | +| `auto` | Native draft streaming on supported chats (currently Telegram DMs); legacy edit-based path otherwise. Falls back gracefully if a draft frame fails. | | `draft` | Force native drafts. Logs a downgrade and falls back to edit if the chat doesn't support drafts (e.g. groups/topics). | -| `edit` | Legacy progressive `editMessageText` polling for every chat type. | +| `edit` (default) | Legacy progressive `editMessageText` polling for every chat type. | | `off` | Disable streaming entirely (final reply only, no progressive updates). | In `~/.hermes/config.yaml`: @@ -630,10 +630,12 @@ In `~/.hermes/config.yaml`: gateway: streaming: enabled: true - transport: auto # auto | draft | edit | off + transport: edit # edit | auto | draft | off ``` -**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. +**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. + +**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. **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.