You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Clones outside repos_dir and dl path or url workspaces #452
Clones outside repos_dir, and workspaces from dl <path> or dl <git-url>: is leaving them still right?
--purge deliberately leaves three things it created, and names them before it asks: a config.toml that points repos_dir outside the cache puts clones where the purge does not remove them, and dl ./some/path and dl <git-url> open a source dl did not clone, so it cannot tell them from a workspace made by hand. The documented reasoning is that erring this way costs a command and the other kind of mistake costs work that cannot be got back, which is principle 1 stated in advance.
So this ticket probably confirms the status quo. It is a ticket anyway because principle 2 makes "we leave it and never mention it again" a thing that has to be chosen, and because the same reasoning has to be checked against --prune rather than only --purge:
--prune is scoped by repos_dir, so a config.toml moving repos_dir moves the scan with it. Confirm that is true of every new reclaim path this map adds, and that a scratch XDG_CACHE_HOME run still finds nothing.
Are these left artifacts discoverable? Principle 2 is satisfied by "the user can see it and decide" far more cheaply than by reclaiming. If dl --ls --size or the purge's own "leaving N" list already names them, the gap may be reporting rather than reclamation.
A dl <path> workspace still gets a devpod workspace, an image, and volumes. The clone is not ours to delete; the rest may be. Say which side of the line each artifact falls on, because "we did not clone the source" does not mean "we caused nothing".
How the answer gets tested: the existing purge tests already pin the "leaving" list; extend rather than replace them, and add one asserting a scratch-cache run of every new reclaim path removes nothing.
breadcrumb: the run on this ticket died on an API spend limit while still reading docs/cleanup.md. Nothing established, nothing recorded, no code touched. Claim released; a resume starts clean.
agent-decided (principle 1, no data loss): the status quo stands, and it is now chosen rather than inherited. Clones outside the cache and the workspaces dl <path> / dl <git-url> create keep being left, --purge keeps naming them, and no new reclaim path is built for them. The gap this ticket actually found is reporting, not reclamation, and it is two lines of output and a paragraph.
1. The scoping property, and the two roots it actually has
It holds today, but not as one root. There are exactly two, and the ticket's phrasing ("scoped by repos_dir") is true of one of them:
cache_dir = xdg::devlaunch_cache() (rust/dl/src/session.rs:68), which only XDG_CACHE_HOME moves. This is what --purge removes whole (purge_all_data), and it is what is_devlaunch_clone compares a workspace source against (rust/devlaunch-core/src/flows/listing.rs:398), so it is also what --purge's delete set, --ls --size, unsaved, checkedOut, and the fuzzy picker's owner/repo/ref grouping (layout_of_clone, same file, line 335) all resolve through.
repos_dir = the configured one, WorkspaceCloneManager::repos_dir() via clone_root (flows/lifecycle.rs, clone_root), defaulting to cache_dir/repos. This is --prune's scan root, its per-repo locks, and the root every workspace source is placed against (ClonePlacement::resolve). A config.toml moves all three together, which is the property clone_root's doc comment exists to state.
So: a scratch XDG_CACHE_HOME run finds nothing on either root in the absence of a config.toml, and XDG_CONFIG_HOME is unscoped, so a config.toml pinning repos_dir defeats the scope for the second root and not the first. That is exactly the hazard CLAUDE.md names, and it is in scope here because it is the one input that can make a maintenance command act on a real tree from a scratch run.
Two requirements for every reclaim path this map adds, and the second is what makes the hazard survivable rather than silent:
R1. A reclaim path's domain is enumerated from cache_dir or from the configured repos_dir, and from nothing else. It may intersect that enumeration with devpod's list or docker's list; it may never take its domain from one. Orphaned volumes with no surviving record #451's volume record satisfies R1 on the first root (copies under the cache, intersected with the workspaces devpod does not list); --prune satisfies it on the second.
R2. It names the root it enumerated, in the plan, before the question. Both existing commands already do: --prune's first line is Clone directories under <root>: (rust/dl/src/render.rs:1380) and --purge's is the cache directory it is about to remove (render.rs:1230). Under R2 the config.toml hazard is a path printed on screen with a y/N after it, not a silent widening. -y is what skips it, which is the same trade every command here already makes.
Do not "fix" the hazard by making the configured root ignore config.toml when XDG_CACHE_HOME is non-default. A reclaim command whose scope depends on two variables in a non-obvious combination is worse than one whose scope is printed. test/conftest.py scoping both variables stays the suite's answer, and CLAUDE.md's one-variable advice stays honest because of R2.
2. What a user can see today, per surface
left standing
dl --ls
--ls --size
--ls --json
--purge plan
--prune
a dl <path> workspace
yes: id, local, the source path
-
devlaunch:false, unsaved:null, disk:null
named, id only
not its business
a dl <git-url> workspace
yes: id and the URL
-
same
named, id only
n/a
a clone under a repos_dir outside the cache, live workspace
yes: id and the clone path
-
same
named, id only
scanned, kept, named with the workspace that opens it
the same, no live workspace
no
no
no
no
removed, or kept and named with the reason, under its own unsaved-work guard
a clone under a formerrepos_dir
only while a workspace still sources it
-
same
no
no
the .bare caches under such a repos_dir
no
no
no
no
never, by design
Read off it:
There is a surface where a clone outside the cache appears, and it is --prune. Because prune's root is the configured repos_dir rather than cache_dir/repos, the orphan clones of a moved-repos_dir install are found, weighed, and reclaimed today, with the unsaved-work refusal intact. The ticket's worry that they accumulate unreclaimed is wrong for that population. Principle 2 is already satisfied there.
Nothing live is silently invisible. Every surviving workspace is in dl --ls with its source, and --purge names it before it asks.
Gap A, and it is the one worth fixing: --purge's leaving list prints the id and not the source (render.rs:1236). That is the one screen where somebody is deciding, and for the moved-repos_dir case the thing being left is a whole clone tree the line does not mention. A survivor's id tells the user nothing about whether it is their hand-made workspace, a dl ./project of theirs, or a clone dl made under a repos_dir they configured.
Gap B: a moved repos_dir makes the listing under-report dl's own clones.SIZE -, unsaved: null, devlaunch: false, no owner/repo/branch in the picker, for clones dl made, whose metadata.json record resolves, and which dl <ws> rm will remove behind its guard. wf reads devlaunch and therefore never cleans them. Reporting only: the work is guarded by rm either way, so this is straggler-shaped, not loss-shaped.
One thing nothing reclaims and nothing names: clone trees under a repos_dir the config no longer names. Not lost -- resolve_clone_path prefers the recorded path whenever it is present (flows/workspace_clone.rs:437), so records_for_absent_directories does not drop the record that points at them. Unreclaimed and unnamed, with a surviving pointer.
The .bare caches under a moved repos_dir are never taken by anything: --prune skips every bare by construction and --purge is not looking there. That is principle 2's shared-cache exception in substance once the last clone of a repo is gone: re-fetchable content referenced by nothing, 0.08 GB for seven repos on the measured host. Stated in the table, not reclaimed.
3. Per artifact, for a dl <path> or dl <git-url> workspace
Both shapes are Plan::Creatable (flows/launch.rs:1980, covering Path, HostPath, Url, SshUrl): dl derives the id and runs devpod up <source> --id <id>, and writes no metadata.json record. So "we did not clone the source" is right about exactly one row.
artifact
did devlaunch cause it
who reclaims it
on what basis
the source directory (dl <path>)
no
nobody, ever
dl did not create it and cannot tell a project directory from something somebody depends on. remove_workspace_by_id answers Removed::NothingRecorded and the delete leaves it.
the source checkout (dl <git-url>)
devpod clones it inside the workspace
goes with the workspace
there is no host-side copy at all, which is the second half of the answer below
the devpod workspace record
yes
dl <ws> rm, never --purge
see W2 below: nothing on disk distinguishes it from devpod up by hand, and the only candidate evidence carries a weaker claim than the deletion needs
the container
yes
goes with the workspace on devpod delete
same as any workspace
the named volumes (<basename>-pixi, dind-var-lib-docker-<id>)
yes
dl <ws> rm already does, and --purge does not
workspace_delete names them from workspace_result.json before the delete and sweeps them with no ownership test at all (flows/lifecycle.rs:1004-1069). Ours by causation, reclaimable by a record read rather than a pattern, so principle 2 applies at no principle-1 cost.
the image
yes
unsettled; the map's images decision owns it
nothing about a path or url source changes an image's status. workspace_result.json names it the same way (#447).
dl's own cache entries (completion cache, tool-verdicts/ marker)
yes
--purge
keyed by workspace id, inside the tree the purge removes
The load-bearing row is the volumes: they are already reclaimed on the rm path today, which means the honest answer to "the clone is not ours to delete, the rest may be" is the rest already is, for the one artifact where it mattered.
Why the two candidate widenings are refused, and on different grounds
W1. Widen ownership to "source under cache_dir or under the configured repos_dir", so --purge deletes those workspaces and takes repos_dir with it. This is the tempting one, because the reason given for the current predicate ("the cache directory is exactly what a purge removes") would hold word for word if the purge removed both. Refused on principle 1: it is the one change that forfeits the scratch-cache property outright. Today a scratch XDG_CACHE_HOME purge is harmless even with a config.toml pinning repos_dir at the real tree, because the delete set reads cache_dir and only cache_dir. Under W1 that run deletes the real workspaces and the real clones, with no unsaved-work check anywhere in the path. What it buys is a tree --prune already reclaims, behind a guard --purge does not have, and dl <ws> rm already removes one at a time. Trading a data-loss guard for a straggler that is already reclaimable is the trade principle 1 exists to forbid.
W2. Use #451/#456's per-workspace record as a second ownership answer, so --purge recognises the workspaces dl <path> and dl <git-url> created. Refused because the record does not carry that claim. It is written at the tail of a completed up, and dl <existing-name> goes through up as well, so its honest meaning is "dl brought this workspace up under this cache", not "dl created it". Reading it as the second is how --purge comes to delete a workspace somebody made with devpod up and then attached with dl. #451's test for that record is that it can be wrong in exactly two ways and both are caught by something other than trust: an absent volume is a --force no-op, a held volume is a docker refusal. Nothing catches a wrong claim about whose workspace this is -- the check is the deletion. Same record, different cost of being wrong, so it is trusted for the volume sweep and not for the delete set. Narrowing it to creates only (Resolution::Cold rather than Warm) does not rescue it: containers are not participants in the per-repo lock and devpod list is a snapshot, so that inference is wrong in the direction that deletes.
And dl <git-url> has a second, stronger reason that is not "we cannot tell it from a hand-made one". devpod clones a git source inside the workspace, so there is no host-side checkout, and dl's unsaved-work guard is structurally blind to it: the container is the only copy of whatever is uncommitted. --purge deletes with devpod delete <id> --force (purge_delete_call) and then remove_tree_as_far_as_it_goes(cache_dir), applying no unsaved check even to dl's own clones. A purge that took url workspaces would be destroying the only copy of work with nothing between it and the confirmation. Leaving them is required, not merely prudent.
Build work this implies
Named rather than ticketed, per the ticket's own framing.
Write the reclaim contract into docs/cleanup.md #455 rows. The seven-row table above, for the dl <path> and dl <git-url> shapes, plus three "left standing" rows: clones under a repos_dir outside the cache (reclaimed by --prune, removed by rm, never by --purge), clones under a formerrepos_dir (nothing, deliberately; the metadata.json record survives as the pointer), and the .bare caches under such a repos_dir (nothing, and it is principle 2's shared-cache exception in substance). R1 and R2 belong in the same table's preamble, because they are the contract every future row has to satisfy.
One small report change, no new surface, nothing deleted. Print each survivor's source beside its id in --purge's leaving list, and say in that block that removing the cache also drops what dl knew about the survivors, so dl <ws> rm them first if their volumes should go. That second sentence is load-bearing for Keep devlaunch's own copy of the volume names, and reclaim from it #456: --purge is the one ordering Orphaned volumes with no surviving record #451's "delete the workspaces first, then the cache" argument does not cover, because it never deletes the foreign ones, so it destroys their volume copies while leaving their volumes standing. Naming it at the confirmation is far cheaper than carving per-workspace copies out of a purge. Cost: one golden (PURGE_PLAN, rust/dl/tests/lifecycle.rs:751) and one paragraph in docs/cleanup.md under "What purge deletes".
Not build work: Gap B. Splitting is_devlaunch_clone into an ownership question (may --purge delete this: cache_dir) and a provenance question (is this a clone dl made: repos_dir) would give a moved repos_dir its SIZE, its unsaved and its picker grouping back. It is refused for now on principle 3: repos_dir is documented nowhere a user reads -- not in the README, and in docs/ only as one incidental clause at docs/cleanup.md:86 -- so the population is very likely empty, and the change is two predicates where the code deliberately keeps one. Recorded as fog below. If that option is ever documented, this becomes real work rather than fog.
How it is tested. Extend, do not replace: the PURGE_PLAN golden gains a source per survivor. Every new reclaim path gets one test asserting a scratch-cache run removes nothing, and one asserting the root it enumerated is the root it printed -- R1 and R2 as assertions on the plan value rather than as prose.
Fog
--purge applies no unsaved-work check to dl's own clones, while rm and --prune both do. Not a defect on its own -- it is an explicit all-or-nothing command behind a confirmation -- but Write the reclaim contract into docs/cleanup.md #455's "what proves removal safe" column has to read "the confirmation, and nothing else" for that row, and the map should decide that rather than discover it.
Is repos_dir supported or vestigial? A config option no user-facing page documents, which nonetheless moves --prune's entire scan root and silently degrades four listing surfaces. Deciding it collapses most of the second half of this ticket in either direction.
Activity
blooop commentedon Aug 25, 2026
breadcrumb: the run on this ticket died on an API spend limit while still reading
docs/cleanup.md. Nothing established, nothing recorded, no code touched. Claim released; a resume starts clean.blooop commentedon Aug 25, 2026
agent-decided (principle 1, no data loss): the status quo stands, and it is now chosen rather than inherited. Clones outside the cache and the workspaces
dl <path>/dl <git-url>create keep being left,--purgekeeps naming them, and no new reclaim path is built for them. The gap this ticket actually found is reporting, not reclamation, and it is two lines of output and a paragraph.1. The scoping property, and the two roots it actually has
It holds today, but not as one root. There are exactly two, and the ticket's phrasing ("scoped by
repos_dir") is true of one of them:cache_dir=xdg::devlaunch_cache()(rust/dl/src/session.rs:68), which onlyXDG_CACHE_HOMEmoves. This is what--purgeremoves whole (purge_all_data), and it is whatis_devlaunch_clonecompares a workspace source against (rust/devlaunch-core/src/flows/listing.rs:398), so it is also what--purge's delete set,--ls --size,unsaved,checkedOut, and the fuzzy picker's owner/repo/ref grouping (layout_of_clone, same file, line 335) all resolve through.repos_dir= the configured one,WorkspaceCloneManager::repos_dir()viaclone_root(flows/lifecycle.rs,clone_root), defaulting tocache_dir/repos. This is--prune's scan root, its per-repo locks, and the root every workspace source is placed against (ClonePlacement::resolve). Aconfig.tomlmoves all three together, which is the propertyclone_root's doc comment exists to state.So: a scratch
XDG_CACHE_HOMErun finds nothing on either root in the absence of aconfig.toml, andXDG_CONFIG_HOMEis unscoped, so aconfig.tomlpinningrepos_dirdefeats the scope for the second root and not the first. That is exactly the hazardCLAUDE.mdnames, and it is in scope here because it is the one input that can make a maintenance command act on a real tree from a scratch run.Two requirements for every reclaim path this map adds, and the second is what makes the hazard survivable rather than silent:
cache_diror from the configuredrepos_dir, and from nothing else. It may intersect that enumeration with devpod's list or docker's list; it may never take its domain from one. Orphaned volumes with no surviving record #451's volume record satisfies R1 on the first root (copies under the cache, intersected with the workspaces devpod does not list);--prunesatisfies it on the second.--prune's first line isClone directories under <root>:(rust/dl/src/render.rs:1380) and--purge's is the cache directory it is about to remove (render.rs:1230). Under R2 theconfig.tomlhazard is a path printed on screen with ay/Nafter it, not a silent widening.-yis what skips it, which is the same trade every command here already makes.Do not "fix" the hazard by making the configured root ignore
config.tomlwhenXDG_CACHE_HOMEis non-default. A reclaim command whose scope depends on two variables in a non-obvious combination is worse than one whose scope is printed.test/conftest.pyscoping both variables stays the suite's answer, andCLAUDE.md's one-variable advice stays honest because of R2.2. What a user can see today, per surface
dl --ls--ls --size--ls --json--purgeplan--prunedl <path>workspacelocal, the source path-devlaunch:false,unsaved:null,disk:nulldl <git-url>workspace-repos_diroutside the cache, live workspace-repos_dir-.barecaches under such arepos_dirRead off it:
--prune. Because prune's root is the configuredrepos_dirrather thancache_dir/repos, the orphan clones of a moved-repos_dirinstall are found, weighed, and reclaimed today, with the unsaved-work refusal intact. The ticket's worry that they accumulate unreclaimed is wrong for that population. Principle 2 is already satisfied there.dl --lswith its source, and--purgenames it before it asks.--purge's leaving list prints the id and not the source (render.rs:1236). That is the one screen where somebody is deciding, and for the moved-repos_dircase the thing being left is a whole clone tree the line does not mention. A survivor's id tells the user nothing about whether it is their hand-made workspace, adl ./projectof theirs, or a clonedlmade under arepos_dirthey configured.repos_dirmakes the listing under-reportdl's own clones.SIZE -,unsaved: null,devlaunch: false, no owner/repo/branch in the picker, for clonesdlmade, whosemetadata.jsonrecord resolves, and whichdl <ws> rmwill remove behind its guard.wfreadsdevlaunchand therefore never cleans them. Reporting only: the work is guarded byrmeither way, so this is straggler-shaped, not loss-shaped.repos_dirthe config no longer names. Not lost --resolve_clone_pathprefers the recorded path whenever it is present (flows/workspace_clone.rs:437), sorecords_for_absent_directoriesdoes not drop the record that points at them. Unreclaimed and unnamed, with a surviving pointer..barecaches under a movedrepos_dirare never taken by anything:--pruneskips every bare by construction and--purgeis not looking there. That is principle 2's shared-cache exception in substance once the last clone of a repo is gone: re-fetchable content referenced by nothing, 0.08 GB for seven repos on the measured host. Stated in the table, not reclaimed.3. Per artifact, for a
dl <path>ordl <git-url>workspaceBoth shapes are
Plan::Creatable(flows/launch.rs:1980, coveringPath,HostPath,Url,SshUrl):dlderives the id and runsdevpod up <source> --id <id>, and writes nometadata.jsonrecord. So "we did not clone the source" is right about exactly one row.dl <path>)dldid not create it and cannot tell a project directory from something somebody depends on.remove_workspace_by_idanswersRemoved::NothingRecordedand the delete leaves it.dl <git-url>)dl <ws> rm, never--purgedevpod upby hand, and the only candidate evidence carries a weaker claim than the deletion needsdevpod delete<basename>-pixi,dind-var-lib-docker-<id>)dl <ws> rmalready does, and--purgedoes notworkspace_deletenames them fromworkspace_result.jsonbefore the delete and sweeps them with no ownership test at all (flows/lifecycle.rs:1004-1069). Ours by causation, reclaimable by a record read rather than a pattern, so principle 2 applies at no principle-1 cost.workspace_result.jsonnames it the same way (#447).dl's own cache entries (completion cache,tool-verdicts/marker)--purgeThe load-bearing row is the volumes: they are already reclaimed on the
rmpath today, which means the honest answer to "the clone is not ours to delete, the rest may be" is the rest already is, for the one artifact where it mattered.Why the two candidate widenings are refused, and on different grounds
W1. Widen ownership to "source under
cache_diror under the configuredrepos_dir", so--purgedeletes those workspaces and takesrepos_dirwith it. This is the tempting one, because the reason given for the current predicate ("the cache directory is exactly what a purge removes") would hold word for word if the purge removed both. Refused on principle 1: it is the one change that forfeits the scratch-cache property outright. Today a scratchXDG_CACHE_HOMEpurge is harmless even with aconfig.tomlpinningrepos_dirat the real tree, because the delete set readscache_dirand onlycache_dir. Under W1 that run deletes the real workspaces and the real clones, with no unsaved-work check anywhere in the path. What it buys is a tree--prunealready reclaims, behind a guard--purgedoes not have, anddl <ws> rmalready removes one at a time. Trading a data-loss guard for a straggler that is already reclaimable is the trade principle 1 exists to forbid.W2. Use #451/#456's per-workspace record as a second ownership answer, so
--purgerecognises the workspacesdl <path>anddl <git-url>created. Refused because the record does not carry that claim. It is written at the tail of a completedup, anddl <existing-name>goes throughupas well, so its honest meaning is "dl brought this workspace up under this cache", not "dl created it". Reading it as the second is how--purgecomes to delete a workspace somebody made withdevpod upand then attached withdl. #451's test for that record is that it can be wrong in exactly two ways and both are caught by something other than trust: an absent volume is a--forceno-op, a held volume is a docker refusal. Nothing catches a wrong claim about whose workspace this is -- the check is the deletion. Same record, different cost of being wrong, so it is trusted for the volume sweep and not for the delete set. Narrowing it to creates only (Resolution::Coldrather thanWarm) does not rescue it: containers are not participants in the per-repo lock anddevpod listis a snapshot, so that inference is wrong in the direction that deletes.And
dl <git-url>has a second, stronger reason that is not "we cannot tell it from a hand-made one". devpod clones a git source inside the workspace, so there is no host-side checkout, anddl's unsaved-work guard is structurally blind to it: the container is the only copy of whatever is uncommitted.--purgedeletes withdevpod delete <id> --force(purge_delete_call) and thenremove_tree_as_far_as_it_goes(cache_dir), applying no unsaved check even todl's own clones. A purge that took url workspaces would be destroying the only copy of work with nothing between it and the confirmation. Leaving them is required, not merely prudent.Build work this implies
Named rather than ticketed, per the ticket's own framing.
dl <path>anddl <git-url>shapes, plus three "left standing" rows: clones under arepos_diroutside the cache (reclaimed by--prune, removed byrm, never by--purge), clones under a formerrepos_dir(nothing, deliberately; themetadata.jsonrecord survives as the pointer), and the.barecaches under such arepos_dir(nothing, and it is principle 2's shared-cache exception in substance). R1 and R2 belong in the same table's preamble, because they are the contract every future row has to satisfy.--purge's leaving list, and say in that block that removing the cache also drops whatdlknew about the survivors, sodl <ws> rmthem first if their volumes should go. That second sentence is load-bearing for Keep devlaunch's own copy of the volume names, and reclaim from it #456:--purgeis the one ordering Orphaned volumes with no surviving record #451's "delete the workspaces first, then the cache" argument does not cover, because it never deletes the foreign ones, so it destroys their volume copies while leaving their volumes standing. Naming it at the confirmation is far cheaper than carving per-workspace copies out of a purge. Cost: one golden (PURGE_PLAN,rust/dl/tests/lifecycle.rs:751) and one paragraph indocs/cleanup.mdunder "What purge deletes".Plan::Creatableworkspaces too. Otherwise adl <path>ordl <git-url>workspace deleted withdevpod deleteleaks its volumes with no record anywhere on the machine, which is the exact population Orphaned volumes with no surviving record #451 built the copy for.is_devlaunch_cloneinto an ownership question (may--purgedelete this:cache_dir) and a provenance question (is this a clonedlmade:repos_dir) would give a movedrepos_diritsSIZE, itsunsavedand its picker grouping back. It is refused for now on principle 3:repos_diris documented nowhere a user reads -- not in the README, and indocs/only as one incidental clause atdocs/cleanup.md:86-- so the population is very likely empty, and the change is two predicates where the code deliberately keeps one. Recorded as fog below. If that option is ever documented, this becomes real work rather than fog.How it is tested. Extend, do not replace: the
PURGE_PLANgolden gains a source per survivor. Every new reclaim path gets one test asserting a scratch-cache run removes nothing, and one asserting the root it enumerated is the root it printed -- R1 and R2 as assertions on the plan value rather than as prose.Fog
--purgeapplies no unsaved-work check todl's own clones, whilermand--pruneboth do. Not a defect on its own -- it is an explicit all-or-nothing command behind a confirmation -- but Write the reclaim contract into docs/cleanup.md #455's "what proves removal safe" column has to read "the confirmation, and nothing else" for that row, and the map should decide that rather than discover it.repos_dirsupported or vestigial? A config option no user-facing page documents, which nonetheless moves--prune's entire scan root and silently degrades four listing surfaces. Deciding it collapses most of the second half of this ticket in either direction.Say what a purge costs the kept copies, now that #456 has landed
Say what a purge costs the kept copies, now that #456 has landed