[Bug]: On github.com, PR sync rewrites every open linked PR twice after each base-branch push, because GitHub briefly reports mergeable UNKNOWN#23860
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
- 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.
- Merge anything into their base branch (
main). - Count
thread.pull-request-link-updatedrows inorchestration_eventsfor 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:
- GitHub marks open PRs' mergeability stale on a base push and answers
mergeable: UNKNOWNuntil it recomputes.toMergeabilitymaps that to"unknown"(gitHubPullRequestJson.ts#L518-L527). snapshotFieldsEqualcomparesmergeabilitystrictly (PullRequestSyncReactor.ts#L104), soconflicting → unknowncounts as a change.syncEntrydispatchesthread.pull-request-link.sync(#L267-L315).- On the next quiet read, about 5 min later (#L217), GitHub has finished,
unknown → conflicting(ormergeable) counts as a change again, and everything is rewritten back. - 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 arestate: 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 themunknown → conflicting|mergeable. The other 2 had a realupdatedAtchange. None differed insyncedAtalone. - One thread's history for a PR last updated 2026-10-05: 12 events from 17:22Z to 21:15Z, alternating
unknown/conflicting, withupdatedAtunchanged. Eachunknownfollows a merge intomainby seconds (merges at 17:58:33–53Z →unknownsynced 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):
| 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
| 1 | # newest 5,000 rows of orchestration_events, thread.pull-request-link-updated only, per minute (UTC) |
| 2 | 21:13 53 events / 19 PRs |
| 3 | 21:14 30 / 1 |
| 4 | 21:15 784 / 35 <- the read after the 21:09 base merges, writing the real mergeability back |
| 5 | 21:19 47 / 17 |
Suggested fix
- Treat an incoming
mergeability: "unknown"as "no information" when the stored snapshot has a known value: keep the stored value, or leavemergeabilityout of the comparison for that read. Providers that always reportunknown(Forgejo, Bitbucket, gitcafe) are unaffected, sinceunknown === unknown. - Optional:
needsStackuses the same comparison (#L340-L349), so a mergeability flap can also cost a stack read whereknownUnstackedis 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.