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) => {