Files
tty7/crates
l0ng-ai dbef42a03d fix(machine): keep earlier generations of the machine tree (#716)
`MachineStore::persist` serialises the whole document and lands it with
`write_atomic_private`, so the file on disk is never torn — but the document it
replaces is simply gone. The tree is rewritten whole on every mutation, which
means the write that loses a layout is also the write that erases the only copy
of it. That is what turned #716 from an annoyance into a lost afternoon: a
client's arrival queued nineteen `TabClose`s, each one persisted, and by the
time anyone read `machine.json` there was nothing anywhere on the machine that
remembered what the workspace had looked like.

`persist` now rotates the document it is about to replace into a small ring of
backups beside it — `machine.json.bak`, then `.bak.1` and `.bak.2` behind it —
before the new one is written.

Two things decide whether such a ring is worth anything. The first is what
"previous good" means. It cannot mean "the last document that parsed": an
emptied tree parses perfectly and is exactly the state you want to recover
*from*, so validity is no signal at all. Age is the only signal available, so
the ring is spaced: a generation is taken only when the newest one is at least
five minutes old. Without that spacing the failure mode above — a burst of
writes seconds apart — would have rolled three copies of the damage through the
whole ring before a human noticed. With it, the oldest generation is a quarter
of an hour of history, and three generations is where the ring stops so a file
rewritten every few seconds does not grow a history without bound.

The second is the atomicity of the rotation itself, since a rotation that can
lose both copies is worse than none. The live file is never renamed, only read:
at every point in `keep_a_generation` the tree is still completely at its own
path, and the new generation lands through `write_atomic_private` — a sibling
temporary renamed into place, which also means the copies inherit the 0600 the
live tree is written under rather than widening anything. A crash mid-rotation
costs at most one backup generation and never the tree. A backup that cannot be
written is logged and the new document is persisted anyway; a machine with no
backup still has to work.

`load_machine` now also falls back to the newest generation that parses when the
live document is unreadable or corrupt, instead of starting from an empty tree.
Deliberately only for those two cases: a tree that parses always wins, however
empty it is, because that is the case a human has to judge — which is why the
files are plain JSON under obvious names, ready to be copied back by hand.

Claude-Session: https://claude.ai/code/session_01UUyWQXzcBAoBzaSX8pc7nU
2026-09-09 18:00:28 +08:00
..