Skip to content

fix(task-board): open the review cycle when the PR link loses the race - #6700

Closed
pedrofrxncx wants to merge 3 commits into
feat/review-cycle-off-lanefrom
pr6689-fix
Closed

fix(task-board): open the review cycle when the PR link loses the race#6700
pedrofrxncx wants to merge 3 commits into
feat/review-cycle-off-lanefrom
pr6689-fix

Conversation

@pedrofrxncx

@pedrofrxncx pedrofrxncx commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #6689 — merge into feat/review-cycle-off-lane, not main.

The hole

Found by running #6689 on a live cluster. A card went to In Review and stayed there for the whole reviewer run — the exact thing the branch exists to prevent.

advanceLinkedTasksToReviewOnThreadFinish reads listPrs to decide repo-backed vs repo-less. A run that links its PR moments after its thread goes terminal is read as repo-less and parked In Review. The link then lands on a card that is no longer In Progress, so openReviewCycleIfInProgress matches nothing and the cycle never opens at all.

Observed at 52 seconds between the two:

15:42:47  thread finished, listPrs empty  ->  card moved to In Review
15:43:39  PR #423 linked                  ->  update matched nothing, no cycle
15:49:37  sweeper dispatched the reviewer via the in_review lane branch

Two consequences, both silent:

  • the card reads In Review while an agent is reviewing it — migration 189 undone for exactly the cards it was written for;
  • review_cycle_started_at stays NULL, so every verdict on that card falls back to the pre-189 activity scan.

The fix

The predicate now also matches a Super-Agent-owned card sitting In Review with no cycle open, and takes it back to In Progress in the same statement.

Safe precisely because the cycle is null: a card whose cycle never opened has had no reviewer and no verdict, so there is nothing behind it to invalidate. Once a cycle IS open, the card is mid-review and parkReviewedCardForHuman is what put it In Review — dragging it back would undo a verdict and re-stamp a boundary that verdicts stand on, so that case still returns null.

The Super Agent guard keeps it off a card a person has taken over (handTaskToHuman clears the assignee). One statement, so the two columns cannot interleave with the sweeper's read.

Testing

Real Postgres (embedded), 27 pass / 0 fail in the file:

  • the late-link rescue (In Review + no cycle -> In Progress + cycle stamped)
  • an In Review card with a cycle already open is left alone
  • a human-owned card is left alone
  • a shipped card is left alone

The test that asserted the old narrow rule (never opens a cycle on a card that is not In Progress) is inverted, not appended.

bun run --cwd=apps/api check, bun run lint (0 errors), bun run fmt clean. 36 pre-existing failures on the base branch, 36 here, zero regressions.


Summary by cubic

Fixes a race that left a card In Review for the whole reviewer run, with review_cycle_started_at NULL, when the PR link landed after the thread finished—so every verdict on that card fell back to the legacy activity scan.

Bug Fixes

  • openReviewCycleIfInProgress now also matches a Super-Agent-owned card sitting In Review with no cycle open, moving it back to In Progress and stamping the cycle in the same statement.
  • The rescue is safe only while the cycle is null; an In Review card with an open cycle was parked on a verdict and is still left alone, and human-owned cards are excluded.

Written for commit bfa845c. Summary will update on new commits.

Review in cubic

Pedro França added 3 commits August 28, 2026 09:12
The dispatch client published the `starting-sandbox` run-status stage
unconditionally before every `ensureSandbox`. An interactive Code Agent keeps
its pod between turns (`interactive: true` skips the release-on-run-end), so
the ensure was a warm resume — but the chat had already been told the machine
was booting, on every single message.

Move the publish into an `onColdStart` callback that `ensureSandbox` fires only
past its live-pod fast path.
A card moved to In Review the moment its PR was opened, so it sat in the
reviewers' lane for the whole time an agent was still working on it. In
Review read as "waiting on a person" when nobody was waiting on anything.

The lane could not simply be changed, because the review CYCLE — the
boundary that decides which reviewer verdicts still count — was derived
from the newest `status_changed → in_review` activity. Moving a card mid
review would have reset that boundary and invalidated its own reviewer's
verdict, which is the failure mode that stranded 13 prod cards holding an
approval that could never merge.

So the cycle stops riding on the lane. `review_cycle_started_at` is a
column (migration 189, backfilled from the timeline so in-flight cycles
survive the deploy), and `reviewCycleStart` reads it, falling back to the
old activity scan only for cards stamped before it.

With that decoupled, the lane is free to say what a person wants to read:

  agent opens a PR   → review cycle opens, card STAYS In Progress
  reviewer decides   → In Review (`parkReviewedCardForHuman`)
  hand-off / retries → In Review, unassigned

`inReviewPhase` is the predicate every automatic path now takes instead of
`status === "in_review"`, and the sweeper's work list mirrors it. Two
regressions that decoupling opens are closed explicitly: a failed REVIEWER
run no longer schedules a Super Agent retry or parks the card on To Do
(the reviewer has its own budget), and `closesOwnReview` gates on the
phase, so an author's run can't mark its own work Done while a reviewer
is still reading it.

Verified against real Postgres: migration up/down from an empty database,
the backfill against a seeded pre-migration card, and the full unit +
integration suite with no new failures.
A card went to In Review and stayed there for the whole reviewer run — the
exact thing this branch exists to prevent.

`advanceLinkedTasksToReviewOnThreadFinish` reads `listPrs` to decide
repo-backed vs repo-less. A run that links its PR moments AFTER its thread goes
terminal is read as repo-less and parked In Review. The link then lands on a
card that is no longer In Progress, so `openReviewCycleIfInProgress` matched
nothing and the cycle never opened at all. Observed on a real board at 52
seconds between the two:

  15:42:47  thread finished, listPrs empty  -> card moved to In Review
  15:43:39  PR #423 linked                  -> update matched nothing, no cycle
  15:49:37  sweeper dispatched the reviewer via the in_review lane branch

The card therefore read In Review while an agent reviewed it, and with
`review_cycle_started_at` NULL every verdict fell back to the legacy activity
scan.

The predicate now also matches a Super-Agent-owned card sitting In Review with
NO cycle open, and takes it back to In Progress in the same statement. That is
safe precisely because the cycle is null: a card whose cycle never opened has
had no reviewer and no verdict, so there is nothing behind it to invalidate.
The Super Agent guard keeps it off a card a person has taken over
(`handTaskToHuman` clears the assignee), and one statement means the two
columns cannot interleave with the sweeper's read.

Real-Postgres tests: the late-link rescue, an In Review card with a cycle
already open left alone (a verdict put it there), a human-owned card left
alone, and a shipped card left alone. The test that asserted the old narrow
rule is inverted rather than appended.
@pedrofrxncx
pedrofrxncx force-pushed the feat/review-cycle-off-lane branch from e42cb0d to 1750b36 Compare August 28, 2026 16:09
@pedrofrxncx

Copy link
Copy Markdown
Collaborator Author

Folded into #6689 directly — the fix belongs in that PR, not stacked on it. Branch pr6689-fix deleted.

@pedrofrxncx
pedrofrxncx deleted the pr6689-fix branch August 28, 2026 16:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant