Skip to content

Auto-reclaim the container after a session, never the clone #172

Description

@blooop

Type: build
Map: #139

User steer (2026-08-09): "in terms of disk performance, consider how to auto prune containers when they are done." This reopens a line the map currently scopes out, deliberately and with a stated argument — so the argument has to be answered rather than stepped around.

What stands in the way, and why it does not actually block this

Two prior decisions look like they forbid this. Neither forbids what is proposed here, and the distinction is the whole design.

workspace_state.py's module docstring is explicit that dl does not decide when work is finished:

whether a piece of work is finished is a fact about a ticket, a review or a person's intent, and dl knows about none of those. It knows about clones and containers. … The alternative — dl inferring "finished" from the branch (merged into the default, or deleted from the remote) — was built first and thrown away. It reads as a git fact but it is a guess at intent.

And #155 put automatic pruning out of scope because a full orphan scan measured 1017 ms (418 ms devpod list + 593 ms of unsaved probes), which cannot sit on the launch path.

Both objections are about deleting the clone directory. The clone is where work lives, deletion is irreversible, and a wrong guess destroys the only copy — so "dl must not guess at intent" is exactly right there, and this ticket does not touch it.

The container is a different object with a different reversibility. The workspace clone is bind-mounted into the container from the host (dl.py:595), so nothing durable lives only inside it. Deleting the container destroys: the writable layer, the ~342 MB lent tools payload, shell history, and anything written outside the mount. The cost of deleting one that was still wanted is therefore a cold relaunch — latency, not lost work.

That is what licenses an automatic decision here and not there. This ticket does not infer "done" and must not: it makes a reversible reclamation on a cheap trigger, and says so.

#155's 1017 ms objection also does not reach this, twice over: the evaluation is scoped to the single workspace that just exited (no devpod list, no orphan scan), and it runs in a detached child, so the foreground path costs nothing at all.

The build

Reclaim the container, never the clone. Explicitly out of scope: clone directories (that is --prune, #159), Docker images and volumes (#155 settled that devlaunch neither deletes nor inventories images — devpod images carry no identifying label).

Trigger. At the end of a launch, in a detached process, reusing the existing spawn shape — update_cache_background already does subprocess.Popen(..., start_new_session=True) at dl.py:429 and is already fired with force=True when a workspace is created, stopped or deleted. Evaluate only the workspace this process just used.

Concurrency. A workspace may be in use by another session. worktree/locks.py hold_lock (PR #138) already exists per workspace; the reclamation must take it non-blockingly and decline when it cannot — the same try_hold_lock shape #144 established for the detached updater. Never wait, never delete a workspace another session holds.

Constructive modeling. The outcome is a total sum, not a bool and not an Optional: reclaimed / declined(reason), with every unreadable or ambiguous reading landing in declined. Optional[str] conflating "nothing to lose" with "could not tell" is precisely the defect #171 documents in the neighbouring guard; do not reproduce its shape here.

Off by default. This trades relaunch latency for disk on a map whose destination is latency, so it is a policy the operator opts into, and the help text must state the cost in plain terms ("the next launch of this workspace will be a cold start"). Do not make it the default in this ticket.

Measure it. Per the map's standing rule: report bytes actually freed by deleting one container, and the cold-relaunch cost incurred. Both numbers, not one.

Test-first

Guards must be shown red under a mutation that reintroduces the fault, not merely green — this map has found hollow guards repeatedly (#145's leaked memo, #171's two uncovered arms). At minimum:

  • the clone directory survives container reclamation, including a clone holding uncommitted and untracked work (this is the safety claim; it must fail if the code ever reaches the clone);
  • reclamation declines when the per-workspace lock is held, and does not block;
  • the foreground launch path spawns no additional round trip — test/test_devpod_spawn_counts.py pins argv sequences and is the natural home;
  • disabled-by-default is asserted by a test that fails if the default flips;
  • the outcome sum is exhaustive (ty), with no arm reachable only by a sentinel.

Blocked by nothing. Related: #171 (the clone guard is unsound — a further reason this ticket stays away from the clone), #159 (--prune, parked), #155 (Docker boundary).

Activity

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