Repository navigation
May a standing site's re-fetchable subtrees be reclaimed #468
Description
Question
May the re-fetchable subtrees of a standing site be reclaimed, even though the site itself must stand?
This is now the map's principle-2 keystone, and it exists because #446 took most of the measured bytes off the table on principle 1.
What changed. #426's Ask list rested on three categories, the first being unregistered leftovers, git has already forgotten them, safe to delete outright, about 33 of 72 directories on the reference host. #446 measured that this population is manufactured by routine git gc: a container-path registration lands on prunable: gitdir file points to non-existent location, which is precisely the arm prune --expire gates, so gc.worktreePruneExpire deregisters it on a documented schedule while the working tree still holds its uncommitted file. Forgotten therefore does not mean abandoned, and that whole category flips to stands.
So principle 1 is served, and principle 2 is now largely unserved: the 104.5 GB stays. Except that most of it was never work in the first place. #426 measured 18 of 72 worktrees carrying a full .pixi/envs/default, and that is the difference between 104 GB and roughly 10.
Hence the question. A .pixi/envs/default is re-derivable from a lockfile with no network beyond the shared download cache, holds nothing a human wrote, and is referenced by nothing outside its own directory. By the map's own definition it is much closer to an actual shared cache than to work. Deleting it from a site that must otherwise stand costs a pixi install, and costs nothing that exists nowhere else.
What to settle:
- Is this legitimate, or is it the thin end of exactly the wedge principle 1 exists to stop? The honest objection: the reason the site stands is that we could not prove it safe, and reaching inside something unproven to delete part of it is a different act from leaving it alone. Argue it, do not assume it.
- What names the reclaimable subtree, without guessing? A
.pixi/envsdirectory beside apixi.lockis a strong signal; a bare directory name is a pattern, and this map has refused patterns twice (#450, #451). Whatever the answer, it must be the same kind of answer those two gave: read something, do not match a shape. - It is a different unit from the keystone's site subtree, which is why this is a decision and not a slice of #454. Say what the unit is.
- Does it generalise, or is
.pixithe only member?node_modules,target,.venvare the obvious neighbours and each is a different argument. A rule that admits one member by name is a pattern wearing a rule's clothes. - Note R3: every byte figure goes through
disk_usage::exclusive_usage, and these envs were measured as real copies (st_nlink=1), not hardlinks into the shared cache, which is why they cost what they cost.
How the answer gets tested: a fixture with a standing site holding both a .pixi/envs/default and a file a human wrote, asserting the first goes, the second stays, and the site still stands.
Activity
blooop commentedon Aug 25, 2026
agent-decided (principle 2, no straggler data, on a gate principle 1 supplies): yes, and the thing that names it is not a directory name but a declaration the creating program wrote into the directory.
.pixi/envs/<name>carries aCACHEDIR.TAGwhose first 43 bytes are the Cache Directory Tagging Specification's signature, written by rattler, and the predicate that admits it never reads the directory's name at all. That is the same kind of answer #450 and #451 gave: read a record, do not match a shape.Everything below is measured in this repo's devcontainer, pixi 0.77.0, ext4, and the probe directory was removed. Nothing was written to the repo.
1. The objection, taken at its strongest, and why it fails
The objection is real: the site stands because we could not prove it safe, and reaching inside something unproven to delete part of it is a different act from leaving it alone. But it fails on the question of what the standing verdict is a statement about.
#446's reasons for standing are answers about git's account of the site: Q2 is
status --porcelainthrough the site's own admin directory, Q3 is reachability from a ref.NothingToAskThroughmeans there is no admin directory, so git's account cannot be obtained. It is silence about the tracked and untracked content git could have accounted for. It is not silence about the whole directory, because a specific set of bytes in there is outside git's account by the creator's own writing: pixi writes.pixi/.gitignorecontainingso nothing under
.pixi/envs/is in any index, in anystatusoutput, or reachable from any commit, in any clone, ever. The set of bytes the site's standing verdict is uncertain about and the set of bytes under the tag are disjoint by construction, and the construction is a file the installer wrote. Deleting the tagged subtree is therefore not reaching into the unproven part; it is removing bytes the standing verdict says nothing whatsoever about, and about which a second record says regenerable.And the contradiction that settles it. The shipped Q2 already reports nothing about
.pixi/envs, because it is gitignored. So a devlaunch that refused this would print "this site holds work that exists nowhere else, 0 bytes" and in the same breath refuse to remove 5 GB of it "because we could not prove it safe" — two incompatible readings of one directory in one report. This map's second precedent is that a plan and the outcome acting on it must not be able to disagree; a report that disagrees with itself is the same defect a level up. The wedge does not open, because the rule is the tagged subtree is not part of what the standing verdict is about, and that is checkable rather than assertable.Where principle 1 does bite is section 3, and it bites on the recipe rather than on the site.
2. What names it, and the name is never read
The general gate: the Cache Directory Tagging Specification. A file
CACHEDIR.TAGwhose first line is exactly43 bytes, from https://bford.info/cachedir/. It is a declaration by the program that created the directory that the contents are regenerable and belong outside a backup. It is cross-vendor, published, and written by the creator, which is what makes reading it a read and not a guess. Measured, in this container:
.pixi/envs/defaultrust/target.venvfromuv venv(uv 0.12.5).pytest_cache.venvfrompython -m venvnode_modulesafter a realnpm install(npm 11.18.0, lodash)The two rows that matter are the last three. The same directory name lands on both sides: a
.venvis admitted or refused depending on which program made it, which no pattern keyed on.venvcan express. Andnode_modulesis refused with nothing to argue about, because npm never made the claim. That is a property, not a member list.And pixi drew the unit's boundary itself, at exactly the right level.
.pixi/carries no tag; only.pixi/envs/<name>does..pixi/config.tomlis the one path.pixi/.gitignoreun-ignores, and it is the one human-writable file in there. So the tag sits precisely inside the line that separates the installer's output from the human's config. Nothing devlaunch invented put it there.The per-file route, and why it is refused. The obvious sharpening is to enumerate the derivable set exactly: union the
filesarray of everyconda-meta/*.jsonand call anything outside it foreign. Measured, it does not hold. On the throwaway probe env it looked perfect (6650 recorded, 6678 walked, delta 28, every one of themCACHEDIR.TAGor a file underconda-meta/). On this repo's real env it is 11002 recorded against 12210 walked, and the 1208-file delta is 1160 real files: the pypi half, installed by uv frompixi.lock's own pypi entries and recorded in.dist-info/RECORDrather than inconda-meta, plus__pycache__trees that no installer records and every import regenerates. So a path-set proof yields ~10% false positives on a normal env and would stand everything. It is #446's rejected content-based proof arriving in a cheaper disguise. Refused, with the numbers.3. Does a lockfile prove derivability: the four cases, measured
The probe: one
pixi.tomldepending onpython = "3.12.*", installed on the same ext4 device as the shared download cache so hardlinking would be physically possible.--frozen --offlineconda-meta/pixi'senvironment_lock_file_hashcame back byte-identical (cb70a71a2c1df89c)HTTP(S)_PROXY/ALL_PROXYpointed at a dead port,--frozen--offlinepromisepixi install --frozen --offlinerefuses:the lock file is not up-to-date with requested environment: 'default', 0 files. Not derivable--frozen --offlinerestored 5507 files. A stale lock is not a stale env: the env on disk was itself produced from that lock, so the lock reproduces exactly what was there. Without--frozenthe solve fails offline, which is why the recipe read is about the lock and not the manifestextraenvironment, install, then drop it from the manifest and reinstall..pixi/envs/extrasurvives, the lockfile no longer lists it, and pixi never mentions it again. Not derivable from anything on diskSo: the lockfile's presence, and its naming of that environment, is what proves derivability, and both are readable. Absent or non-naming, nothing on disk reproduces the directory and it stands — that is principle 1 inside the rule rather than bolted to it. Principle 2 is then served the way #450 served the build cache: by a named figure and a pointer, and the pointer here is real and narrow,
pixi clean -e <env>.One thing the tag does not promise, said out loud. pixi does not defend its own declaration. I planted
my-notes.txtand a hand-writtensite-packages/mypkg/__init__.pyinside the env, ranpixi install --frozen, and both survived and neither was mentioned. So the tag is a claim about the directory's purpose, not a proof about its current contents. That is why the argument in section 1 is the load-bearing one: the case for removal rests on the tagged subtree being outside what the site's verdict is about, corroborated by the creator's declaration and by the recipe being present, and not on a claim that every byte in there was installed.4. R3, and how much this actually reaches
The
st_nlink=1finding reproduces, on the same filesystem, independently of #426. Env atst_dev=66306,/var/tmp/devlaunch-pixi/pkgsatst_dev=66306, and:Same size, different inodes, one link each. All 5507 regular files in the probe env have
st_nlink=1; so do all 10867 in this repo's own 2.3 GB env. rattler copies out ofpkgs/rather than linking into the prefix, even where linking is physically available. Underexclusive_usagethat means every byte of a tagged env is billed to its own tree and every byte comes back on removal — there is no hardlinked pool to make the figure a fiction. This is the one artifact on the map whereduandexclusive_usageagree, and the reason is the same reason the population is expensive.Reach: about 94.5 GB of the measured 104.5 GB, roughly 90%. #426 measured 18 of 72 worktrees carrying a full
.pixi/envs/defaultand stated that this is the difference between 104 GB and roughly 10. Two things worth stating about that figure. It is not contingent on #446's verdict: the env goes whether the site was collectable or standing, which is exactly what makes this the principle-2 keystone rather than a slice of #454. And it leaves ~10 GB across 72 directories standing and named, which is the residual principle 1 buys and which #446 already said out loud.5. The unit
A tagged directory paired with the recipe that re-derives it. Not the directory, which is what makes a bare
.pixi/envsa pattern; and not the recipe, which does not say what to remove.It is a different unit from #445's site subtree in three specific ways, and each one is why this is a decision:
The domain is still enumerated from
cache_dirand from nothing else (R1), and the root is still printed before the question (R2), because the derivative is found by the same walk the sweep already performs. A scratchXDG_CACHE_HOMErun finds no tags, so it is harmless by construction.6. The fold: which standing reasons reach a subtree, and which cannot
This is the answer to "the thin end of the wedge" in its sharpest form, and it is derived rather than chosen. Ask of each
Reasonin #446'sStanding: is this a statement about git's account of the site's content, or a statement by a claimant about the directory?Holds { Unpushed },Holds { Uncommitted }.pixi/.gitignoreCouldNotProve(NothingToAskThrough)CouldNotProve(GitWouldNotSay)CouldNotProve(NotThisClonesToAccountFor)CouldNotProve(ThirdPartyClaim)(git worktree lock)CouldNotProve(CouldNotTakeTheLock)(#459'srun_if_lock_free)A nested site inside a tagged directory is the pathological case, and it needs no new arm: a site under the tag is a claim on the directory by something the tag does not speak for, so it lands in the claimant column with the child's own reason, and the parent tag is never a candidate. The rule composes with #445's fold instead of sitting beside it.
7. The type, per
/constructive-modelingThe distinction the ticket turns on is re-derivable versus unproven, and the mistake to avoid is giving "unproven" a value that can be constructed without a read. Same discipline as
Proofin #446:Derivativeis not the fallthrough of a filter, it is a thing you are handed.Four calls that follow from the measurements:
Option<Derivative>is refused outright. ItsNonewould mean both nothing tagged here and tagged but not costable, which is the two-meanings-one-value smell and is holds_unsaved_work can answer about an ancestor repository and report a dirty clone as safe to delete #171's shipped bug one artifact over, the same one Does --purge need an unsaved-work check #459 refused for the purge figure.Derivativeand nothing else, so there is no path from found a tag to deleted it that skips a read.conda-meta/pixi'smanifest_pathis not a field of anything. Measured, it is an absolute path recorded by whoever ran the install, so for every env installed inside a container it is/workspaces/<id>/...and does not resolve on the host: the same trap as a container-path worktree registration. What proves an agent worktree safe to remove, and what if it cannot be proved #446 and What unit does the agent-worktree sweep reason about #445 answer it by never resolving a recorded path, and the constructive form of that is that the field does not exist to be resolved. The recipe is located by walking up from the tag, inside the site, to apixi.lock. Whatconda-meta/pixiis read for isenvironment_name, which is a name and not a path.DiskUsage(R3), never a bare integer, so weighed nothing and could not weigh stay different values.Reason::subject()is amatchwith no wildcard arm, so adding aBlankvariant later forces somebody to answer §6's question for it rather than inheriting a default.8. Generalisation: the property is general, the evidence is per-tool, and confinement is named
The admission gate is general and measured across four writers and two refusals (§2). The recipe read is not: cargo's
target/is re-derived bycargo buildfromCargo.tomlplusCargo.lockplus the source, a uv.venvbyuv syncfromuv.lock, and a bareuv venvsomebody thenuv pip installed into has no recipe at all — uv's tag overclaims there, and principle 2's own exception is re-fetchable content, which requires something present to fetch from.So the honest shape is: one general gate, an open set of readers, exactly one implemented, and a tag with no reader stands and is named. That is not a rule admitting one member by name, and the test is that
.pixiand.pixi/envsappear nowhere in the predicate: the gate is a 43-byte constant from a published spec, and the reader is selected by whether its recipe read answers, not by what the directory is called. The residual the confinement leaves is small and measured —.pytest_cacheis kilobytes,rust/targetis not inside an agent worktree — and one reader reaches all ~94.5 GB.9. Out of scope, refused, and one piece of fog
.pixi/envs, at the top of a live workspace's checkout. Not decided here, because the clone root is not a standing site and none of dl --prune cannot reclaim agent worktrees inside live workspaces' clones (104 GB measured) #426's 104.5 GB is there. It is the one obvious extension and it has a readable gate available (devpod's own listing says whether the workspace is running, which is a record and not a guess), so it is fog rather than a refusal. Deleting a running container's env would break a session's tooling until reinstall, which is a cost and not a loss, but it should not arrive as a side effect of this decision.--force-worktrees. Gating it there would make one flag carry two consents, which is precisely what Does --purge need an unsaved-work check #459 refused and whatPrunePlandeleted its plan-wideforcefield to forbid.Provenancefield or afrom_tag: boolon the derivative, refused for Orphaned volumes with no surviving record #451's reason: it makes the untagged case representable and then relies on nobody building one.pkgs/even when the prefix is on the same filesystem as the cache, so the ceiling on any dedup fix is real and large.10. Build work this implies
Not a new ticket from me, per the map's instruction. It belongs on #454, because it is the same walk, the same plan, the same acting pass, and the same fold, and splitting it would create the two-passes-that-can-disagree shape this map has punished three times. What it must contain:
Tagged,Derivative,RecipeandNoRecipeas §7 writes them, in the sweep module, withDerivative's fields private and noDefault— and apublic-api.rest.txtrow asserting the absence of any other constructor, the way What unit does the agent-worktree sweep reason about #445 and Rebuild the agent-worktree sweep on the decided unit #454 already assert absences.<dir>/CACHEDIR.TAGequals the 43-byte signature. The walk does not descend past a tag, which is also what stopsfind-shaped waste inside a 12000-file env.NoRecipe: the tag;conda-meta/pixiparses and yieldsenvironment_name, withmanifest_pathnever stored; apixi.lockfound by walking up inside the site whoseenvironments:map names that environment.Reason::subject() -> Subjecton What proves an agent worktree safe to remove, and what if it cannot be proved #446'sReason, a wildcard-freematch, and the fold that turns a claimant reason intoTagged::Claimed. A site nested under a tag routes through the same arm.exclusive_usageonly, with What proves an agent worktree safe to remove, and what if it cannot be proved #446 §6's two-recursions rule extended: a derivative inside a site that is itself going is not reported separately.y/N, and the acting pass re-reads the tag and the recipe before removing. Removal is the tagged directory alone, never.pixi(it holdsconfig.toml) and never anything above it.extracase from §3 as the fixture; tag present with no reader -> stands and is named with bytes; site locked ->Claimed; site dirty -> derivable anyway, which is the row that pins §6; a nested site under a tag ->Claimed; a foreign file planted in the env -> still removed, which is the row that records §3's honesty rather than hiding it; and anode_modulesand a stdlib.venvin the same fixture asserting nothing happens to them, which is the row that proves the predicate never reads a name.docs/cleanup.mdgains the row (who reclaims it, and "the tag plus the lockfile" in the proves-removal-safe column).docs/workspace-tools.md's "The shared pixi package cache" gains one cross-reference sentence, and its headings must not move becauseflows::provision::lending_contractmatches on them. No em or en dashes in either.11. What this container could not establish
The four tag writers, the six install and re-derive measurements, the inode comparison and the record-versus-walk diffs are all from this devcontainer, pixi 0.77.0 / uv 0.12.5 / npm 11.18.0 / cargo, ext4, with
PIXI_CACHE_DIRpointed at the real shared mount at/var/tmp/devlaunch-pixi. The 104.5 GB and the 18-of-72 split are #426's host numbers and were not re-counted from in here; what §4 re-establishes independently is thest_nlink=1property those numbers rest on, including the same-filesystem case, which is the part that decides whether the bytes actually come back.