Repository navigation
Unrecognised GitHub Enterprise host re-probes fj, tea and glab once per project on every sweep #12060
Description
What happened
Threads open slowly and the VCS/PR state in the sidebar takes 10–18s to settle. Not tied to a
particular update — it has always behaved this way on this machine. The desktop app intermittently
shows "Reconnecting" whenever anything else on the box uses CPU.
Every repository I work on is hosted on a GitHub Enterprise Cloud tenant with a data-residency
hostname of the form <tenant>.ghe.com. gh is authenticated against that host and works fine
from the terminal.
Diagnosis
A GHE host is classified unknown, can never be refined, and the failed refinement is retried on
every read without ever being cached. Verified against main @ ccf220b.
1. <tenant>.ghe.com is never detected as GitHub.
packages/shared/src/sourceControl.ts:183 matches GitHub as host === "github.com" or a github
DNS label. <tenant>.ghe.com has neither, so detectSourceControlProviderFromRemoteUrl returns
kind: "unknown".
2. Nothing can refine it. Of the five discovery specs, only GitLab
(GitLabSourceControlProvider.ts:107) and Forgejo (managed-cli) register a refineUnknownRemote.
The GitHub spec registers none. So there is no code path by which a GHE host can ever resolve to
github, no matter how many times it is probed.
3. The refinement runs uncached on every read. SourceControlProviderRegistry.ts:261 only
consults providerContextCache when context is undefined. Both hot callers pass context
explicitly, so both bypass it entirely:
PullRequestService.ts:615(refineUnknownProjectKinds) — no caching whatsoeverserver.ts:307(identity resolverrefine) — 1 min TTL
Negative results are discarded rather than cached: ForgejoSourceControlProvider.ts:150
(orElseSucceed(() => [])), SourceControlProviderDiscovery.ts:328 (orElseSucceed(() => null)),
and the registry cache uses Duration.zero on failure (SourceControlProviderRegistry.ts:258).
4. firstSuccessOf multiplies it by the number of projects on the host.
refineUnknownProjectKinds groups candidates by baseUrl and runs Effect.firstSuccessOf over
them. Every project on the same GHE host lands in one group, and because none can ever succeed, the
entire list is exhausted on every call. 11 of my 14 projects share one host.
Each candidate spawns fj, tea and glab. None are installed — these are pure failed spawns.
Measured: one PullRequestService.canonicalRef = 12 refinements → 24 listLogins → 20.8s.
PullRequestSyncReactor runs syncGroup at concurrency: 8, every 60s.
5. WSL2 makes each failed spawn ~100x more expensive. This is why it is so much worse here than
it would be on native Linux. The server's PATH contains 36 /mnt/c/... entries, so every ENOENT
lookup is a scan across the 9p filesystem. Measured on this machine, 20 spawns of a missing binary:
3.05s (152ms each) versus 0.043s (2.1ms each) with a stub resolving first on PATH.
The result is a sweep that cannot keep up with its own 60s schedule, and a server with no CPU
headroom — so any unrelated load (a test run) pushes the health endpoint past the desktop app's 10s
timeout and triggers "Reconnecting".
Steps to reproduce
- Have
ghauthenticated against a GitHub Enterprise host whose hostname contains nogithubDNS
label — e.g.<tenant>.ghe.com, or any GHES vanity domain. - Add several projects (I have 11) whose
originremote points at that host. - Do not install
fj,teaorglab. - Open the app and watch
server.trace.ndjson.
Every PullRequestSyncReactor sweep re-probes all three CLIs once per project, forever. The effect
scales with the number of projects sharing the host, and is far more visible on WSL2 than on native
Linux or macOS because of the PATH cost above.
Version
0.0.41-nightly.20260916.1795 (t3 triage context reports installed version 0.0.42)
Environment
WSL2 (Ubuntu, kernel 6.6.87.2-microsoft-standard-WSL2) on Windows, x64, 16 cores.
Node v26.8.2. Desktop app against a local server. gh 2.x authenticated to both github.com and
the enterprise host. fj, tea, glab, az not installed.
Evidence
225s window, 4 rotated trace files, 21,847 spans
n p50 max span
1629 335.0ms 2674.6ms ForgejoCli.listLogins (874 failures, ~50%)
816 1175.8ms 4341.6ms refineUnknownRemoteProvider
815 658.4ms 2718.3ms ForgejoSourceControlProvider.refineUnknownRemote
1762 476.2ms 3641.2ms processRunner.runProcessCore
spawn failures in window: glab 881, tea 873
slowest spans:
119405ms PullRequestSyncReactor.sweep (on a 60s Schedule.spaced)
31316ms ws.rpc.vcs.refreshStatus
30127ms ws.rpc.subscribeVcsStatus
23001ms PullRequestSyncReactor.syncGroup (x8 back-to-back, concurrency: 8)
23001ms PullRequestService.canonicalRef
one canonicalRef subtree (20.8s):
12 refineUnknownRemoteProvider
12 ForgejoSourceControlProvider.refineUnknownRemote
12 ForgejoCli.readKeys
24 ForgejoCli.listLogins
24 processRunner.runProcessCore
glab spawn parentage (796 of 881):
VcsProcess.runUnbounded < VcsProcess.run < refineUnknownRemoteProvider < PullRequestService.canonicalRef
Related issues
#5087 (GHE support, closed; its PR #6052 closed unmerged) and #5089 (still open) would both remove
the root cause. This issue is about the cost of the failure mode itself: even with GHE detection
shipped, any genuinely unrecognised host still re-probes three CLIs per project per sweep forever,
because the negative result is never cached and firstSuccessOf exhausts the whole candidate group.
#11220 is adjacent — idle sweep cadence and caches that expire before the next sweep — but its root
cause is scheduling, not unrefinable provider detection. #5932 is the change that made hostname
matching deliberately strict.
Fix applied or workaround
No fix for the detection itself; the playbook rules out patching the source. As a local mitigation,
shadowed the three never-installed CLIs so the lookups stop scanning the Windows PATH:
for c in tea fj glab; do
printf '#!/bin/sh\nexit 1\n' > ~/.local/bin/$c
chmod +x ~/.local/bin/$c
doneVcsProcess.ts:167 fails on non-zero exit when allowNonZeroExit is unset, so discovery still
reports both providers as missing and refinement still returns unknown — identical behaviour,
cheaper. Measured before/after:
ForgejoCli.listLogins p50 335ms -> 187ms
canonicalRef p50 20.8s -> 10.9s
syncGroup max 23.0s -> 10.9s
Roughly 2x end-to-end — less than the isolated PATH benchmark suggests, because the spans also
include spawn overhead and 8-way contention. It is a mitigation, not a fix: the probe storm is
still there, just cheaper per probe.
Filed by
claude (opus-5) via t3 triage
Activity
juliusmarminge commentedon Sep 16, 2026
Triage
Confirmed on current
main(ccf220be2) and shipping0.0.42. This is a real server perf bug, not a wash of #5087 / #5089 / #11220. Those would stop this host from entering the unknown bucket. This report is the cost of the bucket itself: an unrefinable host is re-probed on every read, andfirstSuccessOfmultiplies that by every project that shares it.What the code does
<tenant>.ghe.comis classifiedunknown.isGitHubHostishost === "github.com"or a completegithubDNS label (packages/shared/src/sourceControl.ts). That is #5932 / #6175.acme.ghe.comhas neither, sodetectSourceControlProviderFromRemoteUrlreturns{ kind: "unknown", baseUrl: "https://<tenant>.ghe.com" }. There is noghe.comexception.Nothing can refine it. Of the five discovery specs, only GitLab and Forgejo (managed
fj/tea) registerrefineUnknownRemote. GitHub’s spec is version +gh auth statusonly — no remote hook.parseGitHubAuthStatusalready walks every host in that JSON, including enterprise ones, and thenfindAuthenticatedGitHubAccountkeeps a single account for the onegithubdiscovery row. The authenticated GHE host is sitting in the parse and is never asked “does this remote belong to you?”Each refinement therefore always runs
fj,tea, andglabagainst a host none of them will claim. GitHub’sghis on PATH and already logged in to that host, and is not in this loop.The result is discarded, then asked again:
SourceControlProviderRegistry.resolveHandleonly hitsproviderContextCache(5s,Duration.zeroon failure) whencontextis omitted. Both hot callers passcontextand skip it:PullRequestService.refineUnknownProjectKinds, andserver.ts’s identityrefine(which also forceskind: "unknown").orElseSucceeds missing CLIs to[]. CLI specsorElseSucceedtonull. A host-level “nobody claimed this” is indistinguishable from a one-off miss, and is not stored.RepositoryIdentityResolverdoes cache the identity for 1 minute. That is a different cache.refineUnknownProjectKindsnever consults it.firstSuccessOfthen multiplies it. Candidates are grouped bybaseUrl. Every unknown project on the GHE host lands in one group. When no candidate can succeed, the whole list runs. That path is load-bearing for broken checkouts — it does not want eleven healthy checkouts of an unclaimable host.That is the 12-span
canonicalRef.requireProjectfirst refineslistWorkspaceProjects({ projectId })(one candidate). Unknown has no PR API, so it falls through tolistWorkspaceProjects({ host })andfirstSuccessOfs the other projects on that host. 1 + N = N+1refineUnknownRemoteProvider→ 2(N+1)ForgejoCli.listLogins. Matches the trace.PullRequestSyncReactorticksSchedule.spaced("1 minute")atconcurrency: 8. Open + unsettled and null snapshots always are due. Eight of thosecanonicalRefs at ~20s is enough to blow the 60s gap. The 119ssweepspan is that backlog, not a separate scheduler bug.WSL2 is the amplifier, not the defect. A missing
fj/tea/glabwalks every PATH entry;/mnt/c/...on 9p is ~100× a native ENOENT. Same storm on macOS/Linux, cheaper per spawn. Desktop “Reconnecting” under extra load is the event-loop losing a health probe, not a separate reconnect bug.Related, not duplicates
github.com/ agithublabel..ghe.com+ GitHubrefineUnknownRemoteviaghhosts; newgithub-enterprisekindunknown. XXL, unvouched, not the negative-cache /firstSuccessOfhole. Do not wait on it, and do not attributeCloses #12060.refineUnknownProjectKinds.notgithub.example.comis not GitHubacme.ghe.comis unknown. Keep the strict match; add an explicit GHE rule or agh-host refine, do not revert substring matching.No open PR caches a negative refinement or stops
firstSuccessOfafter a host-level miss.Suggested fix
Keep this a small server bugfix on unknown-remote refinement. Do not land #5089 to close this, and do not rebuild provider detection.
baseUrl(and host), including “still unknown”. One TTL for the whole group, minutes not 5s, on both success and “no spec claimed this.”resolveHandle({ context })must read that map. Identity-resolver refine should too. Failures that are cwd-specific (broken checkout, spawn crash) stay uncached so the broken-then-healthy checkout test still walks to/healthy.kind: "unknown"because every spec returnednull, the rest of thatbaseUrlgroup will too.firstSuccessOfonly needs the next cwd when this checkout failed to produce an answer.requireProject’slistWorkspaceProjects({ host })fallback is for “route this PR to another checkout on a supported host.” Forunknownit is a second full probe storm that cannot create an API.*.ghe.comas GitHub (or add a GitHubrefineUnknownRemotethat claims a remote whose host is ingh auth status). That removes the GHE trigger. It does not replace (1)–(3) for GHES vanity domains and everything else that staysunknown.*.ghe.com(orgit.example.internal) withfj/tea/glabmissing → one refine per host per window, not N; the “broken then healthy GitLab cwd” test still asks both;canonicalRefon an unknown project does not calllistWorkspaceProjects({ host })after the project-scoped refine already returned unknown.canonicalRefis onerefineUnknownRemoteProvider(or one per host),PullRequestSyncReactor.sweepstays inside the 60s gap, sidebar VCS settles in hundreds of ms.Do not treat PATH stubs, skipping the idle sweep, or “install
glab” as the fix.Workaround
The reporter’s
~/.local/bin/{tea,fj,glab}exit 1stubs are the right local mitigation on WSL: discovery still reports missing, refinement still returnsunknown, PATH no longer walks/mnt/c. Measured ~2× on that box; the storm is still there. No in-app way to mark a host as GitHub.Classification: bug · accepted · medium (server source-control; uncached unknown-host refine × projects on the host; WSL PATH makes it user-visible; desktop reconnect is the sideshow)
Labels: add
bug,accepted,via-triageDiscord tags:
performance,linuxrenemifsud commentedon Sep 30, 2026
Adding a macOS data point. On macOS the same probe storm does more than slow down the server. It exhausts Gatekeeper (
syspolicyd), and after that other apps cannot start new processes.Setup
0.0.43-nightly.20260927.2344, macOS 26.3 (25D125), Apple Siliconunknown: one<tenant>.ghe.comremote, and two that use an SSH host alias for github.com (git@<alias>:owner/repo.git, same shape as [Bug]: Pull request pane treats SSH aliases as GitHub API hosts #6198)snapshot_jsonis NULL on all 54, so every link is due on every sweepteaandglabare not installed. The server'sPATHhas 21 entriesPer sweep (every ~60 s, 4 consecutive minutes from
server.trace.ndjson)~804 failed
processRunner.runProcessCorespans (glab auth statusandtea login list --output json, allENOENT), 402ForgejoCli.listLogins17,208 to 17,658 kernel log lines per minute, all for the T3 executable:
That is about 21 to 22 lines per failed spawn, close to the number of
PATHentries.What breaks
syspolicydruns out of file descriptors. About 5,000 of each of these lines per 10 s:spctl --assess --type executeon an unrelated, notarized app returnsToo many open files.The kernel then denies new processes of other apps. Google Chrome renderer helpers stayed at
_dyld_start(96 KB footprint) and ASP denied them, so no tab loaded. OrbStack was denied 90 times in 3 minutes.sudo killall syspolicydhelps only briefly. The relaunchedsyspolicydlogged EMFILE errors again after the next sweep, about one minute later.So on macOS the size of each burst is the problem, not only the cost of each probe. I have not tested the stub workaround from the original report on macOS.
Torrey-Smith-165 commentedon Oct 7, 2026
Another macOS data point. The behavior is unchanged at
v0.0.46-nightly.20261006.2752(d8d037eae); line numbers below are from that tag.Setup
0.0.46-nightly.20261005.2689, macOS, Apple Silicon. 194 projects, 1,158 PRs linked to 451 non-archived threads.glabandteaare not installed.~/.ssh/config(git@github-work:owner/repo.git,git@github-personal:owner/repo.git), the same shape as [Bug]: Pull request pane treats SSH aliases as GitHub API hosts #6198.isGitHubHost(packages/shared/src/sourceControl.ts:188) needs agithubDNS label, so these areunknown.GitHubCli.ts:502-504and:512already read a dotless alias containinggithubashub.lumenfield.work.hub.lumenfield.workremote.requireProject(PullRequestService.ts:867) never finds the thread's own repository, so it always falls through tolistWorkspaceProjects({ host })(:894). That lists every project and refines every unknown one (:679-753).resolveHandlegets an explicitcontext(:727), so it skipsproviderContextCache(SourceControlProviderRegistry.ts:261-264).isDuereturns true for them on every sweep (PullRequestSyncReactor.ts:174), andlastSyncedAtis only set after a successful read (:328).Measured from
~/.t3/userdata/logs/server.trace.ndjson*glab+tea)ProjectEnrichmentService.request/sPullRequestService.canonicalRef/sThe trace file rotates a full 10 MB every 1.5 to 2 s. An endpoint security scanner on the same machine ran at 200-290% CPU scanning the spawns and trace writes, and the whole machine lagged.
What would help, in addition to #12071
githubas GitHub indetectSourceControlProviderFromRemoteUrl, matchinggitHubApiHostForRemote. Or resolve the alias withssh -G <alias>and use itshostname.treyecollins commentedon Oct 7, 2026
Another macOS data point, on the newest nightly. Here the probe storm makes the desktop app's own local server show " is reconnecting", with no remote environment involved.
Environment
0.0.46-nightly.20261007.2774(611132c171f3), macOS 26 (Darwin 25.6.0), Apple Silicon, Node v26.8.2git@github-wiens:org/repo.git, a dotless SSH alias for github.com. A per-directoryincludeIfgitconfig had aninsteadOfrule rewritinggit@github.com:org/to that alias.teais not installed.glabis installed but not authenticated for that host.What happened
The banner " is reconnecting" (
ChatView.tsxenvironment-unavailable item) keeps appearing even though the server is local and its process is alive.Evidence from
server.trace.ndjsonteaspawns (ForgejoCli.listLogins, allspawn tea ENOENT), 212glabspawns, about 530VcsProcess.run, 215refineUnknownRemoteProvider, and 870ProjectEnrichmentService.request.PullRequestSyncReactor.sweeptook 236s./api/auth/sessiontook 6s, and each/.well-known/t3/environmentprobe ran for 8 to 10s before it was interrupted insideServerSecretStore.get. The/wssocket then closed and the banner appeared.Diagnosis
Same as above.
isGitHubHost(packages/shared/src/sourceControl.ts:188) needs agithubDNS label, sogithub-wiensis classifiedunknown. A remote likehub.lumenfield.work-foopasses, because splitting on the dot yields agithublabel. When the caller passescontext,resolveHandlebypassesproviderContextCache(SourceControlProviderRegistry.ts:262), so the failed refinement is reprobed on every read. The #16321 fix for #6198 changed only the PR pane path, not this detection, so the problem persists on.2774.Workaround that worked: keep remotes on plain
hub.lumenfield.workand pin the SSH key per directory, rather than using a host alias. In theincludeIfgitconfig:Related: #16503 (same saturation blocking connection setup for a remote environment), #6198.
Produced via
t3 triageby Claude Code (Opus 5.5).