Skip to content

fix: remove the compose containers a forgotten workspace left running - #682

Merged
blooop merged 22 commits into
mainfrom
fix/rm-leaves-compose-containers
Oct 19, 2026
Merged

blooop merged 22 commits into
mainfrom
fix/rm-leaves-compose-containers

Conversation

@blooop

@blooop blooop commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

After this change, dl rm, dl kill and dl up remove the compose containers that devpod lost track of, so a workspace with a fixed container_name: can come up again at the same path.

Evidence

xychart-beta
    title "Leftover containers still Up: bars = dl 0.60.0, line = this branch"
    x-axis ["kill, compose down fails", "up over leftovers", "rm, record has new uid"]
    y-axis "containers Up" 0 --> 3
    bar [2, 2, 2]
    line [0, 0, 0]
Loading

The chart shows the old compose containers that are still Up after each command: 2 of 2 with dl 0.60.0, 0 with this branch. The source is a throwaway local folder dlleakprobe, a devcontainer with two compose files and two services that set container_name:. Its initializeCommand writes the .env that one compose file needs, as kinisi_ros does. The scripts repro.sh <dl> and repro_up_rm.sh <released dl> <fixed dl> ran on 2026-10-16 against the release build of this branch (it prints dl 0.60.1, the version on main). Each scenario:

  1. dl kill after compose down fails. dl up, delete .devcontainer/.env, then dl kill.
    • dl 0.60.0: devpod logs error deleting container: error=exit status 1, then deleted workspace dlleakprobe. dl kill exits 0, and dlleakprobe_dev_fixed and dlleakprobe_side_fixed stay Up.
    • This branch: Removed containers dlleakprobe left behind: dlleakprobe_dev_fixed, dlleakprobe_side_fixed, and docker ps -a --filter name=dlleakprobe is empty.
  2. dl up over the leftovers.
    • dl 0.60.0: Conflict. The container name "/dlleakprobe_dev_fixed" is already in use. The old containers stay Up.
    • This branch: Removed containers dlleakprobe left behind: dlleakprobe_side_fixed, dlleakprobe_dev_fixed, and then compose starts both services again in a new project (default-dl-3b171 → default-dl-6f164).
  3. dl rm after a failed up made a new record with a new uid (default-dl-899e1).
    • dl 0.60.0: dl rm prints Removed workspace dlleakprobe., exits 0, and the two old containers stay Up.
    • This branch: Removed containers dlleakprobe left behind: dlleakprobe_dev_fixed, dlleakprobe_side_fixed, then Removed workspace dlleakprobe., and no container is left.

dl up exits 1 in every run, also after the fix. The cause is in the probe: its image has no git, so the dotfiles step fails (exec: "git": executable file not found in $PATH) after the containers are up.

What this changes

  • New flow rust/devlaunch-core/src/flows/orphan_containers.rs. It runs only when devpod has no record of the workspace id in any context. It lists the compose containers by label and removes each container whose com.docker.compose.project.working_dir or config_files is inside devpod's directory for that id, or inside dl's clone for that id under <cache>/repos. It never matches by container name.
  • dl rm and dl kill run the flow after devpod delete, when the delete succeeds, fails or times out, and before dl deletes the clone.
  • dl up runs the flow inside the launch lock, before devpod up. For the clone, it uses only the exact clone directory of this workspace id, so dl on a parent directory of clones does not touch other workspaces.
  • The output names each container it removes: Removed containers <id> left behind: <names>. If docker refuses, dl prints a docker rm -f <names> hint. If docker ps fails, dl says that it could not check, with docker's words. The outcome types use NonEmpty<String>, so "removed" and "not removed" always carry at least one name.
  • For a local folder, dl matches only devpod's directory. A compose project that the user started by hand in that folder stays.
  • docs/cleanup.md and CHANGELOG.md describe the new behavior. The public-API snapshot and the documented residual count (40 → 41, because NonEmpty is now part of it) match.

Where this fits

devpod v0.26.3 finds a dev container only by the label dev.containers.id=<uid of the record>, and compose names the project after that uid. When no container carries that uid, the delete succeeds and touches nothing. With --force, devpod drops the record even when compose down failed. The new flow (blue) runs after that point:

---
config:
  flowchart:
    curve: linear
    nodeSpacing: 24
    rankSpacing: 36
    wrappingWidth: 340
---
flowchart TB
    classDef default fill:transparent
    classDef changed fill:transparent,stroke:#2563eb,stroke-width:2px
    classDef outside fill:transparent,stroke-dasharray:5 4
    rm["<b>dl rm / dl kill</b>"] --> del["<b>devpod delete</b><br/>finds containers only by<br/>dev.containers.id=#lt;uid#gt;"]
    up["<b>dl up</b><br/>inside the launch lock"] --> sweep
    del -->|ok, failed or timed out| sweep["<b>orphan_containers::sweep</b><br/>skip if devpod holds a record of the id<br/>list compose containers by label<br/>keep those whose working_dir or<br/>config_files is in devpod's dir for the id<br/>or in dl's clone of the id"]
    sweep -->|docker rm -f| out["<b>stderr</b><br/>Removed containers #lt;id#gt; left behind: ..."]
    sweep --> next["<b>devpod up</b> / delete the clone"]
    class sweep,out changed
    class del,next outside
Loading

Blue boxes are new code. Dashed boxes are code that this PR does not change.

How I checked it

The e2e test rm_removes_the_leftovers_once_devpods_delete_drops_its_record fails without the fix:

devpod has a record of ws; docker ps lists compose containers whose labels name devpod's dir for ws
dl ws rm            -> the fake devpod delete drops the record and touches no container
expect docker calls: ps --all --filter label=com.docker.compose.project ..., rm --force <ids>

A deliberate mutant (the "devpod has no record" check inverted) makes rm_removes_the_containers_devpod_left_behind fail. Each new test passes on this branch and fails when its production code is reverted.

Other new tests cover: dl kill with a dropped record, a delete killed at its deadline, a match by working_dir alone, a docker ps that fails, a workspace id that only starts the same (foo / foo-bar), a local folder, a record that devpod still holds, a launch at a parent directory of clones, and the output text.

Commands, run in the pixi env from rust/:

  • cargo fmt --check and cargo clippy --locked --all-targets -- -D warnings: clean.
  • cargo test --workspace --no-fail-fast: all pass except 5 tests that also fail on origin/main on this host. These are 4 core tests about pushed_by_url, a remote that cannot be reached and prune on fetch, and the e2e a_commit_pushed_by_url_does_not_stop_rm.
  • pixi run pytest: 839 passed, 7 skipped.
  • prek run --from-ref origin/main --to-ref HEAD: all hooks pass.

I did not run scripts/public-api-snapshots.sh on the last commits, because it needs the pinned nightly that this host does not have. I edited public-api.api.txt by hand, and the public_api_snapshots cargo tests pass. CI runs the full check.

Merge danger

Door: two-way. The change adds a step and new output lines. It changes no file format and no stored data. A revert removes it.

Blast radius: medium. The flow runs docker rm --force. If a match is wrong, dl removes a container that belongs to some other work. Three guards limit this: dl acts only when devpod has no record of the id in any context, it matches only on compose labels that point inside devpod's directory or dl's own clone for that id, and it names each container it removes.

Review notes

  • dl kill looks for running containers by label=com.docker.compose.project=<workspace id>. devpod 0.26 names the project by the uid, so kill always prints Nothing on this host is holding workspace <id>. This PR does not fix that.
  • The empty <project>_default networks of the old projects are not removed.
  • If a record exists with a uid that is different from the uid of the old containers, dl up still fails with the name conflict, because devpod holds a record of the id. A dl rm removes the containers.
  • dl --purge does not run the flow. The next launch at that path removes the leftovers.

🤖 Generated with Claude Code

Summary by Sourcery

Clean up compose containers left behind by forgotten workspaces before subsequent lifecycle operations or launches.

