Repository navigation
Keep devlaunch's own copy of the volume names, and reclaim from it #456
Description
Work
Keep devlaunch's own copy of the two substituted volume names at the end of a completed up, and reclaim from that copy the volumes of a workspace devpod has forgotten. Decided in #451; the reasoning and every measurement behind it are in that ticket's resolution comment and should be read before starting.
The existing 39 orphaned volumes are out of scope by decision: no route reaches them that is not a pattern, they stay, and their row in #455 says so with the measurement. This ticket is only about the ones after today.
The mechanism
Both names already come from one read of workspace_result.json (SubstitutionContext.LocalWorkspaceFolder, .DevContainerID) taken immediately before devpod delete destroys the file. Add a second read of the same two fields, at the tail of a completed up, kept under devlaunch's own cache. No name is ever synthesized from a pattern, at either read, so #325's rule is unchanged.
Scope
- The copy. A per-workspace marker under the cache directory, sibling to
flows/provision/verdict_cache.rs's markers and modelled on them, not ametadata.jsonfield and not a field on the verdict marker. Orphaned volumes with no surviving record #451 gives the three reasons; the short one is that the verdict marker's presence meansAlreadyProvisionedand this has to be written on every completedup. - Store the derived names, not the substituted values, as
Option<NonEmpty<String>>with no empty-list state. Freezing them at read time is what keeps each name traceable to one real substitution. - The reclaim's domain is the set of copies, not the set of clone directories. A copy whose clone was deleted by hand names volumes no clone-shaped walk reaches, and reasoning over an enumeration that does not cover what it affects is the defect class this map exists to close (What unit does the agent-worktree sweep reason about #445). Per copy the precondition is that no workspace devpod lists carries that id.
- The surface is
--prune. It already owns "the workspace is gone and its leftovers are ours", and today every prune that removes an orphaned clone manufactures a permanent orphan, because devpod's record died at the outside delete. No new flag and no sub-scope. - The names ride on each record, never as a plan-wide list, so the plan and the acting pass cannot disagree about which volumes belong to which entry (the map's second precedent).
- Which read fed a sweep is a two-arm enum on the occasion, not a tag on the name. Live devpod result, or devlaunch's kept copy. No third arm, so adding one is a compile error at every match.
- Drop a copy once, on proof: after a removal that came back
Removedfor a workspace devpod does not list. Kept onRefusedso the retry survives. This diverges fromverdict_cache's "nothing ever deletes a marker" deliberately, and the divergence needs its reason in the module doc. --purgeordering becomes a property, not a habit: delete the workspaces (which sweeps from devpod's live record) before removing the cache that holds the copies. The reverse ordering loses bytes permanently and should be unrepresentable.
Done when
- A workspace deleted by a bare
devpod deleteoutsidedl, then pruned, leaves neither volume behind. This is the regression test for the whole ticket. - A copy naming a volume some container still holds, running or exited, produces a reported refusal and no removal. Measured behaviour: docker 29.7.2 exits 1 with
volume is in use. - A copy naming an absent volume is a silent no-op, which
--forcealready gives. - A scratch
XDG_CACHE_HOMEprune finds no copies and removes no volume. Guards the map's hard constraint. - The test Orphaned volumes with no surviving record #451 asks for: no
docker volume rmis issued for a name that came from anywhere other than a parsed devpod substitution record, live or copied. Ideally by construction, in which case the test asserts the absence of a path rather than a guard firing. - An
upthat never completed still names nothing and runs no docker command, unchanged from today. docs/cleanup.md's "the--prunehalf of the pair still frees no volume at all" is rewritten rather than deleted: prune still never deletes a workspace, it reclaims the volumes of one devpod has already forgotten. Coordinate with Write the reclaim contract into docs/cleanup.md #455, which owns the table.- One sentence in
docs/development.mdbeside the scratch-cache advice: a scratch run that creates a workspace writes its copy into the cache it then discards, so remove those workspaces withdl <ws> rmfirst or their volumes outlive the only record of them.
Calls for wf-tdd then wf-review.
Activity
blooop commentedon Aug 25, 2026
breadcrumb: widened by the images decision on #450, which lands on this ticket rather than beside it.
The marker this ticket introduces must carry the image reference alongside the two volume names, read from the same
workspace_result.jsonat the same moment (.ContainerDetails.Config.Image, plusMergedConfig.imageso a reference the devcontainer named stays distinguishable from one devpod derived). One document, one read, one marker: versioning a small per-workspace file twice within one map would be two migrations for one fact.The reclaim that reads it is #458, blocked on this ticket. Scope boundary: this ticket writes the marker and reclaims volumes; that one reclaims images.
Note the
--purgeordering property this ticket already surfaced now covers images too: delete workspaces before removing the cache holding the markers, or the references are unreachable and the images become permanent orphans.blooop commentedon Aug 25, 2026
breadcrumb: second requirement, from #452.
Write the volume copy for
Plan::Creatableworkspaces too, not only for the ones devlaunch cloned.--purgeis the one ordering #451's argument does not cover: it destroys foreign workspaces' copies while leaving their volumes standing, which manufactures exactly the orphan population #451 declined to reclaim. Writing the copy for every workspace devlaunch brings up closes that, and it costs one more write on a path that is already reading the document.Note this is about the copy, not about ownership. The record's honest claim is "dl brought this up", never "dl created it" — #452 refused to use it as an ownership answer for exactly that reason, and this requirement does not reopen that.
blooop commentedon Aug 25, 2026
breadcrumb: claimed.
mainis at16605f8, carrying #471, #464, #470 and #467. This is the first ticket that adds a record rather than reading or removing one, so the thing to get right is what #451 called the copy being wrong in exactly two ways, both caught by something other than trust. It also unblocks #458, whose whole reclaim reads the record this ticket writes.blooop commentedon Aug 25, 2026
breadcrumb: seams, posted before any test is written.
Five, and the first is the only new one:
up, before the provisioning pass. Before, not after, because provisioning can fail and take the launch with it while the container and its volumes stand. Also on the arm where a sibling'supwon the race, since that sibling may not have beendl.Two requirements from the comments above are in: the copy is written for every workspace
dlbrings up, not only the ones it cloned (#452), and it carries the image reference read from the same document at the same moment (#450), which #458 reads. Nothing here removes an image.blooop commentedon Aug 29, 2026
Claimed 2026-08-29: build in progress from an interactive session (agent in an isolated worktree), managed by session devlaunch-2b as part of closing out map #444. PR to follow. If you are another session looking at this ticket, pick a different one.
blooop commentedon Aug 29, 2026
breadcrumb: built, and the PR is #516.
Five seams as the breadcrumb named them. The copy store is
flows::kept_copies, a per workspace marker under<cache>/workspace-copies/beside the tool verdict markers, with three verbs: write on a completedup(and it does the read itself, so one document, one parse), read one, drop one on proof. The parse moved with it out offlows::lifecycle, so the live read at delete time and the copy's read share one parser rather than agreeing by two implementations being written alike. The marker carries #450's image reference from the same read at the same moment; nothing here removes an image.The write site is the tail of a completed
upbefore the provisioning pass, on both arms including the sibling-won one (#452). The reclaim's plan enumerates the set of copies rather than the clone walk, names ride on each record, and the precondition is re-asked under the second listing the acting pass already pays for. Provenance is a two-armSweepOccasionon the occasion, never a field on the name, anddevlaunch-core/tests/volume_names.rsis the by-construction guard #451 asked for: one module builds a volume name, one call site removes one, both halves two-sided.31 new tests, each red before its seam existed, plus the regression the ticket asks for as the headline: a workspace whose devpod record is gone and whose volumes are not, pruned, with docker's argv as the assertion. The 39 existing orphans stay, as decided.
One thing for review to know:
public-api.rest.txtis hand-edited, since this container has neither prerequisite the snapshot script needs.docs/development.mdnames that case, and it is provisional until CI'spublic-apijob agrees. Nothing promised moved, sopublic-api.api.txtis untouched.Unblocks #458.
blooop commentedon Aug 29, 2026
breadcrumb: #516 is green on
1be803a, every check includinge2eandgate. Not merged.Two things the first run caught that nothing in this container could, both of them the case
docs/development.mdnames:Ordering in
public-api.rest.txt.SweepOccasionsorts afterStopOutcome, not before. Ordering is not a partition property, sotests/public_api_snapshots.rspassed on the wrong placement and only CI's regeneration could say. Taken from that job's diff verbatim.public-api.api.txtmoved too, and that is a change to the promised surface rather than a routine one:api::workspace_deletegains the copy store as its sixth parameter. It is deliberate. A delete that sweeps a workspace's volumes from devpod's live record is also the moment that workspace's copy is provably pointless, and dropping it there is what keeps the next--prunefrom reporting a reclaim of volumes that already went with the workspace. One re-exported declaration, and its only caller in this repository isdl(#266:wflinks none of this). Called out in the PR body, per the snapshot rule.Third and smallest: codespell prefers "unparsable".
prekis on PATH here and I had not run it before pushing, which is the whole of why that reached CI.ci,e2e,rustandrust-coveragewere never failing, only mid-flight when they were first looked at.Note for whoever sequences this: #515 is open and touches the same four files (
CHANGELOG.md,README.md,docs/cleanup.md,dl/src/render.rs,dl/src/commands.rs). Different regions in each, but whichever lands second rebases.feat: keep dl's own copy of the volume names, and reclaim from it
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