@devflow-tools/memory-engine 0.16.1 → 0.16.3

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.
Files changed (45) hide show
  1. package/dist/embedding-provider.d.ts +2 -0
  2. package/dist/embedding-provider.d.ts.map +1 -1
  3. package/dist/embedding-provider.js +15 -0
  4. package/dist/embedding-provider.js.map +1 -1
  5. package/dist/hybrid-search.d.ts +9 -0
  6. package/dist/hybrid-search.d.ts.map +1 -1
  7. package/dist/hybrid-search.js +97 -19
  8. package/dist/hybrid-search.js.map +1 -1
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/lease-store.d.ts +8 -0
  14. package/dist/lease-store.d.ts.map +1 -1
  15. package/dist/lease-store.js +20 -0
  16. package/dist/lease-store.js.map +1 -1
  17. package/dist/memory-engine.d.ts +3 -1
  18. package/dist/memory-engine.d.ts.map +1 -1
  19. package/dist/memory-engine.js +96 -14
  20. package/dist/memory-engine.js.map +1 -1
  21. package/dist/memory-gate.d.ts +3 -2
  22. package/dist/memory-gate.d.ts.map +1 -1
  23. package/dist/memory-gate.js +89 -44
  24. package/dist/memory-gate.js.map +1 -1
  25. package/dist/memory-maintenance.d.ts +123 -0
  26. package/dist/memory-maintenance.d.ts.map +1 -0
  27. package/dist/memory-maintenance.js +594 -0
  28. package/dist/memory-maintenance.js.map +1 -0
  29. package/dist/memory-store.d.ts +85 -0
  30. package/dist/memory-store.d.ts.map +1 -1
  31. package/dist/memory-store.js +456 -30
  32. package/dist/memory-store.js.map +1 -1
  33. package/dist/migrations/v3-to-v4.d.ts +3 -0
  34. package/dist/migrations/v3-to-v4.d.ts.map +1 -0
  35. package/dist/migrations/v3-to-v4.js +93 -0
  36. package/dist/migrations/v3-to-v4.js.map +1 -0
  37. package/dist/observation-quality.d.ts +39 -0
  38. package/dist/observation-quality.d.ts.map +1 -0
  39. package/dist/observation-quality.js +190 -0
  40. package/dist/observation-quality.js.map +1 -0
  41. package/dist/session-summarizer.d.ts +1 -1
  42. package/dist/session-summarizer.d.ts.map +1 -1
  43. package/dist/session-summarizer.js +3 -0
  44. package/dist/session-summarizer.js.map +1 -1
  45. package/package.json +4 -4
@@ -1,9 +1,10 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import * as sqliteVec from 'sqlite-vec';
3
- import { join } from 'node:path';
3
+ import { dirname, join } from 'node:path';
4
4
  import { existsSync, mkdirSync } from 'node:fs';
5
5
  import { randomUUID } from 'node:crypto';
6
6
  import { applyV2ToV3Migration } from './migrations/v2-to-v3.js';
7
+ import { applyV3ToV4Migration } from './migrations/v3-to-v4.js';
7
8
  import { observationDedupKey, } from './observation-types.js';
8
9
  // ── Schema ─────────────────────────────────────────────
9
10
  const SCHEMA_V2 = `
@@ -258,8 +259,10 @@ END;
258
259
  export class MemoryStore {
259
260
  db;
260
261
  dbPath;
262
+ defaultProjectRoot;
261
263
  leaseStore = null;
262
264
  constructor(rootPath) {
265
+ this.defaultProjectRoot = rootPath;
263
266
  const memDir = join(rootPath, '.devflow', 'memory');
264
267
  if (!existsSync(memDir))
265
268
  mkdirSync(memDir, { recursive: true });
@@ -273,6 +276,8 @@ export class MemoryStore {
273
276
  this.db.exec(FTS_TRIGGERS);
274
277
  this.migrateV1ToV2();
275
278
  applyV2ToV3Migration(this.db);
279
+ applyV3ToV4Migration(this.db);
280
+ this.repairOrphanSessions(rootPath);
276
281
  console.log('[MemoryStore] Schema initialized', {
277
282
  dbPath: this.dbPath,
278
283
  tableCount: this.countTables(),
@@ -377,6 +382,9 @@ export class MemoryStore {
377
382
  }
378
383
  // ── Memory CRUD ──────────────────────────────────
379
384
  add(entry) {
385
+ if (entry.sessionId) {
386
+ this.ensureSession(entry.sessionId, this.defaultProjectRoot);
387
+ }
380
388
  const id = entry.id ?? `mem:${Date.now()}:${randomUUID().slice(0, 8)}`;
381
389
  const now = Date.now();
382
390
  const memType = entry.type ?? entry.category ?? 'fact';
@@ -479,6 +487,12 @@ export class MemoryStore {
479
487
  }
480
488
  })();
481
489
  }
490
+ commitObservationEmbeddingBatch(entries, checkpoint) {
491
+ this.db.transaction(() => {
492
+ this.insertObservationEmbeddings(entries);
493
+ this.setMaintenanceCheckpoint(checkpoint.action, checkpoint.cursor, checkpoint.details ?? {});
494
+ })();
495
+ }
482
496
  getEmbedding(id) {
483
497
  const row = this.db.prepare('SELECT embedding FROM memories_vec WHERE id = ?').get(id);
484
498
  return row?.embedding ?? null;
@@ -493,6 +507,8 @@ export class MemoryStore {
493
507
  SELECT o.*, vec_distance_cosine(o.embedding, ?) AS distance
494
508
  FROM observations o
495
509
  WHERE o.embedding IS NOT NULL
510
+ AND o.quality_status = 'accepted'
511
+ AND o.resolution_state <> 'superseded'
496
512
  ORDER BY distance LIMIT ?
497
513
  `).all(buffer, limit);
498
514
  return rows.map(row => ({ observation: row, distance: row.distance }));
@@ -529,19 +545,22 @@ export class MemoryStore {
529
545
  }
530
546
  // ── Events CRUD ──────────────────────────────────
531
547
  recordEvent(event) {
548
+ this.ensureSession(event.sessionId, event.project ?? this.defaultProjectRoot);
532
549
  this.db.prepare(`
533
550
  INSERT OR IGNORE INTO session_events
534
551
  (id, session_id, project, tool, command, exit_code, stderr,
535
552
  duration_ms, created_at, l1_score, dialog_score, user_message,
536
553
  kind, payload)
537
554
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
538
- `).run(event.id, event.sessionId, event.project ?? '', event.tool, event.command ?? null, event.exitCode ?? null, event.stderr ?? null, event.durationMs ?? null, event.createdAt, event.l1Score ?? 0, event.dialogScore ?? 0, event.userMessage ?? null, event.kind ?? null, event.payload ? JSON.stringify(event.payload) : null);
555
+ `).run(event.id, event.sessionId, event.project ?? this.defaultProjectRoot, event.tool, event.command ?? null, event.exitCode ?? null, event.stderr ?? null, event.durationMs ?? null, event.createdAt, event.l1Score ?? 0, event.dialogScore ?? 0, event.userMessage ?? null, event.kind ?? null, event.payload ? JSON.stringify(event.payload) : null);
539
556
  }
540
557
  getSessionEvents(sessionId) {
541
558
  return this.db.prepare('SELECT * FROM session_events WHERE session_id = ? ORDER BY created_at ASC').all(sessionId);
542
559
  }
543
560
  getUnprocessedEvents() {
544
- const events = this.db.prepare('SELECT * FROM session_events WHERE processed = 0 ORDER BY created_at ASC').all();
561
+ const events = this.db.prepare(`SELECT * FROM session_events
562
+ WHERE processed = 0 AND maintenance_status = 'active'
563
+ ORDER BY created_at ASC`).all();
545
564
  const pendingIds = this.leaseStore?.getPendingEventIds();
546
565
  if (!pendingIds || pendingIds.size === 0)
547
566
  return events;
@@ -551,7 +570,170 @@ export class MemoryStore {
551
570
  if (ids.length === 0)
552
571
  return;
553
572
  const placeholders = ids.map(() => '?').join(',');
554
- this.db.prepare(`UPDATE session_events SET processed = 1 WHERE id IN (${placeholders})`).run(...ids);
573
+ const unprocessed = this.db.prepare(`SELECT id FROM session_events WHERE processed = 0 AND id IN (${placeholders})`).all(...ids);
574
+ if (unprocessed.length === 0)
575
+ return;
576
+ const coveredEventIds = new Set();
577
+ const observationRows = this.db.prepare(`SELECT source_event_ids FROM observations
578
+ WHERE quality_status = 'accepted' AND resolution_state <> 'superseded'`).all();
579
+ for (const row of observationRows) {
580
+ try {
581
+ const sourceEventIds = JSON.parse(row.source_event_ids);
582
+ if (Array.isArray(sourceEventIds)) {
583
+ for (const eventId of sourceEventIds) {
584
+ if (typeof eventId === 'string')
585
+ coveredEventIds.add(eventId);
586
+ }
587
+ }
588
+ }
589
+ catch {
590
+ // Malformed historical evidence cannot justify processing an event.
591
+ }
592
+ }
593
+ const missing = unprocessed.map(event => event.id).filter(id => !coveredEventIds.has(id));
594
+ if (missing.length > 0) {
595
+ throw new Error(`Cannot mark events processed without committed distilled observations: ${missing.join(', ')}`);
596
+ }
597
+ const pendingIds = unprocessed.map(event => event.id);
598
+ const pendingPlaceholders = pendingIds.map(() => '?').join(',');
599
+ this.db.prepare(`UPDATE session_events SET processed = 1 WHERE id IN (${pendingPlaceholders})`).run(...pendingIds);
600
+ }
601
+ discardEvents(ids, reason, runId) {
602
+ if (ids.length === 0)
603
+ return 0;
604
+ const uniqueIds = [...new Set(ids)];
605
+ const update = this.db.prepare(`
606
+ UPDATE session_events
607
+ SET processed = 1, maintenance_status = 'discarded', quarantine_reason = ?
608
+ WHERE id = ? AND processed = 0
609
+ `);
610
+ const insertAudit = this.db.prepare(`
611
+ INSERT INTO event_processing_audit
612
+ (id, event_id, disposition, reason, run_id, timestamp)
613
+ VALUES (?, ?, 'discarded', ?, ?, ?)
614
+ `);
615
+ return this.db.transaction(() => {
616
+ let changed = 0;
617
+ const now = Date.now();
618
+ for (const id of uniqueIds) {
619
+ const result = update.run(reason, id);
620
+ if (result.changes === 0)
621
+ continue;
622
+ changed += result.changes;
623
+ insertAudit.run(`epa:${now}:${randomUUID().slice(0, 8)}`, id, reason, runId ?? null, now);
624
+ }
625
+ return changed;
626
+ })();
627
+ }
628
+ listAllEvents() {
629
+ return this.db.prepare('SELECT * FROM session_events ORDER BY created_at ASC, id ASC').all();
630
+ }
631
+ deduplicateEvents(canonicalId, duplicateIds, runId) {
632
+ const duplicates = [...new Set(duplicateIds)].filter(id => id !== canonicalId);
633
+ if (duplicates.length === 0)
634
+ return { deleted: 0, observationsRewritten: 0 };
635
+ const allIds = [canonicalId, ...duplicates];
636
+ const placeholders = allIds.map(() => '?').join(',');
637
+ const rows = this.db.prepare(`SELECT * FROM session_events WHERE id IN (${placeholders}) ORDER BY created_at ASC, id ASC`).all(...allIds);
638
+ if (rows.length !== allIds.length)
639
+ throw new Error('Duplicate event group changed during maintenance');
640
+ const pendingLeaseRows = this.db.prepare("SELECT event_ids FROM distill_leases WHERE status = 'pending'").all();
641
+ for (const lease of pendingLeaseRows) {
642
+ const eventIds = parseStringArray(lease.event_ids);
643
+ if (eventIds.some(id => allIds.includes(id))) {
644
+ throw new Error('Cannot deduplicate events referenced by a pending distill lease');
645
+ }
646
+ }
647
+ return this.db.transaction(() => {
648
+ const canonical = rows.find(row => row.id === canonicalId);
649
+ const richest = [...rows].sort((left, right) => eventRichness(right) - eventRichness(left))[0];
650
+ const processed = rows.some(row => row.processed === 1) ? 1 : 0;
651
+ this.db.prepare(`
652
+ UPDATE session_events SET
653
+ command = ?, exit_code = ?, stderr = ?, duration_ms = ?, processed = ?,
654
+ l1_score = MAX(l1_score, ?), dialog_score = MAX(dialog_score, ?),
655
+ user_message = ?, kind = ?, payload = ?
656
+ WHERE id = ?
657
+ `).run(richest.command ?? canonical.command, richest.exit_code ?? canonical.exit_code, richest.stderr ?? canonical.stderr, richest.duration_ms ?? canonical.duration_ms, processed, ...rows.map(row => row.l1_score).sort((a, b) => b - a).slice(0, 1), ...rows.map(row => row.dialog_score).sort((a, b) => b - a).slice(0, 1), richest.user_message ?? canonical.user_message, richest.kind ?? canonical.kind, richest.payload ?? canonical.payload, canonicalId);
658
+ let observationsRewritten = 0;
659
+ const observations = this.db.prepare('SELECT id, source_event_ids FROM observations').all();
660
+ const duplicateSet = new Set(duplicates);
661
+ const updateObservation = this.db.prepare('UPDATE observations SET source_event_ids = ?, updated_at = ? WHERE id = ?');
662
+ for (const observation of observations) {
663
+ const sourceIds = parseStringArray(observation.source_event_ids);
664
+ if (!sourceIds.some(id => duplicateSet.has(id)))
665
+ continue;
666
+ const rewritten = [...new Set(sourceIds.map(id => duplicateSet.has(id) ? canonicalId : id))];
667
+ updateObservation.run(JSON.stringify(rewritten), Date.now(), observation.id);
668
+ observationsRewritten += 1;
669
+ }
670
+ const deleteEvent = this.db.prepare('DELETE FROM session_events WHERE id = ?');
671
+ const insertAudit = this.db.prepare(`
672
+ INSERT INTO event_processing_audit
673
+ (id, event_id, disposition, reason, run_id, timestamp)
674
+ VALUES (?, ?, 'duplicate_merged', 'historical_duplicate', ?, ?)
675
+ `);
676
+ let deleted = 0;
677
+ const now = Date.now();
678
+ for (const id of duplicates) {
679
+ deleted += deleteEvent.run(id).changes;
680
+ insertAudit.run(`epa:${now}:${randomUUID().slice(0, 8)}`, id, runId ?? null, now);
681
+ }
682
+ return { deleted, observationsRewritten };
683
+ })();
684
+ }
685
+ quarantineDuplicateEvents(groupKey, eventIds, reason) {
686
+ const uniqueIds = [...new Set(eventIds)];
687
+ if (uniqueIds.length === 0)
688
+ return 0;
689
+ return this.db.transaction(() => {
690
+ const update = this.db.prepare(`
691
+ UPDATE session_events
692
+ SET maintenance_status = 'quarantined', quarantine_reason = ?
693
+ WHERE id = ? AND maintenance_status <> 'quarantined'
694
+ `);
695
+ let changed = 0;
696
+ for (const id of uniqueIds)
697
+ changed += update.run(reason, id).changes;
698
+ this.db.prepare(`
699
+ INSERT INTO event_duplicate_quarantine
700
+ (group_key, event_ids, reason, created_at, resolved_at)
701
+ VALUES (?, ?, ?, ?, NULL)
702
+ ON CONFLICT(group_key) DO UPDATE SET
703
+ event_ids = excluded.event_ids,
704
+ reason = excluded.reason,
705
+ resolved_at = NULL
706
+ `).run(groupKey, JSON.stringify(uniqueIds), reason, Date.now());
707
+ return changed;
708
+ })();
709
+ }
710
+ requeueProcessedEventsWithoutAcceptedEvidence() {
711
+ const acceptedEvidence = new Set();
712
+ const observations = this.db.prepare(`
713
+ SELECT source_event_ids FROM observations
714
+ WHERE quality_status = 'accepted' AND resolution_state <> 'superseded'
715
+ `).all();
716
+ for (const observation of observations) {
717
+ for (const id of parseStringArray(observation.source_event_ids))
718
+ acceptedEvidence.add(id);
719
+ }
720
+ const intentionallyDiscarded = new Set(this.db.prepare(`
721
+ SELECT DISTINCT event_id FROM event_processing_audit WHERE disposition = 'discarded'
722
+ `).all().map(row => row.event_id));
723
+ const candidates = this.db.prepare(`
724
+ SELECT id FROM session_events WHERE processed = 1
725
+ `).all();
726
+ const ids = candidates
727
+ .map(row => row.id)
728
+ .filter(id => !acceptedEvidence.has(id) && !intentionallyDiscarded.has(id));
729
+ if (ids.length === 0)
730
+ return 0;
731
+ const update = this.db.prepare(`
732
+ UPDATE session_events
733
+ SET processed = 0, maintenance_status = 'active', quarantine_reason = NULL
734
+ WHERE id = ?
735
+ `);
736
+ return this.db.transaction(() => ids.reduce((count, id) => count + update.run(id).changes, 0))();
555
737
  }
556
738
  eventWindowExists(tool, command, windowMs = 5 * 60 * 1000) {
557
739
  const since = Date.now() - windowMs;
@@ -563,14 +745,72 @@ export class MemoryStore {
563
745
  return row !== undefined;
564
746
  }
565
747
  // ── Session CRUD ─────────────────────────────────
566
- createSession(id, projectRoot, task = '') {
748
+ ensureSession(id, projectRoot = this.defaultProjectRoot, task = '') {
749
+ const sessionId = id.trim();
750
+ if (!sessionId)
751
+ throw new Error('Memory session ID must be a nonblank canonical host session ID');
752
+ const normalizedProjectRoot = projectRoot.trim() || this.defaultProjectRoot;
567
753
  this.db.prepare(`
568
754
  INSERT INTO sessions (id, project_root, task, status, created_at)
569
755
  VALUES (?, ?, ?, 'active', ?)
570
- `).run(id, projectRoot, task, Date.now());
756
+ ON CONFLICT(id) DO UPDATE SET
757
+ project_root = CASE
758
+ WHEN sessions.project_root = '' THEN excluded.project_root
759
+ ELSE sessions.project_root
760
+ END,
761
+ task = CASE
762
+ WHEN sessions.task = '' AND excluded.task <> '' THEN excluded.task
763
+ ELSE sessions.task
764
+ END
765
+ `).run(sessionId, normalizedProjectRoot, task, Date.now());
766
+ return this.getSession(sessionId);
767
+ }
768
+ repairOrphanSessions(projectRoot = this.defaultProjectRoot) {
769
+ const repair = this.db.transaction(() => {
770
+ const blankEvents = this.db.prepare(`
771
+ SELECT id FROM session_events WHERE TRIM(session_id) = '' ORDER BY created_at ASC
772
+ `).all();
773
+ const assignRecoveredSession = this.db.prepare('UPDATE session_events SET session_id = ? WHERE id = ? AND TRIM(session_id) = \'\'');
774
+ for (const event of blankEvents) {
775
+ assignRecoveredSession.run(`recovered:orphan:${event.id}`, event.id);
776
+ }
777
+ const orphanSessions = this.db.prepare(`
778
+ SELECT
779
+ e.session_id AS id,
780
+ COALESCE(NULLIF(MAX(e.project), ''), ?) AS project_root,
781
+ MIN(e.created_at) AS created_at,
782
+ MAX(e.created_at) AS closed_at,
783
+ COUNT(*) AS tool_count,
784
+ SUM(CASE WHEN e.exit_code IS NOT NULL AND e.exit_code <> 0 THEN 1 ELSE 0 END) AS error_count
785
+ FROM session_events e
786
+ LEFT JOIN sessions s ON s.id = e.session_id
787
+ WHERE s.id IS NULL
788
+ GROUP BY e.session_id
789
+ `).all(projectRoot);
790
+ const insertRecovered = this.db.prepare(`
791
+ INSERT OR IGNORE INTO sessions
792
+ (id, project_root, task, status, phase_summary, signal_summary,
793
+ tool_count, error_count, created_at, closed_at)
794
+ VALUES (?, ?, 'Recovered orphan session events', 'closed', ?, '[]', ?, ?, ?, ?)
795
+ `);
796
+ let repaired = 0;
797
+ for (const session of orphanSessions) {
798
+ repaired += insertRecovered.run(session.id, session.project_root || projectRoot, JSON.stringify(['recovered_orphan_events']), session.tool_count, session.error_count, session.created_at, session.closed_at).changes;
799
+ }
800
+ return repaired;
801
+ });
802
+ const repaired = repair();
803
+ if (repaired > 0) {
804
+ console.log('[MemoryStore] Recovered orphan event sessions', { count: repaired });
805
+ }
806
+ return repaired;
807
+ }
808
+ createSession(id, projectRoot, task = '') {
809
+ this.ensureSession(id, projectRoot, task);
571
810
  console.log('[MemoryStore] Session created', { id, project: projectRoot, task });
572
811
  }
573
812
  closeSession(id) {
813
+ this.ensureSession(id, this.defaultProjectRoot, 'Recovered session close');
574
814
  const now = Date.now();
575
815
  this.db.prepare(`
576
816
  UPDATE sessions SET status = 'closed', closed_at = ?
@@ -606,20 +846,24 @@ export class MemoryStore {
606
846
  }
607
847
  // ── Observation CRUD ─────────────────────────────
608
848
  addObservation(obs) {
849
+ this.ensureSession(obs.sessionId, obs.project ?? this.defaultProjectRoot);
609
850
  const id = obs.id ?? `obs:${Date.now()}:${randomUUID().slice(0, 8)}`;
610
851
  const now = obs.timestamp ?? Date.now();
611
852
  const dedupKey = observationDedupKey(obs.type, obs.title);
612
853
  const duplicate = this.findObservationByDedupKey(dedupKey);
613
854
  if (duplicate) {
855
+ this.linkObservationToEvents(duplicate.id, obs.sourceEventIds ?? []);
614
856
  this.incrementObservationAccess(duplicate.id, now);
615
857
  return this.getObservation(duplicate.id);
616
858
  }
617
859
  this.db.prepare(`
618
860
  INSERT INTO observations
619
861
  (id, session_id, project, type, title, narrative, facts, files, tools, concepts,
620
- importance, confidence, source, dedup_key, source_event_ids, timestamp, updated_at)
621
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
622
- `).run(id, obs.sessionId, obs.project ?? '', 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);
862
+ importance, confidence, source, dedup_key, source_event_ids, timestamp, updated_at,
863
+ quality_status, quarantine_reason, quality_details, quarantined_at,
864
+ failure_category, resolution_state)
865
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
866
+ `).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');
623
867
  // Insert concept junction rows
624
868
  if (obs.concepts && obs.concepts.length > 0) {
625
869
  const insertConcept = this.db.prepare('INSERT OR IGNORE INTO observation_concepts (observation_id, concept) VALUES (?, ?)');
@@ -632,9 +876,34 @@ export class MemoryStore {
632
876
  });
633
877
  return this.getObservation(id);
634
878
  }
879
+ addObservationWithEmbedding(obs, embedding) {
880
+ return this.db.transaction(() => {
881
+ const row = this.addObservation(obs);
882
+ this.insertObservationEmbedding(row.id, embedding);
883
+ return this.getObservation(row.id);
884
+ })();
885
+ }
635
886
  getObservation(id) {
636
887
  return this.db.prepare('SELECT * FROM observations WHERE id = ?').get(id) ?? null;
637
888
  }
889
+ linkObservationToEvents(observationId, eventIds) {
890
+ if (eventIds.length === 0)
891
+ return;
892
+ const observation = this.getObservation(observationId);
893
+ if (!observation)
894
+ throw new Error(`Observation ${observationId} not found`);
895
+ let existing = [];
896
+ try {
897
+ const parsed = JSON.parse(observation.source_event_ids);
898
+ if (Array.isArray(parsed))
899
+ existing = parsed.filter((id) => typeof id === 'string');
900
+ }
901
+ catch {
902
+ // Replace malformed historical evidence with the current committed links.
903
+ }
904
+ const merged = [...new Set([...existing, ...eventIds])];
905
+ this.db.prepare('UPDATE observations SET source_event_ids = ?, updated_at = ? WHERE id = ?').run(JSON.stringify(merged), Date.now(), observationId);
906
+ }
638
907
  findObservationByDedupKey(dedupKey) {
639
908
  return this.db.prepare('SELECT * FROM observations WHERE dedup_key = ?').get(dedupKey) ?? null;
640
909
  }
@@ -644,26 +913,30 @@ export class MemoryStore {
644
913
  SELECT o.*
645
914
  FROM observations o
646
915
  WHERE o.type = ? AND o.timestamp >= ? AND o.embedding IS NOT NULL
916
+ AND o.quality_status = 'accepted' AND o.resolution_state <> 'superseded'
647
917
  AND (1.0 - vec_distance_cosine(o.embedding, ?)) > ?
648
918
  ORDER BY vec_distance_cosine(o.embedding, ?) ASC
649
919
  LIMIT 1
650
920
  `).get(type, cutoff, buffer, threshold, buffer) ?? null;
651
921
  }
652
922
  incrementObservationAccess(id, now = Date.now()) {
653
- this.db.prepare(`
654
- UPDATE observations
655
- SET access_count = access_count + 1, updated_at = ?
656
- WHERE id = ?
657
- `).run(now, id);
923
+ this.recordRetrievalAccess({
924
+ sourceType: 'observation',
925
+ sourceId: id,
926
+ accessType: 'operational',
927
+ purpose: 'dedup_or_reinforcement',
928
+ timestamp: now,
929
+ });
658
930
  }
659
931
  promoteObservationToExplicitIntent(id, now = Date.now()) {
660
932
  this.db.prepare(`
661
933
  UPDATE observations
662
934
  SET source = 'explicit_intent', confidence = MAX(confidence, 0.95),
663
- importance = MAX(importance, 10), access_count = access_count + 1,
935
+ importance = MAX(importance, 10),
664
936
  updated_at = ?
665
937
  WHERE id = ?
666
938
  `).run(now, id);
939
+ this.incrementObservationAccess(id, now);
667
940
  return this.getObservation(id);
668
941
  }
669
942
  listObservations(filter) {
@@ -681,6 +954,14 @@ export class MemoryStore {
681
954
  conditions.push('o.type = ?');
682
955
  vals.push(filter.type);
683
956
  }
957
+ if (filter?.qualityStatus) {
958
+ conditions.push('o.quality_status = ?');
959
+ vals.push(filter.qualityStatus);
960
+ }
961
+ if (filter?.resolutionState) {
962
+ conditions.push('o.resolution_state = ?');
963
+ vals.push(filter.resolutionState);
964
+ }
684
965
  if (filter?.memoryId !== undefined) {
685
966
  if (filter.memoryId === null) {
686
967
  conditions.push('o.memory_id IS NULL');
@@ -709,23 +990,44 @@ export class MemoryStore {
709
990
  listObservationsMissingEmbedding(limit = 32, afterId) {
710
991
  return this.db.prepare(`
711
992
  SELECT * FROM observations
712
- WHERE embedding IS NULL AND (? IS NULL OR id > ?)
993
+ WHERE embedding IS NULL AND quality_status = 'accepted'
994
+ AND resolution_state <> 'superseded' AND (? IS NULL OR id > ?)
713
995
  ORDER BY id LIMIT ?
714
996
  `).all(afterId ?? null, afterId ?? null, limit);
715
997
  }
716
998
  countObservationVectors() {
717
999
  const row = this.db.prepare(`
718
1000
  SELECT COUNT(*) AS observations,
719
- SUM(CASE WHEN embedding IS NOT NULL THEN 1 ELSE 0 END) AS vectors,
720
- SUM(CASE WHEN embedding IS NULL THEN 1 ELSE 0 END) AS missing
1001
+ SUM(CASE WHEN quality_status = 'accepted' AND resolution_state <> 'superseded' THEN 1 ELSE 0 END) AS eligible,
1002
+ SUM(CASE WHEN quality_status = 'accepted' AND resolution_state <> 'superseded' AND embedding IS NOT NULL THEN 1 ELSE 0 END) AS vectors,
1003
+ SUM(CASE WHEN quality_status = 'accepted' AND resolution_state <> 'superseded' AND embedding IS NULL THEN 1 ELSE 0 END) AS missing,
1004
+ SUM(CASE WHEN quality_status = 'quarantined' THEN 1 ELSE 0 END) AS quarantined
721
1005
  FROM observations
722
1006
  `).get();
723
1007
  return {
724
1008
  observations: row.observations,
1009
+ eligible: row.eligible ?? 0,
725
1010
  vectors: row.vectors ?? 0,
726
1011
  missing: row.missing ?? 0,
1012
+ quarantined: row.quarantined ?? 0,
727
1013
  };
728
1014
  }
1015
+ quarantineObservation(id, reason, details = {}, now = Date.now()) {
1016
+ const result = this.db.prepare(`
1017
+ UPDATE observations
1018
+ SET quality_status = 'quarantined', quarantine_reason = ?, quality_details = ?,
1019
+ quarantined_at = ?, updated_at = ?
1020
+ WHERE id = ? AND quality_status <> 'quarantined'
1021
+ `).run(reason, JSON.stringify(details), now, now, id);
1022
+ return result.changes === 1;
1023
+ }
1024
+ setObservationResolution(id, state, supersededBy) {
1025
+ this.db.prepare(`
1026
+ UPDATE observations
1027
+ SET resolution_state = ?, superseded_by_observation = ?, updated_at = ?
1028
+ WHERE id = ?
1029
+ `).run(state, supersededBy ?? null, Date.now(), id);
1030
+ }
729
1031
  getObservationsByConcept(concept, limit = 50) {
730
1032
  return this.listObservations({ concept, limit });
731
1033
  }
@@ -749,6 +1051,7 @@ export class MemoryStore {
749
1051
  return this.db.prepare(`
750
1052
  SELECT * FROM observations
751
1053
  WHERE project = ? AND memory_id IS NULL AND importance >= ?
1054
+ AND quality_status = 'accepted' AND resolution_state <> 'superseded'
752
1055
  ORDER BY timestamp DESC
753
1056
  `).all(project, minImportance);
754
1057
  }
@@ -763,6 +1066,7 @@ export class MemoryStore {
763
1066
  FROM observation_concepts oc
764
1067
  JOIN observations o ON o.id = oc.observation_id
765
1068
  WHERE o.project = ? AND o.memory_id IS NULL AND o.importance >= 5
1069
+ AND o.quality_status = 'accepted' AND o.resolution_state <> 'superseded'
766
1070
  GROUP BY oc.concept
767
1071
  HAVING observation_count >= ? AND session_count >= ?
768
1072
  ORDER BY observation_count DESC
@@ -782,6 +1086,7 @@ export class MemoryStore {
782
1086
  VALUES (?, ?, ?, ?, ?, ?, ?)
783
1087
  `);
784
1088
  for (const s of signals) {
1089
+ this.ensureSession(s.sessionId, this.defaultProjectRoot);
785
1090
  insert.run(s.id, s.sessionId, s.type, s.strength, JSON.stringify(s.sourceEventIds), JSON.stringify(s.payload), s.timestamp);
786
1091
  }
787
1092
  if (signals.length > 0) {
@@ -804,21 +1109,65 @@ export class MemoryStore {
804
1109
  return this.db.prepare('SELECT * FROM evidence WHERE memory_id = ? ORDER BY timestamp DESC').all(memoryId);
805
1110
  }
806
1111
  // ── Access Log ───────────────────────────────────
1112
+ recordRetrievalAccess(input) {
1113
+ const timestamp = input.timestamp ?? Date.now();
1114
+ const id = `racc:${timestamp}:${randomUUID().slice(0, 8)}`;
1115
+ this.db.transaction(() => {
1116
+ if (input.sourceType === 'memory') {
1117
+ const updated = this.db.prepare(`
1118
+ UPDATE memories SET access_count = access_count + 1,
1119
+ last_reinforced_at = COALESCE(last_reinforced_at, ?),
1120
+ last_accessed_at = ?, updated_at = ?
1121
+ WHERE id = ?
1122
+ `).run(timestamp, timestamp, timestamp, input.sourceId);
1123
+ if (updated.changes !== 1)
1124
+ throw new Error(`Memory ${input.sourceId} not found`);
1125
+ const legacyAccessTypes = new Set(['search_hit', 'user_view', 'agent_use', 'manual_ref']);
1126
+ const accessType = legacyAccessTypes.has(input.accessType ?? '')
1127
+ ? input.accessType
1128
+ : 'agent_use';
1129
+ this.db.prepare(`
1130
+ INSERT INTO access_log
1131
+ (id, memory_id, access_type, session_id, timestamp, query, purpose, rank, score)
1132
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1133
+ `).run(`acc:${timestamp}:${randomUUID().slice(0, 8)}`, input.sourceId, accessType, input.sessionId ?? null, timestamp, input.query ?? null, input.purpose ?? null, input.rank ?? null, input.score ?? null);
1134
+ }
1135
+ else {
1136
+ const updated = this.db.prepare(`
1137
+ UPDATE observations
1138
+ SET access_count = access_count + 1, updated_at = ?
1139
+ WHERE id = ?
1140
+ `).run(timestamp, input.sourceId);
1141
+ if (updated.changes !== 1)
1142
+ throw new Error(`Observation ${input.sourceId} not found`);
1143
+ }
1144
+ this.db.prepare(`
1145
+ INSERT INTO retrieval_access_log
1146
+ (id, source_type, source_id, access_type, session_id, query, purpose, rank, score, timestamp)
1147
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1148
+ `).run(id, input.sourceType, input.sourceId, input.accessType ?? 'search_hit', input.sessionId ?? null, input.query ?? null, input.purpose ?? null, input.rank ?? null, input.score ?? null, timestamp);
1149
+ })();
1150
+ }
807
1151
  recordAccess(memoryId, accessType, sessionId) {
808
- const id = `acc:${Date.now()}:${randomUUID().slice(0, 8)}`;
809
- this.db.prepare(`
810
- INSERT INTO access_log (id, memory_id, access_type, session_id, timestamp)
811
- VALUES (?, ?, ?, ?, ?)
812
- `).run(id, memoryId, accessType, sessionId ?? null, Date.now());
1152
+ this.recordRetrievalAccess({
1153
+ sourceType: 'memory',
1154
+ sourceId: memoryId,
1155
+ accessType,
1156
+ sessionId,
1157
+ });
813
1158
  }
814
1159
  incrementAccess(id) {
815
- const now = Date.now();
816
- this.db.prepare(`
817
- UPDATE memories SET access_count = access_count + 1,
818
- last_reinforced_at = COALESCE(last_reinforced_at, ?),
819
- last_accessed_at = ?, updated_at = ?
820
- WHERE id = ?
821
- `).run(now, now, now, id);
1160
+ this.recordRetrievalAccess({
1161
+ sourceType: 'memory',
1162
+ sourceId: id,
1163
+ accessType: 'agent_use',
1164
+ purpose: 'reinforcement',
1165
+ });
1166
+ }
1167
+ listRetrievalAccessLog(limit = 100) {
1168
+ return this.db.prepare(`
1169
+ SELECT * FROM retrieval_access_log ORDER BY timestamp DESC, id DESC LIMIT ?
1170
+ `).all(limit);
822
1171
  }
823
1172
  // ── Memory History ───────────────────────────────
824
1173
  recordHistory(entry) {
@@ -979,6 +1328,52 @@ export class MemoryStore {
979
1328
  const result = this.db.prepare("DELETE FROM memories WHERE status = 'pending' AND source = 'rule_engine' AND confidence < 0.6").run();
980
1329
  return { removed: result.changes };
981
1330
  }
1331
+ getMaintenanceCheckpoint(action) {
1332
+ const row = this.db.prepare(`
1333
+ SELECT action, cursor, details, updated_at
1334
+ FROM maintenance_checkpoints WHERE action = ?
1335
+ `).get(action);
1336
+ if (!row)
1337
+ return null;
1338
+ return {
1339
+ action: row.action,
1340
+ cursor: row.cursor,
1341
+ details: parseObject(row.details),
1342
+ updatedAt: row.updated_at,
1343
+ };
1344
+ }
1345
+ setMaintenanceCheckpoint(action, cursor, details = {}) {
1346
+ this.db.prepare(`
1347
+ INSERT INTO maintenance_checkpoints (action, cursor, details, updated_at)
1348
+ VALUES (?, ?, ?, ?)
1349
+ ON CONFLICT(action) DO UPDATE SET
1350
+ cursor = excluded.cursor,
1351
+ details = excluded.details,
1352
+ updated_at = excluded.updated_at
1353
+ `).run(action, cursor, JSON.stringify(details), Date.now());
1354
+ }
1355
+ clearMaintenanceCheckpoint(action) {
1356
+ this.db.prepare('DELETE FROM maintenance_checkpoints WHERE action = ?').run(action);
1357
+ }
1358
+ recordMaintenanceRun(input) {
1359
+ this.db.prepare(`
1360
+ INSERT INTO memory_maintenance_runs (id, mode, actions, manifest, created_at)
1361
+ VALUES (?, ?, ?, ?, ?)
1362
+ `).run(input.id, input.mode, JSON.stringify(input.actions), JSON.stringify(input.manifest), input.createdAt ?? Date.now());
1363
+ }
1364
+ countRecentRetrievalAccess(since) {
1365
+ const row = this.db.prepare(`
1366
+ SELECT COUNT(*) AS count FROM retrieval_access_log WHERE timestamp >= ?
1367
+ `).get(since);
1368
+ return row.count;
1369
+ }
1370
+ getDatabasePath() {
1371
+ return this.dbPath;
1372
+ }
1373
+ async backupTo(destination) {
1374
+ mkdirSync(dirname(destination), { recursive: true });
1375
+ await this.db.backup(destination);
1376
+ }
982
1377
  // ── Lifecycle ────────────────────────────────────
983
1378
  close() {
984
1379
  this.db.close();
@@ -988,4 +1383,35 @@ export class MemoryStore {
988
1383
  function camelToSnake(str) {
989
1384
  return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
990
1385
  }
1386
+ function parseStringArray(value) {
1387
+ try {
1388
+ const parsed = JSON.parse(value);
1389
+ return Array.isArray(parsed)
1390
+ ? parsed.filter((entry) => typeof entry === 'string' && entry.length > 0)
1391
+ : [];
1392
+ }
1393
+ catch {
1394
+ return [];
1395
+ }
1396
+ }
1397
+ function parseObject(value) {
1398
+ try {
1399
+ const parsed = JSON.parse(value);
1400
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
1401
+ ? parsed
1402
+ : {};
1403
+ }
1404
+ catch {
1405
+ return {};
1406
+ }
1407
+ }
1408
+ function eventRichness(event) {
1409
+ return [
1410
+ event.command,
1411
+ event.stderr,
1412
+ event.user_message,
1413
+ event.kind,
1414
+ event.payload,
1415
+ ].filter(value => value !== null && value !== '').length;
1416
+ }
991
1417
  //# sourceMappingURL=memory-store.js.map