v1.6.4
Five fixes on top of 1.6.3, all from a code review of the audit-trail work that shipped in 1.6.3. No breaking changes, no config or schema changes — upgrading is a straight swap of the exe.
The window stops freezing during a backup
Writing a receipt is synchronous file I/O: a cross-process lock whose backoff sleeps up to ten seconds when a scheduled 9lives run holds it, a whole-file read, a redaction pass over every script and log, then a serialize and a file replace. The screens did all of that on the UI thread, once per database — so a fifty-database backup did fifty whole-file rewrites there and the window could stop repainting for minutes.
Worse, the same write sat in the cancellation handler. Pressing Stop — the one control whose entire purpose is to respond immediately — froze the app instead of stopping the run.
Receipts now go off the dispatcher, and are still awaited one at a time. That last part is deliberate: a run that dies on the sixth database leaves the first five receipts on disk, and that half-finished run is exactly the incident somebody opens the history for. Batching the run into a single write was the alternative and trades that away.
Recovery actions leave a trace
The panel that appears after a restore has failed runs RESTORE ... WITH RECOVERY and DBCC CHECKDB against a production database. It is the highest-stakes statement this app sends, at the worst possible moment — and it wrote nothing to the history at all. An incident write-up showed the failed restore and not the statement that brought the database back.
All three endings are now filed, cancelled included: "I stopped it and the database was left alone" is exactly what a change ticket needs, and the panel said that only on screen, where it scrolls away. Recovery is also offered in the History screen's filter, so the receipts can actually be found.
A differential's file name no longer claims COPY_ONLY
COPY_ONLY is the default for backups this app takes, and the marker went into every destination name — but the generated statement omits the keyword on a differential, and rightly so: there is no copy-only differential, because what COPY_ONLY protects is the differential base and a differential does not move it.
The marker is load-bearing. The listing reads it back out of the name to classify what it finds. Backups already written keep their names and are still read correctly; this changes what gets written from here on.
Two internal fixes, and why they are in these notes
Neither changes anything you can see, but one of them is the reason a real bug reached a released build once already.
The config-store test double now behaves like the real one. The real store re-reads and deserializes on every call, so callers get fresh objects; the fake handed back cached instances. That single difference meant a screen which rebuilds its list and reselects by id saw "no change" under test and a genuine change in production — so every visit to the Backup or Copy screen silently opened a connection to a production instance and wiped the user's selections, with a fully green suite throughout. A test double that diverges from production does not merely fail to catch a bug; it makes that bug invisible.
The Restore screen can no longer default its history store to the real file. It was an optional argument defaulting to the installed app's own audit trail, and this screen hands it to the thing that writes — so one forgotten argument meant a test appending invented runs to a real history, with the entry cap trimming genuine receipts to make room. Filed as latent; it turned out one test file was already passing nothing at all.
Full detail in CHANGELOG.md.
Downloads. NineLives.exe is the app, 9lives.exe the CLI; take the -arm64 builds on Windows on ARM. Both are single self-contained executables — no installation. The zips carry both front ends together.
The binaries are unsigned, so SmartScreen will warn on first run (#33); each release carries a build provenance attestation you can verify with gh attestation verify.