Decision 0040
Generations are kept by declaration and collected by the keeper
Architecture decision record 0040: Generations are kept by declaration and collected by the keeper
Context
ADR-0013 decided what a machine keeps: the last ten generations, everything from the last thirty
days, and anything pinned, with the store collected weekly at a quiet point. It did not decide
who applies the rule, and the stem carried a collect-garbage operation that applied a rule of
its own — nix-collect-garbage --delete-older-than — which forgets the old generations of every
profile on the machine, keeps no count, knows no pin, and runs inside a confinement that cannot
write the profile or the store. Nothing called it. A machine that updates itself hourly and
never forgets is a machine whose disk is a countdown, and one whose retention is a sentence in a
plan is a machine with no retention.
The question is where the rule lives. Three places were possible: the timer that asks for a collection, the stem that carries it out, or the keeper between them.
Prior decisions
ADR-0004 gives the keeper the immune system and puts it outside the agent’s reach; ADR-0010
says the agent cannot write tacid.core.*; ADR-0033 makes the stem a set of typed operations
laid out as fixed argument vectors, with no policy of its own; ADR-0038 has the update check
propose and never decide.
Decision
The owner declares, the keeper decides, the stem forgets by number.
The owner declares what is kept, in the genome, where the body’s keeping is:
tacid.core.retention = {
count = 10; # the newest so many, however old
days = 30; # everything younger, however many
pinned = [ 12 ]; # by number, whatever their age
collect = "weekly"; # when, as systemd reads a calendar; "" for never on a timer
};
It is rendered into the keeper’s policy, and the keeper reads it from there and from nowhere
else: nothing that asks for a collection says what it keeps. A generation is kept if any one
reason says so — it is running, it is what the machine booted, it is what the profile points at
(the boot default, staged or not), it is pinned, it is among the newest count, or it is
younger than days — and a generation whose age is not known is kept, because nothing is
forgotten on a guess. The keeper answers generations.collect with what it forgot and why each
of the rest stayed, and writes the same in the journal.
The stem’s operation names generations, never a rule: nix-env --delete-generations for the
numbers it is given, nix-store --gc, and then the profile’s own switch-to-configuration boot, which writes the bootloader’s entries again from what is left. The profile’s generation
rather than the running one, so that a revision staged for the next boot stays the default. Each
of the three writes where the stem’s confinement does not reach, so each runs as an activation
does: as a transient unit of its own, waited for, named. nix-env refuses to forget the
generation the profile points at, which is the one guard the stem keeps for itself besides
refusing a generation 0.
A timer the owner sets runs tacid-collect, which asks the keeper when nobody has a session on
the machine and otherwise says “not now” and lets the timer come back. A full disk is a reason
to collect and not a reason to wait, so that is the only quietness asked for. tacid collect
asks the same way, and tacid generations shows the numbers a pin names. Models are not store
paths and are never collected.
Consequences
- The agent cannot loosen retention: the options are under
tacid.core, which the policy engine refuses from an agent principal, and the keeper reads its own policy rather than the caller’s request. - A machine keeps what it runs, what it booted and what it will boot, whatever the numbers say;
an owner who sets
count = 1anddays = 0keeps those three and nothing else. - The stem lost its only rule.
Garbage { keep_days, keep_count }becameGarbage { generations }, and what it carries out is what the keeper wrote down. - The lifecycle test runs the timer’s service on a machine with no system profile and finds the keeper answering by its policy and the journal saying so; the forgetting of real generations — a channel’s abandoned revision among them — is set out for the channel test, whose follower has three, and this record will say what that run found.
system.generationsgaineddefaultandmade_ms, which is what the decision needs and what a person readingtacid generationswants: which one the machine will boot, and how old each is.
Alternatives rejected
- The stem keeps a rule (
--delete-older-than). A process running as root should carry out what it was told and nothing it assumed; a rule in the stem is a policy nobody declared and nobody can pin against, andnix-collect-garbageapplies it to every profile on the machine, a person’s included. - The timer’s service says what to keep. It runs as the agent’s user, and so would the agent asking. A retention that arrives with the request is a retention the requester chose.
- A generation’s age from the store path. Store paths have no time; the profile’s link does,
and it is what
nix-envitself counts by.