agent v0.105.0 — R-88 Part 2: /backup/due gains age_state

newestArchiveOn's (time.Time, bool) signature could not express the 'unknown'
its own doc comment promised: a read 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, and the controller fired its window-gate valve on an
unreadable storage.

Three states now: known / absent / unknown, carried as a STRING enum so the zero
value unambiguously means 'legacy agent' rather than masquerading as an answer.
Fail-safe direction unchanged — unknown is still DUE; only the window-gate bypass
narrows to ABSENT.

A service with NO lister deliberately stays ABSENT: calling it unknown would stop
a genuinely new box on a pre-R-84 build from ever backing up outside its window.
An unparseable timestamp becomes unknown — a backup happened, we cannot date it.
This commit is contained in:
2026-07-27 18:00:56 +02:00
parent 5bca7bfc9a
commit 1c2664b0c1
3 changed files with 282 additions and 12 deletions
+41
View File
@@ -1,3 +1,44 @@
# felhom-agent — Changelog
## v0.105.0 — R-88 Part 2: the agent can finally say "unknown" (2026-07-27)
`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` answered a POSITIVE
`"no successful backup recorded yet"` with a nil age. The controller read that as *never backed up*
and fired its window-gate safety valve — quiescing customer app stacks **outside** the backup window.
That is what happened on 2026-07-27 during the PBS outage. The comment described an intent the type
forbade.
**The fix: three states, on the wire.** `archiveLookup` (`found` / `absent` / `unknown`) internally,
and `age_state` on `/backup/due`:
- `known``age_seconds` is set and meaningful;
- `absent` — a POSITIVE determination that no backup has ever landed. **The only state that licenses
the controller to bypass its backup window;**
- `unknown` — could not determine (storage unreadable, or an unparseable timestamp).
**A string enum, not a bool.** The zero value must mean *"legacy agent, no information"*, and `""`
says that unambiguously where `false` would masquerade as a real answer. It also matches the repo's
existing wire convention (`phase` on `/backup/status`).
**The fail-safe direction is unchanged: unknown is still DUE.** An agent that cannot read the storage
must never suppress a backup. What changes is only whether the *window gate* may be bypassed.
**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.
**A service with NO lister stays `absent`, deliberately.** "Unknown" is the tempting answer there and
it would regress the first-backup safety valve: a genuinely new box on a pre-R-84 build 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.
**Compatibility:** the field is additive. An OLD controller decodes into a struct without it and
behaves exactly as today — every pre-existing field is unchanged (pinned by
`TestAgeState_IsAdditive_PreExistingFieldsUnchanged`).
Tests +5; 29 packages ok. Red-proofs observed for the unknown/absent collapse in both directions.
## v0.104.0 — R-85: unattended per-tier restore-test (Phase 2): tier rotation, persisted state, one heavy op at a time (2026-07-26)
The scheduler could only ever see `cfg.Backup.BackupTarget()`, so the offsite tier's archives were