Goal
Run periodic guest-state maintenance on a timer, driven by minvmd: a cache sweep (delete stale build artifacts and dead sandbox/task/temp dirs) followed by fstrim on /var/lib/minimal, so both the guest filesystem and the host's backing raw image stay bounded over a long-lived VM's life.
The two halves are ordered and both required: the sweep frees blocks inside ext4, the trim returns those blocks to the host image. Neither alone is sufficient.
Motivation
Nothing reclaims guest state today. cache/built grows monotonically, and each session keeps its own uploaded workspace tree for the session's lifetime. mip cache clean exists but is a manual, project-scoped command that nobody runs inside the VM.
Deleting files does not shrink the host image. The state volume is mounted with MS_NOATIME only — no discard option (crates/minimald/src/guest.rs:439). Measured during #658 / #583 on libkrun 1.19.0: rm alone reclaims nothing on the host, while guest fstrim /var/lib/minimal took a host image from 2061 MiB down to 11 MiB — libkrun's virtio-blk advertises discard and forwards virtio UNMAP to an APFS hole-punch. Without a trim, data-vol.raw is a high-water mark of everything ever written to the volume, and it only ever goes up.
Evidence
From a user support bundle collected 2026-07-24 on 0.5.0-rc2.dev.36.g5720bf64 — a MacBook Air running two sessions against the same repo, roughly two weeks of light use since the volume was created:
| Subtree |
Size |
Files |
cache/built |
1.44 GB |
18,429 |
sessions/425ba (tree + home) |
0.80 GB |
2,996 |
sessions/e976c (tree + home) |
0.79 GB |
2,557 |
Each session tree holds its own copy of the same 726 MB git pack, uploaded once per session. Nothing in the system will ever remove any of it.
Worth being precise about what this is not: the volume is not currently bloated. ext4 reports 15.7 GiB used, but ~12.5 GiB of that is ext4's default 5% root reserve on a 251 GiB filesystem, leaving ~3.4 GiB of real data — consistent with the table above plus journal and metadata. This issue is about unbounded growth over time, not a present-day leak.
Side observation from the same bundle, probably its own cleanup: the host state dir carries two 256 GiB-nominal raw volumes — the live providers/local-0/data-vol.raw and a stale minvmd/data-vol.raw from the pre-provider layout, last touched two weeks earlier. (Apparent sizes; the listing does not carry allocated block counts, so how much either actually occupies is unknown.)
What already exists
- The sweep logic.
crates/mip/src/cmd_cache.rs already implements it: read-tracker atimes via Cache::atimes(), an --older-than cutoff defaulting to 14d, entries with no recorded use at all, plus reaping sandbox/task/temp dirs whose owning pid is gone (a /proc/<pid> check, so guest-side only — which is where we want it anyway).
- The host→guest control pattern.
minvmd's rpc_client.rs already drives the guest through the Shutdown RPC to make it drain sessions and quiesce the volume. A maintenance trigger is the same shape.
fstrim in the guest. The rootfs ships util-linux and e2fsprogs (rootfs #365), and the production-path reclaim was verified through it.
What's missing
- Reuse of
cache clean is not a drop-in. It is project-scoped: it protects ctx.scaffolding_packages() for the current project. A daemon-side sweep has no single project and must instead protect the union of packages referenced by every live session, or it will delete cache entries out from under a running session. This is the main correctness question in the issue.
- No scheduler in
minvmd. Only ad-hoc tokio::time::sleep calls in net.rs. This needs a real interval task with a configurable period.
- No maintenance RPC. Whatever triggers the guest-side work needs a verb; today only Shutdown exists.
Design notes
Schedule on the host, not in the guest. The guest clock is not trustworthy for scheduling: it sets from the pl031 RTC once at boot and nothing resyncs it on host wake, so it drifts by the host's accumulated sleep. The same support bundle above shows the guest 6 h 36 m behind the host — its two halves are timestamped 14:01 and 20:37 for a collection that took 2.5 seconds. A guest-side cron would fire on a clock that silently loses hours a day; a host-side timer would not. (The clock drift is a real defect in its own right and should be filed separately.)
That also matches the Shutdown precedent — policy stays on the host, the guest executes — and it keeps working when the guest is wedged enough to miss its own timer.
Ordering and observability. Sweep, sync, settle, then trim; measuring the trim too soon after the deletes reads as "no reclaim." Log bytes deleted and bytes trimmed (FITRIM reports the trimmed count) — without that, a silent no-op is indistinguishable from a healthy run.
Concurrency. Online fstrim on a live ext4 is safe, but FITRIM holds block-group locks and can be slow, so it should defer rather than contend when a build is in flight. Distinct from the shutdown quiesce path: this is steady-state maintenance, not teardown.
Open questions
- Retention policy: inherit
cache clean's 14d default, or make it a minvmd.toml knob? Should the trim run on a different (longer) period than the sweep?
- Does the sweep also touch session trees for stopped/destroyed sessions, or is that out of scope for a first cut?
- Should the timer be suppressed while the host is on battery? On the sampled host, the VM lives through heavy clamshell sleep cycling.
Acceptance
- A timer in
minvmd fires on a configurable interval and drives the guest through sweep-then-trim.
- Both steps log what they reclaimed.
- Host-side proof:
st_blocks * 512 for providers/<name>/data-vol.raw measurably drops after a run that sweeps a large stale cache entry.
- No live session loses a cache entry it depends on.
Goal
Run periodic guest-state maintenance on a timer, driven by
minvmd: a cache sweep (delete stale build artifacts and dead sandbox/task/temp dirs) followed byfstrimon/var/lib/minimal, so both the guest filesystem and the host's backing raw image stay bounded over a long-lived VM's life.The two halves are ordered and both required: the sweep frees blocks inside ext4, the trim returns those blocks to the host image. Neither alone is sufficient.
Motivation
Nothing reclaims guest state today.
cache/builtgrows monotonically, and each session keeps its own uploaded workspace tree for the session's lifetime.mip cache cleanexists but is a manual, project-scoped command that nobody runs inside the VM.Deleting files does not shrink the host image. The state volume is mounted with
MS_NOATIMEonly — nodiscardoption (crates/minimald/src/guest.rs:439). Measured during #658 / #583 on libkrun 1.19.0:rmalone reclaims nothing on the host, while guestfstrim /var/lib/minimaltook a host image from 2061 MiB down to 11 MiB — libkrun's virtio-blk advertises discard and forwards virtio UNMAP to an APFS hole-punch. Without a trim,data-vol.rawis a high-water mark of everything ever written to the volume, and it only ever goes up.Evidence
From a user support bundle collected 2026-07-24 on
0.5.0-rc2.dev.36.g5720bf64— a MacBook Air running two sessions against the same repo, roughly two weeks of light use since the volume was created:cache/builtsessions/425ba(tree + home)sessions/e976c(tree + home)Each session tree holds its own copy of the same 726 MB git pack, uploaded once per session. Nothing in the system will ever remove any of it.
Worth being precise about what this is not: the volume is not currently bloated. ext4 reports 15.7 GiB used, but ~12.5 GiB of that is ext4's default 5% root reserve on a 251 GiB filesystem, leaving ~3.4 GiB of real data — consistent with the table above plus journal and metadata. This issue is about unbounded growth over time, not a present-day leak.
Side observation from the same bundle, probably its own cleanup: the host state dir carries two 256 GiB-nominal raw volumes — the live
providers/local-0/data-vol.rawand a staleminvmd/data-vol.rawfrom the pre-provider layout, last touched two weeks earlier. (Apparent sizes; the listing does not carry allocated block counts, so how much either actually occupies is unknown.)What already exists
crates/mip/src/cmd_cache.rsalready implements it: read-tracker atimes viaCache::atimes(), an--older-thancutoff defaulting to 14d, entries with no recorded use at all, plus reaping sandbox/task/temp dirs whose owning pid is gone (a/proc/<pid>check, so guest-side only — which is where we want it anyway).minvmd'srpc_client.rsalready drives the guest through the Shutdown RPC to make it drain sessions and quiesce the volume. A maintenance trigger is the same shape.fstrimin the guest. The rootfs shipsutil-linuxande2fsprogs(rootfs #365), and the production-path reclaim was verified through it.What's missing
cache cleanis not a drop-in. It is project-scoped: it protectsctx.scaffolding_packages()for the current project. A daemon-side sweep has no single project and must instead protect the union of packages referenced by every live session, or it will delete cache entries out from under a running session. This is the main correctness question in the issue.minvmd. Only ad-hoctokio::time::sleepcalls innet.rs. This needs a real interval task with a configurable period.Design notes
Schedule on the host, not in the guest. The guest clock is not trustworthy for scheduling: it sets from the pl031 RTC once at boot and nothing resyncs it on host wake, so it drifts by the host's accumulated sleep. The same support bundle above shows the guest 6 h 36 m behind the host — its two halves are timestamped 14:01 and 20:37 for a collection that took 2.5 seconds. A guest-side cron would fire on a clock that silently loses hours a day; a host-side timer would not. (The clock drift is a real defect in its own right and should be filed separately.)
That also matches the Shutdown precedent — policy stays on the host, the guest executes — and it keeps working when the guest is wedged enough to miss its own timer.
Ordering and observability. Sweep, sync, settle, then trim; measuring the trim too soon after the deletes reads as "no reclaim." Log bytes deleted and bytes trimmed (FITRIM reports the trimmed count) — without that, a silent no-op is indistinguishable from a healthy run.
Concurrency. Online
fstrimon a live ext4 is safe, but FITRIM holds block-group locks and can be slow, so it should defer rather than contend when a build is in flight. Distinct from the shutdown quiesce path: this is steady-state maintenance, not teardown.Open questions
cache clean's 14d default, or make it aminvmd.tomlknob? Should the trim run on a different (longer) period than the sweep?Acceptance
minvmdfires on a configurable interval and drives the guest through sweep-then-trim.st_blocks * 512forproviders/<name>/data-vol.rawmeasurably drops after a run that sweeps a large stale cache entry.