mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
`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