fix: resolve missing_disposition recovery loop for blocked issues with active blockers

When an issue is `blocked` with unresolved first-class blockers, it has a
valid disposition. But `classifySourceRecoveryRevalidation()` gated the
`blocked + unresolvedBlockerCount > 0` check behind `durableSourceChange`,
which was only true when status/assignee/blockers/etc. actually changed in
the PATCH. Heartbeat comment-only PATCHes had `durableSourceChange = false`,
so the function returned `null` (no resolution) even though the issue was
correctly blocked. This caused a `missing_disposition` recovery action to
stay `active` indefinitely, waking the agent on every scheduler tick.

Fix: move the `blocked + unresolvedBlockerCount > 0` check before the
`read_projection` early-return gate so it fires on every revalidation
regardless of what changed. A blocked issue with active blockers always
has a valid disposition.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Van Echeverri 2026-06-03 06:42:40 -04:00
parent 70b1a9109d
commit 02b2fecdf7
1 changed files with 11 additions and 0 deletions

View File

@ -995,6 +995,17 @@ export function issueRoutes(
return "Recovery action became stale because the source issue was manually moved from blocked to todo.";
}
// Check blocked+blockers before any trigger-based early returns: a blocked issue with
// active first-class blockers always has a valid disposition regardless of what changed.
// This prevents recovery-action loops where the issue is correctly blocked but the
// missing_disposition action stays active because no "durable change" has occurred.
if (issue.status === "blocked") {
const readiness = await svc.getDependencyReadiness(issue.id);
if (readiness.unresolvedBlockerCount > 0) {
return "Recovery action became stale because the source issue now has unresolved first-class blockers.";
}
}
if (input.trigger === "read_projection") return null;
if (
input.trigger === "comment" &&