[BUG] Desktop PR bar pins the oldest closed-unmerged PR on a long-lived branch that has no open PR
Preflight Checklist
- [x] I have searched existing issues. The closest are #48694 / #63767 / #57546 / #65970 / #71326 / #15313 / #30757, all closed. This report is not a duplicate of the diagnosis in those issues — see "Why this is not #48694" below.
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
The PR bar above the composer in the desktop app pins a closed, unmerged pull request from 13 months ago to a long-lived integration branch that currently has zero open PRs. It reappears in every new session on that branch, and the × only hides it for the current session.
The branch is a long-lived integration branch (develop) that is periodically the head of release PRs into main. Over ~2.5 years it accumulated 42 PRs with that branch as head: 40 merged, 2 closed-unmerged. The bar consistently shows the older of the two closed-unmerged ones (created 2025-07-31), not the most recent PR on the branch (a release PR merged the day before I filed this).
Confirmed — things I observed directly
- The displayed value is refreshed live, not stale on disk. In the desktop app's
Local Storageleveldb (originhttps://claude.ai), the keybranch-status-cacheholds an entry keyed<owner>/<repo>:<branch>whosedata.pull_requestis the wrong PR, with"state":"closed"and atimestamp~1 minute before I inspected it. So the app re-fetches and re-derives the same wrong PR continuously. gh pr viewon that branch returns nothing:no pull requests found for branch "develop".- The REST path is not what's being displayed.
GET /repos/{owner}/{repo}/pulls?head={owner}:develop&state=all&per_page=3returns the most recently created PR first — a merged release PR from the day before, not the 13-month-old one. - A GraphQL query that reproduces the displayed value exactly:
``graphql``
{
repository(owner: "OWNER", name: "REPO") {
ref(qualifiedName: "refs/heads/develop") {
associatedPullRequests(first: 5, states: [OPEN, CLOSED]) {
totalCount
nodes { number state createdAt baseRefName }
}
}
}
}
Result: totalCount: 2, nodes in ascending creation order — [{2025-07-31, CLOSED}, {2026-05-05, CLOSED}]. The first node is exactly the PR shown in the bar. The same query with states: [OPEN] returns totalCount: 0. Note that states: [OPEN, CLOSED] excludes MERGED, which is why all 40 merged PRs on this branch are filtered out and only these two survive.
- Dismissal is stored per session. localStorage key
epitaxy.dismissedPrMaphas the shape{"orgUuid":"…","bySession":{"session_…":["<owner>/<repo>#<number>"]}}. Nothing records the dismissal at the repo/branch level, which matches the observed behaviour:×works for the current session only and the bar returns in the next one. (Also reported in #65970 and #57546.) - Resolution is not in the local CLI. The strings
associatedPullRequests,branch-status-cacheandprBarSnapshotdo not appear in the CLI binary bundled with the desktop app (2.1.247), so the branch→PR resolution appears to happen server-side. - The same stale PR is shown in the Claude mobile app, on the same repo/branch, which is consistent with (6).
Not confirmed — inference and untested items
- I cannot see the query the app actually issues. Item 4 is a query that reproduces the observed selection exactly; it is not proof that the app runs that query. Something else with the same effect (e.g. any "associated PRs, excluding merged, take the first" path) would fit equally well.
- With only 2 candidates I cannot fully separate the ordering rules. "First node returned by
associatedPullRequests", "oldest bycreatedAt" and "lowest PR number" all predict the same winner here. What is certain is that it is not "most recent". - I did not test whether clearing local state helps. I inferred it would not from the ~1-minute-old cache timestamp in item 1. #48694's reporter did test it — clearing
Local Storage,Session Storageand the app cache did not fix it there. - I have not tested other branch shapes (e.g. a feature branch whose only PR is merged, or a branch with one closed and one open PR), so I can't say how far the selection rule generalises.
Why this is not #48694
#48694 and its follow-up #63767 diagnosed the cause as the REST call pulls?head=…&state=all returning the most recently created PR regardless of state, and proposed switching to state=open. On my repo that REST call returns the correct recent PR (item 3), so that diagnosis does not explain this instance. The failure here is the opposite direction: merged PRs are already excluded, and the survivor set is ordered such that the oldest closed one wins.
What Should Happen?
- When a branch has no open PR, the PR bar should show no PR indicator. A closed-unmerged PR is not an actionable state for the current branch.
- If a non-open PR is deliberately surfaced, it should be the most recent associated PR, not the oldest, and its state should be visible in the bar.
- Dismissal via
×should persist at the repo+branch level (or offer "don't show for this branch"), not per session — otherwise a branch in this state costs a dismissal in every new session, indefinitely.
There is currently no user-side remedy: GitHub does not allow deleting a PR or changing its head ref, so a branch that has ever been the head of a closed-unmerged PR is permanently in this state.
Error Messages/Logs
# 1. The branch has no open PR
$ gh pr view
no pull requests found for branch "develop"
# 2. REST returns the correct, most recent PR first (so this is not the #48694 path)
$ gh api 'repos/OWNER/REPO/pulls?head=OWNER:develop&state=all&per_page=3' \
--jq '.[] | {number, state, created_at}'
{"number":<recent>, "state":"closed", "created_at":"2026-08-28T15:37:10Z"} # merged release PR
{"number":<recent-1>,"state":"closed","created_at":"2026-08-25T10:33:21Z"}
{"number":<recent-2>,"state":"closed","created_at":"2026-08-15T06:15:58Z"}
# 3. GraphQL associatedPullRequests with merged excluded — first node is what the bar shows
$ gh api graphql -f query='{ repository(owner:"OWNER", name:"REPO") {
ref(qualifiedName:"refs/heads/develop") {
associatedPullRequests(first:5, states:[OPEN,CLOSED]) {
totalCount nodes { number state createdAt } } } } }'
{"totalCount":2,
"nodes":[{"number":<old>, "state":"CLOSED","createdAt":"2025-07-31T01:28:57Z"}, # <- displayed in the bar
{"number":<old2>, "state":"CLOSED","createdAt":"2026-05-05T08:26:11Z"}]}
# 4. No open PR exists for the branch
$ ... associatedPullRequests(first:5, states:[OPEN]) ...
{"totalCount":0,"nodes":[]}
# 5. Live cache entry in the desktop app's Local Storage (origin https://claude.ai),
# key "branch-status-cache", timestamp ~1 minute before inspection
"OWNER/REPO:develop": {
"data": { "branch":"develop", "branch_exists":true, "commits":<n>, "has_changes":true,
"pull_request": { "number":<old>, "state":"closed", "base_ref":"main", ... } },
"timestamp": <epoch-ms, ~1 min before inspection>
}
# 6. Dismissal is recorded per session only, key "epitaxy.dismissedPrMap"
{"orgUuid":"…","bySession":{"session_…":["OWNER/REPO#<old>"]}}
Steps to Reproduce
- Take a repository with a long-lived branch (e.g.
develop) that is periodically the head of PRs intomain. - Ensure that branch has at least one PR that was closed without merging (an abandoned release PR is the common way this happens), and that all newer PRs from that branch were merged.
- Ensure the branch currently has no open PR (
gh pr view→no pull requests found). - Check out that branch and open a session in the desktop app.
- The PR bar above the composer shows the oldest closed-unmerged PR, with a live CI control, as if it were current.
- Click
×. Open a new session on the same branch — the bar is back.
Minimal construction of the state in step 2: open a PR from develop to main, then close it without merging. Any later merged PRs from develop do not displace it.
Claude Model
Opus
Is this a regression?
I don't know
Claude Code Version
2.1.251 (Claude Code); desktop app 1.40609.0 with bundled claude-code 2.1.247
Platform
Anthropic API
Operating System
macOS 26.6.2 (arm64)
Terminal/Shell
Other (Claude Code desktop app)
Additional Information
Repository details are withheld because the repository is private; owner, repo name, PR numbers and PR titles are replaced with placeholders above. The dates, states, counts and query results are verbatim.
Related, all closed: #48694 (closed as completed, no fix), #63767 (follow-up, closed as not planned), #57546, #65970, #71326, #15313, #30757. #77826 is open but concerns a different surface (the status row on long-lived background jobs, backed by ~/.claude/jobs/*/state.json).