feat(cleanup): walk every backend, reap smolmachines orphans too #79

Merged
didericis merged 1 commits from cleanup-cross-backend into main 2026-05-27 20:01:32 -04:00
Collaborator

Summary

./cli.py cleanup walked only the env-var-selected backend, so a leftover smolvm machine / bundle container / bundle network from a crashed smolmachines bottle survived a default docker-mode cleanup indefinitely. This PR makes cleanup walk every backend.

Changes

  • Smolmachines cleanup module. New backend/smolmachines/cleanup.py enumerates:

    • smolvm machines named claude-bottle-* (via smolvm machine ls --json)
    • bundle containers claude-bottle-sidecars-*
    • bundle networks claude-bottle-bundle-*

    cleanup runs stop+delete on the machines, force-rm on the containers, network rm on the networks. Each step is best-effort so a failed rm doesn't block the rest. SmolmachinesBottleCleanupPlan now actually carries something. Docker presence is checked via has_backend('docker') (centralized helper), late-imported to dodge the cycle.

  • cli/cleanup.py walks every backend in known_backend_names(). One y/N prompt against a combined plan; each backend's cleanup runs in turn if confirmed.

  • Docker orphan-state-dir respects all-backend live set. State dirs (~/.claude-bottle/state/<slug>/) are shared layout. Docker remains the single owner of the orphan-state-dir bucket (only one home for it) but now consults enumerate_active_agents() for the cross-backend live identity set, so a running smolmachines bottle's state dir isn't reaped while the VM is up. _list_orphan_state_dirs(live_projects, protected_identities) — second arg is the union.

Tests

  • test_smolmachines_cleanup.py: prepare-cleanup enumeration; cleanup ordering (machines → bundles → networks); failure-handling (best-effort, doesn't bail).
  • test_cli_cleanup_cross_backend.py: cmd_cleanup walks both backends; short-circuits on all-empty; aborts on N; skips empty-plan backends.
  • test_docker_cleanup.py: new test_protected_identity_skips_dir + test_protected_overrides_no_live_project for the cross-backend protection.

619 unit tests pass.

End-to-end check

$ smolvm machine ls --json | jq '.[].name'
"claude-bottle-researcher-m3hxd"
$ ./cli.py cleanup
--- smolmachines backend ---
smolvm machine:  claude-bottle-researcher-m3hxd
remove all of the above? [y/N]

Stacking

Rebased onto current main (5e0130b) — PR #78 and PR #80 are already merged in, so enumerate_active_agents(), known_backend_names(), and has_backend() are available.

## Summary `./cli.py cleanup` walked only the env-var-selected backend, so a leftover smolvm machine / bundle container / bundle network from a crashed smolmachines bottle survived a default `docker`-mode cleanup indefinitely. This PR makes cleanup walk every backend. ## Changes - **Smolmachines `cleanup` module.** New `backend/smolmachines/cleanup.py` enumerates: - smolvm machines named `claude-bottle-*` (via `smolvm machine ls --json`) - bundle containers `claude-bottle-sidecars-*` - bundle networks `claude-bottle-bundle-*` `cleanup` runs stop+delete on the machines, force-rm on the containers, network rm on the networks. Each step is best-effort so a failed rm doesn't block the rest. `SmolmachinesBottleCleanupPlan` now actually carries something. Docker presence is checked via `has_backend('docker')` (centralized helper), late-imported to dodge the cycle. - **`cli/cleanup.py`** walks every backend in `known_backend_names()`. One y/N prompt against a combined plan; each backend's `cleanup` runs in turn if confirmed. - **Docker orphan-state-dir respects all-backend live set.** State dirs (`~/.claude-bottle/state/<slug>/`) are shared layout. Docker remains the single owner of the orphan-state-dir bucket (only one home for it) but now consults `enumerate_active_agents()` for the cross-backend live identity set, so a running smolmachines bottle's state dir isn't reaped while the VM is up. `_list_orphan_state_dirs(live_projects, protected_identities)` — second arg is the union. ## Tests - `test_smolmachines_cleanup.py`: prepare-cleanup enumeration; cleanup ordering (machines → bundles → networks); failure-handling (best-effort, doesn't bail). - `test_cli_cleanup_cross_backend.py`: cmd_cleanup walks both backends; short-circuits on all-empty; aborts on N; skips empty-plan backends. - `test_docker_cleanup.py`: new `test_protected_identity_skips_dir` + `test_protected_overrides_no_live_project` for the cross-backend protection. 619 unit tests pass. ## End-to-end check ``` $ smolvm machine ls --json | jq '.[].name' "claude-bottle-researcher-m3hxd" $ ./cli.py cleanup --- smolmachines backend --- smolvm machine: claude-bottle-researcher-m3hxd remove all of the above? [y/N] ``` ## Stacking Rebased onto current main (`5e0130b`) — PR #78 and PR #80 are already merged in, so `enumerate_active_agents()`, `known_backend_names()`, and `has_backend()` are available.
didericis changed target branch from cli-backend-aware-list-and-flag to main 2026-05-27 19:18:55 -04:00
didericis force-pushed cleanup-cross-backend from 4859040c6f to 5323fc1b53 2026-05-27 19:21:28 -04:00 Compare
didericis force-pushed cleanup-cross-backend from 5323fc1b53 to 346367ca32 2026-05-27 19:45:58 -04:00 Compare
didericis added 1 commit 2026-05-27 19:57:02 -04:00
feat(cleanup): walk every backend, reap smolmachines orphans too
test / unit (pull_request) Successful in 26s
test / integration (pull_request) Successful in 43s
test / unit (push) Successful in 29s
test / integration (push) Successful in 38s
a3a9ec065e
`./cli.py cleanup` previously called only the env-var-selected
backend's `prepare_cleanup` / `cleanup` — so a leftover smolvm
machine + bundle container + bundle network from a crashed
smolmachines bottle would survive a default `docker`-mode cleanup
indefinitely.

Smolmachines now has a real `cleanup` module (alongside
`enumerate.py` from issue #77) that walks:

  - smolvm machines named `claude-bottle-*` (via
    `smolvm machine ls --json`)
  - bundle containers `claude-bottle-sidecars-*`
  - bundle networks `claude-bottle-bundle-*`

Cleanup runs stop+delete on the machines, force-rm on the
containers, network rm on the networks. Each step is best-effort
so a failed rm doesn't block the rest.

`cli.py cleanup` walks every backend in `known_backend_names()`
and runs each backend's `cleanup` after a single y/N prompt that
shows a combined plan.

State dirs (`~/.claude-bottle/state/<slug>/`) are shared layout
with the docker backend, which still owns the orphan-state-dir
bucket. It now consults `enumerate_active_bottles()` for the
cross-backend live identity set so a running smolmachines
bottle's state dir isn't reaped during a cleanup.

Tests: smolmachines cleanup (prepare + cleanup ordering + failure
handling); cross-backend orphan protection on the docker
state-dir check; CLI cmd_cleanup walks both backends, short-
circuits on all-empty, aborts on N. 617 unit tests pass.

End-to-end verified on this host:
  $ smolvm machine ls --json | jq '.[].name'
  "claude-bottle-researcher-m3hxd"
  $ ./cli.py cleanup
  --- smolmachines backend ---
  smolvm machine:  claude-bottle-researcher-m3hxd
  remove all of the above? [y/N]

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
didericis force-pushed cleanup-cross-backend from 346367ca32 to a3a9ec065e 2026-05-27 19:57:02 -04:00 Compare
didericis approved these changes 2026-05-27 20:01:19 -04:00
didericis merged commit a3a9ec065e into main 2026-05-27 20:01:32 -04:00
didericis deleted branch cleanup-cross-backend 2026-05-27 20:01:32 -04:00
Sign in to join this conversation.