@devflow-tools/memory-engine 0.16.11 → 0.16.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/embedding-model-store.d.ts +67 -0
- package/dist/embedding-model-store.d.ts.map +1 -0
- package/dist/embedding-model-store.js +350 -0
- package/dist/embedding-model-store.js.map +1 -0
- package/dist/embedding-provider.d.ts +8 -0
- package/dist/embedding-provider.d.ts.map +1 -1
- package/dist/embedding-provider.js +68 -11
- package/dist/embedding-provider.js.map +1 -1
- package/dist/explicit-memory-enricher.d.ts +24 -0
- package/dist/explicit-memory-enricher.d.ts.map +1 -0
- package/dist/explicit-memory-enricher.js +292 -0
- package/dist/explicit-memory-enricher.js.map +1 -0
- package/dist/hybrid-search.d.ts +31 -0
- package/dist/hybrid-search.d.ts.map +1 -1
- package/dist/hybrid-search.js +132 -32
- package/dist/hybrid-search.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/memory-engine.d.ts +22 -0
- package/dist/memory-engine.d.ts.map +1 -1
- package/dist/memory-engine.js +199 -47
- package/dist/memory-engine.js.map +1 -1
- package/dist/memory-gate.d.ts +20 -1
- package/dist/memory-gate.d.ts.map +1 -1
- package/dist/memory-gate.js +89 -56
- package/dist/memory-gate.js.map +1 -1
- package/dist/memory-maintenance.d.ts.map +1 -1
- package/dist/memory-maintenance.js +4 -2
- package/dist/memory-maintenance.js.map +1 -1
- package/dist/memory-relevance.d.ts +38 -0
- package/dist/memory-relevance.d.ts.map +1 -0
- package/dist/memory-relevance.js +289 -0
- package/dist/memory-relevance.js.map +1 -0
- package/dist/memory-store.d.ts +52 -2
- package/dist/memory-store.d.ts.map +1 -1
- package/dist/memory-store.js +599 -20
- package/dist/memory-store.js.map +1 -1
- package/dist/migrations/v2-to-v3.js +1 -1
- package/dist/migrations/v2-to-v3.js.map +1 -1
- package/dist/migrations/v3-to-v4.d.ts.map +1 -1
- package/dist/migrations/v3-to-v4.js +4 -0
- package/dist/migrations/v3-to-v4.js.map +1 -1
- package/dist/migrations/v6-to-v7.d.ts +3 -0
- package/dist/migrations/v6-to-v7.d.ts.map +1 -0
- package/dist/migrations/v6-to-v7.js +18 -0
- package/dist/migrations/v6-to-v7.js.map +1 -0
- package/dist/migrations/v6-to-v7.test.d.ts +2 -0
- package/dist/migrations/v6-to-v7.test.d.ts.map +1 -0
- package/dist/migrations/v6-to-v7.test.js +33 -0
- package/dist/migrations/v6-to-v7.test.js.map +1 -0
- package/dist/migrations/v7-to-v8.d.ts +3 -0
- package/dist/migrations/v7-to-v8.d.ts.map +1 -0
- package/dist/migrations/v7-to-v8.js +20 -0
- package/dist/migrations/v7-to-v8.js.map +1 -0
- package/dist/migrations/v8-to-v9.d.ts +3 -0
- package/dist/migrations/v8-to-v9.d.ts.map +1 -0
- package/dist/migrations/v8-to-v9.js +18 -0
- package/dist/migrations/v8-to-v9.js.map +1 -0
- package/dist/preference-conflicts.d.ts +19 -0
- package/dist/preference-conflicts.d.ts.map +1 -0
- package/dist/preference-conflicts.js +293 -0
- package/dist/preference-conflicts.js.map +1 -0
- package/dist/preference-conflicts.test.d.ts +2 -0
- package/dist/preference-conflicts.test.d.ts.map +1 -0
- package/dist/preference-conflicts.test.js +72 -0
- package/dist/preference-conflicts.test.js.map +1 -0
- package/package.json +4 -4
package/dist/memory-store.js
CHANGED
|
@@ -2,11 +2,15 @@ import Database from 'better-sqlite3';
|
|
|
2
2
|
import * as sqliteVec from 'sqlite-vec';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
5
|
-
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
6
6
|
import { applyV2ToV3Migration } from './migrations/v2-to-v3.js';
|
|
7
7
|
import { applyV3ToV4Migration } from './migrations/v3-to-v4.js';
|
|
8
8
|
import { applyV4ToV5Migration } from './migrations/v4-to-v5.js';
|
|
9
|
+
import { applyV6ToV7Migration } from './migrations/v6-to-v7.js';
|
|
10
|
+
import { applyV7ToV8Migration } from './migrations/v7-to-v8.js';
|
|
11
|
+
import { applyV8ToV9Migration } from './migrations/v8-to-v9.js';
|
|
9
12
|
import { observationDedupKey, } from './observation-types.js';
|
|
13
|
+
import { analysesConflict, analyzePreference, analyzePreferenceObservation, } from './preference-conflicts.js';
|
|
10
14
|
// ── Schema ─────────────────────────────────────────────
|
|
11
15
|
const SCHEMA_V2 = `
|
|
12
16
|
-- ============================================================
|
|
@@ -169,7 +173,10 @@ CREATE TABLE IF NOT EXISTS observations (
|
|
|
169
173
|
source_event_ids TEXT NOT NULL DEFAULT '[]',
|
|
170
174
|
memory_id TEXT,
|
|
171
175
|
embedding BLOB,
|
|
172
|
-
timestamp INTEGER NOT NULL
|
|
176
|
+
timestamp INTEGER NOT NULL,
|
|
177
|
+
explicit_accepted_at INTEGER,
|
|
178
|
+
evidence_status TEXT NOT NULL DEFAULT 'unverified',
|
|
179
|
+
evidence_details TEXT NOT NULL DEFAULT '[]'
|
|
173
180
|
);
|
|
174
181
|
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
175
182
|
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project);
|
|
@@ -256,6 +263,126 @@ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
|
256
263
|
VALUES (new.rowid, new.id, new.content, new.scope);
|
|
257
264
|
END;
|
|
258
265
|
`;
|
|
266
|
+
const OBSERVATION_FTS_SCHEMA = `
|
|
267
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS observations_fts USING fts5(
|
|
268
|
+
id UNINDEXED,
|
|
269
|
+
title,
|
|
270
|
+
narrative,
|
|
271
|
+
facts,
|
|
272
|
+
concepts,
|
|
273
|
+
tokenize='trigram'
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS observations_terms_fts USING fts5(
|
|
277
|
+
id UNINDEXED,
|
|
278
|
+
title,
|
|
279
|
+
narrative,
|
|
280
|
+
facts,
|
|
281
|
+
concepts,
|
|
282
|
+
tokenize = "unicode61 tokenchars '#+'"
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
CREATE TRIGGER IF NOT EXISTS observations_fts_ai
|
|
286
|
+
AFTER INSERT ON observations
|
|
287
|
+
WHEN new.quality_status = 'accepted' AND new.resolution_state = 'current'
|
|
288
|
+
BEGIN
|
|
289
|
+
INSERT INTO observations_fts(rowid, id, title, narrative, facts, concepts)
|
|
290
|
+
VALUES (new.rowid, new.id, new.title, new.narrative, new.facts, new.concepts);
|
|
291
|
+
INSERT INTO observations_terms_fts(rowid, id, title, narrative, facts, concepts)
|
|
292
|
+
VALUES (new.rowid, new.id, new.title, new.narrative, new.facts, new.concepts);
|
|
293
|
+
END;
|
|
294
|
+
|
|
295
|
+
CREATE TRIGGER IF NOT EXISTS observations_fts_ad
|
|
296
|
+
AFTER DELETE ON observations
|
|
297
|
+
WHEN old.quality_status = 'accepted' AND old.resolution_state = 'current'
|
|
298
|
+
BEGIN
|
|
299
|
+
DELETE FROM observations_fts WHERE rowid = old.rowid;
|
|
300
|
+
DELETE FROM observations_terms_fts WHERE rowid = old.rowid;
|
|
301
|
+
END;
|
|
302
|
+
|
|
303
|
+
CREATE TRIGGER IF NOT EXISTS observations_fts_au
|
|
304
|
+
AFTER UPDATE OF title, narrative, facts, concepts, quality_status, resolution_state ON observations
|
|
305
|
+
BEGIN
|
|
306
|
+
DELETE FROM observations_fts WHERE rowid = old.rowid;
|
|
307
|
+
DELETE FROM observations_terms_fts WHERE rowid = old.rowid;
|
|
308
|
+
INSERT INTO observations_fts(rowid, id, title, narrative, facts, concepts)
|
|
309
|
+
SELECT new.rowid, new.id, new.title, new.narrative, new.facts, new.concepts
|
|
310
|
+
WHERE new.quality_status = 'accepted' AND new.resolution_state = 'current';
|
|
311
|
+
INSERT INTO observations_terms_fts(rowid, id, title, narrative, facts, concepts)
|
|
312
|
+
SELECT new.rowid, new.id, new.title, new.narrative, new.facts, new.concepts
|
|
313
|
+
WHERE new.quality_status = 'accepted' AND new.resolution_state = 'current';
|
|
314
|
+
END;
|
|
315
|
+
`;
|
|
316
|
+
const MAX_OBSERVATION_FTS_QUERY_LENGTH = 512;
|
|
317
|
+
const MAX_OBSERVATION_FTS_TERMS = 16;
|
|
318
|
+
const MAX_OBSERVATION_FTS_TERM_LENGTH = 64;
|
|
319
|
+
const OBSERVATION_FTS_SCHEMA_VERSION = 6;
|
|
320
|
+
const OBSERVATION_EXCLUSION_CTE = `
|
|
321
|
+
WITH excluded_observation_ids(id) AS (
|
|
322
|
+
SELECT CAST(value AS TEXT) FROM json_each(?)
|
|
323
|
+
)
|
|
324
|
+
`;
|
|
325
|
+
function serializeObservationSearchExcludedIds(excludedIds) {
|
|
326
|
+
return JSON.stringify(excludedIds ? [...excludedIds] : []);
|
|
327
|
+
}
|
|
328
|
+
function normalizeObservationLexicalQuery(query) {
|
|
329
|
+
const phrase = query
|
|
330
|
+
.slice(0, MAX_OBSERVATION_FTS_QUERY_LENGTH)
|
|
331
|
+
.normalize('NFKC')
|
|
332
|
+
.slice(0, MAX_OBSERVATION_FTS_QUERY_LENGTH)
|
|
333
|
+
.replace(/\s+/gu, ' ')
|
|
334
|
+
.trim();
|
|
335
|
+
if (!phrase)
|
|
336
|
+
return { trigramQuery: null, termQuery: null };
|
|
337
|
+
const lexicalTerms = phrase.match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
338
|
+
const compact = lexicalTerms.join('');
|
|
339
|
+
const candidates = [compact, phrase, ...lexicalTerms];
|
|
340
|
+
const trigramTerms = [];
|
|
341
|
+
const termTerms = [];
|
|
342
|
+
const seenTrigrams = new Set();
|
|
343
|
+
const seenTerms = new Set();
|
|
344
|
+
for (const candidate of candidates) {
|
|
345
|
+
const term = [...candidate].slice(0, MAX_OBSERVATION_FTS_TERM_LENGTH).join('');
|
|
346
|
+
const searchableCharacters = term.match(/[\p{L}\p{N}]/gu) ?? [];
|
|
347
|
+
const normalized = term.toLocaleLowerCase('en-US');
|
|
348
|
+
const quoted = `"${term.replaceAll('"', '""')}"`;
|
|
349
|
+
if (searchableCharacters.length >= 3 && !seenTrigrams.has(normalized)) {
|
|
350
|
+
seenTrigrams.add(normalized);
|
|
351
|
+
trigramTerms.push(quoted);
|
|
352
|
+
}
|
|
353
|
+
if (searchableCharacters.length >= 1 && !seenTerms.has(normalized)) {
|
|
354
|
+
seenTerms.add(normalized);
|
|
355
|
+
termTerms.push(quoted);
|
|
356
|
+
}
|
|
357
|
+
if (trigramTerms.length >= MAX_OBSERVATION_FTS_TERMS
|
|
358
|
+
&& termTerms.length >= MAX_OBSERVATION_FTS_TERMS)
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
trigramQuery: trigramTerms.length > 0
|
|
363
|
+
? trigramTerms.slice(0, MAX_OBSERVATION_FTS_TERMS).join(' OR ')
|
|
364
|
+
: null,
|
|
365
|
+
termQuery: termTerms.length > 0
|
|
366
|
+
? termTerms.slice(0, MAX_OBSERVATION_FTS_TERMS).join(' OR ')
|
|
367
|
+
: null,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
export function isRecoverableObservationFtsError(error) {
|
|
371
|
+
const candidate = error;
|
|
372
|
+
const code = typeof candidate?.code === 'string' ? candidate.code.toUpperCase() : '';
|
|
373
|
+
const message = typeof candidate?.message === 'string' ? candidate.message : '';
|
|
374
|
+
if (/^SQLITE_(?:BUSY|LOCKED|READONLY|IOERR|FULL|CANTOPEN|PERM)/.test(code))
|
|
375
|
+
return false;
|
|
376
|
+
if (code === 'SQLITE_CORRUPT_VTAB')
|
|
377
|
+
return true;
|
|
378
|
+
const referencesObservationFts = /observations_(?:terms_)?fts(?:_(?:data|idx|content|docsize|config))?/i
|
|
379
|
+
.test(message);
|
|
380
|
+
if (!referencesObservationFts)
|
|
381
|
+
return false;
|
|
382
|
+
if (/^SQLITE_CORRUPT(?:_|$)/.test(code))
|
|
383
|
+
return true;
|
|
384
|
+
return code === 'SQLITE_ERROR' && /no such table|malformed schema|vtable constructor failed/i.test(message);
|
|
385
|
+
}
|
|
259
386
|
// ── MemoryStore ─────────────────────────────────────────
|
|
260
387
|
export class MemoryStore {
|
|
261
388
|
db;
|
|
@@ -275,10 +402,15 @@ export class MemoryStore {
|
|
|
275
402
|
sqliteVec.load(this.db);
|
|
276
403
|
this.db.exec(SCHEMA_V2);
|
|
277
404
|
this.db.exec(FTS_TRIGGERS);
|
|
405
|
+
this.dropOrphanedObservationFtsTriggers();
|
|
278
406
|
this.migrateV1ToV2();
|
|
279
407
|
applyV2ToV3Migration(this.db);
|
|
280
408
|
applyV3ToV4Migration(this.db);
|
|
281
409
|
applyV4ToV5Migration(this.db);
|
|
410
|
+
this.ensureObservationFts();
|
|
411
|
+
applyV6ToV7Migration(this.db);
|
|
412
|
+
applyV7ToV8Migration(this.db);
|
|
413
|
+
applyV8ToV9Migration(this.db);
|
|
282
414
|
this.repairOrphanSessions(rootPath);
|
|
283
415
|
console.log('[MemoryStore] Schema initialized', {
|
|
284
416
|
dbPath: this.dbPath,
|
|
@@ -292,6 +424,66 @@ export class MemoryStore {
|
|
|
292
424
|
const row = this.db.prepare("SELECT COUNT(*) as c FROM sqlite_master WHERE type='table'").get();
|
|
293
425
|
return row.c;
|
|
294
426
|
}
|
|
427
|
+
dropOrphanedObservationFtsTriggers() {
|
|
428
|
+
const ftsTables = this.db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name IN ('observations_fts', 'observations_terms_fts')").all();
|
|
429
|
+
if (ftsTables.length === 2)
|
|
430
|
+
return;
|
|
431
|
+
this.db.exec(`
|
|
432
|
+
DROP TRIGGER IF EXISTS observations_fts_ai;
|
|
433
|
+
DROP TRIGGER IF EXISTS observations_fts_ad;
|
|
434
|
+
DROP TRIGGER IF EXISTS observations_fts_au;
|
|
435
|
+
`);
|
|
436
|
+
}
|
|
437
|
+
ensureObservationFts(forceRebuild = false) {
|
|
438
|
+
this.db.transaction(() => {
|
|
439
|
+
const ftsTableCount = this.db.prepare("SELECT COUNT(*) AS count FROM sqlite_master WHERE type = 'table' AND name IN ('observations_fts', 'observations_terms_fts')").get().count;
|
|
440
|
+
const migrationApplied = this.db.prepare('SELECT 1 FROM schema_versions WHERE version = ?').get(OBSERVATION_FTS_SCHEMA_VERSION) !== undefined;
|
|
441
|
+
const triggersCurrent = this.db.prepare(`
|
|
442
|
+
SELECT COUNT(*) AS count
|
|
443
|
+
FROM sqlite_master
|
|
444
|
+
WHERE type = 'trigger'
|
|
445
|
+
AND name IN ('observations_fts_ai', 'observations_fts_ad', 'observations_fts_au')
|
|
446
|
+
AND sql LIKE '%observations_terms_fts%'
|
|
447
|
+
`).get().count === 3;
|
|
448
|
+
const needsRebuild = forceRebuild
|
|
449
|
+
|| ftsTableCount !== 2
|
|
450
|
+
|| !migrationApplied
|
|
451
|
+
|| !triggersCurrent;
|
|
452
|
+
if (needsRebuild) {
|
|
453
|
+
this.db.exec(`
|
|
454
|
+
DROP TRIGGER IF EXISTS observations_fts_ai;
|
|
455
|
+
DROP TRIGGER IF EXISTS observations_fts_ad;
|
|
456
|
+
DROP TRIGGER IF EXISTS observations_fts_au;
|
|
457
|
+
`);
|
|
458
|
+
}
|
|
459
|
+
if (forceRebuild) {
|
|
460
|
+
this.db.exec(`
|
|
461
|
+
DROP TABLE IF EXISTS observations_fts;
|
|
462
|
+
DROP TABLE IF EXISTS observations_terms_fts;
|
|
463
|
+
`);
|
|
464
|
+
}
|
|
465
|
+
this.db.exec(OBSERVATION_FTS_SCHEMA);
|
|
466
|
+
if (!needsRebuild)
|
|
467
|
+
return;
|
|
468
|
+
this.db.exec(`
|
|
469
|
+
DELETE FROM observations_fts;
|
|
470
|
+
DELETE FROM observations_terms_fts;
|
|
471
|
+
|
|
472
|
+
INSERT INTO observations_fts(rowid, id, title, narrative, facts, concepts)
|
|
473
|
+
SELECT o.rowid, o.id, o.title, o.narrative, o.facts, o.concepts
|
|
474
|
+
FROM observations o
|
|
475
|
+
WHERE o.quality_status = 'accepted'
|
|
476
|
+
AND o.resolution_state = 'current';
|
|
477
|
+
|
|
478
|
+
INSERT INTO observations_terms_fts(rowid, id, title, narrative, facts, concepts)
|
|
479
|
+
SELECT o.rowid, o.id, o.title, o.narrative, o.facts, o.concepts
|
|
480
|
+
FROM observations o
|
|
481
|
+
WHERE o.quality_status = 'accepted'
|
|
482
|
+
AND o.resolution_state = 'current';
|
|
483
|
+
`);
|
|
484
|
+
this.db.prepare('INSERT OR REPLACE INTO schema_versions(version, applied_at) VALUES (?, ?)').run(OBSERVATION_FTS_SCHEMA_VERSION, Date.now());
|
|
485
|
+
})();
|
|
486
|
+
}
|
|
295
487
|
// ── V1 → V2 Migration ────────────────────────────
|
|
296
488
|
migrateV1ToV2() {
|
|
297
489
|
const tables = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(r => r.name);
|
|
@@ -503,16 +695,18 @@ export class MemoryStore {
|
|
|
503
695
|
const row = this.db.prepare('SELECT vector FROM embeddings WHERE source_type = ? AND source_id = ?').get(sourceType, sourceId);
|
|
504
696
|
return row?.vector ?? null;
|
|
505
697
|
}
|
|
506
|
-
searchObservationVector(embedding, limit = 20) {
|
|
698
|
+
searchObservationVector(embedding, limit = 20, excludedIds) {
|
|
507
699
|
const buffer = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
|
|
508
|
-
const
|
|
700
|
+
const excludedJson = serializeObservationSearchExcludedIds(excludedIds);
|
|
701
|
+
const rows = this.db.prepare(`${OBSERVATION_EXCLUSION_CTE}
|
|
509
702
|
SELECT o.*, vec_distance_cosine(o.embedding, ?) AS distance
|
|
510
703
|
FROM observations o
|
|
511
704
|
WHERE o.embedding IS NOT NULL
|
|
512
705
|
AND o.quality_status = 'accepted'
|
|
513
706
|
AND o.resolution_state <> 'superseded'
|
|
707
|
+
AND o.id NOT IN (SELECT id FROM excluded_observation_ids)
|
|
514
708
|
ORDER BY distance LIMIT ?
|
|
515
|
-
`).all(buffer, limit);
|
|
709
|
+
`).all(excludedJson, buffer, limit);
|
|
516
710
|
return rows.map(row => ({ observation: row, distance: row.distance }));
|
|
517
711
|
}
|
|
518
712
|
// ── Search ───────────────────────────────────────
|
|
@@ -525,6 +719,66 @@ export class MemoryStore {
|
|
|
525
719
|
ORDER BY rank LIMIT ?
|
|
526
720
|
`).all(query, limit);
|
|
527
721
|
}
|
|
722
|
+
searchObservationBM25(query, limit = 20, excludedIds) {
|
|
723
|
+
const normalizedQuery = normalizeObservationLexicalQuery(query);
|
|
724
|
+
if ((!normalizedQuery.trigramQuery && !normalizedQuery.termQuery) || limit <= 0)
|
|
725
|
+
return [];
|
|
726
|
+
const boundedLimit = Number.isFinite(limit)
|
|
727
|
+
? Math.min(Math.max(Math.floor(limit), 1), 100)
|
|
728
|
+
: 20;
|
|
729
|
+
const excludedJson = serializeObservationSearchExcludedIds(excludedIds);
|
|
730
|
+
const scores = new Map();
|
|
731
|
+
const addRanked = (rows) => {
|
|
732
|
+
for (let index = 0; index < rows.length; index++) {
|
|
733
|
+
const id = rows[index].id;
|
|
734
|
+
scores.set(id, (scores.get(id) ?? 0) + 1 / (60 + index + 1));
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
if (normalizedQuery.trigramQuery) {
|
|
738
|
+
addRanked(this.searchObservationFtsWithRecovery('observations_fts', normalizedQuery.trigramQuery, boundedLimit, excludedJson));
|
|
739
|
+
}
|
|
740
|
+
if (normalizedQuery.termQuery) {
|
|
741
|
+
addRanked(this.searchObservationFtsWithRecovery('observations_terms_fts', normalizedQuery.termQuery, boundedLimit, excludedJson));
|
|
742
|
+
}
|
|
743
|
+
return [...scores]
|
|
744
|
+
.map(([id, score]) => ({ id, rank: -score }))
|
|
745
|
+
.sort((left, right) => left.rank - right.rank || left.id.localeCompare(right.id))
|
|
746
|
+
.slice(0, boundedLimit);
|
|
747
|
+
}
|
|
748
|
+
searchObservationFtsWithRecovery(table, query, limit, excludedJson) {
|
|
749
|
+
try {
|
|
750
|
+
return this.searchObservationFts(table, query, limit, excludedJson);
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
if (!isRecoverableObservationFtsError(error))
|
|
754
|
+
throw error;
|
|
755
|
+
this.ensureObservationFts(true);
|
|
756
|
+
return this.searchObservationFts(table, query, limit, excludedJson);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
searchObservationFts(table, query, limit, excludedJson) {
|
|
760
|
+
const sql = table === 'observations_fts' ? `${OBSERVATION_EXCLUSION_CTE}
|
|
761
|
+
SELECT o.id, bm25(observations_fts) AS rank
|
|
762
|
+
FROM observations_fts f
|
|
763
|
+
JOIN observations o ON o.rowid = f.rowid
|
|
764
|
+
WHERE observations_fts MATCH ?
|
|
765
|
+
AND o.quality_status = 'accepted'
|
|
766
|
+
AND o.resolution_state = 'current'
|
|
767
|
+
AND o.id NOT IN (SELECT id FROM excluded_observation_ids)
|
|
768
|
+
ORDER BY rank LIMIT ?
|
|
769
|
+
` : `${OBSERVATION_EXCLUSION_CTE}
|
|
770
|
+
SELECT o.id, bm25(observations_terms_fts) AS rank
|
|
771
|
+
FROM observations_terms_fts f
|
|
772
|
+
JOIN observations o ON o.rowid = f.rowid
|
|
773
|
+
WHERE observations_terms_fts MATCH ?
|
|
774
|
+
AND o.quality_status = 'accepted'
|
|
775
|
+
AND o.resolution_state = 'current'
|
|
776
|
+
AND o.id NOT IN (SELECT id FROM excluded_observation_ids)
|
|
777
|
+
ORDER BY rank, updated_at DESC
|
|
778
|
+
LIMIT ?
|
|
779
|
+
`;
|
|
780
|
+
return this.db.prepare(sql).all(excludedJson, query, limit);
|
|
781
|
+
}
|
|
528
782
|
searchVector(embedding, limit = 20) {
|
|
529
783
|
const buffer = Buffer.from(embedding.buffer);
|
|
530
784
|
return this.db.prepare(`
|
|
@@ -569,6 +823,123 @@ export class MemoryStore {
|
|
|
569
823
|
getSessionEvents(sessionId) {
|
|
570
824
|
return this.db.prepare('SELECT * FROM session_events WHERE session_id = ? ORDER BY created_at ASC').all(sessionId);
|
|
571
825
|
}
|
|
826
|
+
reconcileDecidedTurnEventWindows(sessionId, turns, sessionEndAt = Number.MAX_SAFE_INTEGER) {
|
|
827
|
+
const normalizedSessionId = sessionId.trim();
|
|
828
|
+
if (!normalizedSessionId)
|
|
829
|
+
throw new Error('Session event reconciliation requires a session ID');
|
|
830
|
+
if (!Number.isSafeInteger(sessionEndAt) || sessionEndAt < 0) {
|
|
831
|
+
throw new Error('Session event reconciliation requires a safe session end timestamp');
|
|
832
|
+
}
|
|
833
|
+
const orderedTurns = turns.filter(turn => turn.sessionId === normalizedSessionId);
|
|
834
|
+
if (orderedTurns.length !== turns.length) {
|
|
835
|
+
throw new Error('Memory turn session does not match event reconciliation session');
|
|
836
|
+
}
|
|
837
|
+
for (let index = 1; index < orderedTurns.length; index += 1) {
|
|
838
|
+
if (orderedTurns[index].createdAt < orderedTurns[index - 1].createdAt) {
|
|
839
|
+
throw new Error('Memory turns must be ordered by createdAt for event reconciliation');
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
const selectSessionEvents = this.db.prepare(`
|
|
843
|
+
SELECT id, created_at FROM session_events
|
|
844
|
+
WHERE session_id = ? AND processed = 0 AND maintenance_status = 'active'
|
|
845
|
+
ORDER BY created_at ASC, id ASC
|
|
846
|
+
`);
|
|
847
|
+
const markCommitted = this.db.prepare(`
|
|
848
|
+
UPDATE session_events SET processed = 1
|
|
849
|
+
WHERE id = ? AND session_id = ? AND processed = 0 AND maintenance_status = 'active'
|
|
850
|
+
`);
|
|
851
|
+
const markSkipped = this.db.prepare(`
|
|
852
|
+
UPDATE session_events
|
|
853
|
+
SET processed = 1, maintenance_status = 'discarded', quarantine_reason = ?
|
|
854
|
+
WHERE id = ? AND session_id = ? AND processed = 0 AND maintenance_status = 'active'
|
|
855
|
+
`);
|
|
856
|
+
const insertAudit = this.db.prepare(`
|
|
857
|
+
INSERT INTO event_processing_audit
|
|
858
|
+
(id, event_id, disposition, reason, run_id, timestamp)
|
|
859
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
860
|
+
`);
|
|
861
|
+
const countAuditsForRuns = this.db.prepare(`
|
|
862
|
+
WITH requested_runs(run_id) AS (
|
|
863
|
+
SELECT DISTINCT value FROM json_each(?)
|
|
864
|
+
)
|
|
865
|
+
SELECT audit.disposition, COUNT(*) AS count
|
|
866
|
+
FROM event_processing_audit audit
|
|
867
|
+
INNER JOIN requested_runs requested ON requested.run_id = audit.run_id
|
|
868
|
+
GROUP BY audit.disposition
|
|
869
|
+
`);
|
|
870
|
+
return this.db.transaction(() => {
|
|
871
|
+
const reconciledAt = Date.now();
|
|
872
|
+
const groups = [];
|
|
873
|
+
for (const turn of orderedTurns) {
|
|
874
|
+
const current = groups.at(-1);
|
|
875
|
+
if (current?.[0]?.createdAt === turn.createdAt)
|
|
876
|
+
current.push(turn);
|
|
877
|
+
else
|
|
878
|
+
groups.push([turn]);
|
|
879
|
+
}
|
|
880
|
+
let processedThisRun = 0;
|
|
881
|
+
let discardedThisRun = 0;
|
|
882
|
+
let decidedTurnCount = 0;
|
|
883
|
+
let eventIndex = 0;
|
|
884
|
+
const canonicalRunIds = [];
|
|
885
|
+
const events = selectSessionEvents.all(normalizedSessionId);
|
|
886
|
+
for (let groupIndex = 0; groupIndex < groups.length; groupIndex += 1) {
|
|
887
|
+
const group = groups[groupIndex];
|
|
888
|
+
const windowStart = group[0].createdAt;
|
|
889
|
+
const windowEnd = Math.min(groups[groupIndex + 1]?.[0]?.createdAt ?? sessionEndAt, sessionEndAt);
|
|
890
|
+
while (eventIndex < events.length && events[eventIndex].created_at < windowStart)
|
|
891
|
+
eventIndex += 1;
|
|
892
|
+
if (windowStart >= windowEnd)
|
|
893
|
+
continue;
|
|
894
|
+
const allCanonical = group.every(turn => turn.status !== 'pending' && turn.receiptId?.startsWith('memory-receipt:'));
|
|
895
|
+
const allCommitted = allCanonical && group.every(turn => turn.status === 'committed');
|
|
896
|
+
const allSkipped = allCanonical && group.every(turn => turn.status === 'skipped');
|
|
897
|
+
const disposition = allCommitted ? 'processed' : allSkipped ? 'discarded' : null;
|
|
898
|
+
const canonicalGroup = disposition
|
|
899
|
+
? group.map(turn => `${turn.turnId}=${turn.receiptId}`).join(',')
|
|
900
|
+
: '';
|
|
901
|
+
const reason = disposition
|
|
902
|
+
? `canonical_turn_group_${allCommitted ? 'committed' : 'skipped'}:${windowStart}:${canonicalGroup}`
|
|
903
|
+
: '';
|
|
904
|
+
const runId = disposition
|
|
905
|
+
? `turn-reconcile:${createHash('sha256')
|
|
906
|
+
.update(`${normalizedSessionId}\0${windowStart}\0${allCommitted ? 'committed' : 'skipped'}\0${canonicalGroup}`)
|
|
907
|
+
.digest('hex').slice(0, 32)}`
|
|
908
|
+
: '';
|
|
909
|
+
if (disposition) {
|
|
910
|
+
decidedTurnCount += group.length;
|
|
911
|
+
canonicalRunIds.push(runId);
|
|
912
|
+
}
|
|
913
|
+
while (eventIndex < events.length && events[eventIndex].created_at < windowEnd) {
|
|
914
|
+
const event = events[eventIndex++];
|
|
915
|
+
if (!disposition)
|
|
916
|
+
continue;
|
|
917
|
+
const changed = disposition === 'processed'
|
|
918
|
+
? markCommitted.run(event.id, normalizedSessionId).changes
|
|
919
|
+
: markSkipped.run(reason, event.id, normalizedSessionId).changes;
|
|
920
|
+
if (changed === 0)
|
|
921
|
+
continue;
|
|
922
|
+
if (disposition === 'processed')
|
|
923
|
+
processedThisRun += changed;
|
|
924
|
+
else
|
|
925
|
+
discardedThisRun += changed;
|
|
926
|
+
insertAudit.run(`epa:${reconciledAt}:${randomUUID().slice(0, 8)}`, event.id, disposition, reason, runId, reconciledAt);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
const historicalCounts = canonicalRunIds.length > 0
|
|
930
|
+
? countAuditsForRuns.all(JSON.stringify(canonicalRunIds))
|
|
931
|
+
: [];
|
|
932
|
+
const countByDisposition = new Map(historicalCounts.map(row => [row.disposition, Number(row.count)]));
|
|
933
|
+
return {
|
|
934
|
+
sessionId: normalizedSessionId,
|
|
935
|
+
turnCount: orderedTurns.length,
|
|
936
|
+
decidedTurnCount,
|
|
937
|
+
committedProcessed: countByDisposition.get('processed') ?? 0,
|
|
938
|
+
skippedDiscarded: countByDisposition.get('discarded') ?? 0,
|
|
939
|
+
remainingUndecided: events.length - processedThisRun - discardedThisRun,
|
|
940
|
+
};
|
|
941
|
+
})();
|
|
942
|
+
}
|
|
572
943
|
getUnprocessedEvents() {
|
|
573
944
|
const events = this.db.prepare(`SELECT * FROM session_events
|
|
574
945
|
WHERE processed = 0 AND maintenance_status = 'active'
|
|
@@ -873,9 +1244,18 @@ export class MemoryStore {
|
|
|
873
1244
|
(id, session_id, project, type, title, narrative, facts, files, tools, concepts,
|
|
874
1245
|
importance, confidence, source, dedup_key, source_event_ids, timestamp, updated_at,
|
|
875
1246
|
quality_status, quarantine_reason, quality_details, quarantined_at,
|
|
876
|
-
failure_category, resolution_state)
|
|
877
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
878
|
-
`).run(id, obs.sessionId, obs.project ?? this.defaultProjectRoot, obs.type, obs.title, obs.narrative ?? '', JSON.stringify(obs.facts ?? []), JSON.stringify(obs.files ?? []), JSON.stringify(obs.tools ?? []), JSON.stringify(obs.concepts ?? []), obs.importance ?? 5, obs.confidence ?? 0.5, obs.source ?? 'distill', dedupKey, JSON.stringify(obs.sourceEventIds ?? []), now, now, obs.qualityStatus ?? 'accepted', obs.quarantineReason ?? null, obs.qualityDetails ? JSON.stringify(obs.qualityDetails) : null, obs.qualityStatus === 'quarantined' ? now : null, obs.failureCategory ?? null, obs.resolutionState ?? 'current'
|
|
1247
|
+
failure_category, resolution_state, evidence_status, evidence_details)
|
|
1248
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1249
|
+
`).run(id, obs.sessionId, obs.project ?? this.defaultProjectRoot, obs.type, obs.title, obs.narrative ?? '', JSON.stringify(obs.facts ?? []), JSON.stringify(obs.files ?? []), JSON.stringify(obs.tools ?? []), JSON.stringify(obs.concepts ?? []), obs.importance ?? 5, obs.confidence ?? 0.5, obs.source ?? 'distill', dedupKey, JSON.stringify(obs.sourceEventIds ?? []), now, now, obs.qualityStatus ?? 'accepted', obs.quarantineReason ?? null, obs.qualityDetails ? JSON.stringify(obs.qualityDetails) : null, obs.qualityStatus === 'quarantined' ? now : null, obs.failureCategory ?? null, obs.resolutionState ?? 'current', obs.evidenceStatus
|
|
1250
|
+
?? (obs.source === 'explicit_intent'
|
|
1251
|
+
? 'unverified'
|
|
1252
|
+
: obs.source === 'manual' || (obs.sourceEventIds?.length ?? 0) > 0
|
|
1253
|
+
? 'verified'
|
|
1254
|
+
: 'unverified'), JSON.stringify(obs.evidenceDetails ?? (obs.sourceEventIds ?? []).map(source => ({
|
|
1255
|
+
kind: 'source_event',
|
|
1256
|
+
source,
|
|
1257
|
+
summary: 'Observation was accepted from an evidence-bound session event.',
|
|
1258
|
+
}))));
|
|
879
1259
|
// Insert concept junction rows
|
|
880
1260
|
if (obs.concepts && obs.concepts.length > 0) {
|
|
881
1261
|
const insertConcept = this.db.prepare('INSERT OR IGNORE INTO observation_concepts (observation_id, concept) VALUES (?, ?)');
|
|
@@ -895,6 +1275,197 @@ export class MemoryStore {
|
|
|
895
1275
|
return this.getObservation(row.id);
|
|
896
1276
|
})();
|
|
897
1277
|
}
|
|
1278
|
+
acceptExplicitPreference(input) {
|
|
1279
|
+
return this.db.transaction(() => {
|
|
1280
|
+
this.ensureSession(input.sessionId, input.project);
|
|
1281
|
+
const latest = this.db.prepare(`
|
|
1282
|
+
SELECT MAX(COALESCE(explicit_accepted_at, timestamp)) AS latest
|
|
1283
|
+
FROM observations
|
|
1284
|
+
WHERE project = ? AND type IN ('user_preference', 'project_convention', 'decision_made')
|
|
1285
|
+
AND source = 'explicit_intent'
|
|
1286
|
+
`).get(input.project);
|
|
1287
|
+
const acceptedAt = Math.max(Date.now(), (latest.latest ?? 0) + 1);
|
|
1288
|
+
const dedupKey = observationDedupKey('user_preference', input.title);
|
|
1289
|
+
const exact = this.db.prepare('SELECT * FROM observations WHERE dedup_key = ? AND project = ?').get(dedupKey, input.project) ?? null;
|
|
1290
|
+
let row;
|
|
1291
|
+
if (exact) {
|
|
1292
|
+
const contentChanged = exact.title !== input.title || exact.narrative !== input.content;
|
|
1293
|
+
const sourceEventIds = new Set(parseStringArray(exact.source_event_ids));
|
|
1294
|
+
if (input.sourceEventId)
|
|
1295
|
+
sourceEventIds.add(input.sourceEventId);
|
|
1296
|
+
this.db.prepare(`
|
|
1297
|
+
UPDATE observations
|
|
1298
|
+
SET title = ?, narrative = ?, source = 'explicit_intent',
|
|
1299
|
+
confidence = MAX(confidence, 0.95),
|
|
1300
|
+
importance = MAX(importance, 10), quality_status = 'accepted',
|
|
1301
|
+
quarantine_reason = NULL, quality_details = NULL, quarantined_at = NULL,
|
|
1302
|
+
resolution_state = 'current', superseded_by_observation = NULL,
|
|
1303
|
+
source_event_ids = ?, updated_at = ?, explicit_accepted_at = ?,
|
|
1304
|
+
embedding = CASE WHEN ? = 1 THEN NULL ELSE embedding END
|
|
1305
|
+
WHERE id = ?
|
|
1306
|
+
`).run(input.title, input.content, JSON.stringify([...sourceEventIds]), acceptedAt, acceptedAt, contentChanged ? 1 : 0, exact.id);
|
|
1307
|
+
if (contentChanged) {
|
|
1308
|
+
this.db.prepare("DELETE FROM embeddings WHERE source_type = 'observation' AND source_id = ?").run(exact.id);
|
|
1309
|
+
}
|
|
1310
|
+
this.incrementObservationAccess(exact.id, acceptedAt);
|
|
1311
|
+
row = this.getObservation(exact.id);
|
|
1312
|
+
}
|
|
1313
|
+
else {
|
|
1314
|
+
row = this.addObservation({
|
|
1315
|
+
sessionId: input.sessionId,
|
|
1316
|
+
project: input.project,
|
|
1317
|
+
type: 'user_preference',
|
|
1318
|
+
title: input.title,
|
|
1319
|
+
narrative: input.content,
|
|
1320
|
+
importance: 10,
|
|
1321
|
+
confidence: 0.95,
|
|
1322
|
+
source: 'explicit_intent',
|
|
1323
|
+
sourceEventIds: input.sourceEventId ? [input.sourceEventId] : [],
|
|
1324
|
+
timestamp: acceptedAt,
|
|
1325
|
+
qualityStatus: 'accepted',
|
|
1326
|
+
});
|
|
1327
|
+
this.db.prepare('UPDATE observations SET explicit_accepted_at = ? WHERE id = ?').run(acceptedAt, row.id);
|
|
1328
|
+
row = this.getObservation(row.id);
|
|
1329
|
+
}
|
|
1330
|
+
this.supersedeConflictingExplicitPreferences(row, analyzePreference(input.content));
|
|
1331
|
+
if (input.sourceEventId)
|
|
1332
|
+
this.markEventsProcessed([input.sourceEventId]);
|
|
1333
|
+
return this.getObservation(row.id);
|
|
1334
|
+
})();
|
|
1335
|
+
}
|
|
1336
|
+
applyExplicitMemoryEnrichment(observationId, enrichment) {
|
|
1337
|
+
return this.db.transaction(() => {
|
|
1338
|
+
const current = this.getObservation(observationId);
|
|
1339
|
+
if (!current)
|
|
1340
|
+
throw new Error(`Observation ${observationId} not found`);
|
|
1341
|
+
if (current.source !== 'explicit_intent') {
|
|
1342
|
+
throw new Error(`Observation ${observationId} is not an explicit memory`);
|
|
1343
|
+
}
|
|
1344
|
+
const dedupKey = observationDedupKey(enrichment.type, enrichment.title);
|
|
1345
|
+
const collisions = this.db.prepare(`
|
|
1346
|
+
SELECT id, timestamp, explicit_accepted_at FROM observations
|
|
1347
|
+
WHERE project = ? AND dedup_key = ? AND id <> ?
|
|
1348
|
+
`).all(current.project, dedupKey, observationId);
|
|
1349
|
+
let dedupKeyForCurrent = dedupKey;
|
|
1350
|
+
if (collisions.length > 0) {
|
|
1351
|
+
const candidates = [
|
|
1352
|
+
{
|
|
1353
|
+
id: current.id,
|
|
1354
|
+
timestamp: current.timestamp,
|
|
1355
|
+
explicit_accepted_at: current.explicit_accepted_at,
|
|
1356
|
+
},
|
|
1357
|
+
...collisions,
|
|
1358
|
+
].sort((left, right) => (right.explicit_accepted_at ?? right.timestamp) - (left.explicit_accepted_at ?? left.timestamp)
|
|
1359
|
+
|| right.id.localeCompare(left.id));
|
|
1360
|
+
const winner = candidates[0];
|
|
1361
|
+
const now = Date.now();
|
|
1362
|
+
const supersede = this.db.prepare(`
|
|
1363
|
+
UPDATE observations
|
|
1364
|
+
SET resolution_state = 'superseded', superseded_by_observation = ?,
|
|
1365
|
+
dedup_key = NULL, updated_at = ?
|
|
1366
|
+
WHERE id = ?
|
|
1367
|
+
`);
|
|
1368
|
+
for (const candidate of candidates.slice(1)) {
|
|
1369
|
+
supersede.run(winner.id, now, candidate.id);
|
|
1370
|
+
}
|
|
1371
|
+
this.db.prepare(`
|
|
1372
|
+
UPDATE observations
|
|
1373
|
+
SET resolution_state = 'current', superseded_by_observation = NULL,
|
|
1374
|
+
dedup_key = ?, updated_at = ?
|
|
1375
|
+
WHERE id = ?
|
|
1376
|
+
`).run(dedupKey, now, winner.id);
|
|
1377
|
+
dedupKeyForCurrent = winner.id === current.id ? dedupKey : null;
|
|
1378
|
+
}
|
|
1379
|
+
const nextFacts = JSON.stringify(enrichment.facts);
|
|
1380
|
+
const nextFiles = JSON.stringify(enrichment.files);
|
|
1381
|
+
const nextTools = JSON.stringify(enrichment.tools);
|
|
1382
|
+
const nextConcepts = JSON.stringify(enrichment.concepts);
|
|
1383
|
+
const semanticContentChanged = current.type !== enrichment.type
|
|
1384
|
+
|| current.title !== enrichment.title
|
|
1385
|
+
|| current.narrative !== enrichment.narrative
|
|
1386
|
+
|| current.facts !== nextFacts
|
|
1387
|
+
|| current.concepts !== nextConcepts;
|
|
1388
|
+
const now = Date.now();
|
|
1389
|
+
this.db.prepare(`
|
|
1390
|
+
UPDATE observations
|
|
1391
|
+
SET type = ?, title = ?, narrative = ?, facts = ?, files = ?, tools = ?, concepts = ?,
|
|
1392
|
+
dedup_key = ?, evidence_status = ?, evidence_details = ?, updated_at = ?,
|
|
1393
|
+
embedding = CASE WHEN ? = 1 THEN NULL ELSE embedding END
|
|
1394
|
+
WHERE id = ?
|
|
1395
|
+
`).run(enrichment.type, enrichment.title, enrichment.narrative, nextFacts, nextFiles, nextTools, nextConcepts, dedupKeyForCurrent, enrichment.evidenceStatus, JSON.stringify(enrichment.evidence), now, semanticContentChanged ? 1 : 0, observationId);
|
|
1396
|
+
if (semanticContentChanged) {
|
|
1397
|
+
this.db.prepare("DELETE FROM embeddings WHERE source_type = 'observation' AND source_id = ?").run(observationId);
|
|
1398
|
+
}
|
|
1399
|
+
this.db.prepare('DELETE FROM observation_concepts WHERE observation_id = ?').run(observationId);
|
|
1400
|
+
const insertConcept = this.db.prepare('INSERT OR IGNORE INTO observation_concepts (observation_id, concept) VALUES (?, ?)');
|
|
1401
|
+
for (const concept of enrichment.concepts)
|
|
1402
|
+
insertConcept.run(observationId, concept);
|
|
1403
|
+
this.reconcileExplicitPreferenceConflicts(current.project);
|
|
1404
|
+
return this.getObservation(observationId);
|
|
1405
|
+
})();
|
|
1406
|
+
}
|
|
1407
|
+
reconcileExplicitPreferenceConflicts(project = this.defaultProjectRoot) {
|
|
1408
|
+
return this.db.transaction(() => {
|
|
1409
|
+
const rows = this.listCurrentExplicitPreferences(project).sort((left, right) => this.explicitPreferenceAcceptanceTime(left)
|
|
1410
|
+
- this.explicitPreferenceAcceptanceTime(right)
|
|
1411
|
+
|| left.timestamp - right.timestamp
|
|
1412
|
+
|| left.id.localeCompare(right.id));
|
|
1413
|
+
let superseded = 0;
|
|
1414
|
+
const active = [];
|
|
1415
|
+
for (const row of rows) {
|
|
1416
|
+
const analysis = analyzePreferenceObservation(row);
|
|
1417
|
+
if (analysis.status !== 'high_confidence') {
|
|
1418
|
+
active.push({ row, analysis });
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1421
|
+
for (let index = active.length - 1; index >= 0; index--) {
|
|
1422
|
+
const previous = active[index];
|
|
1423
|
+
if (!analysesConflict(previous.analysis, analysis))
|
|
1424
|
+
continue;
|
|
1425
|
+
this.db.prepare(`
|
|
1426
|
+
UPDATE observations
|
|
1427
|
+
SET resolution_state = 'superseded', superseded_by_observation = ?
|
|
1428
|
+
WHERE id = ? AND resolution_state = 'current'
|
|
1429
|
+
`).run(row.id, previous.row.id);
|
|
1430
|
+
active.splice(index, 1);
|
|
1431
|
+
superseded += 1;
|
|
1432
|
+
}
|
|
1433
|
+
active.push({ row, analysis });
|
|
1434
|
+
}
|
|
1435
|
+
return superseded;
|
|
1436
|
+
})();
|
|
1437
|
+
}
|
|
1438
|
+
listCurrentExplicitPreferences(project = this.defaultProjectRoot) {
|
|
1439
|
+
return this.db.prepare(`
|
|
1440
|
+
SELECT * FROM observations
|
|
1441
|
+
WHERE project = ? AND type IN ('user_preference', 'project_convention', 'decision_made')
|
|
1442
|
+
AND source = 'explicit_intent'
|
|
1443
|
+
AND quality_status = 'accepted' AND resolution_state = 'current'
|
|
1444
|
+
ORDER BY COALESCE(explicit_accepted_at, timestamp) DESC, timestamp DESC, id DESC
|
|
1445
|
+
`).all(project);
|
|
1446
|
+
}
|
|
1447
|
+
explicitPreferenceAcceptanceTime(row) {
|
|
1448
|
+
return row.explicit_accepted_at ?? row.timestamp;
|
|
1449
|
+
}
|
|
1450
|
+
supersedeConflictingExplicitPreferences(winner, winnerAnalysis) {
|
|
1451
|
+
if (winnerAnalysis.status !== 'high_confidence')
|
|
1452
|
+
return 0;
|
|
1453
|
+
const update = this.db.prepare(`
|
|
1454
|
+
UPDATE observations
|
|
1455
|
+
SET resolution_state = 'superseded', superseded_by_observation = ?
|
|
1456
|
+
WHERE id = ? AND resolution_state = 'current'
|
|
1457
|
+
`);
|
|
1458
|
+
let superseded = 0;
|
|
1459
|
+
for (const candidate of this.listCurrentExplicitPreferences(winner.project)) {
|
|
1460
|
+
if (candidate.id === winner.id)
|
|
1461
|
+
continue;
|
|
1462
|
+
const analysis = analyzePreferenceObservation(candidate);
|
|
1463
|
+
if (!analysesConflict(analysis, winnerAnalysis))
|
|
1464
|
+
continue;
|
|
1465
|
+
superseded += update.run(winner.id, candidate.id).changes;
|
|
1466
|
+
}
|
|
1467
|
+
return superseded;
|
|
1468
|
+
}
|
|
898
1469
|
getObservation(id) {
|
|
899
1470
|
return this.db.prepare('SELECT * FROM observations WHERE id = ?').get(id) ?? null;
|
|
900
1471
|
}
|
|
@@ -935,7 +1506,7 @@ export class MemoryStore {
|
|
|
935
1506
|
this.recordRetrievalAccess({
|
|
936
1507
|
sourceType: 'observation',
|
|
937
1508
|
sourceId: id,
|
|
938
|
-
accessType: '
|
|
1509
|
+
accessType: 'adopted',
|
|
939
1510
|
purpose: 'dedup_or_reinforcement',
|
|
940
1511
|
timestamp: now,
|
|
941
1512
|
});
|
|
@@ -1125,7 +1696,9 @@ export class MemoryStore {
|
|
|
1125
1696
|
const timestamp = input.timestamp ?? Date.now();
|
|
1126
1697
|
const id = `racc:${timestamp}:${randomUUID().slice(0, 8)}`;
|
|
1127
1698
|
this.db.transaction(() => {
|
|
1128
|
-
|
|
1699
|
+
const accessType = input.accessType ?? 'exposed';
|
|
1700
|
+
const reinforces = accessType === 'adopted';
|
|
1701
|
+
if (input.sourceType === 'memory' && reinforces) {
|
|
1129
1702
|
const updated = this.db.prepare(`
|
|
1130
1703
|
UPDATE memories SET access_count = access_count + 1,
|
|
1131
1704
|
last_reinforced_at = COALESCE(last_reinforced_at, ?),
|
|
@@ -1134,17 +1707,13 @@ export class MemoryStore {
|
|
|
1134
1707
|
`).run(timestamp, timestamp, timestamp, input.sourceId);
|
|
1135
1708
|
if (updated.changes !== 1)
|
|
1136
1709
|
throw new Error(`Memory ${input.sourceId} not found`);
|
|
1137
|
-
const legacyAccessTypes = new Set(['search_hit', 'user_view', 'agent_use', 'manual_ref']);
|
|
1138
|
-
const accessType = legacyAccessTypes.has(input.accessType ?? '')
|
|
1139
|
-
? input.accessType
|
|
1140
|
-
: 'agent_use';
|
|
1141
1710
|
this.db.prepare(`
|
|
1142
1711
|
INSERT INTO access_log
|
|
1143
1712
|
(id, memory_id, access_type, session_id, timestamp, query, purpose, rank, score)
|
|
1144
1713
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1145
|
-
`).run(`acc:${timestamp}:${randomUUID().slice(0, 8)}`, input.sourceId,
|
|
1714
|
+
`).run(`acc:${timestamp}:${randomUUID().slice(0, 8)}`, input.sourceId, 'agent_use', input.sessionId ?? null, timestamp, input.query ?? null, input.purpose ?? null, input.rank ?? null, input.score ?? null);
|
|
1146
1715
|
}
|
|
1147
|
-
else {
|
|
1716
|
+
else if (input.sourceType === 'observation' && reinforces) {
|
|
1148
1717
|
const updated = this.db.prepare(`
|
|
1149
1718
|
UPDATE observations
|
|
1150
1719
|
SET access_count = access_count + 1, updated_at = ?
|
|
@@ -1155,9 +1724,10 @@ export class MemoryStore {
|
|
|
1155
1724
|
}
|
|
1156
1725
|
this.db.prepare(`
|
|
1157
1726
|
INSERT INTO retrieval_access_log
|
|
1158
|
-
(id, source_type, source_id, access_type,
|
|
1159
|
-
|
|
1160
|
-
|
|
1727
|
+
(id, source_type, source_id, access_type, project, session_id, request_id,
|
|
1728
|
+
execution_id, query, purpose, rank, score, timestamp)
|
|
1729
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1730
|
+
`).run(id, input.sourceType, input.sourceId, accessType, input.project ?? this.defaultProjectRoot, input.sessionId ?? null, input.requestId ?? null, input.executionId ?? null, input.query ?? null, input.purpose ?? null, input.rank ?? null, input.score ?? null, timestamp);
|
|
1161
1731
|
})();
|
|
1162
1732
|
}
|
|
1163
1733
|
recordAccess(memoryId, accessType, sessionId) {
|
|
@@ -1172,7 +1742,7 @@ export class MemoryStore {
|
|
|
1172
1742
|
this.recordRetrievalAccess({
|
|
1173
1743
|
sourceType: 'memory',
|
|
1174
1744
|
sourceId: id,
|
|
1175
|
-
accessType: '
|
|
1745
|
+
accessType: 'adopted',
|
|
1176
1746
|
purpose: 'reinforcement',
|
|
1177
1747
|
});
|
|
1178
1748
|
}
|
|
@@ -1181,6 +1751,15 @@ export class MemoryStore {
|
|
|
1181
1751
|
SELECT * FROM retrieval_access_log ORDER BY timestamp DESC, id DESC LIMIT ?
|
|
1182
1752
|
`).all(limit);
|
|
1183
1753
|
}
|
|
1754
|
+
listRetrievalAccessForAttribution(sessionId, executionId) {
|
|
1755
|
+
return this.db.prepare(`
|
|
1756
|
+
SELECT * FROM retrieval_access_log
|
|
1757
|
+
WHERE session_id = ? AND access_type = 'selected'
|
|
1758
|
+
AND (? IS NULL OR execution_id = ?)
|
|
1759
|
+
ORDER BY timestamp DESC, id DESC
|
|
1760
|
+
LIMIT 500
|
|
1761
|
+
`).all(sessionId, executionId ?? null, executionId ?? null);
|
|
1762
|
+
}
|
|
1184
1763
|
// ── Memory History ───────────────────────────────
|
|
1185
1764
|
recordHistory(entry) {
|
|
1186
1765
|
const id = `hist:${Date.now()}:${randomUUID().slice(0, 8)}`;
|