diff --git a/RELEASES.md b/RELEASES.md index b1f95f49..a77ae27b 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -10,6 +10,32 @@ Full auto-generated changelog: `CHANGELOG.md` · Releases: https://github.com/so --- +## v8.2.4 — 2026-07-12 (restore can no longer destroy the store it's recovering) + +Recovery-safety fix. `restore()` removed the entire live brain directory and THEN copied the +snapshot in — so any copy failure left the store destroyed with only a partial copy. The sharpest +edge: the copy (`fs.cp`) materialized the holes of sparse mmap blob files, so a snapshot that fits +on disk could balloon and `ENOSPC` mid-copy, and the recovery tool would have just destroyed the +brain it was asked to recover. Reported from a downstream incident's recovery forensics. + +Restore is now **non-destructive and crash-resumable**: + +- The snapshot is copied into a staging area (`_restore_staging/`) **before any live data is + touched**. The copy is **sparse-aware** — all-zero regions are left as holes, so a store of + mostly-hole blobs restores at its true allocated size instead of its apparent size. +- A copy failure (including `ENOSPC`) removes only the half-written staging area and throws; the + live store is left **exactly as it was**. +- Only after the copy succeeds and a completion marker is `fsync`'d does an **atomic per-entry + swap** move the staged data into place — same-filesystem renames that cannot fail for disk space. +- A crash mid-swap is finished **forward** on the next open: startup resumes a committed-but- + incomplete swap, or discards an uncommitted staging area (live data still authoritative). + +No API change — `restore(path, { confirm: true })` is unchanged. `persist()` was already safe +(hard-link snapshot). Regression (`tests/integration/restore-nondestructive.test.ts`): a forced +copy failure leaves live data fully intact, a normal restore round-trips, an interrupted-but- +committed restore completes on reopen, an uncommitted staging area is discarded, and the sparse +copy is byte-identical with allocation far below apparent size. + ## v8.2.3 — 2026-07-12 (a committed transaction is durable on return) Durability fix. A `transact()` reported "committed" while its canonical entity writes were still