diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e869cf4ce2..8395a9129fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- 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. - 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/aria snapshots: bind `format=aria` `axN` refs to live DOM nodes through backend DOM ids when Playwright is available, so follow-up browser actions can use those refs without timing out. (#62434) Thanks @MrKipler. - Telegram: prevent duplicate in-process long pollers for the same bot token and add clearer `getUpdates` conflict diagnostics for external duplicate pollers. Fixes #56230. diff --git a/extensions/telegram/src/monitor.test.ts b/extensions/telegram/src/monitor.test.ts index 76f636dc79c..63de1536b20 100644 --- a/extensions/telegram/src/monitor.test.ts +++ b/extensions/telegram/src/monitor.test.ts @@ -829,16 +829,14 @@ describe("monitorTelegramProvider (grammY)", () => { vi.useRealTimers(); }); - it("confirms persisted offset with Telegram before starting runner", async () => { + it("does not call getUpdates for offset confirmation (avoids 409 conflicts)", async () => { const { order } = await runMonitorAndCaptureStartupOrder({ persistedOffset: 549076203, }); - expect(api.getUpdates).toHaveBeenCalledWith( - { offset: 549076204, limit: 1, timeout: 0 }, - expect.any(AbortSignal), - ); - expect(order).toEqual(["deleteWebhook", "getUpdates", "run"]); + // OpenClaw middleware skips duplicates using the persisted update offset. + expect(api.getUpdates).not.toHaveBeenCalled(); + expect(order).toEqual(["deleteWebhook", "run"]); }); it("skips offset confirmation when no persisted offset exists", async () => { diff --git a/extensions/telegram/src/polling-session.test.ts b/extensions/telegram/src/polling-session.test.ts index 2150fb2c332..34cbc97d8ce 100644 --- a/extensions/telegram/src/polling-session.test.ts +++ b/extensions/telegram/src/polling-session.test.ts @@ -280,10 +280,8 @@ describe("TelegramPollingSession", () => { expect(sleepWithAbortMock).toHaveBeenCalledTimes(1); }); - it("bounds the persisted offset confirmation getUpdates call", async () => { + it("does not call getUpdates for offset confirmation (avoiding 409 conflicts)", async () => { const abort = new AbortController(); - const timeoutSignal = new AbortController().signal; - const timeoutSpy = vi.spyOn(AbortSignal, "timeout").mockReturnValue(timeoutSignal); const bot = makeBot(); createTelegramBotMock.mockReturnValueOnce(bot); runMock.mockReturnValueOnce({ @@ -308,17 +306,11 @@ describe("TelegramPollingSession", () => { telegramTransport: undefined, }); - try { - await session.runUntilAbort(); + await session.runUntilAbort(); - expect(timeoutSpy).toHaveBeenCalledWith(10_000); - expect(bot.api.getUpdates).toHaveBeenCalledWith( - { offset: 42, limit: 1, timeout: 0 }, - timeoutSignal, - ); - } finally { - timeoutSpy.mockRestore(); - } + // Offset confirmation was removed because it could self-conflict with the runner. + // OpenClaw middleware still skips duplicates using the persisted update offset. + expect(bot.api.getUpdates).not.toHaveBeenCalled(); }); it("forces a restart when polling stalls without getUpdates activity", async () => { diff --git a/extensions/telegram/src/polling-session.ts b/extensions/telegram/src/polling-session.ts index bd80a91abb9..011b77de5db 100644 --- a/extensions/telegram/src/polling-session.ts +++ b/extensions/telegram/src/polling-session.ts @@ -27,10 +27,8 @@ const MIN_POLL_STALL_THRESHOLD_MS = 30_000; const MAX_POLL_STALL_THRESHOLD_MS = 600_000; const POLL_WATCHDOG_INTERVAL_MS = 30_000; const POLL_STOP_GRACE_MS = 15_000; -const CONFIRM_PERSISTED_OFFSET_TIMEOUT_MS = 10_000; type TelegramBot = ReturnType; -type TelegramApiAbortSignal = Parameters[1]; const waitForGracefulStop = async (stop: () => Promise) => { let timer: ReturnType | undefined; @@ -49,9 +47,6 @@ const waitForGracefulStop = async (stop: () => Promise) => { } }; -const telegramApiTimeoutSignal = (timeoutMs: number): TelegramApiAbortSignal => - AbortSignal.timeout(timeoutMs) as unknown as TelegramApiAbortSignal; - const resolvePollingStallThresholdMs = (value: number | undefined): number => { if (typeof value !== "number" || !Number.isFinite(value)) { return DEFAULT_POLL_STALL_THRESHOLD_MS; @@ -225,24 +220,7 @@ export class TelegramPollingSession { } } - async #confirmPersistedOffset(bot: TelegramBot): Promise { - const lastUpdateId = this.opts.getLastUpdateId(); - if (lastUpdateId === null || lastUpdateId >= Number.MAX_SAFE_INTEGER) { - return; - } - try { - await bot.api.getUpdates( - { offset: lastUpdateId + 1, limit: 1, timeout: 0 }, - telegramApiTimeoutSignal(CONFIRM_PERSISTED_OFFSET_TIMEOUT_MS), - ); - } catch { - // Non-fatal: runner middleware still skips duplicates via shouldSkipUpdate. - } - } - async #runPollingCycle(bot: TelegramBot): Promise<"continue" | "exit"> { - await this.#confirmPersistedOffset(bot); - const liveness = new TelegramPollingLivenessTracker({ onPollSuccess: (finishedAt) => this.#status.notePollSuccess(finishedAt), });