psychmem 1.0.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.
Files changed (93) hide show
  1. package/README.md +632 -0
  2. package/dist/adapters/claude-code/index.d.ts +125 -0
  3. package/dist/adapters/claude-code/index.d.ts.map +1 -0
  4. package/dist/adapters/claude-code/index.js +398 -0
  5. package/dist/adapters/claude-code/index.js.map +1 -0
  6. package/dist/adapters/opencode/index.d.ts +50 -0
  7. package/dist/adapters/opencode/index.d.ts.map +1 -0
  8. package/dist/adapters/opencode/index.js +793 -0
  9. package/dist/adapters/opencode/index.js.map +1 -0
  10. package/dist/adapters/types.d.ts +226 -0
  11. package/dist/adapters/types.d.ts.map +1 -0
  12. package/dist/adapters/types.js +6 -0
  13. package/dist/adapters/types.js.map +1 -0
  14. package/dist/cli.d.ts +19 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +461 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/hooks/index.d.ts +92 -0
  19. package/dist/hooks/index.d.ts.map +1 -0
  20. package/dist/hooks/index.js +304 -0
  21. package/dist/hooks/index.js.map +1 -0
  22. package/dist/hooks/post-tool-use.d.ts +26 -0
  23. package/dist/hooks/post-tool-use.d.ts.map +1 -0
  24. package/dist/hooks/post-tool-use.js +69 -0
  25. package/dist/hooks/post-tool-use.js.map +1 -0
  26. package/dist/hooks/session-end.d.ts +32 -0
  27. package/dist/hooks/session-end.d.ts.map +1 -0
  28. package/dist/hooks/session-end.js +66 -0
  29. package/dist/hooks/session-end.js.map +1 -0
  30. package/dist/hooks/session-start.d.ts +55 -0
  31. package/dist/hooks/session-start.d.ts.map +1 -0
  32. package/dist/hooks/session-start.js +173 -0
  33. package/dist/hooks/session-start.js.map +1 -0
  34. package/dist/hooks/stop.d.ts +72 -0
  35. package/dist/hooks/stop.d.ts.map +1 -0
  36. package/dist/hooks/stop.js +273 -0
  37. package/dist/hooks/stop.js.map +1 -0
  38. package/dist/index.d.ts +114 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +191 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/memory/context-sweep.d.ts +107 -0
  43. package/dist/memory/context-sweep.d.ts.map +1 -0
  44. package/dist/memory/context-sweep.js +557 -0
  45. package/dist/memory/context-sweep.js.map +1 -0
  46. package/dist/memory/patterns.d.ts +106 -0
  47. package/dist/memory/patterns.d.ts.map +1 -0
  48. package/dist/memory/patterns.js +613 -0
  49. package/dist/memory/patterns.js.map +1 -0
  50. package/dist/memory/selective-memory.d.ts +78 -0
  51. package/dist/memory/selective-memory.d.ts.map +1 -0
  52. package/dist/memory/selective-memory.js +227 -0
  53. package/dist/memory/selective-memory.js.map +1 -0
  54. package/dist/memory/structural-analyzer.d.ts +75 -0
  55. package/dist/memory/structural-analyzer.d.ts.map +1 -0
  56. package/dist/memory/structural-analyzer.js +359 -0
  57. package/dist/memory/structural-analyzer.js.map +1 -0
  58. package/dist/retrieval/index.d.ts +106 -0
  59. package/dist/retrieval/index.d.ts.map +1 -0
  60. package/dist/retrieval/index.js +291 -0
  61. package/dist/retrieval/index.js.map +1 -0
  62. package/dist/storage/database.d.ts +138 -0
  63. package/dist/storage/database.d.ts.map +1 -0
  64. package/dist/storage/database.js +748 -0
  65. package/dist/storage/database.js.map +1 -0
  66. package/dist/storage/sqlite-adapter.d.ts +35 -0
  67. package/dist/storage/sqlite-adapter.d.ts.map +1 -0
  68. package/dist/storage/sqlite-adapter.js +103 -0
  69. package/dist/storage/sqlite-adapter.js.map +1 -0
  70. package/dist/transcript/index.d.ts +8 -0
  71. package/dist/transcript/index.d.ts.map +1 -0
  72. package/dist/transcript/index.js +6 -0
  73. package/dist/transcript/index.js.map +1 -0
  74. package/dist/transcript/parser.d.ts +93 -0
  75. package/dist/transcript/parser.d.ts.map +1 -0
  76. package/dist/transcript/parser.js +373 -0
  77. package/dist/transcript/parser.js.map +1 -0
  78. package/dist/transcript/sweep.d.ts +75 -0
  79. package/dist/transcript/sweep.d.ts.map +1 -0
  80. package/dist/transcript/sweep.js +202 -0
  81. package/dist/transcript/sweep.js.map +1 -0
  82. package/dist/types/index.d.ts +328 -0
  83. package/dist/types/index.d.ts.map +1 -0
  84. package/dist/types/index.js +80 -0
  85. package/dist/types/index.js.map +1 -0
  86. package/dist/utils/paths.d.ts +19 -0
  87. package/dist/utils/paths.d.ts.map +1 -0
  88. package/dist/utils/paths.js +43 -0
  89. package/dist/utils/paths.js.map +1 -0
  90. package/hooks/hooks.json +54 -0
  91. package/package.json +83 -0
  92. package/plugin.js +45 -0
  93. package/plugin.json +19 -0
