Files
felhom-agent/REPORT.md
T
admin 9842c52853 docs: REPORT for agent v0.105.0 (R-88 Part 2)
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.
2026-07-27 18:12:04 +02:00

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
!foundarchiveUnknown (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 unknown state has ever been observed on real hardware. Both boxes report known; the unknown path is unit-proven only, and reproducing it live means making PBS unreadable, which must not be done to ep0.
  • The absent path is likewise unobserved live — it needs a genuinely fresh tier.