fix(recovery): stop escalating dependency-blocked issues as stranded (BLO-19124) - #1062
Open
allyblockcast[bot] wants to merge 1 commit into
Open
fix(recovery): stop escalating dependency-blocked issues as stranded (BLO-19124)#1062allyblockcast[bot] wants to merge 1 commit into
allyblockcast[bot] wants to merge 1 commit into
Conversation
…(BLO-19124) `issue_dependencies_blocked` is the dispatcher declining to run an issue whose blockers are still open — a DAG node correctly waiting its turn, not a lost execution path. Its own cancellation reason promises "Paperclip will wake the assignee when blockers resolve", and that wake arrives via listWakeableBlockedDependents once the blocker closes. Because the code is a member of NON_RETRYABLE_CONTINUATION_ERROR_CODES, the stranded-issue sweep treated it as a non-retryable failure and escalated every occurrence to `blocked` with a recovery action and a recovery owner. Measured on one inbox: 158 of 161 active recovery actions were this code, all 158 with a genuinely unresolved blocker. Escalating is worse than a no-op — escalateStrandedAssignedIssue reassigns the issue, so the dependency wake then fires at an agent that no longer owns the work. Re-evaluate readiness at sweep time via the same listDependencyReadiness the dispatcher gate used, and skip the escalation while that gate would still refuse. Keep escalating when the issue IS dependency-ready, because "dependency-blocked with nothing blocking it" is a real defect and is exactly the blocked-with-zero- blockers state this ticket forbids. The Set membership stays: it also governs "do not burn retry attempts", which is correct for a wait. Only the escalation is suppressed. Refs: BLO-19124 Co-Authored-By: Claude <noreply@anthropic.com>
Author
1 similar comment
Author
Author
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Important Issues (1)
Suggestions (1)
Strengths
Recommended Action
This PR is authored by |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Thinking Path
Linked Issues or Issue Description
Refs: BLO-19124 — https://paperclip.blockcast.net/BLO/issues/BLO-19124
Problem or motivation
issue_dependencies_blockedis emitted by the dispatcher's dependency gate(
heartbeat.ts→cancelQueuedRunForBlockedDependencies) whenlistDependencyReadinessreports an issue is not dependency-ready. Its owncancellation reason promises "Paperclip will wake the assignee when blockers
resolve" — that wake arrives via
listWakeableBlockedDependentsonce the blockercloses. It is a wait, not a lost execution path.
Because the code is a member of
NON_RETRYABLE_CONTINUATION_ERROR_CODES, thestranded-issue sweep treated it as a non-retryable failure and escalated every
occurrence:
blockedstatus, a recovery action, and reassignment to a recoveryowner. On a single inbox that produced 158 of 161 active recovery actions, every
one of which had a genuinely unresolved blocker and nothing an owner could act on.
The oldest such action had been active for seven weeks.
Proposed solution
Re-evaluate readiness at sweep time and skip the escalation when the dispatcher's
gate would still refuse for the same reason. Keep escalating when the issue is
dependency-ready, because "dependency-blocked with nothing blocking it" is a real
defect that must stay visible.
Alternatives considered
Removing
issue_dependencies_blockedfromNON_RETRYABLE_CONTINUATION_ERROR_CODESentirely was rejected: that Set also governs "do not burn retry attempts", which is
correct behaviour for a dependency wait. Suppressing only the escalation is the
narrower change and leaves the retry semantics untouched.
Trusting the failing run's recorded evidence instead of re-querying was also
rejected — the evidence can be minutes to days stale, and a blocker that has since
closed must not be silently skipped.
What Changed
server/src/services/recovery/service.ts: added a guard ahead of thenon_retryableescalation branch that callsissuesSvc.listDependencyReadinessfor the issue and
continues without escalating whenisDependencyReadyisfalse. Uses the same readiness function as the dispatcher gate that produced the
error code, so the two cannot disagree.
issue_dependencies_blockedliteral intoDEPENDENCY_BLOCKED_ERROR_CODEand referenced it fromNON_RETRYABLE_CONTINUATION_ERROR_CODES, so the Set membership and the new guardcannot drift apart.
dependencyWaitSkippedcounter to the sweep result so suppressed waitsare observable rather than being folded anonymously into
skipped.server/src/__tests__/heartbeat-process-recovery.test.ts: two paired regressiontests (see Verification).
Verification
CI is the signal for this PR.
pnpm installdid not converge in this workspace(>20 min,
node_modulesnever populated), so I could not complete a local vitestrun and have left that checklist box unchecked rather than claim a green I do not
have. The relevant job is the server test suite over
server/src/__tests__/heartbeat-process-recovery.test.ts.The two new tests are deliberately disjoint, so neither a blanket suppression nor
the current blanket escalation can pass both:
open" — asserts
dependencyWaitSkipped === 1,escalated === 0, the issuestays
in_progresswith its originalassigneeAgentId, and no recovery issueor comment is created.
blocking it" — same error code, blocker
done, assertsdependencyWaitSkipped === 0andescalated === 1.Reverting the guard should fail (1); widening it to suppress on the error code
alone should fail (2). Reviewers: please confirm both fail in those two directions
rather than taking the pairing on trust.
Field evidence for the premise (CEO inbox, 2026-08-05): 166
blockedissues,143 active recovery actions, all
stranded_assigned_issue. 128 of 143 (89.5%)carry
latestRunErrorCode: issue_dependencies_blocked, and every one of those 128reports
unresolvedBlockerCount >= 1. Two sampled at random and verified againstthe authoritative
blockedByhydration:BLO-9658→ blocked byBLO-9663, statustodo, assigned to a human — whileits recovery action instructs an agent owner to "fix the runtime/adapter
failure". Its
timeoutAtelapsed 3 days ago and the row is stillactive.BLO-4296→ blocked byBLO-8329, statusblocked.Both are real explicit
blocksedges, so both are suppressed by this guard.Risks
Low, and deliberately fail-closed. The guard only ever suppresses an escalation,
never creates one, and only for a single error code. If the readiness lookup returns
no entry for the issue the guard does not fire and the pre-existing escalation path
runs unchanged.
The one behavioural shift worth naming: an issue that is genuinely stuck behind a
blocker that is itself dead will no longer be surfaced by this sweep. That case was
never served by this path anyway — the escalation reassigned it to an owner who
could not act — and it needs a blocker-liveness check on the root rather than
retry-exhaustion on the child. Tracked as follow-up scope on BLO-19124.
Model Used
Claude Opus 5 (
claude-opus-5), 1M context, extended thinking, with tool use andcode execution via Claude Code.
Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue templatepnpm installdid not converge in this workspace, so CI is the first real run of these tests