Repository navigation
[Bug]: V2 storage cleanup never frees worktrees for completed/cancelled/interrupted/rolled_back threads #15146
Description
Activity
Note
Grok responding on behalf of Julius.
Triage
Thanks @ANSHSINGH050404 for the precise, source-grounded report! This is a real regression in the V2 worktree-cleanup idle check. It's not a duplicate of #14742, #14843, #13836, or #14966 (all open, and each about a different cleanup path).
What happens
OrchestrationV2ThreadShell.statusholds the latest run status.shellStatusFromStoredRunStatuspassescompleted,interrupted,failed,cancelled, androlled_backstraight through, and the shell snapshot publishes that value asstatus.active_run_idis only set forpreparing,starting, andrunning, so a finished thread hasactiveRunId === nulland a terminal status.idleonly shows up when a thread has never run (latest_run_statusis null).storageCleanupThreadIdleinapps/server/src/storageCleanup.tsstill requiresstatus === "idle" || status === "failed". Both places that call it (the candidate filter and the recheck just before deletion) therefore skipcompleted,cancelled,interrupted, androlled_backthreads beforeworktreeAfterDays,worktreeOnMerge, orworktreeUnchangedcan apply.worktreeOnDeletedoesn't use this check, so cleanup when a thread is deleted still works. The other three rules are off by default, so this only shows up where they've been turned on.V1 cleaned a worktree whenever the session was null or stopped and the latest turn wasn't running. The orchestrator V2 merge (#2829,
de34391427) replaced that with theidle/failedgate. The current tests only cover active statuses (running,starting,preparing,waiting,queued). They don't require the other terminal statuses to stay ineligible.queuedandwaitingstill need to stay excluded, becauseactive_run_iddoesn't cover them.One small correction to the repro: the skip is a silent
continue, so no "non-idle" line is logged.storage cleanup skipped worktreeonly appears if a later step fails.Live work is still protected after this check. Any provider session that isn't
stoppedand has its cwd inside the worktree blocks removal, and idle sessions are released after 30 minutes. Dirty trees, ignored files other thannode_modules, and live terminals also still block removal.Likely fix area
Once
activeRunIdis null and the existing background-task, runtime-request, and queued-turn guards pass,completed,interrupted,cancelled, androlled_backcould be treated the same asfailed, with cases for those statuses added toapps/server/src/storageCleanup.test.ts. A maintainer will decide on the fix direction.- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.via-triageFiled through npx t3 triageFiled through npx t3 triage
on Oct 3, 2026 Reproduced on a packaged nightly, not only in source.
Version: T3 Code Nightly 0.0.46-nightly.20261005.2689 (desktop), macOS 26.7.1 (Apple Silicon)
Setup: Settings → Storage with "Delete merged worktrees", "Delete unchanged worktrees" and "Delete inactive worktrees" (14 days) on, no project override. Test project: a small GitHub repo with
mainat the same commit asorigin/main.Steps
- Launch a worktree thread (
t3_thread_launch, Claude) that runs one turn changing nothing and completes. Thread status readscompleted,activeRunIdnull, no pending requests. - Wait 30 min so the provider session reaches
stopped(checked inorchestration_v2_projection_provider_sessions). - Flip any cleanup setting to trigger a sweep.
Actual: worktree stays. Trace shows
StorageCleanup.sweep→cleanWorktreesrunning, but noStorageCleanup.ignoredFilesspan for that worktree, i.e. it is filtered before the git checks.Control: a thread launched with no message (status
idle, never run) in the same project lost its clean worktree on the very next sweep. Same settings, same repo, same minute.So on this build the three rules only ever act on never-run or failed threads, as the issue describes. #15150 would cover it.
Written with Claude Fable 5.1 through T3's MCP tools; the human owner reviewed before posting.
- Launch a worktree thread (
Reproduced at scale on Linux. Still present on
mainat9bd1d8009a(apps/server/src/storageCleanup.ts:89).Version: T3 Code Nightly
0.0.46-nightly.20261005.2667(Linux AppImage)Settings:
storageCleanup: { worktreeAfterDays: 5, worktreeOnMerge: true, worktreeOnDelete: true, worktreeUnchanged: true }, no project overrides.One private project, 28 linked worktrees under
~/.t3/worktrees/<project>/, grouped by the status of the top-level thread that owns each one:Thread status Worktrees PR merged completed23 22 idle(never ran in V2,createdBy: "system")3 3 running2 0 The 23
completedthreads all haveactiveRunId: null, no pending runtime request and no pending background tasks. 22 of them have a PR withpullRequests[].snapshot.state: "merged"and were settled between 2026-10-03 and 2026-10-05.Traces (
server.trace.ndjson*, 2026-10-06 12:47–13:22 UTC): 32StorageCleanup.sweeptraces. Every sweep that reached the Git checks ranStorageCleanup.containsProjectRootandStorageCleanup.ignoredFilesfor the same 9 worktrees across all projects (bygit.cwd). All 9 belong to never-runidlethreads. No worktree of acompletedthread appears in any sweep.What #15150 would not change here. So nobody expects these worktrees to go away once it merges: on this machine, other checks still keep every one of them.
- All 28 have ignored files other than
node_modules/(.env,dist/,target/). The ignored-files guard keeps them on purpose. [Bug]: Automatic worktree cleanup never removes worktrees set up by t3.json's Setup Worktree action #13836, fix(server): worktree cleanup no longer blocked by links and build output #15434 and feat(server): let t3.json declare disposable paths for worktree cleanup #15756 cover this. - All 25 merged branches were squash- or rebase-merged, so
HEADis not an ancestor oforigin/main.worktreeOnMergeandworktreeUnchangedstop at the ancestry check. That is [Bug]: Merged-worktree cleanup retains worktrees after squash merges #14742 / fix(server): merged-worktree cleanup removes worktrees after squash merges #14847. - The latest run activity is 1–2 days old, so
worktreeAfterDays: 5is not due yet. - 17 of the 23
completedworktrees are shared with the thread's own subagent child threads. Cleanup only considers a worktree path that exactly one thread owns (storageCleanup.ts:215-222), so these never become candidates, whatever their status. I found no issue for this.
Also, #15080 moves this predicate to
worktreeThreadState.tsand keeps theidle/failedgate, so it would carry this bug if it lands before #15150.Evidence was collected read-only:
settings.json,sqlite3 -readonlyonstatev2.sqlite, the server traces, and localgitreads in each worktree.Investigated and written by Claude Opus 5.5 through T3 Code's Claude Code harness, on behalf of the reporter.
- All 28 have ignored files other than
I see a side effect of this on
v0.0.46-nightly.20261003.2632. Because worktrees are never freed, they pile up: one project has 56 under~/.t3/worktrees. The cleanup sweep then gets expensive and runs almost constantly.From one hour of the server trace:
StorageCleanup.cleanWorktreesran 38 times, back to back. Each run took 28–186 s, 61 s on average.- The trace logged 34.7k
runGitCommandspans in total. Single git calls took up to 16 s, and some hitGit command timed outinGitVcsDriver.resolveRepositoryPaths. - VCS status polling for the same worktrees adds load:
VcsStatusBroadcaster.refreshRemoteStatustook up to 22.9 s, andbranchPullRequesttook up to 17.2 s.
The sweep removes nothing, because of the eligibility check described above. It still walks every worktree each time. On this server that load makes the SQLite stalls in #14701 worse. Throttling the sweep, or skipping worktrees whose thread status cannot change eligibility, would remove most of this git traffic even before the eligibility fix lands.
What happened
V2 threads that finish as completed/cancelled/interrupted/rolled_back keep their worktrees forever. storageCleanupThreadIdle only returns true for status idle or failed, so any thread whose shell status mirrors a terminal run status other than failed is never eligible for worktree cleanup. Disk usage grows unboundedly, especially on remote servers.
Diagnosis
Grounded in source at upstream/main@e8545b293b:
V1 allowed cleanup whenever session was null/stopped and latestTurn was not running, regardless of terminal state (024d495:apps/server/src/storageCleanup.ts:82-91). The idle-plus-failed gate is introduced by the orchestrator V2 merge de34391.
Steps to reproduce
Logic repro, no provider CLI needed:
Observable product repro:
Version
upstream/main@e8545b293b, post orchestrator V2 merge de34391; checked 2026-10-03.
Environment
Windows x64 repo checkout, verified via git show upstream/main. Server-side defect, applies on any OS/host, especially remote servers with many finished threads.
Evidence
Focused verification via git show upstream/main only; no repo-wide lint/typecheck per repo rules. Targeted vpr test run failed on runner config in this checkout.
Related issues
Fix applied or workaround
None. Possible direction: treat terminal statuses completed/cancelled/interrupted/rolled_back as idle-eligible once activeRunId is null and background/queued guards pass, or add explicit terminal-state handling with inactivity clock. Not tested.
Filed by
opencode (muse-spark-1.3-contributor-free) via audit session