From 9cfa57d4984f068be8492be5489727ae04fa9203 Mon Sep 17 00:00:00 2001 From: "79475432@qq.com" Date: Thu, 26 Mar 2026 17:16:17 +0800 Subject: [PATCH] fix: use null-byte delimiter in observation content hash to prevent collisions Fields concatenated without separators allowed different tuples to produce identical hashes (e.g. session="ab", title="cd" vs session="abc", title="d"). This could cause legitimate observations to be silently deduplicated. Join with \x00 so field boundaries are unambiguous. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/services/sqlite/observations/store.ts | 2 +- tests/sqlite/data-integrity.test.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/services/sqlite/observations/store.ts b/src/services/sqlite/observations/store.ts index 20727332..e012ed56 100644 --- a/src/services/sqlite/observations/store.ts +++ b/src/services/sqlite/observations/store.ts @@ -22,7 +22,7 @@ export function computeObservationContentHash( narrative: string | null ): string { return createHash('sha256') - .update((memorySessionId || '') + (title || '') + (narrative || '')) + .update([memorySessionId || '', title || '', narrative || ''].join('\x00')) .digest('hex') .slice(0, 16); } diff --git a/tests/sqlite/data-integrity.test.ts b/tests/sqlite/data-integrity.test.ts index 81204d99..307c4600 100644 --- a/tests/sqlite/data-integrity.test.ts +++ b/tests/sqlite/data-integrity.test.ts @@ -69,6 +69,16 @@ describe('TRIAGE-03: Data Integrity', () => { expect(hash.length).toBe(16); }); + it('computeObservationContentHash avoids collision from field boundary ambiguity', () => { + // These tuples would collide without a delimiter between fields + const hash1 = computeObservationContentHash('session-abc', 'debug log', ''); + const hash2 = computeObservationContentHash('session-ab', 'cdebug log', ''); + const hash3 = computeObservationContentHash('session-', 'abcdebug log', ''); + const hash4 = computeObservationContentHash('', 'session-abcdebug log', ''); + const hashes = new Set([hash1, hash2, hash3, hash4]); + expect(hashes.size).toBe(4); + }); + it('storeObservation deduplicates identical observations within 30s window', () => { const memId = createSessionWithMemoryId(db, 'content-dedup-1', 'mem-dedup-1'); const obs = createObservationInput({ title: 'Same Title', narrative: 'Same Narrative' });