From fbc1bd6f8e61a1293a3990fac1fe1894b9bb6d82 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Thu, 12 Mar 2026 09:30:21 +0530 Subject: [PATCH] fix: clear telegram polling cleanup timers --- CHANGELOG.md | 1 + src/telegram/monitor.test.ts | 14 ++++++++++++++ src/telegram/polling-session.ts | 27 +++++++++++++++++++-------- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94f41b521d5..6983b27cc6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -123,6 +123,7 @@ Docs: https://docs.openclaw.ai - Agents/fallback cooldown probing: cap cooldown-bypass probing to one attempt per provider per fallback run so multi-model same-provider cooldown chains can continue to cross-provider fallbacks instead of repeatedly stalling on duplicate cooldown probes. (#41711) Thanks @cgdusek. - Telegram/direct delivery: bridge direct delivery sends to internal `message:sent` hooks so internal hook listeners observe successful Telegram deliveries. (#40185) Thanks @vincentkoc. - Dependencies: refresh workspace dependencies except the pinned Carbon package, and harden ACP session-config writes against non-string SDK values so newer ACP clients fail fast instead of tripping type/runtime mismatches. +- Telegram/polling restarts: clear bounded cleanup timeout handles after `runner.stop()` and `bot.stop()` settle so stall recovery no longer leaves stray 15-second timers behind on clean shutdown. (#43188) thanks @kyohwang. ## 2026.3.8 diff --git a/src/telegram/monitor.test.ts b/src/telegram/monitor.test.ts index bd9a35fc97c..f8423866fd0 100644 --- a/src/telegram/monitor.test.ts +++ b/src/telegram/monitor.test.ts @@ -398,6 +398,20 @@ describe("monitorTelegramProvider (grammY)", () => { expect(createdBotStops[0]).toHaveBeenCalledTimes(1); }); + it("clears bounded cleanup timers after a clean stop", async () => { + vi.useFakeTimers(); + try { + const abort = new AbortController(); + mockRunOnceAndAbort(abort); + + await monitorTelegramProvider({ token: "tok", abortSignal: abort.signal }); + + expect(vi.getTimerCount()).toBe(0); + } finally { + vi.useRealTimers(); + } + }); + it("surfaces non-recoverable errors", async () => { runSpy.mockImplementationOnce(() => makeRunnerStub({ diff --git a/src/telegram/polling-session.ts b/src/telegram/polling-session.ts index 6925b8784ae..3a78747e41f 100644 --- a/src/telegram/polling-session.ts +++ b/src/telegram/polling-session.ts @@ -17,6 +17,23 @@ const POLL_STALL_THRESHOLD_MS = 90_000; const POLL_WATCHDOG_INTERVAL_MS = 30_000; const POLL_STOP_GRACE_MS = 15_000; +const waitForGracefulStop = async (stop: () => Promise) => { + let timer: ReturnType | undefined; + try { + await Promise.race([ + stop(), + new Promise((resolve) => { + timer = setTimeout(resolve, POLL_STOP_GRACE_MS); + timer.unref?.(); + }), + ]); + } finally { + if (timer) { + clearTimeout(timer); + } + } +}; + type TelegramBot = ReturnType; type TelegramPollingSessionOpts = { @@ -271,14 +288,8 @@ export class TelegramPollingSession { clearTimeout(forceCycleTimer); } this.opts.abortSignal?.removeEventListener("abort", stopOnAbort); - await Promise.race([ - stopRunner(), - new Promise((resolve) => setTimeout(resolve, POLL_STOP_GRACE_MS)), - ]); - await Promise.race([ - stopBot(), - new Promise((resolve) => setTimeout(resolve, POLL_STOP_GRACE_MS)), - ]); + await waitForGracefulStop(stopRunner); + await waitForGracefulStop(stopBot); this.#activeRunner = undefined; if (this.#activeFetchAbort === fetchAbortController) { this.#activeFetchAbort = undefined;