You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When a cron's delivery.to targets a Telegram DM topic (e.g., 666972334:topic:76641) and Telegram rejects the topic-routed sendMessage with 400: Bad Request: message thread not found, the gateway falls back to a threadless sendMessage to the bare chat (the documented behavior introduced by #2122). The fallback succeeds, and the cron's lastDeliveryStatus is recorded as "delivered" and lastDelivered: true in ~/.openclaw/cron/jobs-state.json — identical to a fully successful, correctly-routed delivery.
Operators cannot distinguish a correctly-routed delivery from a fallback-misrouted one without manually grepping openclaw.log for telegram/api ERROR lines around each cron fire. For background crons (briefings, reminders) that humans don't actively watch, this hides routing regressions for arbitrary durations.
Repro (encountered today)
openclaw 2026.5.7 (eeef486), Linux, Node 22.22.2.
Personal DM chat type private (per getChat).
Two crons routed to 666972334:topic:76641 (a "Daily Briefing" reply-thread anchor in the user's DM):
Daily Tech Briefing (30 6 * * * Asia/Jakarta) — delivered correctly to the topic at 06:30 WIB on 2026-05-08.
Evening Wrap-up (0 22 * * *) — fired 22:00 WIB the same day; Telegram returned 400: Bad Request: message thread not found; gateway fell back to bare DM. User noticed only because they happened to look at Telegram.
Direct probes against Bot API confirm Telegram has, sometime between 06:30 and 22:00 WIB on 2026-05-08, stopped accepting message_thread_id for this private DM for any value (probed 76641, 2514 — a fresh user message in the chat — both rejected with the same 400). Bare DM sendMessage and reply_parameters.message_id (no thread) both still succeed.
The Telegram-side change is plausibly part of an ongoing tightening (sibling: #79408 filed earlier today, covers the forum scope where DM-style threadless retry is not enabled and replies vanish entirely). This issue covers the DM scope where the retry is enabled and silently misroutes.
Gateway log excerpt (22:01 WIB fire)
2026-05-08T22:01:00.625+07:00 ERROR telegram/api
telegram message failed: Call to 'sendMessage' failed!
(400: Bad Request: message thread not found)
No second ERROR line for the bare-DM retry — that succeeds and is logged at INFO. jobs-state.json after this fire:
Identical to yesterday's successful topic-routed delivery.
Why this is a bug, not a feature
#2122 ("feat(telegram): fallback to main chat when topic delivery fails") intentionally added the fallback to keep the user's content delivered when topic state is broken — that's reasonable. But the same change should have surfaced the fact that the fallback was triggered, so operators have a signal to act on. Right now the only signal is a single ERROR log line that gets buried in routine output, and cron runs <id> / cron list reports the job as fully ok.
Comparable upstream behavior in this repo: in extensions/telegram/src/draft-stream.ts and extensions/telegram/src/send.ts threadless retry is logged as a recoverable warning, not silently absorbed. The cron-state writer should pick that up.
Suggested fix (small)
In whichever cron-runner reports back lastDeliveryStatus, distinguish the threadless-fallback case from a clean delivery. Two minimally-invasive options:
New status string.lastDeliveryStatus: "delivered-fallback" (or "delivered-misroute") when sendTelegramWithThreadFallback() had to retry without message_thread_id. cron list / cron runs then surface a yellow indicator instead of ok.
Persistent counter. Add consecutiveFallbackRoutes next to consecutiveErrors / consecutiveSkipped. Increments on threadless-retry success, resets on clean delivery. Doctor surfaces consecutiveFallbackRoutes >= 1 as a warning.
Either signal is enough to give the operator a chance to notice within one cron cycle instead of "whenever I happen to scroll to that topic in Telegram".
Pre-condition: the user's Telegram client treats <chat_id> as a private chat with reply-thread "topics" (not a forum supergroup), and Telegram has tightened message_thread_id for that chat (current behavior as of 2026-05-08). To reproduce on demand, just point the cron at any non-existent message_thread_id for a DM and watch the cron-state report delivered.
Real-world impact
One Evening Wrap-up cycle silently misrouted; user discovered only via visual Telegram check ~10 minutes later.
Two crons (Daily Tech Briefing, Evening Wrap-up) would have continued silently misrouting on every fire indefinitely. Topic-routed reminder crons (separate Reminder topic, ~5 of them) would have done the same once they next fired.
Today's host-side mitigation: stripped the :topic:<id> suffix from both crons via openclaw cron edit --to <bare> so future fires deliver to bare DM without the misleading delivered signal. This is a workaround for the silent-mismark, not the routing — until upstream surfaces the fallback, the operator has no way to know whether a topic-routed cron is actually hitting the topic.
Filed because of standing rule: silent failures > loud failures, and "delivered" being the same string for a correct delivery and a misroute makes silent failures the default.
Summary
When a cron's
delivery.totargets a Telegram DM topic (e.g.,666972334:topic:76641) and Telegram rejects the topic-routedsendMessagewith400: Bad Request: message thread not found, the gateway falls back to a threadlesssendMessageto the bare chat (the documented behavior introduced by #2122). The fallback succeeds, and the cron'slastDeliveryStatusis recorded as"delivered"andlastDelivered: truein~/.openclaw/cron/jobs-state.json— identical to a fully successful, correctly-routed delivery.Operators cannot distinguish a correctly-routed delivery from a fallback-misrouted one without manually grepping
openclaw.logfortelegram/api ERRORlines around each cron fire. For background crons (briefings, reminders) that humans don't actively watch, this hides routing regressions for arbitrary durations.Repro (encountered today)
openclaw 2026.5.7 (eeef486), Linux, Node 22.22.2.private(pergetChat).666972334:topic:76641(a "Daily Briefing" reply-thread anchor in the user's DM):Daily Tech Briefing(30 6 * * *Asia/Jakarta) — delivered correctly to the topic at 06:30 WIB on 2026-05-08.Evening Wrap-up(0 22 * * *) — fired 22:00 WIB the same day; Telegram returned400: Bad Request: message thread not found; gateway fell back to bare DM. User noticed only because they happened to look at Telegram.message_thread_idfor this private DM for any value (probed76641,2514— a fresh user message in the chat — both rejected with the same 400). Bare DMsendMessageandreply_parameters.message_id(no thread) both still succeed.The Telegram-side change is plausibly part of an ongoing tightening (sibling: #79408 filed earlier today, covers the forum scope where DM-style threadless retry is not enabled and replies vanish entirely). This issue covers the DM scope where the retry is enabled and silently misroutes.
Gateway log excerpt (22:01 WIB fire)
No second ERROR line for the bare-DM retry — that succeeds and is logged at INFO.
jobs-state.jsonafter this fire:{ "1c590924-b0ea-4619-b542-53166fedc499": { "state": { "lastRunStatus": "ok", "lastStatus": "ok", "lastDeliveryStatus": "delivered", "lastDelivered": true, ... } } }Identical to yesterday's successful topic-routed delivery.
Why this is a bug, not a feature
#2122("feat(telegram): fallback to main chat when topic delivery fails") intentionally added the fallback to keep the user's content delivered when topic state is broken — that's reasonable. But the same change should have surfaced the fact that the fallback was triggered, so operators have a signal to act on. Right now the only signal is a single ERROR log line that gets buried in routine output, andcron runs <id>/cron listreports the job as fullyok.Comparable upstream behavior in this repo: in
extensions/telegram/src/draft-stream.tsandextensions/telegram/src/send.tsthreadless retry is logged as a recoverable warning, not silently absorbed. The cron-state writer should pick that up.Suggested fix (small)
In whichever cron-runner reports back
lastDeliveryStatus, distinguish the threadless-fallback case from a clean delivery. Two minimally-invasive options:lastDeliveryStatus: "delivered-fallback"(or"delivered-misroute") whensendTelegramWithThreadFallback()had to retry withoutmessage_thread_id.cron list/cron runsthen surface a yellow indicator instead ofok.consecutiveFallbackRoutesnext toconsecutiveErrors/consecutiveSkipped. Increments on threadless-retry success, resets on clean delivery. Doctor surfacesconsecutiveFallbackRoutes >= 1as a warning.Either signal is enough to give the operator a chance to notice within one cron cycle instead of "whenever I happen to scroll to that topic in Telegram".
Cross-references
message_thread_idin DM rejection: Telegram: message_thread_id sent on private DMs causes 'message thread not found' error #12929, [Bug]: Telegram: message_thread_id sent in private chats causes 400 "message thread not found", silently drops messages #17242, Telegram: message tool sends stale message_thread_id in DM chats, causing 'message thread not found' error #15662, [Bug]: Telegram cron jobs fail with "message thread not found" error for private chats #14742, [Bug]: Telegram message tool fails with "message thread not found" after Topics/Forum mode was toggled #11620, Telegram: proactive sends fail with 'message thread not found' when Topics enabled in private chat (Premium) #14383 — pattern keeps recurring; this one is different because it's about the status reporting, not the rejection itself.Repro tarball / minimal reproducer
Operator config (sanitized chat ID):
Pre-condition: the user's Telegram client treats
<chat_id>as a private chat with reply-thread "topics" (not a forum supergroup), and Telegram has tightenedmessage_thread_idfor that chat (current behavior as of 2026-05-08). To reproduce on demand, just point the cron at any non-existentmessage_thread_idfor a DM and watch the cron-state reportdelivered.Real-world impact
Daily Tech Briefing,Evening Wrap-up) would have continued silently misrouting on every fire indefinitely. Topic-routed reminder crons (separate Reminder topic, ~5 of them) would have done the same once they next fired.:topic:<id>suffix from both crons viaopenclaw cron edit --to <bare>so future fires deliver to bare DM without the misleadingdeliveredsignal. This is a workaround for the silent-mismark, not the routing — until upstream surfaces the fallback, the operator has no way to know whether a topic-routed cron is actually hitting the topic.Filed because of standing rule: silent failures > loud failures, and "delivered" being the same string for a correct delivery and a misroute makes silent failures the default.