Records the wire encoding (string enum, empty = legacy not unknown) and the two judgement calls: the fail-safe direction is unchanged (unknown is still due), and a service with no lister stays ABSENT because calling it unknown would starve a genuinely new box on a pre-R-84 build.
3.8 KiB
REPORT — R-88 Part 2: the agent can say "unknown" (v0.105.0) (2026-07-27)
Overwritten per the standing rule. Agent v0.104.0 → v0.105.0. Producer half of a wire contract; the controller consumer is v0.178.0.
What was broken
newestArchiveOn promised in its own doc comment that "errors and unsupported services degrade to
'unknown', never to 'no backup'". Its (time.Time, bool) signature made that impossible: an
error and a genuine not-found both returned (zero, false), so /backup/due emitted a POSITIVE
"no successful backup recorded yet" with a nil age. The controller read that as never backed up,
fired its window-gate safety valve, and quiesced customer app stacks outside the backup window —
what happened during the 2026-07-27 PBS outage. The comment described an intent the type forbade.
The wire encoding, and how the zero value is handled
A string enum, matching the repo's existing convention (phase on /backup/status):
age_state: "known" | "absent" | "unknown" (omitted entirely by a pre-v0.105.0 agent)
The zero value is "", and it means "legacy agent, no information" — never "unknown". A bool
would have made the legacy case indistinguishable from a real false, which is the trap the task
named. The controller maps "" (and any unrecognised future value) to AgeStateLegacy explicitly.
Internally archiveLookup (found/absent/unknown) replaces the old bool, so the doc comment's
promise is now something the type can actually express.
Two decisions worth stating
The fail-safe direction is unchanged: unknown is still DUE. An agent that cannot read the storage must never suppress a backup. Only the window-gate bypass narrows.
A service with NO lister deliberately stays absent, not unknown. This broke a pre-existing
test (TestBackupDue_ServiceWithoutLister_UnchangedBehaviour) and the test was right: calling it
"unknown" would stop the controller firing its first-backup valve on a pre-R-84 build, so a genuinely
new box would never back up outside its window and nobody would notice for weeks. On that path the
in-memory record is the only registry that exists, so its absence means "no backup recorded" in the
only terms available. unknown is reserved for a lister that was asked and could not answer.
An unparseable in-memory timestamp is now unknown too — a backup did happen, we simply cannot
date it. It previously fell through to the same positive "never" claim.
Tests — 29 packages ok, 0 failed; build/vet/test each rc=0, run separately, vet unpiped
+5. Red-proofs observed and restored, in both directions:
| Mutation | Observed |
|---|---|
error path → archiveAbsent (the pre-fix collapse) |
an unreadable storage must report age_state="unknown", got "absent" — that is a POSITIVE claim of 'never backed up' built out of two absences |
!found → archiveUnknown (the over-correction) |
a genuine never-backed-up tier must report age_state="absent", got "unknown" — the controller only licenses a first backup outside the window on ABSENT |
Deployed — producer BEFORE consumer
demo-hp then demo-felhom, both felhom-agent 0.105.0, service active, previous binary kept as
.bak-0.104.0. Verified on the live wire from inside guest 9201, over the exact route the
controller calls:
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
NOT yet live-validated
- No
unknownstate has ever been observed on real hardware. Both boxes reportknown; the unknown path is unit-proven only, and reproducing it live means making PBS unreadable, which must not be done to ep0. - The
absentpath is likewise unobserved live — it needs a genuinely fresh tier.