fix(telegram): remove offset confirmation getUpdates call

Remove the startup persisted-offset getUpdates preflight so polling restarts do not self-conflict before the grammY runner starts.\n\nFixes #69304.\n\nThanks @chinar-amrutkar.
This commit is contained in:
Chinar Amrutkar
2026-04-25 09:53:50 +01:00
committed by GitHub
parent 19017bad96
commit bf34fde235
4 changed files with 10 additions and 41 deletions

View File

@@ -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.

View File

@@ -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 () => {

View File

@@ -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 () => {

View File

@@ -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<typeof createTelegramBot>;
type TelegramApiAbortSignal = Parameters<TelegramBot["api"]["getUpdates"]>[1];
const waitForGracefulStop = async (stop: () => Promise<void>) => {
let timer: ReturnType<typeof setTimeout> | undefined;
@@ -49,9 +47,6 @@ const waitForGracefulStop = async (stop: () => Promise<void>) => {
}
};
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<void> {
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),
});