Skip to content

fix: scope Telegram update offset to bot token#11347

Closed
anooprdawar wants to merge 1 commit intoopenclaw:mainfrom
anooprdawar:fix/telegram-offset-bot-scope
Closed

fix: scope Telegram update offset to bot token#11347
anooprdawar wants to merge 1 commit intoopenclaw:mainfrom
anooprdawar:fix/telegram-offset-bot-scope

Conversation

@anooprdawar
Copy link

@anooprdawar anooprdawar commented Feb 7, 2026

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 Overview

Greptile Summary

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.

Confidence Score: 4/5

  • 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

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

@openclaw-barnacle openclaw-barnacle bot added the channel: telegram Channel integration: telegram label Feb 7, 2026
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +71 to +76
if (parsed.botId && params.botToken) {
const currentBotId = extractBotIdFromToken(params.botToken);
if (currentBotId && parsed.botId !== currentBotId) {
return null;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Token change not invalidated

readTelegramUpdateOffset only discards a stored botId when extractBotIdFromToken(params.botToken) succeeds. If the new token is malformed/unset (but still passed in) and currentBotId is undefined, the stale offset will be accepted and you can still end up skipping all inbound messages for the new bot. Consider treating “token provided but botId can’t be extracted” as a mismatch (return null) when the file contains a botId.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/telegram/update-offset-store.ts
Line: 71:76

Comment:
**Token change not invalidated**

`readTelegramUpdateOffset` only discards a stored `botId` when `extractBotIdFromToken(params.botToken)` succeeds. If the new token is malformed/unset (but still passed in) and `currentBotId` is `undefined`, the stale offset will be accepted and you can still end up skipping all inbound messages for the new bot. Consider treating “token provided but botId can’t be extracted” as a mismatch (return `null`) when the file contains a `botId`.

How can I resolve this? If you propose a fix, please make it concise.

@mukhtharcm mukhtharcm self-assigned this Feb 8, 2026
SutanuNandigrami pushed a commit to SutanuNandigrami/openclaw that referenced this pull request Feb 8, 2026
@mukhtharcm mukhtharcm removed their assignment Feb 9, 2026
@openclaw-barnacle
Copy link

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added the stale Marked as stale due to inactivity label Feb 21, 2026
@openclaw-barnacle openclaw-barnacle bot removed the stale Marked as stale due to inactivity label Feb 23, 2026
@vincentkoc
Copy link
Member

Useful direction here, and the scope is focused.

Main still reads/writes offset files without token identity, so this fix family remains relevant. Before merge we should harden one edge case: when a token is provided but bot-id extraction fails, stale offsets should not be trusted.

Please rebase and include that guard explicitly in tests; then we can review as a candidate that preserves older lineage while reducing token-rotation regressions.

@anooprdawar
Copy link
Author

Thanks @vincentkoc — rebased onto main and hardened the malformed-token edge case.

What changed:

  • When botToken is provided but extractBotIdFromToken() fails (malformed/empty token), we now unconditionally discard the persisted offset. This applies regardless of whether the file has a stored botId (new format) or not (legacy format). Stale offsets are never trusted when token identity can't be verified.
  • Rebased cleanly onto current main (c92c3ad)
  • Tests now use the project's withStateDirEnv helper
  • Added explicit test cases for:
    • Malformed token with stored botId → discard ✅ (existed before)
    • Malformed token without stored botId (legacy file) → discard ✅ (new)
    • Empty string token → discard ✅ (new)
    • Token with no numeric prefix (:AAFsecret) → discard ✅ (new)

All 13 tests passing.

The update offset file was keyed only by account name, not by bot token.
When a bot token changed (common during initial setup), the persisted
lastUpdateId from the old token would silently reject all inbound
messages because shouldSkipUpdate() saw updateId <= lastUpdateId.

Changes:
- Store botId (extracted from token) in the offset file
- On read, validate botId matches current token; discard if mismatched
- When botToken is provided but extraction fails (malformed token),
  discard the offset unconditionally — stale offsets must not be trusted
- Pass botToken through read/write calls in monitor.ts
- Backward compatible: legacy files without botId still work
- Tests use withStateDirEnv helper, cover token change, malformed token
  (with and without stored botId), empty string, and legacy files

Fixes openclaw#11337
Related: openclaw#11011
@vincentkoc
Copy link
Member

Superseded by #24549 (merged on 2026-02-23), which rebased and integrated this token-scoped Telegram offset fix with current main.

Thank you @anooprdawar for the original implementation.

@vincentkoc
Copy link
Member

Closing as superseded by #24549 (merged). Credit preserved in changelog.

@vincentkoc vincentkoc closed this Feb 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: telegram Channel integration: telegram size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Telegram update offset should be scoped to bot token

4 participants