feat(telegram): implement Bot API 10.0 guest mode (@mention from non-member chats) - #43049
feat(telegram): implement Bot API 10.0 guest mode (@mention from non-member chats)#43049elphamale wants to merge 5 commits into
Conversation
|
Thanks for the PR! The guest mode implementation looks well-thought-out, especially the buffering approach and the cleanup in . A few observations: Missing automated tests — The test plan is entirely manual, but this feature modifies several core gateway paths (, , , ) and parses raw API payloads via . Even a few unit tests would add confidence:
The stale entry on the denial path (line 143) is minor since rejection should be rare, but it's worth noting for completeness. Overall the approach is solid — deferring to processing completion ensures the user sees the full answer. Nice work! |
|
Thanks for the PR! The guest mode implementation looks well-thought-out, especially the buffering approach and the cleanup in Missing automated tests - The test plan is entirely manual, but this feature modifies several core gateway paths (
The stale Overall the approach is solid - deferring |
|
Addressed all four review points: Bug fix — on the denial path ( Tests — added
|
|
Reproduced locally against Hermes Telegram gateway. Observed behavior:
So the implementation now appears group-only in practice, despite the feature writeup implying broader private-chat support as well. If that distinction is intentional in Telegram Bot API 10, the PR should probably be narrowed; if not, this looks like an API/docs mismatch or an unsupported edge case worth tracking. I also validated the local gateway path by wiring |
|
Correction to my earlier comment on the PR: for private chats, Hermes does not appear to render a visible reply in the intermediary 1:1 chat. Instead, it sends the response directly to the other user's inbox as a separate message, which is why it looked like Guest Mode failed from the original chat. So the behavior split is:
This is worth capturing explicitly because it changes the bug from "Guest Mode absent in private chats" to "Guest Mode routes private-chat responses into the wrong conversational surface." |
|
Thanks for testing and for the correction, @abner-augusto — that distinction is useful. The Telegram docs say guest mode works in "any group or private chat" and that the bot can "Respond Directly with a message back to the chat where the interaction occurred." The private-chat routing you observed (reply landing in the bot's DM with the user, not the P2P thread) is Telegram's own behavior, not a bug in this implementation. Our code calls This is arguably reasonable UX — the user does get a reply — but the conversational surface mismatch is worth noting. I'll add a code comment documenting this behavior so future maintainers don't mistake it for a routing bug. |
|
Thanks — agreed on the private-chat surface distinction. I updated my findings in #46196 to narrow the remaining issue. I no longer think the core problem is that the The remaining bug appears to be delivery semantics: So I think the fix belongs mostly in the gateway runner, not in Telegram payload parsing:
I tracked the narrowed repro/root cause here: #46196 I’m going to take a pass at a PR for that unless you already have it covered. |
|
Thanks for the narrowed repro in #46196, @abner-augusto — the That said, this PR already covers it, though from the platform layer rather than the runner. Here's how:
The net effect is the same as gating at the runner level: no interim/streaming output reaches Telegram during processing, and the response is sent exactly once when processing is finished. If you end up going with a runner-level approach in a separate PR, the two implementations would be functionally equivalent — just at different layers. Either way, happy to coordinate so we don't duplicate effort. |
588d88b to
1445bf2
Compare
…member chats) PTB 22.6 doesn't natively support the `guest_message` update type or `answerGuestQuery` method introduced in Telegram Bot API 10.0. This patch adds full guest bot support as a backward-compatible layer on top of the existing adapter. Changes: - Register a `TypeHandler(Update, ...)` in group=1 to intercept `guest_message` updates, which PTB passes through via `update.api_kwargs` since they're unknown to its typed layer. - Add `"guest_message"` to `allowed_updates` in all three start-polling / webhook paths so Telegram actually delivers the updates. - Parse the raw `guest_message` payload via `Message.de_json()` and route it through the existing text-processing pipeline. - Store the `guest_query_id` per chat in `_pending_guest_queries` and mark the chat in `_guest_only_chats` for the duration of the request. - In `send()`, `send_draft()`, and `send_or_update_status()`, silently buffer/suppress all outgoing content for guest chats rather than attempting `sendMessage` (which would fail — the bot is not a member). `send()` keeps only the most recent write (final answer overwrites any earlier thinking/tool-progress text). - In `on_processing_complete()`, flush the buffered reply via `answerGuestQuery` using an `InlineQueryResultArticle`-shaped payload, then clean up all per-chat guest state. Flushing at completion means the user receives the full, coherent final answer rather than a mid-processing fragment. The Telegram API imposes no documented timeout on `guest_query_id` (unlike `answerInlineQuery`), so deferral until completion is safe. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… tests Fix a stale-state bug: when _should_process_message() rejects a guest message, _pending_guest_queries was popped but _guest_only_chats was not discarded. Any subsequent send() to that chat (e.g. after the bot joins the group later) would be silently suppressed forever. Add 12 unit tests covering the four scenarios raised in PR review: - guest_query_id extraction and state setup in _handle_guest_message_update - send() buffering instead of sendMessage for guest chats - on_processing_complete answerGuestQuery flush and full state cleanup - denial path: both _pending_guest_queries and _guest_only_chats cleared Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In P2P private chats, Telegram cannot post a bot message into the conversation, so it surfaces the answerGuestQuery reply in the bot's DM thread with the mentioning user instead of the original chat. This is Telegram API behaviour; our call is identical for groups and private chats. Add a code comment so future maintainers don't mistake the private-chat routing for a bug in this implementation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
send_voice / send_video / send_image_file / send_document: add the same
guest-chat guard already present on send() / send_draft() /
send_or_update_status(). Without it, the adapter attempts bot.send_voice()
on a chat the bot is not a member of, getting a Telegram API rejection.
Also fix edit_message: return early on the "__no_edit__" stream_consumer
sentinel instead of crashing int("__no_edit__").
Add _resolve_workspace_path() to translate /workspace/<file> Docker container
paths to their host equivalents before os.path.exists() is called.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously send_audio/send_image/send_document/send_video returned success=True (silent no-op) when the chat was a guest chat. The agent saw "success", reported "sent silently", and the file never arrived in the group. When the user complained the agent would explain the limitation, but the initial false-success response was confusing. Now these methods return success=False with an explicit error message instructing the agent to send to the user's private DMs and notify them in the group chat. The agent can then proactively communicate the guest-mode media limitation before the user has to ask. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
83569bc to
d3b8e51
Compare
|
Force-pushed to clean up branch history — previous push accidentally included commits from other in-progress PRs (Spotify auth, Docker path translation). Same content and intent, clean diff now: only |
…sor, fix accumulation In guest-chat mode (Bot API 10.0 answerGuestQuery), all send() calls during a turn are buffered and flushed as a single reply. Three bugs caused the delivered reply to contain garbled content: 1. Tool-use progress blocks (💻 terminal …) reached send() from send_progress_messages() and were stored in the guest reply buffer, polluting the final answer with intermediate streaming state. Fix: tag every adapter.send() / edit_message() call inside send_progress_messages() with metadata["tool_progress"] = True (run.py, _progress_metadata). telegram.py send() drops these immediately for guest chats before touching the buffer. 2. The streaming cursor " ▉" was stored verbatim in the buffer from the first streaming frame, embedding it mid-word in the final reply (e.g. "Відмін ▉но"). Fix: strip the trailing " ▉" / "▉" from content before buffering. 3. The stream consumer uses the __no_edit__ path for guest chats: it sends a short first frame, then _send_fallback_final delivers only the continuation (text after the first frame). Simple overwrite caused the buffer to hold just the continuation, losing the opening words. Fix: replace only when the new content starts with what is already buffered (cumulative streaming update); otherwise append (continuation or overflow chunk). Follow-up to NousResearch#43049 (guest mode / @mention support). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Upstream commit 5600105 moved gateway/platforms/telegram.py → plugins/platforms/telegram/adapter.py. This applies the equivalent of PRs NousResearch#43049 / NousResearch#49116 / NousResearch#49186 to the new path: - NousResearch#43049 (guest mode): _pending_guest_queries / _guest_only_chats / _guest_reply_buffer state; send() buffer block; TypeHandler registration; _handle_guest_message_update(); on_processing_complete() answerGuestQuery flush; media-method guards for send_voice / send_image_file / send_document / send_video / send_image. - NousResearch#49116 (sender_chat): _build_message_event() uses sender_chat.id / .title when from_user is None (channel-as-user posts in groups). - NousResearch#49186 (buffer quality): tool-progress drops (expect_edits/notify flags), cursor-strip before buffering, startswith-accumulation so cumulative streaming frames replace rather than double-append. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Closing as superseded by the clean guest-mode rebuild: #56476 (two-phase reply, Bot API 10.0 text-only) and #56477 ( This PR's foundational design — |
Relates to #21587.
Summary
Telegram Bot API 10.0 (May 2026) introduced Guest Bots — bots that can receive
@mentionupdates from groups they haven't joined and reply viaanswerGuestQuery. PTB 22.6 targets Bot API 9.5 and has no native support for the newguest_messageupdate type oranswerGuestQuerymethod. This PR adds full guest mode support as a backward-compatible layer on top of the existingTelegramAdapter.The feature is opt-in via
guest_mode: truein thetelegram:config section (mirrors the BotFather setting).What changed
Update ingestion — register a
TypeHandler(Update, _handle_guest_message_update)in handler group 1 to interceptguest_messageupdates, which PTB forwards viaupdate.api_kwargssince they're outside its typed schema. Add"guest_message"toallowed_updatesin all three connection paths (polling, webhook, reconnect polling).Routing —
_handle_guest_message_updateparses the raw payload viaMessage.de_json(), stores theguest_query_idin_pending_guest_queries[chat_id], marks the chat in_guest_only_chats, and routes the message through the existing text-processing pipeline unchanged.Send suppression — for the duration of processing,
send_draft()andsend_or_update_status()return silent success for guest chats (the bot is not a member so there is no message to edit or animate).send()buffers content rather than callingsendMessage, keeping only the most recent write so earlier thinking/tool-progress text is overwritten by the final answer.Flush on completion —
on_processing_complete()callsanswerGuestQuerywith anInlineQueryResultArticle-shaped payload containing the buffered final answer, then cleans up all per-chat guest state. Deferring to completion ensures the user sees the full, coherent response. The Telegram API documents no timeout forguest_query_id(unlike the 10-minute window onanswerInlineQuery), so this is safe even for long-running tool-use responses.Compatibility
guest_modeconfig or the presence of aguest_query_id.TypeHandlerimport fails (warning logged, feature disabled for that session).TELEGRAM_ALLOWED_USERS/TELEGRAM_GROUP_ALLOWED_CHATSauthorization gates.Test plan
tests/gateway/test_telegram_guest_mode.py— all passing/setguestmode)guest_mode: trueintelegram:config section🤖 Generated with Claude Code