[Bug]: PR sync reads pull requests GitHub cannot find on every sweep, even on settled threads#17799

opened24d agobyAabiassi

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. Link a pull request URL for which GitHub already returns NOT_FOUND when using the server's credential. The repository can be deleted, or private and inaccessible to the server's GitHub credential. My links point to private repositories that my credential cannot access. The link never gets a snapshot.
  2. Settle the thread.
  3. Leave the server running. No client needs to be open.
  4. Count the getPullRequestSummary spans under PullRequestSyncReactor.sweep in server.trace.ndjson.

The access failure itself may be expected. The bug is that the sync retries the failed read every minute indefinitely, even on settled threads.

Expected behavior

Settled threads do not trigger periodic reads of their linked pull requests. The user guide says "A settled thread's reviews stop refreshing until you unsettle it." On unsettled threads, the sync retries pull requests that GitHub cannot find every 15 minutes, like closed pull requests.

Actual behavior

The sync reads each of these pull requests on every one-minute sweep indefinitely, including those on settled threads. On my machine, 9 such links, all on settled threads, made 10 failed GraphQL requests a minute: one failed batch and 9 single reads. In my current trace files, 52 sweeps made 10 failed reads each. All 520 reads failed. These failed reads accounted for 520 of the 538 GraphQL requests the server sent during that time. My earlier measurement, with 38 such links across about 150 threads, counted 1,240 failed reads in 36 minutes. The account ran out of GraphQL quota, and other gh commands that use GraphQL failed until the reset.

Three paths in apps/server/src/orchestration-v2/PullRequestSyncReactor.ts keep these links due:

  1. isDue returns true for any link without a snapshot before it filters out settled threads. #16762 left this check above the settled filter.
  2. syncGroup clears a requested key only after a successful read. After a not-found read, the key stays requested. Every later sweep reads the pull request again, even if the thread is settled.
  3. Each thread.pull-request-synced event requests every link on the thread that has no snapshot. A sync of any other link on that thread requests the unreadable ones again.

The GitHub batch fallback adds to the cost. One NOT_FOUND alias causes the whole batched summary query to fail. GitHubPullRequestApi then reads each pull request in that batch separately, including readable ones.

I have a fix that changes only the reactor and includes regression tests. It applies the no-snapshot rule only to unsettled threads. The fix treats a not-found answer as a finished read and clears the request. On unsettled threads, periodic sweeps retry the pull request every 15 minutes. Another link's sync no longer requests the missing pull request again. A reader's refresh still does. A PR with this fix follows this issue.

The reactor's polling logic is the same on main at c77a7b7. A dev server on main with a copy of my data makes the same 10 failed requests per sweep.

Related:

  • #16762 stopped periodic reads for settled threads but kept the no-snapshot check first.
  • A comment on #12060 reports that links without a snapshot are "due on every sweep". The triage on that issue fixes how unknown Git hosts are probed. It does not change this sweep.
  • #13496 and #11220 cover other polling paths.
  • #17609 (open) changes the same event handler and adds a request for unsynced sibling links when a link syncs.

Investigated and filed with Claude Code (Claude Opus 5.5).

Impact

Major degradation or frequent failure

Version or commit

0.0.46-nightly.20261008.2833

Environment

macOS 27.0, T3 Code (Nightly) desktop app, GitHub through the app's GitHub API transport.

Logs or stack traces

shell
1# Summary reads under PullRequestSyncReactor.sweep, from server.trace.ndjson
2sweep 10:41 GitHubApiNotFoundError 10
3sweep 10:42 GitHubApiNotFoundError 10
4sweep 10:43 GitHubApiNotFoundError 10
5# 52 of the 53 sweeps from 08:56 to 10:43 UTC made 10 failed reads each: 520 reads, 0 successes.
6
7WARN pull request sync skipped
8 { count: 9, reason: 'Pull request operation summary failed: GitHub could not find the requested resource, or the credential cannot see it.' }

Screenshots, recordings, or supporting files

No response

Workaround

Unlink the unreadable pull requests, or archive their threads. The sweep skips unlinked pull requests and archived threads.

Synced from GitHub

Same issue here on T3 Code (Beta) 0.1.0, macOS, on a much larger scale.

Setup: three github.com accounts logged into gh. Threads had PRs linked from repos owned by the two non-active accounts, which the active credential can't see (also #14123). 112 links across ~25 threads (open, settled and older ones) never got a snapshot.

What I measured (server.trace.ndjson, 30-minute window):

  • 4,489 getPullRequestSummary GraphQL calls, all under PullRequestSyncReactor.sweep โ†’ syncGroup โ†’ PullRequestService.persistedRead โ†’ PullRequestReadCache.get
  • ~180 calls/min (~10,800/h), more than twice the 5,000/h budget, so the quota ran out about 30 minutes after each reset
  • nearly every read failed with GitHub could not find the requested resource, or the credential cannot see it
  • gh and everything else on the same account then hit API rate limit exceeded for GraphQL

Workaround: unlinking the 112 links (via the unlink_pull_request MCP tool, passing threadId) took it from ~185 calls/min to 0โ€“2 calls/min within two minutes.

They come back whenever an agent links a new PR in one of those repos, so it would help to remember NOT_FOUND results per link with a long backoff, as the issue suggests, or to pick the credential per repo owner (#14123).

Reproduced on Linux with the web dev server at 70e5e384da. Linked a nonexistent GitHub pull request, received GitHubApiNotFoundError, then settled its thread. The reactor retried at 22:52, 22:53, and 22:54 UTC; the last attempt occurred after closing the browser.

Runtime transcript. Regression tests also reproduce the settled-thread retries, missing-result backoff, and sibling re-request paths.

Synced from GitHub

Same on Bitbucket Cloud (0.1.6-nightly.20261030.3917, Linux).

I had 12 linked Bitbucket pull requests, 11 of them on settled threads, and none had a snapshot. Every one-minute sweep reread all 12: isDue returns true for a missing snapshot before it checks whether the thread is settled. The reads failed with 404 first, then 429.

Each Bitbucket read is 5 requests (#27578), so that came to about 3,600 requests an hour, against Bitbucket's limit of 1,000. The host stayed paused, and while it was paused I couldn't open pull requests either (#27579). Unlinking the pull requests stopped it.