Skip to content

[Feature]: Add StreamMode::On via Telegram sendMessageDraft for native streaming responses #2561

@Lemonawa

Description

@Lemonawa

Summary

Add a new StreamMode::On variant backed by the Telegram Bot API 9.5 sendMessageDraft method, delivering native animated streaming responses in private chats without the current editMessageText polling workaround.

Problem statement

Telegram Bot API 9.3 introduced sendMessageDraft (Dec 31 2025), and Bot API 9.5 (Mar 1 2026) opened it to all bots unconditionally. This method streams partial message text to a user with native Telegram animation — the user sees characters appear progressively, as if the bot is typing in real time.

ZeroClaw's StreamMode currently has two variants (Off, Partial). Partial simulates streaming by:

  1. Posting a "..." placeholder via sendMessage
  2. Repeatedly calling editMessageText (rate-limited to draft_update_interval_ms, default 1 s)
  3. Finalising with editMessageText (HTML), with a delete+resend fallback for oversized text

This workaround has several UX gaps:

  • 1-second update lag — rate limiting produces batched, jerky updates instead of smooth token-by-token flow
  • Edit indicatoreditMessageText shows the pencil icon, signalling the message was edited rather than being generated live
  • No parse_mode mid-stream — partial HTML formatting cannot be applied until finalisation
  • Delete+resend edge case — oversized finalisations break thread continuity

sendMessageDraft resolves all of these: it is purpose-built for streaming, animates natively on Telegram clients, and supports parse_mode on each partial update.

Proposed solution

Add StreamMode::On to src/config/schema.rs:

pub enum StreamMode {
    Off,      // complete response sent as a single message (existing)
    Partial,  // editMessageText polling simulation (existing)
    On,       // sendMessageDraft native streaming (new)
}

In src/channels/telegram.rs, activate the new path when stream_mode == On and the chat is a private chat (Telegram restricts sendMessageDraft to private chats):

  • send_draft: call sendMessageDraft with draft_id = 1 and initial text instead of sendMessage. No placeholder message is created, so no message_id to track.
  • update_draft: call sendMessageDraft with the same draft_id and accumulated text. Rate-limit floor can be removed or significantly relaxed since the method is designed for high-frequency updates.
  • finalize_draft: send a regular sendMessage with the final HTML-formatted response; the draft clears naturally once the real message arrives.
  • cancel_draft: no deleteMessage needed — no placeholder message was created.
  • supports_draft_updates: returns true for both Partial and On.

Private-chat detection gates On; group/supergroup/channel chats with stream_mode = "on" fall back to Partial behaviour transparently.

Non-goals / out of scope

  • No changes to the agent loop (src/agent/), provider-level token streaming (Provider::stream_chat_with_history), or other channels.
  • No group/supergroup/channel streaming via sendMessageDraft — the API only supports private chats.
  • Partial mode is unchanged and remains the streaming option for non-private chats.
  • No new config keys beyond stream_mode = "on".

Alternatives considered

  • Keep Partial only — continues to work but UX remains inferior: 1 s lag, edit icon, no mid-stream formatting.
  • Increase editMessageText call rate under Partial — Telegram rate limits would cause 429 errors; not viable without the dedicated endpoint.
  • Replace Partial entirely — would break group/channel streaming setups; keeping both variants is safer and backward-compatible.

Acceptance criteria

  • StreamMode::On is defined in src/config/schema.rs and documented.
  • sendMessageDraft is called in send_draft / update_draft for private chats when stream_mode = "on".
  • Group and non-private chats with stream_mode = "on" fall back to Partial path without regression.
  • Finalisation delivers the full HTML-formatted response as a proper sendMessage.
  • cancel_draft does not call deleteMessage for the On path.
  • Tests cover: On private-chat path, group fallback, finalisation, and cancellation branches.
  • docs/channels-reference.md updated to document the three stream_mode values and their behaviour.

Architecture impact

  • src/config/schema.rs — add On variant to StreamMode
  • src/channels/telegram.rs — new sendMessageDraft HTTP calls; updated send_draft, update_draft, cancel_draft, finalize_draft, supports_draft_updates
  • src/channels/mod.rs — minor: handle_channel_message draft-ID handling may need adjustment (caller-generated draft_id vs returned message_id)
  • docs/channels-reference.md — document new stream_mode = "on" behaviour

No changes required to: src/providers/, src/agent/, src/security/, src/gateway/, src/runtime/.

Risk and rollback

Risk: Low. Additive change scoped to src/channels/telegram.rs and the StreamMode enum. Existing Off and Partial behaviour is untouched. sendMessageDraft is available to all bots as of Bot API 9.5 — no capability gating needed. If sendMessageDraft fails unexpectedly, fall back to Partial path to preserve message delivery.

Rollback: Revert telegram.rs and schema.rs changes. No config migration needed; users on stream_mode = "on" would need to downgrade to "partial" manually, which is documented.

Breaking change?

No

References


Data hygiene: no personal or sensitive data included. All identifiers use neutral project-scoped placeholders.

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions