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:
- Posting a
"..." placeholder via sendMessage
- Repeatedly calling
editMessageText (rate-limited to draft_update_interval_ms, default 1 s)
- 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 indicator —
editMessageText 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.
Summary
Add a new
StreamMode::Onvariant backed by the Telegram Bot API 9.5sendMessageDraftmethod, delivering native animated streaming responses in private chats without the currenteditMessageTextpolling 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
StreamModecurrently has two variants (Off,Partial).Partialsimulates streaming by:"..."placeholder viasendMessageeditMessageText(rate-limited todraft_update_interval_ms, default 1 s)editMessageText(HTML), with a delete+resend fallback for oversized textThis workaround has several UX gaps:
editMessageTextshows the pencil icon, signalling the message was edited rather than being generated livesendMessageDraftresolves all of these: it is purpose-built for streaming, animates natively on Telegram clients, and supportsparse_modeon each partial update.Proposed solution
Add
StreamMode::Ontosrc/config/schema.rs:In
src/channels/telegram.rs, activate the new path whenstream_mode == Onand the chat is a private chat (Telegram restrictssendMessageDraftto private chats):send_draft: callsendMessageDraftwithdraft_id = 1and initial text instead ofsendMessage. No placeholder message is created, so nomessage_idto track.update_draft: callsendMessageDraftwith the samedraft_idand accumulated text. Rate-limit floor can be removed or significantly relaxed since the method is designed for high-frequency updates.finalize_draft: send a regularsendMessagewith the final HTML-formatted response; the draft clears naturally once the real message arrives.cancel_draft: nodeleteMessageneeded — no placeholder message was created.supports_draft_updates: returnstruefor bothPartialandOn.Private-chat detection gates
On; group/supergroup/channel chats withstream_mode = "on"fall back toPartialbehaviour transparently.Non-goals / out of scope
src/agent/), provider-level token streaming (Provider::stream_chat_with_history), or other channels.sendMessageDraft— the API only supports private chats.Partialmode is unchanged and remains the streaming option for non-private chats.stream_mode = "on".Alternatives considered
Partialonly — continues to work but UX remains inferior: 1 s lag, edit icon, no mid-stream formatting.editMessageTextcall rate underPartial— Telegram rate limits would cause 429 errors; not viable without the dedicated endpoint.Partialentirely — would break group/channel streaming setups; keeping both variants is safer and backward-compatible.Acceptance criteria
StreamMode::Onis defined insrc/config/schema.rsand documented.sendMessageDraftis called insend_draft/update_draftfor private chats whenstream_mode = "on".stream_mode = "on"fall back toPartialpath without regression.sendMessage.cancel_draftdoes not calldeleteMessagefor theOnpath.Onprivate-chat path, group fallback, finalisation, and cancellation branches.docs/channels-reference.mdupdated to document the threestream_modevalues and their behaviour.Architecture impact
src/config/schema.rs— addOnvariant toStreamModesrc/channels/telegram.rs— newsendMessageDraftHTTP calls; updatedsend_draft,update_draft,cancel_draft,finalize_draft,supports_draft_updatessrc/channels/mod.rs— minor:handle_channel_messagedraft-ID handling may need adjustment (caller-generateddraft_idvs returnedmessage_id)docs/channels-reference.md— document newstream_mode = "on"behaviourNo 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.rsand theStreamModeenum. ExistingOffandPartialbehaviour is untouched.sendMessageDraftis available to all bots as of Bot API 9.5 — no capability gating needed. IfsendMessageDraftfails unexpectedly, fall back toPartialpath to preserve message delivery.Rollback: Revert
telegram.rsandschema.rschanges. No config migration needed; users onstream_mode = "on"would need to downgrade to"partial"manually, which is documented.Breaking change?
No
References
sendMessageDraftintroduced: https://core.telegram.org/bots/api#december-31-2025StreamMode:src/config/schema.rs:4617src/channels/telegram.rs(send_draft,update_draft,finalize_draft,cancel_draft)src/channels/mod.rs(handle_channel_message)Data hygiene: no personal or sensitive data included. All identifiers use neutral project-scoped placeholders.