Skip to content

[Bug]: Pull request sync runs all projects' gh lookups from one directory, breaking per-account gh setups #14123

Closed as not planned

Description

@jcardama

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. Use two GitHub accounts, each able to see a different set of private repositories. Account A sees org-a/*; account B sees org-b/*. Neither can see the other's repositories.
  2. Select the account per repository with a small gh wrapper on PATH that sets GH_CONFIG_DIR from the working directory. Each account has its own config directory, so gh run from an org-a checkout authenticates as A, and from an org-b checkout as B.
  3. Open projects for repositories from both orgs, with threads that have linked pull requests.

Expected behavior

Pull request lookups for a thread run in that thread's workspace, the way they did in 0.0.43-nightly.20260925.2269. Each lookup then authenticates as the account that can see the repository, and pull request status and the pull request pane load for every project.

Actual behavior

In 0.0.43-nightly.20260928.2375, pull request sync runs every project's lookups from one working directory. It appears to be the first project's workspace, unrelated to the thread. I sampled the spawned processes:

  • gh pr view <n> --repo github.com/org-b/<repo> --json ... runs from the org-a project's directory, so it authenticates as A and fails with Could not resolve to a Repository.
  • gh api graphql with a PullRequestSummaries query batches repositories from both orgs into one query (s0: repository(owner: "org-b", ...), s1: repository(owner: "org-a", ...)). No single account can answer that query. Under either account, GitHub returns partial data plus a NOT_FOUND error, and gh exits 1.

The server logs pull request sync skipped for every pull request in the other org each minute, and the pull request pane shows "Could not load pull requests. Pull request operation detail failed: GitHub CLI command failed."

Impact

Anyone who separates GitHub accounts per client or organization (for example with GH_CONFIG_DIR per directory, or with gh configured differently per checkout) loses pull request status and the pull request pane for every repository the directory T3 picked can't see. The only workarounds are giving one account access to every org, which defeats the separation, or routing the account from the command's arguments instead of the working directory.

Version or commit

Regressed between 0.0.43-nightly.20260925.2269 (works) and 0.0.43-nightly.20260928.2375 (fails). Possibly related to the pull request sync changes in #13704 or #13691, but I haven't bisected.

Environment

Linux x64, server run as a systemd user service, gh 2.x.

Logs or stack traces

[17:43:25.899] WARN (#513610): pull request sync skipped
  { key: 'github.com/org-b/repo-x#15' }

A mixed-owner query reproduces the partial failure directly under either account:

$ gh api graphql -f query='query { s0: repository(owner: "org-b", name: "repo-x") { pullRequest(number: 15) { state } } s1: repository(owner: "org-a", name: "repo-y") { pullRequest(number: 41) { state } } }'
{"data":{"s0":null,"s1":{"pullRequest":{"state":"OPEN"}}},"errors":[{"type":"NOT_FOUND","path":["s0"],...}]}
gh: Could not resolve to a Repository with the name 'org-b/repo-x'.
exit status 1

Workaround

Route the account from the repository named in the command (--repo, repos/<owner>/..., GraphQL owner:) rather than the working directory. That restores the per-pull-request lookups. A mixed-owner batched query still fails for one side.

Suggested fix: run lookups from each thread's workspace as before, or batch GraphQL queries per workspace or per credential rather than across all projects.

Activity

  1. juliusmarminge commented on Sep 28, 2026

    @juliusmarminge
    Member

    Triage

    Confirmed on current main (d15210cd, which is v0.0.43-nightly.20260928.2375). The mixed-owner PullRequestSummaries query is what this sync does. Not a duplicate of #14113: that failure is an interrupt on first open (All fibers interrupted without error). This one is gh exiting 1.

    Background sync asks for every due link together (PullRequestSyncReactor, concurrency 25). Each summary becomes a PullRequestSummaryRead, and those reads are grouped only by host and pinned credential:

      const summaryResolver = RequestResolver.makeGrouped<PullRequestSummaryRead, string>({
        key: ({ request, context }) =>
          JSON.stringify([
            request.host.toLowerCase(),
            Context.getOrElse(context, GitHubCli.PinnedGitHubCredential, () => null)
              ?.credentialFingerprint ?? null,
            Context.getOrElse(context, SourceControlRateLimit.CredentialScope, () => ""),
          ]),
        resolver: (entries) => {
          const [first] = entries;
          // ...
              : graphqlRead({
                  cwd: first.request.cwd,
                  host: first.request.host,
                  operation: "getPullRequestSummary",
                  query,

    Sync never pins a credential, so every github.com link that arrives in the 10ms window shares one group. The query runs in entries[0]'s working directory, which is whichever request got there first, not the thread that owns the repository. gh exits 1 when any aliased repository is invisible to that directory's account, the whole document is dropped, and the sweep logs pull request sync skipped with only the key.

    A failed document is then supposed to fall back to one gh pr view per entry, using that entry's own cwd. summary sets that cwd from the project requireProject returns. When the link's repository matches the thread's project, that is the thread's checkout. repositoryArgs always passes --repo <host>/<owner>/<name>, so --repo github.com/org-b/... does not by itself mean the target was rewritten.

    A gh pr view for org-b whose process cwd is an org-a checkout means the serving project was not org-b. That is the host fallback in requireProject: a GitHub ref may be answered by any checkout on the host, and the last resort is the first non-Azure checkout, with the requested repository written onto it. The pull-request list (readTogether) and listStats do the same thing on purpose (cwd is chunk[0]'s workspace; the search API comment says "any checkout on the host"). Detail is not in the summary batch. Pull request operation detail failed: GitHub CLI command failed is that read's own gh failing in whichever checkout requireProject selected, which is also what the pane renders.

    This resolver is unchanged between v0.0.43-nightly.20260925.2269 and 2375. It landed in #13198 (2026-09-22), which is already in 2269. #13704 only stops the sweep from loading threads that have no links. #13691 changes which threads settlement reads, not which directory gh runs in. If 2269 really did run each lookup in the thread checkout, the difference is whether those reads landed in one 10ms batch. The code that collapses them was already there.

    Group summary batches by the credential gh auth token returns in that request's cwd (the fingerprint captureVerifiedCredential already computes), and run each query from a cwd that produced it. Do the same for the list and stat reads that take the first workspace in the chunk. A mixed-owner document cannot be split after gh has already failed it.

  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Sep 28, 2026
  3. added 2 commits that reference this issue on Oct 17, 2026
    5d89532
    7d5e658
  4. Chinoman10 commented on Oct 18, 2026

    @Chinoman10
    Contributor

    I reproduced a related case with two organisation-scoped PATs belonging to the same GitHub login: Git HTTPS access works, but T3's saved host token cannot resolve the other organisation's private repository before cloning. #21264 records that reproduction.

    #21265 adds an explicitly enabled server option that reuses existing global Git HTTPS helpers and separates native GraphQL summary/watch batches by selected credential and repository. It also keeps operation pins and caches separate when the tokens have the same viewer. This provides a Git-helper route for the mixed-credential boundary described here; it preserves the existing default and does not change per-checkout gh wrapper behavior. The PR includes focused tests and native remote import/PR-read evidence.

  5. 1nto5 commented on Oct 19, 2026

    @1nto5

    +1, still happening on 0.1.1-nightly. Setup: three accounts on github.com (a personal default plus two work accounts for different owners) and one self-hosted GitLab. A gh wrapper earlier on PATH picks the account from the repo owner (-R, then the origin remote). Terminal and git work per repo, but T3 uses one GitHub token per host for every project, so the mobile Pulls tab shows <owner>/<repo> could not be read for the other two owners.

    What would fix it: pick the credential per project or per owner (for example an account picker per project in Settings → Source Control), or resolve it in each project's checkout as before the API transport. Ideally the same for GitLab and other forges.

  6. KristijanJovanovski commented on Oct 29, 2026

    @KristijanJovanovski

    Still happening on 0.1.4-nightly.20261028.3824 (macOS), and it affects T3's own GitHub sign-in, not only gh wrapper setups.

    Setup: two GitHub accounts on the same machine, each able to see private repositories the other cannot. Selecting the account in one project's Source Control settings changes it for every project, so it behaves as a single global setting and it is always one account or the other.

    Projects whose repositories belong to the other account then fail every PR sync with GitHubApiNotFoundError: GitHub could not find the requested resource, or the credential cannot see it (most GitHubPullRequestApi.getPullRequestSummary spans in server.trace.ndjson exit with this), and watch_pull_request drops after 8 failed reads. gh authenticated as the right account reads the same PR without issue.

    +1 for selecting the credential per project (or per repository owner).

  7. jcardama commented on Oct 30, 2026

    @jcardama
    Author

    Still happens on 0.1.6-nightly.20261030.3904 (Linux x64, server as a systemd user service). The setup is the same as above: account A sees org-a/*, account B sees org-b/*, and a gh wrapper on PATH selects the account.

    Observed:

    • In 30 minutes, the server logged pull request sync skipped 28 times. All 28 entries are for linked pull requests in org-b repositories. Each entry has this reason: Pull request operation summary failed: GitHub could not find the requested resource, or the credential cannot see it.
    • Pull requests in org-a repositories give no errors.

    Cause, from the 0.1.6 bundle (GitHubCredentials):

    • The server gets its token with gh auth token --hostname github.com. This command names no repository.
    • The command runs in the server's working directory (process.cwd(), here the home directory), not in the project's checkout. Thus a wrapper that selects the account from the working directory or from --repo always returns account A.
    • The server caches the token by host and selected account. The account choice in Settings → Source Control (hosts[host].account) is also per host. No setting selects a different account for one owner or repository.

    #21581 and #21582 (select the account by owner and repository) appear to fix this.

  8. bigintersmind commented on Nov 12, 2026

    @bigintersmind

    Still happening on 0.1.8 (T3 Code Desktop, macOS arm64). The server runs on an always-on Mac, and I connect to it from the desktop app on another Mac. Which client I use makes no difference, because the reads happen on the server. v0.1.9 doesn't fix it either: GitHubCredentials hasn't changed, and #21581, #21582 and #21265 are still open.

    Setup: two github.com accounts. A is the default gh login. B lives in its own GH_CONFIG_DIR and is the only one that can see the org-b/* repos. direnv picks the account per directory, and a gh wrapper earlier on PATH picks it per repo owner (from --repo, then repos/<owner>/…, then the origin remote). Git uses a per-URL credential helper pointed at B's config. Git, gh in a terminal, and the agents' own gh calls all use the right account.

    What T3 does: GitHubCredentials runs gh auth token --hostname github.com from the server's working directory without naming a repository. The wrapper has nothing to route on, so it returns A, and T3 caches that one token for every github.com repo. Every read of an org-b PR then fails with GitHubApiNotFoundError (credential: "gh", in getPullRequestDetail and getPullRequestSummary).

    Impact on this install:

    • About 90 minutes of server.trace.ndjson show 3,377 failed PullRequestSyncReactor.syncGroup spans, each with Pull request operation summary failed: GitHub could not find the requested resource, or the credential cannot see it. Every PR the failures name is in an org-b repo.
    • Since 2026-11-06, watch_pull_request has given up 79 times across 13 threads ("stopped watching … because it failed to read it from the host 8 times in a row"). All 79 watches were on PRs in two org-b repos.

    Not covered in this thread yet: agents can't fall back to their own gh. The instructions T3 adds for agents (<pull_request_linking>) tell them to use watch_pull_request and forbid gh --watch, loops, sleep, Monitor and ScheduleWakeup for PR waits. That holds even when T3's credential can't see the repo. The agent calls watch_pull_request, the watch fails 8 reads and stops, and the agent is then told to check by hand. Its own shell has a gh that works for these repos, but it isn't allowed to wait with it. Until per-owner or per-repo accounts land, either of these would help:

    • After a watch ends as unreadable, the message (or the instructions) lets the agent poll with its own gh.
    • A setting turns off the PR-watch part of the added instructions.

    My workaround for now is a user-level instruction: for org-b PRs, don't call watch_pull_request; run gh pr checks <n> --repo … --watch in the background instead.

    +1 for #21582 (choosing the account by owner and repository). The opt-in in #21265, which reuses existing git credential helpers, would also fix this setup without extra configuration, since the git helper already sends org-b to account B.

    Investigated and written by Claude Opus 5.5 (Claude Code harness, T3 Code triage session).

  9. juliusmarminge commented on Nov 12, 2026

    @juliusmarminge
    Member

    Thanks for writing this up. It asks for new behaviour rather than reporting a defect, so it does not belong in the bug tracker and I'm closing it as not planned here. Feature ideas are discussed in Discussions: please post it there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions