v0.192.0 — the capture floor replaces the bulkhead (R-165, decision B2)
gates / gates (push) Successful in 8s

Ships BEFORE the disk-layout merge it exists for, and is harmless on a box
that never gets it. The mp1 partition was a BULKHEAD as well as a ceiling:
it kept a runaway capture from filling the space the container runtime
needs, because /var/lib/docker was a different filesystem. After the merge
it is the same one, and a full Docker data-root is a stopped box.

The floor sits in captureAllRecoveryUnits, checked BEFORE anything is
written: below the reserve, that ONE app's capture is refused, its previous
unit is left byte-identical, the R-158 alert fires with the space figures,
and the loop continues.

Two terms whichever binds first (97% used / 1 GiB free) in fillwatch's
shape, deliberately BEYOND its critical band (95% / 2 GiB) so the customer
is always warned before a refusal can happen — a floor that fires before
its own warning is a silent failure wearing a threshold.

Headroom, never unit size: a per-unit cap would be R-163 rebuilt inside one
volume. Refuses, never deletes: nothing here is generational, so pruning
could only destroy a different app's only local copy; pruneStalePrimaryDirs
is an orphan sweep, not retention, and must not be repurposed.

Tests 1184 -> 1191. One fixture strengthened mid-red-proof: the "old 20 G
ceiling is gone" test sat at exactly 20 GB and survived a literal
UsedGB > 20 cap — hollow. Now 120 GB, and the mutation fails it.
This commit is contained in:
2026-08-03 06:30:19 +02:00
parent d5be67b913
commit 4be6467b50
5 changed files with 436 additions and 1 deletions
+45
View File
@@ -1,5 +1,50 @@
## Changelog
### v0.192.0 — the capture floor replaces the bulkhead (2026-08-03, R-165 · decision B2) — MinAgent: none
**Ships BEFORE the disk-layout merge it exists for, and is harmless on a box that never gets it.**
The `mp1``mp0` merge (R-165 / decision D-a) removes a wall that was quietly doing a second job: the
20 G backup partition kept a runaway recovery-unit capture from filling the space the container
runtime itself needs, because `/var/lib/docker` was a **different filesystem**. After the merge it is
the same one, and a full Docker data-root is a stopped box, not a slow one. Decision **B2** is that
bulkhead, done deliberately instead of by accident.
**The floor, in `captureAllRecoveryUnits`, checked BEFORE anything is written.** If the target
filesystem is below the reserve, that **one app's** capture is refused, its previous unit is left
**byte-identical**, the operator alert wired in v0.191.0 fires with the used/free figures, and the
loop continues to the next app.
**Two terms, whichever binds first — 97% used or 1 GiB free** — the same shape as `internal/fillwatch`,
which proved live on 2026-08-02 that a percentage alone is not enough (its critical alert fired on the
free-byte term at 91% used, where a percent-only rule stayed silent).
**They sit deliberately BEYOND fillwatch's critical band (95% / 2 GiB), so the customer is ALWAYS
warned before a refusal can happen.** A floor that fires before its own warning is a silent failure
wearing a threshold; `TestFloorSitsBelowTheCriticalWarningBand` pins the whole ordering
(warn → critical → refuse) on both terms, and a red-proof setting the floor equal to the critical band
fails it.
**IT IS ABOUT THE FILESYSTEM'S HEADROOM, NEVER THE UNIT'S SIZE.** A per-unit cap would be R-163 rebuilt
inside one volume — the wall moved rather than removed — so a 120 GB app on a filesystem with 180 GB
free is captured. A red-proof substituting `UsedGB > 20` for the headroom predicate fails two tests.
**IT REFUSES; IT NEVER DELETES, and the reason is recorded because the question will be asked again.**
Nothing on this filesystem is generational: a unit is ONE fixed path per app
(`backups/primary/<app>`) refreshed in place, and a DB dump is `<stack>-<dbtype>.sql`, also fixed. So
"prune the oldest" could only mean deleting a **different** app's only local recovery unit to make room
for this one. `pruneStalePrimaryDirs` is **not** a retention policy — it removes ORPHANED directories
left when an app moves drives and has no notion of age — and must never be repurposed here.
**A nil usage read neither refuses nor warns** (§8.4): an unreadable filesystem is the drive gate's
business and already has its own alert, and refusing on it would block every capture on a box whose
drive merely blipped.
**Tests:** 1184 → **1191** (+7). New `unitSpaceFn` seam so a filesystem's occupancy is a test input
rather than something a test must manufacture on a real disk. One fixture was **strengthened** during
the red-proofs: `TestFloor_TheOld20GCeilingIsGone` originally sat at exactly 20 GB and therefore
survived a literal `UsedGB > 20` cap — a hollow test that passed the very shape it forbids. Its
figure is now 120 GB and the mutation fails it.
### v0.191.2 — a quiet fill check now says so (2026-08-02, R-167) — MinAgent: none
**Earned during v0.191.1's own live validation, which is the strongest evidence it was needed.** After