@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.
- 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/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 +3 -1
- package/dist/memory-engine.d.ts.map +1 -1
- package/dist/memory-engine.js +96 -14
- package/dist/memory-engine.js.map +1 -1
- package/dist/memory-gate.d.ts +3 -2
- package/dist/memory-gate.d.ts.map +1 -1
- package/dist/memory-gate.js +89 -44
- package/dist/memory-gate.js.map +1 -1
- package/dist/memory-maintenance.d.ts +123 -0
- package/dist/memory-maintenance.d.ts.map +1 -0
- package/dist/memory-maintenance.js +594 -0
- package/dist/memory-maintenance.js.map +1 -0
- package/dist/memory-store.d.ts +85 -0
- package/dist/memory-store.d.ts.map +1 -1
- package/dist/memory-store.js +456 -30
- 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 = `
|
|
@@ -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 ??
|
|
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(
|
|
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(`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
622
|
-
|
|
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.
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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),
|
|
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
|
|
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
|
|
720
|
-
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
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
1152
|
+
this.recordRetrievalAccess({
|
|
1153
|
+
sourceType: 'memory',
|
|
1154
|
+
sourceId: memoryId,
|
|
1155
|
+
accessType,
|
|
1156
|
+
sessionId,
|
|
1157
|
+
});
|
|
813
1158
|
}
|
|
814
1159
|
incrementAccess(id) {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|