mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:40:44 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user