feat(review-quill): parallel review execution, no global lock - #501
Merged
Conversation
Removes review-quill's single-worker reconciliation lock and runs reviews as independent agents — the same shape patchrelay uses for implementation runs. Discovery (the cheap part — GitHub list calls, DB lookups, eligibility math) runs serially per pass, but dispatch of executions (the expensive part — Codex turn on a tmp worktree) fans out under a soft cap. Why --- The previous architecture serialized all work through a global `reconcileInProgress` flag. A hot repo whose webhooks fired continuously held that flag indefinitely and starved every other watched repo. Observed in production: 3 doc PRs opened simultaneously across 3 repos — only the repo whose reconcile happened to be in-flight got reviewed. The other two sat unattended for an hour. Worktrees were already isolated (mkdtemp per call in materialize.ts), Codex's app-server protocol is multi-thread by design (startThread → startTurn), and the DB has a UNIQUE(repo_full_name, pr_number, head_sha) constraint that provides cross-restart dedup. The lock wasn't protecting anything real — it was just an accidental ceiling. What changed ------------ src/service.ts: - Drop reconcileInProgress / pendingFullReconcile / pendingRepoReconciles / runQueuedReconcile / prependQueuedReconciles / queueReconcileRequest. Gone. - Replace with discoverRepo() (read-only walk + dispatch) and dispatchReview() (fire-and-forget worker under a semaphore). - New `inFlightReviews: Map<key, Promise>` for in-memory dedup on (repo, pr, headSha). DB UNIQUE constraint covers cross-restart. - New semaphore (acquireReviewSlot / releaseReviewSlot) bounds total parallel executions at `maxConcurrentReviews` (default 20). - reconcileAll now runs all repos' discovery in parallel via Promise.all — discovery itself is read-only and cheap. src/types.ts: - Add `reconciliation.maxConcurrentReviews?: number` (default 20). - ReviewQuillRuntimeStatus: drop `reconcileInProgress`, add `inFlightReviews: number` and `repoLastReconciledAt: Record<...>` for operator visibility into per-repo discovery and the live worker count. Tests ----- Replace the prior fairness/prepend test (which described semantics that no longer exist) with four parallelism tests: - Discovery passes run in parallel; neither repo blocks the other. - dispatchReview dedupes identical (repo, pr, head) calls. - The semaphore actually caps in-flight executions at the configured number — verified with cap=2, dispatch 5, observe peak=2. - End-to-end: triggerReconcile fans out two reviews as independent workers that start before either completes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Removes review-quill's single-worker reconciliation lock and runs reviews as independent agents — the same shape patchrelay uses for implementation runs. Discovery (cheap: GitHub list calls, DB lookups, eligibility math) runs in parallel across repos; dispatch of executions (expensive: Codex turn on a tmp worktree) fans out under a soft cap.
Why
The previous architecture serialized all work through a global `reconcileInProgress` flag. A hot repo whose webhooks fired continuously held that flag indefinitely and starved every other watched repo. Observed in production: 3 doc PRs opened simultaneously across 3 repos — only the repo whose reconcile happened to be in-flight got reviewed. The other two sat unattended for an hour.
Worktrees were already isolated (`mkdtemp` per call in `materialize.ts`), Codex's app-server protocol is multi-thread by design (`startThread` → `startTurn`), and the DB has a `UNIQUE(repo_full_name, pr_number, head_sha)` constraint that provides cross-restart dedup. The lock wasn't protecting anything real — it was an accidental architectural ceiling.
The right model is the one patchrelay already uses: react to external events, run independent agents in parallel, soft cap on total concurrency.
What changed
`packages/review-quill/src/service.ts`:
`packages/review-quill/src/types.ts`:
Architectural parity with patchrelay
Both services now follow the same pattern:
|-|-|-|
Both can run dozens of independent agents on the same host. Bumping concurrency further is a config change.
Tests
Replaced the prior fairness/prepend test (which described semantics that no longer exist) with four parallelism tests in `service.test.ts`:
162/162 review-quill tests pass. Repo-wide typecheck clean. Lint shows only pre-existing warnings unrelated to the change.
Migration
`ReviewQuillRuntimeStatus` is internal — no external consumers. The dashboard test fixture is updated; no public API change.