TacidOS
Developers
Documentation

Decision 0065

0065. A generation the bootloader gave up on is not one of the newest worth keeping

Architecture decision record 0065: 0065. A generation the bootloader gave up on is not one of the newest worth keeping

Status
accepted
Date
2026-09-07

Context

Retention is the owner’s declaration: keep the newest count, keep everything younger than days, keep what is pinned, and — whatever they said — keep what runs, what booted and what the profile points at (ADR-0040). The keeper applies it and the stem carries it out by number.

The channel test set the owner’s count to 1 on a machine with three generations: 1, the installed system; 2, running; 3, the channel revision whose daemon cannot start, which the bootloader tried three times and passed over. The rule kept 3 as recent and 2 only because it happened to be running, and forgot 1 — the one system on the machine known to boot and not running. The expectation was the reverse.

This is not an off-by-one. Newest is a proxy for most likely to be wanted, and on a machine that follows a channel the proxy inverts: the newest generations are the ones the update system just made, which are precisely the ones most likely to be bad. A retention of the newest N fills with the failures. With count = 1 it fills entirely.

Decision

A generation the bootloader gave up on is excluded from recent, and from young too. The exclusion is from those two rules only: it is still kept if it runs, if it booted, if it is the profile’s default, or if the owner pinned it. The owner’s pinned is the owner’s word, and the keeper does not overrule the owner about their own machine.

The exclusion from young matters more than it looks. An abandoned generation is always young — it was made moments before it failed — so a days rule with the recent fix alone would keep every failure for thirty days and forget the working systems underneath them.

“Gave up on” is systemd-boot’s boot counter, and the keeper writes it down as a typed entry. reconcile_boot already ran on every boot and could see it: the machine came back on a generation older than the one the profile pointed at, which happens when and only when the counter exhausted its tries. What it wrote was prose, in an Entry::Note. Now it writes

Entry::PassedOver { running: GenerationId, passed_over: GenerationId }

and Keeper::abandoned() reads the set back out of the journal. Parsing the prose would have been the third time in one day that a rendering was mistaken for a value, and this one drives a deletion.

plan_collection returns Result<Collection, KeeperError> rather than treating an unreadable journal as an empty abandoned set. A journal the keeper cannot read is not a machine with no failures on it; assuming so silently keeps the bad generation and forgets a good one.

Consequences

  • The channel test’s third generation is forgotten and the installed system is kept, which is what a person who set count = 1 meant.
  • Entry::PassedOver is a new variant in the journal’s frozen enum. It changes no existing record’s bytes, and the frozen digests in the_record_format_is_frozen_and_this_is_where_it_is_frozen still hold — the first live confirmation of what ADR-0064 claims about adding a variant to a tagged enum.
  • Abandonment is recorded, not inferred, so the record survives the reboot that discovers it and a later collection can act on a boot failure from weeks ago.
  • A machine whose journal has been rotated or replaced loses the record, and an old abandoned generation there is kept like any other. The keeper does not go back to the bootloader’s counters: they are consumed by the boot that succeeded.

Alternatives rejected

Treat every generation numbered above the boot default as abandoned. No journal, no new variant, and correct in the failing case — but an owner who runs tacid undo moves the same profile backwards, and their working system, the one they may want back tomorrow, would be collected out from under them. A rule that cannot tell a failure from a person’s decision is not a rule about failures.

Ask the bootloader. systemd-boot’s counter lives in the EFI filesystem in the entry’s filename, and a successful boot renames the file: by the time the keeper looks, the evidence of the machine giving up has been consumed. The keeper sees it exactly once, in reconcile_boot, which is why it must write it down.

Forget an abandoned generation outright, wherever it appears. Tempting and wrong. The generation the bootloader gave up on may be the profile’s default — the update staged it and the counter has not yet had its last try — and forgetting what the machine will next attempt to boot would leave the profile pointing at nothing.