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); } } } diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index f90f79ac8..df94ca7be 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -1176,6 +1176,31 @@ async function dispatchNextUnit( } } + // ── Secrets re-check gate — runs before every dispatch, not just at startAuto ── + // plan-milestone writes the milestone SECRETS file (e.g., M001-SECRETS.md) during its unit. By the time we + // reach the next dispatchNextUnit call the manifest exists but hasn't been + // presented to the user yet. Without this re-check the model would proceed + // into plan-slice / execute-task with no real credentials and mock everything. + const runSecretsGate = async () => { + try { + const manifestStatus = await getManifestStatus(basePath, mid); + if (manifestStatus && manifestStatus.pending.length > 0) { + const result = await collectSecretsFromManifest(basePath, mid, ctx); + ctx.ui.notify( + `Secrets collected: ${result.applied.length} applied, ${result.skipped.length} skipped, ${result.existingSkipped.length} already set.`, + "info", + ); + } + } catch (err) { + ctx.ui.notify( + `Secrets collection error: ${err instanceof Error ? err.message : String(err)}`, + "warning", + ); + } + }; + + await runSecretsGate(); + const needsRunUat = await checkNeedsRunUat(basePath, mid, state, prefs); // Flag: for human/mixed UAT, pause auto-mode after the prompt is sent so the user // can perform the UAT manually. On next resume, result file will exist → skip.