Context
recordPreviewPollAttempt (src/review/visual/preview-poll-budget.ts:70-79) does an unguarded read-modify-write against R2: read the current attempt-count marker via readBudgetMarker (:43-55), compute count + 1, write it back. There's no compare-and-swap or conditional write. The module's own header explains this budget exists precisely because multiple independent triggers (self-poll job chain, CI-completion webhook, deployment_status webhook, sweep pass) can all call into buildCapture for the same head SHA around the same time. If two triggers race — both read the marker at count=N before either writes — both write count=N+1, silently losing one increment. This lets the actual number of "still building, keep trying" poll attempts modestly exceed MAX_PREVIEW_POLL_ATTEMPTS (5). Consequence is soft (a few extra polls, not a correctness/security issue) — the existing doc comment already reasons about a single write failing ("a failed write just means this specific attempt doesn't count toward the budget... the safer failure direction"), just not about two concurrent successful writes racing.
Requirements
Make recordPreviewPollAttempt's increment safe under concurrent callers for the same head SHA — e.g. a conditional/compare-and-swap write against R2 (only write if the marker's current value still matches what was just read; retry on mismatch), or an equivalent mechanism already used elsewhere in this codebase for the same class of problem if one exists (check for a precedent before inventing a new one). Preserve the existing fail-open behavior for a genuine write failure (per the module's own stated rationale) — this issue is only about the concurrent-success race, not changing the failure-direction policy.
Deliverables
Test Coverage Requirements
src/review/** is under the top-level 99% patch coverage gate — the fix and its concurrency test must both be covered.
Expected Outcome
Two triggers racing to record a preview-poll attempt for the same head SHA both have their increment counted, keeping the actual poll count from silently exceeding MAX_PREVIEW_POLL_ATTEMPTS by more than the documented fail-open margin.
Links & Resources
src/review/visual/preview-poll-budget.ts:43-55 (readBudgetMarker), :70-79 (recordPreviewPollAttempt)
Context
recordPreviewPollAttempt(src/review/visual/preview-poll-budget.ts:70-79) does an unguarded read-modify-write against R2: read the current attempt-count marker viareadBudgetMarker(:43-55), computecount + 1, write it back. There's no compare-and-swap or conditional write. The module's own header explains this budget exists precisely because multiple independent triggers (self-poll job chain, CI-completion webhook,deployment_statuswebhook, sweep pass) can all call intobuildCapturefor the same head SHA around the same time. If two triggers race — both read the marker atcount=Nbefore either writes — both writecount=N+1, silently losing one increment. This lets the actual number of "still building, keep trying" poll attempts modestly exceedMAX_PREVIEW_POLL_ATTEMPTS(5). Consequence is soft (a few extra polls, not a correctness/security issue) — the existing doc comment already reasons about a single write failing ("a failed write just means this specific attempt doesn't count toward the budget... the safer failure direction"), just not about two concurrent successful writes racing.Requirements
Make
recordPreviewPollAttempt's increment safe under concurrent callers for the same head SHA — e.g. a conditional/compare-and-swap write against R2 (only write if the marker's current value still matches what was just read; retry on mismatch), or an equivalent mechanism already used elsewhere in this codebase for the same class of problem if one exists (check for a precedent before inventing a new one). Preserve the existing fail-open behavior for a genuine write failure (per the module's own stated rationale) — this issue is only about the concurrent-success race, not changing the failure-direction policy.Deliverables
recordPreviewPollAttemptinsrc/review/visual/preview-poll-budget.tsno longer loses increments when two triggers race for the same head SHA.Test Coverage Requirements
src/review/**is under the top-level 99% patch coverage gate — the fix and its concurrency test must both be covered.Expected Outcome
Two triggers racing to record a preview-poll attempt for the same head SHA both have their increment counted, keeping the actual poll count from silently exceeding
MAX_PREVIEW_POLL_ATTEMPTSby more than the documented fail-open margin.Links & Resources
src/review/visual/preview-poll-budget.ts:43-55(readBudgetMarker),:70-79(recordPreviewPollAttempt)