fix(cards): stop finishRunForLane overwriting a run's stalled status#92
fix(cards): stop finishRunForLane overwriting a run's stalled status#92anagnorisis2peripeteia wants to merge 1 commit into
Conversation
|
Codex review: needs maintainer review before merge. Reviewed July 17, 2026, 6:47 PM ET / 22:47 UTC. Summary Reproducibility: yes. The PR supplies a focused real-Miniflare-D1 path that seeds a stalled run, invokes Review metrics: 2 noteworthy metrics.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Merge this narrow guarded-transition repair after ordinary maintainer review, preserving the atomic update/event batch and its D1 regression cases as the contract for run terminal provenance. Do we have a high-confidence way to reproduce the issue? Yes. The PR supplies a focused real-Miniflare-D1 path that seeds a stalled run, invokes Is this the best way to solve the issue? Yes. Guarding the persisted transition and conditionally inserting its audit event in the same D1 batch is the narrowest maintainable fix for the stale read-to-write race without changing the card-lane operation. AGENTS.md: not found in the target repository. Codex review notes: model internal, reasoning high; reviewed against 656c3514fcd7. Label changesLabel justifications:
Evidence reviewedWhat I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (4 earlier review cycles)
|
bed54f6 to
845c52d
Compare
|
@clawsweeper re-review Addressed both prior findings: (1) the guarded transition and its audit event now commit in one atomic |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
845c52d to
ec8f6b4
Compare
|
@clawsweeper re-review Addressed the [P1] duplicate-audit-event risk: the conditional event insert is now gated on the UPDATE's own row count — |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
finishRunForLane overwrote a run's status unconditionally. If a reconciler had already marked the run 'stalled' during the caller's read->write window, its terminal provenance was replaced and a false 'run <status>' event recorded -- the same check-then-act race stall()/ reconcileStalledRuns() already fence with WHERE status IN (...). Fence the transition on the run still being pre-terminal, and commit the transition and its audit event in one atomic D1 batch (the guarded-update + conditional-insert pattern claimRun uses). The event insert is gated on the UPDATE's own row count -- (SELECT changes()) = 1 -- so it belongs strictly to the transition THIS batch performed: a stalled/terminal run is never overwritten or given a false event, and two finish calls sharing the same 'now' cannot each emit the event. Proven against a real Miniflare D1.
ec8f6b4 to
6ef99a0
Compare
|
@clawsweeper re-review Addressed the [P1]: |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
What Problem This Solves
When a run finishes for a lane (advance to Done / Human Review / Backlog),
finishRunForLaneoverwrotethe run's
statusunconditionally. If a reconciler had already marked that runstalled— aheartbeat timeout or crash detected during the caller's read→write window — its terminal provenance
was silently replaced with
completed/review/canceled, and a false "run <status>" eventwas recorded. The stalled fact (and the preserved workspace it implies) was lost.
This is the same check-then-act race the two siblings on this table already guard against:
stall()and
reconcileStalledRuns()both fence theirrun_attemptsUPDATE withWHERE status IN (activeRunStatuses)and only emit their event when a row actually changed.finishRunForLanewas theone writer that didn't.
The change
Fence the transition on the run still being pre-terminal, and commit the transition and its audit
event in one atomic
DB.batch— the same guarded-update + conditional-insert patternclaimRunalready uses on this table:
'review'is allowed in the precondition only for the Done transition (review→completed).For the Human Review / Backlog transitions the run must still be active, so a run already in
reviewis never re-transitioned — a repeated or concurrent Human Review completion cannot re-fire the audit
event or rewrite the run's terminal timing.
A run a reconciler moved to
stalled(or any terminal status) no longer matches the UPDATE, so thetransition is skipped. The event insert is gated on
(SELECT changes()) = 1— the row count of theimmediately-preceding UPDATE — so it belongs strictly to the transition this batch performed: a
stalled/terminal run gets no false event, an already-terminal run is not re-announced, and two finish
calls sharing the same
nowcannot each emit the event (only the one whose UPDATE changed the rowdoes — this closes the duplicate-audit-event risk raised on the previous revision). Sharing one batch
means the status write and its event commit or roll back together — they can never persist separately.
Real-behaviour proof (before → after, real D1)
The same interleaving, driven against a real Miniflare D1 (SQLite) so the actual compiled SQL runs:
Reproducible transcript: https://gist.github.com/anagnorisis2peripeteia/1c6b581b8cd3236008ee787582a7040e
Testing
pnpm check(tsc + oxlint + oxfmt),pnpm build,pnpm exec wrangler deploy --dry-run,pnpm testall green.
tests/card-run-finish-guard.test.tsruns against a real Miniflare D1 (not a mock):a live run transitions and records exactly one event; a
stalledrun is left untouched with no event(the race fix); an already-terminal run is not re-announced; two finish calls sharing the same
nowemit exactly one event (pins thechanges()-gated conditional insert against duplicate auditevents); a
reviewrun promotes to Done; a repeated Human Review completion re-fires no event anddoes not rewrite the run's timing (pins the review-only-for-Done restriction); and a non-terminal
lane cancels with
control_intent. Mutation testing of the changed range scores 100%. A concurrencylint of the diff is clean.
Scope: this fixes only the run-provenance overwrite. The card's own lane move (
moveCard) is aseparate, pre-existing, user-initiated step and is intentionally left as-is.
Related: FINDING 2 of #88.