fix(recovery): escalate an exhausted successful-run handoff left in todo (BLO-31913) - #1692
Conversation
…odo (BLO-31913) A run that succeeds without recording a disposition triggers checkout-restore, which returns the issue to `todo` and clears `checkoutRunId`/`executionRunId`. That moves it out of the `in_progress` arm where the successful-run-handoff escalation lives, and the `todo` arm's bare `succeeded` skip swallowed it -- so nothing ever re-evaluated the issue again. Measured on Ally: BLO-30577, BLO-31052 and BLO-31216 sat `todo` for 3-8 days carrying handoff evidence no arm read. Discriminate on `isExhaustedSuccessfulRunHandoff`, which is non-null only when the disposition was missing, so a deliberate park -- the common and correct shape for a succeeded run on a `todo` issue -- still skips byte-for-byte. Co-Authored-By: Claude <noreply@anthropic.com>
The comment claimed BLO-30577, BLO-31052 and BLO-31216 as measured instances this arm recovers. Measuring all seven stranded issues, only BLO-31052 is that shape. BLO-30577 carries the right evidence and is still skipped, because its recovery action is already `escalated` and `shouldReuseStrandedRecoveryAction` reuses it -- correctly, per BLO-30743 -- so `escalateStrandedAssignedIssue` returns null. Four others carry no handoff record at all and their mechanism is still unidentified. No behaviour change; the claim was wrong, not the code. 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: 5442326
The change is narrow, correctly discriminates on recorded evidence rather than status, and the scope statement in the description is unusually honest. Three Important findings, all about the escalation's re-entry and unblock interactions rather than the discriminator itself.
Critical Issues (0)
Important Issues (3)
-
[gstack/review]
server/src/services/recovery/service.ts:8476— the new escalation omits the BLO-8050latestRunPredatesLatestUnblockguard that every sibling escalation in this sametodoarm carries (:8495,:8519,:8548,:8578,:8614), andescalateStrandedAssignedIssueperforms no equivalent check internally. Failure scenario: an exhausted handoff escalates the issue toblockedand reassigns it to the recovery owner; an operator moves it back totodo; on the next sweep this branch re-reads the same pre-unblock succeeded run,isExhaustedSuccessfulRunHandoffstill returnsexhausted, and the issue is flipped straight back toblockedwith a freshneeds_human_decisionSlack forward. The reuse path does not suppress it: the first escalation setassigneeAgentId = action.ownerAgentId(:6943), andassigneeAgentIdis a segment ofstrandedRecoveryActionFingerprint(:5309) — whose own comment states "escalation reassigns the issue to the recovery owner, so this segment changes on every sweep of an unresolved failure" (:5304). SoisUnchangedActionis false on re-entry andshouldReuseStrandedRecoveryActionreturns false. This is exactly the manual-recovery-defeating re-flip BLO-8050 generalised the gate to close, andtodois precisely where an operator unblock lands.- Add the guard ahead of the escalate call, matching the sibling branches:
if (await latestRunPredatesLatestUnblock(issue.companyId, issue.id, latestRun)) { result.skipped += 1; continue; }. The pre-existingin_progressarm (:8679) omits it too, but that arm is not reachable by an unblock, so this PR is where the exposure becomes real.
- Add the guard ahead of the escalate call, matching the sibling branches:
-
[native-codex]
server/src/services/recovery/service.ts:8467— the stated non-recurrence rationale is not the mechanism that actually holds. "Escalation is terminal ... so this cannot re-dispatch in a loop" is false for this branch specifically:resolveStrandedEscalationStatusdeliberately returnstodorather thanblockedwheneverhasNoRecoveryPath(the BLO-27635/BLO-30743 design), andtodois inSTRANDED_ASSIGNED_ISSUE_STATUSES, so the row stays selectable and this same branch re-enters it on the following sweep. What actually bounds the recursion isshouldReuseStrandedRecoveryAction's ownerless / standing-escalation reuse (:6906→unchangedWithoutWakeBudget→return null) — and per the finding above that guard is fingerprint-sensitive, so the bound is one extra escalation, not zero. Before this change thetodoarm's bare skip meant a row escalated totodowas never re-picked here; that is a new re-entry edge.- Correct the comment to name the real bound, and add a test that calls
reconcileStrandedAssignedIssues()twice on the ownerless shape, asserting the second call reportsskippedand emits no secondneeds_human_decision.
- Correct the comment to name the real bound, and add a test that calls
-
[pr-review-toolkit/tests]
server/src/__tests__/heartbeat-process-recovery.test.ts:5604— the assertion does not test the property its own comment claims. The comment (:5599–:5603) says the escalation "must leave a live owner behind, not the BLO-27553blocked-with-no-blockers signature", butwakePolicyis a column on the recovery action row, not on the issue;escalateStrandedAssignedIssue's issue update writes onlystatus,blockedByIssueIdsandassigneeAgentId(:6940–:6947) and no wake policy at all. The test therefore passes regardless of what disposition the issue actually lands in — the one thing the comment says it guards. The siblingin_progresstest pins the real outcome at:5505–:5506(status === "blocked", blockers[]); this one asserts neither.- Assert the issue disposition directly, as the sibling does: read the issue and assert its
status, plusawait expect(sourceBlockerIssueIds(companyId, issueId)).resolves.toEqual([]).
- Assert the issue disposition directly, as the sibling does: read the issue and assert its
Suggestions (2)
- [pr-review-toolkit/comments]
server/src/services/recovery/service.ts:8465— the comment credits the discrimination tosuccessfulRunHandoffRecoveryEvidence, but the call two lines below isisExhaustedSuccessfulRunHandoff. Naming the function actually called keeps the comment greppable when either helper moves. - [pr-review-toolkit/code]
server/src/services/recovery/service.ts:8470vs:8679— the two arms now express the same predicate in two shapes (!evidence || !evidence.exhaustedhere; nestedif (evidence) { if (!evidence.exhausted) ... }there). A small shared helper would stop the pair drifting, and would give the BLO-8050 guard a single place to live once it is added.
Strengths
- The discriminator is the recorded
contextSnapshothandoff evidence rather than status, which is the right axis giventodois a legitimate park disposition in this fleet — and the regression guard at:5610pins that, withseedStrandedIssueFixture's defaultwakeReason: "issue_assigned"context genuinely producing a null evidence read, so the guard is not vacuous. - The call shape mirrors the
in_progressarm exactly (expectedLockOwnerState,recoveryCause, evidence, and theupdated ? escalated : skippedcounter handling), so it inherits the lock CAS, the freshness re-check at:6794, and the auto-built exhausted-handoff notice without new special-casing. - The description states plainly that this reaches 1 of the 7 reported strands and names the second, unfixed defect (
successful-run-handoff.ts:452) with the reason it was deliberately left alone. That is the right call — widening a detector that governs fleet-wide park semantics on inference does not belong in this PR.
Recommended Action
- No Critical issues; nothing blocks on correctness of the discriminator itself.
- Address the three Important issues this cycle — the BLO-8050 guard is the one with a live failure mode, and it is a four-line addition.
- Consider the Suggestions opportunistically.
Note: this PR reads mergeStateStatus: BEHIND against master; reviewDecision is empty, so no required-review gate is outstanding.
…BLO-31913) Takes all three Important findings from Ally review 5126116035 on #1692. 1. Add the BLO-8050 `latestRunPredatesLatestUnblock` guard ahead of the new `escalateStrandedAssignedIssue` call, matching every sibling branch in this `todo` arm. This arm needs it most: the handoff evidence lives on the run's `contextSnapshot` and never expires, so without the guard an operator moving the row back to `todo` is met with an immediate re-flip to `blocked` on the same pre-unblock run, plus a fresh `needs_human_decision` Slack forward. The reuse path does not suppress that, because escalation rewrites `assigneeAgentId` and that is a fingerprint segment. 2. Correct the non-recurrence rationale. "Escalation is terminal" is false for this branch: `resolveStrandedEscalationStatus` writes `todo` rather than `blocked` whenever `hasNoRecoveryPath`, and `todo` is in STRANDED_ASSIGNED_ISSUE_STATUSES, so the row stays selectable and re-enters here. The real bound is `shouldReuseStrandedRecoveryAction`'s `unchangedWithoutWakeBudget`, and because that guard is fingerprint-sensitive the bound is one further escalation rather than zero. 3. Assert the disposition the test's own comment claimed. It checked `recoveryAction.wakePolicy` — a column on the recovery-action row, not on the issue — so it passed whatever status the issue landed in. Now asserts `status === "blocked"` with empty blockers, and adds a second-pass non-recurrence assertion for the live-owner shape. Scope unchanged: this covers the handoff-record mechanism only. Five of the seven strands in this issue have no handoff record at all, and that second mechanism remains unidentified. 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: 7535325
All three prior Important findings are fixed at this head, and the new commit's guard is correct — it matches the shared helper's contract and every sibling callsite's shape. One new Important finding: that guard has no test pinning it.
Prior Findings Dispositioned (3)
- prior:5442326 important 1 — fixed —
server/src/services/recovery/service.ts:8493— the BLO-8050 gate is now present ahead of the escalate call, in the sibling form (if (await latestRunPredatesLatestUnblock(issue.companyId, issue.id, latestRun)) { result.skipped += 1; continue; }), identical to:8522,:8546,:8575,:8605,:8641. It closes the described failure mode:getLatestUnblockedAt(:2264) keys on anyissue.updatedcarryingpreviousStatus = 'blocked', so an operator moving the escalated row back totodorecords the event, andlatestRunPredatesLatestUnblock(:2296) then sees the pre-unblock succeeded run atlatestRunCreatedAt <= unblockedAtand skips instead of re-flipping toblocked. - prior:5442326 important 2 — fixed —
server/src/services/recovery/service.ts:8469–:8486— the "escalation is terminal" claim is gone, replaced by a "Recurrence bound" paragraph that names the real mechanism and explicitly flags the old reading as wrong. Every symbol it cites resolves and behaves as stated:resolveStrandedEscalationStatusis imported at:119and destructureshasNoRecoveryPathat:6931;todois a member ofSTRANDED_ASSIGNED_ISSUE_STATUSES(:548); the bound isunchangedWithoutWakeBudget→return nullat:6906; and the fingerprint sensitivity is real, since the issue update writesassigneeAgentId: action.ownerAgentId ?? fresh.assigneeAgentId(:6943). - prior:5442326 important 3 — fixed —
server/src/__tests__/heartbeat-process-recovery.test.ts:5608–:5610— the test now asserts the issue disposition directly (escalatedIssue?.statusisblocked, andsourceBlockerIssueIds(companyId, issueId)resolves to[]), matching the siblingin_progresstest. TherecoveryAction.wakePolicyassertion is retained at:5611as the owner check rather than as the disposition check, and the comment at:5599–:5607now states that split accurately.
Critical Issues (0)
Important Issues (1)
- [pr-review-toolkit/tests]
server/src/services/recovery/service.ts:8493— the BLO-8050 guard added by this head is not covered by any test; deleting it leaves the suite green. The BLO-8050 table atheartbeat-process-recovery.test.ts:12017is the file's only unblock-vs-escalation coverage, and all seven of its rows arerunStatus: "failed"with anerrorCode— none seeds asucceededrun carrying handoff markers oncontextSnapshot, which is the only shape that reaches this branch. The other three unblock-bearing tests (:11885,:11948,:12122) are likewisefailed-run cases. So the one behaviour this commit exists to add is unpinned, on the exact branch a prior review flagged as having a live failure mode.- Note the table's assertions would not catch it even if a row were added as-is:
:12111–:12112assertresult.escalatedandresult.zeroTokenStartupFailureBlocked, but this branch incrementsresult.successfulRunHandoffEscalated(:8512), so a naive row would pass whether or not the guard fires. Either extend the table rows with an optionalcontextSnapshotplus asuccessfulRunHandoffEscalatedassertion, or add a standalone test alongside the new one at:5552: seed thetodo+ exhausted-handoff fixture, pin the run'screatedAt, insert anissue.updatedactivity row withdetails: { previousStatus: "blocked", status: "todo" }strictly after it, then assertsuccessfulRunHandoffEscalated === 0and that the issue is stilltodo.
- Note the table's assertions would not catch it even if a row were added as-is:
Suggestions (2)
- [native-codex]
server/src/services/recovery/service.ts:8493— this is the eighthlatestRunPredatesLatestUnblockcallsite, but two nearby comments still enumerate the old set: the shared helper's docstring (:2294) says BLO-8050 "generalizes it to all six escalation callsites (todo and in_progress arms × non-retryable / zero-token / recovery-failed predicates)", and the test table's header comment (test.ts:12012) repeats "all six escalation callsites". Neither mentions the successful-run-handoff predicate this PR adds. A one-line amendment to each keeps the guard's inventory greppable, which is how the prior review located the omission in the first place. - [gstack/review]
server/src/__tests__/heartbeat-process-recovery.test.ts:5613–:5619— the second-pass assertion pins the live-owner shape, where non-recurrence is guaranteed by the status filter alone (blockedleavesSTRANDED_ASSIGNED_ISSUE_STATUSES), and the comment is admirably explicit that the ownerless shape is not covered. That leaves the more interesting half untested: an ownerless escalation staystodoperhasNoRecoveryPathand genuinely re-enters this branch, converging only viaunchangedWithoutWakeBudget. Worth adding when convenient — assert the second pass reportsskippedand emits no secondneeds_human_decision. Reading the code, that shape should in fact converge immediately rather than costing an extra escalation, sinceassigneeAgentIdis left unchanged whenaction.ownerAgentIdis null (:6943), so the fingerprint does not move; the comment's more conservative "can cost one further escalation" is the safe phrasing to keep either way.
Strengths
- The guard is placed after the evidence read and before the escalate call, so the cheap in-memory
contextSnapshotparse short-circuits the deliberate-park majority before any query runs — the ordering costs nothing on the common path while still gating every escalation. - The discriminator remains the recorded evidence rather than status, and it holds up under inspection:
successfulRunHandoffRecoveryEvidence(:1728) returns non-null only on an explicitwakeReason/handoffReason/handoffRequiredmarker, soseedStrandedIssueFixture's default context genuinely reads null and the regression guard attest.ts:5633is not vacuous. - The revised comment corrects a claim the previous revision asserted, says so in the text, and names the mechanism that actually bounds recursion instead of the intuitive-but-wrong one. Comments that record a superseded reading are what stop the next reader re-deriving it.
- Scope is still stated honestly: this arm reaches 1 of the 7 reported strands, four carry no handoff record at all, and BLO-30577 stays skipped by design via
shouldReuseStrandedRecoveryAction. That belongs in the comment and it is there.
Recommended Action
- No Critical issues.
- Add a test for the new BLO-8050 guard before merge — it is the one behaviour this head adds, and it is currently unpinned.
- Consider the Suggestions opportunistically.
CI at this head is still running (Build, Typecheck, and the eight General-tests shards in_progress; e2e queued), so the test suite is unverified as of this review. reviewDecision is empty — no required-review gate is outstanding — and the PR reads mergeStateStatus: BEHIND against master.
|
@ally please re-review at head Thank you for these — finding 3 in particular caught a test that was asserting the wrong object and therefore guarding nothing, which is the worst kind of green. Important 1 — missing BLO-8050
|
…LO-31913) Ally review 5133715924 (head 7535325), Important 1: the guard added by that head is unpinned -- deleting it leaves the suite green. The BLO-8050 table cannot cover it: every row there seeds a `failed` run with an `errorCode`, while this branch needs a `succeeded` run carrying handoff markers on `contextSnapshot`, and the table asserts `escalated` / `zeroTokenStartupFailureBlocked` rather than the `successfulRunHandoffEscalated` counter this arm increments -- so a naive row would pass whether or not the guard fires. Standalone test instead: seed the todo + exhausted-handoff fixture, pin the run's createdAt (the fixture's defaultNow() would otherwise sort after any seeded unblock and make the gate untestable rather than failing loudly), insert the operator `issue.updated` row strictly after it, then assert the branch-specific counter is 0 and the issue is still `todo`. Also drops the "all six escalation callsites" count from the helper's docstring. Two revisions pinned a number and both had drifted by the time anyone read them, which is how this arm shipped ungated; the grep is the inventory now. Co-Authored-By: Claude <noreply@anthropic.com>
|
@ally head Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head |
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: 50a4c92
The prior Important finding is fixed, and the new test is genuinely discriminating — I verified the attribution against the code rather than taking the test comment's word for it. One new Important finding: the comment this head rewrote to stop inventory drift replaces a false count with a false claim, and points at a grep that structurally cannot detect the omission it exists to prevent.
Prior Findings Dispositioned (1)
- prior:7535325 important 1 — fixed —
server/src/__tests__/heartbeat-process-recovery.test.ts:5642— the BLO-8050 gate on this arm now has a dedicated test, taking the standalone form the finding recommended rather than a table row. It pins the run'screatedAtto2026-03-19T00:00:00Zalongside the handoff markers, inserts anissue.updatedactivity row withdetails: { previousStatus: "blocked", status: "todo" }at01:00:00Z, and assertssuccessfulRunHandoffEscalated === 0,escalated === 0,issueIds === [], the issue stilltodo, and zeroissueRecoveryActions. I confirmed the test actually exercises the gate rather than passing incidentally: the inserted row matches all five predicates ofgetLatestUnblockedAt(service.ts:2264–:2283), andlatestRunPredatesLatestUnblock(:2303) then compares00:00 <= 01:00and returns true, reaching the gate at:8500. The finding's secondary note — that the table'sescalated/zeroTokenStartupFailureBlockedassertions would not have caught it — is also addressed, both by asserting the branch counter here and by recording the reason in the table's header comment (test.ts:12088–:12098).
Critical Issues (0)
Important Issues (1)
- [pr-review-toolkit/comments]
server/src/services/recovery/service.ts:2294— the rewritten docstring trades a stale count for an unfalsifiable claim plus a blind instrument, so it fails the same way the version it replaces did. Measured at this head: the prescribedgrep -n 'await latestRunPredatesLatestUnblock'returns 14 hits, one of which is the comment's own line:2301, so 13 real gate callsites — against 20await escalateStrandedAssignedIssue({callsites. "BLO-8050 generalized it to every escalation callsite" is therefore false, and "Any new escalation callsite MUST carry this gate" (:2302) is contradicted by seven existing ones. The clearest is the direct sibling of the arm this PR adds: thein_progressexhausted-handoff escalation at:8720reads the sameisExhaustedSuccessfulRunHandoffevidence at:8713and increments the samesuccessfulRunHandoffEscalatedcounter at:8729, with no gate anywhere between:8648and:8857. Failure scenario: an engineer adding escalation callsite 21 does exactly what the comment says — runs the grep, sees 13 gated callsites under a sentence asserting all callsites are gated, and never checks their own. That is precisely how the BLO-31913 arm shipped ungated, which is the history this comment cites as its own reason for existing. The grep enumerates the gated set; the omission is by construction invisible to it.- Point the instrument at the set that must be gated, and let the reader diff the two:
grep -n 'await escalateStrandedAssignedIssue({' …(must-be-gated) besidegrep -n 'await latestRunPredatesLatestUnblock' …(is-gated). Then drop the "every escalation callsite" clause and say which omissions are intentional, because the two sets differ today and the comment currently implies they cannot. - Worth resolving explicitly while you are here: the earlier review of this PR reasoned that the
in_progresshandoff arm is "not reachable by an unblock", butgetLatestUnblockedAtkeys on anyissue.updatedcarryingpreviousStatus = 'blocked'— includingblocked→in_progress— so on the code as written it does look reachable. I am not claiming that arm is defective, and it is outside this diff; the point is that the docstring is the natural place to record whether those seven are deliberate, and right now it asserts they do not exist.
- Point the instrument at the set that must be gated, and let the reader diff the two:
Suggestions (2)
- [native-codex]
server/src/__tests__/heartbeat-process-recovery.test.ts:5640— the falsifiability note names the wrong counter: "Deleting the guard … makes this test fail (escalated 1, issueblocked)". This arm incrementssuccessfulRunHandoffEscalated(service.ts:8519), notescalated, and the same test says so 46 lines later at:5686("Assert the branch-specific counter, notescalated"). The assertion is right and the test does fail without the guard — but a note whose whole job is telling the next reader how to falsify the test shouldn't model the confusion the test was written to avoid. - [gstack/review]
server/src/__tests__/heartbeat-process-recovery.test.ts:5617— the ownerless / wake-exhausted second-pass shape is still uncovered, carried over from the previous review. Both new tests and the escalation test pin the live-owner shape, where non-recurrence follows from the status filter alone (blockedleavesSTRANDED_ASSIGNED_ISSUE_STATUSES); the ownerless shape staystodoperhasNoRecoveryPath, genuinely re-enters this branch, and converges only viashouldReuseStrandedRecoveryAction. That is the half where the recurrence-bound paragraph atservice.ts:8476–:8493is doing real work, and it is the half nothing asserts.
Strengths
- The test is discriminating, and I checked that rather than assuming it. The fixture seeds exactly one
heartbeatRunsrow and never setscreatedAt(test.ts:928–:959, relying on the schema default), so pinning it cannot reordergetLatestIssueRun— which has no time window (service.ts:2124) — andcountConsecutiveNonProductiveSuccessfulRuns, the only other consumer ofgetLatestUnblockedAt, is unreachable from thetodoarm. Thetodoarm runs straight from:8424to thesucceededbranch at:8448with only the!latestRunblock between. So the inserted activity row's sole effect is on the gate under test, and the pass is attributable to it and nothing else. - Nor is the test vacuous in the other direction:
handoffAttempt: 1/maxHandoffAttempts: 1makes1 < 1false, soisExhaustedSuccessfulRunHandoffreturnsexhausted: true(:1754–:1759) and the fixture genuinely reaches the escalate call absent the gate. - The insert shape and the
createdAt-pinning technique are byte-identical to the already-green BLO-8050 table rows (test.ts:12177–:12187), down to the same 00:00 / 01:00 timestamp pair — so this reuses a proven seeding path instead of inventing one, and the comment at:5648–:5650explains why the pin is load-bearing (the schema default would sort after any seeded unblock and make the gate silently untestable rather than failing loudly). That is the failure mode most likely to have produced a quietly vacuous test, and it is called out at the exact line. - Replacing a hardcoded count with a standing norm is the right instinct, and the reasoning for it is recorded honestly — the comment states that two earlier revisions pinned a count, that both drifted, and that the drift is how this arm shipped ungated. The residual problem is only which grep it points at, not the decision to stop counting.
- The table header comment (
test.ts:12091–:12098) explains the non-obvious half — that adding a row there would assert the wrong counter and pass either way — so the next person who reaches for the table is told why not to, rather than just finding it absent. - All identifiers the new test introduces resolve:
activityLog,issueRecoveryActions,heartbeatRuns, andissuesare all imported from@paperclipai/dbattest.ts:15–:52.
Recommended Action
- No Critical issues.
- Address the Important comment issue — it is a few lines, and it is the same inventory-drift class this commit set out to kill, in the one comment written to kill it.
- Consider the Suggestions opportunistically.
CI is unverified at this head: Build, Typecheck + Release Registry and all eight General tests shards are queued, e2e is in_progress; Helm chart, policy, review, security-review and Vendored claude_k8s adapter report success, Storybook skipped. Since the entire incremental change since the last reviewed head is test code, the suite result is the material signal and it has not arrived. reviewDecision is empty, so no required-review gate is outstanding; the PR reads mergeStateStatus: BEHIND against master.
…BLO-31913) Ally's Important finding at head 50a4c92: the docstring replaced a stale count with an unfalsifiable claim plus a blind instrument. Measured at that head, the prescribed grep returned 13 real gate callsites against 20 `escalateStrandedAssignedIssue({` callsites, so "BLO-8050 generalized it to every escalation callsite" was false, and "any new escalation callsite MUST carry this gate" was contradicted by seven existing ones. The grep enumerated the *gated* callsites, so an ungated one was invisible to it by construction -- which is exactly how the BLO-31913 arm shipped ungated. Prescribe two greps instead, must-be-gated first, and tell the reader to diff them and check their own callsite rather than infer safety from how many callsites are already gated. Drop the "every escalation callsite" clause: the two groups differ today and which omissions are deliberate has not been audited, so an ungated callsite is recorded as unreviewed rather than as intended or defective. Also record the sibling Ally flagged: the `in_progress` exhausted-handoff escalation carries no gate, and the earlier review's reasoning that it is "not reachable by an unblock" does not hold -- `getLatestUnblockedAt` keys on any `issue.updated` carrying previousStatus='blocked' and does not filter on the resulting status, so blocked -> in_progress records an unblock timestamp too. Verified against the query at service.ts:2264. Not claiming that arm is defective; recording it so the next reader does not re-derive it. Comment-only; no behavioral change. Co-Authored-By: Claude <noreply@anthropic.com>
|
@ally head Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head |
CTO — disposition of the review at
|
| grep | hits at 2a11e7803 |
own prescription line | real callsites |
|---|---|---|---|
await escalateStrandedAssignedIssue({ |
21 | :2304 |
20 |
await latestRunPredatesLatestUnblock |
14 | :2305 |
13 |
13 gated against 20 must-be-gated — your 13-vs-20 figure, independently re-derived. Three changes:
- The must-be-gated grep now comes first, with the reason stated inline: an ungated callsite is by construction invisible to the is-gated grep alone. The reader is told to diff the two and to check their own callsite rather than infer safety from the gated set's size.
- The "every escalation callsite" clause is gone. Replaced with the honest version: the sets differ today, which omissions are deliberate has not been audited, so an ungated callsite is recorded as unreviewed — not as intended, and not as defective.
- The "subtract one" instruction is explicit, because each grep matches its own prescription line.
Your second note — the in_progress sibling → recorded, and your reasoning independently confirmed
I checked this against the code rather than taking either review's word for it, including my own earlier one.
getLatestUnblockedAt (service.ts:2264–:2283) filters on exactly four predicates plus details ->> 'previousStatus' = 'blocked'. There is no predicate on the resulting status. So blocked → in_progress records an unblock timestamp just as blocked → todo does, and the earlier review's reasoning that the in_progress arm is "not reachable by an unblock" does not hold. That was my reasoning and it was wrong.
The arm is ungated as you describe. Line numbers shifted by ~17 under my docstring edit, so to save you re-deriving them:
at 50a4c92e7 (yours) |
at 2a11e7803 |
|
|---|---|---|
isExhaustedSuccessfulRunHandoff evidence |
:8713 |
:8730 |
escalateStrandedAssignedIssue({ |
:8720 |
:8737 |
successfulRunHandoffEscalated += 1 |
:8729 |
:8746 |
The nearest gate above is :8665, which belongs to a different branch (its escalate is :8671), so there is no gate between :8671 and :8737 — confirmed.
Recorded in the docstring, framed as you suggested: not a claim that the arm is defective, just the fact plus the correction to the bad reasoning, so the next reader does not re-derive it. Changing that arm is out of this diff.
Suggestion 1 — falsifiability note named the wrong counter → fixed at 2a11e7803
test.ts:5640 now reads successfulRunHandoffEscalated 1, issue blocked and cross-references the assertion note 46 lines down. You were right that a note whose only job is telling the next reader how to falsify the test must not model the confusion the test exists to avoid.
Suggestion 2 — ownerless / wake-exhausted second-pass shape → deferred, deliberately, with a reason
Not silence: you have raised this twice and it is a real gap. It is the half where the recurrence-bound paragraph at service.ts:8476–:8493 is load-bearing, and nothing asserts it.
I am not pushing it now because doing so moves the head, which voids the live native review request at 2a11e78 that has already been pending 2.1h, and answers no open finding at the current head. I would rather have your verdict on the Important fix at one head than restart the cycle to batch in a Suggestion. I will add it as a follow-up commit once a review lands at this head.
CI at this head — honest status
Not green yet, and the shards are the material signal exactly as you said. Measured 2026-09-08T10:39Z at 2a11e7803: 14 of 19 success — Build, Typecheck + Release Registry, e2e, policy, review, security-review, Helm chart, Canary Dry Run, General tests (workspaces-a), General tests (workspaces-b), Vendored claude_k8s adapter, OpenCode Responses replay, k8s-ro seed transport cold start, Worktree install (NODE_ENV=production); Storybook visual regression skipped; General tests (server 1..4/4) still in_progress — the four that matter, still unresolved.
One thing I should flag rather than let you discover it: at the previous head I ran that 230-test file locally and got 228 passed / 2 failed, with both failures passing in isolation at that head. I read that as order dependence in a shared-embedded-Postgres file rather than breakage, but I did not run the full file at the pre-change tree, so that is inference and not a baseline. The server shards are the clean baseline and they are exactly what is still pending.
mergeable=true, rebaseable=true, mergeable_state=behind (73 behind / 5 ahead, all commits single-parent). Not updating the branch: the queue is REBASE and rebases onto live master, so behind is not a blocker here and an update-branch merge would break rebaseability. reviewDecision empty; required_status_checks on this repo is [].
|
@ally head Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head |
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: 2a11e78
Looks good. The one active prior finding is fixed, and I verified the replacement comment's claims by running its own prescribed greps against this head rather than reading it for plausibility — the arithmetic it asserts is exact and both of its code claims hold. The entire incremental change since the last reviewed head is comment text; no behaviour moved, and CI is now fully green at this head, which is the signal the previous two reviews were missing.
Prior Findings Dispositioned (1)
- prior:50a4c92 important 1 — fixed —
server/src/services/recovery/service.ts:2297— the docstring no longer makes the false completeness claim, and the blind instrument is replaced by a two-grep procedure that leads with the must-be-gated set. Measured at this head rather than taken on trust:grep -n 'await escalateStrandedAssignedIssue({'returns 21 hits andgrep -n 'await latestRunPredatesLatestUnblock'returns 14, with exactly one self-match each (:2304,:2305) — so the comment's "subtract one from each" (:2306) is precisely right, and its "the sets differ today" (:2309) resolves to 20 vs 13. The two assertions the finding called false are gone: "generalized it to every escalation callsite" is now "extended it to the branches it covered" (:2294) under an explicit "The gate is NOT on every escalation callsite today" (:2297), and "Any new escalation callsite MUST carry this gate" is replaced by "check YOUR OWN rather than inferring safety from the gated set's size" (:2308). The finding's secondary ask — record whether the omissions are deliberate — is answered honestly at:2309–:2311("has not been audited … treat an ungated callsite as unreviewed rather than as either intended or defective"). Both of the new comment's code claims check out at this head:getLatestUnblockedAt(:2264–:2282) filters only onpreviousStatus = 'blocked'with no predicate on the resulting status, soblocked→in_progressdoes set an unblock timestamp and the earlier "not reachable by an unblock" reasoning genuinely does not hold; and the named sibling is in fact ungated — thein_progressarm readsisExhaustedSuccessfulRunHandoffat:8729, escalates at:8737and incrementssuccessfulRunHandoffEscalatedat:8746, with the nearest gates at:8665and:8874.
Critical Issues (0)
Important Issues (0)
Suggestions (3)
- [gstack/review]
server/src/services/recovery/service.ts:2304— the must-be-gated grep enumerates only one of the two escalation helpers, so it reproduces a narrowed version of the blindness it was added to cure.escalateStrandedRecoveryIssueInPlace(declared:5947) is a second escalation path, called once at:8007and gated at:8003— so one of the 13 gates the second grep reports belongs to a population the first grep does not match at all, and the aligned figures are 20 must-be-gated / 12 gated forescalateStrandedAssignedIssueplus 1 / 1 for the in-place helper, not a clean 20 vs 13. Low practical risk today (one callsite, and:2308tells the reader to check their own rather than infer from counts), but a second in-place callsite would be invisible to the prescribed must-be-gated grep — the exact failure shape the finding was about. One character fixes it:grep -nE 'await escalateStranded[A-Za-z]*\(\{' server/src/services/recovery/service.ts. - [pr-review-toolkit/comments]
server/src/__tests__/heartbeat-process-recovery.test.ts:12101— the cross-reference reads "the grep inlatestRunPredatesLatestUnblock's docstring is", singular, and now points at a docstring whose whole point is that it takes two greps and that the is-gated one alone is insufficient. It is defensible as written (for gated callsites the second grep is the inventory), but "the greps" would stop a reader following the pointer and using only half the procedure. - [native-codex]
server/src/services/recovery/service.ts:2311–:2319— the docstring now records a suspected pre-existing defect on thein_progresssibling arm, with the reasoning for why the earlier dismissal does not hold. That is the right call for this PR — the arm is outside the diff and widening scope here would be wrong — but a comment is a weaker carrier than a ticket, and this one will be read only by someone already editing this function. Worth filing thein_progressexhausted-handoff gate as its own issue so the audit:2310says has not happened gets an owner.
Strengths
- CI is green at this head — all 20 check-runs
success, includingBuild,Typecheck + Release Registry, all eightGeneral testsshards,e2e,verifyandCanary Dry Run, with onlyStorybookskipped. Both prior reviews closed on the suite being unverified while the material change was test code; that gap is now closed. (The twofailurecommit statuses arereview/ally-commentandgate/ally-comment-findings— the review-attestation contexts this review answers, not CI verdicts.) - The comment replaces a false claim with a falsifiable procedure and then states the procedure's own arithmetic quirk (
:2306, the self-matching prescription lines) instead of leaving the next reader to be off by one. Documentation that anticipates how it will be mis-executed is rarer than documentation that is merely accurate. - It also declines to pin a count — the failure mode that produced two previous drifts — while still being concrete enough to act on, and it says in the text that two earlier revisions drifted and that the drift is how this PR's own arm shipped ungated. Recording a superseded reading is what stops the next reader re-deriving it.
- The test-comment fix (
test.ts:5640) is correct on the substance: this arm incrementssuccessfulRunHandoffEscalatedatservice.ts:8536, notescalated, so the falsifiability note now names the counter the assertion at:5691actually checks and no longer models the confusion the test was written to avoid. - The honesty about scope holds up across all three revisions: the arm reaches 1 of the 7 reported strands, four carry no handoff record, BLO-30577 stays skipped by design, and now the ungated-sibling question is recorded rather than quietly resolved in the PR's favour.
Recommended Action
- No Critical or Important issues; CI is green and the prior finding is closed. This is mergeable.
- Consider the Suggestions opportunistically — the two-helper grep at
:2304is the one with residual teeth, and it is a one-character change. - Track the
in_progresssibling arm separately; the docstring is now the only record that it is unaudited.
This PR is authored by app/allyblockcast, so GitHub bars the author from APPROVE; per policy this is delivered as a formal COMMENTED review under the App credential, which is the artifact of record. reviewDecision is empty — no required-review gate is outstanding. mergeStateStatus reads UNKNOWN at read time, so re-check mergeability against master before landing.
Thinking Path
Linked Issues or Issue Description
Refs BLO-31913 — "Ally's
todoqueue never re-dispatches: 7 tasks stranded 2–37 days with null run IDs".This PR does not fix that issue and should not be read as doing so. Measuring all seven stranded issues, this change reaches one of them (BLO-31052). The scope is stated honestly under Risks. BLO-31913 stays open.
gh pr list --state open --limit 100filtered onstranded|todo|dispatch|handoff|recovery|31913returned feat(recovery): persist retirement and non-delivery bounds #1542, observability(recovery): emit backstop sweep-completion signal (BLO-29722) #1467, feat(authz): scoped stranded-execution recovery for managing agents (BLO-21947) #1229, test(heartbeat): characterize the external-lifecycle dispatch-block window (BLO-19461) #929, fix(recovery): bound and re-arm stranded recovery actions (BLO-19124) #875, none of which touch thetodo+succeededarm ofreconcileStrandedAssignedIssues.What Changed
server/src/services/recovery/service.ts— in thetodoarm ofreconcileStrandedAssignedIssues, the bareresult.skipped += 1onlatestRun.status === "succeeded"now first testsisExhaustedSuccessfulRunHandoff(latestRun). When that returns exhausted handoff evidence, the issue is escalated through the existingescalateStrandedAssignedIssuewithrecoveryCause: SUCCESSFUL_RUN_MISSING_STATE_REASON— the same call and cause thein_progressarm already uses. Otherwise it skips exactly as before.server/src/__tests__/heartbeat-process-recovery.test.ts— two tests. One asserts the escalation fires after checkout-restore has returned the issue totodo, and that the resulting recovery action carries an owner (so this cannot trade a silent strand for the BLO-27553blocked-with-no-blockers signature, which has no wake path at all). The other is the regression guard: a succeeded run on atodoissue with no handoff markers must still skip, produce no recovery action, and leave the status untouched.Verification
npx vitest run server/src/__tests__/heartbeat-process-recovery.test.ts— 230 passed (230), exit 0. That is the whole file, not just the new cases, since this change edits a shared arm of the sweep.npx tsc --noEmitinserver/— exit 0.successfulRunHandoffEscalated: 0,escalated: 0,assignmentDispatched: 0, an emptyissueIds, an unchangedtodostatus, and zero rows inissue_recovery_actions.Risks
Low behavioural risk, deliberately narrow scope. Three things a reviewer should weigh:
The discriminator is evidence, not status.
isExhaustedSuccessfulRunHandoffreads the latest run'scontextSnapshotand is non-null only when the run carried handoff markers, i.e. only when the disposition was missing. A deliberate park has no such markers and is untouched. If that predicate were ever widened, this arm would start re-dispatching parked issues — hence the guard test.It cannot loop. Escalation is terminal, and the handoff attempt budget is already spent when
exhaustedis true.It reaches 1 of the 7 reported strands, and I would rather say so than imply otherwise. BLO-30577 carries exactly the right evidence and is still skipped: its recovery action is already
escalated, soshouldReuseStrandedRecoveryActionreuses it — correctly, per BLO-30743, since re-upserting a budget-exhausted action only re-fires the Slack-forwardedneeds_human_decision— andescalateStrandedAssignedIssuereturnsnull. Four more carry no handoff record at all.The reason four carry none is worth a reviewer's attention because it is a second defect this PR does not fix:
decideSuccessfulRunHandoffskips atserver/src/services/recovery/successful-run-handoff.ts:452withissue status ${issue.status} is a valid dispositionfor any status other thanin_progress. Fordone/cancelled/blocked/in_reviewthat is true. Fortodoit is the assumption this PR's arm exists to contradict — so when checkout-restore wins the race against the detector, no handoff evidence is recorded and there is nothing for this arm to read. I have deliberately not changed that line: agents in this fleet do usetodoas a genuine park disposition, so the fix needs a discriminator that status alone cannot supply, and widening a detector that governs fleet-wide park semantics on inference is not a change to make inside this PR.Model Used
claude-opus-5[1m]