You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two passes can publish the same PR concurrently — duplicate gate check-runs, verdict thrash
maybePublishPrPublicSurface runs entirely outside the per-PR actuation mutex. Both entry points (sweep src/queue/processors.ts ~3762, webhook ~6644) call it unlocked; only its AI-review sub-section takes ai-review-lock (~10119-10141) and only the latermaybeRunAgentMaintenance takes pr-actuation-lock (~2447). The file's own comment (~2439-2447) acknowledges concurrent same-PR passes are reachable.
Observed live on PR #8965 (2026-07-26): two github_app.pr_public_surface_published events 13 seconds apart (14:55:11 and 14:55:24) with a type_label_decision | denied | lock_contended between them — i.e. pass B lost the type-label lock, logged it, and continued publishing anyway (~8985: logTypeLabelSkip(...) then falls through).
Two concrete harms:
Duplicate gate check-runs.createOrUpdatePendingGateCheckRun (~9138) → createOrUpdateNamedCheckRun with no checkRunId does GET-latest-by-name-and-SHA then POST-if-absent (src/github/app.ts ~1057-1080). On the first publish for a head, both passes GET (find nothing) and both POST → two check-runs named "LoopOver Gate" on one commit with divergent conclusions. Unlike panel comments — which self-heal via deleteDuplicateMarkerComments (src/github/comments.ts ~177-195) — there is no dedup for check-runs; later passes PATCH only filter:latest, so the stale duplicate persists forever and can hold the PR under strict branch protection.
A lock-loser overwrites a real verdict with a placeholder. The AI-review lock loser takes aiReviewLockContendedResult (~10126-10127) and keeps going to publish a full surface carrying the ai_review_inconclusive hold. Because the bodies differ, the byte-identical no-op guard (comments.ts ~143-147) doesn't absorb it: whichever pass PATCHes last owns the panel. If the loser lands second, a genuine verdict is replaced by "AI review already in progress". Both passes also record pr_public_surface_published, double-counting public stats.
Two passes can publish the same PR concurrently — duplicate gate check-runs, verdict thrash
maybePublishPrPublicSurfaceruns entirely outside the per-PR actuation mutex. Both entry points (sweepsrc/queue/processors.ts~3762, webhook ~6644) call it unlocked; only its AI-review sub-section takesai-review-lock(~10119-10141) and only the latermaybeRunAgentMaintenancetakespr-actuation-lock(~2447). The file's own comment (~2439-2447) acknowledges concurrent same-PR passes are reachable.Observed live on PR #8965 (2026-07-26): two
github_app.pr_public_surface_publishedevents 13 seconds apart (14:55:11 and 14:55:24) with atype_label_decision | denied | lock_contendedbetween them — i.e. pass B lost the type-label lock, logged it, and continued publishing anyway (~8985:logTypeLabelSkip(...)then falls through).Two concrete harms:
Duplicate gate check-runs.
createOrUpdatePendingGateCheckRun(~9138) →createOrUpdateNamedCheckRunwith nocheckRunIddoes GET-latest-by-name-and-SHA then POST-if-absent (src/github/app.ts~1057-1080). On the first publish for a head, both passes GET (find nothing) and both POST → two check-runs named "LoopOver Gate" on one commit with divergent conclusions. Unlike panel comments — which self-heal viadeleteDuplicateMarkerComments(src/github/comments.ts~177-195) — there is no dedup for check-runs; later passes PATCH onlyfilter:latest, so the stale duplicate persists forever and can hold the PR under strict branch protection.A lock-loser overwrites a real verdict with a placeholder. The AI-review lock loser takes
aiReviewLockContendedResult(~10126-10127) and keeps going to publish a full surface carrying theai_review_inconclusivehold. Because the bodies differ, the byte-identical no-op guard (comments.ts~143-147) doesn't absorb it: whichever pass PATCHes last owns the panel. If the loser lands second, a genuine verdict is replaced by "AI review already in progress". Both passes also recordpr_public_surface_published, double-counting public stats.Fix
Acceptance
Refs #8997, #9008.