Skip to content

refactor(task-board): fence reviewer dispatch on ids, sign reviewer identity - #5813

Merged
pedrofrxncx merged 1 commit into
mainfrom
claude/drop-review-claims
Aug 13, 2026
Merged

refactor(task-board): fence reviewer dispatch on ids, sign reviewer identity#5813
pedrofrxncx merged 1 commit into
mainfrom
claude/drop-review-claims

Conversation

@pedrofrxncx

@pedrofrxncx pedrofrxncx commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Implements apps/api/src/tools/task-board/review-claims-removal-spec.md — step A of its two-deploy rollout.

What

task_board_review_claims existed to do two unrelated jobs. Neither needs a table.

1. Idempotent dispatch → deterministic ids. The reviewer run's thread id is now derived from (task, reviewer, cycleAt.toISOString()), so the threads primary key is the fence: the racing triggers (60s sweeper, the task dialog's 10s poll) collapse on an INSERT … ON CONFLICT DO NOTHING, and the loser returns isNew: false before anything else happens — no duplicate thread, no duplicate review_requested, no duplicate run. The run also carries a matching deterministic DBOS workflow ID (review:<item>:<kind>:<cycle>), the stall-nudge:<item>:<thread> idiom.

2. Reviewer identity → HMAC. The rtok_ token is now a signature over the same tuple (review-token.ts, modelled on file-storage/share-password.ts: same signing key, same timingSafeEqual) instead of a random string resolved through the table. TASK_BOARD_REVIEW_DECISION verifies it statelessly, deriving the cycle from the existing lastInReviewTime helper. Semantics are unchanged: a missing/wrong token still records the decision with verified: false, so it never counts toward allEnabledReviewersVerifiedApproved. Fail open on recording, fail closed on merging. The web ship button's deliberate asymmetry is untouched.

Deviation from the spec

The spec puts the fence at the DBOS workflow ID alone. That is not sufficient: enqueueAgentRunForTask creates the thread, links it, and records activity before enqueueThreadRun, so both racers would still produce a thread and a timeline entry with only the run collapsing. Hence the deterministic thread id as the real fence.

For the same reason the compensation the spec deletes could not be deleted outright — a dispatch that throws after the thread exists would leave reviewerHandledThisCycle reading it as this cycle's reviewer forever. It is now a threads.delete of the fence thread instead of a claim-row delete.

Rollout

  • This PR (A): claims are no longer written; claimReviewer and releaseReviewerClaim deleted. The decision tool accepts an HMAC match or a legacy claim lookup, so review cycles in flight across the deploy keep counting toward auto-merge. Table still present.
  • Follow-up (B), after a day: delete the legacy branch, resolveReviewClaimByToken, TaskBoardReviewClaimTable, and add the drop migration.

Testing

  • New review-token.test.ts: round-trip plus wrong-reviewer / wrong-task / wrong-cycle / tampered / empty rejection.
  • bun test apps/api/src/tools/task-board/{review-token,enqueue-reviewer,review-sweeper}.test.ts — 17 pass. No existing test asserted claim behavior (nothing to invert).
  • bun run check, bun run lint, bun run fmt, knip all clean.
  • Not run: the real-Postgres integration tests and the end-to-end live-race check on a real card (spec's verification 2 and 4).

Summary by cubic

Fences reviewer dispatch on deterministic thread and workflow IDs keyed by (task, reviewer, cycle, attempt), and replaces claim-table tokens with HMAC-signed rtok_ tokens. This stops duplicate runs and verifies reviewer identity while keeping the “record unverified decisions but exclude them from auto-merge” behavior.

  • Derives the reviewer thread ID from (task, reviewer, cycle, attempt) so the threads PK is the fence; the run uses a matching workflow ID review:<item>:<kind>:<cycle>:<attempt>. enqueueAgentRunForTask now accepts fence and returns isNew; on enqueue failure, it deletes the fence thread to avoid a stuck “handled” state.
  • Mints and verifies reviewer tokens statelessly via review-token.ts (TASK_BOARD_REVIEW_DECISION checks an HMAC over (task, reviewer, current cycle)); unverified decisions still record but don’t count toward auto-merge. Falls back to legacy claim lookup for cycles dispatched before this change.

Rollout

  • Step A: stop writing claims; remove claimReviewer/releaseReviewerClaim; mint HMAC tokens; accept legacy claim tokens for in-flight cycles.
  • Step B (after cycles drain): delete the legacy branch, remove resolveReviewClaimByToken, and drop task_board_review_claims with its migration.

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

Review in cubic

@pedrofrxncx
pedrofrxncx force-pushed the claude/drop-review-claims branch from 61e91ca to 52874be Compare August 6, 2026 15:29
@pedrofrxncx
pedrofrxncx enabled auto-merge (squash) August 6, 2026 15:33
@pedrofrxncx
pedrofrxncx force-pushed the claude/drop-review-claims branch from 7b5c542 to 4dfd425 Compare August 13, 2026 14:35
…dentity

`task_board_review_claims` did two unrelated jobs — idempotent dispatch and
reviewer-identity binding — and neither needs a table.

Dispatch: the reviewer run's thread id is now derived from
(task, reviewer, cycle, attempt), so the `threads` PK is the fence — the racing
triggers (60s sweeper, the dialog's 10s poll) collapse on an INSERT … ON
CONFLICT DO NOTHING, and the run itself carries the matching deterministic DBOS
workflow ID (`review:<item>:<kind>:<cycle>:<attempt>`), the same idiom as
`stall-nudge:<item>:<thread>`. `attempt` is the cycle's spent-attempt count, so
the retry `MAX_REVIEWER_ATTEMPTS` allows after a dead run gets ids of its own
instead of collapsing onto the corpse — the job `releaseReviewerClaim` did.

Identity: the `rtok_` token is an HMAC over (task, reviewer, cycle)
(`review-token.ts`, modelled on `file-storage/share-password.ts`) instead of a
random string looked up in the table. `TASK_BOARD_REVIEW_DECISION` verifies it
statelessly and keeps today's semantics exactly — a missing/wrong token still
records the decision, just with `verified: false`, so it never counts toward
auto-merge, and it is scoped to the CURRENT cycle on both paths so a token kept
across a bounce cannot re-approve without a review.

Rollout step A: claims are no longer written and `claimReviewer` /
`releaseReviewerClaim` are gone, but the decision tool still accepts a legacy
claim token so review cycles in flight across the deploy keep counting.
Step B (follow-up, after a day): drop the legacy branch,
`resolveReviewClaimByToken`, `reviewTokenVerified`, `TaskBoardReviewClaimTable`,
and the table.
@pedrofrxncx
pedrofrxncx force-pushed the claude/drop-review-claims branch from 4dfd425 to b759ea0 Compare August 13, 2026 14:36
@pedrofrxncx
pedrofrxncx merged commit 7fe2e8b into main Aug 13, 2026
25 checks passed
@pedrofrxncx
pedrofrxncx deleted the claude/drop-review-claims branch August 13, 2026 14:45
decocms Bot pushed a commit that referenced this pull request Aug 13, 2026
PR: #5813 refactor(task-board): fence reviewer dispatch on ids, sign reviewer identity
Bump type: patch

- decocms (apps/api/package.json): 4.213.0 -> 4.213.1
- @decocms/native (apps/native/package.json): 4.213.0 -> 4.213.1

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