@@ -0,0 +1,748 @@
1
+ /**
2
+ * PsychMem Database Layer
3
+ * SQLite with runtime-agnostic adapter (supports Node.js + Bun)
4
+ */
5
+ import { v4 as uuidv4 } from 'uuid';
6
+ import { DEFAULT_CONFIG } from '../types/index.js';
7
+ import { resolveDbPath } from '../utils/paths.js';
8
+ import { createDatabase, isBun } from './sqlite-adapter.js';
9
+ export class MemoryDatabase {
10
+ db;
11
+ config;
12
+ initialized = false;
13
+ _inTransaction = false;
14
+ constructor(config = {}) {
15
+ this.config = { ...DEFAULT_CONFIG, ...config };
16
+ }
17
+ /**
18
+ * Initialize database (must be called before use)
19
+ * For sync compatibility, also auto-initializes on first use
20
+ */
21
+ async init() {
22
+ if (this.initialized)
23
+ return;
24
+ const dbPath = resolveDbPath(this.config.dbPath, this.config.agentType);
25
+ this.db = await createDatabase(dbPath);
26
+ // Set WAL mode
27
+ this.db.exec('PRAGMA journal_mode = WAL');
28
+ this.initializeSchema();
29
+ this.initialized = true;
30
+ }
31
+ /**
32
+ * Ensure database is initialized (for backwards compatibility)
33
+ */
34
+ ensureInit() {
35
+ if (!this.initialized) {
36
+ throw new Error('Database not initialized. Call await db.init() first.');
37
+ }
38
+ }
39
+ /**
40
+ * Initialize database schema
41
+ */
42
+ initializeSchema() {
43
+ // First, create base tables (without project_scope index)
44
+ this.db.exec(`
45
+ -- Schema version table — bump SCHEMA_VERSION constant on breaking changes
46
+ CREATE TABLE IF NOT EXISTS schema_version (
47
+ version INTEGER NOT NULL,
48
+ applied_at TEXT NOT NULL
49
+ );
50
+
51
+ -- Sessions table
52
+ CREATE TABLE IF NOT EXISTS sessions (
53
+ id TEXT PRIMARY KEY,
54
+ project TEXT NOT NULL,
55
+ started_at TEXT NOT NULL,
56
+ ended_at TEXT,
57
+ status TEXT NOT NULL DEFAULT 'active',
58
+ metadata TEXT,
59
+ transcript_path TEXT,
60
+ transcript_watermark INTEGER DEFAULT 0,
61
+ message_watermark INTEGER DEFAULT 0
62
+ );
63
+
64
+ -- Events table (raw hook events)
65
+ CREATE TABLE IF NOT EXISTS events (
66
+ id TEXT PRIMARY KEY,
67
+ session_id TEXT NOT NULL,
68
+ hook_type TEXT NOT NULL,
69
+ timestamp TEXT NOT NULL,
70
+ content TEXT NOT NULL,
71
+ tool_name TEXT,
72
+ tool_input TEXT,
73
+ tool_output TEXT,
74
+ metadata TEXT,
75
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
76
+ );
77
+
78
+ -- Memory units table (consolidated memories)
79
+ CREATE TABLE IF NOT EXISTS memory_units (
80
+ id TEXT PRIMARY KEY,
81
+ session_id TEXT,
82
+ store TEXT NOT NULL,
83
+ classification TEXT NOT NULL,
84
+ summary TEXT NOT NULL,
85
+ source_event_ids TEXT NOT NULL,
86
+ project_scope TEXT,
87
+
88
+ created_at TEXT NOT NULL,
89
+ updated_at TEXT NOT NULL,
90
+ last_accessed_at TEXT NOT NULL,
91
+
92
+ recency REAL NOT NULL DEFAULT 0,
93
+ frequency INTEGER NOT NULL DEFAULT 1,
94
+ importance REAL NOT NULL DEFAULT 0.5,
95
+ utility REAL NOT NULL DEFAULT 0.5,
96
+ novelty REAL NOT NULL DEFAULT 0.5,
97
+ confidence REAL NOT NULL DEFAULT 0.5,
98
+ interference REAL NOT NULL DEFAULT 0,
99
+
100
+ strength REAL NOT NULL DEFAULT 0.5,
101
+ decay_rate REAL NOT NULL,
102
+
103
+ tags TEXT,
104
+ associations TEXT,
105
+
106
+ status TEXT NOT NULL DEFAULT 'active',
107
+ version INTEGER NOT NULL DEFAULT 1,
108
+
109
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
110
+ );
111
+
112
+ -- Memory evidence table
113
+ CREATE TABLE IF NOT EXISTS memory_evidence (
114
+ id TEXT PRIMARY KEY,
115
+ memory_id TEXT NOT NULL,
116
+ event_id TEXT NOT NULL,
117
+ timestamp TEXT NOT NULL,
118
+ contribution TEXT,
119
+ confidence_delta REAL DEFAULT 0,
120
+ FOREIGN KEY (memory_id) REFERENCES memory_units(id),
121
+ FOREIGN KEY (event_id) REFERENCES events(id)
122
+ );
123
+
124
+ -- Retrieval logs for feedback learning
125
+ CREATE TABLE IF NOT EXISTS retrieval_logs (
126
+ id TEXT PRIMARY KEY,
127
+ session_id TEXT NOT NULL,
128
+ memory_id TEXT NOT NULL,
129
+ query TEXT NOT NULL,
130
+ timestamp TEXT NOT NULL,
131
+ was_used INTEGER NOT NULL DEFAULT 0,
132
+ user_feedback TEXT,
133
+ relevance_score REAL NOT NULL,
134
+ FOREIGN KEY (session_id) REFERENCES sessions(id),
135
+ FOREIGN KEY (memory_id) REFERENCES memory_units(id)
136
+ );
137
+
138
+ -- User feedback table
139
+ CREATE TABLE IF NOT EXISTS feedback (
140
+ id TEXT PRIMARY KEY,
141
+ memory_id TEXT,
142
+ type TEXT NOT NULL,
143
+ content TEXT,
144
+ timestamp TEXT NOT NULL,
145
+ FOREIGN KEY (memory_id) REFERENCES memory_units(id)
146
+ );
147
+
148
+ -- Indexes for performance (excluding project_scope - added after migration)
149
+ CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
150
+ CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
151
+ CREATE INDEX IF NOT EXISTS idx_memory_store ON memory_units(store);
152
+ CREATE INDEX IF NOT EXISTS idx_memory_status ON memory_units(status);
153
+ CREATE INDEX IF NOT EXISTS idx_memory_strength ON memory_units(strength);
154
+ CREATE INDEX IF NOT EXISTS idx_memory_classification ON memory_units(classification);
155
+ CREATE INDEX IF NOT EXISTS idx_memory_session ON memory_units(session_id);
156
+ CREATE INDEX IF NOT EXISTS idx_retrieval_session ON retrieval_logs(session_id);
157
+ -- Composite indexes for common query patterns
158
+ CREATE INDEX IF NOT EXISTS idx_memory_status_strength ON memory_units(status, strength);
159
+ CREATE INDEX IF NOT EXISTS idx_memory_session_status ON memory_units(session_id, status);
160
+ `);
161
+ // Schema version check — fail loudly on mismatch (prevents silent corruption)
162
+ const SCHEMA_VERSION = 2;
163
+ const row = this.db.prepare('SELECT version FROM schema_version ORDER BY rowid DESC LIMIT 1').get();
164
+ if (!row) {
165
+ // Fresh DB — stamp with current version
166
+ this.db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)').run(SCHEMA_VERSION, new Date().toISOString());
167
+ }
168
+ else if (row.version !== SCHEMA_VERSION) {
169
+ throw new Error(`psychmem DB schema mismatch: expected version ${SCHEMA_VERSION}, found ${row.version}. ` +
170
+ 'Delete the database file or run migrations to continue.');
171
+ }
172
+ // Migration: Add project_scope column if it doesn't exist (for existing DBs pre-v1.6)
173
+ // MUST run before creating index on project_scope
174
+ this.migrateProjectScope();
175
+ // Now safe to create index on project_scope (column guaranteed to exist)
176
+ this.db.exec(`
177
+ CREATE INDEX IF NOT EXISTS idx_memory_project_scope ON memory_units(project_scope);
178
+ `);
179
+ }
180
+ /**
181
+ * Migration: Add project_scope column to existing databases
182
+ */
183
+ migrateProjectScope() {
184
+ // Check if column exists
185
+ const tableInfo = this.db.prepare(`PRAGMA table_info(memory_units)`).all();
186
+ const hasProjectScope = tableInfo.some(col => col.name === 'project_scope');
187
+ if (!hasProjectScope) {
188
+ this.db.exec(`ALTER TABLE memory_units ADD COLUMN project_scope TEXT`);
189
+ }
190
+ }
191
+ // ===========================================================================
192
+ // Session Operations
193
+ // ===========================================================================
194
+ createSession(project, metadata, transcriptPath) {
195
+ this.ensureInit();
196
+ const session = {
197
+ id: uuidv4(),
198
+ project,
199
+ startedAt: new Date(),
200
+ status: 'active',
201
+ metadata,
202
+ transcriptPath,
203
+ transcriptWatermark: 0,
204
+ messageWatermark: 0,
205
+ };
206
+ this.db.prepare(`
207
+ INSERT INTO sessions (id, project, started_at, status, metadata, transcript_path, transcript_watermark, message_watermark)
208
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
209
+ `).run(session.id, session.project, session.startedAt.toISOString(), session.status, metadata ? JSON.stringify(metadata) : null, transcriptPath ?? null, 0, 0);
210
+ return session;
211
+ }
212
+ endSession(sessionId, status = 'completed') {
213
+ this.ensureInit();
214
+ this.db.prepare(`
215
+ UPDATE sessions SET ended_at = ?, status = ? WHERE id = ?
216
+ `).run(new Date().toISOString(), status, sessionId);
217
+ }
218
+ getSession(sessionId) {
219
+ this.ensureInit();
220
+ const row = this.db.prepare(`SELECT * FROM sessions WHERE id = ?`).get(sessionId);
221
+ if (!row)
222
+ return null;
223
+ return this.rowToSession(row);
224
+ }
225
+ getActiveSessions() {
226
+ this.ensureInit();
227
+ const rows = this.db.prepare(`SELECT * FROM sessions WHERE status = 'active'`).all();
228
+ return rows.map(row => this.rowToSession(row));
229
+ }
230
+ /**
231
+ * Get the current transcript watermark (byte offset) for a session
232
+ */
233
+ getSessionWatermark(sessionId) {
234
+ this.ensureInit();
235
+ const row = this.db.prepare(`
236
+ SELECT transcript_watermark FROM sessions WHERE id = ?
237
+ `).get(sessionId);
238
+ return row?.transcript_watermark ?? 0;
239
+ }
240
+ /**
241
+ * Update the transcript watermark (byte offset) for a session
242
+ */
243
+ updateSessionWatermark(sessionId, watermark) {
244
+ this.ensureInit();
245
+ this.db.prepare(`
246
+ UPDATE sessions SET transcript_watermark = ? WHERE id = ?
247
+ `).run(watermark, sessionId);
248
+ }
249
+ /**
250
+ * Get the current message watermark (message index) for a session
251
+ * Used by OpenCode adapter to track which messages have been processed
252
+ */
253
+ getMessageWatermark(sessionId) {
254
+ this.ensureInit();
255
+ const row = this.db.prepare(`
256
+ SELECT message_watermark FROM sessions WHERE id = ?
257
+ `).get(sessionId);
258
+ return row?.message_watermark ?? 0;
259
+ }
260
+ /**
261
+ * Update the message watermark (message index) for a session
262
+ * Used by OpenCode adapter to mark messages as processed
263
+ */
264
+ updateMessageWatermark(sessionId, watermark) {
265
+ this.ensureInit();
266
+ this.db.prepare(`
267
+ UPDATE sessions SET message_watermark = ? WHERE id = ?
268
+ `).run(watermark, sessionId);
269
+ }
270
+ /**
271
+ * Get all memories for a specific session (for deduplication)
272
+ */
273
+ getSessionMemories(sessionId, status = 'active') {
274
+ this.ensureInit();
275
+ const rows = this.db.prepare(`
276
+ SELECT * FROM memory_units
277
+ WHERE session_id = ? AND status = ?
278
+ ORDER BY created_at DESC
279
+ `).all(sessionId, status);
280
+ return rows.map(this.rowToMemoryUnit.bind(this));
281
+ }
282
+ // ===========================================================================
283
+ // Event Operations
284
+ // ===========================================================================
285
+ createEvent(sessionId, hookType, content, options) {
286
+ this.ensureInit();
287
+ const event = {
288
+ id: uuidv4(),
289
+ sessionId,
290
+ hookType,
291
+ timestamp: new Date(),
292
+ content,
293
+ toolName: options?.toolName,
294
+ toolInput: options?.toolInput,
295
+ toolOutput: options?.toolOutput,
296
+ metadata: options?.metadata,
297
+ };
298
+ this.db.prepare(`
299
+ INSERT INTO events (id, session_id, hook_type, timestamp, content, tool_name, tool_input, tool_output, metadata)
300
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
301
+ `).run(event.id, event.sessionId, event.hookType, event.timestamp.toISOString(), event.content, event.toolName ?? null, event.toolInput ?? null, event.toolOutput ?? null, event.metadata ? JSON.stringify(event.metadata) : null);
302
+ return event;
303
+ }
304
+ getSessionEvents(sessionId) {
305
+ this.ensureInit();
306
+ const rows = this.db.prepare(`
307
+ SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC
308
+ `).all(sessionId);
309
+ return rows.map(this.rowToEvent);
310
+ }
311
+ getRecentEvents(limit = 100) {
312
+ this.ensureInit();
313
+ const rows = this.db.prepare(`
314
+ SELECT * FROM events ORDER BY timestamp DESC LIMIT ?
315
+ `).all(limit);
316
+ return rows.map(this.rowToEvent);
317
+ }
318
+ // ===========================================================================
319
+ // Memory Unit Operations
320
+ // ===========================================================================
321
+ createMemory(store, classification, summary, sourceEventIds, features = {}) {
322
+ this.ensureInit();
323
+ const now = new Date();
324
+ const decayRate = store === 'stm' ? this.config.stmDecayRate : this.config.ltmDecayRate;
325
+ const memory = {
326
+ id: uuidv4(),
327
+ sessionId: features.sessionId,
328
+ store,
329
+ classification,
330
+ summary,
331
+ sourceEventIds,
332
+ projectScope: features.projectScope,
333
+ createdAt: now,
334
+ updatedAt: now,
335
+ lastAccessedAt: now,
336
+ recency: 0,
337
+ frequency: 1,
338
+ importance: features.importance ?? 0.5,
339
+ utility: features.utility ?? 0.5,
340
+ novelty: features.novelty ?? 0.5,
341
+ confidence: features.confidence ?? 0.5,
342
+ interference: 0,
343
+ strength: this.calculateStrength({
344
+ recency: 0,
345
+ frequency: 1,
346
+ importance: features.importance ?? 0.5,
347
+ utility: features.utility ?? 0.5,
348
+ novelty: features.novelty ?? 0.5,
349
+ confidence: features.confidence ?? 0.5,
350
+ interference: 0,
351
+ }),
352
+ decayRate,
353
+ tags: features.tags ?? [],
354
+ associations: [],
355
+ status: 'active',
356
+ version: 1,
357
+ evidence: [],
358
+ };
359
+ this.db.prepare(`
360
+ INSERT INTO memory_units (
361
+ id, session_id, store, classification, summary, source_event_ids, project_scope,
362
+ created_at, updated_at, last_accessed_at,
363
+ recency, frequency, importance, utility, novelty, confidence, interference,
364
+ strength, decay_rate, tags, associations, status, version
365
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
366
+ `).run(memory.id, memory.sessionId ?? null, memory.store, memory.classification, memory.summary, JSON.stringify(memory.sourceEventIds), memory.projectScope ?? null, memory.createdAt.toISOString(), memory.updatedAt.toISOString(), memory.lastAccessedAt.toISOString(), memory.recency, memory.frequency, memory.importance, memory.utility, memory.novelty, memory.confidence, memory.interference, memory.strength, memory.decayRate, JSON.stringify(memory.tags), JSON.stringify(memory.associations), memory.status, memory.version);
367
+ return memory;
368
+ }
369
+ getMemory(memoryId) {
370
+ this.ensureInit();
371
+ const row = this.db.prepare(`SELECT * FROM memory_units WHERE id = ?`).get(memoryId);
372
+ if (!row)
373
+ return null;
374
+ return this.rowToMemoryUnit(row);
375
+ }
376
+ getMemoriesByStore(store, status = 'active') {
377
+ this.ensureInit();
378
+ const rows = this.db.prepare(`
379
+ SELECT * FROM memory_units
380
+ WHERE store = ? AND status = ?
381
+ ORDER BY strength DESC
382
+ `).all(store, status);
383
+ return rows.map(this.rowToMemoryUnit.bind(this));
384
+ }
385
+ getTopMemories(limit = 20, store) {
386
+ this.ensureInit();
387
+ let query = `SELECT * FROM memory_units WHERE status = 'active'`;
388
+ const params = [];
389
+ if (store) {
390
+ query += ` AND store = ?`;
391
+ params.push(store);
392
+ }
393
+ query += ` ORDER BY strength DESC LIMIT ?`;
394
+ params.push(limit);
395
+ const rows = this.db.prepare(query).all(...params);
396
+ return rows.map(this.rowToMemoryUnit.bind(this));
397
+ }
398
+ /**
399
+ * Get memories filtered by scope for context injection.
400
+ * Returns:
401
+ * - All user-level memories (classification in constraint, preference, learning, procedural)
402
+ * - Project-level memories only if they match the given project
403
+ *
404
+ * @param currentProject - The current project path (used to filter project-level memories)
405
+ * @param limit - Maximum number of memories to return
406
+ * @param store - Optional filter by STM/LTM
407
+ */
408
+ getMemoriesByScope(currentProject, limit = 20, store) {
409
+ this.ensureInit();
410
+ // User-level classifications (always included)
411
+ const userLevelClassifications = ['constraint', 'preference', 'learning', 'procedural'];
412
+ const userClassPlaceholders = userLevelClassifications.map(() => '?').join(', ');
413
+ // Build query: user-level OR (project-level AND matching project)
414
+ let query = `
415
+ SELECT * FROM memory_units
416
+ WHERE status = 'active'
417
+ AND (
418
+ classification IN (${userClassPlaceholders})
419
+ OR (project_scope IS NOT NULL AND project_scope = ?)
420
+ )
421
+ `;
422
+ const params = [...userLevelClassifications, currentProject ?? ''];
423
+ if (store) {
424
+ query += ` AND store = ?`;
425
+ params.push(store);
426
+ }
427
+ query += ` ORDER BY strength DESC LIMIT ?`;
428
+ params.push(limit);
429
+ const rows = this.db.prepare(query).all(...params);
430
+ return rows.map(this.rowToMemoryUnit.bind(this));
431
+ }
432
+ /**
433
+ * Get only user-level memories (always applicable across all projects)
434
+ */
435
+ getUserLevelMemories(limit = 20, store) {
436
+ this.ensureInit();
437
+ const userLevelClassifications = ['constraint', 'preference', 'learning', 'procedural'];
438
+ const placeholders = userLevelClassifications.map(() => '?').join(', ');
439
+ let query = `
440
+ SELECT * FROM memory_units
441
+ WHERE status = 'active'
442
+ AND classification IN (${placeholders})
443
+ `;
444
+ const params = [...userLevelClassifications];
445
+ if (store) {
446
+ query += ` AND store = ?`;
447
+ params.push(store);
448
+ }
449
+ query += ` ORDER BY strength DESC LIMIT ?`;
450
+ params.push(limit);
451
+ const rows = this.db.prepare(query).all(...params);
452
+ return rows.map(this.rowToMemoryUnit.bind(this));
453
+ }
454
+ /**
455
+ * Get project-level memories for a specific project
456
+ */
457
+ getProjectMemories(project, limit = 20, store) {
458
+ this.ensureInit();
459
+ let query = `
460
+ SELECT * FROM memory_units
461
+ WHERE status = 'active'
462
+ AND project_scope = ?
463
+ `;
464
+ const params = [project];
465
+ if (store) {
466
+ query += ` AND store = ?`;
467
+ params.push(store);
468
+ }
469
+ query += ` ORDER BY strength DESC LIMIT ?`;
470
+ params.push(limit);
471
+ const rows = this.db.prepare(query).all(...params);
472
+ return rows.map(this.rowToMemoryUnit.bind(this));
473
+ }
474
+ updateMemoryStrength(memoryId, strength) {
475
+ this.ensureInit();
476
+ this.db.prepare(`
477
+ UPDATE memory_units
478
+ SET strength = ?, updated_at = ?
479
+ WHERE id = ?
480
+ `).run(strength, new Date().toISOString(), memoryId);
481
+ }
482
+ updateMemoryStatus(memoryId, status) {
483
+ this.ensureInit();
484
+ this.db.prepare(`
485
+ UPDATE memory_units
486
+ SET status = ?, updated_at = ?
487
+ WHERE id = ?
488
+ `).run(status, new Date().toISOString(), memoryId);
489
+ }
490
+ incrementFrequency(memoryId) {
491
+ this.ensureInit();
492
+ const now = new Date().toISOString();
493
+ this.db.prepare(`
494
+ UPDATE memory_units
495
+ SET frequency = frequency + 1, last_accessed_at = ?, updated_at = ?
496
+ WHERE id = ?
497
+ `).run(now, now, memoryId);
498
+ }
499
+ promoteToLtm(memoryId) {
500
+ this.ensureInit();
501
+ this.db.prepare(`
502
+ UPDATE memory_units
503
+ SET store = 'ltm', decay_rate = ?, updated_at = ?
504
+ WHERE id = ?
505
+ `).run(this.config.ltmDecayRate, new Date().toISOString(), memoryId);
506
+ }
507
+ // ===========================================================================
508
+ // Retrieval Log Operations
509
+ // ===========================================================================
510
+ logRetrieval(sessionId, memoryId, query, relevanceScore) {
511
+ this.ensureInit();
512
+ const id = uuidv4();
513
+ this.db.prepare(`
514
+ INSERT INTO retrieval_logs (id, session_id, memory_id, query, timestamp, relevance_score)
515
+ VALUES (?, ?, ?, ?, ?, ?)
516
+ `).run(id, sessionId, memoryId, query, new Date().toISOString(), relevanceScore);
517
+ return id;
518
+ }
519
+ markRetrievalUsed(logId, wasUsed) {
520
+ this.ensureInit();
521
+ this.db.prepare(`
522
+ UPDATE retrieval_logs SET was_used = ? WHERE id = ?
523
+ `).run(wasUsed ? 1 : 0, logId);
524
+ }
525
+ addRetrievalFeedback(logId, feedback) {
526
+ this.ensureInit();
527
+ this.db.prepare(`
528
+ UPDATE retrieval_logs SET user_feedback = ? WHERE id = ?
529
+ `).run(feedback, logId);
530
+ }
531
+ // ===========================================================================
532
+ // Feedback Operations
533
+ // ===========================================================================
534
+ addFeedback(type, memoryId, content) {
535
+ this.ensureInit();
536
+ this.db.prepare(`
537
+ INSERT INTO feedback (id, memory_id, type, content, timestamp)
538
+ VALUES (?, ?, ?, ?, ?)
539
+ `).run(uuidv4(), memoryId ?? null, type, content ?? null, new Date().toISOString());
540
+ // Apply feedback immediately
541
+ if (memoryId) {
542
+ switch (type) {
543
+ case 'pin':
544
+ this.updateMemoryStatus(memoryId, 'pinned');
545
+ break;
546
+ case 'forget':
547
+ this.updateMemoryStatus(memoryId, 'forgotten');
548
+ break;
549
+ case 'remember':
550
+ // Boost importance and promote to LTM
551
+ this.db.prepare(`
552
+ UPDATE memory_units
553
+ SET importance = MIN(1.0, importance + 0.3), store = 'ltm', decay_rate = ?
554
+ WHERE id = ?
555
+ `).run(this.config.ltmDecayRate, memoryId);
556
+ break;
557
+ }
558
+ }
559
+ }
560
+ // ===========================================================================
561
+ // Transaction helpers
562
+ // ===========================================================================
563
+ beginTransaction() {
564
+ this.ensureInit();
565
+ if (this._inTransaction)
566
+ return; // already in one — no nested BEGIN
567
+ this.db.exec('BEGIN IMMEDIATE');
568
+ this._inTransaction = true;
569
+ }
570
+ commitTransaction() {
571
+ if (!this._inTransaction)
572
+ return;
573
+ this.db.exec('COMMIT');
574
+ this._inTransaction = false;
575
+ }
576
+ rollbackTransaction() {
577
+ if (!this._inTransaction)
578
+ return;
579
+ try {
580
+ this.db.exec('ROLLBACK');
581
+ }
582
+ catch (_) { /* ignore if already rolled back */ }
583
+ this._inTransaction = false;
584
+ }
585
+ // ===========================================================================
586
+ // Decay and Consolidation
587
+ // ===========================================================================
588
+ /**
589
+ * Apply exponential decay to all active memories
590
+ * strength_t = strength_0 * exp(-lambda * dt)
591
+ */
592
+ applyDecay() {
593
+ this.ensureInit();
594
+ const now = new Date();
595
+ const memories = this.db.prepare(`
596
+ SELECT id, strength, decay_rate, updated_at, status
597
+ FROM memory_units
598
+ WHERE status = 'active'
599
+ `).all();
600
+ let decayedCount = 0;
601
+ const decayThreshold = 0.1; // Below this, mark as decayed
602
+ // Skip starting a new transaction if the caller already opened one
603
+ const ownTransaction = !this._inTransaction;
604
+ if (ownTransaction) {
605
+ this.db.exec('BEGIN IMMEDIATE');
606
+ this._inTransaction = true;
607
+ }
608
+ try {
609
+ for (const mem of memories) {
610
+ const updatedAt = new Date(mem.updated_at);
611
+ const dtHours = (now.getTime() - updatedAt.getTime()) / (1000 * 60 * 60);
612
+ const newStrength = mem.strength * Math.exp(-mem.decay_rate * dtHours);
613
+ if (newStrength < decayThreshold) {
614
+ this.updateMemoryStatus(mem.id, 'decayed');
615
+ decayedCount++;
616
+ }
617
+ else if (newStrength !== mem.strength) {
618
+ this.updateMemoryStrength(mem.id, newStrength);
619
+ }
620
+ }
621
+ if (ownTransaction) {
622
+ this.db.exec('COMMIT');
623
+ this._inTransaction = false;
624
+ }
625
+ }
626
+ catch (err) {
627
+ if (ownTransaction) {
628
+ this.db.exec('ROLLBACK');
629
+ this._inTransaction = false;
630
+ }
631
+ throw err;
632
+ }
633
+ return decayedCount;
634
+ }
635
+ /**
636
+ * Check and promote eligible STM memories to LTM
637
+ */
638
+ runConsolidation() {
639
+ this.ensureInit();
640
+ const stmMemories = this.getMemoriesByStore('stm');
641
+ let promotedCount = 0;
642
+ for (const mem of stmMemories) {
643
+ const shouldPromote = mem.strength >= this.config.stmToLtmStrengthThreshold ||
644
+ mem.frequency >= this.config.stmToLtmFrequencyThreshold ||
645
+ this.config.autoPromoteToLtm.includes(mem.classification);
646
+ if (shouldPromote) {
647
+ this.promoteToLtm(mem.id);
648
+ promotedCount++;
649
+ }
650
+ }
651
+ return promotedCount;
652
+ }
653
+ // ===========================================================================
654
+ // Scoring
655
+ // ===========================================================================
656
+ /**
657
+ * Calculate memory strength from feature vector (rule-based v1)
658
+ */
659
+ calculateStrength(features) {
660
+ const w = this.config.scoringWeights;
661
+ // Normalize frequency (log scale)
662
+ const normalizedFrequency = Math.min(1, Math.log(features.frequency + 1) / Math.log(10));
663
+ // Recency factor (0 = now, 1 = old)
664
+ const recencyFactor = 1 - Math.min(1, features.recency / 168); // 168 hours = 1 week
665
+ const strength = w.recency * recencyFactor +
666
+ w.frequency * normalizedFrequency +
667
+ w.importance * features.importance +
668
+ w.utility * features.utility +
669
+ w.novelty * features.novelty +
670
+ w.confidence * features.confidence +
671
+ w.interference * features.interference; // Negative weight
672
+ return Math.max(0, Math.min(1, strength));
673
+ }
674
+ // ===========================================================================
675
+ // Helpers
676
+ // ===========================================================================
677
+ rowToSession(row) {
678
+ return {
679
+ id: row.id,
680
+ project: row.project,
681
+ startedAt: new Date(row.started_at),
682
+ endedAt: row.ended_at ? new Date(row.ended_at) : undefined,
683
+ status: row.status,
684
+ metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
685
+ transcriptPath: row.transcript_path ?? undefined,
686
+ transcriptWatermark: row.transcript_watermark ?? 0,
687
+ messageWatermark: row.message_watermark ?? 0,
688
+ };
689
+ }
690
+ rowToEvent(row) {
691
+ return {
692
+ id: row.id,
693
+ sessionId: row.session_id,
694
+ hookType: row.hook_type,
695
+ timestamp: new Date(row.timestamp),
696
+ content: row.content,
697
+ toolName: row.tool_name ?? undefined,
698
+ toolInput: row.tool_input ?? undefined,
699
+ toolOutput: row.tool_output ?? undefined,
700
+ metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
701
+ };
702
+ }
703
+ rowToMemoryUnit(row) {
704
+ return {
705
+ id: row.id,
706
+ sessionId: row.session_id ?? undefined,
707
+ store: row.store,
708
+ classification: row.classification,
709
+ summary: row.summary,
710
+ sourceEventIds: JSON.parse(row.source_event_ids),
711
+ projectScope: row.project_scope ?? undefined,
712
+ createdAt: new Date(row.created_at),
713
+ updatedAt: new Date(row.updated_at),
714
+ lastAccessedAt: new Date(row.last_accessed_at),
715
+ recency: row.recency,
716
+ frequency: row.frequency,
717
+ importance: row.importance,
718
+ utility: row.utility,
719
+ novelty: row.novelty,
720
+ confidence: row.confidence,
721
+ interference: row.interference,
722
+ strength: row.strength,
723
+ decayRate: row.decay_rate,
724
+ tags: row.tags ? JSON.parse(row.tags) : [],
725
+ associations: row.associations ? JSON.parse(row.associations) : [],
726
+ status: row.status,
727
+ version: row.version,
728
+ evidence: [], // Loaded separately if needed
729
+ };
730
+ }
731
+ /**
732
+ * Close database connection
733
+ */
734
+ close() {
735
+ if (this.db) {
736
+ this.db.close();
737
+ }
738
+ }
739
+ }
740
+ /**
741
+ * Create and initialize a MemoryDatabase instance
742
+ */
743
+ export async function createMemoryDatabase(config = {}) {
744
+ const db = new MemoryDatabase(config);
745
+ await db.init();
746
+ return db;
747
+ }
748
+ //# sourceMappingURL=database.js.map