← Back to PRs

#10850: fix(telegram): await runner.stop() to prevent polling race condition on hot-reload

by talhaorak open 2026-02-07 02:54 View on GitHub →
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