fix(telegram): reset webhook cleanup latch after polling 409 conflicts (#39205, thanks @amittell)

Co-authored-by: amittell <mittell@me.com>
This commit is contained in:
Peter Steinberger
2026-03-07 22:08:41 +00:00
parent c934dd51c0
commit 42bf4998d3
3 changed files with 42 additions and 0 deletions

View File

@@ -285,6 +285,7 @@ Docs: https://docs.openclaw.ai
- Gateway/systemd service restart hardening: clear stale gateway listeners by explicit run-port before service bind, add restart stale-pid port-override support, tune systemd start/stop/exit handling, and disable detached child mode only in service-managed runtime so cgroup stop semantics clean up descendants reliably. (#38463) Thanks @spirittechie.
- Discord/plugin native command aliases: let plugins declare provider-specific slash names so native Discord registration can avoid built-in command collisions; the bundled Talk voice plugin now uses `/talkvoice` natively on Discord while keeping text `/voice`.
- Daemon/Windows schtasks status normalization: derive runtime state from locale-neutral numeric `Last Run Result` codes only (without language string matching) and surface unknown when numeric result data is unavailable, preventing locale-specific misclassification drift. (#39153) Thanks @scoootscooob.
- Telegram/polling conflict recovery: reset the polling `webhookCleared` latch on `getUpdates` 409 conflicts so webhook cleanup re-runs on restart cycles and polling avoids infinite conflict loops. (#39205) Thanks @amittell.
## 2026.3.2

View File

@@ -591,6 +591,44 @@ describe("monitorTelegramProvider (grammY)", () => {
expect(api.getUpdates).not.toHaveBeenCalled();
});
it("resets webhookCleared latch on 409 conflict so deleteWebhook re-runs", async () => {
const abort = new AbortController();
api.deleteWebhook.mockReset();
api.deleteWebhook.mockResolvedValue(true);
const conflictError = Object.assign(
new Error("Conflict: terminated by other getUpdates request"),
{
error_code: 409,
method: "getUpdates",
},
);
let pollingCycle = 0;
runSpy
// First cycle: throw 409 conflict
.mockImplementationOnce(() =>
makeRunnerStub({
task: () => {
pollingCycle++;
return Promise.reject(conflictError);
},
}),
)
// Second cycle: succeed then abort
.mockImplementationOnce(() => {
pollingCycle++;
return makeAbortRunner(abort);
});
await monitorTelegramProvider({ token: "tok", abortSignal: abort.signal });
// deleteWebhook should be called twice: once on initial cleanup, once after 409 reset
expect(api.deleteWebhook).toHaveBeenCalledTimes(2);
expect(pollingCycle).toBe(2);
expect(runSpy).toHaveBeenCalledTimes(2);
});
it("falls back to configured webhookSecret when not passed explicitly", async () => {
await monitorTelegramProvider({
token: "tok",

View File

@@ -373,6 +373,9 @@ export async function monitorTelegramProvider(opts: MonitorTelegramOpts = {}) {
throw err;
}
const isConflict = isGetUpdatesConflict(err);
if (isConflict) {
webhookCleared = false;
}
const isRecoverable = isRecoverableTelegramNetworkError(err, { context: "polling" });
if (!isConflict && !isRecoverable) {
throw err;