Bug
When a webhook subscription has deliver: telegram with a message_thread_id in deliver_extra targeting a private DM with topics enabled, every delivery fails with:
Telegram DM topic delivery requires a reply anchor; refusing to send outside the requested topic — trying plain-text fallback
[Webhook] Fallback send also failed: Telegram DM topic delivery requires a reply anchor; refusing to send outside the requested topic
Hermes accepts the webhook (HTTP 202, delivered: true) but the Hermes → Telegram leg silently never sends. Reliably reproducible.
Repro
- Create a webhook subscription targeting a private DM topic:
{
"huddle": {
"secret": "<route-secret>",
"deliver": "telegram",
"deliver_extra": {
"chat_id": "<your-user-id>",
"message_thread_id": "<topic-id-from-web.telegram.org-url>"
},
"prompt": "...",
"skills": [...]
}
}
- Configure a Telegram bot in Hermes (
TELEGRAM_BOT_TOKEN set).
- Fire any webhook test that posts to the route (e.g. signed
curl -X POST http://<ip>:8644/webhooks/huddle).
- Hermes accepts (
{"delivered": true, "status": 202}).
- Nothing arrives in Telegram. Logs show the "reply anchor" error.
Root Cause
gateway/platforms/webhook.py:918-924 builds metadata for the cross-platform Telegram dispatch as:
metadata = None
thread_id = extra.get("message_thread_id") or extra.get("thread_id")
if thread_id:
metadata = {"thread_id": thread_id}
This is passed to TelegramAdapter.send(), which calls _is_private_dm_topic_send() at gateway/platforms/telegram.py:591. That function returns True for any private DM with a thread_id and no opt-out flag, which triggers the anchor demand at gateway/platforms/telegram.py:611.
The webhook adapter has no way to provide telegram_reply_to_message_id (the only field that satisfies the guard), and no way to set the telegram_dm_topic_created_for_send or direct_messages_topic_id opt-out flags from deliver_extra.
Empirically, the bare message_thread_id form actually works for these chats — directly hitting api.telegram.org/bot<token>/sendMessage with chat_id=<user>&message_thread_id=<topic> succeeds and lands in the right topic. The guard is over-strict for this code path.
Related
The webhook adapter wasn't covered by any of those.
Workaround
Post-start patch hook that sets telegram_dm_topic_created_for_send: True alongside thread_id, which bypasses the over-cautious adapter guard:
metadata = {
"thread_id": thread_id,
"telegram_dm_topic_created_for_send": True,
}
Applied via a bind-mounted script that runs before hermes init (so it survives image updates).
Suggested Fix
Two reasonable options:
-
Surface the existing opt-out flags via deliver_extra — let webhook subscriptions explicitly say "I've validated this thread_id; bypass the anchor check" by passing one of the existing flags (telegram_dm_topic_created_for_send, direct_messages_topic_id, or telegram_reply_to_message_id) through to metadata. ~4 lines.
-
Relax the adapter guard for the webhook code path — operator-configured webhook routes are by definition pre-validated; the anchor check makes sense for synthetic/recovered sends, not for explicit static config. Detect the webhook source and skip the guard, matching how telegram_dm_topic_created_for_send already works for in-session creates.
Environment
- Hermes:
nousresearch/hermes-agent:latest (digest fac5c1306df3..., pushed 2026-05-27 13:13 UTC)
- Bot API context: private DM with topics enabled (Bot API 10+)
- 100% reproducible on the setup above
Bug
When a webhook subscription has
deliver: telegramwith amessage_thread_idindeliver_extratargeting a private DM with topics enabled, every delivery fails with:Hermes accepts the webhook (
HTTP 202,delivered: true) but the Hermes → Telegram leg silently never sends. Reliably reproducible.Repro
{ "huddle": { "secret": "<route-secret>", "deliver": "telegram", "deliver_extra": { "chat_id": "<your-user-id>", "message_thread_id": "<topic-id-from-web.telegram.org-url>" }, "prompt": "...", "skills": [...] } }TELEGRAM_BOT_TOKENset).curl -X POST http://<ip>:8644/webhooks/huddle).{"delivered": true, "status": 202}).Root Cause
gateway/platforms/webhook.py:918-924builds metadata for the cross-platform Telegram dispatch as:This is passed to
TelegramAdapter.send(), which calls_is_private_dm_topic_send()atgateway/platforms/telegram.py:591. That function returnsTruefor any private DM with a thread_id and no opt-out flag, which triggers the anchor demand atgateway/platforms/telegram.py:611.The webhook adapter has no way to provide
telegram_reply_to_message_id(the only field that satisfies the guard), and no way to set thetelegram_dm_topic_created_for_sendordirect_messages_topic_idopt-out flags fromdeliver_extra.Empirically, the bare
message_thread_idform actually works for these chats — directly hittingapi.telegram.org/bot<token>/sendMessagewithchat_id=<user>&message_thread_id=<topic>succeeds and lands in the right topic. The guard is over-strict for this code path.Related
tools/send_message_tool.py(cron path). Open since 2026-05-09.The webhook adapter wasn't covered by any of those.
Workaround
Post-start patch hook that sets
telegram_dm_topic_created_for_send: Truealongsidethread_id, which bypasses the over-cautious adapter guard:Applied via a bind-mounted script that runs before hermes init (so it survives image updates).
Suggested Fix
Two reasonable options:
Surface the existing opt-out flags via
deliver_extra— let webhook subscriptions explicitly say "I've validated this thread_id; bypass the anchor check" by passing one of the existing flags (telegram_dm_topic_created_for_send,direct_messages_topic_id, ortelegram_reply_to_message_id) through to metadata. ~4 lines.Relax the adapter guard for the webhook code path — operator-configured webhook routes are by definition pre-validated; the anchor check makes sense for synthetic/recovered sends, not for explicit static config. Detect the webhook source and skip the guard, matching how
telegram_dm_topic_created_for_sendalready works for in-session creates.Environment
nousresearch/hermes-agent:latest(digestfac5c1306df3..., pushed 2026-05-27 13:13 UTC)