Bug Fixes:

  • Remove compose containers left behind after devpod forgets a workspace during rm, kill, or launch, preventing fixed container names from blocking future launches.

Enhancements:

  • Match leftovers using compose metadata tied to the workspace’s devpod directory or managed clone, while preserving containers that may belong to another workspace or manually started local compose project.
  • Report removed or unresolved containers clearly, including Docker errors and a manual removal command when cleanup fails.

Documentation:

  • Document the orphan-container cleanup behavior and its safety rules.

Tests:

  • Add unit, lifecycle, and launch coverage for orphan detection, cleanup timing, record states, Docker failures, nested workspaces, clone boundaries, and output.

Chores:

  • Update public API residual-count documentation and snapshots.

Review fixes

Fixes for the review, one commit per finding:

  1. A sweep removed the live containers of a workspace nested in the clone: ac8696c. A container now stays when a label names another id's devpod directory, or a folder devpod holds as another workspace's source inside the clone. When devpod's records do not read, the clone alone matches nothing.
  2. An unreadable devpod record counted as no record: d878940. record_presence answers Held / Absent / Unknown; only a not-found is Absent, and the sweep runs on Absent alone.
  3. No test ran the launch's sweep: 4d33d05 (the launch test world now has a fake docker).
  4. No test for the launch's NotListed / NotRemoved arms: e15f8ae.
  5. No test for a refused delete that keeps the record: 4ef0fb5.
  6. A partly failed docker rm named every container as not removed: 84373a2. The sweep lists them again and names only those still there.
  7. managed_clone checked only a prefix: fdcce24. Delete and launch share one <owner>/<repo>/<id> shape check.

CHANGELOG entry updated: f7b97a2. The layout guard fix for a finding-2 test: eca5810. No public-api change (all new items are pub(crate)).

devpod delete finds a workspace's container only by
dev.containers.id=<uid of the record it deletes>, and runs compose down
only when it finds it. Two ordinary events leave a compose project up
with nothing that owns it:

- devpod delete --force (what `dl <ws> kill` runs) logs a failed
  compose down and drops the record anyway. A compose file that no longer
  interpolates is enough: a required variable from a .env the
  initializeCommand wrote, gone from the clone.
- A devpod up over the leftovers makes a new record with a new uid and
  fails on their fixed container_name. The next devpod delete looks for
  the new uid, finds no container, and reports success.

The leftovers then fail every later up of the workspace with
"Conflict. The container name ... is already in use".

Once devpod holds no record of the workspace in any context, `rm`, `kill`
and the launch (before its devpod up) now list compose containers and
remove the ones whose working_dir or config_files label names devpod's
agent directory for that id, or the clone dl made. Each removal is named
on stderr. A workspace devpod still holds is left alone; a local folder
is matched by devpod's directory only, so a compose project the user
started there by hand stays.
…iners

sweep_leftovers took any source under the clone root as the workspace's clone, so `dl <cache>/repos/org/repo` matched every live clone of org/repo and force-removed their containers. Only the exact clone dir for this workspace id now counts.
OrphanSweep::NotRemoved used empty names to mean "docker ps failed", and
the renderer branched on that, so a NotRemoved with no names printed the
listing-failure line. A failed listing is now its own arm, NotListed, and
the removed and not-removed names are NonEmpty, so neither sentinel can be
built. The two notice enums follow.
The orphan-sweep tests from the cherry-picked branches built Removed and
LeftoverContainersRemoved from a Vec, and matched a failed listing as a
NotRemoved with no names. Build NonEmpty names, and match the failed
listing as NotListed, which is the shape that answer now has.
…oined it

The leftover-container notices name NonEmpty<String>, which put
domain::workspace_state::NonEmpty in the residual: 41 types now, and the
doc guard that recomputes the count from the snapshots failed.
@sourcery-ai

sourcery-ai Bot commented Oct 16, 2026

Copy link
Copy Markdown

Sorry @blooop, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 3 days and 3 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Oct 16, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR adds a label- and path-based orphan compose-container sweep, guarded by the absence of any devpod workspace record, and invokes it during rm/kill cleanup and before up while preserving local-folder and unrelated-workspace containers. It also adds bounded Docker operations, explicit diagnostics, extensive regression coverage, and documentation/API snapshot updates.

Sequence diagram for orphan container cleanup during workspace lifecycle commands

sequenceDiagram
    actor User
    participant DL as dl rm kill up
    participant Devpod as devpod
    participant Sweep as orphan_containers::sweep
    participant Docker as Docker

    User->>DL: Run lifecycle command
    alt rm or kill
        DL->>Devpod: delete workspace
        Devpod-->>DL: Record removed, failed, or timed out
    end
    DL->>Sweep: sweep workspace_id and managed clone
    Sweep->>Sweep: Check holds_any_record(workspace_id)
    alt devpod still holds a record
        Sweep-->>DL: Nothing
    else no record exists
        Sweep->>Docker: compose_containers()
        Docker-->>Sweep: Compose labels and paths
        Sweep->>Sweep: Match working_dir or config_files
        alt matching containers found
            Sweep->>Docker: remove_containers(ids)
            Docker-->>Sweep: Removal result
            Sweep-->>DL: Removed names or failure details
        else no matches
            Sweep-->>DL: Nothing
        end
    end
    alt up
        DL->>Devpod: up workspace
    else rm or kill
        DL->>DL: Delete managed clone after sweep
    end
Loading

Flow diagram for label- and path-based orphan container matching

flowchart TB
    start["Workspace lifecycle command"] --> record{"Does devpod hold any record for workspace id?"}
    record -->|Yes| keep["Leave containers untouched"]
    record -->|No| list["docker ps --all --filter label=com.docker.compose.project"]
    list --> paths{"working_dir or config_files is inside a trusted path?"}
    paths -->|No| unrelated["Keep unrelated containers"]
    paths -->|Yes| matched["Collect container ids and names"]
    matched --> remove["docker rm --force"]
    remove --> report["Report removed container names"]
    list -->|Docker unavailable or fails| diagnose["Report that leftovers could not be checked"]
    subgraph trusted["Trusted paths"]
        agent["devpod agent directory for workspace id"]
        clone["Exact dl-managed clone for workspace id"]
    end
    agent --> paths
    clone --> paths
Loading

File-Level Changes

Change Details Files
Add a guarded orphan-container sweep that identifies compose containers by workspace directory labels rather than container names.
  • List all compose containers with working-directory and config-file labels.
  • Only sweep when devpod has no record of the workspace in any context.
  • Match devpod agent directories and, for managed workspaces, the exact clone path; exclude local folders and sibling clones.
  • Force-remove matching containers by ID with bounded Docker timeouts and structured outcomes.
rust/devlaunch-core/src/flows/orphan_containers.rs
rust/devlaunch-core/src/clients/docker.rs
rust/devlaunch-core/src/clients/devpod_home.rs
rust/devlaunch-core/src/domain/workspace_state.rs
Integrate orphan cleanup into workspace deletion, killing, and launch lifecycle points.
  • Run cleanup after devpod delete succeeds, fails, or times out, before deleting the clone.
  • Run cleanup inside the launch lock before devpod up.
  • Preserve existing behavior when devpod still owns a record, Docker is unavailable, or no containers match.
rust/devlaunch-core/src/flows/lifecycle/delete.rs
rust/devlaunch-core/src/flows/launch.rs
rust/devlaunch-core/src/flows/mod.rs
Expose explicit cleanup results and actionable diagnostics to users.
  • Add notices for removed, unlisted, and unremovable leftovers.
  • Name every removed or retained container and provide a docker rm -f remediation command on failure.
  • Render consistent messages for rm, kill, and up.
rust/devlaunch-core/src/flows/lifecycle/notices.rs
rust/dl/src/render.rs
Expand coverage for orphan detection, lifecycle integration, path-safety guards, and user-facing output.
  • Add unit and integration tests for dropped records, timeouts, label matching, context ownership, local folders, parent clone paths, Docker failures, and prefix collisions.
  • Update fake Docker/devpod fixtures to model record removal and compose listings.
  • Document the behavior and update changelog/API residual counts.
rust/devlaunch-core/src/flows/orphan_containers.rs
rust/devlaunch-core/src/flows/launch.rs
rust/devlaunch-core/src/flows/lifecycle/tests.rs
rust/dl/tests/lifecycle.rs
rust/dl/tests/lifecycle_scenario.py
rust/dl/src/render.rs
docs/cleanup.md
CHANGELOG.md
docs/development.md
rust/devlaunch-core/public-api.api.txt
rust/devlaunch-core/src/lib.rs
scripts/public-api-snapshots.sh

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov

codecov Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.69240% with 44 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.61%. Comparing base (9af13ca) to head (eca5810).

Files with missing lines Patch % Lines
rust/devlaunch-core/src/flows/lifecycle/delete.rs 75.51% 12 Missing ⚠️
rust/devlaunch-core/src/clients/devpod_home.rs 89.77% 9 Missing ⚠️
rust/devlaunch-core/src/clients/docker.rs 91.48% 8 Missing ⚠️
rust/devlaunch-core/src/flows/orphan_containers.rs 97.85% 8 Missing ⚠️
rust/dl/src/render.rs 88.13% 7 Missing ⚠️
Additional details and impacted files
Flag Coverage Δ
python 42.98% <ø> (ø)
rust 94.81% <94.69%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
shipped code (rust) 94.81% <94.69%> (+0.01%) ⬆️
harness and tooling (python) 42.98% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…from main

main's #673 stack and this branch's NonEmpty each added one residual
type, so after the merge the count is 42.
…61.0

The entry sat under [Unreleased], which main cut into 0.61.0, so the
merge kept it in a released section. It goes back under [Unreleased].

@blooop blooop left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during review.

This PR removes the compose containers that devpod lost track of, on rm, kill and every up. One proven defect blocks the merge: the clone-path match can remove the live containers of another workspace that devpod still holds (inline on orphan_containers.rs:125). Removing the wrong containers is the one outcome this change must never cause.

Ran: Defects, Types, Tests and Mutant (1264 changed lines, so the five-axis split; Spec skipped, because no spec is available: the PR closes no issue). Defects reproduced findings 1 and 2 with probe tests. Mutant: the old prefix rule restored in sweep_leftovers makes a_launch_at_a_clones_parent_leaves_the_clones_containers fail, so the mutant is killed.

