Skip to content

dl --prune cannot reclaim agent worktrees inside live workspaces' clones (104 GB measured) #426

Description

@blooop

Symptom

A host filled up: 468 GB volume at 100%, 1.5 GB free. The largest consumer was
~/.cache/devlaunch/ at 152 GB, and dl --prune reclaimed 1.3 GB of it.

The space is in agent git worktrees that live inside the workspace clones:

~/.cache/devlaunch/repos/<owner>/<repo>/<workspace-id>/.claude/worktrees/agent-<hash>/

Measured 2026-08-24 on one host, after a manual cleanup had already run:

agent-* worktree directories 72
...carrying a full .pixi/envs/default 18
Total size 104.5 GB
repos/ tree total (single du, hardlinks deduped) 127 GB
Worktrees as a share of the clone tree ~82%

One clone held 55 GB of them on its own. The envs are real copies, not hardlinks
into the shared pixi cache devlaunch bind-mounts at /var/tmp/devlaunch-pixi:
a sampled 217 MB locale-archive.tmpl had st_nlink=1, and du -shx vs
du -shxl over the whole tree differ by only ~13 GB, so this is not a
measurement artifact.

Why --prune cannot see it

dl --prune removes clone directories no workspace references (#159). Every one
of those 72 worktrees is inside a clone belonging to a live devpod workspace:

worktrees inside LIVE workspaces:      72  (104.5 GB)
worktrees inside ORPHANED clones:       0  (0.0 GB)

So the existing rule is not merely missing them — it must never fire here, because
firing would delete a live workspace's checkout. Reclaiming this needs its own rule,
scoped to .claude/worktrees/ within a clone that --prune otherwise leaves alone.

This is the same class as the orphaned-volume problem (#324): something creates
named state, nothing garbage-collects it, and devpod delete was never going to.

What makes it non-trivial

These are registered git worktrees, not stray directories, and they were created
from inside the container, so their registered paths are container paths that do not
resolve on the host:

$ git -C <clone> worktree list
/home/ags/.cache/.../devlaunch-fix-arch-pinivoti                     eb8a29d [fix_arch]
/workspaces/devlaunch-fix-arch-pinivoti/.claude/worktrees/agent-a49a 57955a3 [fix/git-refusal-classification] locked
/workspaces/devlaunch-fix-arch-pinivoti/.claude/worktrees/agent-a8da 5f9d40c [fix/aid-pty-flake] prunable

Across all clones on this host: 6 locked, 33 prunable registered entries against
72 directories on disk — so there are three distinct categories, with different
safety profiles:

  1. Unregistered leftovers (~33 dirs): git has already forgotten them, the directory
    remains. Safe to delete outright.
  2. Registered and prunable: git worktree prune will drop the metadata, then the
    directory can go.
  3. Registered and locked: git worktree prune deliberately skips these. Claude Code
    locks a worktree so it is not collected mid-run, so a lock may mean in use right now
    or may be a leftover from a killed session. These must not be removed on a timer.

And the branches matter. In the clone sampled, all 7 worktree branches had 0 unpushed
commits, but 3 of them did not exist on origin at all — so the commits were reachable
only from that worktree's branch ref. A blind rm -rf of a worktree whose branch is
unpushed and unmerged loses work, and it will not look like data loss until much later.

Asks

  • dl --prune reclaims .claude/worktrees/agent-* inside clones it keeps, for
    categories 1 and 2 above.
  • locked worktrees are never removed implicitly. Require an explicit opt-in flag,
    and say how many were skipped and why.
  • Refuse — or require the same explicit flag — for any worktree whose branch has commits
    not reachable from the default branch or any remote ref. Report those separately
    rather than silently keeping or silently deleting them.
  • git worktree prune runs in the clone so git metadata and the directory do not drift
    apart, and the container-path registrations get cleaned up too.
  • dl --ls --size attributes worktree bytes, so this is visible before it is a full disk.
    It was invisible in --ls --size here, which is why it reached 100%.
  • A dry run that prints what would go and what it would free, given the amounts involved.

Notes for whoever picks this up

  • The container-path registrations (/workspaces/<id>/...) mean git worktree remove
    from the host will not resolve the path; either run it inside the container, or
    git worktree prune the metadata and remove the directory from the host.
  • The agent-* naming comes from the agent harness, not from devlaunch, so match on the
    .claude/worktrees/ container rather than the agent- prefix.
  • Worth checking whether the per-worktree pixi install can be pointed at the shared
    cache so 18 envs are not 18 independent copies. That is a separate fix and probably a
    separate issue, but it is the reason the number is 104 GB rather than 10.

Found while auditing devcontainer compatibility across the fleet; not caused by any
devcontainer change.

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions