Skip to content

[Bug]: Closed-PR threads auto-settle again after every completed turn #6417

Description

@Adamulek123

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

packages/client-runtime (affects apps/web, apps/desktop, and apps/mobile)

Steps to reproduce

  1. Open a T3 Code thread whose branch is associated with a PR that was closed without merging.
  2. If the thread is already in the Settled shelf, choose Un-settle.
  3. Send another message in that same thread to continue working.
  4. Observe that the thread is active while the turn is queued, starting, or running.
  5. Let the agent finish the turn.
  6. Observe the sidebar classification around completion.

This is deterministic whenever the branch's resolved PR state remains closed.

Expected behavior

Continuing work after a PR closes is real new activity. After the agent finishes, the thread should remain in the active list with its normal Done state. It should not jump into Settled merely because the branch still has a historical closed PR.

A merged PR may reasonably remain an unconditional terminal signal. A closed-but-unmerged PR is different: users commonly continue in the same thread to revise the approach, push more commits, or prepare to reopen/replace the PR.

Actual behavior

The thread is temporarily active while the agent runs, then classifies as settled again as soon as the running blocker disappears. Depending on render/subscription timing, this can look like the row bouncing between Settled and Working/Done.

Manually un-settling does not provide a durable workaround: sending the next message intentionally clears the "active" override, so the same closed PR wins again after every completed turn.

Disabling Auto-settle inactive threads does not help. The closed-PR short-circuit runs before the inactivity setting.

Impact

Major degradation or frequent failure

This affects every follow-up turn on a thread whose resolved PR is closed. It makes the active list unreliable and requires repeatedly finding and un-settling the same thread.

Version or commit

main @ 1e59b4c4004ce3c724d09ca0b140ed4523758d1e (inspected 2026-08-13)

Environment

Observed in T3 Code on Windows. The classification is shared client-runtime logic, so the same behavior applies to:

  • web
  • desktop (web shell)
  • mobile's Home and thread navigation lists

The bug is provider-independent once VCS status reports the PR as closed.

State-transition data

The relevant state sequence is:

Moment settledOverride Session/turn blocker PR state Effective result
User chooses Un-settle active none closed active
User sends next message reset to null queued closed active
Agent starts/runs null starting/running closed active
Agent completes null none closed settled

That result follows directly from two independent rules:

  1. apps/server/src/orchestration/decider.ts clears any settle override when a new user message is sent, including the explicit "active" keep-active override.
  2. packages/client-runtime/src/state/threadSettled.ts returns true unconditionally for changeRequestState === "merged" || changeRequestState === "closed" after activity blockers disappear.

The existing unit tests explicitly assert that closed PRs behave like merged PRs and settle immediately even when inactivity auto-settle is disabled.

Additional implementation findings

  • The server's Git status lookup searches PRs in all states. It prefers an open PR; when none exists, it returns the most recently updated closed or merged PR for the branch. That is useful for showing historical PR status and should not necessarily be removed.
  • Sidebar rows report only the PR's state to the parent partition.
  • The VCS status contract currently carries open | closed | merged, but no reliable closedAt.
  • A separate PR detail contract has closure timestamps, but the sidebar classification does not use that path, and closure-time availability differs across providers.
  • updatedAt is not a reliable substitute for closedAt, since comments or metadata edits can update a PR after closure.

Suggested fix

The smallest predictable behavior change is:

  • Merged PR: continue to auto-settle immediately.
  • Open PR: continue to block inactivity auto-settle.
  • Closed PR: use the ordinary thread rules—explicit override first, then the configured inactivity threshold.

In practice, remove closed from the unconditional terminal branch in effectiveSettled. Keep historical closed PR association for badges and navigation.

A more nuanced rule—auto-settle at closure but stay active after post-close work—would require transporting a reliable closure timestamp through the VCS status contract for every source-control provider. That is a larger contracts/server/web/mobile change and should not use updatedAt as a proxy.

Acceptance criteria

  • A fresh completed turn on a closed-PR thread stays active and shows Done.
  • The row does not transition through the Settled shelf at turn completion.
  • Closed-PR threads still auto-settle after the configured inactivity window.
  • Disabling inactivity auto-settle keeps closed-PR threads active until explicitly settled.
  • Merged PRs continue to settle immediately.
  • Open PRs continue to block inactivity settlement.
  • Queued, starting, running, approval, and user-input states remain settlement blockers.
  • Explicit manual settle/unsettle behavior remains intact.
  • Web/desktop sidebar, chat settled banner/action menu, and both mobile list entry points agree.
  • Settings copy and shared truth-table tests are updated.

Relevant code

  • packages/client-runtime/src/state/threadSettled.ts — shared effectiveSettled predicate
  • packages/client-runtime/src/state/threadSettled.test.ts — current merged/closed truth table
  • apps/server/src/orchestration/decider.ts — activity clears the settle override
  • apps/server/src/git/GitManager.ts — latest PR lookup includes terminal PRs
  • packages/contracts/src/git.ts — VCS status exposes state but no closure timestamp
  • apps/web/src/components/Sidebar.tsx — web/desktop PR-state reporting and partition
  • apps/web/src/components/ChatView.tsx — open-thread settled banner classification
  • apps/mobile/src/features/threads/threadListV2.ts — shared mobile partition
  • apps/mobile/src/features/home/HomeScreen.tsx and ThreadNavigationSidebar.tsx — mobile entry points
  • apps/web/src/components/settings/SettingsPanels.tsx — current “merged or closed PRs always settle” copy

Related issues

Workaround

