Summary
Two near-simultaneous GitHub webhook deliveries for the same PR at the identical head SHA can each independently reach the AI-review cache-read decision in maybePublishPrPublicSurface before either one claims the per-(repo, PR, head, mode) claimAiReviewLock. Both log a cache miss and proceed toward a fresh AI review, duplicating expensive prep work (manifest resolution, review-file loading, fingerprint computation) and, depending on exact timing, a real LLM call.
Area
GitHub App
Expected behavior
Only one of two racing passes for the same (repo, PR, head SHA, mode) should reach the cache-read/AI-review decision. The losing pass should defer cleanly to the winner (or the next webhook/sweep tick), without duplicating cache reads, cache-miss logging, or AI spend.
Actual behavior
claimAiReviewLock is claimed deep inside runAiReviewForAdvisory, but the caller (maybePublishPrPublicSurface) already performs the cache-read, cache-miss logging, review-manifest resolution, and review-file loading before runAiReviewForAdvisory (and therefore the lock) is ever reached. Two concurrent deliveries can both sail through that unguarded window, both log an ai_review_cache_miss audit event, and only get arbitrated once they're already at the point of running the reviewer.
Reproduction
Two GitHub webhook deliveries (or a webhook racing a scheduled re-gate sweep tick) for the same PR, arriving within roughly a second of each other, at an unchanged head SHA.
Validation
Reproduced with a regression test that fires two concurrent agent-regate-pr jobs for the same PR/head via Promise.all and asserts github_app.ai_review_cache_miss is recorded exactly once (not twice).
Summary
Two near-simultaneous GitHub webhook deliveries for the same PR at the identical head SHA can each independently reach the AI-review cache-read decision in
maybePublishPrPublicSurfacebefore either one claims the per-(repo, PR, head, mode)claimAiReviewLock. Both log a cache miss and proceed toward a fresh AI review, duplicating expensive prep work (manifest resolution, review-file loading, fingerprint computation) and, depending on exact timing, a real LLM call.Area
GitHub App
Expected behavior
Only one of two racing passes for the same (repo, PR, head SHA, mode) should reach the cache-read/AI-review decision. The losing pass should defer cleanly to the winner (or the next webhook/sweep tick), without duplicating cache reads, cache-miss logging, or AI spend.
Actual behavior
claimAiReviewLockis claimed deep insiderunAiReviewForAdvisory, but the caller (maybePublishPrPublicSurface) already performs the cache-read, cache-miss logging, review-manifest resolution, and review-file loading beforerunAiReviewForAdvisory(and therefore the lock) is ever reached. Two concurrent deliveries can both sail through that unguarded window, both log anai_review_cache_missaudit event, and only get arbitrated once they're already at the point of running the reviewer.Reproduction
Two GitHub webhook deliveries (or a webhook racing a scheduled re-gate sweep tick) for the same PR, arriving within roughly a second of each other, at an unchanged head SHA.
Validation
Reproduced with a regression test that fires two concurrent
agent-regate-prjobs for the same PR/head viaPromise.alland assertsgithub_app.ai_review_cache_missis recorded exactly once (not twice).