#10850: fix(telegram): await runner.stop() to prevent polling race condition on hot-reload
channel: telegram
stale
## Summary
When config hot-reload triggers a channel restart (via SIGUSR1 or `config.patch`), the Telegram polling runner's `stop()` promise was being discarded with `void`. This created a race condition where a new polling loop could start before the old one fully stopped.
## Problem
```typescript
// Before: fire-and-forget
const stopOnAbort = () => {
if (opts.abortSignal?.aborted) {
void runner.stop(); // ❌ Not awaited!
}
};
```
This causes:
- **409 Conflict errors** — Multiple pollers hitting `getUpdates` on the same bot token
- **Duplicate message delivery** — Both old and new pollers process the same updates
- **Wrong bot routing** — In multi-account setups, messages route through whichever bot connects first
## Solution
Store the `stop()` promise and await it in the finally block:
```typescript
// After: properly awaited
const stopState: { promise: Promise<void> | null } = { promise: null };
const stopOnAbort = () => {
if (opts.abortSignal?.aborted) {
stopState.promise = runner.stop();
}
};
// ... in finally:
if (stopState.promise) {
await stopState.promise.catch(() => {});
}
```
## Testing
- [x] `pnpm build` passes
- [x] `pnpm check` passes (lint + format)
- [x] `pnpm test` passes for telegram-related tests
- [ ] Manual testing with SIGUSR1 restarts (tested locally, no more duplicate deliveries)
## Related Issues
- Fixes #8140 (Telegram Channel Issues - duplicate delivery + no auto-recovery)
- Related to #9097 (Double SIGUSR1 restart causes message loss)
- Related to #6402 (Wrong bot routing after restart in multi-account)
- Related to #4302 (Polling dies silently)
## AI Disclosure
🤖 This PR was AI-assisted (Claude via OpenClaw). The fix was identified by analyzing the codebase and understanding the race condition mechanics. Tested locally by the human collaborator.
cc @joshp123 (Telegram maintainer)
<!-- greptile_comment -->
<h2>Greptile Overview</h2>
<h3>Greptile Summary</h3>
- Updates the Telegram polling monitor loop to track a `runner.stop()` promise and await it during cleanup.
- Adds abort-signal wiring intended to stop the active grammY runner on shutdown.
- Goal is to reduce overlapping pollers (409 getUpdates conflicts / duplicate deliveries) during restarts/hot-reload.
- Change is localized to `src/telegram/monitor.ts` and affects the polling runner lifecycle/cleanup path.
<h3>Confidence Score: 2/5</h3>
- Not safe to merge as-is due to an incomplete fix for the stated race condition.
- The new await only runs if an abort event fires; in the described hot-reload restart path, the abort signal often isn’t triggered, so the old runner may continue while a new one starts, leaving the 409/duplicate-delivery issue unresolved.
- src/telegram/monitor.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
#6447: fix(telegram): auto-restart polling when grammY runner exits silently
by AugmentAdvertise · 2026-02-01
85.7%
#7247: fix(telegram): abort stale getUpdates connections after long-poll t...
by JanderV · 2026-02-02
82.8%
#8166: fix(telegram): lifecycle fixes for duplicate messages and auto-reco...
by cheenu1092-oss · 2026-02-03
82.6%
#3186: fix(telegram): sanitize update offset + lock polling
by daxiong888 · 2026-01-28
82.0%
#6463: fix(telegram): improve timeout handling and prevent channel exits
by ai-fanatic · 2026-02-01
81.7%
#11688: feat(telegram): add health check watchdog for long-polling
by rmfalco89 · 2026-02-08
80.2%
#18115: fix: prevent voice message loss during concurrent update processing
by AlekseyRodkin · 2026-02-16
78.0%
#16105: fix: handle message_reaction updates in group polling mode
by claw-sylphx · 2026-02-14
77.7%
#10509: fix(telegram): bare abort words bypass debounce + clear buffered me...
by romancircus · 2026-02-06
77.6%
#7141: fix(telegram): unify network error detection to prevent poll crashes
by hclsys · 2026-02-02
77.5%