#11347: fix: scope Telegram update offset to bot token
channel: telegram
stale
Cluster:
Telegram Command Fixes
## Problem
The update offset file (`~/.openclaw/telegram/update-offset-{account}.json`) was keyed only by account name, not by bot token. When a bot token changed (common during initial setup or debugging), the persisted `lastUpdateId` from the old token would silently reject **all** inbound messages from the new bot.
`shouldSkipUpdate()` saw `updateId <= lastUpdateId` as always true because the old offset (e.g. 432M) was far ahead of the new bot's actual update IDs (e.g. 363M).
## Root Cause
Discovered while debugging #11011. The offset file had no awareness of which bot it belonged to. Token changes left a stale, impossibly-high offset that caused every inbound message to be dedupe-skipped.
## Changes
- **`update-offset-store.ts`**: Store `botId` (extracted from token) in the offset JSON. On read, validate `botId` matches the current token — discard offset if mismatched.
- **`monitor.ts`**: Pass `botToken` through `readTelegramUpdateOffset` and `writeTelegramUpdateOffset` calls.
- **Backward compatible**: Legacy files without `botId` field still work (no forced migration).
- **Tests**: Added 5 new tests covering token change invalidation, backward compat, legacy file reads, and `extractBotIdFromToken`.
## Test Results
```
48 test files | 405 tests — all passing ✅
```
Fixes #11337
Related: #11011
<!-- greptile_comment -->
<h2>Greptile Overview</h2>
<h3>Greptile Summary</h3>
This PR fixes a Telegram polling edge case where the persisted `lastUpdateId` offset was keyed only by account id. It now stores the bot’s numeric id (derived from the bot token) alongside the offset, and on read discards the stored offset if the current token belongs to a different bot. The monitor passes the token through to the offset store, and new tests cover token-change invalidation, backward-compat reads, legacy files, and bot-id extraction.
<h3>Confidence Score: 4/5</h3>
- Generally safe to merge once the token-parse mismatch case is addressed.
- The change is localized and well-tested, but the current invalidation logic can still accept a stale offset when a `botToken` is provided but can’t be parsed into a bot id while the stored file has a `botId` (leading to the original skip-all-updates failure mode).
- src/telegram/update-offset-store.ts
<!-- greptile_other_comments_section -->
<sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub>
<!-- /greptile_comment -->
Most Similar PRs
#22363: fix(telegram): isolate update offset state by bot token
by AIflow-Labs · 2026-02-21
86.2%
#3186: fix(telegram): sanitize update offset + lock polling
by daxiong888 · 2026-01-28
86.1%
#14359: fix: prefer named Telegram account over orphan 'default' in binding...
by itsGustav · 2026-02-12
82.9%
#7611: fix: migrate channels.telegram.token to botToken on config load
by luiginotmario · 2026-02-03
82.2%
#18115: fix: prevent voice message loss during concurrent update processing
by AlekseyRodkin · 2026-02-16
81.6%
#16995: fix(telegram): record update ID before processing to prevent crash ...
by AI-Reviewer-QS · 2026-02-15
81.4%
#9734: fix(telegram): correct sender identification for channel messages (...
by divol89 · 2026-02-05
80.9%
#8694: Fix Telegram routing when token override omits accountId
by codvik · 2026-02-04
80.5%
#14443: fix(telegram): skip General topic thread ID for all chat types (#14...
by lailoo · 2026-02-12
80.5%
#12936: fix(telegram): omit message_thread_id for private DM chats
by omair445 · 2026-02-09
80.3%