Skip to content

Keep devlaunch's own copy of the volume names, and reclaim from it #456

Closed

Description

@blooop

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 a metadata.json field 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 means AlreadyProvisioned and this has to be written on every completed up.
  • 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 Removed for a workspace devpod does not list. Kept on Refused so the retry survives. This diverges from verdict_cache's "nothing ever deletes a marker" deliberately, and the divergence needs its reason in the module doc.
  • --purge ordering 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 delete outside dl, 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 --force already gives.
  • A scratch XDG_CACHE_HOME prune 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 rm is 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 up that never completed still names nothing and runs no docker command, unchanged from today.
  • docs/cleanup.md's "the --prune half 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.md beside the scratch-cache advice: a scratch run that creates a workspace writes its copy into the cache it then discards, so remove those workspaces with dl <ws> rm first or their volumes outlive the only record of them.

Calls for wf-tdd then wf-review.

Activity

  1. blooop commented on Aug 25, 2026

    @blooop
    OwnerAuthor

    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.json at the same moment (.ContainerDetails.Config.Image, plus MergedConfig.image so 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 --purge ordering 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.

  2. blooop commented on Aug 25, 2026

    @blooop
    OwnerAuthor

    breadcrumb: second requirement, from #452.

    Write the volume copy for Plan::Creatable workspaces too, not only for the ones devlaunch cloned. --purge is 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.

  3. self-assigned this
    on Aug 25, 2026
  4. blooop commented on Aug 25, 2026

    @blooop
    OwnerAuthor

    breadcrumb: claimed. main is at 16605f8, 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.

  5. blooop commented on Aug 25, 2026

    @blooop
    OwnerAuthor

    breadcrumb: seams, posted before any test is written.

    Five, and the first is the only new one:

    1. The copy store itself — a per-workspace marker under the cache, alongside the tool-verdict markers rather than inside them. Its three verbs are write-on-a-completed-up, read-one, and drop-one-on-proof. Tested over a scratch cache and a scratch devpod home, no docker and no devpod involved.
    2. The write site: the tail of a successful 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's up won the race, since that sibling may not have been dl.
    3. The reclaim's plan — enumerated from the set of copies, not from the clone walk, and each entry carries its own names. Precondition per copy: no workspace devpod lists carries that id.
    4. The reclaim's act — re-asked under the second listing the acting pass already pays for, then the same removal the delete path uses.
    5. The binary boundary — the whole regression in one run: a workspace whose devpod record is gone and whose volumes are not, pruned, with docker's argv as the assertion.

    Two requirements from the comments above are in: the copy is written for every workspace dl brings 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.

  6. blooop commented on Aug 29, 2026

    @blooop
    OwnerAuthor

    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.

  7. blooop commented on Aug 29, 2026

    @blooop
    OwnerAuthor

    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 completed up (and it does the read itself, so one document, one parse), read one, drop one on proof. The parse moved with it out of flows::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 up before 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-arm SweepOccasion on the occasion, never a field on the name, and devlaunch-core/tests/volume_names.rs is 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.txt is hand-edited, since this container has neither prerequisite the snapshot script needs. docs/development.md names that case, and it is provisional until CI's public-api job agrees. Nothing promised moved, so public-api.api.txt is untouched.

    Unblocks #458.

  8. blooop commented on Aug 29, 2026

    @blooop
    OwnerAuthor

    breadcrumb: #516 is green on 1be803a, every check including e2e and gate. Not merged.

    Two things the first run caught that nothing in this container could, both of them the case docs/development.md names:

    Ordering in public-api.rest.txt. SweepOccasion sorts after StopOutcome, not before. Ordering is not a partition property, so tests/public_api_snapshots.rs passed on the wrong placement and only CI's regeneration could say. Taken from that job's diff verbatim.

    public-api.api.txt moved too, and that is a change to the promised surface rather than a routine one: api::workspace_delete gains 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 --prune from reporting a reclaim of volumes that already went with the workspace. One re-exported declaration, and its only caller in this repository is dl (#266: wf links none of this). Called out in the PR body, per the snapshot rule.

    Third and smallest: codespell prefers "unparsable". prek is on PATH here and I had not run it before pushing, which is the whole of why that reached CI.

    ci, e2e, rust and rust-coverage were 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.

  9. added a commit that references this issue on Aug 29, 2026
    3ceddea
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions