From a7c7aa5102b96d1ae8db969b4e5761a17e828251 Mon Sep 17 00:00:00 2001 From: David Snelling Date: Sun, 12 Jul 2026 12:19:09 -0700 Subject: [PATCH] docs: RELEASES.md entry for 8.2.5 (honest rollback-failure response) --- RELEASES.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/RELEASES.md b/RELEASES.md index a77ae27b..a6bc927e 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -10,6 +10,40 @@ Full auto-generated changelog: `CHANGELOG.md` · Releases: https://github.com/so --- +## v8.2.5 — 2026-07-12 (honest response when a transaction rollback can't complete) + +Data-integrity fix. When a transaction failed and its rollback then *also* failed to undo a +canonical write (retries exhausted), the old behavior logged, continued, and threw +`TransactionRollbackError` — while the record it couldn't undo stayed durable on disk, and the +transaction even reported its state as `'rolled_back'`. The caller got an error implying the write +was undone; a read-back showed the record. A failed rollback had no truthful response. + +Rollback now tells the truth, with a two-branch contract: + +- **Adopt-forward (safe case).** A single-op write (`add`/`update`/…) whose only damage is a + durably-present record — the write the caller asked for — is **adopted**: its generation is + committed, the write returns success, and a loud warning records that the derived index may be + incomplete for that id until the next rebuild/`repairIndex()`. No error, no double-write; the + record is immediately durable and retrievable by `get()`. +- **Fail loud + quarantine (unsafe case).** A multi-operation batch, or *any* case where a record + was lost (a remove/update whose restore-undo failed), throws the new **`StoreInconsistentError`** + naming every unreconciled record and its disposition (`orphan` vs `loss`), and puts the brain into + **write-quarantine**: reads keep working, but writes are refused until `repairIndex()` reconciles + the derived indexes against canonical storage and lifts the quarantine. The generation counter is + not advanced, and a transaction whose rollback failed is now `'inconsistent'`, never the + `'rolled_back'` lie. + +The decision is made by *observation*, not guesswork: both commit paths already hold byte-identical +before-images, so after a failed rollback the store compares current canonical state to them to +classify exactly which records are orphaned or lost. + +New export: `StoreInconsistentError` (with `.records` and `.cause`) and the `UnreconciledRecord` type. +No other API change; `repairIndex()` gains the quarantine-lift behavior. Regression +(`tests/integration/rollback-trapdoor.test.ts`) injects the exact failure (index add throws → +canonical delete-undo fails) and pins adopt-forward (durable, get-able, not quarantined), fail-loud +(`StoreInconsistentError` + quarantine + reads work + `repairIndex()` lifts it), and the error's +record naming. + ## 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