feat(gc): sandy --gc — unified Docker-resource reclaim (#36) - #63
Merged
Conversation
Adds a `sandy --gc` fast-path command (in the --print-state/--prune-orphans family — pre-preflight, needs only a reachable Docker daemon) that reclaims the resources sandy leaks during normal use: - dead-owner containers (sandy-*/sandy-proxy-* whose owning session is dead) - orphaned sandy_* networks (folds in the existing reaper) - orphaned per-project (sandy-project-<name>) and skill (sandy-skills-*) images whose sandbox/config no longer references them - dangling <none> sandy images, scoped by a new `sandy.managed=1` build label so a stranger's dangling image is never in range Sub-flags --dry-run / --yes mirror --update-sessions (nothing-to-do exits 0; non-TTY without --yes when there IS something to reclaim → exit 1; TTY → y/N). Reap order is containers → networks → images so rmi (no -f) actually frees. Safety (never reap a live session's resource): - Container liveness reuses the "container is truth only with a live inner tmux session" rule (D6/DEC-B/--stop), NEVER trusting sandy.daemon_pid — the D9 reboot case (--restart resurrects a container whose supervisor pid is dead) is kept alive by probing `tmux has-session`. - Agent-vs-proxy is discriminated by IMAGE, not name prefix; a proxy's liveness follows its paired agent (two-pass classifier). This keeps a live daemon's proxy when its supervisor was killed, and stops a workspace literally named `proxy` from being mis-stripped and reaped. - The reaper's daemon has-session probe uses the same 5x/sleep-1 mid-startup retry as --start so a concurrent launch isn't reaped out from under itself; the informational --print-state count stays single-probe. Adds two full-mode-only --print-state keys (dangling_images, orphaned_containers) — null in light mode, zero added light-mode spawns. schema_version stays 1. --prune-orphans is kept unchanged as a documented network-only subset (shares the lister/reaper, can't drift). Real reclaim against live Docker is deferred to a maintainer-run acceptance harness; run-tests.sh §79 covers structure + stubbed-docker behavior (49 assertions, incl. the D9-alive and unlabeled- dangling-untouched safety cases). Plan(fable)/implement(sonnet)/verify(opus) loop: verify caught two live-session reap paths (proxy strand + `proxy`-named workspace), both fixed and re-verified. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #36.
Problem
Sandy accumulates Docker resources during normal use with no reclaim path: dangling
<none>images from same-tag rebuilds, orphaned per-project/skill images, and dead-owner containers left bySIGKILL/OOM where thecleanup()trap never fired. Networks were already handled (--prune-orphans); nothing else was.What this adds
sandy --gc— a global, workspace-independent fast-path command (same pre-preflight family as--print-state/--prune-orphans; needs only a reachable Docker daemon) that reclaims all four resource types:sandy-*/sandy-proxy-*whose owning session is dead_sandy_reap_orphan_networkssandy-project-<name>/sandy-skills-*no longer referenced by a sandbox/config<none>sandy images — scoped by a newsandy.managed=1build label so a stranger's dangling image is never touched--dry-run/--yesmirror--update-sessions(nothing-to-do → exit 0; non-TTY with something to reclaim and no--yes→ exit 1; TTY →y/N). Reap order is containers → networks → images sodocker rmi(no-f) actually frees. Two new full-mode-only--print-statekeys —dangling_images,orphaned_containers(null in light mode, zero added light-mode spawns).schema_versionstays 1.Safety — never reap a live session's resource
--stop), never trustingsandy.daemon_pid— so the D9 reboot case (a container--restart-resurrected with a dead supervisor pid) stays alive via atmux has-sessionprobe.proxyfrom being mis-stripped.sleep 1mid-startup retry as--startso a concurrent launch isn't reaped out from under itself.Process
Plan (fable) → implement (Sonnet 5) → adversarial verify (Opus 4.8). The verify pass caught two live-session reap paths (proxy strand of a supervisor-less daemon;
proxy-named workspace) + two majors (mid-startup race; skills in-use set ignoring host-globalSANDY_SKILL_PACKS). All four fixed and re-verified: SHIP — no new false-positive paths, tests genuinely bind.Tests
run-tests.sh §79— 49 assertions: structural + a stateful stubbed-dockerharness covering the D9-alive-survives, unlabeled-dangling-untouched,proxy-paired/proxy-named, orphan-proxy, mid-startup-retry, and host-config-skills cases. Real reclaim against live Docker is deferred to a maintainer-run acceptance harness (like other Docker-runtime features).Notes
--prune-orphanskept unchanged as a documented network-only subset (shares the lister/reaper, can't drift).sandy.managed=1label and won't be caught by the dangling-image step — self-heals as images churn.Additive-only (1.3.0); sandbox forward-compat holds.
🤖 Generated with Claude Code