Skip to content

fix(queue): dedupe concurrent AI reviews for the same PR head - #2429

Merged
JSONbored merged 4 commits into
mainfrom
fix/ai-review-cache-merge-recheck
Jul 1, 2026
Merged

fix(queue): dedupe concurrent AI reviews for the same PR head#2429
JSONbored merged 4 commits into
mainfrom
fix/ai-review-cache-merge-recheck

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

  • A webhook pass and an agent-regate-pr sweep pass can independently miss the AI review cache for the same (repo, PR, head SHA, review mode) and each fire a real LLM call, potentially landing different verdicts (e.g. one call flags a consensus defect, the other doesn't) for the identical head — since both share a head SHA, freshness guards elsewhere never catch this.
  • Adds claimAiReviewLock/releaseAiReviewLock, a per-(repo, PR, head SHA, mode) advisory lock mirroring the existing per-PR maintenance lock (fix(queue): serialize concurrent webhook/sweep passes for the same PR #2368), claimed immediately before the expensive grounding/RAG/enrichment/LLM section of runAiReviewForAdvisory and released in a finally covering every exit path.
  • A pass that loses the race returns the existing ai_review_inconclusive shape (non-cacheable, reviewerCount: 0), which the gate already treats as "held for human review" (neutral) rather than success/failure — reusing the established fail-safe instead of inventing new gate logic.
  • Lock TTL is 30 minutes (a crash-safety backstop only, not a throughput bound), sized to the codebase's own hard ceiling on self-host AI provider timeouts, not copied from the faster 60s maintenance lock.
  • Fails open on any transient-cache fault, matching the sibling lock's philosophy: the lock is defense-in-depth, never the primary safety gate.

Test plan

  • npx tsc --noEmit -p . clean
  • Targeted suites (queue.test.ts, ai-review-advisory.test.ts, gate-check-policy.test.ts): 380/380 passing
  • Full unsharded npm run test:coverage: 312 files / 5831 tests passing, 0 failures
  • Per-file patch coverage: src/queue/processors.ts — zero uncovered added lines/branches
  • Stash/revert proof: reverting the source change makes the 8 new tests fail (claimAiReviewLock is not a function); restoring makes them pass again
  • git diff --check clean; npm audit --audit-level=moderate: 0 vulnerabilities

A webhook pass and an agent-regate-pr sweep pass can independently miss
the AI review cache for the same (repo, PR, head SHA, mode) and each
fire a real LLM call, potentially landing different verdicts at the
same head. Add a per-key advisory lock (mirroring the existing
per-PR maintenance lock) around the review call; a pass that loses the
race returns the existing ai_review_inconclusive shape so the gate
holds for human review instead of racing an independent verdict.
@loopover-orb

loopover-orb Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Warning

🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨

⏸️ Gittensory review result - manual review recommended

Review updated: 2026-07-01 23:42:15 UTC

4 files · 1 AI reviewer · no blockers · readiness 93/100 · CI pending · blocked

⏸️ Suggested Action - Manual Review

  • Touches a guarded path — held for manual review

Review summary
The change adds a shared transient-cache claim helper, uses it for both the existing maintenance lock and a new per-repo/PR/head/mode AI review lock, and short-circuits lock losers with the established inconclusive AI finding so the gate stays neutral. The core concurrency path is wired in the right place around the expensive AI section and the tests cover same-key contention, release, fail-open behavior, and gate treatment. The main maintainability concern is that the new helper changes the no-claim adapter contract for the existing maintenance lock, but the diff documents and tests that behavior explicitly.

Nits — 6 non-blocking
  • nit: src/queue/processors.ts:2445 adds a very long helper comment with incident-style detail that would be easier to maintain if the race explanation stayed concise and the adapter contract lived near the cache interface.
  • nit: test/unit/queue.test.ts:2686 validates lock contention by manually pre-claiming the lock, but it does not drive two real `runAiReviewForAdvisory` callers concurrently, so it only partially proves the webhook-vs-sweep race shape.
  • nit: src/queue/processors.ts:2519 uses `mode: string` for the lock key even though callers pass repository review modes; tightening this to the existing mode type would prevent accidental key drift.
  • src/queue/processors.ts:2519: type `mode` as the repository AI review mode type used by `args.settings.aiReviewMode`, or add a local alias if the exported type is awkward to import here.
  • test/unit/ai-review-advisory.test.ts:524: add one production-path race test that starts two `runAiReviewForAdvisory` calls for the same head with a delayed AI mock and asserts only one mock invocation succeeds.
  • Touches a guarded path — held for manual review — A maintainer must review and merge this change.
Signal Result Evidence
Code review ✅ No blockers 1 reviewer
Linked issue ✅ No-issue rationale PR body explains why no issue is linked.
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (size label size:L; no linked issue context).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 65 registered-repo PR(s), 55 merged, 572 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 65 PR(s), 572 issue(s).
Gate result ⚠️ Not blocking Advisory; not blocking this PR.
Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository registration is not available in the local Gittensory cache.
  • Public profile languages: not available
  • Official Gittensor activity: 65 PR(s), 572 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Triage stale or unlinked PRs.
  • No action.
  • Link the issue being solved, or explicitly explain why this is a no-issue PR.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.

  • Re-run Gittensory review

@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.88%. Comparing base (22ec2e4) to head (5d642cc).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2429      +/-   ##
==========================================
- Coverage   95.88%   95.88%   -0.01%     
==========================================
  Files         224      224              
  Lines       25158    25165       +7     
  Branches     9150     9150              
==========================================
+ Hits        24124    24130       +6     
- Misses        421      422       +1     
  Partials      613      613              
Files with missing lines Coverage Δ
src/queue/processors.ts 91.50% <100.00%> (-0.03%) ⬇️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@loopover-orb loopover-orb Bot added gittensor gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. labels Jul 1, 2026
…/set fallback

claimAiReviewLock's fallback path (used when the cache adapter has no
atomic claim()) was a plain get-then-set pair: two concurrent callers
can both observe an absent key before either writes, and both believe
they claimed the lock -- defeating the dedupe guarantee for exactly
the double-LLM-call race this lock exists to prevent. The same latent
gap already existed in the sibling claimAgentMaintenanceLock fallback.

Extract a shared claimTransientLock helper used by both locks. Its
fallback now writes a token unique to the attempt, then reads the key
back -- since a correctly-behaved key-value store serializes writes to
a single key, only the caller whose token survives the final read
actually won; every other concurrent caller reads a different (later)
token and correctly backs off. Still fails open on a missing cache or
any read/write error, matching the existing defense-in-depth design.
@dosubot dosubot Bot added size:L and removed size:M labels Jul 1, 2026
JSONbored added 2 commits July 1, 2026 16:30
… without atomic claim()

The prior fallback (write a unique token, then re-read to verify) does
not close the race: caller A can write its token, read it back, and
return true entirely before caller B's later write/read also returns
true -- both callers can still "win" under real concurrent
interleavings, which defeats the whole point of the lock.

There is no way to build real mutual exclusion out of separate
get/set calls without an atomic primitive. Rather than keep pretending
to serialize, claimTransientLock now requires the cache adapter's
native claim() for any exclusivity at all; without it, every caller
proceeds (fail open), matching this lock family's existing
defense-in-depth philosophy. Self-host's Redis-backed cache always
implements claim(), so this is a documented limitation for a
hypothetical future adapter, not a live production gap.
@JSONbored
JSONbored merged commit c31de60 into main Jul 1, 2026
12 checks passed
@JSONbored
JSONbored deleted the fix/ai-review-cache-merge-recheck branch July 1, 2026 23:50
@github-project-automation github-project-automation Bot moved this from Todo to Done in gittensory - v1 roadmap Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier.

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant