#16105: fix: handle message_reaction updates in group polling mode
channel: telegram
size: M
Cluster:
Telegram Command Fixes
## Problem
Fixes #15753
`message_reaction` updates from groups/supergroups are never delivered to the `bot.on("message_reaction")` handler when using polling mode, while DM reactions work correctly.
## Root Cause
The `shouldSkipUpdate()` function in `bot.ts` compares `updateId <= lastUpdateId` where `lastUpdateId` is a **runtime-advancing high-water mark** that tracks the maximum `update_id` seen across all chats. With `@grammyjs/runner` concurrent processing (via `sequentialize`), updates with different keys (different chats) run in parallel.
This creates a race condition:
1. Updates arrive: `[100: msg in chat A, 101: reaction in chat A, 102: msg in chat B]`
2. `sequentialize` serializes chat A updates `[100, 101]` but processes chat B `[102]` concurrently
3. Chat B's update 102 completes first, advancing `lastUpdateId` to `102`
4. Chat A's update 100 completes, `lastUpdateId` stays at `102`
5. Chat A's reaction (update 101) starts, `shouldSkipUpdate(101)` sees `101 <= 102` and **incorrectly skips it**
DM reactions work because DMs have less concurrent cross-chat activity, making the race condition rare.
## Fix
Change `shouldSkipUpdate()` to compare against `initialLastUpdateId` (the persisted offset from before this session started) instead of the runtime-advancing `lastUpdateId`. This preserves crash-recovery dedup (skip updates already processed in prior sessions) while eliminating the concurrent processing race.
The runtime dedupe cache (`recentUpdates.check()`) already handles within-session deduplication correctly.
## Changes
- `src/telegram/bot.ts`: Split `lastUpdateId` into `initialLastUpdateId` (const, for skip checks) and `lastUpdateId` (mutable, for offset persistence only)
- New test file verifying:
- Group reactions are not skipped when a cross-chat update has a higher update_id
- Updates at or below the persisted initial offset are still skipped (crash recovery)
- Group messages are also protected from the same race condition
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Fixes a race condition in the Telegram polling dedup logic where `shouldSkipUpdate()` compared incoming `update_id` values against a runtime-advancing high-water mark (`lastUpdateId`). When `@grammyjs/runner`'s `sequentialize` processes updates from different chats concurrently, a fast-completing update from one chat could advance the offset past a still-queued update from another chat, causing it to be incorrectly skipped. This primarily affected group `message_reaction` updates but could also drop regular messages.
- `src/telegram/bot.ts`: Introduces `initialLastUpdateId` (the persisted offset from before this session) as a `const` used solely by `shouldSkipUpdate()`, while the mutable `lastUpdateId` continues to track the runtime high-water mark for offset persistence via `recordUpdateId`. Within-session dedup remains handled by the existing TTL-based `recentUpdates` cache.
- New test file covering the race condition scenario (group reactions not skipped when cross-chat message advances offset), crash-recovery dedup (updates at/below initial offset still skipped), and the same protection for group messages.
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge — it's a minimal, well-targeted fix with comprehensive test coverage.
- The change is small (splitting one variable into two with clear separation of concerns), the root cause analysis is thorough and correct, and three test cases cover both the fix and the preserved crash-recovery behavior. The mutable `lastUpdateId` continues to serve offset persistence correctly, and within-session dedup is already handled by the TTL-based dedupe cache. No new dependencies or architectural changes are introduced.
- No files require special attention.
<sub>Last reviewed commit: 4310355</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#16995: fix(telegram): record update ID before processing to prevent crash ...
by AI-Reviewer-QS · 2026-02-15
81.8%
#18115: fix: prevent voice message loss during concurrent update processing
by AlekseyRodkin · 2026-02-16
80.9%
#11347: fix: scope Telegram update offset to bot token
by anooprdawar · 2026-02-07
79.6%
#17316: fix: ack reaction not removed when block streaming is enabled (Tele...
by czmathew · 2026-02-15
79.3%
#19213: Telegram: preserve DM topic thread in direct replies
by Kemalau · 2026-02-17
79.0%
#12978: fix(telegram): use real message_id for inline button callback react...
by omair445 · 2026-02-10
78.8%
#3368: fix: sessions navigation, DM thread display, and DM thread delivery...
by Lukavyi · 2026-01-28
78.5%
#13580: fix(telegram): skip updateLastRoute when dmScope isolates DM sessions
by lailoo · 2026-02-10
78.0%
#10850: fix(telegram): await runner.stop() to prevent polling race conditio...
by talhaorak · 2026-02-07
77.7%
#14443: fix(telegram): skip General topic thread ID for all chat types (#14...
by lailoo · 2026-02-12
77.7%