diff --git a/docs/user-docs/auto-mode.md b/docs/user-docs/auto-mode.md index 2b7e9d05a5..f59f6f1db9 100644 --- a/docs/user-docs/auto-mode.md +++ b/docs/user-docs/auto-mode.md @@ -187,6 +187,8 @@ The sliding-window approach reduces false positives on legitimate retries (e.g., Auto mode also enforces a same-unit consecutive dispatch cap for `complete-milestone`, `validate-milestone`, and `research-slice`. The loop allows two consecutive dispatches of the same unit in the same phase and stops before a third attempt with a repeat-cap warning that instructs you to run `/gsd resume` after intervention. +Auto mode also tracks consecutive dispatch-claim skips with reason `already-active`. If the same unit claim returns `already-active` 3 times in a row, auto mode pauses and surfaces a manual-recovery warning instead of spinning indefinitely on claim retries. + ### Artifact Verification Retries After each unit, GSD verifies that the expected artifact exists on disk. If the artifact is missing, auto mode re-dispatches the unit with explicit failure context and records an `artifact-verification-retry` journal event. diff --git a/docs/user-docs/troubleshooting.md b/docs/user-docs/troubleshooting.md index 029d15ebec..442900156f 100644 --- a/docs/user-docs/troubleshooting.md +++ b/docs/user-docs/troubleshooting.md @@ -34,6 +34,14 @@ It checks: **Fix:** Check the task plan for clarity. If the plan is ambiguous, refine it manually, then `/gsd auto` to resume. +### Auto mode pauses after repeated `already-active` dispatch claims + +**Symptoms:** Auto mode repeatedly skips dispatch with reason `already-active`, then pauses with a message that manual recovery is required. + +**Cause:** GSD treats 3 consecutive `already-active` claim skips for the same unit as a stuck claim path and pauses auto mode instead of retrying forever. + +**Fix:** Resolve the underlying active-claim/worker state (usually with `/gsd doctor` or `/gsd doctor fix`), then run `/gsd auto` or `/gsd resume`. + ### Auto mode pauses after a timeout or finalize failure **Symptoms:** Auto mode reports a unit hard timeout, a finalize timeout, or a post-unit closeout failure. diff --git a/mintlify-docs/guides/auto-mode.mdx b/mintlify-docs/guides/auto-mode.mdx index 0459612d42..43ad9f52c1 100644 --- a/mintlify-docs/guides/auto-mode.mdx +++ b/mintlify-docs/guides/auto-mode.mdx @@ -114,6 +114,8 @@ A sliding-window analysis detects stuck loops — catching cycles like A→B→A Auto mode also enforces a same-unit consecutive dispatch cap for `complete-milestone`, `validate-milestone`, and `research-slice`. The loop allows two consecutive dispatches of the same unit in the same phase and stops before a third attempt with a repeat-cap warning that instructs you to run `/gsd resume` after intervention. +Auto mode also tracks consecutive dispatch-claim skips with reason `already-active`. If the same unit claim returns `already-active` 3 times in a row, auto mode pauses and requires manual recovery instead of retrying claim skips indefinitely. + ### Timeout supervision | Timeout | Default | Behavior | diff --git a/mintlify-docs/guides/troubleshooting.mdx b/mintlify-docs/guides/troubleshooting.mdx index 8363048ced..11815d3bec 100644 --- a/mintlify-docs/guides/troubleshooting.mdx +++ b/mintlify-docs/guides/troubleshooting.mdx @@ -28,6 +28,14 @@ It checks file structure, referential integrity, completion state consistency, g **Fix:** Check the task plan for clarity. Refine it manually, then `/gsd auto`. + + **Symptoms:** Auto mode repeatedly skips dispatch with reason `already-active`, then pauses for manual recovery. + + **Cause:** GSD pauses when the same unit hits 3 consecutive `already-active` claim skips, preventing an infinite claim-retry loop. + + **Fix:** Repair the active-claim/worker state (usually `/gsd doctor` or `/gsd doctor fix`), then continue with `/gsd auto` or `/gsd resume`. + + **Symptoms:** Auto mode reports a hard timeout, finalize timeout, or post-unit closeout failure. diff --git a/src/resources/extensions/gsd/auto/loop.ts b/src/resources/extensions/gsd/auto/loop.ts index 0eb6514baf..4c9afa94e1 100644 --- a/src/resources/extensions/gsd/auto/loop.ts +++ b/src/resources/extensions/gsd/auto/loop.ts @@ -112,6 +112,7 @@ import { handleCustomEngineReconcileOutcome } from "./workflow-custom-engine-rec // tolerates — same behavior as a fresh session. const STUCK_RECOVERY_ATTEMPTS_KEY = "stuck_recovery_attempts"; const RECENT_UNIT_KEYS_LIMIT = 20; +const MAX_CONSECUTIVE_ALREADY_ACTIVE_SKIPS = 3; function stableStuckStateScopeId(s: AutoSession): string { return normalizeRealPath(s.scope?.workspace.projectRoot ?? (s.originalBasePath || s.basePath)); @@ -292,6 +293,7 @@ export async function autoLoop( }; let consecutiveErrors = 0; let consecutiveCooldowns = 0; + let consecutiveAlreadyActiveSkips = 0; const recentErrorMessages: string[] = []; while (s.active) { @@ -904,15 +906,29 @@ export async function autoLoop( } if (dispatchDecision.action === "skip") { if (dispatchDecision.reason === "stale-lease") { + consecutiveAlreadyActiveSkips = 0; const msg = `Lost milestone lease for ${iterData.mid ?? "unknown"} while claiming ${iterData.unitType} ${iterData.unitId}; dispatch claim still failed after recovery.`; ctx.ui.notify(msg, "error"); finishTurn("stopped", "execution", msg); await deps.stopAuto(ctx, pi, msg); break; } + if (dispatchDecision.reason === "already-active" || dispatchDecision.reason === "already_active") { + consecutiveAlreadyActiveSkips++; + if (consecutiveAlreadyActiveSkips >= MAX_CONSECUTIVE_ALREADY_ACTIVE_SKIPS) { + const msg = `Dispatch claim for ${iterData.unitType} ${iterData.unitId} remained already-active for ${consecutiveAlreadyActiveSkips} consecutive attempts. Pausing auto-mode for manual recovery.`; + ctx.ui.notify(msg, "error"); + await deps.pauseAuto(ctx, pi); + finishTurn("paused", "manual-attention", msg); + break; + } + } else { + consecutiveAlreadyActiveSkips = 0; + } finishTurn("skipped", "execution", dispatchDecision.reason); continue; } + consecutiveAlreadyActiveSkips = 0; dispatchId = dispatchDecision.dispatchId; let unitPhaseResult: Awaited>; diff --git a/src/resources/extensions/gsd/tests/auto-loop.test.ts b/src/resources/extensions/gsd/tests/auto-loop.test.ts index a966ee2b0d..ddd2a9866d 100644 --- a/src/resources/extensions/gsd/tests/auto-loop.test.ts +++ b/src/resources/extensions/gsd/tests/auto-loop.test.ts @@ -2025,7 +2025,7 @@ test("autoLoop handles dispatch skip action by continuing", async (t) => { let dispatchCallCount = 0; // Pre-queued dispatch responses: first call returns "skip", second returns "stop" const dispatchResponses = [ - { action: "skip" as const }, + { action: "skip" as const, reason: "already-active" as const }, { action: "stop" as const, reason: "done", level: "info" as const }, ]; const deps = makeMockDeps({ @@ -2051,6 +2051,10 @@ test("autoLoop handles dispatch skip action by continuing", async (t) => { deriveCalls.length >= 2, "deriveState should be called at least twice (one per iteration)", ); + assert.ok( + !deps.callLog.includes("pauseAuto"), + "single already-active skip should not pause auto-mode", + ); }); test("autoLoop drains sidecar queue after postUnitPostVerification enqueues items", async (t) => {