fix: restore loadBinaryBlob fault-propagation (native column-store lockstep)

The Pass-1 hardening that makes loadBinaryBlob distinguish genuine absence
(ENOENT -> null) from a real fault (EIO/EACCES/... -> throw) was held out of
8.2.6 because the native accelerator's two column-store read sites still relied
on null-on-error. That accelerator release has now hardened those sites to
handle the throw (a faulted segment read marks the field unavailable and throws
a named error), so the fault-propagating form is restored and ships lockstep.

A present-but-unreadable index blob no longer masquerades as absent (which drove
needless rebuilds / empty reads). Adds the loadBinaryBlob leg to the blob
durability suite (absent -> null; present -> bytes; real fault -> throws).
This commit is contained in:
David Snelling 2026-07-13 12:09:55 -07:00
parent 76843b782e
commit b6c7039769
3 changed files with 65 additions and 19 deletions

View file

@ -10,6 +10,24 @@ Full auto-generated changelog: `CHANGELOG.md` · Releases: https://github.com/so
---
## v8.2.7 — 2026-07-13 (loadBinaryBlob fault-propagation — the lockstep completion of 8.2.6)
Completes the Pass-1 spine hardening. `loadBinaryBlob` (the raw-blob read a native accelerator mmaps
for its index files) previously returned `null` on ANY read error, so a real IO fault
(EIO/EACCES/EMFILE) on a present-but-unreadable index blob masqueraded as "the blob is absent" —
driving a needless full rebuild or an empty read. It now distinguishes genuine absence (ENOENT →
`null`, the documented contract) from a real fault (→ throw), so a transient disk fault surfaces
loudly instead of silently degrading the index.
This one change was deliberately held out of 8.2.6 and ships now, in lockstep with the native
accelerator release that hardened its two column-store read sites to handle the throw (a faulted
segment read marks the field unavailable and throws a named error, instead of relying on
null-on-error). A consumer on new brainy + an older accelerator was never at risk: 8.2.6 kept the
prior swallow-on-fault behavior for this method until the accelerator was ready.
No API change. Regression: `tests/unit/storage/blob-save-durability.test.ts` gains the loadBinaryBlob
leg (absent → null; present → bytes; real fault → throws).
## v8.2.6 — 2026-07-13 (write/index-spine hardening — loud errors, never quiet losses)
Durability + integrity hardening across the write and index paths. Every fix converts a place that

View file

@ -17,6 +17,7 @@ import {
WriterLockInfo
} from '../baseStorage.js'
import { getBrainyVersion } from '../../utils/index.js'
import { isAbsentError } from '../../utils/errorClassification.js'
// Node.js modules - dynamically imported to avoid issues in browser environments
let fs: any
@ -1228,25 +1229,17 @@ export class FileSystemStorage extends BaseStorage {
await this.ensureInitialized()
try {
return await fs.promises.readFile(this.blobPath(key))
} catch {
// COORDINATION HOLD — restore the fault-propagating form lockstep with
// cortex 3.0.13 (CORTEX-BILLION-SCALE-SPINE). The correct behavior
// distinguishes genuine absence from a real fault so a present-but-
// unreadable blob no longer masquerades as "absent" (which drove needless
// rebuilds / empty reads). Restore to:
// } catch (err) {
// // re-add: import { isAbsentError } from '../../utils/errorClassification.js'
// if (isAbsentError(err)) return null
// throw err
// }
// The throw is HELD because the native column-store still has 2 call sites
// that rely on null-on-error; cortex hardens them in 3.0.13, then this
// reverts to the throwing form and ships lockstep. Until then this
// preserves the shipped 8.2.5 swallow-on-fault behavior, so a consumer on
// new-brainy + old-cortex can never break. All other Pass-1 hardening
// (saveBinaryBlob honesty, clear(), pending-flush durability, count
// symmetry, degraded surfacing, aggregation) ships now, independent of this.
return null
} catch (err) {
// Absent blob → null (the documented contract). A real fault
// (EIO/EACCES/EMFILE/…) must NOT be masked as "absent": doing so makes a
// present-but-unreadable blob look missing and drives a needless rebuild
// or an empty read (the native provider consumes this). Mandate: loud
// errors, never quiet losses. Fault-propagation restored lockstep with
// cortex 3.0.13, whose two column-store call sites now handle the throw
// (they mark the field unavailable + throw a named error) instead of
// relying on null-on-error (CORTEX-BILLION-SCALE-SPINE).
if (isAbsentError(err)) return null
throw err
}
}

View file

@ -74,3 +74,38 @@ describe('FileSystemStorage.saveBinaryBlob durable-write honesty (finding 13)',
).rejects.toMatchObject({ code: 'EIO' })
})
})
describe('FileSystemStorage.loadBinaryBlob fault-propagation (finding 11; cor 3.0.13 lockstep)', () => {
let dir: string
let storage: any
beforeEach(async () => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'brainy-blob-load-'))
storage = new FileSystemStorage(dir)
await storage.init()
})
afterEach(() => {
vi.restoreAllMocks()
fs.rmSync(dir, { recursive: true, force: true })
})
it('returns null for a genuinely absent blob (ENOENT)', async () => {
expect(await storage.loadBinaryBlob('never/written')).toBeNull()
})
it('reads back a present blob', async () => {
await storage.saveBinaryBlob('present/blob', Buffer.from([1, 2, 3]))
expect(await storage.loadBinaryBlob('present/blob')).toEqual(Buffer.from([1, 2, 3]))
})
it('propagates a real IO fault instead of masking it as absent', async () => {
await storage.saveBinaryBlob('present/blob', Buffer.from([1, 2, 3]))
vi.spyOn(fs.promises, 'readFile').mockRejectedValue(
Object.assign(new Error('disk fault'), { code: 'EIO' })
)
// A present-but-unreadable blob must NOT read as null (that drove needless
// native rebuilds / empty reads); cor 3.0.13's column-store handles the throw.
await expect(storage.loadBinaryBlob('present/blob')).rejects.toMatchObject({ code: 'EIO' })
})
})