From ceace835563db76e661092292966be88b6b341cb Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 25 Apr 2026 02:18:49 -0700 Subject: [PATCH] fix(telegram): keep polling watchdog active for wedged runner --- CHANGELOG.md | 1 + .../telegram/src/polling-liveness.test.ts | 4 -- extensions/telegram/src/polling-liveness.ts | 9 +--- .../telegram/src/polling-session.test.ts | 54 +++++++++++++++++++ extensions/telegram/src/polling-session.ts | 1 - 5 files changed, 56 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1ca541b09d..395812e7470 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai - Heartbeat: clamp oversized scheduler delays through the shared safe timer helper, preventing `every` values over Node's timeout cap from becoming a 1 ms crash loop. Fixes #71414. (#71478) Thanks @hclsys. - Telegram: remove the startup persisted-offset `getUpdates` preflight so polling restarts do not self-conflict before the runner starts. Fixes #69304. (#69779) Thanks @chinar-amrutkar. +- Telegram: keep the polling stall watchdog active even when grammY reports the runner as not running while its task is still pending, so a rebuilt transport cannot leave `getUpdates` silent until a manual gateway restart. Fixes #69064. Thanks @LDLoeb. - Browser/Playwright: ignore benign already-handled route races during guarded navigation so browser-page tasks no longer fail when Playwright tears down a route mid-flight. (#68708) Thanks @Steady-ai. - Browser/downloads: seed managed Chrome profiles with OpenClaw download prefs and capture unmanaged click-triggered downloads under the guarded downloads directory, while explicit download waiters still own their target file. (#64558) Thanks @Pearcekieser. - Browser/Chrome: stop passing redundant `--disable-setuid-sandbox` when `browser.noSandbox` is enabled; `--no-sandbox` remains the effective sandbox opt-out. (#67939) Thanks @sebykrueger. diff --git a/extensions/telegram/src/polling-liveness.test.ts b/extensions/telegram/src/polling-liveness.test.ts index f41413838a2..b38b1bb886d 100644 --- a/extensions/telegram/src/polling-liveness.test.ts +++ b/extensions/telegram/src/polling-liveness.test.ts @@ -31,7 +31,6 @@ describe("TelegramPollingLivenessTracker", () => { expect( tracker.detectStall({ thresholdMs: POLL_STALL_THRESHOLD_MS, - runnerIsRunning: true, }), ).toBeNull(); @@ -45,7 +44,6 @@ describe("TelegramPollingLivenessTracker", () => { now = 120_001; const stall = tracker.detectStall({ thresholdMs: POLL_STALL_THRESHOLD_MS, - runnerIsRunning: true, }); expect(stall?.message).toContain("Polling stall detected (no completed getUpdates"); expect(stall?.message).toContain("inFlight=0 outcome=not-started"); @@ -54,7 +52,6 @@ describe("TelegramPollingLivenessTracker", () => { expect( tracker.detectStall({ thresholdMs: POLL_STALL_THRESHOLD_MS, - runnerIsRunning: true, }), ).toBeNull(); }); @@ -69,7 +66,6 @@ describe("TelegramPollingLivenessTracker", () => { now = 120_001; const stall = tracker.detectStall({ thresholdMs: POLL_STALL_THRESHOLD_MS, - runnerIsRunning: true, }); expect(stall?.message).toContain("active getUpdates stuck"); diff --git a/extensions/telegram/src/polling-liveness.ts b/extensions/telegram/src/polling-liveness.ts index 6fcd200f6fb..b57237de020 100644 --- a/extensions/telegram/src/polling-liveness.ts +++ b/extensions/telegram/src/polling-liveness.ts @@ -89,14 +89,7 @@ export class TelegramPollingLivenessTracker { this.#inFlightGetUpdates = Math.max(0, this.#inFlightGetUpdates - 1); } - detectStall(params: { - thresholdMs: number; - runnerIsRunning: boolean; - now?: number; - }): TelegramPollingStall | null { - if (!params.runnerIsRunning) { - return null; - } + detectStall(params: { thresholdMs: number; now?: number }): TelegramPollingStall | null { const now = params.now ?? this.#now(); const activeElapsed = this.#inFlightGetUpdates > 0 && this.#lastGetUpdatesStartedAt != null diff --git a/extensions/telegram/src/polling-session.test.ts b/extensions/telegram/src/polling-session.test.ts index 34cbc97d8ce..a29a03a6877 100644 --- a/extensions/telegram/src/polling-session.test.ts +++ b/extensions/telegram/src/polling-session.test.ts @@ -387,6 +387,60 @@ describe("TelegramPollingSession", () => { } }); + it("forces a restart when the runner task is pending but reports not running", async () => { + const abort = new AbortController(); + const firstRunnerStop = vi.fn(async () => undefined); + const secondRunnerStop = vi.fn(async () => undefined); + createTelegramBotMock.mockReturnValue(makeBot()); + + let firstTaskResolve: (() => void) | undefined; + const firstTask = new Promise((resolve) => { + firstTaskResolve = resolve; + }); + let cycle = 0; + runMock.mockImplementation(() => { + cycle += 1; + if (cycle === 1) { + return { + task: () => firstTask, + stop: async () => { + await firstRunnerStop(); + firstTaskResolve?.(); + }, + isRunning: () => false, + }; + } + return { + task: async () => { + abort.abort(); + }, + stop: secondRunnerStop, + isRunning: () => false, + }; + }); + + const watchdogHarness = installPollingStallWatchdogHarness(); + + const log = vi.fn(); + const session = createPollingSession({ + abortSignal: abort.signal, + log, + }); + + try { + const runPromise = session.runUntilAbort(); + const watchdog = await watchdogHarness.waitForWatchdog(); + watchdog?.(); + await runPromise; + + expect(runMock).toHaveBeenCalledTimes(2); + expect(firstRunnerStop).toHaveBeenCalledTimes(1); + expect(log).toHaveBeenCalledWith(expect.stringContaining("Polling stall detected")); + } finally { + watchdogHarness.restore(); + } + }); + it("honors a custom polling stall threshold", async () => { const abort = new AbortController(); const botStop = vi.fn(async () => undefined); diff --git a/extensions/telegram/src/polling-session.ts b/extensions/telegram/src/polling-session.ts index 011b77de5db..11cc14f755a 100644 --- a/extensions/telegram/src/polling-session.ts +++ b/extensions/telegram/src/polling-session.ts @@ -295,7 +295,6 @@ export class TelegramPollingSession { const stall = liveness.detectStall({ thresholdMs: this.#stallThresholdMs, - runnerIsRunning: runner.isRunning(), }); if (stall) { this.#transportState.markDirty();