Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/web
Steps to reproduce
- Use a long-lived integration branch in a local checkout, for example
dev. - Open a PR from
devtomainand merge it. - Continue using the same
devbranch after the merge for new work. - Create a new T3 Code thread on the local checkout while
devis checked out. - Complete a turn and inspect the branch/PR indicator and Sidebar V2 thread state.
Concrete observed case: GitHub PR rebel-tickets/NewWeb#182 was merged from dev into main. New unrelated threads created later on the still-active dev branch display #182 as their PR.
Expected behavior
A new thread on a reused long-lived branch should not inherit a historical merged PR merely because its head branch name matches.
T3 Code should associate a terminal PR only while it still describes the current branch state. If the branch HEAD has advanced beyond the PR head after merge, the historical PR badge/state should be ignored. Consequently, unrelated new threads should remain active.
Actual behavior
T3 Code displays the historical merged PR on every new thread using the reused dev branch. The merged PR state is then fed into automatic settlement, so each new unrelated thread settles when its turn finishes even when Auto-settle inactive threads is disabled.
The settle behavior is secondary damage. The root problem is stale PR association for a reused branch.
This appears adjacent to #4460, which prevents PR state from a different branch affecting settlement, but exact branch-name equality is insufficient for a long-lived branch reused after a merge. It is also related to #4752/#4755, which retain terminal PR state for local checkouts.
Suggested minimal fix: for merged/closed PRs, verify that the current branch HEAD still corresponds to the PR head commit before displaying or applying its terminal state. If the branch has advanced, discard the historical association. Open PR selection can continue using the existing branch match.
Impact
Major degradation or frequent failure
Version or commit
Current T3 Code desktop build as of 2026-07-30
Environment
macOS desktop app, local checkout, Sidebar V2, GitHub repository, Codex provider
Logs or stack traces
No runtime error. GitHub reports the historical PR as:
baseRefName=main
headRefName=dev
state=MERGEDWorkaround
Disable Sidebar V2 or create threads on a new uniquely named branch/worktree. Disabling Auto-settle inactive threads does not help because merged/closed PR settlement is unconditional.
Activity
iamtoled commentedon Aug 6, 2026
Third facet of the same root cause, just filed as #5514: opening a new release PR from the shared
devbranch retroactively adopts every historical thread on that branch — all my settled threads woke up simultaneously stamped with the new PR number. Branch-name-keyed association really needs to become a per-thread binding (e.g. PR created from within the thread, or explicitly attached).JasperG134 commentedon Aug 9, 2026
Confirmed on T3 Code Nightly 0.0.33-nightly.20260809.1043 with inactivity auto-settlement explicitly disabled. A newly created thread on a reused branch was classified as settled because a pull request for that branch had already merged several days earlier. The local branch HEAD still exactly matched the historical pull request head, so checking only whether the branch advanced after the pull request is not sufficient. A terminal pull request should only influence settlement when its merged/closed timestamp is at or after the thread creation time; otherwise it is historical context and should not settle the new thread. This reproduces the configuration-independent failure described here.
HemalR commentedon Aug 11, 2026
This is driving me nuts - I made the cardinal sin of pressing the wrong button yesterday (Create PR when I thought I was clicking on the Commit button) and now my dev branch is unusable because every agent reply auto-settles it.
It would be super helpful if there was support for a long lived branch
Mnigos commentedon Aug 12, 2026
Confirmed with a local thread that reproduces this exactly.
Setup:
sidebarAutoSettleAfterDays: null).Observed:
Expected:
A PR merged before the thread was created should be treated as historical context and should not settle that thread.
AntoineArt commentedon Aug 18, 2026
Root cause is reproducible by reading, not just by symptom — it sits in
findLatestPrForHeadContextand its single caller guard inapps/server/src/git/GitManager.ts(main @cebac35).Three ingredients combine:
The query deliberately includes terminal change requests —
listChangeRequests({ headSelector, state: "all", limit: 20 })(L1308-1313). Merged and closed MRs/PRs are in the candidate set by design.The fallback returns a terminal change request when no open one exists (L1324-1330):
parsedis sorted byupdatedAtdesc, so with zero open candidates this hands back the historical merged one.The only filter is scoped to the default branch (L1025-1031):
So the suppression that this comment describes as the general rule is applied only when
isDefaultBranchis true. On any non-default branch the merged change request flows straight through to the thread.That explains why this reads as intermittent across setups: on a repo where work happens on short-lived branches off
main, the guard covers the common case and the bug looks rare. On a GitFlow-style repo — long-liveddevwithmainas the default branch —devis neverisDefaultBranch, so every thread ondevinherits the last merged change request fromdevindefinitely. Same for any feature branch recycled after its MR merged.This hits GitLab remotes identically:
GitLabCli.listMergeRequestsmapsstate: "all"ontoglab mr list --all, andnormalizeGitLabMergeRequestStatefaithfully returns"merged"— the state is decoded correctly, it is just never used as a rejection criterion off the default branch.Worth noting for the per-thread-binding proposal above:
projection_threadshas no change-request column at all (thread_id, project_id, title, branch, worktree_path, …). The association is derived at read time frombranch, and the lookup cache is explicitly keyed on(cwd, branch)(L1020-1023). Nothing is persisted binding a thread to the MR/PR it actually opened, which is why every consumer has to re-guess from the branch name, and why #5514 is this same defect seen from the other side.Two fixes, independent, both cheap:
merged_at/closed_atis at or after thread creation. Older than that, it is historical context. No schema change, and it fixes the recycled-branch case on every provider.Happy to send a PR for the first one if a maintainer confirms the time-guard framing is the direction you want.
AntoineArt commentedon Aug 18, 2026
Follow-up: there is a second, independent root cause, and it is the one that actually fires in practice. The
isDefaultBranchvalue that the whole suppression hinges on is resolved from a stale local snapshot, never from the forge.resolveDefaultBranchName(apps/server/src/vcs/GitVcsDriverCore.tsL1260-1267 on main) does exactly one thing:with the fallback at L1717:
refs/remotes/origin/HEADis a clone-time snapshot. Git never refreshes it — not onfetch, not onpull. It only moves if someone explicitly runsgit remote set-head origin -a. So when a project changes its default branch on the forge after you cloned, this value stays silently wrong forever, and nothing in the codebase ever asks GitLab/GitHub what the default branch actually is.Live reproduction I just captured (details anonymised, private repo):
dev(confirmed viaglab repo view).refs/remotes/origin/HEADstill points atorigin/main— set at clone time, before the default moved.defaultBranchresolves to"main", so a thread checked out ondevgetsisDefaultBranch = false.findLatestPrForHeadContextfalls through toparsed[0]— the most recently updated MR sourced fromdev, which is a closed MR last updated 2026-07-07.The event store shows the signature clearly: for that thread, 6
thread.unsettledevents against 1thread.settled. The un-settles do not stick, and — worth noting for anyone trying to debug this from the event log — the re-settle emits no event at all, because the settled state is re-derived from the branch→MR resolution on read rather than stored. That asymmetry (unsettled ≫ settled on a single thread) is a reliable fingerprint for this bug.Two consequences beyond the reported case:
dev/develop/trunkand whereorigin/HEADwas neverset-headis permanently exposed, because thenullfallback guessesmain/master. That is very likely why the reports here cluster on GitFlow-style setups.maingets correct behaviour by accident, which is why this looks unreproducible to anyone testing on a standard GitHub layout.Suggested fix, orthogonal to the time-based guard from my previous comment and cheaper: resolve the default branch from the forge first —
gh repo view --json defaultBranchRef/glab repo view— and fall back to the localsymbolic-refonly when that fails, rather than the other way around. Both CLIs are already hard dependencies of the source-control layer, and the result is cacheable per repo. That alone makes the existing L1028 guard do what its comment claims it does.mohamedaminetebini commentedon Sep 22, 2026
I can reproduce a related variant on the Linux desktop app with a local/shared checkout.
Expected: once the conversation is associated with the newly created branch, the historical merged PR indicator should disappear.
Actual: the old merged PR stays attached to the new conversation/branch.
This appears to be stale terminal PR state retained across a shared-checkout branch change, rather than a rendering-only issue.
Environment: Linux desktop app, local/shared checkout, GitHub repository, [provider/version if known].
Fastdesigner commentedon Oct 2, 2026
Additional current reproduction on iPad in a Safari Home Screen web app, accessing T3 over Tailscale:
worker.worker, basemain.workerstill display the red New application architecture #9 badge. One affected thread concerns generating/distributing a Font Awesome subset, rather than the old PR's UI-kit changes.This means the manual unlink workaround described in #7517 / #11087 is not available for this observed association. The screenshots supplied by the reporter show the badge and that menu together. Installed T3 version has not yet been captured.
A source check of current main shows
usePullRequestLinking.isLinked()inspecting explicitpullRequestsentries (or legacylinkedPullRequest), rather than the automatically detectedbranchPullRequest. That appears consistent with this menu/badge mismatch, but the installed build's actual matching path has not been traced.Please cover closed-without-merge PRs as well as merged PRs in the fix. A user should be able to dismiss an automatically detected historical branch PR durably, without explicitly linking unrelated work first, deleting the thread, or abandoning the integration branch. A badge that is already displayed should not leave the user with only “Link” and no route to remove the association.
shivamhwp commentedon Oct 5, 2026
Still happens on current main (1e2ecbd, orchestration V2). I reproduced it in the web app with a clone of this repo on
fix/cursor-usage-cache-savings. That branch still exists here at the head of merged #13731. While the branch sits on that commit, the thread shows #13731, which is correct. After one more commit on the branch, the thread and the git panel still show #13731.Recordings and a fix are in #16006. A merged or closed PR stops being the branch's PR once the branch tip moves past the PR's head commit. GitHub's
headRefOidcomes from thegh pr listcall T3 already makes.MRNAQA commentedon Oct 5, 2026
Another case of this one, on desktop 0.0.45 (macOS, local checkout, Claude provider). This time the PR was closed without a merge.
Setup: long-lived integration branch
development. The default branch ismain. Before this, no PR had ever useddevelopmentas its head branch.Timeline (UTC, 2026-10-05, from
orchestration_events):development → mainby mistakethread.meta-updatedsetbranchPullRequestto that PR on 9 threads ondevelopmentdevelopmentgets it when it is createdResult: all 62 active local-checkout threads on
developmentshow the closed PR. That includes threads created hours after it closed and unrelated to it. One thread linked the PR explicitly through the agent. That link is correct.The branch tip has moved since then: PR
headRefOidis310f0bf…anddevelopmentis now24f2010…. So the head-OID check in #16006 should drop it. Two notes:ThreadPullRequestService, a lookup that returns no PR keeps a stored closed or mergedbranchPullRequestfor local-checkout threads. That is why the badge survives. fix(git): a long-lived branch stops showing an old merged PR once it moves on #16006 says it handles this. I mention it only so this case stays covered by a test.fix(server): drop a terminal PR once its branch moves past it
fix(server): drop a terminal PR once its branch moves past it
fix(server): drop a terminal PR once its branch moves past it
juliusmarminge commentedon Oct 21, 2026
This overlaps #14858: a reused branch retains a historical merged PR after its tip advances. This report predates that one. No runtime reproduction completed; triage stopped at the duplicate check.