Merged PR is dropped once its remote branch is deleted, un-settling the thread #7653

Description

@LouisHaftmann

What happened

From the user:

I work remotely over Tailscale through the desktop app and sometimes after updating the desktop app many of the previously settled threads just come back. I checked the web app of the backend t3 server I'm using remotely over the desktop app and it also shows the threads as unsettled.

Follow-up answers: every affected thread had settled because its PR merged, never by clicking Settle. The threads stay wrong for a long time, and opening one does not fix it.

Diagnosis

The settle lifecycle in the database is intact. Across 715,331 orchestration events there are 38 thread.unsettled events total, and for all 774 threads carrying settled_override = 'settled' the projection agrees with the last settle-related event in the log. Nothing un-settled anything.

The threads that come back are the ones with no override, which stay settled only while the server reports a merged or closed PR. They lose that PR permanently, and the reason is branch cleanup after merge.

  1. The repo has "Automatically delete head branches" enabled, so merging deletes the head branch on GitHub. Once anything prunes the stale remote-tracking ref locally, refs/remotes/*/<branch> is gone and @{u} no longer resolves. T3 does not prune (its background fetch is git fetch --quiet <remote>, apps/server/src/vcs/GitVcsDriverCore.ts:2909), but VS Code auto-fetch, git fetch --prune, and gh pr merge --delete-branch all do.

  2. isUnpublishedBranch (apps/server/src/git/GitManager.ts:1277) decides whether a branch was ever pushed using exactly that ref: the repo tracks some remote, but no refs/remotes/*/<branch> exists. A merged and cleaned-up branch is indistinguishable from a branch that was never pushed.

  3. GitManager.ts:974 then short-circuits the PR lookup and returns { latest: null }. gh is never called, so the merged PR that gh pr list --head <branch> --state all still returns is never seen.

  4. effectiveSettled (packages/client-runtime/src/state/threadSettled.ts:300) receives changeRequest = null, changeRequestAutoSettles returns false, and the thread falls through to the inactivity rule. With the default sidebarAutoSettleAfterDays = 3 it sits in Active until three days of silence pass.

Because this is derived from server data, the web app and the desktop app agree, which is what the user saw.

The tie to updating the desktop app is indirect. No orchestration event fires when the PR vanishes, so a connected client keeps rendering its last good thread list. A restart refetches and the whole backlog appears at once, and an app update is a restart.

On this machine 24 of the 35 threads that depend on PR state have no remote-tracking ref and no upstream, so they are un-settled right now. The 11 healthy ones are recent merges whose stale ref has not been pruned yet.

Steps to reproduce

  1. Use a repo with "Automatically delete head branches" enabled.
  2. Start a thread on its own worktree branch, push it, open a PR.
  3. Merge the PR. The thread auto-settles and shows the merged PR badge.
  4. Prune the deleted remote branch locally, for example git fetch --prune in that checkout. VS Code auto-fetch with git.pruneOnFetch does this on its own.
  5. Wait out PR_LOOKUP_CACHE_TTL (2 minutes), then load the thread list in a freshly started client.

Expected: the thread stays settled and keeps its merged PR badge.

Actual: the PR badge is gone, the thread is back in Active, and it stays there until the 3 day inactivity rule catches it. Clicking into the thread does not restore it.

Version

0.0.34-nightly.20260820.1141 (diagnosed against main at f708f63)

Environment

Linux x64, Node v26.6.0. Server runs as a systemd user service (npm exec t3@nightly server --host <tailscale-ip>), desktop app connects to it remotely over Tailscale. GitHub CLI as the source control provider.

Evidence

