Skip to content

fix(task-board): a review cycle always ends in a verdict or a person - #6688

Merged
pedrofrxncx merged 3 commits into
mainfrom
fix/reviewer-completed-without-verdict
Aug 28, 2026
Merged

fix(task-board): a review cycle always ends in a verdict or a person#6688
pedrofrxncx merged 3 commits into
mainfrom
fix/reviewer-completed-without-verdict

Conversation

@pedrofrxncx

@pedrofrxncx pedrofrxncx commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Problem

Prod, org osklen, card OS-303 ("[GLOBAL][PERF] Habilitar cache de borda"): the reviewer thread reached completed and the card shows a red 0/1 with nothing able to move it.

The run never recorded a decision — its last parts are "Standing by for the background deno type-check…" and then finish. No review_approved / review_changes_requested on the timeline.

From there the card is a dead end:

  • reviewerHandledThisCycle treats any non-failed terminal thread as a review ("a reviewer records its decision and completes"), so neither the sweeper nor the dialog poll re-dispatches.
  • isSpentAttempt counts only failed/hung threads, so reviewerAttemptsExhausted is false and the hand-to-human path never fires.
  • Only a verdict parks a card for a person (parkReviewedCardForHuman), so after feat(task-board): keep a card In Progress while its reviewer runs #6689 this card doesn't even sit in the reviewers' lane — it stays In Progress with an open review cycle, indefinitely, which is a quieter version of the same strand.

The board's 0/1 is correct throughout. There genuinely is no verdict. It just never changes.

Fix — three rungs, so the cycle always terminates

  1. Ask. A reviewer thread that completed without a verdict is asked for one on its own thread (nudgeThreadTurn, the mechanism ensureReviewerCommented already uses) — its session still holds everything it reviewed, so this is cheaper and better-informed than a fresh reviewer. The prompt is narrow: record the verdict you already reached, request_changes if you could not finish, do not re-review, do not wait on background tasks.
  2. Spend. If it still ends undecided, isSpentAttempt now counts it: verdictRecorded (from reviewCycleVerdicts on the current cycle) replaces "terminal and not failed" as the proof of a review. The existing MAX_REVIEWER_ATTEMPTS budget dispatches a second reviewer.
  3. Hand over. Budget gone → the existing reviewerAttemptsExhaustedhandTaskToHuman path, which parks the card In Review, unassigned.

The ask happens at most once per thread, fenced on the timeline (review_verdict_requested, migration 191) rather than in memory, so it survives a pod restart. An unanswered ask holds the attempt budget back for VERDICT_NUDGE_GRACE_MS (10 min), so the follow-up run's queue wait can't be mistaken for another silent completion.

The reviewer prompt also now says outright that ending without a decision is the one failure the run cannot leave behind.

Note on the extra read

enqueueEnabledReviewers now always reads the activity list. #6689 made the cycle boundary a column precisely to avoid that read — but the verdicts and the verdict-asks only exist on the timeline, so this path needs it regardless; the boundary still comes from reviewCycleStartedAt, with reviewCycleStart's activity fallback for pre-190 cards. One indexed query per swept card per 5 min, which is what this path cost before #6689.

Testing

  • bun test apps/api/src/tools/task-board/enqueue-reviewer.test.ts — 42 pass, 8 new cases (undecided → asked once, failed/live never asked, timeline-scoped asks, grace window, asked-then-still-undecided → spent → hand-off).
  • activity-actions.test.ts re-pointed at migration 191; notification-types.test.ts still holds — the new action is deliberately not a notification type, so NOTIFIED_ACTIONS keeps it out of the inbox.
  • bun run check and bun run lint clean. The 10 remaining task-board test failures are pre-existing on main (they need Postgres).

Needs bun run migrate on deploy (CHECK constraint swap only).

@pedrofrxncx pedrofrxncx changed the title fix(task-board): a reviewer that completes without a verdict is a spent attempt fix(task-board): a review cycle always ends in a verdict or a person Aug 28, 2026
Pedro França added 2 commits August 28, 2026 14:16
…nt attempt

A reviewer run that finishes without calling the decision tool - it ran out
of turns, or stopped while waiting on a background task - left the card
stranded In Review at 0/1 forever: `reviewerHandledThisCycle` read any
completed thread as a review, so nothing re-dispatched it, and its attempt
counted as unspent, so nothing handed it to a human either.

Spentness now asks the cycle's timeline for a verdict instead of trusting the
thread's terminal status. A completed-but-undecided attempt re-dispatches
within the existing `MAX_REVIEWER_ATTEMPTS` budget and hands over when it runs
out.
A reviewer run that finishes without calling TASK_BOARD_REVIEW_DECISION is now
asked for one on its own thread, once per attempt, recorded on the timeline as
`review_verdict_requested` so the ask survives a restart and can only happen
once. Prompt says: decide on what you know, request_changes if you could not
finish.

If it still ends undecided the attempt is spent, so the existing budget takes
over — a second reviewer, then the hand-to-human path. Every review cycle now
terminates in a verdict or a person.
@pedrofrxncx
pedrofrxncx force-pushed the fix/reviewer-completed-without-verdict branch from aa25350 to cb35aa4 Compare August 28, 2026 17:17
@pedrofrxncx
pedrofrxncx enabled auto-merge (squash) August 28, 2026 17:19
@pedrofrxncx
pedrofrxncx merged commit 3338669 into main Aug 28, 2026
34 checks passed
@pedrofrxncx
pedrofrxncx deleted the fix/reviewer-completed-without-verdict branch August 28, 2026 18:07
decocms Bot pushed a commit that referenced this pull request Aug 28, 2026
PR: #6688 fix(task-board): a review cycle always ends in a verdict or a person
Bump type: patch

- decocms (apps/api/package.json): 4.297.3 -> 4.297.4
- @decocms/native (apps/native/package.json): 4.297.3 -> 4.297.4
- @decocms/shared (packages/shared/package.json): 0.71.1 -> 0.71.2

Deploy-Scope: both
pedrofrxncx added a commit that referenced this pull request Aug 31, 2026
…6688)

* fix(task-board): a reviewer that completes without a verdict is a spent attempt

A reviewer run that finishes without calling the decision tool - it ran out
of turns, or stopped while waiting on a background task - left the card
stranded In Review at 0/1 forever: `reviewerHandledThisCycle` read any
completed thread as a review, so nothing re-dispatched it, and its attempt
counted as unspent, so nothing handed it to a human either.

Spentness now asks the cycle's timeline for a verdict instead of trusting the
thread's terminal status. A completed-but-undecided attempt re-dispatches
within the existing `MAX_REVIEWER_ATTEMPTS` budget and hands over when it runs
out.

* fix(task-board): ask a reviewer that ended undecided for its verdict

A reviewer run that finishes without calling TASK_BOARD_REVIEW_DECISION is now
asked for one on its own thread, once per attempt, recorded on the timeline as
`review_verdict_requested` so the ask survives a restart and can only happen
once. Prompt says: decide on what you know, request_changes if you could not
finish.

If it still ends undecided the attempt is spent, so the existing budget takes
over — a second reviewer, then the hand-to-human path. Every review cycle now
terminates in a verdict or a person.

---------

Co-authored-by: Pedro França <pedrofrxncx@deco.cx>
pedrofrxncx pushed a commit that referenced this pull request Aug 31, 2026
PR: #6688 fix(task-board): a review cycle always ends in a verdict or a person
Bump type: patch

- decocms (apps/api/package.json): 4.297.3 -> 4.297.4
- @decocms/native (apps/native/package.json): 4.297.3 -> 4.297.4
- @decocms/shared (packages/shared/package.json): 0.71.1 -> 0.71.2

Deploy-Scope: both
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