Skip to content

What dl reclaims and what it must leave #444

Description

@blooop

Destination

One stated, tested contract for what dl reclaims and what it must leave, covering everything devlaunch causes to exist rather than one artifact at a time, and --prune implementing it on a unit that makes the current defect class unrepresentable rather than guarded case by case.

Done looks like: a table in docs/cleanup.md naming every artifact devlaunch causes, who reclaims it, and what proves removal safe; a reclaim path for each straggler or a written reason there is none; and the agent-worktree sweep rebuilt so that a worktree nested inside a removed one, and a registration created after the plan was printed, cannot be lost by construction.

Notes

Re-charting after the single-ticket map hit its retry limit. Three fix rounds on #426 each closed their instance and left the shape: a pass reasons over an enumeration that does not cover what it actually affects. Read PR #442 and both review reports on it before deciding anything; the second (5019431339) has the reproductions.

Principles for this effort, reordered. The human's ordering replaces the default and outranks it:

  1. No data loss. Work that exists nowhere else is never removed without an explicit human answer. Where a check cannot prove safety, fail towards keeping. This beats every principle below it.
  2. No straggler data. Nothing devlaunch causes to exist should accumulate unreclaimed. One legitimate exception: an actual shared cache, meaning re-fetchable content referenced by nothing. Today that is the pixi download cache and nothing else.
  3. Then the defaults: long-term maintainability, simplicity, constructive modeling, test-first.

Where 1 and 2 pull against each other, 1 wins and the run says so on the ticket.

