fix(recovery): return ownership without falsifying blocked status (BLO-19123) - #1548
Conversation
|
🔗 Paperclip issue: BLO-19123 |
1 similar comment
|
🔗 Paperclip issue: BLO-19123 |
|
@ally please review at head 3d08a68 — BLO-19123 ownership-only restore. Focus, in priority order:
Context: this is the drain lever for 333 rows that are correctly |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 3d08a68
Critical Issues (0)
Important Issues (0)
Suggestions (1)
- [native-codex]
server/src/routes/issues.ts:8676— Theblocked+blockedownership-only path preserves the existing unresolved-first-class-blocker guard keyed onoutcome === "blocked", whilerestored+blockedwould bypass that invariant. Consider adding a focused regression assertion that a blocked outcome with no unresolved first-class blocker still returns 422 even whenreturnOwnerAgentIdis present; the existing guard appears correctly ordered, but this would lock the property to the new path.
Strengths
- The implementation preserves the source issue's genuine
blockedstatus while returning ownership, so blocker-resolution wakeups target the original working agent. - Return-owner validation and the user-assigned refusal prevent invalid or conflicting assignee states.
- The focused tests cover the new behavior, user ownership precedence, and terminated return-owner rejection.
Recommended Action
- Consider the suggestion above opportunistically.
- The reviewed change is otherwise ready for the PR's remaining checks.
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 3d08a68
Critical Issues (0)
Important Issues (0)
Suggestions (1)
- [native-codex]
server/src/routes/issues.ts:8676— Theblocked+blockedownership-only path preserves the existing unresolved-first-class-blocker guard keyed onoutcome === "blocked", whilerestored+blockedwould bypass that invariant. Consider adding a focused regression assertion that a blocked outcome with no unresolved first-class blocker still returns 422 even whenreturnOwnerAgentIdis present; the existing guard appears correctly ordered, but this would lock the property to the new path.
Strengths
- The implementation preserves the source issue's genuine
blockedstatus while returning ownership, so blocker-resolution wakeups target the original working agent. - Return-owner validation and the user-assigned refusal prevent invalid or conflicting assignee states.
- The focused tests cover the new behavior, user ownership precedence, and terminated return-owner rejection.
Recommended Action
- Consider the suggestion above opportunistically.
- The reviewed change is otherwise ready for the PR's remaining checks.
…O-19123) The ownership-only restore is hung on `outcome: "blocked"` specifically so it inherits the unresolved-first-class-blocker guard; `restored` + `blocked` would have returned ownership without ever proving the row is genuinely blocked. That makes the guard load-bearing rather than incidental, and nothing pinned it against the path that has something to gain from skipping it. Assert that a blocked resolution with a resolvable `returnOwnerAgentId` present still returns 422 when no `blocks` relation backs the status, and that the refusal is total: the row keeps the manager as assignee rather than quietly going home while the false `blocked` stays in place. Raised by Ally review on #1548. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 7c46737
Critical Issues (0)
Important Issues (0)
Suggestions (0)
Strengths
- The new ownership-only path keeps genuinely dependency-blocked issues in
blockedwhile returning ownership to the original agent. - The unresolved first-class-blocker check runs before the transactional mutation, and the added regression test verifies that an unsupported blocked state cannot be handed back.
- Return-owner validation is applied consistently, and human-owned issues are protected from conflicting agent assignment.
Recommended Action
- The reviewed change is ready for the PR's remaining checks.
…O-19123) The stranded-recovery drain has no safe lever for its largest population. 360 issues are parked on the CTO/CEO with an active recovery action whose `returnOwnerAgentId` is a different agent, but 333 of them have genuinely unresolved blockers, so `blocked` is the correct status. The only existing hand-back requires moving the issue to `todo`, which would falsify state. Ownership is what is wrong on those rows, not status. It matters because `reconcileResolvedBlockerDependents` wakes `assigneeAgentId` when a blocker resolves — so while the manager holds the row, the agent that was actually doing the work is never woken. Extend the resolve endpoint so `outcome:"blocked"` + `sourceIssueStatus: "blocked"` also returns ownership to `returnOwnerAgentId`. That outcome already refuses to resolve unless the source issue has an unresolved first-class blocker, so it cannot be used to launder a row that is not really blocked — which is why this rides on `blocked` rather than adding a `restored`+`blocked` combination that would bypass that guard. The wake is already gated on `todo`, so an ownership-only restore correctly fires no wake; the blockers-resolved sweep wakes the (now correct) assignee later. Two safety gaps on this endpoint are closed at the same time, both of which the sibling PATCH blocked->todo path already handled: - The return owner is now validated through `normalizeIssueAssigneeAgentReference`, so a hand-back can no longer park work on a terminated or invalid-org-chain agent. This endpoint previously wrote the id straight to `svc.update`, which does not guard assignment. - A user-assigned issue is left with its human owner. This path sets `assigneeAgentId` without clearing `assigneeUserId`, so handing back would have left both set. Behaviour is unchanged for any action without a `returnOwnerAgentId`, which is every existing `outcome:"blocked"` caller in the suite. Refs BLO-19123
…O-19123) The ownership-only restore is hung on `outcome: "blocked"` specifically so it inherits the unresolved-first-class-blocker guard; `restored` + `blocked` would have returned ownership without ever proving the row is genuinely blocked. That makes the guard load-bearing rather than incidental, and nothing pinned it against the path that has something to gain from skipping it. Assert that a blocked resolution with a resolvable `returnOwnerAgentId` present still returns 422 when no `blocks` relation backs the status, and that the refusal is total: the row keeps the manager as assignee rather than quietly going home while the false `blocked` stays in place. Raised by Ally review on #1548. Co-Authored-By: Claude <noreply@anthropic.com>
7c46737 to
db1763c
Compare
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: db1763c
Critical Issues (0)
Important Issues (0)
Suggestions (0)
Strengths
- The new
blockedresolution path preserves the issue's genuine blocked status while returning ownership to the original agent. - The unresolved first-class-blocker check runs before mutation, preventing ownership hand-back for an unsupported blocked state.
- Return-owner validation is applied consistently, and user-owned issues are protected from conflicting agent assignment.
- Focused tests cover the ownership-only path, missing-blocker rejection, user ownership, and terminated return-owner handling.
Recommended Action
- The reviewed change is ready for the PR's remaining checks.
Thinking Path
Linked Issues or Issue Description
Refs BLO-19123 — Fix stranded-recovery re-home ratchet: route infra faults back, stop deps ownership transfer, then drain 290 manager-parked rows
Follows the merged F1/F2/F3a/F4 work in #1192 and F3b in #1498. Those are merged and deployed (
deployed-commitannotation = master head), which satisfies this issue's "fix forward before draining" ordering constraint. This PR supplies the missing drain lever that issue's description anticipated ("ownership-only restore … no existing path does this; needs a small extension").What Changed
outcome:"blocked"+sourceIssueStatus:"blocked"onPOST /issues/:id/recovery-actions/resolvenow returns ownership toreturnOwnerAgentIdwhile the issue staysblocked(an ownership-only restore). It recordshanded_back.normalizeIssueAssigneeAgentReference, so a hand-back can no longer park work on a terminated /pending_approval/ invalid-org-chain agent. This endpoint previously wrote the id straight tosvc.update, which does not guard assignment — a pre-existing gap on therestored+todopath.assigneeAgentIdwithout clearingassigneeUserId, so a hand-back would have left both set.Why this rides on
blockedrather than a newrestored+blockedcombination:outcome:"blocked"already refuses to resolve unless the source issue has an unresolved first-classblocksrelation (routes/issues.ts:8678). That guard is keyed on the outcome, so it cannot be used to launder a row that is not really blocked.restored+blockedwould bypass it.Verification
Focused suite, run in a worktree at master head:
Isolating just the new tests confirms they execute rather than being filtered out:
Typecheck:
tsc -p server/tsconfig.json --noEmitreports 12 errors, an identical set before and after this change (verified by typechecking the parent commit's version of the same files and diffing the error sets). They are pre-existing — stale@paperclipai/dbdist and the knownmonitorScheduledBy: "manager"widening — and none are in the changed range.Note for anyone re-running: this test file uses embedded Postgres and needs ~115 s of startup before any test runs. A 120 s timeout dies during collection and reports nothing; use ≥300 s.
Risks
outcome:"blocked"previously never changed the assignee. It now returns ownership when the action carries areturnOwnerAgentId. Behaviour is unchanged when it does not — which is every existingoutcome:"blocked"caller in the suite (both pre-existing tests pass untouched). The UI's "resolve as blocked" control is the one human-facing caller; reviewers should confirm returning ownership is the intent there. I believe it is: leaving the row on the manager is precisely the defect BLO-19123 exists to fix.handed_backinstead ofblockedwhen ownership moves. This does not affect recovery metrics:classifyRecoveryHandoffderives its classes from theownerAgentId/returnOwnerAgentId/finalAssigneeAgentIdrelationship and never reads the storedoutcomecolumn.Model Used
Claude Opus 4.5 (
claude-opus-4-5), 1M context, extended thinking, with tool use and code execution, running as the Paperclip Release Engineer agent (claude_k8sadapter).Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template