@devflow-tools/memory-engine 0.13.3 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/consolidator.d.ts +13 -0
  2. package/dist/consolidator.d.ts.map +1 -0
  3. package/dist/consolidator.js +184 -0
  4. package/dist/consolidator.js.map +1 -0
  5. package/dist/event-grouper.d.ts +13 -0
  6. package/dist/event-grouper.d.ts.map +1 -0
  7. package/dist/event-grouper.js +178 -0
  8. package/dist/event-grouper.js.map +1 -0
  9. package/dist/graph-store.d.ts.map +1 -1
  10. package/dist/graph-store.js +2 -4
  11. package/dist/graph-store.js.map +1 -1
  12. package/dist/hybrid-search.d.ts +7 -4
  13. package/dist/hybrid-search.d.ts.map +1 -1
  14. package/dist/hybrid-search.js +69 -8
  15. package/dist/hybrid-search.js.map +1 -1
  16. package/dist/index.d.ts +14 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +7 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/l2-evaluator.d.ts +31 -0
  21. package/dist/l2-evaluator.d.ts.map +1 -0
  22. package/dist/l2-evaluator.js +103 -0
  23. package/dist/l2-evaluator.js.map +1 -0
  24. package/dist/llm-provider.d.ts +9 -0
  25. package/dist/llm-provider.d.ts.map +1 -0
  26. package/dist/llm-provider.js +37 -0
  27. package/dist/llm-provider.js.map +1 -0
  28. package/dist/memory-engine.d.ts +27 -4
  29. package/dist/memory-engine.d.ts.map +1 -1
  30. package/dist/memory-engine.js +275 -182
  31. package/dist/memory-engine.js.map +1 -1
  32. package/dist/memory-gate.d.ts +116 -0
  33. package/dist/memory-gate.d.ts.map +1 -0
  34. package/dist/memory-gate.js +385 -0
  35. package/dist/memory-gate.js.map +1 -0
  36. package/dist/memory-store.d.ts +234 -32
  37. package/dist/memory-store.d.ts.map +1 -1
  38. package/dist/memory-store.js +729 -142
  39. package/dist/memory-store.js.map +1 -1
  40. package/dist/project-validator.d.ts +10 -0
  41. package/dist/project-validator.d.ts.map +1 -0
  42. package/dist/project-validator.js +36 -0
  43. package/dist/project-validator.js.map +1 -0
  44. package/dist/session-summarizer.d.ts +10 -0
  45. package/dist/session-summarizer.d.ts.map +1 -0
  46. package/dist/session-summarizer.js +197 -0
  47. package/dist/session-summarizer.js.map +1 -0
  48. package/dist/signal-buffer.d.ts +42 -0
  49. package/dist/signal-buffer.d.ts.map +1 -0
  50. package/dist/signal-buffer.js +104 -0
  51. package/dist/signal-buffer.js.map +1 -0
  52. package/dist/signal-detector.d.ts +39 -0
  53. package/dist/signal-detector.d.ts.map +1 -0
  54. package/dist/signal-detector.js +239 -0
  55. package/dist/signal-detector.js.map +1 -0
  56. package/package.json +5 -5
@@ -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
- const SCHEMA = `
6
+ // ── Schema ─────────────────────────────────────────────
7
+ const SCHEMA_V2 = `
8
+ -- ============================================================
9
+ -- Core: memories (expanded from v1)
10
+ -- ============================================================
7
11
  CREATE TABLE IF NOT EXISTS memories (
8
- id TEXT PRIMARY KEY,
9
- category TEXT NOT NULL CHECK(category IN ('procedural','semantic','episodic','failure_lesson','mcp_correction','project_knowledge')),
10
- content TEXT NOT NULL,
11
- original_text TEXT,
12
- source TEXT NOT NULL DEFAULT 'auto',
13
- status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','approved','rejected','expired')),
14
- confidence REAL NOT NULL DEFAULT 0.5,
15
- scope TEXT NOT NULL DEFAULT '',
16
- session_id TEXT,
17
- created_at INTEGER NOT NULL,
18
- approved_at INTEGER
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
- relation TEXT NOT NULL,
38
- weight REAL DEFAULT 1.0,
39
- PRIMARY KEY (source_id, target_id, relation),
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
- CREATE TABLE IF NOT EXISTS events (
45
- id TEXT PRIMARY KEY,
46
- session_id TEXT NOT NULL,
47
- tool TEXT NOT NULL,
48
- command TEXT,
49
- exit_code INTEGER,
50
- stderr TEXT,
51
- duration_ms INTEGER,
52
- created_at INTEGER NOT NULL,
53
- processed INTEGER NOT NULL DEFAULT 0
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 idx_events_session ON events(session_id);
56
- CREATE INDEX IF NOT EXISTS idx_events_processed ON events(processed);
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) VALUES (new.rowid, new.id, new.content, new.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) VALUES('delete', old.id, old.content, old.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) VALUES('delete', old.id, old.content, old.scope);
67
- INSERT INTO memories_fts(rowid, id, content, scope) VALUES (new.rowid, new.id, new.content, new.scope);
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
- const dbPath = join(memDir, 'memories.db');
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(SCHEMA);
266
+ this.db.exec(SCHEMA_V2);
83
267
  this.db.exec(FTS_TRIGGERS);
84
- this.migrateSchema();
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 approvedAt = entry.status === 'approved' ? now : null;
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, category, content, original_text, source, status, confidence, scope, session_id, created_at, approved_at)
92
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
93
- `).run(id, entry.category, entry.content, entry.originalText ?? null, entry.source ?? 'auto', entry.status ?? 'pending', entry.confidence ?? 0.5, entry.scope ?? '', entry.sessionId ?? null, now, approvedAt);
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
- const row = this.db.prepare('SELECT * FROM memories WHERE id = ?').get(id);
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
- if (updates.content !== undefined) {
104
- sets.push('content = ?');
105
- vals.push(updates.content);
106
- }
107
- if (updates.status !== undefined) {
108
- sets.push('status = ?');
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 1=1';
397
+ let sql = 'SELECT * FROM memories WHERE superseded_by IS NULL';
129
398
  const vals = [];
130
- if (filter?.category) {
131
- sql += ' AND category = ?';
132
- vals.push(filter.category);
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 created_at DESC';
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.db.prepare('SELECT * FROM memories WHERE category = ? AND status = ? ORDER BY created_at DESC LIMIT ?').all(category, 'approved', limit);
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
- const rows = this.db.prepare(`
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 = 'approved'
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
- const rows = this.db.prepare(`
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 = 'approved'
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
- addEdge(sourceId, targetId, relation, weight = 1.0) {
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, relation, weight) VALUES (?, ?, ?, ?)
179
- `).run(sourceId, targetId, relation, weight);
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, relation, weight FROM memory_edges WHERE source_id = ?').all(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
- close() {
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 events (id, session_id, tool, command, exit_code, stderr, duration_ms, created_at)
196
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
197
- `).run(event.id, event.sessionId, event.tool, event.command ?? null, event.exitCode ?? null, event.stderr ?? null, event.durationMs ?? null, event.createdAt);
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
- const rows = this.db.prepare('SELECT id, session_id, tool, command, exit_code, stderr, duration_ms, created_at FROM events WHERE processed = 0 ORDER BY created_at ASC').all();
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 events SET processed = 1 WHERE id IN (${placeholders})`).run(...ids);
502
+ this.db.prepare(`UPDATE session_events SET processed = 1 WHERE id IN (${placeholders})`).run(...ids);
208
503
  }
209
- cleanupExpired() {
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
- const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;
212
- this.db.prepare('DELETE FROM events WHERE processed = 1 AND created_at < ?').run(sevenDaysAgo);
213
- // TTL soft-delete
214
- const expired = this.db.prepare("UPDATE memories SET status = 'expired' WHERE expires_at IS NOT NULL AND expires_at < ? AND status != 'expired'").run(now);
215
- // Low-quality pending cleanup
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
- cleanupLowQuality() {
227
- // Remove pending rule-engine memories with low confidence (pre-fix garbage)
228
- const result = this.db.prepare("DELETE FROM memories WHERE status = 'pending' AND source = 'rule_engine' AND confidence < 0.6").run();
229
- return { removed: result.changes };
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
- migrateSchema() {
232
- // Update CHECK constraint to accept new category names
233
- const sql = this.db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='memories'").get();
234
- if (sql && (!sql.sql.includes('procedural') || !sql.sql.includes('expired'))) {
235
- this.db.pragma('writable_schema = ON');
236
- let newSql = sql.sql;
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
- // Add access tracking and TTL columns
243
- try {
244
- this.db.exec('ALTER TABLE memories ADD COLUMN access_count INTEGER DEFAULT 0');
542
+ if (stats.error_count !== undefined) {
543
+ sets.push('error_count = ?');
544
+ vals.push(stats.error_count);
245
545
  }
246
- catch { }
247
- try {
248
- this.db.exec('ALTER TABLE memories ADD COLUMN last_accessed_at INTEGER');
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
- catch { }
251
- try {
252
- this.db.exec('ALTER TABLE memories ADD COLUMN expires_at INTEGER');
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
- catch { }
255
- this.db.exec('CREATE INDEX IF NOT EXISTS idx_memories_access ON memories(access_count, last_accessed_at)');
256
- this.db.exec('CREATE INDEX IF NOT EXISTS idx_memories_expires ON memories(expires_at) WHERE expires_at IS NOT NULL');
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
- countByStatus(status) {
259
- const row = this.db.prepare('SELECT COUNT(*) as c FROM memories WHERE status = ?').get(status);
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
- // --- Dedup helpers ---
263
- eventWindowExists(tool, command, windowMs = 5 * 60 * 1000) {
264
- const since = Date.now() - windowMs;
265
- if (command) {
266
- const row = this.db.prepare('SELECT 1 FROM events WHERE tool = ? AND command = ? AND created_at > ? LIMIT 1').get(tool, command, since);
267
- return row !== undefined;
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
- const row = this.db.prepare('SELECT 1 FROM events WHERE tool = ? AND command IS NULL AND created_at > ? LIMIT 1').get(tool, since);
270
- return row !== undefined;
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('UPDATE memories SET access_count = access_count + 1, last_accessed_at = ? WHERE id = ?').run(Date.now(), id);
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 > 90) {
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
- getSessionEvents(sessionId) {
279
- const rows = this.db.prepare('SELECT id, session_id, tool, command, exit_code, stderr, duration_ms, created_at FROM events WHERE session_id = ? ORDER BY created_at ASC').all(sessionId);
280
- 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 }));
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