@devflow-tools/memory-engine 0.16.2 → 0.16.4

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 (48) 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/event-grouper.d.ts +12 -1
  6. package/dist/event-grouper.d.ts.map +1 -1
  7. package/dist/event-grouper.js +56 -32
  8. package/dist/event-grouper.js.map +1 -1
  9. package/dist/hybrid-search.d.ts +9 -0
  10. package/dist/hybrid-search.d.ts.map +1 -1
  11. package/dist/hybrid-search.js +97 -19
  12. package/dist/hybrid-search.js.map +1 -1
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/lease-store.d.ts +8 -0
  18. package/dist/lease-store.d.ts.map +1 -1
  19. package/dist/lease-store.js +20 -0
  20. package/dist/lease-store.js.map +1 -1
  21. package/dist/memory-engine.d.ts.map +1 -1
  22. package/dist/memory-engine.js +81 -10
  23. package/dist/memory-engine.js.map +1 -1
  24. package/dist/memory-gate.d.ts +1 -1
  25. package/dist/memory-gate.d.ts.map +1 -1
  26. package/dist/memory-gate.js +56 -18
  27. package/dist/memory-gate.js.map +1 -1
  28. package/dist/memory-maintenance.d.ts +125 -0
  29. package/dist/memory-maintenance.d.ts.map +1 -0
  30. package/dist/memory-maintenance.js +606 -0
  31. package/dist/memory-maintenance.js.map +1 -0
  32. package/dist/memory-store.d.ts +81 -0
  33. package/dist/memory-store.d.ts.map +1 -1
  34. package/dist/memory-store.js +342 -27
  35. package/dist/memory-store.js.map +1 -1
  36. package/dist/migrations/v3-to-v4.d.ts +3 -0
  37. package/dist/migrations/v3-to-v4.d.ts.map +1 -0
  38. package/dist/migrations/v3-to-v4.js +93 -0
  39. package/dist/migrations/v3-to-v4.js.map +1 -0
  40. package/dist/observation-quality.d.ts +39 -0
  41. package/dist/observation-quality.d.ts.map +1 -0
  42. package/dist/observation-quality.js +190 -0
  43. package/dist/observation-quality.js.map +1 -0
  44. package/dist/session-summarizer.d.ts +1 -1
  45. package/dist/session-summarizer.d.ts.map +1 -1
  46. package/dist/session-summarizer.js +3 -0
  47. package/dist/session-summarizer.js.map +1 -1
  48. 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 = `
@@ -275,6 +276,7 @@ export class MemoryStore {
275
276
  this.db.exec(FTS_TRIGGERS);
276
277
  this.migrateV1ToV2();
277
278
  applyV2ToV3Migration(this.db);
279
+ applyV3ToV4Migration(this.db);
278
280
  this.repairOrphanSessions(rootPath);
279
281
  console.log('[MemoryStore] Schema initialized', {
280
282
  dbPath: this.dbPath,
@@ -485,6 +487,12 @@ export class MemoryStore {
485
487
  }
486
488
  })();
487
489
  }
490
+ commitObservationEmbeddingBatch(entries, checkpoint) {
491
+ this.db.transaction(() => {
492
+ this.insertObservationEmbeddings(entries);
493
+ this.setMaintenanceCheckpoint(checkpoint.action, checkpoint.cursor, checkpoint.details ?? {});
494
+ })();
495
+ }
488
496
  getEmbedding(id) {
489
497
  const row = this.db.prepare('SELECT embedding FROM memories_vec WHERE id = ?').get(id);
490
498
  return row?.embedding ?? null;
@@ -499,6 +507,8 @@ export class MemoryStore {
499
507
  SELECT o.*, vec_distance_cosine(o.embedding, ?) AS distance
500
508
  FROM observations o
501
509
  WHERE o.embedding IS NOT NULL
510
+ AND o.quality_status = 'accepted'
511
+ AND o.resolution_state <> 'superseded'
502
512
  ORDER BY distance LIMIT ?
503
513
  `).all(buffer, limit);
504
514
  return rows.map(row => ({ observation: row, distance: row.distance }));
@@ -548,7 +558,9 @@ export class MemoryStore {
548
558
  return this.db.prepare('SELECT * FROM session_events WHERE session_id = ? ORDER BY created_at ASC').all(sessionId);
549
559
  }
550
560
  getUnprocessedEvents() {
551
- 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();
552
564
  const pendingIds = this.leaseStore?.getPendingEventIds();
553
565
  if (!pendingIds || pendingIds.size === 0)
554
566
  return events;
@@ -562,7 +574,8 @@ export class MemoryStore {
562
574
  if (unprocessed.length === 0)
563
575
  return;
564
576
  const coveredEventIds = new Set();
565
- const observationRows = this.db.prepare('SELECT source_event_ids FROM observations').all();
577
+ const observationRows = this.db.prepare(`SELECT source_event_ids FROM observations
578
+ WHERE quality_status = 'accepted' AND resolution_state <> 'superseded'`).all();
566
579
  for (const row of observationRows) {
567
580
  try {
568
581
  const sourceEventIds = JSON.parse(row.source_event_ids);
@@ -585,6 +598,143 @@ export class MemoryStore {
585
598
  const pendingPlaceholders = pendingIds.map(() => '?').join(',');
586
599
  this.db.prepare(`UPDATE session_events SET processed = 1 WHERE id IN (${pendingPlaceholders})`).run(...pendingIds);
587
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))();
737
+ }
588
738
  eventWindowExists(tool, command, windowMs = 5 * 60 * 1000) {
589
739
  const since = Date.now() - windowMs;
590
740
  if (command) {
@@ -709,9 +859,11 @@ export class MemoryStore {
709
859
  this.db.prepare(`
710
860
  INSERT INTO observations
711
861
  (id, session_id, project, type, title, narrative, facts, files, tools, concepts,
712
- importance, confidence, source, dedup_key, source_event_ids, timestamp, updated_at)
713
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
714
- `).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);
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');
715
867
  // Insert concept junction rows
716
868
  if (obs.concepts && obs.concepts.length > 0) {
717
869
  const insertConcept = this.db.prepare('INSERT OR IGNORE INTO observation_concepts (observation_id, concept) VALUES (?, ?)');
@@ -724,6 +876,13 @@ export class MemoryStore {
724
876
  });
725
877
  return this.getObservation(id);
726
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
+ }
727
886
  getObservation(id) {
728
887
  return this.db.prepare('SELECT * FROM observations WHERE id = ?').get(id) ?? null;
729
888
  }
@@ -754,26 +913,30 @@ export class MemoryStore {
754
913
  SELECT o.*
755
914
  FROM observations o
756
915
  WHERE o.type = ? AND o.timestamp >= ? AND o.embedding IS NOT NULL
916
+ AND o.quality_status = 'accepted' AND o.resolution_state <> 'superseded'
757
917
  AND (1.0 - vec_distance_cosine(o.embedding, ?)) > ?
758
918
  ORDER BY vec_distance_cosine(o.embedding, ?) ASC
759
919
  LIMIT 1
760
920
  `).get(type, cutoff, buffer, threshold, buffer) ?? null;
761
921
  }
762
922
  incrementObservationAccess(id, now = Date.now()) {
763
- this.db.prepare(`
764
- UPDATE observations
765
- SET access_count = access_count + 1, updated_at = ?
766
- WHERE id = ?
767
- `).run(now, id);
923
+ this.recordRetrievalAccess({
924
+ sourceType: 'observation',
925
+ sourceId: id,
926
+ accessType: 'operational',
927
+ purpose: 'dedup_or_reinforcement',
928
+ timestamp: now,
929
+ });
768
930
  }
769
931
  promoteObservationToExplicitIntent(id, now = Date.now()) {
770
932
  this.db.prepare(`
771
933
  UPDATE observations
772
934
  SET source = 'explicit_intent', confidence = MAX(confidence, 0.95),
773
- importance = MAX(importance, 10), access_count = access_count + 1,
935
+ importance = MAX(importance, 10),
774
936
  updated_at = ?
775
937
  WHERE id = ?
776
938
  `).run(now, id);
939
+ this.incrementObservationAccess(id, now);
777
940
  return this.getObservation(id);
778
941
  }
779
942
  listObservations(filter) {
@@ -791,6 +954,14 @@ export class MemoryStore {
791
954
  conditions.push('o.type = ?');
792
955
  vals.push(filter.type);
793
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
+ }
794
965
  if (filter?.memoryId !== undefined) {
795
966
  if (filter.memoryId === null) {
796
967
  conditions.push('o.memory_id IS NULL');
@@ -819,23 +990,44 @@ export class MemoryStore {
819
990
  listObservationsMissingEmbedding(limit = 32, afterId) {
820
991
  return this.db.prepare(`
821
992
  SELECT * FROM observations
822
- 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 > ?)
823
995
  ORDER BY id LIMIT ?
824
996
  `).all(afterId ?? null, afterId ?? null, limit);
825
997
  }
826
998
  countObservationVectors() {
827
999
  const row = this.db.prepare(`
828
1000
  SELECT COUNT(*) AS observations,
829
- SUM(CASE WHEN embedding IS NOT NULL THEN 1 ELSE 0 END) AS vectors,
830
- 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
831
1005
  FROM observations
832
1006
  `).get();
833
1007
  return {
834
1008
  observations: row.observations,
1009
+ eligible: row.eligible ?? 0,
835
1010
  vectors: row.vectors ?? 0,
836
1011
  missing: row.missing ?? 0,
1012
+ quarantined: row.quarantined ?? 0,
837
1013
  };
838
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
+ }
839
1031
  getObservationsByConcept(concept, limit = 50) {
840
1032
  return this.listObservations({ concept, limit });
841
1033
  }
@@ -859,6 +1051,7 @@ export class MemoryStore {
859
1051
  return this.db.prepare(`
860
1052
  SELECT * FROM observations
861
1053
  WHERE project = ? AND memory_id IS NULL AND importance >= ?
1054
+ AND quality_status = 'accepted' AND resolution_state <> 'superseded'
862
1055
  ORDER BY timestamp DESC
863
1056
  `).all(project, minImportance);
864
1057
  }
@@ -873,6 +1066,7 @@ export class MemoryStore {
873
1066
  FROM observation_concepts oc
874
1067
  JOIN observations o ON o.id = oc.observation_id
875
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'
876
1070
  GROUP BY oc.concept
877
1071
  HAVING observation_count >= ? AND session_count >= ?
878
1072
  ORDER BY observation_count DESC
@@ -915,21 +1109,65 @@ export class MemoryStore {
915
1109
  return this.db.prepare('SELECT * FROM evidence WHERE memory_id = ? ORDER BY timestamp DESC').all(memoryId);
916
1110
  }
917
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
+ }
918
1151
  recordAccess(memoryId, accessType, sessionId) {
919
- const id = `acc:${Date.now()}:${randomUUID().slice(0, 8)}`;
920
- this.db.prepare(`
921
- INSERT INTO access_log (id, memory_id, access_type, session_id, timestamp)
922
- VALUES (?, ?, ?, ?, ?)
923
- `).run(id, memoryId, accessType, sessionId ?? null, Date.now());
1152
+ this.recordRetrievalAccess({
1153
+ sourceType: 'memory',
1154
+ sourceId: memoryId,
1155
+ accessType,
1156
+ sessionId,
1157
+ });
924
1158
  }
925
1159
  incrementAccess(id) {
926
- const now = Date.now();
927
- this.db.prepare(`
928
- UPDATE memories SET access_count = access_count + 1,
929
- last_reinforced_at = COALESCE(last_reinforced_at, ?),
930
- last_accessed_at = ?, updated_at = ?
931
- WHERE id = ?
932
- `).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);
933
1171
  }
934
1172
  // ── Memory History ───────────────────────────────
935
1173
  recordHistory(entry) {
@@ -1090,6 +1328,52 @@ export class MemoryStore {
1090
1328
  const result = this.db.prepare("DELETE FROM memories WHERE status = 'pending' AND source = 'rule_engine' AND confidence < 0.6").run();
1091
1329
  return { removed: result.changes };
1092
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
+ }
1093
1377
  // ── Lifecycle ────────────────────────────────────
1094
1378
  close() {
1095
1379
  this.db.close();
@@ -1099,4 +1383,35 @@ export class MemoryStore {
1099
1383
  function camelToSnake(str) {
1100
1384
  return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
1101
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
+ }
1102
1417
  //# sourceMappingURL=memory-store.js.map