Branch whose PR merged and whose head branch GitHub deleted:
$ git for-each-ref --count=1 refs/remotes/*/<branch>
(empty) -> isUnpublishedBranch: true
$ git for-each-ref --count=1 refs/remotes | wc -l
86 -> repo tracks remotes
$ git rev-parse --abbrev-ref --symbolic-full-name @{u}
fatal -> details.upstreamRef: null
$ gh pr list --head <branch> --state all --json number,state
[{"number":<n>,"state":"MERGED"}] -> the PR is still there
Scope on this machine (threads not deleted, not archived, no settle override,
activity in the last 4 days):
35 threads depend on PR state
24 have no remote-tracking ref and no upstream -> PR lookup short-circuits
Settle lifecycle is clean, so nothing wrote this state:
sqlite> select count(*) from orchestration_events; 715331
sqlite> select count(*) from orchestration_events
where event_type = 'thread.unsettled'; 38
774 threads with settled_override='settled', all matching the event log
Unrelated but visible in the same window, cold-start PR lookups failing with
an empty in-memory fallback map (GitManager.ts:1003), 75s after service start:
08:03:32 WARN: PR lookup failed; keeping last known PR state.
operation: lookupStatusPr
branch: <redacted>
providerOperation: listChangeRequests
providerCommand: gh
errorDetail: GitHub CLI command failed.

Related issues

#4752 (closed) and #4831 (closed) are the same family, a thread losing a merged PR, but neither covers this trigger. #4752 is the local checkout switching back to the default branch, which is suppressed by the isDefaultBranch rule at GitManager.ts:1067. #4831 is projection_threads.branch going stale after the agent renames the branch. Here the worktree stays on its own branch and the stored branch is correct, and the suppression comes from isUnpublishedBranch. #4970, #5514 and #7517 all point at the same weak thread-to-PR binding by branch name.

Fix applied or workaround

Nothing was written on the machine. The database was read only.

Workaround: click Settle on the affected threads once. effectiveSettled checks the explicit override at threadSettled.ts:328 before it looks at the PR, so the override survives this. Auto-settle on merge stays broken for future merges.

Filed by

Claude Opus 5 via t3 triage (Claude Code)

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

      , 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
       blocks
      (function() {
      function addCopyButtons() {
      document.querySelectorAll('pre code').forEach(function(codeBlock) {
      if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
      codeBlock.parentElement.setAttribute('data-copy-added', 'true');
      var btn = document.createElement('button');
      btn.textContent = 'Copy';
      btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
      btn.onmouseover = function() { this.style.opacity = '1'; };
      btn.onmouseout = function() { this.style.opacity = '0.7'; };
      btn.onclick = function() {
      navigator.clipboard.writeText(codeBlock.textContent).then(function() {
      btn.textContent = 'Copied!';
      setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
      });
      };
      codeBlock.parentElement.style.position = 'relative';
      codeBlock.parentElement.appendChild(btn);
      });
      }
      addCopyButtons();
      // Re-run on dynamic content
      var observer = new MutationObserver(addCopyButtons);
      observer.observe(document.body, { childList: true, subtree: true });
      })();
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      Merged PR is dropped once its remote branch is deleted, un-settling the thread #7653

      Description

      @LouisHaftmann

      What happened

      From the user:

      I work remotely over Tailscale through the desktop app and sometimes after updating the desktop app many of the previously settled threads just come back. I checked the web app of the backend t3 server I'm using remotely over the desktop app and it also shows the threads as unsettled.

      Follow-up answers: every affected thread had settled because its PR merged, never by clicking Settle. The threads stay wrong for a long time, and opening one does not fix it.

      Diagnosis

      The settle lifecycle in the database is intact. Across 715,331 orchestration events there are 38 thread.unsettled events total, and for all 774 threads carrying settled_override = 'settled' the projection agrees with the last settle-related event in the log. Nothing un-settled anything.

      The threads that come back are the ones with no override, which stay settled only while the server reports a merged or closed PR. They lose that PR permanently, and the reason is branch cleanup after merge.

      1. The repo has "Automatically delete head branches" enabled, so merging deletes the head branch on GitHub. Once anything prunes the stale remote-tracking ref locally, refs/remotes/*/<branch> is gone and @{u} no longer resolves. T3 does not prune (its background fetch is git fetch --quiet <remote>, apps/server/src/vcs/GitVcsDriverCore.ts:2909), but VS Code auto-fetch, git fetch --prune, and gh pr merge --delete-branch all do.

      2. isUnpublishedBranch (apps/server/src/git/GitManager.ts:1277) decides whether a branch was ever pushed using exactly that ref: the repo tracks some remote, but no refs/remotes/*/<branch> exists. A merged and cleaned-up branch is indistinguishable from a branch that was never pushed.

      3. GitManager.ts:974 then short-circuits the PR lookup and returns { latest: null }. gh is never called, so the merged PR that gh pr list --head <branch> --state all still returns is never seen.

      4. effectiveSettled (packages/client-runtime/src/state/threadSettled.ts:300) receives changeRequest = null, changeRequestAutoSettles returns false, and the thread falls through to the inactivity rule. With the default sidebarAutoSettleAfterDays = 3 it sits in Active until three days of silence pass.

      Because this is derived from server data, the web app and the desktop app agree, which is what the user saw.

      The tie to updating the desktop app is indirect. No orchestration event fires when the PR vanishes, so a connected client keeps rendering its last good thread list. A restart refetches and the whole backlog appears at once, and an app update is a restart.

      On this machine 24 of the 35 threads that depend on PR state have no remote-tracking ref and no upstream, so they are un-settled right now. The 11 healthy ones are recent merges whose stale ref has not been pruned yet.

      Steps to reproduce

      1. Use a repo with "Automatically delete head branches" enabled.
      2. Start a thread on its own worktree branch, push it, open a PR.
      3. Merge the PR. The thread auto-settles and shows the merged PR badge.
      4. Prune the deleted remote branch locally, for example git fetch --prune in that checkout. VS Code auto-fetch with git.pruneOnFetch does this on its own.
      5. Wait out PR_LOOKUP_CACHE_TTL (2 minutes), then load the thread list in a freshly started client.

      Expected: the thread stays settled and keeps its merged PR badge.

      Actual: the PR badge is gone, the thread is back in Active, and it stays there until the 3 day inactivity rule catches it. Clicking into the thread does not restore it.

      Version

      0.0.34-nightly.20260820.1141 (diagnosed against main at f708f63)

      Environment

      Linux x64, Node v26.6.0. Server runs as a systemd user service (npm exec t3@nightly server --host <tailscale-ip>), desktop app connects to it remotely over Tailscale. GitHub CLI as the source control provider.

      Evidence

      Branch whose PR merged and whose head branch GitHub deleted:
      $ git for-each-ref --count=1 refs/remotes/*/<branch>
      (empty) -> isUnpublishedBranch: true
      $ git for-each-ref --count=1 refs/remotes | wc -l
      86 -> repo tracks remotes
      $ git rev-parse --abbrev-ref --symbolic-full-name @{u}
      fatal -> details.upstreamRef: null
      $ gh pr list --head <branch> --state all --json number,state
      [{"number":<n>,"state":"MERGED"}] -> the PR is still there
      Scope on this machine (threads not deleted, not archived, no settle override,
      activity in the last 4 days):
      35 threads depend on PR state
      24 have no remote-tracking ref and no upstream -> PR lookup short-circuits
      Settle lifecycle is clean, so nothing wrote this state:
      sqlite> select count(*) from orchestration_events; 715331
      sqlite> select count(*) from orchestration_events
      where event_type = 'thread.unsettled'; 38
      774 threads with settled_override='settled', all matching the event log
      Unrelated but visible in the same window, cold-start PR lookups failing with
      an empty in-memory fallback map (GitManager.ts:1003), 75s after service start:
      08:03:32 WARN: PR lookup failed; keeping last known PR state.
      operation: lookupStatusPr
      branch: <redacted>
      providerOperation: listChangeRequests
      providerCommand: gh
      errorDetail: GitHub CLI command failed.
      

      Related issues

      #4752 (closed) and #4831 (closed) are the same family, a thread losing a merged PR, but neither covers this trigger. #4752 is the local checkout switching back to the default branch, which is suppressed by the isDefaultBranch rule at GitManager.ts:1067. #4831 is projection_threads.branch going stale after the agent renames the branch. Here the worktree stays on its own branch and the stored branch is correct, and the suppression comes from isUnpublishedBranch. #4970, #5514 and #7517 all point at the same weak thread-to-PR binding by branch name.

      Fix applied or workaround

      Nothing was written on the machine. The database was read only.

      Workaround: click Settle on the affected threads once. effectiveSettled checks the explicit override at threadSettled.ts:328 before it looks at the PR, so the override survives this. Auto-settle on merge stays broken for future merges.

      Filed by

      Claude Opus 5 via t3 triage (Claude Code)

      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

          , 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          Merged PR is dropped once its remote branch is deleted, un-settling the thread #7653

          Description

          @LouisHaftmann

          What happened

          From the user:

          I work remotely over Tailscale through the desktop app and sometimes after updating the desktop app many of the previously settled threads just come back. I checked the web app of the backend t3 server I'm using remotely over the desktop app and it also shows the threads as unsettled.

          Follow-up answers: every affected thread had settled because its PR merged, never by clicking Settle. The threads stay wrong for a long time, and opening one does not fix it.

          Diagnosis

          The settle lifecycle in the database is intact. Across 715,331 orchestration events there are 38 thread.unsettled events total, and for all 774 threads carrying settled_override = 'settled' the projection agrees with the last settle-related event in the log. Nothing un-settled anything.

          The threads that come back are the ones with no override, which stay settled only while the server reports a merged or closed PR. They lose that PR permanently, and the reason is branch cleanup after merge.

          1. The repo has "Automatically delete head branches" enabled, so merging deletes the head branch on GitHub. Once anything prunes the stale remote-tracking ref locally, refs/remotes/*/<branch> is gone and @{u} no longer resolves. T3 does not prune (its background fetch is git fetch --quiet <remote>, apps/server/src/vcs/GitVcsDriverCore.ts:2909), but VS Code auto-fetch, git fetch --prune, and gh pr merge --delete-branch all do.

          2. isUnpublishedBranch (apps/server/src/git/GitManager.ts:1277) decides whether a branch was ever pushed using exactly that ref: the repo tracks some remote, but no refs/remotes/*/<branch> exists. A merged and cleaned-up branch is indistinguishable from a branch that was never pushed.

          3. GitManager.ts:974 then short-circuits the PR lookup and returns { latest: null }. gh is never called, so the merged PR that gh pr list --head <branch> --state all still returns is never seen.

          4. effectiveSettled (packages/client-runtime/src/state/threadSettled.ts:300) receives changeRequest = null, changeRequestAutoSettles returns false, and the thread falls through to the inactivity rule. With the default sidebarAutoSettleAfterDays = 3 it sits in Active until three days of silence pass.

          Because this is derived from server data, the web app and the desktop app agree, which is what the user saw.

          The tie to updating the desktop app is indirect. No orchestration event fires when the PR vanishes, so a connected client keeps rendering its last good thread list. A restart refetches and the whole backlog appears at once, and an app update is a restart.

          On this machine 24 of the 35 threads that depend on PR state have no remote-tracking ref and no upstream, so they are un-settled right now. The 11 healthy ones are recent merges whose stale ref has not been pruned yet.

          Steps to reproduce

          1. Use a repo with "Automatically delete head branches" enabled.
          2. Start a thread on its own worktree branch, push it, open a PR.
          3. Merge the PR. The thread auto-settles and shows the merged PR badge.
          4. Prune the deleted remote branch locally, for example git fetch --prune in that checkout. VS Code auto-fetch with git.pruneOnFetch does this on its own.
          5. Wait out PR_LOOKUP_CACHE_TTL (2 minutes), then load the thread list in a freshly started client.

          Expected: the thread stays settled and keeps its merged PR badge.

          Actual: the PR badge is gone, the thread is back in Active, and it stays there until the 3 day inactivity rule catches it. Clicking into the thread does not restore it.

          Version

          0.0.34-nightly.20260820.1141 (diagnosed against main at f708f63)

          Environment

          Linux x64, Node v26.6.0. Server runs as a systemd user service (npm exec t3@nightly server --host <tailscale-ip>), desktop app connects to it remotely over Tailscale. GitHub CLI as the source control provider.

          Evidence

          Branch whose PR merged and whose head branch GitHub deleted:
          $ git for-each-ref --count=1 refs/remotes/*/<branch>
          (empty) -> isUnpublishedBranch: true
          $ git for-each-ref --count=1 refs/remotes | wc -l
          86 -> repo tracks remotes
          $ git rev-parse --abbrev-ref --symbolic-full-name @{u}
          fatal -> details.upstreamRef: null
          $ gh pr list --head <branch> --state all --json number,state
          [{"number":<n>,"state":"MERGED"}] -> the PR is still there
          Scope on this machine (threads not deleted, not archived, no settle override,
          activity in the last 4 days):
          35 threads depend on PR state
          24 have no remote-tracking ref and no upstream -> PR lookup short-circuits
          Settle lifecycle is clean, so nothing wrote this state:
          sqlite> select count(*) from orchestration_events; 715331
          sqlite> select count(*) from orchestration_events
          where event_type = 'thread.unsettled'; 38
          774 threads with settled_override='settled', all matching the event log
          Unrelated but visible in the same window, cold-start PR lookups failing with
          an empty in-memory fallback map (GitManager.ts:1003), 75s after service start:
          08:03:32 WARN: PR lookup failed; keeping last known PR state.
          operation: lookupStatusPr
          branch: <redacted>
          providerOperation: listChangeRequests
          providerCommand: gh
          errorDetail: GitHub CLI command failed.
          

          Related issues

          #4752 (closed) and #4831 (closed) are the same family, a thread losing a merged PR, but neither covers this trigger. #4752 is the local checkout switching back to the default branch, which is suppressed by the isDefaultBranch rule at GitManager.ts:1067. #4831 is projection_threads.branch going stale after the agent renames the branch. Here the worktree stays on its own branch and the stored branch is correct, and the suppression comes from isUnpublishedBranch. #4970, #5514 and #7517 all point at the same weak thread-to-PR binding by branch name.

          Fix applied or workaround

          Nothing was written on the machine. The database was read only.

          Workaround: click Settle on the affected threads once. effectiveSettled checks the explicit override at threadSettled.ts:328 before it looks at the PR, so the override survives this. Auto-settle on merge stays broken for future merges.

          Filed by

          Claude Opus 5 via t3 triage (Claude Code)

          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

              , 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              Merged PR is dropped once its remote branch is deleted, un-settling the thread #7653

              Description

              @LouisHaftmann

              What happened

              From the user:

              I work remotely over Tailscale through the desktop app and sometimes after updating the desktop app many of the previously settled threads just come back. I checked the web app of the backend t3 server I'm using remotely over the desktop app and it also shows the threads as unsettled.

              Follow-up answers: every affected thread had settled because its PR merged, never by clicking Settle. The threads stay wrong for a long time, and opening one does not fix it.

              Diagnosis

              The settle lifecycle in the database is intact. Across 715,331 orchestration events there are 38 thread.unsettled events total, and for all 774 threads carrying settled_override = 'settled' the projection agrees with the last settle-related event in the log. Nothing un-settled anything.

              The threads that come back are the ones with no override, which stay settled only while the server reports a merged or closed PR. They lose that PR permanently, and the reason is branch cleanup after merge.

              1. The repo has "Automatically delete head branches" enabled, so merging deletes the head branch on GitHub. Once anything prunes the stale remote-tracking ref locally, refs/remotes/*/<branch> is gone and @{u} no longer resolves. T3 does not prune (its background fetch is git fetch --quiet <remote>, apps/server/src/vcs/GitVcsDriverCore.ts:2909), but VS Code auto-fetch, git fetch --prune, and gh pr merge --delete-branch all do.

              2. isUnpublishedBranch (apps/server/src/git/GitManager.ts:1277) decides whether a branch was ever pushed using exactly that ref: the repo tracks some remote, but no refs/remotes/*/<branch> exists. A merged and cleaned-up branch is indistinguishable from a branch that was never pushed.

              3. GitManager.ts:974 then short-circuits the PR lookup and returns { latest: null }. gh is never called, so the merged PR that gh pr list --head <branch> --state all still returns is never seen.

              4. effectiveSettled (packages/client-runtime/src/state/threadSettled.ts:300) receives changeRequest = null, changeRequestAutoSettles returns false, and the thread falls through to the inactivity rule. With the default sidebarAutoSettleAfterDays = 3 it sits in Active until three days of silence pass.

              Because this is derived from server data, the web app and the desktop app agree, which is what the user saw.

              The tie to updating the desktop app is indirect. No orchestration event fires when the PR vanishes, so a connected client keeps rendering its last good thread list. A restart refetches and the whole backlog appears at once, and an app update is a restart.

              On this machine 24 of the 35 threads that depend on PR state have no remote-tracking ref and no upstream, so they are un-settled right now. The 11 healthy ones are recent merges whose stale ref has not been pruned yet.

              Steps to reproduce

              1. Use a repo with "Automatically delete head branches" enabled.
              2. Start a thread on its own worktree branch, push it, open a PR.
              3. Merge the PR. The thread auto-settles and shows the merged PR badge.
              4. Prune the deleted remote branch locally, for example git fetch --prune in that checkout. VS Code auto-fetch with git.pruneOnFetch does this on its own.
              5. Wait out PR_LOOKUP_CACHE_TTL (2 minutes), then load the thread list in a freshly started client.

              Expected: the thread stays settled and keeps its merged PR badge.

              Actual: the PR badge is gone, the thread is back in Active, and it stays there until the 3 day inactivity rule catches it. Clicking into the thread does not restore it.

              Version

              0.0.34-nightly.20260820.1141 (diagnosed against main at f708f63)

              Environment

              Linux x64, Node v26.6.0. Server runs as a systemd user service (npm exec t3@nightly server --host <tailscale-ip>), desktop app connects to it remotely over Tailscale. GitHub CLI as the source control provider.

              Evidence

              Branch whose PR merged and whose head branch GitHub deleted:
              $ git for-each-ref --count=1 refs/remotes/*/<branch>
              (empty) -> isUnpublishedBranch: true
              $ git for-each-ref --count=1 refs/remotes | wc -l
              86 -> repo tracks remotes
              $ git rev-parse --abbrev-ref --symbolic-full-name @{u}
              fatal -> details.upstreamRef: null
              $ gh pr list --head <branch> --state all --json number,state
              [{"number":<n>,"state":"MERGED"}] -> the PR is still there
              Scope on this machine (threads not deleted, not archived, no settle override,
              activity in the last 4 days):
              35 threads depend on PR state
              24 have no remote-tracking ref and no upstream -> PR lookup short-circuits
              Settle lifecycle is clean, so nothing wrote this state:
              sqlite> select count(*) from orchestration_events; 715331
              sqlite> select count(*) from orchestration_events
              where event_type = 'thread.unsettled'; 38
              774 threads with settled_override='settled', all matching the event log
              Unrelated but visible in the same window, cold-start PR lookups failing with
              an empty in-memory fallback map (GitManager.ts:1003), 75s after service start:
              08:03:32 WARN: PR lookup failed; keeping last known PR state.
              operation: lookupStatusPr
              branch: <redacted>
              providerOperation: listChangeRequests
              providerCommand: gh
              errorDetail: GitHub CLI command failed.
              

              Related issues

              #4752 (closed) and #4831 (closed) are the same family, a thread losing a merged PR, but neither covers this trigger. #4752 is the local checkout switching back to the default branch, which is suppressed by the isDefaultBranch rule at GitManager.ts:1067. #4831 is projection_threads.branch going stale after the agent renames the branch. Here the worktree stays on its own branch and the stored branch is correct, and the suppression comes from isUnpublishedBranch. #4970, #5514 and #7517 all point at the same weak thread-to-PR binding by branch name.

              Fix applied or workaround

              Nothing was written on the machine. The database was read only.

              Workaround: click Settle on the affected threads once. effectiveSettled checks the explicit override at threadSettled.ts:328 before it looks at the PR, so the override survives this. Auto-settle on merge stays broken for future merges.

              Filed by

              Claude Opus 5 via t3 triage (Claude Code)

              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

                  , 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  Merged PR is dropped once its remote branch is deleted, un-settling the thread #7653

                  Description

                  @LouisHaftmann

                  What happened

                  From the user:

                  I work remotely over Tailscale through the desktop app and sometimes after updating the desktop app many of the previously settled threads just come back. I checked the web app of the backend t3 server I'm using remotely over the desktop app and it also shows the threads as unsettled.

                  Follow-up answers: every affected thread had settled because its PR merged, never by clicking Settle. The threads stay wrong for a long time, and opening one does not fix it.

                  Diagnosis

                  The settle lifecycle in the database is intact. Across 715,331 orchestration events there are 38 thread.unsettled events total, and for all 774 threads carrying settled_override = 'settled' the projection agrees with the last settle-related event in the log. Nothing un-settled anything.

                  The threads that come back are the ones with no override, which stay settled only while the server reports a merged or closed PR. They lose that PR permanently, and the reason is branch cleanup after merge.

                  1. The repo has "Automatically delete head branches" enabled, so merging deletes the head branch on GitHub. Once anything prunes the stale remote-tracking ref locally, refs/remotes/*/<branch> is gone and @{u} no longer resolves. T3 does not prune (its background fetch is git fetch --quiet <remote>, apps/server/src/vcs/GitVcsDriverCore.ts:2909), but VS Code auto-fetch, git fetch --prune, and gh pr merge --delete-branch all do.

                  2. isUnpublishedBranch (apps/server/src/git/GitManager.ts:1277) decides whether a branch was ever pushed using exactly that ref: the repo tracks some remote, but no refs/remotes/*/<branch> exists. A merged and cleaned-up branch is indistinguishable from a branch that was never pushed.

                  3. GitManager.ts:974 then short-circuits the PR lookup and returns { latest: null }. gh is never called, so the merged PR that gh pr list --head <branch> --state all still returns is never seen.

                  4. effectiveSettled (packages/client-runtime/src/state/threadSettled.ts:300) receives changeRequest = null, changeRequestAutoSettles returns false, and the thread falls through to the inactivity rule. With the default sidebarAutoSettleAfterDays = 3 it sits in Active until three days of silence pass.

                  Because this is derived from server data, the web app and the desktop app agree, which is what the user saw.

                  The tie to updating the desktop app is indirect. No orchestration event fires when the PR vanishes, so a connected client keeps rendering its last good thread list. A restart refetches and the whole backlog appears at once, and an app update is a restart.

                  On this machine 24 of the 35 threads that depend on PR state have no remote-tracking ref and no upstream, so they are un-settled right now. The 11 healthy ones are recent merges whose stale ref has not been pruned yet.

                  Steps to reproduce

                  1. Use a repo with "Automatically delete head branches" enabled.
                  2. Start a thread on its own worktree branch, push it, open a PR.
                  3. Merge the PR. The thread auto-settles and shows the merged PR badge.
                  4. Prune the deleted remote branch locally, for example git fetch --prune in that checkout. VS Code auto-fetch with git.pruneOnFetch does this on its own.
                  5. Wait out PR_LOOKUP_CACHE_TTL (2 minutes), then load the thread list in a freshly started client.

                  Expected: the thread stays settled and keeps its merged PR badge.

                  Actual: the PR badge is gone, the thread is back in Active, and it stays there until the 3 day inactivity rule catches it. Clicking into the thread does not restore it.

                  Version

                  0.0.34-nightly.20260820.1141 (diagnosed against main at f708f63)

                  Environment

                  Linux x64, Node v26.6.0. Server runs as a systemd user service (npm exec t3@nightly server --host <tailscale-ip>), desktop app connects to it remotely over Tailscale. GitHub CLI as the source control provider.

                  Evidence

                  Branch whose PR merged and whose head branch GitHub deleted:
                  $ git for-each-ref --count=1 refs/remotes/*/<branch>
                  (empty) -> isUnpublishedBranch: true
                  $ git for-each-ref --count=1 refs/remotes | wc -l
                  86 -> repo tracks remotes
                  $ git rev-parse --abbrev-ref --symbolic-full-name @{u}
                  fatal -> details.upstreamRef: null
                  $ gh pr list --head <branch> --state all --json number,state
                  [{"number":<n>,"state":"MERGED"}] -> the PR is still there
                  Scope on this machine (threads not deleted, not archived, no settle override,
                  activity in the last 4 days):
                  35 threads depend on PR state
                  24 have no remote-tracking ref and no upstream -> PR lookup short-circuits
                  Settle lifecycle is clean, so nothing wrote this state:
                  sqlite> select count(*) from orchestration_events; 715331
                  sqlite> select count(*) from orchestration_events
                  where event_type = 'thread.unsettled'; 38
                  774 threads with settled_override='settled', all matching the event log
                  Unrelated but visible in the same window, cold-start PR lookups failing with
                  an empty in-memory fallback map (GitManager.ts:1003), 75s after service start:
                  08:03:32 WARN: PR lookup failed; keeping last known PR state.
                  operation: lookupStatusPr
                  branch: <redacted>
                  providerOperation: listChangeRequests
                  providerCommand: gh
                  errorDetail: GitHub CLI command failed.
                  

                  Related issues

                  #4752 (closed) and #4831 (closed) are the same family, a thread losing a merged PR, but neither covers this trigger. #4752 is the local checkout switching back to the default branch, which is suppressed by the isDefaultBranch rule at GitManager.ts:1067. #4831 is projection_threads.branch going stale after the agent renames the branch. Here the worktree stays on its own branch and the stored branch is correct, and the suppression comes from isUnpublishedBranch. #4970, #5514 and #7517 all point at the same weak thread-to-PR binding by branch name.

                  Fix applied or workaround

                  Nothing was written on the machine. The database was read only.

                  Workaround: click Settle on the affected threads once. effectiveSettled checks the explicit override at threadSettled.ts:328 before it looks at the PR, so the override survives this. Auto-settle on merge stays broken for future merges.

                  Filed by

                  Claude Opus 5 via t3 triage (Claude Code)

                  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

                      , 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      Merged PR is dropped once its remote branch is deleted, un-settling the thread #7653

                      Description

                      @LouisHaftmann

                      What happened

                      From the user:

                      I work remotely over Tailscale through the desktop app and sometimes after updating the desktop app many of the previously settled threads just come back. I checked the web app of the backend t3 server I'm using remotely over the desktop app and it also shows the threads as unsettled.

                      Follow-up answers: every affected thread had settled because its PR merged, never by clicking Settle. The threads stay wrong for a long time, and opening one does not fix it.

                      Diagnosis

                      The settle lifecycle in the database is intact. Across 715,331 orchestration events there are 38 thread.unsettled events total, and for all 774 threads carrying settled_override = 'settled' the projection agrees with the last settle-related event in the log. Nothing un-settled anything.

                      The threads that come back are the ones with no override, which stay settled only while the server reports a merged or closed PR. They lose that PR permanently, and the reason is branch cleanup after merge.

                      1. The repo has "Automatically delete head branches" enabled, so merging deletes the head branch on GitHub. Once anything prunes the stale remote-tracking ref locally, refs/remotes/*/<branch> is gone and @{u} no longer resolves. T3 does not prune (its background fetch is git fetch --quiet <remote>, apps/server/src/vcs/GitVcsDriverCore.ts:2909), but VS Code auto-fetch, git fetch --prune, and gh pr merge --delete-branch all do.

                      2. isUnpublishedBranch (apps/server/src/git/GitManager.ts:1277) decides whether a branch was ever pushed using exactly that ref: the repo tracks some remote, but no refs/remotes/*/<branch> exists. A merged and cleaned-up branch is indistinguishable from a branch that was never pushed.

                      3. GitManager.ts:974 then short-circuits the PR lookup and returns { latest: null }. gh is never called, so the merged PR that gh pr list --head <branch> --state all still returns is never seen.

                      4. effectiveSettled (packages/client-runtime/src/state/threadSettled.ts:300) receives changeRequest = null, changeRequestAutoSettles returns false, and the thread falls through to the inactivity rule. With the default sidebarAutoSettleAfterDays = 3 it sits in Active until three days of silence pass.

                      Because this is derived from server data, the web app and the desktop app agree, which is what the user saw.

                      The tie to updating the desktop app is indirect. No orchestration event fires when the PR vanishes, so a connected client keeps rendering its last good thread list. A restart refetches and the whole backlog appears at once, and an app update is a restart.

                      On this machine 24 of the 35 threads that depend on PR state have no remote-tracking ref and no upstream, so they are un-settled right now. The 11 healthy ones are recent merges whose stale ref has not been pruned yet.

                      Steps to reproduce

                      1. Use a repo with "Automatically delete head branches" enabled.
                      2. Start a thread on its own worktree branch, push it, open a PR.
                      3. Merge the PR. The thread auto-settles and shows the merged PR badge.
                      4. Prune the deleted remote branch locally, for example git fetch --prune in that checkout. VS Code auto-fetch with git.pruneOnFetch does this on its own.
                      5. Wait out PR_LOOKUP_CACHE_TTL (2 minutes), then load the thread list in a freshly started client.

                      Expected: the thread stays settled and keeps its merged PR badge.

                      Actual: the PR badge is gone, the thread is back in Active, and it stays there until the 3 day inactivity rule catches it. Clicking into the thread does not restore it.

                      Version

                      0.0.34-nightly.20260820.1141 (diagnosed against main at f708f63)

                      Environment

                      Linux x64, Node v26.6.0. Server runs as a systemd user service (npm exec t3@nightly server --host <tailscale-ip>), desktop app connects to it remotely over Tailscale. GitHub CLI as the source control provider.

                      Evidence

                      Branch whose PR merged and whose head branch GitHub deleted:
                      $ git for-each-ref --count=1 refs/remotes/*/<branch>
                      (empty) -> isUnpublishedBranch: true
                      $ git for-each-ref --count=1 refs/remotes | wc -l
                      86 -> repo tracks remotes
                      $ git rev-parse --abbrev-ref --symbolic-full-name @{u}
                      fatal -> details.upstreamRef: null
                      $ gh pr list --head <branch> --state all --json number,state
                      [{"number":<n>,"state":"MERGED"}] -> the PR is still there
                      Scope on this machine (threads not deleted, not archived, no settle override,
                      activity in the last 4 days):
                      35 threads depend on PR state
                      24 have no remote-tracking ref and no upstream -> PR lookup short-circuits
                      Settle lifecycle is clean, so nothing wrote this state:
                      sqlite> select count(*) from orchestration_events; 715331
                      sqlite> select count(*) from orchestration_events
                      where event_type = 'thread.unsettled'; 38
                      774 threads with settled_override='settled', all matching the event log
                      Unrelated but visible in the same window, cold-start PR lookups failing with
                      an empty in-memory fallback map (GitManager.ts:1003), 75s after service start:
                      08:03:32 WARN: PR lookup failed; keeping last known PR state.
                      operation: lookupStatusPr
                      branch: <redacted>
                      providerOperation: listChangeRequests
                      providerCommand: gh
                      errorDetail: GitHub CLI command failed.
                      

                      Related issues

                      #4752 (closed) and #4831 (closed) are the same family, a thread losing a merged PR, but neither covers this trigger. #4752 is the local checkout switching back to the default branch, which is suppressed by the isDefaultBranch rule at GitManager.ts:1067. #4831 is projection_threads.branch going stale after the agent renames the branch. Here the worktree stays on its own branch and the stored branch is correct, and the suppression comes from isUnpublishedBranch. #4970, #5514 and #7517 all point at the same weak thread-to-PR binding by branch name.

                      Fix applied or workaround

                      Nothing was written on the machine. The database was read only.

                      Workaround: click Settle on the affected threads once. effectiveSettled checks the explicit override at threadSettled.ts:328 before it looks at the PR, so the override survives this. Auto-settle on merge stays broken for future merges.

                      Filed by

                      Claude Opus 5 via t3 triage (Claude Code)

                      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

                          , 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          Merged PR is dropped once its remote branch is deleted, un-settling the thread #7653

                          Description

                          @LouisHaftmann

                          What happened

                          From the user:

                          I work remotely over Tailscale through the desktop app and sometimes after updating the desktop app many of the previously settled threads just come back. I checked the web app of the backend t3 server I'm using remotely over the desktop app and it also shows the threads as unsettled.

                          Follow-up answers: every affected thread had settled because its PR merged, never by clicking Settle. The threads stay wrong for a long time, and opening one does not fix it.

                          Diagnosis

                          The settle lifecycle in the database is intact. Across 715,331 orchestration events there are 38 thread.unsettled events total, and for all 774 threads carrying settled_override = 'settled' the projection agrees with the last settle-related event in the log. Nothing un-settled anything.

                          The threads that come back are the ones with no override, which stay settled only while the server reports a merged or closed PR. They lose that PR permanently, and the reason is branch cleanup after merge.

                          1. The repo has "Automatically delete head branches" enabled, so merging deletes the head branch on GitHub. Once anything prunes the stale remote-tracking ref locally, refs/remotes/*/<branch> is gone and @{u} no longer resolves. T3 does not prune (its background fetch is git fetch --quiet <remote>, apps/server/src/vcs/GitVcsDriverCore.ts:2909), but VS Code auto-fetch, git fetch --prune, and gh pr merge --delete-branch all do.

                          2. isUnpublishedBranch (apps/server/src/git/GitManager.ts:1277) decides whether a branch was ever pushed using exactly that ref: the repo tracks some remote, but no refs/remotes/*/<branch> exists. A merged and cleaned-up branch is indistinguishable from a branch that was never pushed.

                          3. GitManager.ts:974 then short-circuits the PR lookup and returns { latest: null }. gh is never called, so the merged PR that gh pr list --head <branch> --state all still returns is never seen.

                          4. effectiveSettled (packages/client-runtime/src/state/threadSettled.ts:300) receives changeRequest = null, changeRequestAutoSettles returns false, and the thread falls through to the inactivity rule. With the default sidebarAutoSettleAfterDays = 3 it sits in Active until three days of silence pass.

                          Because this is derived from server data, the web app and the desktop app agree, which is what the user saw.

                          The tie to updating the desktop app is indirect. No orchestration event fires when the PR vanishes, so a connected client keeps rendering its last good thread list. A restart refetches and the whole backlog appears at once, and an app update is a restart.

                          On this machine 24 of the 35 threads that depend on PR state have no remote-tracking ref and no upstream, so they are un-settled right now. The 11 healthy ones are recent merges whose stale ref has not been pruned yet.

                          Steps to reproduce

                          1. Use a repo with "Automatically delete head branches" enabled.
                          2. Start a thread on its own worktree branch, push it, open a PR.
                          3. Merge the PR. The thread auto-settles and shows the merged PR badge.
                          4. Prune the deleted remote branch locally, for example git fetch --prune in that checkout. VS Code auto-fetch with git.pruneOnFetch does this on its own.
                          5. Wait out PR_LOOKUP_CACHE_TTL (2 minutes), then load the thread list in a freshly started client.

                          Expected: the thread stays settled and keeps its merged PR badge.

                          Actual: the PR badge is gone, the thread is back in Active, and it stays there until the 3 day inactivity rule catches it. Clicking into the thread does not restore it.

                          Version

                          0.0.34-nightly.20260820.1141 (diagnosed against main at f708f63)

                          Environment

                          Linux x64, Node v26.6.0. Server runs as a systemd user service (npm exec t3@nightly server --host <tailscale-ip>), desktop app connects to it remotely over Tailscale. GitHub CLI as the source control provider.

                          Evidence

                          Branch whose PR merged and whose head branch GitHub deleted:
                          $ git for-each-ref --count=1 refs/remotes/*/<branch>
                          (empty) -> isUnpublishedBranch: true
                          $ git for-each-ref --count=1 refs/remotes | wc -l
                          86 -> repo tracks remotes
                          $ git rev-parse --abbrev-ref --symbolic-full-name @{u}
                          fatal -> details.upstreamRef: null
                          $ gh pr list --head <branch> --state all --json number,state
                          [{"number":<n>,"state":"MERGED"}] -> the PR is still there
                          Scope on this machine (threads not deleted, not archived, no settle override,
                          activity in the last 4 days):
                          35 threads depend on PR state
                          24 have no remote-tracking ref and no upstream -> PR lookup short-circuits
                          Settle lifecycle is clean, so nothing wrote this state:
                          sqlite> select count(*) from orchestration_events; 715331
                          sqlite> select count(*) from orchestration_events
                          where event_type = 'thread.unsettled'; 38
                          774 threads with settled_override='settled', all matching the event log
                          Unrelated but visible in the same window, cold-start PR lookups failing with
                          an empty in-memory fallback map (GitManager.ts:1003), 75s after service start:
                          08:03:32 WARN: PR lookup failed; keeping last known PR state.
                          operation: lookupStatusPr
                          branch: <redacted>
                          providerOperation: listChangeRequests
                          providerCommand: gh
                          errorDetail: GitHub CLI command failed.
                          

                          Related issues

                          #4752 (closed) and #4831 (closed) are the same family, a thread losing a merged PR, but neither covers this trigger. #4752 is the local checkout switching back to the default branch, which is suppressed by the isDefaultBranch rule at GitManager.ts:1067. #4831 is projection_threads.branch going stale after the agent renames the branch. Here the worktree stays on its own branch and the stored branch is correct, and the suppression comes from isUnpublishedBranch. #4970, #5514 and #7517 all point at the same weak thread-to-PR binding by branch name.

                          Fix applied or workaround

                          Nothing was written on the machine. The database was read only.

                          Workaround: click Settle on the affected threads once. effectiveSettled checks the explicit override at threadSettled.ts:328 before it looks at the PR, so the override survives this. Auto-settle on merge stays broken for future merges.

                          Filed by

                          Claude Opus 5 via t3 triage (Claude Code)

                          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

                              , 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              Merged PR is dropped once its remote branch is deleted, un-settling the thread #7653

                              Description

                              @LouisHaftmann

                              What happened

                              From the user:

                              I work remotely over Tailscale through the desktop app and sometimes after updating the desktop app many of the previously settled threads just come back. I checked the web app of the backend t3 server I'm using remotely over the desktop app and it also shows the threads as unsettled.

                              Follow-up answers: every affected thread had settled because its PR merged, never by clicking Settle. The threads stay wrong for a long time, and opening one does not fix it.

                              Diagnosis

                              The settle lifecycle in the database is intact. Across 715,331 orchestration events there are 38 thread.unsettled events total, and for all 774 threads carrying settled_override = 'settled' the projection agrees with the last settle-related event in the log. Nothing un-settled anything.

                              The threads that come back are the ones with no override, which stay settled only while the server reports a merged or closed PR. They lose that PR permanently, and the reason is branch cleanup after merge.

                              1. The repo has "Automatically delete head branches" enabled, so merging deletes the head branch on GitHub. Once anything prunes the stale remote-tracking ref locally, refs/remotes/*/<branch> is gone and @{u} no longer resolves. T3 does not prune (its background fetch is git fetch --quiet <remote>, apps/server/src/vcs/GitVcsDriverCore.ts:2909), but VS Code auto-fetch, git fetch --prune, and gh pr merge --delete-branch all do.

                              2. isUnpublishedBranch (apps/server/src/git/GitManager.ts:1277) decides whether a branch was ever pushed using exactly that ref: the repo tracks some remote, but no refs/remotes/*/<branch> exists. A merged and cleaned-up branch is indistinguishable from a branch that was never pushed.

                              3. GitManager.ts:974 then short-circuits the PR lookup and returns { latest: null }. gh is never called, so the merged PR that gh pr list --head <branch> --state all still returns is never seen.

                              4. effectiveSettled (packages/client-runtime/src/state/threadSettled.ts:300) receives changeRequest = null, changeRequestAutoSettles returns false, and the thread falls through to the inactivity rule. With the default sidebarAutoSettleAfterDays = 3 it sits in Active until three days of silence pass.

                              Because this is derived from server data, the web app and the desktop app agree, which is what the user saw.

                              The tie to updating the desktop app is indirect. No orchestration event fires when the PR vanishes, so a connected client keeps rendering its last good thread list. A restart refetches and the whole backlog appears at once, and an app update is a restart.

                              On this machine 24 of the 35 threads that depend on PR state have no remote-tracking ref and no upstream, so they are un-settled right now. The 11 healthy ones are recent merges whose stale ref has not been pruned yet.

                              Steps to reproduce

                              1. Use a repo with "Automatically delete head branches" enabled.
                              2. Start a thread on its own worktree branch, push it, open a PR.
                              3. Merge the PR. The thread auto-settles and shows the merged PR badge.
                              4. Prune the deleted remote branch locally, for example git fetch --prune in that checkout. VS Code auto-fetch with git.pruneOnFetch does this on its own.
                              5. Wait out PR_LOOKUP_CACHE_TTL (2 minutes), then load the thread list in a freshly started client.

                              Expected: the thread stays settled and keeps its merged PR badge.

                              Actual: the PR badge is gone, the thread is back in Active, and it stays there until the 3 day inactivity rule catches it. Clicking into the thread does not restore it.

                              Version

                              0.0.34-nightly.20260820.1141 (diagnosed against main at f708f63)

                              Environment

                              Linux x64, Node v26.6.0. Server runs as a systemd user service (npm exec t3@nightly server --host <tailscale-ip>), desktop app connects to it remotely over Tailscale. GitHub CLI as the source control provider.

                              Evidence

                              Branch whose PR merged and whose head branch GitHub deleted:
                              $ git for-each-ref --count=1 refs/remotes/*/<branch>
                              (empty) -> isUnpublishedBranch: true
                              $ git for-each-ref --count=1 refs/remotes | wc -l
                              86 -> repo tracks remotes
                              $ git rev-parse --abbrev-ref --symbolic-full-name @{u}
                              fatal -> details.upstreamRef: null
                              $ gh pr list --head <branch> --state all --json number,state
                              [{"number":<n>,"state":"MERGED"}] -> the PR is still there
                              Scope on this machine (threads not deleted, not archived, no settle override,
                              activity in the last 4 days):
                              35 threads depend on PR state
                              24 have no remote-tracking ref and no upstream -> PR lookup short-circuits
                              Settle lifecycle is clean, so nothing wrote this state:
                              sqlite> select count(*) from orchestration_events; 715331
                              sqlite> select count(*) from orchestration_events
                              where event_type = 'thread.unsettled'; 38
                              774 threads with settled_override='settled', all matching the event log
                              Unrelated but visible in the same window, cold-start PR lookups failing with
                              an empty in-memory fallback map (GitManager.ts:1003), 75s after service start:
                              08:03:32 WARN: PR lookup failed; keeping last known PR state.
                              operation: lookupStatusPr
                              branch: <redacted>
                              providerOperation: listChangeRequests
                              providerCommand: gh
                              errorDetail: GitHub CLI command failed.
                              

                              Related issues

                              #4752 (closed) and #4831 (closed) are the same family, a thread losing a merged PR, but neither covers this trigger. #4752 is the local checkout switching back to the default branch, which is suppressed by the isDefaultBranch rule at GitManager.ts:1067. #4831 is projection_threads.branch going stale after the agent renames the branch. Here the worktree stays on its own branch and the stored branch is correct, and the suppression comes from isUnpublishedBranch. #4970, #5514 and #7517 all point at the same weak thread-to-PR binding by branch name.

                              Fix applied or workaround

                              Nothing was written on the machine. The database was read only.

                              Workaround: click Settle on the affected threads once. effectiveSettled checks the explicit override at threadSettled.ts:328 before it looks at the PR, so the override survives this. Auto-settle on merge stays broken for future merges.

                              Filed by

                              Claude Opus 5 via t3 triage (Claude Code)

                              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