fix(heartbeat): terminalize queued runs detached from their issue lock (BLO-21621) - #1043
fix(heartbeat): terminalize queued runs detached from their issue lock (BLO-21621)#1043allyblockcast[bot] wants to merge 3 commits into
Conversation
|
🔗 Paperclip issue: BLO-21621 |
1 similar comment
|
🔗 Paperclip issue: BLO-21621 |
|
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 |
1 similar comment
|
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 |
kkroo
left a comment
There was a problem hiding this comment.
Reviewed the detached queued-run reconciliation and the startup test repair. The sweeper is gated on stale + detached state, uses queued-status CAS before cancellation, avoids duplicate recovery wakes when another run owns the lock, and the focused startup regression passes locally.
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Important Issues (2)
Strengths
Recommended Action
This PR is authored by |
…k (BLO-21621) A queued heartbeat run can outlive the issue lock that named it: the lazy-locking model only stamps issues.executionRunId at claim time, and sweepStaleIssueLocks (or a superseding run) can clear/move that pointer without ever touching the still-queued row. Once detached, nothing in the recovery apparatus looks at that row again — hasActiveExecutionPath and every sweep built on it treat any queued row referencing the issue as proof of life, with no staleness check, so the run occupies a live queue slot invisibly forever. Add heartbeat.reconcileDetachedQueuedRuns(), wired into the startup sequence and the periodic scheduler tick alongside sweepStaleIssueLocks. It cancels a queued run only once it is both stale (past STALE_PRE_CLAIM_ISSUE_LOCK_MS) and detached (its issue's checkout/execution lock no longer names it), and fires exactly one recovery wake when nothing else owns the issue. Runs that are still their issue's recognized lock holder are left alone regardless of age, so an agent legitimately saturated at its concurrency ceiling keeps its real queued backlog. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
a586c2a to
1298a0e
Compare
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Prior Findings Dispositioned (2)
Important Issues (2)
Strengths
Recommended Action
This PR is authored by |
…621) Addresses both Important findings from Ally's PR #1043 review: - gstack/review: filter to issue-scoped, nonterminal, detached candidates via a SQL join against `issues`, instead of scanning the oldest `limit` stale queued rows and skipping the ones that turn out attached/terminal/ malformed in application code. The prior version's bound was on the raw age-ordered scan, so once `limit` permanently-skipped rows existed ahead of a repairable row, every pass re-scanned the same prefix and never reached it. - native-codex: reconcile every stale detached row for an issue before calling enqueueWakeup, not just the one row the bounded scan happened to select first. Cancelling one orphan and waking immediately let enqueueWakeup's legacy-run lookup adopt a sibling orphan for the same issue, which then read as "attached" and was left queued-but-dead forever. Adds the two regressions the review asked for: a >limit-permanently- skipped-rows starvation case, and a two-orphan case asserting both rows terminalize and exactly one fresh run survives. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Addressed both carried-forward Important findingsPushed a follow-up commit addressing the two issues from Ally's re-review (head gstack/review (heartbeat.ts:17587) — detachment is now a JOIN condition evaluated in SQL against native-codex (heartbeat.ts:17671) — before calling Added the two regressions the review asked for:
All 5 tests in Note: this PR had been added to the merge queue (position 32, enqueued 21:11:17Z) based on the stale pre-re-review approval. I dequeued it before pushing this fix, since merging at that point would have shipped both known, reviewer-flagged bugs. Re-queue once CI is green and review clears. |
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 (2)
Strengths
Recommended Action
This PR is authored by |
Thinking Path
Linked Issues or Issue Description
Refs: BLO-21621
Root cause traced from the issue investigation and code reading:
sweepStaleIssueLocksintentionally clears a stale pre-claimexecutionRunIdpointer after 6 hours without cancelling the underlying queued run.hasActiveExecutionPath, used byreconcileStrandedAssignedIssuesand blocked-dependency wake logic, treats anyqueued/running/scheduled_retryrow referencing the issue as live work, with no staleness check.What Changed
heartbeat.reconcileDetachedQueuedRuns()to cancel stale queued runs whose issue no longer names them viacheckoutRunIdorexecutionRunId.sweepStaleIssueLocks.Verification
pnpm --filter @paperclipai/server exec vitest run src/__tests__/server-startup-feedback-export.test.tspassed locally: 19/19.pnpm --filter @paperclipai/server exec vitest run src/__tests__/heartbeat-detached-queued-run-reconcile.test.tswas invoked locally; it skipped because embedded Postgres support is unavailable on this host.recovery-stale-issue-lock-sweep.test.tspassing 22/22, andtsc --noEmitoverserver/clean.BLO-21621 detached queued run; only this PR matched.Risks
Medium operational risk: the new sweep cancels queued runs that are stale and detached from their issue lock. The guard intentionally requires both conditions, so age alone cannot discard legitimate backlog. The main residual risk is a hidden producer that creates detached queued rows that are still meaningful despite the issue lock moving; those rows will now be terminalized after the stale-lock window and may trigger a fresh wake when the issue is otherwise unowned.
Model Used
Claude Sonnet 5 authored the original branch through Claude Code. Codex GPT-5 was used for CI triage, startup-test mock repair, PR description cleanup, and rerun/merge-queue handling.
Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template