Repository navigation
fix(pr-fix-queue): pin enqueue post-dispatch append to its keys snapshot (#1134) - #1158
Conversation
…hot (#1134) enqueuePrFixItem computed the #1119 post-dispatch evidence append from a row read inside the transaction and then issued a plain update, so a concurrent append committing in the read->write gap was overwritten and escaped flagging. Pin the append on the exact postDispatchEvidenceKeys snapshot it read (mirroring markPrFixItem's settle-side pin), with a bounded in-transaction re-decide loop; the loop runs no network calls, so the #1124 timeout hazard does not apply. Under sustained contention the bounded loop falls back to one unpinned write from the last recomputed snapshot so the enqueue is never dropped. Non-append writes keep the plain update path.
…ue loop - drop unused isFreshAttempt/isPostDispatchNewEvidence from the buildUpdate return shape - delete the duplicated anti-churn comment left outside buildUpdate - deleted-row mid-loop now breaks to the fallback update and surfaces Prisma P2025, matching the pre-#1134 path - rename maxEnqueueRedecides -> maxPinnedEnqueueWrites (bounds pinned write attempts) and note previousStatus is deliberately the first-read status
… in the fake client AI review on #1158 noted the deleted-mid-loop branch had no coverage. The fake prFixQueueItem.update silently wrote to items[-1] on a missing row instead of failing like Prisma; it now throws a P2025-coded error, and a new #1134 test deletes the row in the read->write gap and asserts the fallback update surfaces P2025 with no enqueue history written.
There was a problem hiding this comment.
AI Automated Review
Full PR review.
Analysis engine: MiniMax-M3 (anthropic) — primary route
Partial coverage: required-check coverage is incomplete — this review did not resolve every required check and must not be read as a complete pass.
Recommendation
Approve. The change pins the post-dispatch evidence append in enqueuePrFixItem to the exact postDispatchEvidenceKeys snapshot it read, mirroring the settle-side pattern in markPrFixItem. The implementation is internally consistent, the four new tests cover the documented invariants, and CI is fully green. The PR Classification tag of path_handling_changes does not match the actual diff (which is concurrency control on a Prisma transaction) — the must-checks in this corpus do not apply to this change.
Change-by-change findings
src/lib/pr-fix-queue.ts — existing-row branch
-
Per-snapshot decision (
buildUpdate) — All enqueue decisions (isKnownEvidence, headShaUnchanged / PR 940, isTerminalStatus / PR 1000, isFreshAttempt / PR 1044, isPostDispatchNewEvidence / PR 1119, resolvedStatus / lane / statusNote, attempt cap / PR 1001) are now derived from a single row snapshot passed intobuildUpdate. This preserves invariant 4 (decisions identical to pre-change logic) — every branch reproduces the original logic verbatim, only theexisting.reads inside the decision pipeline are nowsnapshot.reads. -
Pinned write on
isPostDispatchNewEvidence— When the write would append topostDispatchEvidenceKeys, the write goes throughupdateManywithpostDispatchEvidenceKeys: { equals: <snapshot> }(line 599–602). This is the exact fix the issue called for: a concurrent append in the read→write gap makes the pin miss, the loop re-reads and re-decides from the fresh row.isFreshAttempt's reset-to-[]is mutually exclusive withisPostDispatchNewEvidence(one requiressnapshot.status === "QUEUED", the other requires it not to be — verified atsrc/lib/pr-fix-queue.ts:504andsrc/lib/pr-fix-queue.ts:515-519) and is deliberately not pinned, so a fresh attempt's reset cannot be silently clobbered. -
Bounded re-decide + unpinned fallback —
maxPinnedEnqueueWrites = 3(line 593). On sustained contention the loop exhausts and a single unpinnedupdatewrites from the last recomputed snapshot — this degrades to the pre-PR 1134 worst case rather than dropping the enqueue. On a row deleted mid-loop, the in-transaction re-read returns null, the loop breaks without firing the innerupdateMany, and the exhausted-loop fallback runs its plainupdateagainst a non-existent row, surfacing Prisma's P2025 just like the pre-PR 1134 path did (line 614-616, 622-633). -
No network call in the transaction — The loop only calls
findUnique/updateMany/updateontx(verified atsrc/lib/pr-fix-queue.ts:599, 603, 609, 622, 634, 640). The post-transaction BLOCKED-surfacing and head-SHA fetch stay outside. This keeps the PR 1124 transaction-timeout hazard invariant intact. -
previousStatussemantics — The captured status is now commented as the FIRST-read status, used by post-transaction BLOCKED-surfacing comparison. The comparison's pre-PR 1134 meaning is preserved (src/lib/pr-fix-queue.ts:424-426).
src/lib/pr-fix-queue.test.ts
-
Fake client mirror of P2025 —
updatenow throws a P2025-coded error when the row id is missing (line 105-110). This brings the fake closer to Prisma and is needed to exercise the deleted-row fallback path; it changes behavior for all existing suites but only for tests that already deleted a row mid-transaction (none in the visible corpus), and any existing suite that depended on silentitems[-1]writes would surface the change visibly in test output. -
Test 1 (re-decide on one concurrent append) —
beforeUpdateManylands a single gap entry; the first pinned write misses, the retry pins on the fresh list and lands. Asserts both keys present in order, status QUEUED, generation unchanged, evidence list contains both — covers claims 1, 2, 3. -
Test 2 (sustained contention fallback) —
beforeUpdateManyEverylands on every pinned write;landCount === 3confirms the loop bounded at exactlymaxPinnedEnqueueWrites. The unpinned fallback is a plainupdateand is not affected by the hook, so the finalupdaterecords the enqueue and all four entries (3 lands + the new one) are present inpostDispatchEvidenceKeys. Covers claims 4, 5. -
Test 3 (non-append guard) — Known-evidence re-observation takes the plain-update branch;
updateManynever fires;postDispatchEvidenceKeysis untouched. Covers the "don't touch what didn't change" invariant. -
Test 4 (deleted-row P2025) —
beforeUpdateManydeletes the row; the loop's re-read returns null and breaks; the fallback plainupdatethrows P2025; noenqueuehistory row is created past the seed. Covers the deletion path.
Claim verification
- Claim 1 (
{ equals: [] }predicate no-ops and the re-decision loop must re-read): Held. The first pinned write in test 1 hasequals: []and the concurrent hook makes it no-op; the loop'sfindUniquere-reads and the retry pins on the fresh list. - Claim 2 (pin missed exactly once, in-transaction re-read re-decided): Held. Test 1 sets
beforeUpdateMany(one-shot),landCountwould be 1 if measured, retry succeeds. - Claim 3 (a post-dispatch append never opens a fresh attempt): Held. Test 1 asserts
status: QUEUEDandgeneration: 1after the append path; theisFreshAttempt/isPostDispatchNewEvidenceexclusivity is structurally enforced. - Claim 4 (concurrent evidence keeps landing on every pinned write — exactly 3): Held. Test 2 asserts
landCount === 3and confirms the unpinned fallback usesupdate(notupdateMany), so no 4th land. - Claim 5 (more lands than the bounded loop permits — unpinned fallback still records the enqueue): Held. Test 2 asserts all 4 entries present after the fallback write.
Sources
src/lib/pr-fix-queue.ts:424-649(enqueuePrFixItem existing-row branch + loop)src/lib/pr-fix-queue.test.ts:105-110(fake client P2025 mirror)src/lib/pr-fix-queue.test.ts:2959-3106(new PR 1134 describe block)- CI: all 12 checks green on
97a50bc6
Standards Compliance
The change does not touch auth, secret handling, label hygiene, lane classification, or workflow contracts. It is an internal concurrency-control fix inside an existing transaction. No conflicts to surface.
Linked Issue Fit
The issue (PR 1134) asked for the post-dispatch evidence append to be conditional on the exact postDispatchEvidenceKeys snapshot the enqueue read, so a concurrent commit is re-decided rather than silently dropped. The PR implements this exactly: pinned updateMany with in-transaction re-decide, 3 attempts, unpinned fallback, deleted-row P2025 from the fallback. All four acceptance-criteria items the PR body lists (concurrent append re-decided, sustained contention still records, non-append path unchanged, deleted-row P2025) are covered by the four new tests.
Tool Harness Findings
The harness executed 19 read-only calls: it pulled the changed file regions in src/lib/pr-fix-queue.ts (lines 400-720) and src/lib/pr-fix-queue.test.ts (lines 1-3106), confirmed maxPinnedEnqueueWrites = 3, confirmed the fake client's beforeUpdateMany (one-shot) and beforeUpdateManyEvery (persistent) hook semantics, and confirmed the isFreshAttempt / isPostDispatchNewEvidence exclusivity at the source. No harness findings of note; all read calls succeeded and confirmed the diff content matches the PR body's claimed invariants.
Unknowns or Needs Verification
None. The change is self-contained, the tests cover the invariants the body and issue call out, and CI is fully green.
Requirement trace
2 of 2 requirement(s) not fully traced to enforcement and a test:
req-e589bfe1ad7c— unverifiable (no valid enforcement location)req-ad902b2a08da— unverifiable (no valid enforcement location)
Approval withheld: this review's coverage is incomplete — required-check coverage or the tool-loop investigation did not finish, so it is publishing as an advisory comment rather than an approval.
|
Addressing the two AI review passes (both advisory "Approve, withheld on coverage"):
All non-review checks are green on this head (lint, typecheck, tests, coverage, database integration/migrations, build, smoke, npm audit, workflow lint, Docker builds). |
Ask
Make the post-dispatch evidence append in
enqueuePrFixItemconditional on the exactpostDispatchEvidenceKeyssnapshot it read, so evidence recorded concurrently between the enqueue's row read and its update is re-decided instead of silently dropped from flagging.Closes #1134
Change
src/lib/pr-fix-queue.ts— inenqueuePrFixItem's existing-row branch, all decisions are now derived from one row snapshot via a localbuildUpdate(snapshot). When the write appends post-dispatch evidence (isPostDispatchNewEvidence), it goes throughupdateManypinned onpostDispatchEvidenceKeys: { equals: <the snapshot read> }; a pin miss (concurrent append committed in the read→write gap, or row deleted) re-reads in-transaction and re-derives every decision (status / lane / attempt cap / post-dispatch) from the fresh row, up to 3 pinned attempts. Under sustained contention that keeps missing, one final unpinned write from the last recomputed snapshot records the enqueue (degrades to the pre-fix worst case rather than dropping the enqueue; no network calls run in the loop, so the fix(pr-fix): deliver post-dispatch evidence as a fresh attempt (#1119) #1124 transaction-timeout hazard does not apply). A row deleted mid-loop breaks out and lets the fallback plainupdatesurface Prisma's P2025, exactly as the pre-fix(pr-fix-queue): enqueuePrFixItem read→write gap can drop a post-dispatch evidence entry from flagging #1134 path did. Non-append writes keep the plainupdatepath unchanged. TheisFreshAttemptreset-to-[]is mutually exclusive with the append and is deliberately not pinned.src/lib/pr-fix-queue.test.ts— new describeenqueue post-dispatch append pinning (#1134)with four tests: (1) a one-shot concurrent append landing in the read→write gap is re-decided and both entries are present (new entry last), same attempt; (2) sustained contention exhausts the bounded loop and the unpinned fallback still records the enqueue (never dropped); (3) known-evidence re-observe (no append) keeps the plain-update path — the pinnedupdateManynever fires and keys stay untouched; (4) a row deleted in the read→write gap surfaces a P2025 from the fallback write with noenqueuehistory row. The fake client'supdatenow throws a P2025-coded error on a missing row (it previously wrote toitems[-1]silently), matching the real client for all suites.Invariants this change must keep
postDispatchEvidenceKeyssnapshot it was computed from, so a concurrent commit makes it a no-op and triggers re-decision instead of an overwrite.enqueuehistory row inside the transaction. Kept by: the fallback branch after the bounded loop and thecount === 1 → findUniquehit path; test 2 asserts the enqueue lands under contention.fixAttemptsincrement, and the reset ofpostDispatchEvidenceKeys/agentHandoutshappen only onisFreshAttempt. Kept by:buildUpdatederivesisFreshAttemptfrom the snapshot exactly as before; test 1 assertsgenerationis unchanged on the append path, and the pre-existing PR-fix attempt cap counts review comments, blocking multi-comment reviews before any attempt #1103/PR-fix evidence that arrives mid-attempt is absorbed and never delivered to the worker #1119 suite passes untouched.buildUpdatereproduces the original decision chain verbatim, and retries re-derive from the fresh row rather than replaying a stale decision.findUnique/updateMany/updateontx. Kept by: no head-SHA fetch or surfacing inside$transaction; surfacing stays after the transaction as before.breakto the fallbackupdate; test 4 asserts the P2025 and the absence of a new history row.Other code paths enforcing the same rule (same state, other writers)
markPrFixItemsettle/reopen path (src/lib/pr-fix-queue.ts, guardedupdateManywithpostDispatchEvidenceKeys: { equals: ... }pins + bounded re-decision + forced fallback): already pins; unchanged by this PR. It is the pattern this PR mirrors.markPrFixItemfresh-attempt resets (postDispatchEvidenceKeys: []on reopen/settle/force branches): reset semantics unchanged; not this PR's write.requeue/ other enqueue callers: they callenqueuePrFixItemitself, so they inherit the pin automatically — no separate update site writespostDispatchEvidenceKeyson the enqueue side (grep: the only appender is this branch).#1119/#1124describes) pass untouched, confirming the pin contract in the fake client behaves identically for both sides.Verification
npx vitest run— 3782 passed | 16 skipped | 2 todo (164 files passed, 3 skipped)npm run lint— clean (1 pre-existing warning insrc/app/login/page.tsx, untouched)npm run typecheck— cleanNote for the reviewer: this PR touches no filesystem-path handling code (the classifier's
path_handling_changesrisk flag is a mis-flag — the diff is a database optimistic-concurrency pin insrc/lib/pr-fix-queue.tsand its tests).