Repository navigation
perf(server): the Pull Requests page no longer burns GitHub's GraphQL budget while agents run - #20093
juliusmarminge wants to merge 2 commits into
Conversation
… budget while agents run A repository a listing's search returned nothing for was read on its own on almost every refresh, at four points each, and every finished turn moved the global listings epoch, so an open page re-read everything after every turn in any project. The silent repositories are now checked with one aliased search-coverage request (one point for all of them), and a covered answer is remembered like a returned row. Turns move the listings at most once per 30-second list cache window; the project's own pull requests still refresh at once. Fixes #19363 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ApprovabilityVerdict: Needs Human Approval Macroscope's review found this PR requires human approval — This changes production pull-request listing behavior, cache lifetime and refresh timing, and GitHub GraphQL request patterns across multiple layers. It also conditionally suppresses per-repository fallback reads based on a new search-coverage decision, making the runtime impact broad enough for human review. You can add or adjust custom eligibility rules. Learn more. |
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
- A listing keeps running in the service's scope when its reader goes away, so a page that refreshes mid-read joins it instead of paying GitHub for the same reads again. - A listing narrowed to projects keys by their project epochs too, so a turn's own project reads fresh at once while the turn throttle holds the rest. - One refused repository (SAML, disabled) in a coverage batch is reported as not covered rather than failing the batch and sending every silent repository back to its own read. - A repository search does not cover is remembered as such, so it is read on its own without being asked about again first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
With the Pull Requests page open and a few agents running, the page spent about 450 GraphQL points a minute and ran out the 5,000-point budget in about 10 minutes (#19363). There were two causes:
refreshAfterTurnmoved the global listings epoch, so a turn in any project made every listing miss the 30-second cache. It also told every open page to read again.Fix
Ask GitHub once whether search covers the silent repositories. The new
PullRequestSearchCoverageoperation asks for a repository's search count (first: 0) and its pull request total.GraphQLResolver.aliasedputs every silent repository in one request, which GitHub charges 1 point for. Before, each one was a 4-point listing read. A repository counts as covered when it has no pull requests, or when search finds any of them. Its silence is then believed and remembered for 10 minutes, the same way a returned row is. A repository GitHub doesn't cover, or one the check can't read, is still listed on its own as before. This applies whether or not the page is truncated, so a full first page no longer lists every repository.Turns move the listings at most once per cache window. The first turn refreshes the listings at once. Turns within the next 30 seconds share one refresh at the end of the window, so a listing is never staler than the list cache already allows. A turn still refreshes its own project's pull requests at once, including listings narrowed to that project, such as the composer's pull request picker. Explicit refreshes, mutations and actions are unchanged.
A listing that has started finishes, even if its reader goes away. The page cancelled a listing still in progress whenever it refreshed, and the replacement paid GitHub for the same reads again: 43 cancelled list reads in the issue's trace. The listing now runs in the service's scope, and the next reader with the same question joins it.
Coverage failures are per repository, and both answers are remembered. A repository GitHub refuses (a SAML-gated organisation, a disabled repository) counts as not covered, and the other repositories in the request still get their answers. A repository search doesn't cover is remembered for 10 minutes too, so it is listed on its own without being asked about again first.
The coverage check only exists for GitHub. Hosts without it keep the per-repository read.
Builds on the generated GraphQL client and aliased resolvers from #19224.
Verification
PullRequestService.test.ts: 207 pass. New tests:I broke each fix and confirmed its test fails.
GitHubPullRequestApi.test.ts: a coverage check for five repositories (covered, empty, skipped by search, missing, refused with FORBIDDEN) is one request, and each answer is right.packages/source-control-github: 367 tests pass, typecheck andgenerate --checkclean.apps/servertypechecks, and thepullRequest, run-finalization and push-refresh tests pass (262).Against github.com: the coverage query for three repositories, one missing, cost 1 point, and the missing one failed alone with
NOT_FOUND. A 100-row listing read costs 4 points (rateLimit(dryRun: true)).Adversarial reviews by GPT-6.1 Sol and Claude Fable 5.1 found no correctness bugs. Their four findings are fixed here: the cancelled reads, the batch-wide coverage failure, the turn's own project going stale, and asking about uncovered repositories again.
Fixes #19363
Claude Opus 5.5 in Claude Code (T3 Code)
🤖 Generated with Claude Code