Context
src/github/backfill.ts's fetchLiveReviewThreadBlockers (~line 3841) is the live GraphQL read that
computes unresolved review-thread merge blockers — consumed directly by the merge/close decision path
(src/queue/processors.ts, src/services/agent-approval-queue.ts,
src/services/agent-action-executor.ts).
Its pagination loop (for (;;) { ... }, ~3854-3899) walks reviewThreads(first: 50, after: $cursor)
using GraphQL cursor pagination, and correctly requests pageInfo { hasNextPage endCursor }. However,
unlike every other multi-page pagination loop in src/github/** — PR_DETAIL_MAX_PAGES in this same
file, MAX_WORKFLOW_RUN_LIST_PAGES in app.ts, REVIEW_PAGE_LIMIT/ISSUE_EVENTS_RECENT_PAGE_LIMIT
in pr-actions.ts, COMMENT_SEARCH_PAGE_LIMIT in comments.ts, MAX_REPO_PAGES in public.ts —
this loop has no maximum-page bound. It only stops when GitHub reports no more pages, returns no
nodes, or (defensively) repeats a cursor it has already seen. A PR that accumulates a very large
number of review threads can drive this function through an unbounded number of sequential GraphQL
calls every time merge-readiness is evaluated for that PR.
The same function's inner comments(first: 20) connection (per review thread) also has no pageInfo
and is not paginated, so a thread with more than 20 comments only sees the first 20.
Requirements
- Add an explicit page-cap constant for
fetchLiveReviewThreadBlockers's outer reviewThreads loop,
following this codebase's existing naming convention (e.g. REVIEW_THREAD_MAX_PAGES), and stop the
loop once that cap is reached — mirroring MAX_WORKFLOW_RUN_LIST_PAGES's exact rationale ("bounded
so a pathological repo/PR can't turn one call into an unbounded fetch loop").
- When the cap is hit, return whatever blockers were derived from the threads collected so far (same
fail-open philosophy already documented at the top of this function — "if GraphQL is unavailable
this fails open to [] rather than guessing") rather than throwing.
- Paginate the inner
comments(first: 20) connection per thread the same way, OR (lower-cost
alternative, acceptable if scoping the full nested pagination is judged disproportionate to the
risk) explicitly document in a code comment why 20 is treated as sufficient for blocker detection —
do not leave it silently unbounded-but-uncommented as it is today.
- Add a regression test that simulates at least 3 pages of
reviewThreads (via mocked GraphQL
responses) and confirms the loop terminates at the new cap without exhausting it in the common case,
plus a test that a >20-comment thread's blocker classification is not broken by the change made (if
the inner pagination is implemented) or remains covered by an explicit comment (if not).
Deliverables
Test Coverage Requirements
This repo's Codecov patch gate is 99%+ branch-counted on every changed line/branch in src/**. The
new page-cap branch and the multi-page collection path both need explicit coverage — extend the
existing fetchLiveReviewThreadBlockers describe block in test/unit/backfill-2.test.ts (currently
single-page fixtures only) rather than adding a new file.
Expected Outcome
fetchLiveReviewThreadBlockers can no longer be driven into an unbounded number of GitHub GraphQL
calls by a PR with a pathologically large number of review threads, matching the bounded-pagination
convention every other list-fetch in src/github/** already follows — and the loop's multi-page
branch is actually proven correct by a test, not just present in the code.
Links & Resources
src/github/backfill.ts: fetchLiveReviewThreadBlockers (~3841), its unbounded for (;;) loop
(~3854-3899).
- Precedent constants:
MAX_WORKFLOW_RUN_LIST_PAGES (src/github/app.ts), PR_DETAIL_MAX_PAGES
(src/github/backfill.ts), REVIEW_PAGE_LIMIT/ISSUE_EVENTS_RECENT_PAGE_LIMIT
(src/github/pr-actions.ts), COMMENT_SEARCH_PAGE_LIMIT (src/github/comments.ts),
MAX_REPO_PAGES (src/github/public.ts).
test/unit/backfill-2.test.ts's fetchLiveReviewThreadBlockers describe block (~line 1419).
Context
src/github/backfill.ts'sfetchLiveReviewThreadBlockers(~line 3841) is the live GraphQL read thatcomputes unresolved review-thread merge blockers — consumed directly by the merge/close decision path
(
src/queue/processors.ts,src/services/agent-approval-queue.ts,src/services/agent-action-executor.ts).Its pagination loop (
for (;;) { ... }, ~3854-3899) walksreviewThreads(first: 50, after: $cursor)using GraphQL cursor pagination, and correctly requests
pageInfo { hasNextPage endCursor }. However,unlike every other multi-page pagination loop in
src/github/**—PR_DETAIL_MAX_PAGESin this samefile,
MAX_WORKFLOW_RUN_LIST_PAGESinapp.ts,REVIEW_PAGE_LIMIT/ISSUE_EVENTS_RECENT_PAGE_LIMITin
pr-actions.ts,COMMENT_SEARCH_PAGE_LIMITincomments.ts,MAX_REPO_PAGESinpublic.ts—this loop has no maximum-page bound. It only stops when GitHub reports no more pages, returns no
nodes, or (defensively) repeats a cursor it has already seen. A PR that accumulates a very large
number of review threads can drive this function through an unbounded number of sequential GraphQL
calls every time merge-readiness is evaluated for that PR.
The same function's inner
comments(first: 20)connection (per review thread) also has nopageInfoand is not paginated, so a thread with more than 20 comments only sees the first 20.
Requirements
fetchLiveReviewThreadBlockers's outerreviewThreadsloop,following this codebase's existing naming convention (e.g.
REVIEW_THREAD_MAX_PAGES), and stop theloop once that cap is reached — mirroring
MAX_WORKFLOW_RUN_LIST_PAGES's exact rationale ("boundedso a pathological repo/PR can't turn one call into an unbounded fetch loop").
fail-open philosophy already documented at the top of this function — "if GraphQL is unavailable
this fails open to
[]rather than guessing") rather than throwing.comments(first: 20)connection per thread the same way, OR (lower-costalternative, acceptable if scoping the full nested pagination is judged disproportionate to the
risk) explicitly document in a code comment why 20 is treated as sufficient for blocker detection —
do not leave it silently unbounded-but-uncommented as it is today.
reviewThreads(via mocked GraphQLresponses) and confirms the loop terminates at the new cap without exhausting it in the common case,
plus a test that a >20-comment thread's blocker classification is not broken by the change made (if
the inner pagination is implemented) or remains covered by an explicit comment (if not).
Deliverables
fetchLiveReviewThreadBlockers's outer pagination loop is bounded by an explicit, named page-capconstant.
comments(first: 20)connection is either paginated or has an explicit code commentjustifying the fixed cap.
hasNextPage: trueacross multiple simulated pages, proving bothcorrect collection across pages and correct termination at the cap.
Test Coverage Requirements
This repo's Codecov patch gate is 99%+ branch-counted on every changed line/branch in
src/**. Thenew page-cap branch and the multi-page collection path both need explicit coverage — extend the
existing
fetchLiveReviewThreadBlockersdescribe block intest/unit/backfill-2.test.ts(currentlysingle-page fixtures only) rather than adding a new file.
Expected Outcome
fetchLiveReviewThreadBlockerscan no longer be driven into an unbounded number of GitHub GraphQLcalls by a PR with a pathologically large number of review threads, matching the bounded-pagination
convention every other list-fetch in
src/github/**already follows — and the loop's multi-pagebranch is actually proven correct by a test, not just present in the code.
Links & Resources
src/github/backfill.ts:fetchLiveReviewThreadBlockers(~3841), its unboundedfor (;;)loop(~3854-3899).
MAX_WORKFLOW_RUN_LIST_PAGES(src/github/app.ts),PR_DETAIL_MAX_PAGES(
src/github/backfill.ts),REVIEW_PAGE_LIMIT/ISSUE_EVENTS_RECENT_PAGE_LIMIT(
src/github/pr-actions.ts),COMMENT_SEARCH_PAGE_LIMIT(src/github/comments.ts),MAX_REPO_PAGES(src/github/public.ts).test/unit/backfill-2.test.ts'sfetchLiveReviewThreadBlockersdescribe block (~line 1419).