@devflow-tools/memory-engine 0.13.3 → 0.14.0
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/consolidator.d.ts +13 -0
- package/dist/consolidator.d.ts.map +1 -0
- package/dist/consolidator.js +184 -0
- package/dist/consolidator.js.map +1 -0
- package/dist/event-grouper.d.ts +13 -0
- package/dist/event-grouper.d.ts.map +1 -0
- package/dist/event-grouper.js +178 -0
- package/dist/event-grouper.js.map +1 -0
- package/dist/graph-store.d.ts.map +1 -1
- package/dist/graph-store.js +2 -4
- package/dist/graph-store.js.map +1 -1
- package/dist/hybrid-search.d.ts +6 -4
- package/dist/hybrid-search.d.ts.map +1 -1
- package/dist/hybrid-search.js +23 -5
- package/dist/hybrid-search.js.map +1 -1
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/l2-evaluator.d.ts +31 -0
- package/dist/l2-evaluator.d.ts.map +1 -0
- package/dist/l2-evaluator.js +103 -0
- package/dist/l2-evaluator.js.map +1 -0
- package/dist/llm-provider.d.ts +9 -0
- package/dist/llm-provider.d.ts.map +1 -0
- package/dist/llm-provider.js +37 -0
- package/dist/llm-provider.js.map +1 -0
- package/dist/memory-engine.d.ts +27 -4
- package/dist/memory-engine.d.ts.map +1 -1
- package/dist/memory-engine.js +274 -181
- package/dist/memory-engine.js.map +1 -1
- package/dist/memory-gate.d.ts +116 -0
- package/dist/memory-gate.d.ts.map +1 -0
- package/dist/memory-gate.js +385 -0
- package/dist/memory-gate.js.map +1 -0
- package/dist/memory-store.d.ts +234 -32
- package/dist/memory-store.d.ts.map +1 -1
- package/dist/memory-store.js +729 -142
- package/dist/memory-store.js.map +1 -1
- package/dist/project-validator.d.ts +10 -0
- package/dist/project-validator.d.ts.map +1 -0
- package/dist/project-validator.js +36 -0
- package/dist/project-validator.js.map +1 -0
- package/dist/session-summarizer.d.ts +10 -0
- package/dist/session-summarizer.d.ts.map +1 -0
- package/dist/session-summarizer.js +197 -0
- package/dist/session-summarizer.js.map +1 -0
- package/dist/signal-buffer.d.ts +42 -0
- package/dist/signal-buffer.d.ts.map +1 -0
- package/dist/signal-buffer.js +104 -0
- package/dist/signal-buffer.js.map +1 -0
- package/dist/signal-detector.d.ts +39 -0
- package/dist/signal-detector.d.ts.map +1 -0
- package/dist/signal-detector.js +239 -0
- package/dist/signal-detector.js.map +1 -0
- package/package.json +5 -5
package/dist/memory-store.js
CHANGED
|
@@ -3,21 +3,43 @@ import * as sqliteVec from 'sqlite-vec';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
|
-
|
|
6
|
+
// ── Schema ─────────────────────────────────────────────
|
|
7
|
+
const SCHEMA_V2 = `
|
|
8
|
+
-- ============================================================
|
|
9
|
+
-- Core: memories (expanded from v1)
|
|
10
|
+
-- ============================================================
|
|
7
11
|
CREATE TABLE IF NOT EXISTS memories (
|
|
8
|
-
id
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
type TEXT NOT NULL DEFAULT 'fact'
|
|
14
|
+
CHECK(type IN ('constraint','pattern','preference','lesson','fact',
|
|
15
|
+
'semantic','episodic','procedural','failure_lesson',
|
|
16
|
+
'mcp_correction','project_knowledge')),
|
|
17
|
+
title TEXT NOT NULL DEFAULT '',
|
|
18
|
+
content TEXT NOT NULL,
|
|
19
|
+
context TEXT NOT NULL DEFAULT '',
|
|
20
|
+
original_text TEXT,
|
|
21
|
+
source TEXT NOT NULL DEFAULT 'auto',
|
|
22
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
23
|
+
CHECK(status IN ('active','superseded','expired','pending','approved','rejected')),
|
|
24
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
25
|
+
supersedes TEXT,
|
|
26
|
+
superseded_by TEXT,
|
|
27
|
+
strength REAL NOT NULL DEFAULT 0.5,
|
|
28
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
29
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
30
|
+
scope TEXT NOT NULL DEFAULT '',
|
|
31
|
+
session_id TEXT,
|
|
32
|
+
created_at INTEGER NOT NULL,
|
|
33
|
+
updated_at INTEGER NOT NULL,
|
|
34
|
+
last_reinforced_at INTEGER,
|
|
35
|
+
expires_at INTEGER,
|
|
36
|
+
access_count INTEGER NOT NULL DEFAULT 0,
|
|
37
|
+
reinforce_count INTEGER NOT NULL DEFAULT 0
|
|
19
38
|
);
|
|
20
39
|
|
|
40
|
+
-- ============================================================
|
|
41
|
+
-- FTS5 keyword index
|
|
42
|
+
-- ============================================================
|
|
21
43
|
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
22
44
|
id UNINDEXED,
|
|
23
45
|
content,
|
|
@@ -26,110 +48,358 @@ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
|
26
48
|
tokenize='porter unicode61'
|
|
27
49
|
);
|
|
28
50
|
|
|
51
|
+
-- ============================================================
|
|
52
|
+
-- Vector search (sqlite-vec)
|
|
53
|
+
-- ============================================================
|
|
29
54
|
CREATE VIRTUAL TABLE IF NOT EXISTS memories_vec USING vec0(
|
|
30
55
|
id TEXT PRIMARY KEY,
|
|
31
56
|
embedding FLOAT[384]
|
|
32
57
|
);
|
|
33
58
|
|
|
59
|
+
-- ============================================================
|
|
60
|
+
-- Memory relationship edges
|
|
61
|
+
-- ============================================================
|
|
34
62
|
CREATE TABLE IF NOT EXISTS memory_edges (
|
|
35
63
|
source_id TEXT NOT NULL,
|
|
36
64
|
target_id TEXT NOT NULL,
|
|
37
|
-
|
|
38
|
-
weight
|
|
39
|
-
PRIMARY KEY (source_id, target_id,
|
|
65
|
+
type TEXT NOT NULL CHECK(type IN ('contradicts','related_to','supersedes','references')),
|
|
66
|
+
weight REAL NOT NULL DEFAULT 0.5,
|
|
67
|
+
PRIMARY KEY (source_id, target_id, type),
|
|
40
68
|
FOREIGN KEY (source_id) REFERENCES memories(id) ON DELETE CASCADE,
|
|
41
69
|
FOREIGN KEY (target_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
42
70
|
);
|
|
43
71
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
72
|
+
-- ============================================================
|
|
73
|
+
-- Evidence chain (one memory → many evidence records)
|
|
74
|
+
-- ============================================================
|
|
75
|
+
CREATE TABLE IF NOT EXISTS evidence (
|
|
76
|
+
id TEXT PRIMARY KEY,
|
|
77
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
78
|
+
type TEXT NOT NULL CHECK(type IN ('error_pattern','user_correction','explicit_save','recurrence','consolidation')),
|
|
79
|
+
weight REAL NOT NULL DEFAULT 0.5,
|
|
80
|
+
source_observation_ids TEXT NOT NULL DEFAULT '[]',
|
|
81
|
+
summary TEXT NOT NULL DEFAULT '',
|
|
82
|
+
timestamp INTEGER NOT NULL
|
|
83
|
+
);
|
|
84
|
+
CREATE INDEX IF NOT EXISTS idx_evidence_memory ON evidence(memory_id);
|
|
85
|
+
|
|
86
|
+
-- ============================================================
|
|
87
|
+
-- Memory change audit log
|
|
88
|
+
-- ============================================================
|
|
89
|
+
CREATE TABLE IF NOT EXISTS memory_history (
|
|
90
|
+
id TEXT PRIMARY KEY,
|
|
91
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
92
|
+
event TEXT NOT NULL CHECK(event IN ('ADD','UPDATE','DELETE','REINFORCE','SUPERSEDE','EXPIRE','EVICT')),
|
|
93
|
+
old_content TEXT,
|
|
94
|
+
new_content TEXT,
|
|
95
|
+
old_strength REAL,
|
|
96
|
+
new_strength REAL,
|
|
97
|
+
reason TEXT NOT NULL DEFAULT '',
|
|
98
|
+
created_at INTEGER NOT NULL
|
|
99
|
+
);
|
|
100
|
+
CREATE INDEX IF NOT EXISTS idx_memory_history_memory ON memory_history(memory_id);
|
|
101
|
+
|
|
102
|
+
-- ============================================================
|
|
103
|
+
-- Sessions
|
|
104
|
+
-- ============================================================
|
|
105
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
106
|
+
id TEXT PRIMARY KEY,
|
|
107
|
+
project_root TEXT NOT NULL,
|
|
108
|
+
task TEXT NOT NULL DEFAULT '',
|
|
109
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','closed')),
|
|
110
|
+
phase_summary TEXT NOT NULL DEFAULT '[]',
|
|
111
|
+
signal_summary TEXT NOT NULL DEFAULT '[]',
|
|
112
|
+
tool_count INTEGER NOT NULL DEFAULT 0,
|
|
113
|
+
error_count INTEGER NOT NULL DEFAULT 0,
|
|
114
|
+
created_at INTEGER NOT NULL,
|
|
115
|
+
closed_at INTEGER
|
|
116
|
+
);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_root);
|
|
119
|
+
|
|
120
|
+
-- ============================================================
|
|
121
|
+
-- Session events (raw tool calls)
|
|
122
|
+
-- ============================================================
|
|
123
|
+
CREATE TABLE IF NOT EXISTS session_events (
|
|
124
|
+
id TEXT PRIMARY KEY,
|
|
125
|
+
session_id TEXT NOT NULL,
|
|
126
|
+
project TEXT NOT NULL DEFAULT '',
|
|
127
|
+
tool TEXT NOT NULL,
|
|
128
|
+
command TEXT,
|
|
129
|
+
exit_code INTEGER,
|
|
130
|
+
stderr TEXT,
|
|
131
|
+
duration_ms INTEGER,
|
|
132
|
+
created_at INTEGER NOT NULL,
|
|
133
|
+
processed INTEGER NOT NULL DEFAULT 0,
|
|
134
|
+
l1_score REAL NOT NULL DEFAULT 0,
|
|
135
|
+
dialog_score REAL NOT NULL DEFAULT 0,
|
|
136
|
+
user_message TEXT
|
|
137
|
+
);
|
|
138
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_session ON session_events(session_id);
|
|
139
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_project ON session_events(project);
|
|
140
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_processed ON session_events(processed);
|
|
141
|
+
|
|
142
|
+
-- ============================================================
|
|
143
|
+
-- Observations (Summarizer output — intermediate layer)
|
|
144
|
+
-- ============================================================
|
|
145
|
+
CREATE TABLE IF NOT EXISTS observations (
|
|
146
|
+
id TEXT PRIMARY KEY,
|
|
147
|
+
session_id TEXT NOT NULL,
|
|
148
|
+
project TEXT NOT NULL DEFAULT '',
|
|
149
|
+
type TEXT NOT NULL CHECK(type IN (
|
|
150
|
+
'error_resolution','error_unresolved','config_discovery',
|
|
151
|
+
'tool_first_use','decision_made','user_insight',
|
|
152
|
+
'file_operation','session_summary'
|
|
153
|
+
)),
|
|
154
|
+
title TEXT NOT NULL,
|
|
155
|
+
narrative TEXT NOT NULL DEFAULT '',
|
|
156
|
+
facts TEXT NOT NULL DEFAULT '[]',
|
|
157
|
+
files TEXT NOT NULL DEFAULT '[]',
|
|
158
|
+
tools TEXT NOT NULL DEFAULT '[]',
|
|
159
|
+
concepts TEXT NOT NULL DEFAULT '[]',
|
|
160
|
+
importance INTEGER NOT NULL DEFAULT 5,
|
|
161
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
162
|
+
source_event_ids TEXT NOT NULL DEFAULT '[]',
|
|
163
|
+
memory_id TEXT,
|
|
164
|
+
embedding BLOB,
|
|
165
|
+
timestamp INTEGER NOT NULL
|
|
166
|
+
);
|
|
167
|
+
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
168
|
+
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project);
|
|
169
|
+
CREATE INDEX IF NOT EXISTS idx_observations_memory ON observations(memory_id);
|
|
170
|
+
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
|
171
|
+
|
|
172
|
+
-- ============================================================
|
|
173
|
+
-- Observation → Concept junction (inverted index for clustering)
|
|
174
|
+
-- ============================================================
|
|
175
|
+
CREATE TABLE IF NOT EXISTS observation_concepts (
|
|
176
|
+
observation_id TEXT NOT NULL REFERENCES observations(id) ON DELETE CASCADE,
|
|
177
|
+
concept TEXT NOT NULL COLLATE NOCASE,
|
|
178
|
+
PRIMARY KEY (observation_id, concept)
|
|
179
|
+
);
|
|
180
|
+
CREATE INDEX IF NOT EXISTS idx_obs_concepts_concept ON observation_concepts(concept);
|
|
181
|
+
|
|
182
|
+
-- ============================================================
|
|
183
|
+
-- Unified raw embedding storage (memory + observation)
|
|
184
|
+
-- ============================================================
|
|
185
|
+
CREATE TABLE IF NOT EXISTS embeddings (
|
|
186
|
+
source_type TEXT NOT NULL CHECK(source_type IN ('memory','observation')),
|
|
187
|
+
source_id TEXT NOT NULL,
|
|
188
|
+
vector BLOB NOT NULL,
|
|
189
|
+
PRIMARY KEY (source_type, source_id)
|
|
190
|
+
);
|
|
191
|
+
CREATE INDEX IF NOT EXISTS idx_embeddings_source ON embeddings(source_type, source_id);
|
|
192
|
+
|
|
193
|
+
-- ============================================================
|
|
194
|
+
-- Access log (drives reinforcement boost)
|
|
195
|
+
-- ============================================================
|
|
196
|
+
CREATE TABLE IF NOT EXISTS access_log (
|
|
197
|
+
id TEXT PRIMARY KEY,
|
|
198
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
199
|
+
access_type TEXT NOT NULL CHECK(access_type IN ('search_hit','user_view','agent_use','manual_ref')),
|
|
200
|
+
session_id TEXT,
|
|
201
|
+
timestamp INTEGER NOT NULL
|
|
202
|
+
);
|
|
203
|
+
CREATE INDEX IF NOT EXISTS idx_access_log_memory ON access_log(memory_id);
|
|
204
|
+
CREATE INDEX IF NOT EXISTS idx_access_log_timestamp ON access_log(timestamp);
|
|
205
|
+
|
|
206
|
+
-- ============================================================
|
|
207
|
+
-- Retention score cache
|
|
208
|
+
-- ============================================================
|
|
209
|
+
CREATE TABLE IF NOT EXISTS retention_scores (
|
|
210
|
+
memory_id TEXT PRIMARY KEY REFERENCES memories(id) ON DELETE CASCADE,
|
|
211
|
+
score REAL NOT NULL,
|
|
212
|
+
salience REAL NOT NULL,
|
|
213
|
+
temporal_decay REAL NOT NULL,
|
|
214
|
+
reinforcement_boost REAL NOT NULL,
|
|
215
|
+
last_accessed INTEGER,
|
|
216
|
+
access_count INTEGER NOT NULL DEFAULT 0,
|
|
217
|
+
computed_at INTEGER NOT NULL
|
|
218
|
+
);
|
|
219
|
+
CREATE INDEX IF NOT EXISTS idx_retention_score ON retention_scores(score);
|
|
220
|
+
|
|
221
|
+
-- ============================================================
|
|
222
|
+
-- Signals (Signal Detector output)
|
|
223
|
+
-- ============================================================
|
|
224
|
+
CREATE TABLE IF NOT EXISTS signals (
|
|
225
|
+
id TEXT PRIMARY KEY,
|
|
226
|
+
session_id TEXT NOT NULL,
|
|
227
|
+
type TEXT NOT NULL,
|
|
228
|
+
strength REAL NOT NULL DEFAULT 0.5,
|
|
229
|
+
source_event_ids TEXT NOT NULL DEFAULT '[]',
|
|
230
|
+
payload TEXT NOT NULL DEFAULT '{}',
|
|
231
|
+
timestamp INTEGER NOT NULL
|
|
54
232
|
);
|
|
55
|
-
CREATE INDEX IF NOT EXISTS
|
|
56
|
-
CREATE INDEX IF NOT EXISTS
|
|
233
|
+
CREATE INDEX IF NOT EXISTS idx_signals_session ON signals(session_id);
|
|
234
|
+
CREATE INDEX IF NOT EXISTS idx_signals_type ON signals(type);
|
|
57
235
|
`;
|
|
58
236
|
const FTS_TRIGGERS = `
|
|
59
237
|
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
60
|
-
INSERT INTO memories_fts(rowid, id, content, scope)
|
|
238
|
+
INSERT INTO memories_fts(rowid, id, content, scope)
|
|
239
|
+
VALUES (new.rowid, new.id, new.content, new.scope);
|
|
61
240
|
END;
|
|
62
241
|
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
63
|
-
INSERT INTO memories_fts(memories_fts, id, content, scope)
|
|
242
|
+
INSERT INTO memories_fts(memories_fts, id, content, scope)
|
|
243
|
+
VALUES ('delete', old.id, old.content, old.scope);
|
|
64
244
|
END;
|
|
65
245
|
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
66
|
-
INSERT INTO memories_fts(memories_fts, id, content, scope)
|
|
67
|
-
|
|
246
|
+
INSERT INTO memories_fts(memories_fts, id, content, scope)
|
|
247
|
+
VALUES ('delete', old.id, old.content, old.scope);
|
|
248
|
+
INSERT INTO memories_fts(rowid, id, content, scope)
|
|
249
|
+
VALUES (new.rowid, new.id, new.content, new.scope);
|
|
68
250
|
END;
|
|
69
251
|
`;
|
|
252
|
+
// ── MemoryStore ─────────────────────────────────────────
|
|
70
253
|
export class MemoryStore {
|
|
71
254
|
db;
|
|
255
|
+
dbPath;
|
|
72
256
|
constructor(rootPath) {
|
|
73
257
|
const memDir = join(rootPath, '.devflow', 'memory');
|
|
74
258
|
if (!existsSync(memDir))
|
|
75
259
|
mkdirSync(memDir, { recursive: true });
|
|
76
|
-
|
|
77
|
-
this.db = new Database(dbPath);
|
|
260
|
+
this.dbPath = join(memDir, 'memories.db');
|
|
261
|
+
this.db = new Database(this.dbPath);
|
|
78
262
|
this.db.pragma('journal_mode = WAL');
|
|
79
263
|
this.db.pragma('busy_timeout = 5000');
|
|
80
264
|
this.db.pragma('foreign_keys = ON');
|
|
81
265
|
sqliteVec.load(this.db);
|
|
82
|
-
this.db.exec(
|
|
266
|
+
this.db.exec(SCHEMA_V2);
|
|
83
267
|
this.db.exec(FTS_TRIGGERS);
|
|
84
|
-
this.
|
|
268
|
+
this.migrateV1ToV2();
|
|
269
|
+
console.log('[MemoryStore] Schema initialized', {
|
|
270
|
+
dbPath: this.dbPath,
|
|
271
|
+
tableCount: this.countTables(),
|
|
272
|
+
});
|
|
85
273
|
}
|
|
274
|
+
countTables() {
|
|
275
|
+
const row = this.db.prepare("SELECT COUNT(*) as c FROM sqlite_master WHERE type='table'").get();
|
|
276
|
+
return row.c;
|
|
277
|
+
}
|
|
278
|
+
// ── V1 → V2 Migration ────────────────────────────
|
|
279
|
+
migrateV1ToV2() {
|
|
280
|
+
const tables = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(r => r.name);
|
|
281
|
+
// If old 'events' table exists, migrate data to session_events
|
|
282
|
+
if (tables.includes('events') && tables.includes('session_events')) {
|
|
283
|
+
const count = this.db.prepare('SELECT COUNT(*) as c FROM events').get();
|
|
284
|
+
if (count.c > 0) {
|
|
285
|
+
try {
|
|
286
|
+
this.db.exec(`
|
|
287
|
+
INSERT OR IGNORE INTO session_events
|
|
288
|
+
(id, session_id, project, tool, command, exit_code, stderr,
|
|
289
|
+
duration_ms, created_at, processed, l1_score, dialog_score, user_message)
|
|
290
|
+
SELECT id, session_id, '', tool, command, exit_code, stderr,
|
|
291
|
+
duration_ms, created_at, processed,
|
|
292
|
+
COALESCE(l1_score, 0), COALESCE(dialog_score, 0), user_message
|
|
293
|
+
FROM events
|
|
294
|
+
`);
|
|
295
|
+
console.log('[MemoryStore] Migrated', count.c, 'events to session_events');
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
console.warn('[MemoryStore] Event migration skipped:', err.message);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Add new columns to memories table if missing
|
|
303
|
+
const memCols = this.getTableColumns('memories');
|
|
304
|
+
const newCols = [
|
|
305
|
+
{ name: 'title', sql: "ALTER TABLE memories ADD COLUMN title TEXT NOT NULL DEFAULT ''" },
|
|
306
|
+
{ name: 'type', sql: "ALTER TABLE memories ADD COLUMN type TEXT" },
|
|
307
|
+
{ name: 'version', sql: 'ALTER TABLE memories ADD COLUMN version INTEGER NOT NULL DEFAULT 1' },
|
|
308
|
+
{ name: 'supersedes', sql: 'ALTER TABLE memories ADD COLUMN supersedes TEXT' },
|
|
309
|
+
{ name: 'superseded_by', sql: 'ALTER TABLE memories ADD COLUMN superseded_by TEXT' },
|
|
310
|
+
{ name: 'tags', sql: "ALTER TABLE memories ADD COLUMN tags TEXT NOT NULL DEFAULT '[]'" },
|
|
311
|
+
{ name: 'context', sql: "ALTER TABLE memories ADD COLUMN context TEXT NOT NULL DEFAULT ''" },
|
|
312
|
+
{ name: 'strength', sql: 'ALTER TABLE memories ADD COLUMN strength REAL NOT NULL DEFAULT 0.5' },
|
|
313
|
+
{ name: 'last_reinforced_at', sql: 'ALTER TABLE memories ADD COLUMN last_reinforced_at INTEGER' },
|
|
314
|
+
{ name: 'reinforce_count', sql: 'ALTER TABLE memories ADD COLUMN reinforce_count INTEGER NOT NULL DEFAULT 0' },
|
|
315
|
+
{ name: 'updated_at', sql: 'ALTER TABLE memories ADD COLUMN updated_at INTEGER NOT NULL DEFAULT 0' },
|
|
316
|
+
];
|
|
317
|
+
for (const col of newCols) {
|
|
318
|
+
if (!memCols.includes(col.name)) {
|
|
319
|
+
try {
|
|
320
|
+
this.db.exec(col.sql);
|
|
321
|
+
}
|
|
322
|
+
catch { /* column may already exist */ }
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Backfill: copy category→type, set title from first sentence of content
|
|
326
|
+
if (!memCols.includes('type') && memCols.includes('category')) {
|
|
327
|
+
try {
|
|
328
|
+
this.db.exec("UPDATE memories SET type = category WHERE type IS NULL");
|
|
329
|
+
}
|
|
330
|
+
catch { /* ignore */ }
|
|
331
|
+
}
|
|
332
|
+
if (!memCols.includes('title')) {
|
|
333
|
+
try {
|
|
334
|
+
this.db.exec("UPDATE memories SET title = COALESCE(title, '') WHERE title IS NULL OR title = ''");
|
|
335
|
+
}
|
|
336
|
+
catch { /* ignore */ }
|
|
337
|
+
}
|
|
338
|
+
if (!memCols.includes('updated_at')) {
|
|
339
|
+
try {
|
|
340
|
+
this.db.exec('UPDATE memories SET updated_at = created_at WHERE updated_at = 0');
|
|
341
|
+
}
|
|
342
|
+
catch { /* ignore */ }
|
|
343
|
+
}
|
|
344
|
+
// Wire up old 'events' view for backward compat
|
|
345
|
+
if (tables.includes('events') && !tables.includes('events')) {
|
|
346
|
+
// already handled — old callers use session_events directly
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
getTableColumns(table) {
|
|
350
|
+
try {
|
|
351
|
+
const rows = this.db.pragma(`table_info(${table})`);
|
|
352
|
+
return rows.map(r => r.name);
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return [];
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// ── Memory CRUD ──────────────────────────────────
|
|
86
359
|
add(entry) {
|
|
87
360
|
const id = entry.id ?? `mem:${Date.now()}:${randomUUID().slice(0, 8)}`;
|
|
88
361
|
const now = Date.now();
|
|
89
|
-
const
|
|
362
|
+
const memType = entry.type ?? entry.category ?? 'fact';
|
|
363
|
+
const title = entry.title ?? entry.content.slice(0, 80);
|
|
90
364
|
this.db.prepare(`
|
|
91
|
-
INSERT INTO memories (id,
|
|
92
|
-
|
|
93
|
-
|
|
365
|
+
INSERT INTO memories (id, type, title, content, original_text, source, status,
|
|
366
|
+
confidence, strength, scope, session_id, created_at, updated_at)
|
|
367
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
368
|
+
`).run(id, memType, title, entry.content, entry.originalText ?? null, entry.source ?? 'auto', entry.status ?? 'active', entry.confidence ?? 0.5, entry.strength ?? 0.5, entry.scope ?? '', entry.sessionId ?? null, now, now);
|
|
369
|
+
console.log('[MemoryStore] Memory added', { id, type: memType, status: entry.status ?? 'active' });
|
|
94
370
|
return this.get(id);
|
|
95
371
|
}
|
|
96
372
|
get(id) {
|
|
97
|
-
|
|
98
|
-
return row ?? null;
|
|
373
|
+
return this.db.prepare('SELECT * FROM memories WHERE id = ?').get(id) ?? null;
|
|
99
374
|
}
|
|
100
375
|
update(id, updates) {
|
|
101
376
|
const sets = [];
|
|
102
377
|
const vals = [];
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
vals.push(updates.status);
|
|
110
|
-
}
|
|
111
|
-
if (updates.scope !== undefined) {
|
|
112
|
-
sets.push('scope = ?');
|
|
113
|
-
vals.push(updates.scope);
|
|
114
|
-
}
|
|
115
|
-
if (updates.status === 'approved') {
|
|
116
|
-
sets.push('approved_at = ?');
|
|
117
|
-
vals.push(Date.now());
|
|
378
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
379
|
+
if (value !== undefined) {
|
|
380
|
+
const col = camelToSnake(key);
|
|
381
|
+
sets.push(`${col} = ?`);
|
|
382
|
+
vals.push(value);
|
|
383
|
+
}
|
|
118
384
|
}
|
|
385
|
+
sets.push('updated_at = ?');
|
|
386
|
+
vals.push(Date.now());
|
|
119
387
|
if (sets.length === 0)
|
|
120
388
|
return;
|
|
121
389
|
vals.push(id);
|
|
122
390
|
this.db.prepare(`UPDATE memories SET ${sets.join(', ')} WHERE id = ?`).run(...vals);
|
|
391
|
+
console.log('[MemoryStore] Memory updated', { id, keys: Object.keys(updates) });
|
|
123
392
|
}
|
|
124
393
|
delete(id) {
|
|
125
394
|
this.db.prepare('DELETE FROM memories WHERE id = ?').run(id);
|
|
126
395
|
}
|
|
127
396
|
list(filter) {
|
|
128
|
-
let sql = 'SELECT * FROM memories WHERE
|
|
397
|
+
let sql = 'SELECT * FROM memories WHERE superseded_by IS NULL';
|
|
129
398
|
const vals = [];
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
399
|
+
const typeFilter = filter?.type ?? filter?.category;
|
|
400
|
+
if (typeFilter) {
|
|
401
|
+
sql += ' AND type = ?';
|
|
402
|
+
vals.push(typeFilter);
|
|
133
403
|
}
|
|
134
404
|
if (filter?.status) {
|
|
135
405
|
sql += ' AND status = ?';
|
|
@@ -139,145 +409,462 @@ export class MemoryStore {
|
|
|
139
409
|
sql += ' AND scope = ?';
|
|
140
410
|
vals.push(filter.scope);
|
|
141
411
|
}
|
|
142
|
-
sql += ' ORDER BY
|
|
412
|
+
sql += ' ORDER BY updated_at DESC';
|
|
143
413
|
return this.db.prepare(sql).all(...vals);
|
|
144
414
|
}
|
|
145
415
|
getPending() {
|
|
146
416
|
return this.list({ status: 'pending' });
|
|
147
417
|
}
|
|
418
|
+
listByType(type, limit = 100) {
|
|
419
|
+
return this.db.prepare("SELECT * FROM memories WHERE type = ? AND superseded_by IS NULL AND status IN ('active','approved') ORDER BY updated_at DESC LIMIT ?").all(type, limit);
|
|
420
|
+
}
|
|
421
|
+
listByTypes(types, limit = 100) {
|
|
422
|
+
if (types.length === 0)
|
|
423
|
+
return [];
|
|
424
|
+
const placeholders = types.map(() => '?').join(',');
|
|
425
|
+
return this.db.prepare(`SELECT * FROM memories WHERE type IN (${placeholders}) AND superseded_by IS NULL AND status IN ('active','approved') ORDER BY updated_at DESC LIMIT ?`).all(...types, limit);
|
|
426
|
+
}
|
|
148
427
|
listByCategory(category, limit = 100) {
|
|
149
|
-
return this.
|
|
428
|
+
return this.listByType(category, limit);
|
|
429
|
+
}
|
|
430
|
+
countByStatus(status) {
|
|
431
|
+
const row = this.db.prepare('SELECT COUNT(*) as c FROM memories WHERE status = ?').get(status);
|
|
432
|
+
return row?.c ?? 0;
|
|
150
433
|
}
|
|
434
|
+
// ── Embedding ────────────────────────────────────
|
|
151
435
|
insertEmbedding(id, embedding) {
|
|
152
436
|
const buffer = Buffer.from(embedding.buffer);
|
|
153
437
|
this.db.prepare('INSERT OR REPLACE INTO memories_vec(id, embedding) VALUES (?, ?)').run(id, buffer);
|
|
438
|
+
this.db.prepare('INSERT OR REPLACE INTO embeddings(source_type, source_id, vector) VALUES (?, ?, ?)').run('memory', id, buffer);
|
|
439
|
+
}
|
|
440
|
+
insertObservationEmbedding(obsId, embedding) {
|
|
441
|
+
const buffer = Buffer.from(embedding.buffer);
|
|
442
|
+
this.db.prepare('INSERT OR REPLACE INTO embeddings(source_type, source_id, vector) VALUES (?, ?, ?)').run('observation', obsId, buffer);
|
|
443
|
+
this.db.prepare('UPDATE observations SET embedding = ? WHERE id = ?').run(buffer, obsId);
|
|
444
|
+
}
|
|
445
|
+
getEmbedding(id) {
|
|
446
|
+
const row = this.db.prepare('SELECT embedding FROM memories_vec WHERE id = ?').get(id);
|
|
447
|
+
return row?.embedding ?? null;
|
|
154
448
|
}
|
|
449
|
+
getEmbeddingBySource(sourceType, sourceId) {
|
|
450
|
+
const row = this.db.prepare('SELECT vector FROM embeddings WHERE source_type = ? AND source_id = ?').get(sourceType, sourceId);
|
|
451
|
+
return row?.vector ?? null;
|
|
452
|
+
}
|
|
453
|
+
// ── Search ───────────────────────────────────────
|
|
155
454
|
searchBM25(query, limit = 20) {
|
|
156
|
-
|
|
455
|
+
return this.db.prepare(`
|
|
157
456
|
SELECT m.id, rank
|
|
158
457
|
FROM memories_fts f
|
|
159
458
|
JOIN memories m ON m.rowid = f.rowid
|
|
160
|
-
WHERE memories_fts MATCH ? AND m.status
|
|
459
|
+
WHERE memories_fts MATCH ? AND m.status IN ('active','approved') AND m.superseded_by IS NULL
|
|
161
460
|
ORDER BY rank LIMIT ?
|
|
162
461
|
`).all(query, limit);
|
|
163
|
-
return rows;
|
|
164
462
|
}
|
|
165
463
|
searchVector(embedding, limit = 20) {
|
|
166
464
|
const buffer = Buffer.from(embedding.buffer);
|
|
167
|
-
|
|
465
|
+
return this.db.prepare(`
|
|
168
466
|
SELECT v.id, vec_distance_cosine(v.embedding, ?) as distance
|
|
169
467
|
FROM memories_vec v
|
|
170
468
|
JOIN memories m ON m.id = v.id
|
|
171
|
-
WHERE m.status
|
|
469
|
+
WHERE m.status IN ('active','approved') AND m.superseded_by IS NULL
|
|
172
470
|
ORDER BY distance LIMIT ?
|
|
173
471
|
`).all(buffer, limit);
|
|
174
|
-
return rows;
|
|
175
472
|
}
|
|
176
|
-
|
|
473
|
+
// ── Edges ────────────────────────────────────────
|
|
474
|
+
addEdge(sourceId, targetId, type, weight = 0.5) {
|
|
177
475
|
this.db.prepare(`
|
|
178
|
-
INSERT OR REPLACE INTO memory_edges (source_id, target_id,
|
|
179
|
-
|
|
476
|
+
INSERT OR REPLACE INTO memory_edges (source_id, target_id, type, weight)
|
|
477
|
+
VALUES (?, ?, ?, ?)
|
|
478
|
+
`).run(sourceId, targetId, type, weight);
|
|
180
479
|
}
|
|
181
480
|
getNeighbors(id) {
|
|
182
|
-
return this.db.prepare('SELECT target_id,
|
|
183
|
-
.map(r => ({ targetId: r.target_id, relation: r.relation, weight: r.weight }));
|
|
184
|
-
}
|
|
185
|
-
getEmbedding(id) {
|
|
186
|
-
const row = this.db.prepare('SELECT embedding FROM memories_vec WHERE id = ?').get(id);
|
|
187
|
-
return row?.embedding ?? null;
|
|
481
|
+
return this.db.prepare('SELECT target_id, type, weight FROM memory_edges WHERE source_id = ?').all(id).map(r => ({ targetId: r.target_id, type: r.type, weight: r.weight }));
|
|
188
482
|
}
|
|
189
|
-
|
|
190
|
-
this.db.close();
|
|
191
|
-
}
|
|
192
|
-
// --- Events CRUD ---
|
|
483
|
+
// ── Events CRUD ──────────────────────────────────
|
|
193
484
|
recordEvent(event) {
|
|
194
485
|
this.db.prepare(`
|
|
195
|
-
INSERT OR IGNORE INTO
|
|
196
|
-
|
|
197
|
-
|
|
486
|
+
INSERT OR IGNORE INTO session_events
|
|
487
|
+
(id, session_id, project, tool, command, exit_code, stderr,
|
|
488
|
+
duration_ms, created_at, l1_score, dialog_score, user_message)
|
|
489
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
490
|
+
`).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);
|
|
491
|
+
}
|
|
492
|
+
getSessionEvents(sessionId) {
|
|
493
|
+
return this.db.prepare('SELECT * FROM session_events WHERE session_id = ? ORDER BY created_at ASC').all(sessionId);
|
|
198
494
|
}
|
|
199
495
|
getUnprocessedEvents() {
|
|
200
|
-
|
|
201
|
-
return rows.map(r => ({ id: r.id, sessionId: r.session_id, tool: r.tool, command: r.command, exitCode: r.exit_code, stderr: r.stderr, durationMs: r.duration_ms, createdAt: r.created_at }));
|
|
496
|
+
return this.db.prepare('SELECT * FROM session_events WHERE processed = 0 ORDER BY created_at ASC').all();
|
|
202
497
|
}
|
|
203
498
|
markEventsProcessed(ids) {
|
|
204
499
|
if (ids.length === 0)
|
|
205
500
|
return;
|
|
206
501
|
const placeholders = ids.map(() => '?').join(',');
|
|
207
|
-
this.db.prepare(`UPDATE
|
|
502
|
+
this.db.prepare(`UPDATE session_events SET processed = 1 WHERE id IN (${placeholders})`).run(...ids);
|
|
208
503
|
}
|
|
209
|
-
|
|
504
|
+
eventWindowExists(tool, command, windowMs = 5 * 60 * 1000) {
|
|
505
|
+
const since = Date.now() - windowMs;
|
|
506
|
+
if (command) {
|
|
507
|
+
const row = this.db.prepare('SELECT 1 FROM session_events WHERE tool = ? AND command = ? AND created_at > ? LIMIT 1').get(tool, command, since);
|
|
508
|
+
return row !== undefined;
|
|
509
|
+
}
|
|
510
|
+
const row = this.db.prepare('SELECT 1 FROM session_events WHERE tool = ? AND command IS NULL AND created_at > ? LIMIT 1').get(tool, since);
|
|
511
|
+
return row !== undefined;
|
|
512
|
+
}
|
|
513
|
+
// ── Session CRUD ─────────────────────────────────
|
|
514
|
+
createSession(id, projectRoot, task = '') {
|
|
515
|
+
this.db.prepare(`
|
|
516
|
+
INSERT INTO sessions (id, project_root, task, status, created_at)
|
|
517
|
+
VALUES (?, ?, ?, 'active', ?)
|
|
518
|
+
`).run(id, projectRoot, task, Date.now());
|
|
519
|
+
console.log('[MemoryStore] Session created', { id, project: projectRoot, task });
|
|
520
|
+
}
|
|
521
|
+
closeSession(id) {
|
|
210
522
|
const now = Date.now();
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const fourteenDaysAgo = now - 14 * 24 * 60 * 60 * 1000;
|
|
217
|
-
const lowQuality = this.db.prepare("DELETE FROM memories WHERE status = 'pending' AND confidence < 0.4 AND created_at < ?").run(fourteenDaysAgo);
|
|
218
|
-
// Old pending → rejected
|
|
219
|
-
this.db.prepare("UPDATE memories SET status = 'rejected' WHERE status = 'pending' AND created_at < ?").run(fourteenDaysAgo);
|
|
220
|
-
// Hard-delete expired after 30 days
|
|
221
|
-
const thirtyDaysAgo = now - 30 * 24 * 60 * 60 * 1000;
|
|
222
|
-
this.db.prepare("DELETE FROM memories WHERE status = 'expired' AND created_at < ?").run(thirtyDaysAgo);
|
|
223
|
-
this.db.prepare("DELETE FROM memories WHERE status = 'rejected' AND created_at < ?").run(thirtyDaysAgo);
|
|
224
|
-
return { expired: expired.changes, lowQuality: lowQuality.changes };
|
|
523
|
+
this.db.prepare(`
|
|
524
|
+
UPDATE sessions SET status = 'closed', closed_at = ?
|
|
525
|
+
WHERE id = ? AND status = 'active'
|
|
526
|
+
`).run(now, id);
|
|
527
|
+
console.log('[MemoryStore] Session closed', { id });
|
|
225
528
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
529
|
+
getSession(id) {
|
|
530
|
+
return this.db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) ?? null;
|
|
531
|
+
}
|
|
532
|
+
getActiveSessions() {
|
|
533
|
+
return this.db.prepare("SELECT * FROM sessions WHERE status = 'active' ORDER BY created_at DESC").all();
|
|
230
534
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
newSql = newSql.replace("category TEXT NOT NULL CHECK(category IN ('mcp_correction','project_knowledge','failure_lesson'))", "category TEXT NOT NULL CHECK(category IN ('procedural','semantic','episodic','failure_lesson','mcp_correction','project_knowledge'))");
|
|
238
|
-
newSql = newSql.replace("status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','approved','rejected'))", "status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','approved','rejected','expired'))");
|
|
239
|
-
this.db.prepare("UPDATE sqlite_master SET sql = ? WHERE type='table' AND name='memories'").run(newSql);
|
|
240
|
-
this.db.pragma('writable_schema = OFF');
|
|
535
|
+
updateSessionStats(id, stats) {
|
|
536
|
+
const sets = [];
|
|
537
|
+
const vals = [];
|
|
538
|
+
if (stats.tool_count !== undefined) {
|
|
539
|
+
sets.push('tool_count = ?');
|
|
540
|
+
vals.push(stats.tool_count);
|
|
241
541
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
542
|
+
if (stats.error_count !== undefined) {
|
|
543
|
+
sets.push('error_count = ?');
|
|
544
|
+
vals.push(stats.error_count);
|
|
245
545
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
546
|
+
if (sets.length === 0)
|
|
547
|
+
return;
|
|
548
|
+
vals.push(id);
|
|
549
|
+
this.db.prepare(`UPDATE sessions SET ${sets.join(', ')} WHERE id = ?`).run(...vals);
|
|
550
|
+
}
|
|
551
|
+
getZombieSessions(maxAgeHours = 24) {
|
|
552
|
+
const cutoff = Date.now() - maxAgeHours * 60 * 60 * 1000;
|
|
553
|
+
return this.db.prepare("SELECT * FROM sessions WHERE status = 'active' AND created_at < ?").all(cutoff);
|
|
554
|
+
}
|
|
555
|
+
// ── Observation CRUD ─────────────────────────────
|
|
556
|
+
addObservation(obs) {
|
|
557
|
+
const id = obs.id ?? `obs:${Date.now()}:${randomUUID().slice(0, 8)}`;
|
|
558
|
+
const now = obs.timestamp ?? Date.now();
|
|
559
|
+
this.db.prepare(`
|
|
560
|
+
INSERT INTO observations
|
|
561
|
+
(id, session_id, project, type, title, narrative, facts, files, tools, concepts,
|
|
562
|
+
importance, confidence, source_event_ids, timestamp)
|
|
563
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
564
|
+
`).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, JSON.stringify(obs.sourceEventIds ?? []), now);
|
|
565
|
+
// Insert concept junction rows
|
|
566
|
+
if (obs.concepts && obs.concepts.length > 0) {
|
|
567
|
+
const insertConcept = this.db.prepare('INSERT OR IGNORE INTO observation_concepts (observation_id, concept) VALUES (?, ?)');
|
|
568
|
+
for (const concept of obs.concepts) {
|
|
569
|
+
insertConcept.run(id, concept);
|
|
570
|
+
}
|
|
249
571
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
572
|
+
console.log('[MemoryStore] Observation added', {
|
|
573
|
+
id, sessionId: obs.sessionId, type: obs.type, conceptCount: obs.concepts?.length ?? 0,
|
|
574
|
+
});
|
|
575
|
+
return this.getObservation(id);
|
|
576
|
+
}
|
|
577
|
+
getObservation(id) {
|
|
578
|
+
return this.db.prepare('SELECT * FROM observations WHERE id = ?').get(id) ?? null;
|
|
579
|
+
}
|
|
580
|
+
listObservations(filter) {
|
|
581
|
+
const conditions = [];
|
|
582
|
+
const vals = [];
|
|
583
|
+
if (filter?.sessionId) {
|
|
584
|
+
conditions.push('o.session_id = ?');
|
|
585
|
+
vals.push(filter.sessionId);
|
|
586
|
+
}
|
|
587
|
+
if (filter?.project) {
|
|
588
|
+
conditions.push('o.project = ?');
|
|
589
|
+
vals.push(filter.project);
|
|
590
|
+
}
|
|
591
|
+
if (filter?.type) {
|
|
592
|
+
conditions.push('o.type = ?');
|
|
593
|
+
vals.push(filter.type);
|
|
594
|
+
}
|
|
595
|
+
if (filter?.memoryId !== undefined) {
|
|
596
|
+
if (filter.memoryId === null) {
|
|
597
|
+
conditions.push('o.memory_id IS NULL');
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
conditions.push('o.memory_id = ?');
|
|
601
|
+
vals.push(filter.memoryId);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
let sql = 'SELECT o.* FROM observations o';
|
|
605
|
+
if (filter?.concept) {
|
|
606
|
+
sql += ' JOIN observation_concepts oc ON o.id = oc.observation_id';
|
|
607
|
+
conditions.push('oc.concept = ?');
|
|
608
|
+
vals.push(filter.concept);
|
|
253
609
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
610
|
+
if (conditions.length > 0) {
|
|
611
|
+
sql += ' WHERE ' + conditions.join(' AND ');
|
|
612
|
+
}
|
|
613
|
+
sql += ' ORDER BY o.timestamp DESC';
|
|
614
|
+
if (filter?.limit) {
|
|
615
|
+
sql += ' LIMIT ?';
|
|
616
|
+
vals.push(filter.limit);
|
|
617
|
+
}
|
|
618
|
+
return this.db.prepare(sql).all(...vals);
|
|
257
619
|
}
|
|
258
|
-
|
|
259
|
-
|
|
620
|
+
getObservationsByConcept(concept, limit = 50) {
|
|
621
|
+
return this.listObservations({ concept, limit });
|
|
622
|
+
}
|
|
623
|
+
countObservationsByConcept(concept) {
|
|
624
|
+
const row = this.db.prepare(`
|
|
625
|
+
SELECT COUNT(DISTINCT o.id) as c
|
|
626
|
+
FROM observations o
|
|
627
|
+
JOIN observation_concepts oc ON o.id = oc.observation_id
|
|
628
|
+
WHERE oc.concept = ?
|
|
629
|
+
`).get(concept);
|
|
260
630
|
return row?.c ?? 0;
|
|
261
631
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
632
|
+
getConceptsForObservation(obsId) {
|
|
633
|
+
const rows = this.db.prepare('SELECT concept FROM observation_concepts WHERE observation_id = ?').all(obsId);
|
|
634
|
+
return rows.map(r => r.concept);
|
|
635
|
+
}
|
|
636
|
+
linkObservationToMemory(obsId, memoryId) {
|
|
637
|
+
this.db.prepare('UPDATE observations SET memory_id = ? WHERE id = ?').run(memoryId, obsId);
|
|
638
|
+
}
|
|
639
|
+
getUnconsolidatedObservations(project, minImportance = 5) {
|
|
640
|
+
return this.db.prepare(`
|
|
641
|
+
SELECT * FROM observations
|
|
642
|
+
WHERE project = ? AND memory_id IS NULL AND importance >= ?
|
|
643
|
+
ORDER BY timestamp DESC
|
|
644
|
+
`).all(project, minImportance);
|
|
645
|
+
}
|
|
646
|
+
/** Get concept clusters from unconsolidated observations, grouped by concept with session counts */
|
|
647
|
+
getClusterCandidates(project, minObservations = 3, minSessions = 2) {
|
|
648
|
+
const rows = this.db.prepare(`
|
|
649
|
+
SELECT
|
|
650
|
+
oc.concept,
|
|
651
|
+
COUNT(DISTINCT o.id) as observation_count,
|
|
652
|
+
COUNT(DISTINCT o.session_id) as session_count,
|
|
653
|
+
GROUP_CONCAT(DISTINCT o.id) as observation_ids
|
|
654
|
+
FROM observation_concepts oc
|
|
655
|
+
JOIN observations o ON o.id = oc.observation_id
|
|
656
|
+
WHERE o.project = ? AND o.memory_id IS NULL AND o.importance >= 5
|
|
657
|
+
GROUP BY oc.concept
|
|
658
|
+
HAVING observation_count >= ? AND session_count >= ?
|
|
659
|
+
ORDER BY observation_count DESC
|
|
660
|
+
LIMIT 8
|
|
661
|
+
`).all(project, minObservations, minSessions);
|
|
662
|
+
return rows.map(r => ({
|
|
663
|
+
concept: r.concept,
|
|
664
|
+
observationCount: r.observation_count,
|
|
665
|
+
sessionCount: r.session_count,
|
|
666
|
+
observationIds: r.observation_ids ? r.observation_ids.split(',') : [],
|
|
667
|
+
}));
|
|
668
|
+
}
|
|
669
|
+
// ── Signals ──────────────────────────────────────
|
|
670
|
+
recordSignals(signals) {
|
|
671
|
+
const insert = this.db.prepare(`
|
|
672
|
+
INSERT OR IGNORE INTO signals (id, session_id, type, strength, source_event_ids, payload, timestamp)
|
|
673
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
674
|
+
`);
|
|
675
|
+
for (const s of signals) {
|
|
676
|
+
insert.run(s.id, s.sessionId, s.type, s.strength, JSON.stringify(s.sourceEventIds), JSON.stringify(s.payload), s.timestamp);
|
|
677
|
+
}
|
|
678
|
+
if (signals.length > 0) {
|
|
679
|
+
console.log('[MemoryStore] Signals recorded', { count: signals.length, sessionId: signals[0].sessionId });
|
|
268
680
|
}
|
|
269
|
-
|
|
270
|
-
|
|
681
|
+
}
|
|
682
|
+
getSessionSignals(sessionId) {
|
|
683
|
+
return this.db.prepare('SELECT * FROM signals WHERE session_id = ? ORDER BY timestamp ASC').all(sessionId);
|
|
684
|
+
}
|
|
685
|
+
// ── Evidence ─────────────────────────────────────
|
|
686
|
+
addEvidence(ev) {
|
|
687
|
+
const id = ev.id ?? `ev:${Date.now()}:${randomUUID().slice(0, 8)}`;
|
|
688
|
+
this.db.prepare(`
|
|
689
|
+
INSERT INTO evidence (id, memory_id, type, weight, source_observation_ids, summary, timestamp)
|
|
690
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
691
|
+
`).run(id, ev.memoryId, ev.type, ev.weight, JSON.stringify(ev.sourceObservationIds), ev.summary, ev.timestamp ?? Date.now());
|
|
692
|
+
return id;
|
|
693
|
+
}
|
|
694
|
+
getEvidenceForMemory(memoryId) {
|
|
695
|
+
return this.db.prepare('SELECT * FROM evidence WHERE memory_id = ? ORDER BY timestamp DESC').all(memoryId);
|
|
696
|
+
}
|
|
697
|
+
// ── Access Log ───────────────────────────────────
|
|
698
|
+
recordAccess(memoryId, accessType, sessionId) {
|
|
699
|
+
const id = `acc:${Date.now()}:${randomUUID().slice(0, 8)}`;
|
|
700
|
+
this.db.prepare(`
|
|
701
|
+
INSERT INTO access_log (id, memory_id, access_type, session_id, timestamp)
|
|
702
|
+
VALUES (?, ?, ?, ?, ?)
|
|
703
|
+
`).run(id, memoryId, accessType, sessionId ?? null, Date.now());
|
|
271
704
|
}
|
|
272
705
|
incrementAccess(id) {
|
|
273
|
-
this.db.prepare(
|
|
706
|
+
this.db.prepare(`
|
|
707
|
+
UPDATE memories SET access_count = access_count + 1,
|
|
708
|
+
last_reinforced_at = COALESCE(last_reinforced_at, ?),
|
|
709
|
+
updated_at = ?
|
|
710
|
+
WHERE id = ?
|
|
711
|
+
`).run(Date.now(), Date.now(), id);
|
|
712
|
+
}
|
|
713
|
+
// ── Memory History ───────────────────────────────
|
|
714
|
+
recordHistory(entry) {
|
|
715
|
+
const id = `hist:${Date.now()}:${randomUUID().slice(0, 8)}`;
|
|
716
|
+
this.db.prepare(`
|
|
717
|
+
INSERT INTO memory_history (id, memory_id, event, old_content, new_content,
|
|
718
|
+
old_strength, new_strength, reason, created_at)
|
|
719
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
720
|
+
`).run(id, entry.memoryId, entry.event, entry.oldContent ?? null, entry.newContent ?? null, entry.oldStrength ?? null, entry.newStrength ?? null, entry.reason ?? '', Date.now());
|
|
721
|
+
}
|
|
722
|
+
getVersionChain(id) {
|
|
723
|
+
// Walk supersedes chain to find root, then collect all versions
|
|
724
|
+
let current = this.get(id);
|
|
725
|
+
if (!current)
|
|
726
|
+
return [];
|
|
727
|
+
// Walk backward to find root (oldest version)
|
|
728
|
+
const visited = new Set();
|
|
729
|
+
while (current?.supersedes && !visited.has(current.id)) {
|
|
730
|
+
visited.add(current.id);
|
|
731
|
+
const prev = this.get(current.supersedes);
|
|
732
|
+
if (!prev)
|
|
733
|
+
break;
|
|
734
|
+
current = prev;
|
|
735
|
+
}
|
|
736
|
+
// Collect all versions forward via superseded_by
|
|
737
|
+
const chain = [];
|
|
738
|
+
const rootId = current?.id;
|
|
739
|
+
if (!rootId)
|
|
740
|
+
return [];
|
|
741
|
+
let node = this.get(rootId);
|
|
742
|
+
const collected = new Set();
|
|
743
|
+
while (node && !collected.has(node.id)) {
|
|
744
|
+
collected.add(node.id);
|
|
745
|
+
chain.push(node);
|
|
746
|
+
node = node.superseded_by ? this.get(node.superseded_by) : null;
|
|
747
|
+
}
|
|
748
|
+
return chain.sort((a, b) => a.version - b.version);
|
|
749
|
+
}
|
|
750
|
+
getMemoryHistory(memoryId) {
|
|
751
|
+
return this.db.prepare('SELECT * FROM memory_history WHERE memory_id = ? ORDER BY created_at DESC').all(memoryId);
|
|
752
|
+
}
|
|
753
|
+
// ── Retention ────────────────────────────────────
|
|
754
|
+
upsertRetentionScore(memoryId, data) {
|
|
755
|
+
this.db.prepare(`
|
|
756
|
+
INSERT OR REPLACE INTO retention_scores
|
|
757
|
+
(memory_id, score, salience, temporal_decay, reinforcement_boost,
|
|
758
|
+
last_accessed, access_count, computed_at)
|
|
759
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
760
|
+
`).run(memoryId, data.score, data.salience, data.temporal_decay, data.reinforcement_boost, data.last_accessed ?? null, data.access_count ?? 0, Date.now());
|
|
761
|
+
}
|
|
762
|
+
getColdRetentionScores(threshold) {
|
|
763
|
+
return this.db.prepare('SELECT memory_id, score FROM retention_scores WHERE score < ? ORDER BY score ASC').all(threshold);
|
|
764
|
+
}
|
|
765
|
+
getRetentionScore(memoryId) {
|
|
766
|
+
return this.db.prepare('SELECT * FROM retention_scores WHERE memory_id = ?').get(memoryId) ?? null;
|
|
767
|
+
}
|
|
768
|
+
computeRetentionScore(memory) {
|
|
769
|
+
const ageDays = (Date.now() - memory.created_at) / 86400000;
|
|
770
|
+
const lambdaBase = 0.05;
|
|
771
|
+
const lambda = lambdaBase / (1 + memory.reinforce_count);
|
|
772
|
+
const sourceImportance = {
|
|
773
|
+
manual: 1.0, consolidation: 0.8, auto: 0.6, rule_engine: 0.4,
|
|
774
|
+
};
|
|
775
|
+
const salience = memory.confidence * (sourceImportance[memory.source] ?? 0.5);
|
|
776
|
+
const temporalDecay = Math.exp(-lambda * Math.max(ageDays, 0));
|
|
777
|
+
const reinforcementBoost = 1 + 0.1 * Math.min(memory.access_count, 10);
|
|
778
|
+
const score = salience * temporalDecay * reinforcementBoost;
|
|
779
|
+
return { score, salience, temporalDecay, reinforcementBoost };
|
|
780
|
+
}
|
|
781
|
+
runRetentionSweep(project) {
|
|
782
|
+
const now = Date.now();
|
|
783
|
+
const fourteenDaysAgo = now - 14 * 24 * 60 * 60 * 1000;
|
|
784
|
+
// Load candidates: active/approved/pending memories
|
|
785
|
+
const candidates = this.db.prepare(`
|
|
786
|
+
SELECT * FROM memories
|
|
787
|
+
WHERE status IN ('active','approved','pending')
|
|
788
|
+
AND superseded_by IS NULL
|
|
789
|
+
AND (expires_at IS NULL OR expires_at > ?)
|
|
790
|
+
ORDER BY updated_at ASC
|
|
791
|
+
`).all(now);
|
|
792
|
+
let kept = 0, downgraded = 0, expired = 0;
|
|
793
|
+
for (const mem of candidates) {
|
|
794
|
+
// Explicit expires_at check (takes priority)
|
|
795
|
+
if (mem.expires_at != null && mem.expires_at < now) {
|
|
796
|
+
this.db.prepare("UPDATE memories SET status = 'expired' WHERE id = ?").run(mem.id);
|
|
797
|
+
this.recordHistory({ memoryId: mem.id, event: 'EXPIRE', reason: 'explicit_ttl' });
|
|
798
|
+
expired++;
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
// Never accessed + old → direct expire
|
|
802
|
+
const accessCount = this.db.prepare('SELECT COUNT(*) as c FROM access_log WHERE memory_id = ?').get(mem.id).c;
|
|
803
|
+
const ageDays = (now - mem.created_at) / 86400000;
|
|
804
|
+
if (accessCount === 0 && ageDays > 14) {
|
|
805
|
+
this.db.prepare("UPDATE memories SET status = 'expired' WHERE id = ?").run(mem.id);
|
|
806
|
+
this.recordHistory({ memoryId: mem.id, event: 'EXPIRE', reason: 'never_accessed' });
|
|
807
|
+
expired++;
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
// Compute retention score
|
|
811
|
+
const retention = this.computeRetentionScore(mem);
|
|
812
|
+
this.upsertRetentionScore(mem.id, {
|
|
813
|
+
score: retention.score,
|
|
814
|
+
salience: retention.salience,
|
|
815
|
+
temporal_decay: retention.temporalDecay,
|
|
816
|
+
reinforcement_boost: retention.reinforcementBoost,
|
|
817
|
+
access_count: accessCount,
|
|
818
|
+
});
|
|
819
|
+
// Decision matrix
|
|
820
|
+
if (retention.score >= 0.6) {
|
|
821
|
+
this.db.prepare("UPDATE memories SET expires_at = ? WHERE id = ?").run(now + 30 * 24 * 60 * 60 * 1000, mem.id);
|
|
822
|
+
kept++;
|
|
823
|
+
}
|
|
824
|
+
else if (retention.score >= 0.3) {
|
|
825
|
+
this.db.prepare("UPDATE memories SET status = 'pending' WHERE id = ? AND status = 'active'").run(mem.id);
|
|
826
|
+
downgraded++;
|
|
827
|
+
}
|
|
828
|
+
else {
|
|
829
|
+
this.db.prepare("UPDATE memories SET status = 'expired' WHERE id = ?").run(mem.id);
|
|
830
|
+
this.recordHistory({ memoryId: mem.id, event: 'EXPIRE', reason: `retention_score_${retention.score.toFixed(2)}` });
|
|
831
|
+
expired++;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
// Cleanup: pending + age > 14d → expired
|
|
835
|
+
const pendingExpired = this.db.prepare("UPDATE memories SET status = 'expired' WHERE status = 'pending' AND updated_at < ?").run(fourteenDaysAgo);
|
|
836
|
+
// Clean old processed events
|
|
837
|
+
const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;
|
|
838
|
+
const cleanedEvents = this.db.prepare('DELETE FROM session_events WHERE processed = 1 AND created_at < ?').run(sevenDaysAgo);
|
|
839
|
+
const total = { swept: candidates.length, kept, downgraded, expired: expired + pendingExpired.changes, cleanedEvents: cleanedEvents.changes };
|
|
840
|
+
console.log('[MemoryStore] Retention sweep complete', total);
|
|
841
|
+
return total;
|
|
274
842
|
}
|
|
843
|
+
// ── TTL / Cleanup ────────────────────────────────
|
|
275
844
|
setExpires(id, expiresAt) {
|
|
276
845
|
this.db.prepare('UPDATE memories SET expires_at = ? WHERE id = ?').run(expiresAt, id);
|
|
277
846
|
}
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
|
|
847
|
+
cleanupExpired() {
|
|
848
|
+
const now = Date.now();
|
|
849
|
+
const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;
|
|
850
|
+
this.db.prepare('DELETE FROM session_events WHERE processed = 1 AND created_at < ?').run(sevenDaysAgo);
|
|
851
|
+
const expired = this.db.prepare("UPDATE memories SET status = 'expired' WHERE expires_at IS NOT NULL AND expires_at < ? AND status != 'expired'").run(now);
|
|
852
|
+
const fourteenDaysAgo = now - 14 * 24 * 60 * 60 * 1000;
|
|
853
|
+
const lowQuality = this.db.prepare("DELETE FROM memories WHERE status = 'pending' AND confidence < 0.4 AND created_at < ?").run(fourteenDaysAgo);
|
|
854
|
+
this.db.prepare("UPDATE memories SET status = 'expired' WHERE status = 'pending' AND created_at < ?").run(fourteenDaysAgo);
|
|
855
|
+
return { expired: expired.changes, lowQuality: lowQuality.changes };
|
|
856
|
+
}
|
|
857
|
+
cleanupLowQuality() {
|
|
858
|
+
const result = this.db.prepare("DELETE FROM memories WHERE status = 'pending' AND source = 'rule_engine' AND confidence < 0.6").run();
|
|
859
|
+
return { removed: result.changes };
|
|
281
860
|
}
|
|
861
|
+
// ── Lifecycle ────────────────────────────────────
|
|
862
|
+
close() {
|
|
863
|
+
this.db.close();
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
// ── Helpers ────────────────────────────────────────────
|
|
867
|
+
function camelToSnake(str) {
|
|
868
|
+
return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
|
|
282
869
|
}
|
|
283
870
|
//# sourceMappingURL=memory-store.js.map
|