From f102559d282c733a91cc72c904aeef371bc40deb Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Fri, 13 Mar 2026 22:32:24 -0600 Subject: [PATCH] fix: remove infinite delivery retry loop for background job completions (#301) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Background job completions were delivered via an infinite retry loop with exponential backoff. Since delivery is an in-process function call (not a network operation), retries served no purpose and caused each retry to trigger a full LLM turn — burning tokens indefinitely until the 5-minute eviction timer fired. Delivery is now fire-once. The acknowledgeDeliveries API is retained as a no-op for compatibility with the await_job tool. Closes #301 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../extensions/async-jobs/job-manager.ts | 47 ++----------------- 1 file changed, 4 insertions(+), 43 deletions(-) diff --git a/src/resources/extensions/async-jobs/job-manager.ts b/src/resources/extensions/async-jobs/job-manager.ts index 34a1b0527..174b923eb 100644 --- a/src/resources/extensions/async-jobs/job-manager.ts +++ b/src/resources/extensions/async-jobs/job-manager.ts @@ -31,18 +31,10 @@ export interface JobManagerOptions { onJobComplete?: (job: Job) => void; } -// ── Delivery Retry ───────────────────────────────────────────────────────── - -const DELIVERY_BASE_MS = 500; -const DELIVERY_MAX_MS = 30_000; -const DELIVERY_JITTER_MS = 200; - // ── Manager ──────────────────────────────────────────────────────────────── export class AsyncJobManager { private jobs = new Map(); - private deliveryTimers = new Map>(); - private acknowledgedJobs = new Set(); private evictionTimers = new Map>(); private maxRunning: number; @@ -157,28 +149,16 @@ export class AsyncJobManager { } /** - * Mark jobs as acknowledged so delivery retries stop. + * No-op. Retained for API compatibility with await_job tool. */ - acknowledgeDeliveries(jobIds: string[]): void { - for (const id of jobIds) { - this.acknowledgedJobs.add(id); - const timer = this.deliveryTimers.get(id); - if (timer) { - clearTimeout(timer); - this.deliveryTimers.delete(id); - } - } + acknowledgeDeliveries(_jobIds: string[]): void { + // Delivery is fire-once; no retries to cancel. } /** * Cleanup all timers and resources. */ shutdown(): void { - for (const timer of this.deliveryTimers.values()) { - clearTimeout(timer); - } - this.deliveryTimers.clear(); - for (const timer of this.evictionTimers.values()) { clearTimeout(timer); } @@ -195,26 +175,9 @@ export class AsyncJobManager { // ── Private ──────────────────────────────────────────────────────────── - private deliverResult(job: Job, attempt = 0): void { - if (this.acknowledgedJobs.has(job.id)) return; + private deliverResult(job: Job): void { if (!this.onJobComplete) return; - this.onJobComplete(job); - - // Schedule retry with exponential backoff + jitter - const delay = Math.min( - DELIVERY_BASE_MS * Math.pow(2, attempt) + Math.random() * DELIVERY_JITTER_MS, - DELIVERY_MAX_MS, - ); - - const timer = setTimeout(() => { - this.deliveryTimers.delete(job.id); - if (!this.acknowledgedJobs.has(job.id)) { - this.deliverResult(job, attempt + 1); - } - }, delay); - - this.deliveryTimers.set(job.id, timer); } private scheduleEviction(id: string): void { @@ -224,7 +187,6 @@ export class AsyncJobManager { const timer = setTimeout(() => { this.evictionTimers.delete(id); this.jobs.delete(id); - this.acknowledgedJobs.delete(id); }, this.evictionMs); this.evictionTimers.set(id, timer); @@ -244,7 +206,6 @@ export class AsyncJobManager { if (timer) clearTimeout(timer); this.evictionTimers.delete(oldest.id); this.jobs.delete(oldest.id); - this.acknowledgedJobs.delete(oldest.id); } } }