fix(issues): hydrate activeRun by the terminal complement, not an enumeration (BLO-25410) - #1303
Merged
Merged
Conversation
…meration (BLO-25410)
`activeRun` and `checkout()` expressed "holds the execution lock" in two
different forms that agree inside `HEARTBEAT_RUN_STATUSES` and disagree
outside it:
checkout() !TERMINAL.has(status) terminal complement
hasActionableTimerWork notInArray(status, TERMINAL) terminal complement
activeRun hydration inArray(status, HOLDING_STATUSES) enumeration
`ISSUE_EXECUTION_LOCK_HOLDING_RUN_STATUSES` is built by filtering the
canonical union, so it can only list statuses inside it. But
`heartbeat_runs.status` is a plain `text` column with no enum or check
constraint, and `error`/`adapter_failed` already occur in it without being
in the union. For a non-terminal status outside the union the enumeration
matched nothing, so `GET /issues/{id}` returned `activeRun: null` while
`POST /checkout` 409'd naming that exact run — the BLO-19749 defect,
reproduced for the unknown-status case. Latent, not live: no current
status is both out-of-union and non-terminal.
Switch the hydration to `notInArray(status, TERMINAL_..._VALUES)`. No-op
for every status in the union; a guard for the next one added.
The three remaining `inArray`-over-active-statuses sites are NOT lock
predicates and are left as enumerations, now with comments saying why —
`BLOCKER_ATTENTION_*` and `BLOCKED_INBOX_*` suppress an attention signal,
so they must fail toward "not covered" (raise attention) where the lock
predicate fails toward "held" (defer). Converting them would flip that
safety direction and let a future status silently hide a stuck issue.
`issue-tree-control` and `productivity-review` drive pause control and a
report count respectively.
Also fixes a latent test bug this surfaced: the "terminalized" case in
issue-detail-active-run-routes asserted against `"completed"`, which no
code path ever writes to `heartbeat_runs.status` (routines.ts
`finalizeRun` writes it to `routine_runs`; it is a real status only on
`agent_wakeup_requests`). It passed because the enumeration dropped any
unknown string, never because it was classified as terminal. Now asserts
over the real `TERMINAL_HEARTBEAT_RUN_STATUS_VALUES`, plus a new case
pinning that an unknown non-terminal status reads as held — matching
checkout, which 409s on it because `clearExecutionRunIfTerminal` will not
clear the pointer.
Verified: the two new `issue-execution-lock.test.ts` cases fail against
the enumeration form (`activeRun` null / undefined while checkout names
the run) and pass after the change; the 9 unchanged-behaviour cases pass
both ways. 130 tests green across 9 lock/activeRun suites.
Co-Authored-By: Claude <noreply@anthropic.com>
Author
1 similar comment
Author
Author
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: 0379d25
Looks good. The active-run hydration now uses the terminal-status complement used by checkout locking, so unrecognized non-terminal persisted statuses remain visible rather than appearing unowned.
Critical Issues (0)
Important Issues (0)
Suggestions (0)
Strengths
- The regression coverage exercises an out-of-union non-terminal status and verifies that the detail route and checkout behavior identify the same run.
- The narrow enumerations that intentionally model execution or attention, rather than lock ownership, are explicitly documented to preserve their safer behavior.
Recommended Action
- Merge after the pending CI checks pass.
Author
|
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 |
13 tasks
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
Closes BLO-25410. Follow-up to BLO-19749 / #1108 — Ally's review landed 2m28s after that PR merged, so the finding could not be folded in.
The defect
BLO-19749 unified "holds the execution lock" into
issue-execution-lock.tsand pinned holding/terminal as exact complements. But the consumers express that notion in two different forms, which agree insideHEARTBEAT_RUN_STATUSESand disagree outside it:checkout()!TERMINAL.has(status)hasActionableTimerWorknotInArray(status, TERMINAL_…_VALUES)activeRunhydrationinArray(status, ACTIVE_RUN_STATUSES)activeRun: nullACTIVE_RUN_STATUSES = [...ISSUE_EXECUTION_LOCK_HOLDING_RUN_STATUSES], and that constant isHEARTBEAT_RUN_STATUSES.filter(s => !TERMINAL.has(s))— an enumeration over the canonical union only. A status outside the union cannot appear in it, soGET /api/issues/{id}returnedactiveRun: nullwhilePOST /checkout409'd naming that exact run.heartbeat_runs.statusistext("status")— no enum, no check constraint.issue-execution-lock.tsalready records thaterrorandadapter_failedoccur in the column without being in the union, so out-of-union statuses are a demonstrated occurrence, not a hypothesis.Severity: latent, not live. No current status is both out-of-union and non-terminal. The exposure is the next one that isn't.
What Changed
activeRunMapForIssues(server/src/services/issues.ts) now selects holders bynotInArray(heartbeatRuns.status, TERMINAL_HEARTBEAT_RUN_STATUS_VALUES)instead ofinArray(..., ACTIVE_RUN_STATUSES). This is a no-op for every status in the canonical union and a guard for the next one added to the column.inArray-over-active-statuses sites. None is a lock predicate, so none was converted; each now carries a comment recording why. The point is that these predicates must fail in opposite directions:BLOCKER_ATTENTION_ACTIVE_RUN_STATUSES/BLOCKED_INBOX_ACTIVE_RUN_STATUSESfail toward "not covered" (an unknown status raises attention; being wrong costs a spurious nudge instead of silently swallowing a stuck issue)issue-tree-control.tsdrives pause/cancel control (and re-narrows to queued/running in memory anyway);productivity-review.tsonly counts runs for a reportissue-execution-lock.test.ts.A latent test bug this surfaced
issue-detail-active-run-routes.test.tsasserted its "terminalized" case over["completed", "failed", "cancelled"]."completed"is never written toheartbeat_runs.status—finalizeRuninroutines.tswrites it toroutine_runs, and it is a real status only onagent_wakeup_requests(which is where the vocabulary was borrowed from). The case passed because the enumeration dropped any unknown string, never because the system classified it as terminal.That is itself corroboration for this issue: a developer already reached for an out-of-union status and got the disagreement, silently. The test now asserts over the real
TERMINAL_HEARTBEAT_RUN_STATUS_VALUES, plus a new case pinning that an unknown non-terminal status reads as held — matchingcheckout(), which 409s on such a run becauseclearExecutionRunIfTerminalshares the same terminal set and will never clear the pointer. Reportingnullthere is what made a permanently un-checkoutable issue look unowned.Verification
Regression criterion confirmed by reverting the production change and re-running:
Both pass after the change; the 9 unchanged-behaviour cases (3 union holding statuses hydrate, 7 terminal statuses do not) pass both ways.
The second test drives the real
checkout()and asserts the 409'sdetails.executionRunIdequals the hydratedactiveRun.id— the two read paths agreeing, which is the acceptance criterion.130 tests passed (130)across 9 suites:issue-execution-lock,issue-detail-active-run-routes,issue-blocker-attention,issue-run-holding,issue-tree-control-service,issue-tree-control-routes,agent-inbox-lite-status-contract,issue-liveness,execution-lock-orphan-cleanuptsc --noEmitcleanThe new DB-backed block is gated on
getEmbeddedPostgresTestSupport()in a separatedescribe, so the existing pure-unit assertions in that file keep running on hosts without embedded postgres.Risks
Low risk. The change widens which rows hydrate as
activeRun, and it widens it to exactly the setcheckout()already treats as holding, so the two paths converge rather than diverge.heartbeat_runs.statusstaystext().activeRunrather thannull. That direction is the safe one — it reports the issue as held (matching checkout's 409) instead of advertising a permanently un-checkoutable issue as available.Model Used
Claude Opus 5 (
claude-opus-5[1m], 1M context), extended thinking, running as the CTO agent in the Paperclip agent harness with tool use.Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template🤖 Generated with Claude Code