Repository navigation
[Bug]: PullRequestSyncReactor's gh calls fail with an opaque error — VcsProcessExitError discards stderr, so the real cause can't be diagnosed #12224
Description
Activity
Triage
Confirmed on current
main(0150c6a53). Real server GitHub CLI bug on packaged desktop. Not a duplicate of #11220 (cadence) or #11247 (App installation token). Thegh ($HOME)string is real; the command that produces it is not thepr viewthe reactor thinks it is running.What happens
Packaged desktop starts the backend with cwd = the user’s home directory:
backendCwd: input.isPackaged ? homeDirectory : appRoot,
PullRequestSyncReactor.syncGroupthen callspullRequests.summarywithrecoverTransientFailure: falseevery time an open, unsettled link is due (the 1-minute sweep fromSchedule.spaced("1 minute")):const syncGroup = Effect.fn("PullRequestSyncReactor.syncGroup")(function* ( key: string, entries: ReadonlyArray<LinkEntry>, ) { // ... const summary = yield* pullRequests.summary(ref, { recoverTransientFailure: false });
summaryUncacheddoes pass the project root into the provider:const summaryUncached: PullRequestService["Service"]["summary"] = (input) => requireProject(input).pipe( Effect.flatMap((project) => { const providerInput = { cwd: project.project.workspaceRoot, repository: project.repository, host: project.host, number: input.number, }; const read = project.api.getChangeRequestSummary === undefined ? project.api.getChangeRequest(providerInput) : project.api.getChangeRequestSummary(providerInput);
and
getPullRequestSummaryrunsgh pr view <n> --repo <host>/<repo>, which does not need a local git checkout:getPullRequestSummary: (input) => github .execute({ cwd: input.cwd, args: [ "pr", "view", String(input.number), ...repositoryArgs(input), "--json", PULL_REQUEST_DETAIL_JSON_FIELDS, ], })
That
executenever reachespr viewuntil a quota probe succeeds. Since #11888 the probe is hardcoded to the server cwd:const quota = yield* Cache.makeWith( (key: string) => { const host = key.split("\0")[0]!; return executeRaw({ cwd: globalThis.process.cwd(), args: [ "api", "rate_limit", "--hostname", host, "--jq", ".resources.graphql | {data:{rateLimit:{cost:1,limit:.limit,remaining:.remaining,resetAt:(.reset|todateiso8601)}}}", ], }).pipe( // ... ); }, { capacity: 32, timeToLive: (exit) => (Exit.isSuccess(exit) ? Duration.seconds(30) : Duration.zero), }, ); // pr list | pr view | repo view then: yield* Cache.get(quota, `${host}\0${credential?.credentialFingerprint ?? ""}`); return yield* executeRaw(input);
VcsProcessExitErrorprintsgh (<cwd>). That is why every failure showsgh (/Users/ryan)— it isprocess.cwd(), which packaged desktop set to$HOME. A failed probe is not cached (timeToLiveis0on failure), so every sweep retries it andpr viewnever runs.recoverTransientFailure: falsealso drops any last-good summary.The pasted stack does not include gh stderr.
VcsProcessExitError.detailis onlyProcess exited with a non-zero status.Thecd ~ && gh pr list --json numberrepro is a different command (pr listwith no--repo). T3’s actual read ispr view --repo. The command that matches cwd=$HOMEon every failure is the quotagh api rate_limit.ThreadPullRequestReactoris the contrast: it picks worktree orproject.workspaceRootbefore calling git/gh. The sync reactor never sets cwd itself;GitHubCli.executeoverwrites it for the probe.Nightly
0.0.43-nightly.20260917.1837includes #11888 (merged 2026-09-15). Auth is fine; this is not #11247.Not a duplicate
- Thread PR discovery and settlement sweeps run every minute without client demand, and their caches expire before the next sweep #11220 — same 1-minute sweep, about spawn volume / cache TTL, not wrong cwd.
- [Bug]: Pull Requests page fails with "GitHub CLI command failed" when gh uses a GitHub App installation token #11247 —
gh api user403 on an App installation token. Reporter already ruled that out (personal OAuth). - [Bug]: Git dir followed unconditionally #12204 — wrong project root via separate
git-dir. Different path.
No existing issue for the quota probe using
process.cwd().Likely code
apps/server/src/sourceControl/GitHubCli.ts— probe cwd isprocess.cwd(); failure blockspr view/pr list/repo view.apps/desktop/src/app/DesktopEnvironment.ts— packagedbackendCwdis$HOME.apps/server/src/pullRequest/GitHubPullRequestCli.ts— real read already has project cwd +--repo.apps/server/src/orchestration/PullRequestSyncReactor.tspackages/contracts/src/vcs.ts—gh (<cwd>)in the message.
Fix direction
- Pass
input.cwd(already the project workspace root) into the quotaexecuteRaw, notprocess.cwd(). - Do not fail the real
pr viewwhen the probe fails. Treat a dead probe as “unknown budget” and still run the read. A host-levelrate_limitshould not take the PR sync path down. - Test: packaged-style
process.cwd() === $HOME(not a git repo) +execute({ cwd: "/repo", args: ["pr", "view", …] })must still callpr viewwith/repo. Cover probe-failure-must-not-block-read. - If a follow-up log still has stderr
not a git repositoryonpr view --repoitself, then also checkworkspaceRoot; that is a second bug, not what this stack shows.
Workaround
None in the UI. Unpacked /
npx t3 servefrom a repo may hide it becauseprocess.cwd()is then a git root. No user setting points the probe at the project.Accepting as a server GitHub CLI bug. The reactor cadence in #11220 makes the failed probe loud; it is not the cause.
Correction
I traced the exact code path (
PullRequestSyncReactor.syncGroup→PullRequestService.summary→requireProject→getChangeRequestSummary→GitHubPullRequestCli.getPullRequestSummary) in the actual source rather than guessing from the packaged trace alone.getPullRequestSummaryalways passes--repo <host>/<repository>explicitly:github.execute({ cwd: input.cwd, args: ["pr", "view", String(input.number), ...repositoryArgs(input), "--json", ...], })
gh pr view --repo owner/repodoes not requirecwdto be inside a git repository. I confirmed this directly:$ cd /Users/ryan && gh pr view 11225 --repo pingdotgg/t3code --json number,title {"number":11225,"title":"fix(cursor): retry prompts that only returned a transport failure"}Exit 0, from the bare home directory. So "not a git repository" is not what's failing these calls — retracting that part of the original report.
cwdreally isproject.workspaceRootfor thepr viewcall itself (confirmed viarequireProject), it just isn't the cause of that call failing.I couldn't find the actual failing call site from the trace alone, since
VcsProcessExitErroronly keepsstderrLength, not the text. See the triage comment below — the real mechanism is thegh api rate_limitquota probe insideGitHubCli.execute, which does hardcodeprocess.cwd()rather thaninput.cwd. That's the actual cwd bug; it's just one call earlier in the chain than I looked.- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.acceptedfeature request acceptedfeature request acceptedvia-triageFiled through npx t3 triageFiled through npx t3 triage
on Sep 17, 2026 - changed the title
[-][Bug]: PullRequestSyncReactor runs gh with server cwd ($HOME) instead of project root, failing every sync sweep[/-][+][Bug]: PullRequestSyncReactor's gh calls fail with an opaque error — VcsProcessExitError discards stderr, so the real cause can't be diagnosed[/+]on Sep 17, 2026 The blocking part of this appears fixed on
mainby #14673.The probe that failed with
gh (<home>)wasgh api rate_limitwithprocess.cwd(). #14673 replaced it with a GraphQLrateLimitreading (budgetReadinginapps/server/src/sourceControl/GitHubCli.ts), andguardedReadnow ignores a failed reading:Cache.get(budgetReading, …).pipe(Effect.ignore), commented "A failed reading leaves the budget unknown; it never blocks the read itself." That's fix direction 2 from the triage, sopr view --reporuns even when the reading fails. The reading still runs fromprocess.cwd(), butgh api graphql --hostnamedoesn't need a git checkout.What remains is the title's point that
VcsProcessExitErrordoesn't include stderr. That's a contract-level choice, so it may be worth its own issue if wanted. Otherwise this one can probably be closed.Note
Grok responding on behalf of Julius.
Resolved by the GitHub API stack that landed today (#16319, #16320, #16321). Pull request sync no longer shells out to
gh; it calls GitHub's API in-process, andGitHubCli.executeis gone, so the opaqueVcsProcessExitErrorfrom those calls can't happen anymore. API failures now map to specific reasons (unauthenticated, rate-limited with a retry time, not found). Closing; open a new issue with the new error if PR sync still fails.
Before submitting
Area
apps/server
Steps to reproduce
This is a background-reactor bug, not directly user-triggered, so there isn't a UI click-path repro. It shows up passively:
apps/servertrace logs (~/.t3/userdata/logs/server.trace.ndjson*) and filter for"name":"PullRequestSyncReactor.syncGroup"(orPullRequestReadCache.get,GitHubCli.executeRaw) with"_tag":"Failure".gh (/Users/<user>)in the error chain. Thatcwdis the realproject.workspaceRoot(confirmed by readingPullRequestService.summaryUncached→requireProject→getChangeRequestSummary), not a bug in cwd resolution — see the edit note above.VcsProcessExitErroronly recordsstderrLength/stderrTruncated, not the stderr text, so the actual reasonghexited 1 is not recoverable from this trace. It was classified as the generic"command-failed"bucket byclassifyNonZeroExit(not auth, not rate-limited, not not-found), which only narrows it down, doesn't identify it.Expected behavior
VcsProcessExitError(and itsgh/glab/azcallers throughGitHubCli/VcsProcess) should carry enough of the real stderr to diagnose acommand-failedexit, instead of onlystderrLength. See #4380 for the same defect inGitCommandError(plain git commands) — this is asking for the same fix applied toVcsProcessExitError.Actual behavior
25 distinct
ghinvocations failed with exit 1 over a 3.5h window (below), roughly 60s apart, matching the reactor cadence already described in #11220. The failure is silent from the user's perspective — nothing surfaces in the UI, only the trace logs — so PR data for the affected thread(s) may simply never populate, with no way (yet) to tell why.Observed over a 3.5h window: 25 distinct failed
ghinvocations (PullRequestSyncReactor.syncGroup→PullRequestService.persistedRead→PullRequestReadCache.get→GitHubCli.executeRaw), each one logged at up to 15+ nested effect-span layers (382 raw"_tag":"Failure"log lines total for these 25 logical failures), spaced roughly 60s apart, all withgh (/Users/ryan)in the cause chain.Impact
Major degradation or frequent failure
Version or commit
T3 Code Nightly
0.0.43-nightly.20260917.1837Environment
macOS 27.0 (build 26A428), Bun 1.4.2, Node v26.8.2,
gh2.83.1, GitHub provider = github.com with personal OAuth token (not a GitHub App installation token — rules out #11247).Logs or stack traces
Note:
gh pr view --repo <owner>/<repo> --json ...(what T3 actually runs here) does not requirecwdto be a git repo — verified directly (cd /Users/ryan && gh pr view 11225 --repo pingdotgg/t3code --json number,titlesucceeds, exit 0). An earlier manual repro usinggh pr list(no--repo) is not representative of this call shape and has been removed.Workaround
None found — the real cause is unknown pending a stderr excerpt on
VcsProcessExitError.