Skip to content

perf(server): the Pull Requests page no longer burns GitHub's GraphQL budget while agents run - #20093

Open
juliusmarminge wants to merge 2 commits into
mainfrom
t3code/pr-list-quota
Open

juliusmarminge wants to merge 2 commits into
mainfrom
t3code/pr-list-quota

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

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:

  • Silent repositories were read one by one on almost every refresh. When a search returns nothing for a repository, the listing can't tell whether it has no matching pull requests or GitHub's search index skips it. It settled that by listing the repository on its own (4 points). It only remembered the answer when the search returned a row. So a repository with no open pull requests, or none authored by or requested from you, was re-listed on every read, and so was every repository whenever the page was truncated. That was 7 extra listing reads per list request in the trace.
  • Every finished turn re-read every listing. refreshAfterTurn moved 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 PullRequestSearchCoverage operation asks for a repository's search count (first: 0) and its pull request total. GraphQLResolver.aliased puts 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:

    • a full first page asks about its silent repositories in one coverage call, believes the covered one, still lists the uncovered one on its own, and asks about neither again on the next listing;
    • a burst of turns in three projects re-reads the listing once at once and once at the end of the window;
    • a listing whose reader is cancelled mid-read keeps running, and the next reader gets its answer with one host call, not two;
    • a turn's own project listing reads fresh straight away while another project's waits for the window.

    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 and generate --check clean. apps/server typechecks, and the pullRequest, 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

… 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>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Oct 16, 2026
@juliusmarminge juliusmarminge added the macroscope-review Opt PRs made by unvouched contributors in for Macroscope review. Vouched contributors auto-reviews label Oct 16, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 20.9 KiB 20.9 KiB 0 B (0.0%) 29.3 KiB ✅
Codex Live turn messages 2 2 0 (0.0%) 8 ✅
Claude Total thread wire 5.0 KiB 5.0 KiB −24 B (−0.5%) 6.8 KiB ✅
Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.2 KiB 1.2 KiB −24 B (−2.0%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 21.2 KiB 21.2 KiB −41 B (−0.2%) 29.3 KiB ✅
Claude Live turn messages 2 1 −1 (−50.0%) 8 ✅

Baseline: 4e85a93 · PR result: a34ad18 · Source CI: success

Scenario and decoded snapshot size

10 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.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Path: .coderabbit.config.ts
  • Review profile: CHILL
  • Plan: Team
  • Run ID: 86ad8936-8b73-40f7-83f2-dbd579248d76
📥 Commits

Reviewing files that changed from the base of the PR and between abb5d66 and a34ad18.

📒 Files selected for processing: 4
  • apps/server/src/pullRequest/PullRequestService.test.ts
  • apps/server/src/pullRequest/PullRequestService.ts
  • packages/source-control-github/src/server/GitHubPullRequestApi.test.ts
  • packages/source-control-github/src/server/GitHubPullRequestApi.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.

📝 Walkthrough

The listing service now checks whether GitHub search covers repositories omitted from search results. It reads repositories individually when coverage is not confirmed. Project reads invalidate on each turn, while global listing refreshes are limited to once per cache window, with a trailing refresh.

Changes: Pull-request listings

Layer / File(s) Summary
Search coverage contract and GraphQL query
packages/source-control-core/src/server/PullRequestProvider.ts, packages/source-control-github/src/server/pullRequestListings.graphql, packages/source-control-github/src/server/pullRequestListings.graphql.ts
The provider API adds optional searchCoverage. The GraphQL query and response types expose search issue counts and repository pull-request totals.
GitHub coverage lookup and provider forwarding
packages/source-control-github/src/server/GitHubPullRequestApi.ts, packages/source-control-github/src/server/GitHubPullRequestProvider.ts, packages/source-control-github/src/server/GitHubPullRequestApi.test.ts
The GitHub API batches coverage queries and reports whether each repository is covered. The provider forwards the operation. Tests cover covered, uncovered, missing, and forbidden repositories.
Listing fallback for unconfirmed coverage
apps/server/src/pullRequest/PullRequestService.ts, apps/server/src/pullRequest/PullRequestService.test.ts
The listing service checks eligible silent repositories, records coverage results, and individually reads repositories without confirmed coverage. Tests verify coverage caching and fallback reads.
Listing cache and turn-triggered refresh
apps/server/src/pullRequest/PullRequestService.ts, apps/server/src/pullRequest/PullRequestService.test.ts
Listing cache keys include project epochs, and shared reads continue after caller interruption. Each turn invalidates project reads. Global listing refreshes are limited to once per cache window, with a trailing refresh. Tests cover cancellation and refresh behavior.

Priority: ⬆️ High

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant PullRequestService
  participant GitHubPullRequestProvider
  participant GitHubPullRequestApi
  participant GitHubGraphQL
  PullRequestService->>GitHubPullRequestProvider: searchCoverage(repositories)
  GitHubPullRequestProvider->>GitHubPullRequestApi: forward searchCoverage request
  GitHubPullRequestApi->>GitHubGraphQL: send batched coverage query
  GitHubGraphQL-->>GitHubPullRequestApi: return issueCount and pull-request totalCount
  GitHubPullRequestApi-->>GitHubPullRequestProvider: return per-repository coverage
  GitHubPullRequestProvider-->>PullRequestService: return coverage results
  PullRequestService->>GitHubPullRequestProvider: listChangeRequests for repositories without confirmed coverage
Loading

Suggested reviewers: bil0000

Merge Risk: ⚪ Minimal · up to a34ad

No identified issue blocks merging after normal checks.

Pre-merge checks: Passed 4
Check name Status Explanation
Linked Issues check Passed Issue [#19363] requires lower Pull Requests listing cost and fewer repeated reads after agent turns. PullRequestService batches silent repositories through optional searchCoverage, stores covered …
Out of Scope Changes check Passed The changes stay within issue [#19363]. The provider API addition, GitHub GraphQL operation, generated client updates, service changes, adapter wiring, and tests directly support search coverage, refr…
Title check Passed The title clearly and concisely describes the main performance change: reducing GitHub GraphQL usage while agents run.
Description check Passed The description covers the problem, proposed fix, scope through issue #19363, detailed verification, test results, limitations, and agent attribution. The “Fix” heading serves the template’s “Change” …
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

- 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>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

macroscope-review Opt PRs made by unvouched contributors in for Macroscope review. Vouched contributors auto-reviews size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pull Requests page spends ~170 GraphQL points per refresh and re-lists after every agent turn, exhausting the quota in minutes

1 participant