Two precedents in the existing design that decide more than they look like they do.

  • Names are read from devpod's own record, never guessed from a pattern (docs/cleanup.md, the volume sweep, devlaunch#325). A made-up name is somebody else's disk. Any new reclaim path inherits this, and it is what makes the images question a research question rather than a taste question.
  • A plan and the outcome that acts on it must not be able to disagree (PrunePlan's doc comment, on the plan-wide force boolean that already caused a bug here). The defect class this map exists to close is the same shape one level up.

Hard constraints, not preferences. --prune stays scoped by XDG_CACHE_HOME through repos_dir, so a scratch-cache run stays harmless and nothing reaches into ~/.claude or a container path. Registered worktrees carry container paths that do not resolve on the host, so git worktree remove cannot run there. Containers are not participants in devlaunch's per-repo lock, so TOCTOU is unclosable from the host and must be designed around, never assumed away.

Two requirements every reclaim path on this map inherits (from #452). There is now exactly one root, since #460 retired repos_dir: cache_dir (xdg::devlaunch_cache(), moved only by XDG_CACHE_HOME), from which clone_root becomes <cache>/repos by construction. The config.toml escape that used to defeat the scratch scope is gone with it.

  • R1 — a reclaim path's domain is enumerated from cache_dir, and from nothing else.

  • R2 — it prints that root before it asks its question, so a widened scope is a printed path with a y/N after it rather than a silent one. Both existing commands already satisfy R2.

  • R3 — every byte figure this map states goes through flows::disk_usage::exclusive_usage, which already implements the only correct rule: sum st_blocks * 512 per unique (st_dev, st_ino), and bill an inode to a tree only when every one of its links lies inside that tree. Clone objects are hardlinked from .bare, so this is not a refinement: measured, it differs by 23.8x on a clone and 1487x on a bare, summing separate du runs inflates by 95.7%, and a single du over both gets the total right while attributing the shared pool to whichever argument it met first, so attribution flips with argument order.

Every flag lands in README.md (test_readme_cli_doc.py); no em or en dashes in README.md or docs/ (test_docs_prose.py). Build and test in the container per CLAUDE.md; never run ./dev.sh there. Verify checks against the full 40-char head SHA: a conflicting PR gets no merge ref, so ci and prek silently do not fire, which already happened on #442.

Decisions so far

  • Where the detached sweep's notices landthe record. A per-repo last-sweep note in metadata.json beside last_fetched, overwritten each sweep so it is bounded by construction, read by --ls and --ls --json, the only surfaces actual usage ever looks at. "Nowhere" forfeits the only account of the cache between launches; a log file pays principle 2 to satisfy it. Verified first that no persist-and-print precedent exists (the Retire repos_dir #481 retired-key notice re-derives from config.toml each run and writes nothing), so #508 knowingly introduces the first background-learned condition that survives to a foreground run. (agent, decided with the human)
  • The bare's dangling HEAD symreftotal readers, nothing repointed. default_branch_of verifies the symref's target exists and falls through to its next probe instead of returning a name that lies; dl keeps never writing a bare's HEAD. The measured lie is the adopt path re-recording a dead branch from the stale symref. The trace surfaced the larger half — the recorded default_branch is written once and never revalidated — deliberately split off as #507, off-map and unbuilt, because for the primary usage (own repos, explicit-branch launches) it is nearly a non-event. Build claimed; PR to follow. (agent, decided with the human)
  • The reclaim surface for what lies beyond clones (was fog) — #472 rides --prune: one printed plan, one y/N, no new flag and no sub-scopes. The count of surviving new prune-time reclaim paths is exactly one, so a scope grammar would be the guessing this item was parked to avoid; each derivative is named with its exclusive_usage size in the same plan the user already reads, and a second prompt inside one command would be two answers about one act, the shape Does --purge need an unsaved-work check #459 refused. (agent, decided with the human)
  • Retire repos_dirshipped, PR #481, merged 16605f8. The field goes, and the point was never the smaller struct: WorktreeConfig now carries no paths at all, xdg::clone_root_in is the only producer of clone roots, and WorkspaceCloneManager::in_cache is the only production constructor, so a configuration can no longer express placement and the devlaunch:false / SIZE - / unsaved:null degradation has no representation. Two reds, both judged at the outside: a clone dl placed reads as someone else's from the completeness test, and assert 'repos_dir' in '' from a test driving the shipped binary. The review could not construct the degradation from a configuration and did construct it from a prior build, which is the honest scope: a clone an earlier dl placed under the key still reads as someone else's, retiring the key moved --prune's root inwards so nothing reaches it, and deleting the key stops the notice too, leaving nothing that names the tree. Is repos_dir supported or vestigial #460 accepted that stranding knowingly, so the claim was narrowed to "complete for the clones this build places" in code and on the contract page, where a heading saying "two things" had a third reproduced under it. Migration is one notice, and it diverges from Is repos_dir supported or vestigial #460 deliberately, principle 1 over principle 3: it fires on the key being present, not on it resolving elsewhere, because resolve-and-compare is the step that can be silently wrong (symlinked home, moved cache home) and wrong there means silence about a tree nothing else will name. That is also what let expand_tilde die: the value is quoted back verbatim, resolved by nothing. Verified live to fire exactly once on launch, --ls --json, --prune and --reconcile, with nothing at the old path moved or removed, --purge -y included. One regression flagged and accepted: refresh_cache no longer refuses on an unreadable config.toml, so --install / --refresh / --completion-data proceed, though a broken config is never silently in effect. The --purge case has teeth and went to #461 with its reproduction rather than being absorbed here. (agent)
  • Pack the bare caches' refs on the sweepshipped, PR #478, merged 5cb909a. One pack-refs --all after a successful fetch_all, and Should devlaunch pack the bare caches' refs #465's own premise was wrong in the direction that strengthens it: a git clone --bare arrives packed (601 refs, zero loose), so loose refs are made by fetching, one file per ref updated, shadowing a packed-refs that keeps pre-fetch shas. Confirm the bare object stores are bounded #453's 5286 was never the clone's doing, and the sweep is not a convenient host for the pack, it is the only thing in dl that makes them in quantity. Red says it: the two files left behind are the sweep's own fetch output. Re-measured on 2.51.1, not cited: 601 loose refs hold 2404 KiB of blocks against 33787 B packed, the pack costs 23 ms, and --all is load-bearing (a bare pack-refs packed the tags and left all 301 heads untouched), pinned by three tests and proved by mutation. A refused pack is not a failed fetch: its own CacheNotice::RefsNotPacked arm rather than let _ =, and last_fetched still lands, because withholding the stamp re-fetches the whole repository every interval forever over a representation change that did not come off. The review measured that a permanently failing pack plateaus rather than accumulates (loose refs are rewritten in place, never appended), so principle 2 is not newly engaged. Concurrency verified rather than assumed: 636 clones from a 401-ref bare against 60 rounds of pack-refs --all, every clone exit 0, fsck-clean, zero missing refs. Packing changes nothing a later prune may delete, tags included, and the resurrection case (a loose ref at a new sha over a stale packed line, then retracted) loses both storage forms with nothing returning at the old sha. Two blocking prose findings fixed before merge: docs/cleanup.md stated What proves an agent worktree safe to remove, and what if it cannot be proved #446's bare-side probe in the present tense when what ships asks the clone, so the probe saving is now written as banked rather than collected and placement alone carries the change today; and the promised "line on stderr" reaches nobody, since the sweep is spawned via detach, which nulls it. That second one is now #480. (agent)
  • Fetch tags with a forced refspecshipped, PR #476, merged a5c0049. One word: --tags becomes +refs/tags/*:refs/tags/*, three argv pins move with it, and a --tags clause goes from workspace_clone's "the launch fetch is not the broad sweep" guard because the contains('*') clause beside it already covers the tags refspec. Red at the level the wedge is reported at, not merely caused at: fetch_repo returning Refused { ... ! [rejected] release -> release (would clobber existing tag) } — and the same reason text carries main -> main, because a rejected tag fails the whole fetch and the head pushed in the same breath does not arrive either. Measured on 2.51.1 the ref updates are one transaction and the rejected tag rolls the head back. This makes tag fetching delete refs automatically and unflagged, so the licence was verified rather than asserted: fetch_all has exactly one production caller and its bare can be nothing but bare_dir, clones are siblings repointed at the forge by a step that hard-errors, .bare is never bind-mounted into a container, and git has one entry point in the tree with no tag verb, no push --tags, no --follow-tags. Nothing here authors a tag. Force-overwrite loses nothing either: after gc --prune=now the workspace clone still holds the old commit with a clean fsck, because local clone-from-path hardlinks make it independent of the bare. Interaction with Widen the clone unpushed probe to every local ref #471, measured through the real bare-to-clone topology: it narrows in every reachable non-wedged state. A tag upstream has retracted now reaches no clone at all, which was the strongest half of Widen the clone unpushed probe to every local ref #471's false positive; the half docs/cleanup.md describes survives, so that page reads correctly before and after. The review corrected the build's "cannot widen": a tag force-moved onto a commit upstream then orphans does widen, but only against a permanently failing fetch, and towards keeping. Widen the clone unpushed probe to every local ref #471's trade is untouched; #475 owns reversing it. Two comments the review measured wrong were fixed before merge, one of them a doc claim that named rev-parse HEAD as what fails when it exits 0. Left recorded, not handled: the dangling HEAD symref, now #477. (agent)
  • Widen the clone unpushed probe to every local refshipped, PR #474, merged e55239a. Git::unpushed_commits stops taking a branch and asks log --oneline --all --not --remotes, so CouldNotTell::UnpushedNotListed loses its branch field: there is no longer a single branch the question is about, and a branch there would be the checked-out one standing in for a question it was not asked. Red first, and the failing test was the bug report: a clone on a pushed feature holding a commit on wip, expected a WouldLose: NothingToLose. A second data-loss path fell out of it, and it was the gate rather than the branch name — the probe was skipped whenever HEAD named no commit, so a clone left on an orphan branch reported nothing to lose while holding unpushed commits on the branch it had just stepped off; measured, --all exits 0 silently on a refless clone, so the gate protected nothing and hid that. One deliberate false positive, taken under principle 1: --all spans refs/tags, so a tag no remote branch reaches reads as permanently unpushed, and through devlaunch's own clone topology it is stronger than that — the bare keeps the tag past upstream deleting it (Confirm the bare object stores are bounded #453's monotonicity) while refs/remotes/* is pruned, so every later clone of that repo reports it. A clone kept costs disk, a clone deleted costs the work. Reviewed independently and approved with no blocking findings; the untested half is now #475. (agent)
  • A nested worktree belonging to a different repositorystands, reported, never probed, using What proves an agent worktree safe to remove, and what if it cannot be proved #446's existing CouldNotProve arm rather than a new one, and candidate 2 refused on its merits rather than on its unavailability, so the verdict does not move if P1 is ever weakened. The ticket's premise was false in the worse direction: measured on this tree's dl, a live foreign worktree holding uncommitted work is removed unopposed, with no flag, under the false reason "git has already forgotten it" — because linked_worktree_name reads only the gitfile tail, so Some(name) means both ours and somebody's, and the doc comment says "some repository's admin directory" two lines above the defect. The same read subtracts it from the parent's dirt, so it is neither protected nor reported. Fix is one type: OursHere carries the joined Recorded and derives admin from it, making ownership a positive answer from a listing devlaunch owns, and What proves an agent worktree safe to remove, and what if it cannot be proved #446's borrowed-index collision unconstructible. Candidate 2's decisive leg: a perfect answer still would not license the act, since devlaunch cannot finish it — measured, the removal leaves the other repository holding a registration it may not name and unable to check out its own branch (exit 128). Git contributes zero here. Principle 2 is served without devlaunch reclaiming, because May a standing site's re-fetchable subtrees be reclaimed #468 already decided this row a non-claimant, so the derivative reclaim reaches into the foreign site and the parent it pins. (agent)
  • May a standing site's re-fetchable subtrees be reclaimedyes, on a declaration the creator wrote, not on a name. The gate is the Cache Directory Tagging Specification's published 43-byte CACHEDIR.TAG signature, measured present from rattler, cargo, uv and pytest and absent from python -m venv and npm's node_modules — so the same directory name lands on both sides, which makes this a property rather than a member list, and satisfies the map's twice-stated rule to read something rather than match a shape. The objection ("reaching inside something unproven") is answered by asking what the standing verdict is about: What proves an agent worktree safe to remove, and what if it cannot be proved #446's reasons are answers about git's account of the site, and .pixi/.gitignore puts the env outside that account by construction, so the two byte sets are disjoint. The clincher is a contradiction: the shipped Q2 already reports .pixi/envs as nothing, so refusing would print 0 bytes at risk while declining 5 GB in one report, which is the plan-disagrees-with-outcome defect one level up. Principle 1 bites on the recipe, not the site: lock present and naming the env re-derives 5507/5507 files in 0.52 s with every proxy pointed at a dead port; lock absent refuses outright and the tree stands; an env the lock no longer names stands, with pixi clean -e as the pointer. Unit is a tagged directory paired with its recipe, and the walk does not descend past a tag. Reach: ~94.5 GB of the measured 104.5 GB, and not contingent on What proves an agent worktree safe to remove, and what if it cannot be proved #446's verdict — the env goes whether the site was collectable or standing. Residual ~10 GB across 72 directories, standing and named. (agent)
  • What --prune should do inside a containerthe same thing it does on a host, no detection and no refusal, and the ticket's premise was false: Held was never the cover. The host/container asymmetry is R1's doing, not the host's. Measured from inside a real devcontainer: pixi run dl --prune reports Nothing to prune, because the container's clone is bind-mounted at /workspaces/<id>, outside its own cache_dir, so Held is downstream of enumeration and never runs. Two already-decided properties replace it: P1 locality (the domain is cache_dir alone, and devlaunch mounts a clone outside the container's own cache, so every registration was recorded one namespace in — regenerating at every nesting depth, so aid-in-a-container is the same rule) and P2 ordering (What unit does the agent-worktree sweep reason about #445's directory-first forget means the recorded path does not resolve when the forget runs, even where it resolved a moment earlier). Refusing loses on the clause meant to favour it: the environment predicate it needs has no sound implementation (devpod's vars and injected binary are route-dependent, the hostname is best-effort CAP_SYS_ADMIN, and DEVLAUNCH_WORKSPACE_ID goes out via --init-env and never reaches inside), it fails open, and it would remove the only reclaim path a nested container's clones have. (agent)
  • Should devlaunch pack the bare caches' refsyes, on the detached sweep, one pack-refs --all after a successful fetch_all. The scaling check reframed it: cost is one filesystem block per ref, so it tracks ref count, not repository size. Measured across 10 real remotes, torvalds/linux carries 1887 refs (7.4 MiB) against vscode's 5342 and rust-lang/rust's 334, median around 370 refs (~1.4 MiB), so a whole cache is 30 to 60 MB and vscode's 20.6 MiB is a branch-hygiene outlier. Loose refs are bounded by upstream and do not accumulate, so principle 2 barely bites and "reclaim 20 MiB" cannot carry the change. What carries it is placement and a second payment: fetch_all has exactly one production caller, so "after the fetch" is the sweep by construction, in a lock scope it already holds; and What proves an agent worktree safe to remove, and what if it cannot be proved #446's per-worktree proof is rev-list --not --all on the bare, which today reads 5286 loose ref files per probe instead of one packed-refs. --prune refused on model mismatch, not style: PrunePlan is classify-and-remove-directories with no arm for "mutate something we keep", so there is nothing to weigh and nothing for a y/N to be about. (agent)
  • What proves an agent worktree safe to removea verdict, not a boolean, and not three flat arms either: Collectable(Proof) | Stands(NonEmpty<Reason>), where a reason is proved unsafe or could not be proved, and reasons accumulate up the subtree. Three flat arms are refuted by the fold, since a subtree can be both. Proof is a private-field witness with no Default, mintable only from a probe that answered, so "nothing objected" and "nothing was asked" stop being the same value and finding N4 closes structurally rather than by NonEmpty::one. Four questions with different scopes, and the shipped code has two of them transposed: dirt is per working tree, reachability is per repository — which is both of Does --purge need an unsaved-work check #459's live data-loss defects in one sentence. A lock is an unproved, never a loss, because git documents it as saying nothing about liveness. prunable is never read and Held goes, since it is git's guess about a path this module refuses to resolve. And it flips dl --prune cannot reclaim agent worktrees inside live workspaces' clones (104 GB measured) #426's own premise: the "category 1, safe to delete outright" population is manufactured by routine git gc (prune --expire gates exactly the container-path arm), not left by abandonment, so roughly 33 of 72 directories on the reference host become stands. Principle 1 over principle 2, said out loud. (agent)
  • Does --purge need an unsaved-work check — a report, never a refusal. Principle 1's "explicit human answer" is already present; what was missing is that it be an answer about disk, so the prompt gains a figure for what the tree holds that exists nowhere else. "Fail towards keeping" has no expressible action on a purge (one indivisible act on one root, and the only keep is n), and a refusal is refused on three counts, chiefly that --purge --force is a grammar error so the override would be -y — which CI and the e2e suite already pass, and which already means I read the plan, making one flag carry two consents: the plan-wide-boolean shape PrunePlan deleted its force field to forbid. Two corrections it forced: the framing that --prune already computes this figure is wrong (it weighs only clones it removes and only worktrees inside clones it keeps, and purge's set is the union of both exclusions), and the figure cannot be a bare usize, since 0 would mean both nothing at risk and could not weigh — devlaunch#171's shipped bug one level up. Cost checked: the git probes are the cheap bounded half (593 ms over 37 clones) and must run under run_if_lock_free, or a locked report would be stricter than the deletion it precedes. (agent)
  • Is repos_dir supported or vestigialremoved. No writer has ever existed, no user-facing page has ever documented it, no issue has asked for it, and no test exercises a non-default value (conftest.py scopes XDG_CONFIG_HOME to neutralise it). XDG_CACHE_HOME already provides the capability across every surface. Removal is not just a smaller struct: is_devlaunch_clone's single containment test becomes complete, so the devlaunch:false / SIZE - / unsaved:null degradation that stopped wf cleaning dl's own clones becomes unrepresentable rather than a bug to fix. The killer fact: the pointer was never stable anyway — prepare_workspace derives the directory from repos_dir and add_worktree overwrites local_path in the same write, so editing the key already re-clones and destroys the only record of the old tree, today. Migration is one stderr notice, because silence is what strands a tree. (agent)
  • Are the bare object stores boundedno, and the assumption was wrong in one cheaply fixable way. Heads are bounded: --prune really does remove local refs/heads for branches deleted upstream (measured, 201 heads to 101). But --tags is a non-forced refspec and --prune prunes per-refspec, so tags are never pruned: refs/tags is monotone in every tag the remote has ever advertised and pins everything they reach (a 20,971,520 B blob reachable only from an upstream-deleted tag survived gc --prune=now; with a forced refspec, 20,975,616 B came back). Same one word causes a second, worse bug: a tag force-moved upstream is rejected and the fetch exits 1 permanently, which RepoManager::fetch_repo maps to FetchRepoError::Refused, so freshness fetches stay broken until a human deletes the local tag. Loose refs cost a further 20.6 MiB with no reclaim path (pack-refs --auto is a no-op on the files backend, and nothing in production runs pack-refs). (agent)
  • What unit the agent-worktree sweep reasons about — the site subtree: nodes are places inside the clone, containment edges come from the filesystem walk and never from prefix-comparing recorded paths, and a parent is collectable only when it and every nested site are. That makes T1 unconstructible rather than guarded. git worktree prune is deleted, not gated: its domain is a readdir at act time, so no plan-time unit can ever equal it, which is why the clone cannot be the unit and a directory cannot see past itself. The one metadata operation is git worktree remove <the path git printed>, per registration by name, so a registration devlaunch never saw cannot be named, and measured on 2.51.1 and 2.43.0 could not be acted on even if it were (exit 128). MetadataGate and finding T3 disappear. TOCTOU is not closed, but its radius drops from the whole clone to one subtree the pass just walked. (agent)
  • Clones outside repos_dir and dl <path> / dl <git-url> workspacesstatus quo, now chosen. Widening --purge to the configured repos_dir is refused: it is the one change that forfeits the scratch-XDG_CACHE_HOME safety property outright, for a tree --prune already reclaims behind a guard --purge does not have. Using Orphaned volumes with no surviving record #451's per-workspace record as a second ownership answer is refused too, because that record's honest claim is "dl brought this up", not "dl created it", and a wrong claim there is caught by nothing but the deletion. dl <git-url> has a stronger second reason: devpod clones inside the workspace, so the container is the only copy of uncommitted work and dl's guard is structurally blind to it. The volumes of a path/url workspace are already reclaimed on the rm path, so "the rest may be ours" is answered already is. (agent)
  • Docker images and build cachedl removes exactly the references devpod recorded: unforced docker image rm at delete beside the volume sweep, and on --prune every recorded reference that is not a live workspace's current one. That last clause reaches the rebuild orphans behind the 86.5 GB without a pattern — the superseded tag was named by a real record when it was current, and devlaunch simply declines to discard the name, so the population stops growing. clients/docker.rs gains remove_image and deliberately no listing verb, so a pattern sweep is inexpressible rather than merely unwritten. MergedConfig.image non-null separates a reference the devcontainer named (alpine:3.20, never touched) from one devpod derived, as a two-arm type whose Derived arm is the only constructor for a removable reference. The existing 86.5 GB, never-completed creates and devpod build leftovers stay, refused as a design. Build cache: nothing, deliberately, on a different argument — buildkit's own GC already bounds it (measured: v0.32.2, rule#1 keeps 1440h under reserved 43.77 GiB), which is principle 2's shared-cache exception rather than an abstention. (agent)
  • Orphaned volumes with no surviving record — the existing 39 volumes / 37.28 GB stay, because every route to them is a pattern and dind-var-lib-docker-<devcontainerId> is a shape every docker-in-docker devcontainer produces, so subtracting devpod's live list narrows towards volumes whose owner devlaunch cannot see rather than towards its own. Future ones are covered by a second read of one document: the same two SubstitutionContext fields, taken at the tail of a completed up and kept as a per-workspace marker, so no name is ever synthesized and Remove the workspace's pixi and dind volumes on delete #325's rule is untouched. The copy can be wrong in exactly two ways, both caught by something other than trust (an already-gone volume is a --force no-op; a held volume is a docker refusal, measured on 29.7.2). It cannot be wrong the way a pattern is. Built by #456. (agent)
  • Does devpod record the image it built for a workspace — Yes: workspace_result.json.ContainerDetails.Config.Image, the same file the volume sweep already parses, uniform across build, pull and prebuild paths and unchanged since loft-sh v0.1.0. So the read-a-record precedent transfers with no architecture change. But it is a lower bound, not the set: no record for a failed create, the record is destroyed by devpod delete while the image stands, and a rebuild leaves the old tagged image unnamed, which is the actual 86.5 GB mechanism. 9 of 13 measured images were record-named. No devpod up flag can label an image, and the one route that could moves the prebuild tag hash. (agent)
  • Nested worktree guarantees and git worktree prune's real blast radiusgit worktree remove <recorded path> is a supported single-registration prune: it works precisely because a container path does not resolve, refuses when the path resolves to something unrelated, and cannot be forced past that refusal. So the clone-wide metadata prune is unnecessary rather than needing a gate. And git records no parent-child relation between worktrees, so containment exists only as a prefix relation between recorded path strings, and a parent's removal destroys a nested worktree's work silently, lock included. Measured identically on git 2.30.2, 2.34.1, 2.43.0 and 2.51.1. (agent)

Not yet specified

  • The .claude/worktrees/ place is not devlaunch's to define. A foreign worktree lands there by nothing more than a cd and a git worktree add. Whether devlaunch should mark the place it reasons about is the same shape as #451's marker question, and #468's CACHEDIR.TAG read is the precedent for reading a published marker rather than minting one. Not blocking anything.
  • The clone's own top-level .pixi/envs, which sits outside dl --prune cannot reclaim agent worktrees inside live workspaces' clones (104 GB measured) #426's 104.5 GB and so outside #468's reach. A readable gate is available in devpod's own running/stopped listing, which is the same read-a-record discipline the rest of the map settled on, so this is a ticket waiting for someone to want it rather than an open question.
  • DEVLAUNCH_WORKSPACE_ID is one --workspace-env away from being the only sound self-identification devlaunch owns. It goes out via --init-env today and never reaches inside the container, which is why #462 found no soundly implementable environment predicate. Nothing on this map needs it, so it stays fog; it becomes a ticket the moment a surface wants to ask "am I inside a workspace, and which one".
  • Whether config.toml should exist at all. With repos_dir retired it has one live knob (fetch_interval) and four dead ones: enabled, cleanup.auto_prune, cleanup.prune_after_days and fallback_image are parsed, defaulted, unit-tested, published on the binary API surface, and consumed by nothing outside config.rs. Left as fog rather than ticketed because auto_prune and prune_after_days are plausibly knobs this map's own destination wants, and deciding them before the reclaim surfaces are settled would be guessing.
  • Whether devlaunch should prevent the duplicated .pixi/envs copies rather than only reclaim them. 18 copies is why dl --prune cannot reclaim agent worktrees inside live workspaces' clones (104 GB measured) #426 measured 104 GB and not ~10, and no map owns the duplication itself.

Out of scope

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions