Skip to main content
New and experimental. File bugs and suggestions to aspect-build/aspect-cli/issues.
Delete a slot and the Bazel output base behind it. Name one to delete that one, or name nothing to take every free slot that has gone stale.
--dry-run only reads, so it does not wait for the pool lock while another call holds it. Without --dry-run it asks, then says what it removes as it goes:
Each slot costs a checkout plus an output base, tens of gigabytes on a large repo, so a pool that grew during a busy week keeps paying for it afterwards. The why column says why each slot is eligible, rather than quoting one threshold at all of them: idle 41d for a slot nothing has used in that long, named for a slot you named, and no recorded age, nothing on disk for an entry with no checkout, no output base and nothing to age it by.

Deleting one slot

Name a branch, a slot id or a slot’s directory — the first two are on screen in aspect worktree list — and how long it has been idle is not consulted. Naming it is the decision.
A slot somebody is working in is refused, because deleting it would take the checkout and the warm output base with it. Ending the lease is the reversible thing, and the refusal says so:
This also runs automatically during aspect worktree add, at the default threshold. Ordinary use keeps the pool trimmed; this command is for doing it now, or at a tighter threshold.
A dry run applies the same exemptions a real one does — a live server, a checkout holding uncommitted work — so it previews the outcome rather than listing everything old enough to be considered. Without a terminal to ask — a script, CI, an agent — prune refuses rather than guessing, with the needs_force token and exit 1:

What it will not delete

Uncommitted work. A free slot should hold no checkout. If one does and git reports work in it, or cannot read it at all, the slot is kept and reported rather than deleted — the bookkeeping is what is wrong, not the work. A repository of its own at the slot’s path — a .git directory rather than the .git file a linked worktree has — counts as a checkout git cannot read (UR), since nothing here can vouch for its branches and history; so do a plain file at the slot’s path, and a checkout whose git reads another directory as its work tree (core.worktree set in the worktree’s own config). A slot whose directory is gone while git still keeps commits only its HEAD reflog reaches, refs only it has, an operation in progress or a staged index for it (VG) is kept too. Only naming the slot with --force=all overrides that. A slot someone is working in. Only free slots are considered, whatever their age, and whether you named one or not — except a stranded slot, which can be named. A lease means a checkout that may have uncommitted work in it, and neither a timer nor an argument is grounds for deleting that — aspect worktree release ends the lease first. A slot being set up. An aspect worktree add claims a slot before it checks a worktree out into it, and deleting the output base in between would break a command that is still running. Such a slot reads setup in aspect worktree list. A slot whose Bazel server is still running. Unlinking an output base from under a live server leaves it writing into directories that no longer exist. Such a slot is also the warmest thing in the pool, which is the whole point of having one, so it is reported and kept:
It becomes prunable on its own once Bazel’s idle timeout fires.

Slots stranded by a clone that is gone

A slot belongs to one clone, because git worktree add is scoped to a single object store, and records it by path. Move, rename or delete that clone and the next add, release, path or prune in a clone of the repository takes back what it can:
  • A free slot goes to that clone, warm output base and all, and from then on ages like any other free slot. It holds no checkout, so it is nobody’s in particular.
  • A leased slot goes only to the clone whose git still lists its checkout — the moved clone itself — and git worktree repair points the checkout back at it, so the lease and its work carry on there. When the slot has moved too, recovering the lease takes git worktree repair run by hand. A slot moved away on its own whose git records hold none of the commits only its HEAD reflog reaches, refs only it has, an operation in progress or a staged index has its git registration dropped, so the moved copy’s git link breaks; its files are kept.
Until then, aspect worktree list tags such slots clone moved. prune says what it took over, and prune --dry-run what it would, rather than listing them for deletion:
Ownership of a lease is asked of git, not of the path. A clone moved away and another cloned where it was leaves the path existing, and the two kinds of slot part ways:
  • The moved clone’s free slots are recorded for that path and hold no checkout, so they go to the new clone, warm bases and all: a free slot is anybody’s.
  • Its leased slots stay the moved clone’s. Only the moved clone’s git lists their checkouts, so they go back to it, and the new clone, which cannot read them, is never handed them as its own. Such a slot is recorded for the new clone’s path while the new clone’s git has no worktree there. From the new clone, add, release and path refuse it with slot_stranded, and aspect worktree list tags it another clone's. The next add, release, path or prune run from the moved clone takes it back.
From the new clone’s side that refusal has two readings, and the message gives both: the checkout of a clone that was here and moved, or this clone’s own checkout, damaged. If the clone moved, run the command from where it is now; if the checkout is this clone’s, git worktree repair <path> from here may make it readable. Failing both, aspect worktree prune <slot> --force=all deletes it once you have copied out anything you need, refused while the session holding it is still running. What is left is stranded: a leased slot whose clone is no longer at its path and that no clone here lists — deleted, or moved somewhere this clone cannot see — and, outside any clone, where nothing is taken over, any slot of a clone that is gone. A stranded slot is deleted by naming it, whatever its age — and so is a slot recorded for this clone’s path that this clone’s git does not list. It takes --force=all when its checkout holds work, and a checkout git cannot read always does, which is what a leased slot of a clone that is gone has. A leased one whose session is still running is refused with held_by_another_session, --force=all or not: a session at work in it says the clone moved rather than went, and a command run from where it is now takes the slot back. One whose process was never captured cannot be shown to have stopped, so it is refused the same way unless --force=all is given. Both are asked before whether a Bazel server holds the slot. Whether the holder runs is asked again once a prompt is answered: a named stranded lease whose holder started running again meanwhile is not deleted, and prune exits 1. From outside any clone, prune takes a slot id, a unique prefix of one of at least four characters, or a slot’s directory, as aspect worktree list --all prints it, and finds the pool on disk. That is the way to delete slots for a repository you no longer have a clone of. With nothing named it refuses, since there is no pool to sweep:
A checkout whose clone is gone cannot be read by git, so uncommitted work in it cannot be ruled out: deleting it takes --force=all, after you have copied out anything you need. A second clone of the same repository that still exists keeps its own slots: it shares the pool directory but not the slots in it, and reclaiming them is not this clone’s call. Naming one says so rather than pretending not to find it:

Refusals

Flags

string
A branch, a slot id or a unique prefix of one (four or more characters), or a slot’s directory to delete, as aspect worktree list shows them. How long it has been idle is not consulted — naming it is the decision. Leave it out to work by idle time across every free slot.
int
default:"30"
Delete free slots untouched for at least this many days, when no slot is named. 0 considers every free slot.
“Untouched” is the newer of two things: Bazel’s last activity in the slot’s output base — the newest mtime among the files Bazel writes as it works, the evidence aspect gc measures a base by — and the slot’s last lease, taken or ended. It is what aspect worktree list shows as last used, and never reaches back before the slot was created. Because a lease counts here and not to gc, a slot leased recently whose base Bazel has not touched in a month can have that base removed by gc while prune keeps the slot: it goes cold, not away.
boolean
default:"false"
Report what would be deleted and delete nothing.
false | true | all
default:"false"
Delete without asking. true, which bare --force means, is required when stdin is not a terminal, so a script or an agent has to opt in rather than have a prompt answered on its behalf. all also deletes a named slot whose checkout holds uncommitted work, which is otherwise refused with the files listed — the same two grades as release --force.
string
default:""
The session asking, as given to add with the same flags. Needed only when the lease was taken with them, so this command recognises it as yours; detected automatically otherwise.
text | json
default:"text"
text writes the report to stderr and leaves stdout empty. json writes one document to stdout.

JSON output

candidates is what a run would delete, taken_over the slots of a moved or deleted clone it took over (or, with --dry-run, would), kept what it spared and why, and declined says whether you were asked and said no — which an empty pruned on its own cannot tell you. candidates is reported whether or not anything was deleted, so a dry run says what it would do rather than only that it did nothing. A candidate’s idle_days is null for a slot with no recorded age, and leased is the branch a stranded slot still holds a lease on, empty for a free one. A leased candidate’s last_branch may be empty, since the slot’s last branch is recorded when a lease ends: read the name from leased.

Relationship to aspect gc

The two reclaim different things, and neither makes the other redundant. aspect gc never treats a pooled slot as orphaned. A free slot looks exactly like an abandoned worktree, because the worktree is gone on purpose and that is what keeps the server warm, so the rule that reaps orphans skips pool slots — except one nothing will lease again, its directory gone and its pool holding no record of it. It does still collect them by idle time: a pooled base unused past --base-max-idle-days (30 by default) is removed like any other idle base, which leaves the slot in place and reading cold. aspect worktree prune removes the slot: its worktree, its registry entry and its output base together. That is why it deletes the base itself rather than leaving it for gc — a prune that reclaimed no disk and told you to go run something else would not be a prune.
aspect output-bases lists every Bazel output base on the machine, pooled or not, and aspect gc reclaims the idle ones.