Records the valve-licence table, why a missing field means legacy rather than unknown, and the near-miss: TieredBackend is runtime-asserted, so the signature change built and vetted clean while the adapter silently stopped satisfying it.
4.9 KiB
REPORT — R-88 Part 2 (controller) + R-97c follow-through (v0.178.0) (2026-07-27)
Overwritten per the standing rule. Controller v0.177.0 → v0.178.0.
MinAgent: 0.105.0 — the age_state semantics. Deployed AFTER agent v0.105.0 on both boxes.
What changed
scheduledRunAllowed fired the safety valve on ANY nil age. It now requires a licence:
age_state |
licenses the valve? | why |
|---|---|---|
absent |
yes | the agent looked; nothing is there |
unknown |
no | unreadable storage — the whole point of R-88 |
known |
n/a | a real age; the comparison decides |
| (empty) | yes | pre-v0.105.0 agent — deliberate, see below |
A missing field means LEGACY, not unknown. Reading an old agent's silence as "unknown" looks
safer and regresses Scenario D: the valve would stop firing on every un-upgraded box, so a genuinely
new box would never take its first backup outside its window and nobody would notice for weeks. The
MinAgent floor drives the upgrade; the valve is not the place to force it. Logged once per
process (the logTierDegradeOnce shape). An unrecognised FUTURE value also maps to legacy — a newer
agent inventing a fourth state must not inherit "unknown" semantics from a controller that has never
heard of it.
Untouched, as required: the R-88 Part 1 breaker and its timings, dropBackedOffTiers, the window
bounds, and TriggerNow (still ungated by everything).
The near-miss worth reporting
TieredBackend is satisfied by a runtime type assertion in resolveDueTiers
(l.backend.(TieredBackend)), not at compile time. When DueFor's signature changed, the whole repo
built and vetted clean while quiesceBackend silently stopped satisfying the interface. That
would have shipped, degraded every box to the untargeted single-tier path, and lost R-82's multi-tier
backups entirely — with no error anywhere. TestQuiesceBackendSatisfiesTieredBackend is now the
compile-time witness; it caught this during the change. Sixth instance of the inert-seam class.
R-97c follow-through
The comment in internal/notify/notifier.go claiming these event types are operator-only "because
they have no customerMessages entry" was wrong and is corrected. The hub falls back to the raw
message when the entry is missing, and the only customer gate is prefs.EnabledEvents — pure
configuration. Enforcement is hub-side operatorOnlyEvents (hub >= v0.79.0).
Tests — 27 packages ok, 0 failed; build/vet/test each rc=0, run separately, vet unpiped
+7 (6 age-state + 1 interface witness). Red-proofs observed and restored:
| Scenario | Mutation | Observed |
|---|---|---|
| A | unknown licenses the valve (pre-fix) |
R-88 Part 2: an UNKNOWN age bypassed the backup window and stopped 1 stack(s) — an unreadable storage must not masquerade as a first-ever backup |
| B | drop absent from the licence |
a genuine first-ever backup (absent) must RUN outside the window; 0 stack(s) stopped — the safety valve was lost and a new box would starve |
| C | treat legacy as unknown | C: a pre-v0.105.0 agent must behave EXACTLY as before — nil age fires the valve. 0 stack(s) stopped; an un-upgraded box just silently stopped backing up outside its window |
C asserts behaviour, not the log line — a controller that logs the degrade and then defers would pass a log-only assertion while silently changing behaviour on every un-upgraded box.
Deployed + verified
Agent v0.105.0 first, then controller v0.178.0 — demo-hp, then demo-felhom. Both Up (healthy).
The MinAgent floor is satisfied, proven positively: zero pre-v0.105.0 degrade warnings on
either box (they would fire on the first due tier against an old agent), and the live wire carries
the field:
tier=local due=True age_state=known age_seconds=91097 reason=older than cadence
tier=felhom-pbs due=False age_state=known age_seconds=100172 reason=within cadence window
And the gate is demonstrably working on a known age: local is genuinely due (25.3 h against a
24 h cadence) at 18:11 Budapest — outside [04:30, 08:30) and well under the 48 h valve threshold —
so it correctly defers. No vzdump has run since the incident's last failure at 07:13:06 UTC.
Zero spurious backoff/suppression lines, zero startup errors on both boxes.
NOT yet live-validated
- No
unknownstate has ever been observed on real hardware — both boxes reportknown. The fix's central path is unit-proven only; reproducing it live means making PBS unreadable, which must not be done to ep0. - No R-88 backoff has ever armed on real hardware (carried forward, unchanged).
- R-97b's app-alarm suppression has never fired live — it needs a real quiesce cycle.
- The legacy-agent path is now unobservable on this fleet — both agents are v0.105.0, so Scenario C rests on tests alone from here.
- demo-felhom's first in-window scheduled backup lands 2026-07-28 at the earliest.