[Bug]: On github.com, PR sync rewrites every open linked PR twice after each base-branch push, because GitHub briefly reports mergeable UNKNOWN#23860

opened1d agobyWWhiteWarrior625

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 open github.com pull requests to many threads (we have 683 threads linking 61 open PRs in one repository, up to 268 threads per PR). Leave some of the PRs untouched for days, with merge conflicts or not.
  2. Merge anything into their base branch (main).
  3. Count thread.pull-request-link-updated rows in orchestration_events for the next 10 minutes, and diff each thread's consecutive payloads for the same PR.

Expected behavior

A push to the base branch writes nothing for PRs whose snapshot did not change. GitHub's mergeable: UNKNOWN means "still being calculated", so it should not replace a known value.

Actual behavior

After every push to the base branch, every linked thread gets two new link events for every open PR on that base, with nothing else about the PR changed:

  1. GitHub marks open PRs' mergeability stale on a base push and answers mergeable: UNKNOWN until it recomputes. toMergeability maps that to "unknown" (gitHubPullRequestJson.ts#L518-L527).
  2. snapshotFieldsEqual compares mergeability strictly (PullRequestSyncReactor.ts#L104), so conflicting → unknown counts as a change. syncEntry dispatches thread.pull-request-link.sync (#L267-L315).
  3. On the next quiet read, about 5 min later (#L217), GitHub has finished, unknown → conflicting (or mergeable) counts as a change again, and everything is rewritten back.
  4. The dispatch happens once per linked thread (#L413-L426), so one PR linked from 268 threads costs 268 events each way.

This is the github.com counterpart of #20273. #20352 fixed the ghe.com cause (tokenized avatar URLs). github.com avatar URLs are stable (?v=4), so that fix doesn't apply here. The cause on github.com is the transient UNKNOWN. The result is the same: #19248 ("persist only changed links") is defeated for every open PR, once per base push.

Measured on our server (0.1.2-nightly.20261022.3541), read-only, from the newest 5,000 rows of orchestration_events plus indexed per-thread lookups:

  • Newest 5,000 events (21:11:40–21:24:05Z): 946 are thread.pull-request-link-updated, across 683 threads and 61 distinct PRs. All 946 are state: open. One sweep at 21:15Z wrote 784 of them within a minute. One PR, untouched since 2026-10-05, got 268 events (one per linking thread) in that sweep.
  • Base-branch merges landed at 21:09:26Z and 21:09:33Z. The sweep at 21:09:41Z stored unknown, and the one at 21:14–21:15Z stored the real value back.
  • In a random sample of 45 threads, comparing each thread's last two events for the same PR: 43 differ in mergeability (+ syncedAt), all of them unknown → conflicting|mergeable. The other 2 had a real updatedAt change. None differed in syncedAt alone.
  • One thread's history for a PR last updated 2026-10-05: 12 events from 17:22Z to 21:15Z, alternating unknown/conflicting, with updatedAt unchanged. Each unknown follows a merge into main by seconds (merges at 17:58:33–53Z → unknown synced 17:58:58Z; merge at 18:39:17Z → 18:39:55Z; merges at 21:09:26–33Z → 21:09:41Z).
  • An earlier 25-minute window (18:37–19:02Z, which had several base merges): 1,131 link-updated events across 679 threads.

Consecutive payloads for one thread (repository, title, branches and author redacted):

diff
1 {"host":"github.com","repository":"<owner>/<repo>","number":15,"source":"agent","linkedAt":"2026-10-04T00:29:41.413Z",
2 "snapshot":{"state":"open","isDraft":true,"updatedAt":"2026-10-05T21:32:46Z",
3- "syncedAt":"2026-10-22T19:13:41.848Z", ... "mergeability":"conflicting"},
4+ "syncedAt":"2026-10-22T21:09:41.590Z", ... "mergeability":"unknown"},
5 ...
6- "syncedAt":"2026-10-22T21:09:41.590Z", ... "mergeability":"unknown"},
7+ "syncedAt":"2026-10-22T21:14:56.730Z", ... "mergeability":"conflicting"},

On a large database these bursts are hundreds of write transactions within seconds, in the same minute a base merge usually triggers other work.

Impact

Major degradation or frequent failure

Version or commit

0.1.2-nightly.20261022.3541 (044f07257c). Same code on main @ 2c8bdf6177.

Environment

Linux desktop, github.com via gh, one busy private repository with frequent merges to main, 41 GB statev2.sqlite.

Logs or stack traces

shell
1# newest 5,000 rows of orchestration_events, thread.pull-request-link-updated only, per minute (UTC)
221:13 53 events / 19 PRs
321:14 30 / 1
421:15 784 / 35 <- the read after the 21:09 base merges, writing the real mergeability back
521:19 47 / 17

Suggested fix

  1. Treat an incoming mergeability: "unknown" as "no information" when the stored snapshot has a known value: keep the stored value, or leave mergeability out of the comparison for that read. Providers that always report unknown (Forgejo, Bitbucket, gitcafe) are unaffected, since unknown === unknown.
  2. Optional: needsStack uses the same comparison (#L340-L349), so a mergeability flap can also cost a stack read where knownUnstacked is false.

Workaround

Settle or unlink threads that link long-idle PRs. Each such link stops costing once every thread that links it is settled.

Related

  • #20273 / #20352: the same symptom on ghe.com, caused by avatar tokens.
  • #19248: persist only changed pull request links.
  • #22073: quiet PRs read every five minutes. That sets the cadence of the flip back.
  • #16558: link snapshot updates and event-log size.