DiskBlobAdapter.putAttachment writes bytes straight to their final content-addressed path (blob-adapter-disk/src/index.ts:29-35):
const fileId = createHash('sha256').update(data).digest('hex');
if (!existsSync(join(this.dir, fileId))) {
await writeFile(join(this.dir, fileId), data);
}
A crash mid-writeFile leaves a partial file at its final, hash-named path. Because dedup keys on existence of that path, every future upload of the same bytes sees the file exists and skips writing (existsSync → true) — so the torn file is never repaired, and reads serve bytes that don't match their own fileId. A silent content-integrity failure, permanent by construction.
Fix
Write to a temp file, then atomic rename() to the final path — the exact idiom the spec already prescribes for the planned adapter-json (§Concurrency & storage ownership) and that #72 assumes for the streaming blob path. A rename is atomic on a local filesystem, so a crash leaves either the complete file or no file, never a torn one at the final name. (Optional defense-in-depth: verify the hash on dedup-skip, but temp+rename is the core fix.)
Tests
Refs
#45 (atomic temp-file-and-rename idiom), #72 (streaming blob path assumes this), #106 (adjacent attachment-atomicity — a different window: bytes-vs-metadata, not the byte write itself). From docs/design-assessment-2026-07.md §F8.
DiskBlobAdapter.putAttachmentwrites bytes straight to their final content-addressed path (blob-adapter-disk/src/index.ts:29-35):A crash mid-
writeFileleaves a partial file at its final, hash-named path. Because dedup keys on existence of that path, every future upload of the same bytes sees the file exists and skips writing (existsSync→ true) — so the torn file is never repaired, and reads serve bytes that don't match their own fileId. A silent content-integrity failure, permanent by construction.Fix
Write to a temp file, then atomic
rename()to the final path — the exact idiom the spec already prescribes for the plannedadapter-json(§Concurrency & storage ownership) and that #72 assumes for the streaming blob path. A rename is atomic on a local filesystem, so a crash leaves either the complete file or no file, never a torn one at the final name. (Optional defense-in-depth: verify the hash on dedup-skip, but temp+rename is the core fix.)Tests
Refs
#45 (atomic temp-file-and-rename idiom), #72 (streaming blob path assumes this), #106 (adjacent attachment-atomicity — a different window: bytes-vs-metadata, not the byte write itself). From
docs/design-assessment-2026-07.md§F8.