Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
Storage cleanup enabled with Remove after N inactive days (also applies to the merged and unchanged rules).
A. Branch renamed or switched inside the worktree
- Start a thread in a new worktree and let the first turn finish. The thread records the branch, for example
t3code/fix-thing. - In that worktree (the agent or a terminal), run
git branch -m fix/thing, orgit switch -c fix/thing. - Commit, leave the worktree clean, close its terminals, and let the thread go idle past the inactivity window.
- Run cleanup from Settings → Storage.
B. Detached HEAD
- Start a thread in a new worktree as above.
- Detach HEAD in the worktree, for example
git switch --detach origin/main. A colocated Jujutsu repository does this without the user asking: when jj exports refs and the T3 branch is gone, the worktree's HEAD reflog showsexport from jjand HEAD is left as a bare commit. - Leave the worktree clean and idle past the inactivity window, then run cleanup.
The steps are derived from the source and from the state of the affected worktrees; I have not replayed them on a fresh dev server.
Expected behavior
The worktree is evaluated against the configured rules. A clean, idle worktree whose commits are all reachable from a branch or remote is removed once it is old enough, and the thread's recorded branch follows a rename or switch made inside its own worktree.
Actual behavior
The worktree is kept on every sweep with the reason "Repository or branch changed.", and it never becomes eligible.
cleanWorktrees compares the checked-out branch with the thread's recorded branch before any rule runs:
t3code/apps/server/src/storageCleanup.ts
Lines 418 to 420 in f7664fd
| const status = yield* git.statusDetailsLocal(worktreePath); | |
| if (!status.isRepo || status.branch !== thread.branch) | |
| return keep("repository or branch changed"); |
Two things make that permanent:
- The recorded branch is never reconciled for worktree threads.
thread.branchis written at launch, by the one-time temporary-branch rename, by the branch picker, and by the git action. The client's mismatch handling (resolveLocalCheckoutBranchMismatch) returnsnullwhenever the thread has a worktree. After a turn,RunFinalizationServicesees the mismatch and returns, which also skips that thread's pull request refresh:
t3code/apps/server/src/orchestration-v2/RunFinalizationService.ts
Lines 105 to 107 in f7664fd
if (local.refName === null || local.isDefaultRef) return; const thread = yield* projections.getThreadShell(threadId); if (!thread || thread.branch !== local.refName) return; - Detached HEAD reports
branch: null, which can never equal the recorded branch.
The check is not purely a bug: removal relies on ProviderTurnStartService recreating the checkout from thread.branch on resume, so removing a worktree that sits on another branch would resume the thread on the wrong one. The gap is that nothing brings the record back in line, so the protection never lifts. The check has no test in storageCleanup.test.ts and is not mentioned in docs/user/project-settings.md.
Observed on one machine: 10 single-thread worktrees kept this way across two projects. 7 were on a detached HEAD (6 detached by jj export, with the recorded branch no longer existing locally or on the remote; 1 left detached after a rebase). 3 were on a renamed or newly created branch. In all 10, git rev-list --count HEAD --not --branches --remotes was 0, so removal would have orphaned no commits, and 8 had no uncommitted changes.
Impact
Minor bug or occasional failure
Version or commit
0.0.46-nightly.20261010.2922; source read at main @ f7664fd
Environment
Linux, desktop server. One affected project is a colocated Jujutsu repository.
Workaround
Remove the worktree manually, or switch it back to the branch the thread recorded if that branch still exists.
Related
- fix(storage): make worktree cleanup work and show why it skipped #17563 added the "Repository or branch changed" reason, which made this visible.
- feat(worktrees): manage checkout lifecycle on the server #15080 (open) rewrites this code path and keeps the same comparison.
- feat(jj): support colocated Jujutsu repositories #16380 (open) adds Jujutsu support and touches
storageCleanup.ts.
A possible direction, for maintainers to judge: record the worktree's current named branch on the thread when a run finishes, and for a detached HEAD allow removal only when every commit reachable from HEAD is also reachable from a branch, remote, or tag. What a resumed thread should do when its recorded branch no longer exists is an open question.
Activity
feat(skills): add merge-pr and T3 branch rule
jesseroldan-sb commentedon Oct 28, 2026
Hit this hard on Windows. Our delegated-task worktrees were created as
t3/<id>, and the agents doing the work then switched to their own feature branches (reviewers detached). SoStorageCleanup.cleanWorktreeskept every one with "repository or branch changed", even pastworktreeAfterDays(8) and on merge/delete. In 11 days that piled up 506 worktrees (~165 GB, 196 with their ownnode_modules) and filled a 952 GB drive.Related, from the trace logs: 132 leftover folders with no
.gitfile make each sweep throwNotFoundonstat .gitinstead of being skipped.Our fix was setting Worktree branch naming to custom instructions, so T3 creates the exact branch our agents need. They no longer switch branches, so cleanup works again. The underlying bug still bites anyone whose agent changes branches; #22805 looks like the right fix, and we're happy to test it.
Eduardo-Orsi commentedon Nov 3, 2026
Same here on 0.1.6-nightly.20261030.3940 (macOS). The agent moves off the
t3/*branch in nearly every thread, so on 2026-11-03 18 of my 20 remaining worktrees could never be cleaned. Most had merged PRs, and they were taking up most of about 39 GB. The only one removed automatically had kept its original branch. Threads with subagents also hit "a subagent thread's branch differs from this worktree's branch", so a fix needs to update subagent threads too, not just the parent.VDrozd-Cyclis commentedon Nov 4, 2026
Note
Written by Claude Opus 5.5 on behalf of Vincent
Same bug here, with data on the cases #28508 deliberately skips. On my machine, those skipped cases are all of the mismatches.
6 of 52 worktrees are in this state, using about 10 GB of real disk. Cleanup is set to remove worktrees on merge, 1 day after settling, or after 14 days inactive.
origin/mainafter finishing. The other ended on its pull request's head.t3/<hex>I matched these by comparing each thread's recorded branch with the worktree's HEAD and reflog, then reading the guard. I didn't read T3's cleanup report for them. The detached worktree with one unpushed commit is the case the guard should protect: its pull request was closed unmerged, and no branch contains that commit.
These agents are already told to stay on T3's branch, and they still move. The custom branch naming workaround above doesn't help either, because a shared worktree moves through several branches.
The #22805 closure asked for maintainer agreement on adopting branch switches and on propagating branch changes across shared worktrees, so these are the open questions:
git switch, not only a rename?thread.branchfor every thread sharing the worktree at the end of a run? Or should it leave the records alone and let cleanup decide per worktree? The second is smaller, but a resumed thread could land on an older branch.git rev-list --count HEAD --not --branches --remotesis 0, and keep it otherwise? That would keep the one commit above and release the other detached worktree.t3/<hex>after a rename. Should that switch be adopted too, once the first-turn rename has finished?My preference is updating every sharing thread, plus the reachability check for detached HEAD. That keeps the protection for unpushed work and lets the rest go. I'm happy to test a build against these worktrees and report what changes.
tskimmett commentedon Nov 5, 2026
Is there some way some people aren't hitting this problem? Seems like a high severity issue.
rieckt commentedon Nov 7, 2026
Current-main update: #31599 merged on November 5 and added
WorktreeBranchSync, which reconciles saved branch metadata for threads sharing a worktree when the server observes a named-branch change. It includes archived threads and rechecks the current branch before dispatch (source at 0f4a1307). This addresses the named-branch mismatch when that status event is observed; I have not verified it in a running client.Detached HEAD remains outside that fix:
refName === nullreturns immediately, and cleanup still checks the recorded branch againstsymbolic-ref HEAD(cleanup guard). Removing that guard alone would leave resume/recreation using the old saved branch, so I have not proposed bypassing it. This narrows which parts of this report still need a fix.Investigated with GPT-6.1 Sol via Codex in T3 Code.
rianradeck commentedon Nov 10, 2026
Another data point, from a setup that integrates through a branch other than the default one.
Version: server 0.1.8-nightly.20261105.4234 (Linux). I have not checked whether that build already includes #31599.
Setup: Storage cleanup has Delete merged worktrees and Delete worktrees with deleted threads on. Agents work in T3-created worktrees.
What I saw: of 21 T3 worktrees, 11 had a different branch checked out than the one recorded on the thread. In each case the agent had moved off the branch T3 created, either by renaming it or by switching to a branch named after the issue it was working on. The sweep removed none of the 21. Separately, projects that integrate into a branch other than the default one hit another rule; I'm describing that in a separate Ideas discussion.
An agent removed 20 of them by hand with
git worktree remove, after checking that each was clean, had no commits missing from a remote, was integrated, had no running thread and no process using it. One stayed because it has uncommitted work.So the rename/switch case is not rare for agent-driven threads. In this setup it covered about half of the worktrees.
Written by Claude Sonnet 5.5 on behalf of rianradeck.