From e3da57d956a848c00aa07667632c1a76b0c9f42a Mon Sep 17 00:00:00 2001 From: banna-commits Date: Tue, 24 Feb 2026 04:33:34 +0100 Subject: [PATCH] fix: add exponential backoff to announce queue drain on failure (#24783) When the gateway rejects connections (e.g. scope-upgrade 'pairing required'), the announce queue drain loop would retry every ~1s indefinitely because the only delay was the fixed debounceMs (default 1000ms). This adds a consecutiveFailures counter with exponential backoff: 2s, 4s, 8s, 16s, 32s, 60s (capped). The counter resets on successful drain. The backoff is applied by shifting lastEnqueuedAt forward so that waitForQueueDebounce naturally delays the next attempt. Fixes #24777 Co-authored-by: Knut --- src/agents/subagent-announce-queue.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/agents/subagent-announce-queue.ts b/src/agents/subagent-announce-queue.ts index c81dd94b1d9..611541c186e 100644 --- a/src/agents/subagent-announce-queue.ts +++ b/src/agents/subagent-announce-queue.ts @@ -48,6 +48,8 @@ type AnnounceQueueState = { droppedCount: number; summaryLines: string[]; send: (item: AnnounceQueueItem) => Promise; + /** Consecutive drain failures — drives exponential backoff on errors. */ + consecutiveFailures: number; }; const ANNOUNCE_QUEUES = new Map(); @@ -89,6 +91,7 @@ function getAnnounceQueue( droppedCount: 0, summaryLines: [], send, + consecutiveFailures: 0, }; applyQueueRuntimeSettings({ target: created, @@ -174,10 +177,16 @@ function scheduleAnnounceDrain(key: string) { break; } } + // Drain succeeded — reset failure counter. + queue.consecutiveFailures = 0; } catch (err) { - // Keep items in queue and retry after debounce; avoid hot-loop retries. - queue.lastEnqueuedAt = Date.now(); - defaultRuntime.error?.(`announce queue drain failed for ${key}: ${String(err)}`); + queue.consecutiveFailures++; + // Exponential backoff on consecutive failures: 2s, 4s, 8s, ... capped at 60s. + const errorBackoffMs = Math.min(1000 * Math.pow(2, queue.consecutiveFailures), 60_000); + queue.lastEnqueuedAt = Date.now() + errorBackoffMs - queue.debounceMs; + defaultRuntime.error?.( + `announce queue drain failed for ${key} (attempt ${queue.consecutiveFailures}, retry in ${Math.round(errorBackoffMs / 1000)}s): ${String(err)}`, + ); } finally { queue.draining = false; if (queue.items.length === 0 && queue.droppedCount === 0) {