feat(scripts): add disk reclaim sweep with build-artifact retention - #349
Merged
Conversation
The machine hit 5.2 GB free of 926 GB. Reclaiming was a manual hunt each time, and nothing aged out build artifacts, so dead target/ and .venv dirs accumulated indefinitely — 103 GB sat in seven Rust target dirs alone. Adds two deterministic scripts plus a weekly unattended run: - reclaim.sh splits artifacts by staleness. Directories whose mtime is older than --days are removed whole; still-active Rust targets instead get `cargo sweep --time`, which trims stale artifacts from within a live target/ so recent builds stay warm. Package caches (uv, bun, go, brew, pre-commit, docker build) are pruned alongside. - ollama-idle.sh ranks models by blob atime rather than `ollama list`'s MODIFIED column, which reports pull time and so cannot distinguish a model used daily from one downloaded once and never run. Twelve models totalling 185 GB had not been read in 47+ days. The rollup reports artifact and cache figures separately and does not sum them: whole-dir removals are exact, while `uv cache prune` and `pre-commit gc` evict only unreferenced entries, so cache size is an upper bound on what they actually free. Ollama pruning is deliberately excluded from the scheduled job — re-pulling a 25 GB model is slow enough to stay a deliberate choice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FUeU4LVGqKNPF7wrRFRRpf
Contributor
|
🤖 Claude analyzed the CI failures but determined no code changes are needed. Failed workflow: https://github.com/laurigates/dotfiles/actions/runs/30831652472 This may indicate:
Please review the failure logs manually. |
laurigates
added a commit
that referenced
this pull request
Aug 4, 2026
Follow-up to #349, which shipped a rollup that overstates what the sweep frees. ## The bug The rollup summed `du` output into one headline number. Measured on this machine: | | | |---|---| | What #349 would have printed | **68.7 GB** | | What the sweep actually freed (`df` delta) | **17.7 GB** | Off by roughly 4×. ## Why `du` sums `st_blocks`, and APFS reports a copy-on-write clone at **full size** even though its blocks are shared and cost nothing to hold. - **uv** and **bun** materialise `.venv` / `node_modules` from a global package cache via `clonefile`. Verified empirically: a second project installing the same package cost **0 bytes** of real disk while `du` billed it at full size. Removing such a tree frees real space only where it holds the *last* reference to those blocks. - **cargo** is the exception. Every project compiles its own copy of each dependency — there's no shared compiled-artifact cache (that's what `sccache` is for) — so `target/` dirs are genuinely distinct bytes and `du` is accurate. This matches the original session: deleting 7 target dirs moved `df` by 103.8 GB against a `du` estimate of 103 GB. ## The fix Splits the estimate by error mode instead of summing figures that aren't comparable: ``` ARTIFACT_EXACT_GB=12.6 (cargo target/ — du is accurate here) ARTIFACT_BOUNDED_GB=17.3 (node_modules/.venv — upper bound, CoW clones) CACHE_SIZE_GB=38.8 (upper bound; prune/gc evict only unused entries) ACTUAL_FREED_GB=17.7 (df delta — ground truth) ``` `ACTUAL_FREED_GB` supersedes all three under `--apply`. Internal consistency holds: actual (17.7) exceeds `ARTIFACT_EXACT` (12.6), as it must if cargo targets are real bytes and the bounded figure is mostly shared. ## This is not fixable by changing tools Every tool that reads `st_blocks` double-counts clones identically — verified against a 200 MB file plus one clone (ground truth 200 MB): | tool | reports | |---|---| | BSD `du` | 400M | | GNU `du` | 400M | | `dust` 1.2.4 | 400M | | `dust -s` | 600M | | `df` delta | **0 MB for the clone** ✅ | A `df` delta is the only clone-aware measurement available on macOS. Reported upstream as [bootandy/dust#590](bootandy/dust#590), with a proposed fix via `fcntl(F_LOG2PHYS_EXT)` physical-extent dedup (clones share device offsets while having distinct inodes). ## Verification - `shellcheck` clean - Dry run and a real `--apply` both exercised; the 17.7 GB figure above is from that run - Applied with path-scoped `chezmoi apply`, leaving three pre-existing drifted targets untouched 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FUeU4LVGqKNPF7wrRFRRpf Co-authored-by: Claude Opus 5 <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.
Context
The machine had 5.2 GB free of 926 GB (APFS container 99.4% used). Reclaiming was a manual hunt every time, and nothing aged out build artifacts — 103 GB sat in seven abandoned Rust
target/dirs alone, and 185 GB of ollama models had not been read in 47+ days.Reclaimed this session: 5.2 GB → 325 GB free.
What this adds
scripts/reclaim.shSplits build artifacts by staleness, which is the part that needed thinking about:
--daystarget//node_modules//.venv/--days(Rust only)cargo sweep --time <days>inside itPlus regenerable package caches: uv, bun, go modcache, Homebrew downloads, pre-commit, docker build cache.
Dry-run by default;
--applydeletes. Emits the structuredKEY=VALUErollup convention so a caller reads a verdict rather than re-deriving the computation.scripts/ollama-idle.shRanks models by blob atime, not
ollama list's MODIFIED column — that reports pull time, so a model downloaded once and never run looks fresher than one used daily. Verified atime is meaningful here: neither/nor/System/Volumes/Datamountsnoatime.just -grecipes + weekly launchd agentreclaim-dry,reclaim,ollama-idle,ollama-pruneinmaint.just.com.lgates.reclaimruns the sweep unattended Sundays 09:23, logging to~/Library/Logs/reclaim.run.log.Two deliberate choices
uv cache pruneandpre-commit gcevict only unreferenced entries, so cache size is an upper bound on what they free. An earlier draft summed them and claimed 67.5 GB reclaimable when most of that was live uv cache.Verification
shellcheckclean on both scripts;plutil -lintOK on the plistjust -g --listrenders all four recipeschezmoi apply, leaving three pre-existing drifted targets untouched (confirmed viachezmoi status)Not addressed
~/.cache/uv(36 GB) could not be cleared — the lock is held by four liveuvprocesses runningpal-mcp-serverandpython-lsp-serverinterpreters out of~/.cache/uv/archive-v0/. Forcing it would delete files under running processes. The weekly job usesuv cache pruneand tolerates the failure.🤖 Generated with Claude Code
https://claude.ai/code/session_01FUeU4LVGqKNPF7wrRFRRpf