Manually un-settle the thread again after every completed turn, or move the follow-up work to a new branch/thread without a closed PR association. Disabling inactivity auto-settle alone does not work.

Activity

  1. bman654 commented on Aug 13, 2026

    @bman654

    the forced auto-settle feature is frustrating. It is compounded by the re-settle on every turn.

    My workaround: pin every thread I have so that it stays in the active thread list even if it is settled, but then that moves it in the list to a place I don't necessarily want it. Trade-offs everywhere.

  2. andrewfree commented on Aug 14, 2026

    @andrewfree

    I have a live confirmation of this exact state transition from a real Codex-backed thread.

    Environment:

    • T3 v0.0.34-nightly.20260814.1090
    • macOS desktop connected to an Ubuntu remote environment
    • the thread's branch resolves to a PR that is closed and not merged
    • the user-visible task was still unfinished; a separately supervised QEMU job and its safety guard remained active

    Read-only inspection of T3's projection/event database showed:

    1. The long turn completed at 2026-08-14T11:59:31Z, after which the UI placed the thread in Settled.
    2. There was no thread.settled event and no thread.archived event. The thread had archived_at = NULL, settled_override = NULL, and settled_at = NULL. This was derived auto-settlement, not an archive or explicit settle.
    3. The user explicitly un-settled it at 12:11:37Z (thread.unsettled, reason user).
    4. Sending the next message at 12:11:49Z produced thread.unsettled, reason activity, which cleared the keep-active override back to neutral.
    5. That turn completed at 12:12:38Z; the UI immediately classified the thread as Settled again.
    6. The same sequence began a second time: explicit un-settle at 12:14:33Z, then new activity at 12:14:42Z.

    That matches the transition table in this issue exactly: explicit Un-settle temporarily sets "active", real activity clears it to null, the running session blocks settlement only while the turn is active, and completion exposes the still-closed PR rule again.

    One correction to the initial user-facing diagnosis: Codex get_goal returned null, but that was not causal. T3's effectiveSettled path does not consume Codex goal state. The final response correlated with the move only because turn completion removed the running-session blocker. Also, the thread was never actually archived; “Settled” simply felt like archival because it left the active list.

    This is therefore a concrete production reproduction of #6417, not a missing-goal issue. A second, separate concern is that user-visible background work can remain live after the parent turn completes, but the closed-PR rule alone is sufficient to explain this reproduction.

  3. maslinedwin commented on Aug 16, 2026

    @maslinedwin
    Contributor

    Fix is in #7178. Closed-but-unmerged PRs no longer auto-settle after every turn; merged PRs still do.

  4. jarrodwatts commented on Aug 19, 2026

    @jarrodwatts

    I have auto-settle turned off and they still auto-settle after every turn. Every new thread I create attaches itself to an already merged PR, so every new thread is auto settle after the first turn even with this setting off

  5. andrewfree commented on Aug 19, 2026

    @andrewfree

    I have auto-settle turned off and they still auto-settle after every turn. Every new thread I create attaches itself to an already merged PR, so every new thread is auto settle after the first turn even with this setting off

    Are you on the latest nightly? You didn't include the build version you are having issues with or repeatable conditions.

  6. mattholla commented on Aug 23, 2026

    @mattholla

    The live confirmation andrewfree posted earlier shows the whole wipe sequence: explicit un-settle writes the override, the automatic activity un-settle clears it. I traced where that happens in the code and have a fix.

    The reducer handles every unsettle event with:

    settledOverride: payload.reason === "user" ? "active" : null

    so any reason: "activity" event wipes a user's pin back to null, and the merged/closed-PR check settles the thread again when the turn completes. The part that makes it unavoidable: the decider only emits the activity un-settle when settledOverride !== null, so clicking Un-settle creates the exact state that triggers the event that erases it. An explicit Un-settle can never survive past one turn.

    The fix I went with: emit the automatic activity un-settle only when the override is "settled", so a user's "active" pin stays until the user settles the thread themselves. That is the same small guard at the three emit sites in apps/server/src/orchestration/decider.ts, and the apps/server test suite passes with the change. PR coming right after this comment.

    Repro on 0.0.33 stable, macOS 26.5, desktop app: take a thread whose most recent branch PR is merged with no newer PR open, click Un-settle, send any message. The badge re-settles when the turn completes. In orchestration_events I can watch the user pin written at 15:21:10Z and wiped by the activity event at 15:21:16Z.

    Fix is up as #8015.

  7. DmitriyAlergant commented on Aug 24, 2026

    @DmitriyAlergant

    Fix is in #7178. Closed-but-unmerged PRs no longer auto-settle after every turn; merged PRs still do.

    But why? At the very least, this should be a configurable behavior.

    There are those who work in non-standard git flows, e.g. maintaining one long-running branch (e.g. "dev") which sees many PRs from it. Having one PR merged from the current branch history does not necessarily mean each subsequent turn should continue auto-settling the thread. This is just wrong, and very annoying.

  8. TheMarstonConnell commented on Aug 29, 2026

    @TheMarstonConnell

    It should be a configurable setting imo. Sometimes I leave the main checkout on a PR that closed because I forget and it settles every message I get back

  9. juliusmarminge commented on Sep 1, 2026

    @juliusmarminge
    Member

    Reopening: #8600 fixes the routine re-settlement, but still uses the PR's updatedAt. A comment or metadata edit after resumed work can settle a closed-PR thread again, even with inactivity auto-settle disabled. That gap is explicitly covered by this report. My initial closure treated this partial fix as complete.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions