Repository navigation
Description
Correction (see comment below): the multiple-PRs-on-one-branch case described in point 1 is already handled correctly today. The real problem is the cross-branch case, and the "smallest useful scope" below is superseded.
Before submitting
- I searched existing issues and did not find a duplicate.
- I am describing a concrete problem or use case, not just a vague idea.
Area
packages/contracts or packages/shared
Problem or use case
A thread's PR association is derived from the branch its worktree currently points at, and collapses to at most one PR. A thread that produces more than one PR therefore can't settle correctly.
Concretely, resolveThreadPr (apps/web/src/components/ThreadStatusIndicators.tsx:113) returns gitStatus.pr only when gitStatus.refName === thread.branch, and gitStatus.pr is a single nullable value (packages/contracts/src/git.ts:227). The server does fetch up to 20 PRs for the branch's head selectors but then collapses them — latest open wins, else most recently updated (apps/server/src/git/GitManager.ts:1291-1320). effectiveSettled consumes the result as one changeRequestState (packages/client-runtime/src/state/threadSettled.ts:230-281).
Two ways this bites:
- Several PRs off one branch (stacked/split work). Only one is ever considered. The others are invisible to settle — their merge or close changes nothing, and their being open blocks nothing.
- Sequential PRs across branches. Once the thread's worktree moves to a new branch, the earlier PR stops matching
refNameand disappears from the settle logic entirely. The thread silently falls back to the inactivity timer, so it can auto-settle with real PRs still open.
I think this is the same root cause behind several open issues: #4970 (reused branch stays associated with a historical merged PR), #4752 (settled thread unsettles and loses its merged PR# when switching back to main), #5514 (a release PR retroactively adopts every thread on the shared branch), and arguably #5476. Each is a symptom of "PR association is incidental, via current branch" rather than a durable thread↔PR link.
Proposed solution
Treat a thread's change requests as a set rather than a single value, and settle on the aggregate.
- Widen the settle input from
changeRequestState: ChangeRequestStateLike | nullto a collection of linked PR states. - Settle rule: settled if at least one PR is linked and every linked PR is merged or closed. Any open PR keeps the thread active — which generalizes the current "an open PR blocks the inactivity path entirely" rule rather than replacing it. No linked PRs falls back to the existing
autoSettleAfterDaysbehavior, unchanged. - Longer term, link PRs to the thread durably (a thread-scoped set of PR numbers, accumulated as PRs are observed on branches the thread actually worked on) instead of re-deriving from the current branch each status poll. That is what fixes the cross-branch case and the issues listed above.
Why this matters
Auto-settle is the mechanism that keeps the inbox trustworthy. Both failure modes break that trust in the more dangerous direction: a thread with unmerged work still open disappears from the inbox on the inactivity timer, because the PR that should have held it active wasn't the one PR the branch lookup happened to pick. The inverse also happens — work that is fully merged stays active because the merged PRs aren't the selected one.
Anyone whose workflow splits a task into more than one PR (stacked PRs, or a long-running thread that ships in stages) currently gets settle behavior that is effectively arbitrary.
Smallest useful scope
A first pass does not need the durable link at all. findLatestPrForHeadContext already retrieves every PR for the head selectors and discards all but one at apps/server/src/git/GitManager.ts:1313-1319. The narrow version:
- Surface all matching PRs instead of collapsing to one (keep the current single
prfield for display so nothing in the UI has to change). - Feed the aggregate state into
effectiveSettled, with the "all merged/closed → settled, any open → active" rule.
That fixes case 1 (multiple PRs on one branch) with a contract addition and a settle-rule change, no new persistence and no UI work. Case 2 (PRs across branches) needs the durable link and can be a separate, later change.
Alternatives considered
- Leave it and rely on manual settle. Workable, but the failure is silent — you don't notice the thread vanished from the inbox until you go looking for it.
- Make the single-PR pick smarter (e.g. prefer the newest open across all branches ever seen). Cheaper, but still wrong whenever two PRs are open at once, and it doesn't help the "all merged" case.
- Link PRs explicitly by user action. More precise, but adds UI and manual bookkeeping for something that can be inferred.
Risks or tradeoffs
- Behavior change on existing threads. A thread with several PRs on its branch that settles today (because the one selected PR merged) would stay active if a sibling PR is still open. That is the intended correction, but it is a visible change and might warrant being gated, especially given [Feature]: Separate toggle for merged/closed-PR auto-settle (enables fully manual settling) #4982 which asks for a toggle over this same auto-settle path.
ghcost. Aggregating states shouldn't add calls — the PRs are already fetched — but the durable-link follow-up would need care not to poll per linked PR.- Stale links. Without an unlink path, a durable set could accumulate PRs from a reused long-lived branch, which is exactly the failure in [Bug]: Reused long-lived branch remains associated with a historical merged PR #4970. The accumulation rule needs to be conservative.
- Cross-client surface. The settle rule is shared (
packages/client-runtime), so web, mobile, and the sidebar/home partitions all consume it; the contract change has to land consistently.
Examples or references
- Settle logic:
packages/client-runtime/src/state/threadSettled.ts:230-281 - PR collapse:
apps/server/src/git/GitManager.ts:1291-1320 - Branch-derived association:
apps/web/src/components/ThreadStatusIndicators.tsx:113-127 - Contract:
packages/contracts/src/git.ts:227 - Related: [Bug]: Reused long-lived branch remains associated with a historical merged PR #4970, [Bug]: Settled local thread unsettles and loses merged PR# when switching back to main for pull / new branch #4752, [Bug]: Opening a release PR (dev → main) retroactively adopts every thread on the shared branch — all settled threads wake up stamped with the new PR number #5514, [Bug]: Thread with an open PR auto-settles after every completed turn — even with background watchers still running (regression, started 2026-08-06) #5476, [Feature]: Separate toggle for merged/closed-PR auto-settle (enables fully manual settling) #4982
Contribution
- I would be open to helping implement this.
Activity
artieeg commentedon Aug 8, 2026
Correction to the original post — I got a key detail wrong, and it changes the scope of this request.
Multiple PRs on a single branch are already handled correctly. I claimed
findLatestPrForHeadContextcollapses the PR list in a way that loses sibling PRs. It does collapse, but it preserves the property that matters: it gathers every matching PR across all head selectors, sorts byupdatedAt, and returns the latest open one if any exists, falling back to the most recent otherwise (apps/server/src/git/GitManager.ts:1313-1319). Combined with the existing settle rule, that already yields "any open PR keeps the thread active; all merged/closed settles it." So the stacked-PRs-off-one-branch case in my original point 1 is not broken, and the "smallest useful scope" I proposed would have been close to a no-op. Apologies for the noise.The cross-branch case is still real, and I think it's the more important one. There is no persisted thread↔PR link anywhere:
OrchestrationThreadShellcarries no PR field, and the only memory is an in-processlastKnownPrfallback keyed by cwd + branch (apps/server/src/git/GitManager.ts:955-959). Association is re-derived every poll from the branch the worktree currently points at, gated onrefName === thread.branch(apps/web/src/components/ThreadStatusIndicators.tsx:122).So once a thread moves to a new branch, every PR it opened earlier becomes invisible to settle logic, and the thread silently falls back to the
autoSettleAfterDaystimer — it can auto-settle with unmerged work still open. A thread that ships in stages, or any thread whose agent switches branches mid-flight, gets settle behavior driven by whichever branch it happens to be sitting on.That framing makes this less a new capability and more the shared root cause under several existing bug reports:
Each is "PR association is incidental, via current branch" seen from a different angle. A durable link would address all of them together.
Revised scope. The narrow first pass I originally described isn't worth doing. The minimum that buys anything is the durable link itself: a thread-scoped record of PRs observed on branches that thread actually owned, projected into the thread shell, with a conservative accumulation rule so a reused branch cannot inherit stale PRs (#4970 is exactly that failure). Settle then reads the aggregate.
One implementation note that keeps it contained: if the aggregate is folded into a single
"open" | "closed" | "merged" | nullat the source,effectiveSettledand all its call sites need no signature change — the work stays server-side plus contracts.I'm still happy to implement this, but it is meaningfully larger than what I first suggested, and it touches persistence. Given CONTRIBUTING's stance on scope, I'd rather get a maintainer read on whether you want it at all — and whether the durable link is the shape you'd accept — before writing anything.
artieeg commentedon Aug 9, 2026
Closing this. After digging further, the scope I originally described was wrong in ways that make the issue more misleading than useful:
findLatestPrForHeadContextpreserves open-ness through its collapse).What remains is a narrow ordering problem — an older PR still open while the current branch's PR is merged or closed, letting inactivity auto-settle hide a thread with unmerged work. That is a much smaller claim than this issue is written around, and it overlaps #4752, #4970, and #5514. If it's worth filing, it deserves a focused issue with a concrete repro rather than this one.
Sorry for the churn, and thanks for the patience.