Not inline:

  • The refused-delete skip has no test. A binary test with a kept record (--devcontainer-volumes), a failed delete and leave_containers_behind should assert no docker ps and no rm --force. The current kept-record tests write no docker-ps file, so the skip passes trivially.
  • managed_clone (delete.rs:418) checks only starts_with(repos_dir). The launch checks the <owner>/<repo>/<id> shape. Only a hand-edited local_path reaches the gap today.

) -> bool {
container.paths().any(|path| {
home.is_in_agent_dir_of(path, workspace_id)
|| clone.is_some_and(|clone| path.starts_with(clone))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removes another workspace's live containers (reproduced). A local-folder workspace sub at <repos>/o/r/ws/sub has a devpod record. ws has none. dl ws rm / kill / a relaunch of ws sweeps with clone <repos>/o/r/ws. The container of sub has working_dir=<clone>/sub/.devcontainer, so path.starts_with(clone) matches, and the sweep returns Removed(["sub_dev"]) and runs docker rm --force on it. That container's own config_files label names the agent dir of sub, which devpod holds. This breaks the invariant at lines 23-24 and in docs/cleanup.md. Fix: skip a container when any of its labels names the agent dir of a different id that devpod holds. Add the test with exactly this shape.

context
.file_name()
.to_str()
.is_some_and(|name| self.record(name, workspace_id).is_file())

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An unreadable record counts as "no record" (reproduced; Defects and Types both found it). is_file() returns false on EACCES, .flatten() drops entry errors, and a non-UTF-8 context name fails to_str(). Root-owned dirs from container mounts happen on real hosts. With contexts/default/workspaces/ws mode 000, holds_any_record("ws") is false, and the sweep removes ws's containers through the agent-dir match. The doc at 148-150 promises the opposite. Shape: fn record_presence(..) -> Held | Absent | Unknown(io::Error) from fs::metadata, with only NotFound as Absent, and sweep only on Absent.

// under a fixed `container_name` makes that project's create fail. The sweep
// does nothing for a workspace devpod still holds.
if let Some(identity) = request.naming.identity() {
sweep_leftovers(context.runner(), host, identity, request.source, notices);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No test runs this call. leftover_tests calls sweep_leftovers directly, and no launch-flow test or rust/dl/tests/launch.rs test feeds a docker ps row, so deleting this line leaves the suite green. This is the path that runs on every up. Cases to add in rust/dl/tests/launch.rs: (1) a world with no record and one leftover in docker-ps: docker_calls() holds rm --force <id> before devpod up, and stderr names it; (2) a world with a record: docker_calls() is empty, so a normal up costs no docker call.

workspace_id: workspace_id.to_owned(),
names,
}),
OrphanSweep::NotListed { reason } => {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two failure arms (NotListed here, NotRemoved below) have no test. Add cases in leftover_tests: a docker ps that fails gives LeftoverContainersNotListed with docker's words, and a docker rm that fails gives LeftoverContainersNotRemoved with docker's words.

let ids = left.map(|c| c.id.clone());
match docker::remove_containers(runner, &ids) {
docker::Answer::Ran { exit, .. } if exit.is_success() => OrphanSweep::Removed(names),
docker::Answer::Ran { exit, stderr } => OrphanSweep::NotRemoved {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A partly failed rm reports the wrong names (argued). If container a goes away between docker ps and docker rm --force a b, docker exits 1 but still removes b. NotRemoved { names: [a, b] } then tells the user that neither was removed. Nothing is removed in error, but the message is wrong. A docker ps after the failure could name what is really left.

… clone

A local folder opened as its own workspace can sit inside another
workspace's clone. Sweeping the outer workspace matched the inner one's
containers by the clone path alone and removed them, though their labels
named the inner workspace's devpod directory and devpod still held it.

A container now stays when a label names another id's devpod directory,
or a folder devpod holds as another workspace's source inside the clone.
When devpod's records do not read, the clone alone matches nothing.
holds_any_record answered false when a record could not be read: is_file()
is false on EACCES, entry errors were dropped, and a context name that is
not UTF-8 was skipped. The sweep then removed the containers of a workspace
devpod may still hold. record_presence now answers Held, Absent or Unknown,
only a not-found is Absent, and the sweep runs on Absent alone.
No test ran the sweep on the launch path, so deleting it left the suite
green. The launch world now has a fake docker that logs where each call
fell among devpod's, so one test sees the removal land before the up and
another sees a workspace devpod holds cost no docker call.
…ords

The NotListed and NotRemoved arms of the launch's sweep had no test, so
either could go silent unnoticed.
The kept-record tests wrote no docker listing, so the skip passed
trivially. This one puts leftovers on the machine, has devpod refuse the
delete, and sees no docker ps and no docker rm.
docker rm exits non-zero when one of several containers is already gone,
and still removes the rest. The sweep then said none was removed. It now
lists them again and names only those still there; when none is, it says
they were removed.
managed_clone took any recorded local_path under the clone root as the
workspace's clone, so a hand-edited path naming a repository's directory
would match the containers of every clone in it. Delete now uses the
same <owner>/<repo>/<id> shape check as the launch, from one helper.
The review fixes narrowed the rule: a record dl cannot read counts as
held, and a container that names another workspace stays.
The devpod_layout guard allows devpod's on-disk layout only in
devpod_home.rs. The non-UTF-8 context test now writes its record
through a test helper there.
@blooop
blooop merged commit 6c06ec2 into main Oct 19, 2026
15 checks passed
@blooop
blooop deleted the fix/rm-leaves-compose-containers branch October 19, 2026 10:56
@blooop blooop mentioned this pull request Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant