Skip to content

[Feature]: Settle a thread on all its linked PRs, not the one PR its current branch happens to resolve to #5717

Closed as not planned

Description

@artieeg

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:

  1. 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.
  2. Sequential PRs across branches. Once the thread's worktree moves to a new branch, the earlier PR stops matching refName and 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 | null to 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 autoSettleAfterDays behavior, 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:

  1. Surface all matching PRs instead of collapsing to one (keep the current single pr field for display so nothing in the UI has to change).
  2. 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.
  • gh cost. 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

Contribution

  • I would be open to helping implement this.

Activity

  1. added
    enhancementRequested improvement or new capability.
    needs-triageIssue needs maintainer review and initial categorization.
    on Aug 8, 2026
  2. artieeg commented on Aug 8, 2026

    @artieeg
    Author

    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 findLatestPrForHeadContext collapses 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 by updatedAt, 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: OrchestrationThreadShell carries no PR field, and the only memory is an in-process lastKnownPr fallback 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 on refName === 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 autoSettleAfterDays timer — 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" | null at the source, effectiveSettled and 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.

  3. artieeg commented on Aug 9, 2026

    @artieeg
    Author

    Closing this. After digging further, the scope I originally described was wrong in ways that make the issue more misleading than useful:

    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.

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

    enhancementRequested improvement or new capability.needs-triageIssue needs maintainer review and initial categorization.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions