Skip to content

[Bug]: Storage cleanup never removes a worktree that a thread shared with its own subagents #16472

Description

@Info-Cado

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

  1. Enable any worktree rule under Settings → Storage (for example Delete unchanged worktrees).
  2. Start a thread in a new worktree, in a repository without ignored files other than node_modules/.
  3. In that thread, let the agent start a subagent: Claude's Agent tool, a Codex or Cursor native subagent, or T3's delegate_task.
  4. Let the turn finish, and leave the checkout clean with no open terminal.
  5. Wait for a cleanup sweep (startup, a settings change, or the hourly sweep).

Expected behavior

The subagent works inside its parent's checkout, so it should count as part of the parent thread. The checkout should be removed under the same rules as a thread without subagents, as long as the parent and all its subagents are idle.

Actual behavior

The worktree is never considered. Every subagent thread is stored with its parent's worktreePath. cleanWorktrees in apps/server/src/storageCleanup.ts (around lines 215-222) only keeps a worktree path that exactly one thread owns:

const groups = Map.groupBy(threads.filter((t) => t.worktreePath !== null), (t) => path.resolve(t.worktreePath!));
const candidates = [...groups.values()].flatMap((group) => (group.length === 1 ? [group[0]!] : []));

So a thread that ever used a subagent keeps its worktree forever, whatever its age, merge state or status. The re-check before removal (latest.length !== 1) has the same rule.

The single-owner rule came from V1 (#11598), where subagents were not app threads. In V2 they are app threads with lineage.relationshipToParent: "subagent", so the rule now also catches a thread's own subagents. The user guide says "shared worktrees ... prevent removal", which reads as sharing between separate threads, not a thread and its own subagents.

Note on delegated children: delegate_task children (creationSource: "mcp") can take messages later. When a turn starts on a thread whose checkout is missing, ProviderTurnStartService recreates it from that thread's own branch. Some delegated children on my machine have branch: null, so a fix should only count a child that cannot take messages (provider-native) or that has the parent's branch.

Impact

Major degradation or frequent failure

Version or commit

main @ 9bd1d80; T3 Code Nightly 0.0.46-nightly.20261005.2667

Environment

Linux, desktop AppImage. Claude and Codex providers.

Logs or stack traces

# One private project, 28 linked worktrees, read-only from statev2.sqlite.
# Every subagent thread with a worktree has the same worktreePath as its parent (133 of 133).
# Top-level thread status: 23 completed, 3 idle, 2 running.
# Of the 23 completed: 17 share the worktree with 1-12 subagent threads; 6 do not.
# Subagent kinds sharing a parent's worktree: provider-native and delegate_task (mcp).

On this machine, #15146 (the completed status gate) also blocks these 17 worktrees, so they are hidden twice. Fixing #15146 alone still leaves them, because they never pass the single-owner grouping. This issue is separate from #15146 and from #14742 (squash merges).

Workaround

Remove the worktree by hand. The branch stays, and the next turn on the thread recreates the checkout.

Investigated and written by Claude Opus 5.5 through T3 Code's Claude Code harness, on behalf of the reporter.

Activity

  1. Info-Cado commented on Oct 6, 2026

    @Info-Cado
    Author

    Fix PR: #16474. Related: #15146 (completed-status gate) and #16473 (archived threads missing from the cleanup snapshot).

  2. juliusmarminge commented on Oct 6, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Triage

    Thanks @Info-Cado for the precise report and the numbers from your machine. From the code on main, this looks like a separate issue from #15146, #14742, and the idle-status change in #15150.

    What I found

    Likely fix area

    The grouping in cleanWorktrees and worktreeOnDelete. One option is to fold only "subagent" children into their parent's ownership, with a couple of constraints:

    • Provider-native children (creationSource: "provider") can't take messages (isProviderNativeSubagentThread), while delegate_task children (creationSource: "mcp") can. ProviderTurnStartService recreates a missing checkout only when a thread has both worktreePath and branch, and thread.metadata.update can change them independently. So folding could be limited to children that can't take messages or that still share the parent's branch, with any other co-owner keeping the path shared.
    • Folded children would still need to be idle (a delegated child can still be running or queued after the parent's turn ends), the existing session, terminal, dirty-tree, and ignored-file checks would stay, and the inactivity clock could use the latest activity across the family rather than the parent alone.

    A maintainer will decide on the fix direction.

  3. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 6, 2026
  4. added a commit that references this issue on Oct 7, 2026
    7c8b1e7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions