@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.
- package/dist/embedding-provider.d.ts +2 -0
- package/dist/embedding-provider.d.ts.map +1 -1
- package/dist/embedding-provider.js +15 -0
- package/dist/embedding-provider.js.map +1 -1
- package/dist/event-grouper.d.ts +12 -1
- package/dist/event-grouper.d.ts.map +1 -1
- package/dist/event-grouper.js +56 -32
- package/dist/event-grouper.js.map +1 -1
- package/dist/hybrid-search.d.ts +9 -0
- package/dist/hybrid-search.d.ts.map +1 -1
- package/dist/hybrid-search.js +97 -19
- package/dist/hybrid-search.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lease-store.d.ts +8 -0
- package/dist/lease-store.d.ts.map +1 -1
- package/dist/lease-store.js +20 -0
- package/dist/lease-store.js.map +1 -1
- package/dist/memory-engine.d.ts.map +1 -1
- package/dist/memory-engine.js +81 -10
- package/dist/memory-engine.js.map +1 -1
- package/dist/memory-gate.d.ts +1 -1
- package/dist/memory-gate.d.ts.map +1 -1
- package/dist/memory-gate.js +56 -18
- package/dist/memory-gate.js.map +1 -1
- package/dist/memory-maintenance.d.ts +125 -0
- package/dist/memory-maintenance.d.ts.map +1 -0
- package/dist/memory-maintenance.js +606 -0
- package/dist/memory-maintenance.js.map +1 -0
- package/dist/memory-store.d.ts +81 -0
- package/dist/memory-store.d.ts.map +1 -1
- package/dist/memory-store.js +342 -27
- package/dist/memory-store.js.map +1 -1
- package/dist/migrations/v3-to-v4.d.ts +3 -0
- package/dist/migrations/v3-to-v4.d.ts.map +1 -0
- package/dist/migrations/v3-to-v4.js +93 -0
- package/dist/migrations/v3-to-v4.js.map +1 -0
- package/dist/observation-quality.d.ts +39 -0
- package/dist/observation-quality.d.ts.map +1 -0
- package/dist/observation-quality.js +190 -0
- package/dist/observation-quality.js.map +1 -0
- package/dist/session-summarizer.d.ts +1 -1
- package/dist/session-summarizer.d.ts.map +1 -1
- package/dist/session-summarizer.js +3 -0
- package/dist/session-summarizer.js.map +1 -1
- package/package.json +4 -4
package/dist/memory-store.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
714
|
-
|
|
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.
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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),
|
|
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
|
|
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
|
|
830
|
-
SUM(CASE WHEN embedding IS NULL THEN 1 ELSE 0 END) AS
|
|
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
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1152
|
+
this.recordRetrievalAccess({
|
|
1153
|
+
sourceType: 'memory',
|
|
1154
|
+
sourceId: memoryId,
|
|
1155
|
+
accessType,
|
|
1156
|
+
sessionId,
|
|
1157
|
+
});
|
|
924
1158
|
}
|
|
925
1159
|
incrementAccess(id) {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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
|