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' });