openclaw - 💡(How to fix) Fix [Bug]: Telegram DM topic-route fallback silently masks routing regression — `lastDeliveryStatus: delivered` for fallback-to-main delivery

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

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.

Error Message

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. 2026-05-08T22:01:00.625+07:00 ERROR telegram/api No second ERROR line for the bare-DM retry — that succeeds and is logged at INFO. jobs-state.json after this fire: #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.

Root Cause

  • 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.

Fix Action

Fix / Workaround

  • 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.

Code Example

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)

---

{
  "1c590924-b0ea-4619-b542-53166fedc499": {
    "state": {
      "lastRunStatus": "ok",
      "lastStatus": "ok",
      "lastDeliveryStatus": "delivered",
      "lastDelivered": true,
      ...
    }
  }
}

---

"delivery": {
  "mode": "announce",
  "channel": "telegram",
  "to": "<chat_id>:topic:<thread_id>"
}
RAW_BUFFERClick to expand / collapse

Summary

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:

{
  "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, 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:

  1. 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.
  2. 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".

Cross-references

  • Sibling, open today: #79408 — forum-topic replies vanish entirely because threadless retry is DM-only. My case is the DM half: retry happens, but status hides the misroute.
  • Original feature: #2122 (closed) — introduced the topic→main-chat fallback. Worth linking back here.
  • Recent regressions covering same Telegram-side cause: #77059 (closed, regression) — "Telegram forum topics ignored after update (DM works, group broken)"; #77248 (closed) — "[Bug]: Telegram Forum Topic Delivery Silently Fails".
  • Older closed dups around message_thread_id in DM rejection: #12929, #17242, #15662, #14742, #11620, #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):

"delivery": {
  "mode": "announce",
  "channel": "telegram",
  "to": "<chat_id>:topic:<thread_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 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.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix [Bug]: Telegram DM topic-route fallback silently masks routing regression — `lastDeliveryStatus: delivered` for fallback-to-main delivery