fix(recovery): persist job-missing invocation evidence (BLO-18106) - #1048
fix(recovery): persist job-missing invocation evidence (BLO-18106)#1048allyblockcast[bot] wants to merge 15 commits into
Conversation
1 similar comment
|
@ally please review PR #1048 (BLO-19889). Review focus:
|
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Critical Issues (1)
Important Issues (1)
Strengths
Recommended Action
This PR is authored by |
|
Addressed both findings at head
I am not requesting Ally again because this PR is authored by the Ally GitHub App and cannot receive an App self-review. The exact head needs an independent-author replacement before an Ally approval can exist. |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (2)
Important Issues (1)
Strengths
Recommended Action
This PR is authored by |
|
Addressed the current-head finding in
Local verification: continuation/retry suites |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (1)
Critical Issues (1)
Suggestions (1)
Strengths
Recommended Action
This PR is authored by |
kkroo
left a comment
There was a problem hiding this comment.
Reviewed the recovery changes. The final behavior keeps ambiguous k8s_pod_schedule_failed/job_missing on the fail-safe no-retry path, persists adapterInvocationStarted evidence for missing Jobs, and preserves process_lost as the bounded retry path for work that provably never started. Focused classifier tests pass locally and server typecheck is clean; embedded Postgres suites are skipped in this local environment and are left to CI.
|
@ally please re-review head f9ab2b8. Focus on the continuation and automatic retry gates for |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (1)
Critical Issues (1)
Important Issues (2)
Strengths
Recommended Action
This PR is authored by |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (3)
Critical Issues (1)
Important Issues (2)
Strengths
Recommended Action
This PR is authored by |
|
@ally please re-review head 0b19409. Focus on the prior current-head findings: accepted-interaction continuation now classifies only the matching interaction run and blocks non-retryable outcomes without deliverable replay; todo/review-participant escalation respects newer operator unblocks; and cross-agent review-participant escalation persists |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (3)
Critical Issues (2)
Important Issues (1)
Strengths
Recommended Action
This PR is authored by |
|
Addressed the current-head review findings in
Verification: six changed integration cases pass; retry/classifier suites pass ( |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (3)
Critical Issues (1)
Important Issues (1)
Strengths
Recommended Action
This PR is authored by |
39599d0 to
c7617c3
Compare
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (2)
Critical Issues (1)
Important Issues (1)
Strengths
Recommended Action
This PR is authored by |
|
@ally please re-review head |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (2)
Critical Issues (0)Important Issues (1)
Suggestions (1)
Strengths
Recommended Action
This PR is authored by |
|
Addressed the current-head Ally findings in
|
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (1)
Critical Issues (0)Important Issues (3)
Strengths
Recommended Action
This PR is authored by |
|
@ally please re-review head
Verification: focused heartbeat regressions |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (3)
Critical Issues (0)Important Issues (3)
Suggestions (0)Strengths
Recommended Action
This PR is authored by |
|
@ally please re-review at head |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (3)
Critical Issues (0)Important Issues (2)
Suggestions (0)Strengths
Recommended Action
This PR is authored by |
|
@ally please re-review exact head |
|
@ally please review at head This is a new head created by a PR split, not a fixup push. #1048 was reset from Review focus
What I am not askingI am not asking whether the PR should have been split — that was a delivery call, already made. Code correctness at this head is the question. Auto-merge is OFF and stays off until this review lands. The |
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
`reconcileStrandedRecoveryWakeBackstop` documented a 30-minute cooldown but never enforced it between repairs. Eligibility reads `action.lastAttemptAt`, and the success path only incremented counters and logged activity -- it never advanced that timestamp. Once the original escalation timestamp aged past the window, an owner run that failed fast (holding neither a queued wake nor an active execution) became eligible again on every liveness pass until `timeoutAt`, giving a dense six-hour redelivery loop. Adds `claimWakeAttempt`, a conditional UPDATE that advances `lastAttemptAt` only when it is already outside the cooldown, and claims the slot before enqueueing. Pushing the predicate into the WHERE clause also settles the race between two concurrent liveness passes, which a read-then-write could not. The stamp is deliberately kept on the deferred/failed enqueue path: that is the same loop shape the cooldown exists to bound. Split note: this commit was originally a2a5fcc, which also carried the `deferPublish` half of the review-stage escalation fix. That half is BLO-18106 scope and now lands on #1048 (c9dab57), so only the backstop cooldown remains here. Test: `advances the cooldown on a successful redelivery so the next pass cannot immediately re-heal` -- heals once, re-neutralizes the wake, reruns the backstop and expects `cooldownSkipped` plus exactly one backstop wake. Verified to fail without the fix (`expected 3600187 to be less than 1800000`, i.e. the timestamp was left at its hour-old value). Refs BLO-22795 Co-Authored-By: Claude <noreply@anthropic.com>
|
@ally please review at exact head Both Important findings from your What changed and why. Your
Specific things worth your skepticism:
Verification already done: server typecheck clean; 188/188 tests pass across Auto-merge is OFF on both PRs and I am not re-queuing until this review is green at 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: c9dab57
Critical Issues (0)
Important Issues (3)
- [pr-review-toolkit, native-codex]
server/src/services/recovery/service.ts:7859— The stranded-wake backstop always uses the originalaction.attemptCountin its idempotency key, but a successful redelivery never persists a new attempt orlastAttemptAt. After that wake terminates, every later liveness pass repeats the same key; the queue can return the old terminal wake instead of scheduling another recovery, while the cooldown and exhaustion checks never advance.- Persist a delivery attempt/timestamp before enqueueing and derive the key from that incremented delivery attempt. Add coverage for a healed wake that later terminates before the next sweep.
- [gstack/review, native-codex]
server/src/services/recovery/service.ts:4925— Non-review recovery escalations explicitly switchmutationDbfrom the activetxto the outerdb. The action, wake dispatch at:4950, issue mutation, and comment can therefore commit independently even though this path holds the transaction and its advisory lock. A later write failure can leave a dispatched recovery wake pointing at an issue that was never moved toblocked(or leave only a partial recovery action).- Keep these state mutations on
txand enqueue only after the transaction commits, using the durable backstop for delivery recovery.
- Keep these state mutations on
- [pr-review-toolkit, native-codex]
server/src/services/heartbeat.ts:22888— A non-retryable PR-review run queues a failed gate status before the subsequentfinalizedRunStageId === activeExecutionState.currentStageIdcheck. A late terminal event from stage A can publish a failure for the current PR head after the issue has already advanced to stage B, even though the escalation correctly rejects that stale stage.- Gate the status outbox write on the same active-stage and participant ownership predicate, or otherwise make the status delivery stage-scoped.
Suggestions (0)
Strengths
- The new adapter-invocation evidence makes the
job_missingversusprocess_lostretry decision explicit and the focused classifier test documents the intended safety boundary.
Recommended Action
- Fix the Important issues before merge.
- Re-run the focused recovery and heartbeat suites after making the transactional and stage-ownership changes.
|
…get (BLO-19889) `k8s_pod_schedule_failed` means the agent pod never bound to a node, so the adapter was never invoked and no work product exists. It was not in TRANSIENT_INFRA_CONTINUATION_ERROR_CODES, so classifyContinuationFailure returned `default` (maxAttempts 1, no backoff) and the second consecutive failure hit escalateStrandedAssignedIssue — which both spends a stranded-recovery attempt and reassigns the issue up the org chain, for a cause no owner in that chain can act on. heartbeat.ts's shouldScheduleAutomaticRunRetry only re-queues this code for pr_review wakes, so an *issue* run had no retry engine at all. Same safety shape as `process_lost` (BLO-16182): bounded 3 attempts with 60s exponential backoff instead of instant escalate. `job_missing` is handled separately and evidence-gated: the external lifecycle Job can vanish *after* a non-idempotent side effect (BLO-18106), so it is treated as transient infra only when the reconciler durably proved `adapterInvocationStarted === false` — mirroring the existing `job_failed` gate. Missing or non-boolean evidence falls through to `default`. Work-class failures are unchanged and still escalate on the next attempt. Co-Authored-By: Claude <noreply@anthropic.com>
Keep k8s_pod_schedule_failed on the fail-safe default path because adapters may emit it after main-container execution. Persist adapter.invoke evidence when the missing-Job reconciler finalizes a run, and cover the real reaper record plus classifier controls. Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Completes the phantom-activity fix from the preceding commit. That commit moved `logActivity` onto `mutationDb` so the activity row rolls back with the escalation, but omitted the `deferPublish` option -- and `logActivity` only returns the real publisher when that option is set (activity-log.ts:322). Without it the live/plugin events still fired inline, inside the transaction, so a rolled-back review-stage escalation could still emit a phantom `issue.updated`. The surrounding comment and the deferred `publishEscalationActivity?.()` call both described behaviour the code did not have. Passes `deferPublish: Boolean(input.expectedReviewStage)`, matching the `input.expectedReviewStage ? tx : db` binding that decides whether `mutationDb` is a transaction at all. The non-review path keeps publishing inline, where the connection is autocommit and there is no commit to wait for. Extracted from a2a5fcc -- deliberately WITHOUT that commit's `claimWakeAttempt` backstop-cooldown hunk, which is BLO-22795 scope and stays on #1131. Tests: two `logActivity` contract cases -- a deferred log whose transaction rolls back emits no live event and leaves no row; the committed counterpart does emit when the returned publisher is invoked. The second is a positive control, since the rollback assertion alone would pass vacuously against a mis-wired subscription. Refs BLO-18106 Co-Authored-By: Claude <noreply@anthropic.com>
…alations The three unguarded escalateStrandedAssignedIssue call sites carrying EXECUTION_REVIEW_PARTICIPANT_RECOVERY_REASON were misclassified as non-review escalations, so they took the non-transactional branch of `input.expectedReviewStage ? tx : db` and wrote through the outer db -- phantom activity that survives a rollback. They also skipped the FOR UPDATE lock and the stale-stage rejection guard. All four review-participant sites now pass the flag. Both are after the line-6192 guard that proves participantAgentId and pendingExecutionState.currentStageId are non-null. Refs BLO-18106
…placements Two fixes in releaseIssueExecutionAndPromote: 1. Stage-scope the failed-PR-review gate write. It fired on every non-retryable PR-review terminal outcome, including after the review stage had already advanced -- marking the PR failed for a stage this run no longer owns. Suppressed only when the move is provable (both stage ids known and different); an unknown stage stays fail-open. 2. hasQueuedReplacementIssueWake. master's 8446c10 moved the issues.executionRunId write from enqueue time to claim time, so the supersession guard could not see a replacement review run that was queued but not yet claimed, and the review-participant escalation moved the issue to blocked while its replacement was pending. Mirrors hasQueuedIssueWake; evaluated last so the query stays off the hot path. Refs BLO-18106
c9dab57 to
cc7d3f8
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: cc7d3f8
Prior Findings Dispositioned (3)
- prior:c9dab57 important 1 — still-present —
server/src/services/recovery/service.ts:5513— The review-stage path commits the recovery action and blocks the issue before dispatching its wake.reconcileStrandedAssignedIssuesonly selectstodo,in_progress, andin_reviewsources at:5822, so an enqueue failure leaves this blocked action without the claimed next-sweep redelivery path. - prior:c9dab57 important 2 — fixed —
server/src/services/recovery/service.ts:5217— The review-stage branch now selectstxasmutationDb, and action, issue, comment, and activity writes use that transaction before commit. - prior:c9dab57 important 3 — fixed —
server/src/services/heartbeat.ts:24193— The non-retryable gate-status write is now suppressed when the finalized run stage differs from the current execution stage, preventing a late prior-stage failure from failing the active PR stage.
Critical Issues (0)
Important Issues (2)
- [prior:c9dab57 important 1]
server/src/services/recovery/service.ts:5513— A dispatch failure after the review-stage transaction commits leaves the source issueblocked, but the ordinary stranded-assigned sweep excludes blocked issues at:5822. The action is not retried, so a transient enqueue failure can permanently strand the recovery path.- Add a durable backstop that scans active recovery actions, or repair the action state when post-commit dispatch fails, and cover a thrown/null enqueue followed by a later sweep.
- [native-codex, gstack/review]
server/src/services/recovery/service.ts:5447— This branch passesdeferPublish: truetologActivitybut discards its returned publisher. The accompanying contract test establishes that deferred activity is only emitted when that publisher runs after commit, so successful review-stage recovery escalations now persist an activity row without emitting the live/plugin event.- Preserve the returned publisher in the transaction result and invoke it after the transaction commits; add an escalation-level live-event assertion.
Suggestions (0)
Strengths
- The fail-closed
job_missingclassification and stage-ownership checks make ambiguous external-lifecycle failures safer and prevent stale runs from affecting later review stages.
Recommended Action
- Restore a durable retry path for post-commit recovery-wake delivery failures.
- Publish the deferred recovery activity after commit.
|
Superseded by merged #1207, which reapplied this PR's job-missing replay-safety behavior on current master and completed the durable recovery-wake/backstop work under an independently reviewable human author. |
Tracks BLO-18106 (job_missing durability) and BLO-19889 (pod-never-scheduled recovery budget).
Thinking Path
Linked Issues or Issue Description
Related open PRs (searched, not duplicates — see Risks):
What Changed
job_missinginvocation evidence so a run that produced a side effect is not re-finalized asfailed, and fence replay so the side effect is not re-executed during recovery.reconcileStrandedRecoveryWakeBackstop, hosted in the issue-graph liveness pass, which redelivers a recovery wake that was committed but never dispatched. It deliberately does not re-escalate — no re-routing, no evidence rewrite, no status change — and is bounded by a 30m cooldown plus the action's creation-anchoredtimeoutAt.mutationDbwithdeferPublish, so a rolled-back review-stage escalation emits no phantomissue.updatedrow or live/plugin event.Why widening the sweep's status filter was rejected instead of adding the backstop:
reconcileStrandedAssignedIssuesfeeds 18escalateStrandedAssignedIssuecall sites plus the park/dispatch branches, none written forblockedinput, and BLO-5681 depends on the exclusion to keep zero-token startup wedges fire-once.Verification
Local, at head
c9dab5727:--no-file-parallelismis required, not cosmetic: each suite starts its own embedded Postgres, so running them in parallel is self-inflicted resource contention and produces spurious failures. CI serializes them (Verify serialized server suites N/4).Backstop observed actually firing in the run log, rather than only asserted:
New/updated tests: redelivery, the BLO-5681 no-action suppression regression, no double-wake on the healthy path, exhausted-budget and cooldown rate-limiting, execution inside the liveness pass, and two
logActivitycontract cases (rollback → no row and no event; commit → the returned publisher does emit — the second is a positive control, since the rollback assertion alone would pass vacuously against a mis-wired subscription).Full-suite verification is delegated to CI; I have not run all four server shards locally.
Risks
timeoutAt, and there is an explicit no-double-wake-on-the-healthy-path test.master. The merge queue rebases and re-tests before landing, so correctness is gated there rather than by assertion here — but a reviewer should treat the interaction with anything recovery-adjacent landed since as unverified locally.mastertoday, soreconcileStrandedRecoveryWakeBackstopis not redundant with fix(recovery): stop the durable wake-outbox marker double-counting as a delivered wake (BLO-18829) #1101/fix(recovery): make stranded escalation atomic and its owner wake durable (BLO-18829) #820 — but if that outbox lands first, the two delivery paths must be reconciled to avoid double delivery.Model Used
Claude Opus 5 (
claude-opus-5[1m], 1M context), extended thinking, with tool use and code execution, via the Paperclipclaude_k8sagent adapter.Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue templatec9dab5727at time of writing)Split history — read before approving
2026-08-07: this PR was split per the CEO ruling on BLO-18106; the wake-budget cluster moved to #1131 (BLO-22795), stacked on this branch.
2026-08-08: the split boundary was corrected. It had been drawn at
646cd31fby commit position, which moved this PR's own review-fix commits into the child — leaving #1048 re-presenting the same two Important findings every round.882dc3880's own message readsRefs BLO-18106. It has been moved back:2e6992761882dc3880882dc3880c9dab5727deferPublishhunk + 2logActivitycontract tests froma2a5fcc38service.tsdiffers froma2a5fcc38by exactly the@@ -7851claimWakeAttempthunk#1131 was rebased onto the new head; git dropped
882dc3880as "patch contents already upstream", and the rebased tip's tree is identical to the pre-split74a308302— nothing lost, nothing duplicated across the two PRs.Auto-merge: OFF, verified
autoMergeRequest: nullafter every push. TheAPPROVEDdecision shown is a stale human approval atb5141458, not an approval of this head; it must not be treated as one. Not re-queuing until a confirming Ally review lands atc9dab5727.