agent-working-memory 0.3.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 (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +311 -0
  3. package/dist/api/index.d.ts +2 -0
  4. package/dist/api/index.d.ts.map +1 -0
  5. package/dist/api/index.js +2 -0
  6. package/dist/api/index.js.map +1 -0
  7. package/dist/api/routes.d.ts +53 -0
  8. package/dist/api/routes.d.ts.map +1 -0
  9. package/dist/api/routes.js +388 -0
  10. package/dist/api/routes.js.map +1 -0
  11. package/dist/cli.d.ts +12 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +245 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/core/decay.d.ts +36 -0
  16. package/dist/core/decay.d.ts.map +1 -0
  17. package/dist/core/decay.js +38 -0
  18. package/dist/core/decay.js.map +1 -0
  19. package/dist/core/embeddings.d.ts +33 -0
  20. package/dist/core/embeddings.d.ts.map +1 -0
  21. package/dist/core/embeddings.js +76 -0
  22. package/dist/core/embeddings.js.map +1 -0
  23. package/dist/core/hebbian.d.ts +38 -0
  24. package/dist/core/hebbian.d.ts.map +1 -0
  25. package/dist/core/hebbian.js +74 -0
  26. package/dist/core/hebbian.js.map +1 -0
  27. package/dist/core/index.d.ts +4 -0
  28. package/dist/core/index.d.ts.map +1 -0
  29. package/dist/core/index.js +4 -0
  30. package/dist/core/index.js.map +1 -0
  31. package/dist/core/query-expander.d.ts +24 -0
  32. package/dist/core/query-expander.d.ts.map +1 -0
  33. package/dist/core/query-expander.js +58 -0
  34. package/dist/core/query-expander.js.map +1 -0
  35. package/dist/core/reranker.d.ts +25 -0
  36. package/dist/core/reranker.d.ts.map +1 -0
  37. package/dist/core/reranker.js +75 -0
  38. package/dist/core/reranker.js.map +1 -0
  39. package/dist/core/salience.d.ts +30 -0
  40. package/dist/core/salience.d.ts.map +1 -0
  41. package/dist/core/salience.js +81 -0
  42. package/dist/core/salience.js.map +1 -0
  43. package/dist/engine/activation.d.ts +38 -0
  44. package/dist/engine/activation.d.ts.map +1 -0
  45. package/dist/engine/activation.js +516 -0
  46. package/dist/engine/activation.js.map +1 -0
  47. package/dist/engine/connections.d.ts +31 -0
  48. package/dist/engine/connections.d.ts.map +1 -0
  49. package/dist/engine/connections.js +74 -0
  50. package/dist/engine/connections.js.map +1 -0
  51. package/dist/engine/consolidation-scheduler.d.ts +31 -0
  52. package/dist/engine/consolidation-scheduler.d.ts.map +1 -0
  53. package/dist/engine/consolidation-scheduler.js +115 -0
  54. package/dist/engine/consolidation-scheduler.js.map +1 -0
  55. package/dist/engine/consolidation.d.ts +62 -0
  56. package/dist/engine/consolidation.d.ts.map +1 -0
  57. package/dist/engine/consolidation.js +368 -0
  58. package/dist/engine/consolidation.js.map +1 -0
  59. package/dist/engine/eval.d.ts +22 -0
  60. package/dist/engine/eval.d.ts.map +1 -0
  61. package/dist/engine/eval.js +79 -0
  62. package/dist/engine/eval.js.map +1 -0
  63. package/dist/engine/eviction.d.ts +29 -0
  64. package/dist/engine/eviction.d.ts.map +1 -0
  65. package/dist/engine/eviction.js +86 -0
  66. package/dist/engine/eviction.js.map +1 -0
  67. package/dist/engine/index.d.ts +7 -0
  68. package/dist/engine/index.d.ts.map +1 -0
  69. package/dist/engine/index.js +7 -0
  70. package/dist/engine/index.js.map +1 -0
  71. package/dist/engine/retraction.d.ts +32 -0
  72. package/dist/engine/retraction.d.ts.map +1 -0
  73. package/dist/engine/retraction.js +77 -0
  74. package/dist/engine/retraction.js.map +1 -0
  75. package/dist/engine/staging.d.ts +33 -0
  76. package/dist/engine/staging.d.ts.map +1 -0
  77. package/dist/engine/staging.js +63 -0
  78. package/dist/engine/staging.js.map +1 -0
  79. package/dist/index.d.ts +2 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +95 -0
  82. package/dist/index.js.map +1 -0
  83. package/dist/mcp.d.ts +24 -0
  84. package/dist/mcp.d.ts.map +1 -0
  85. package/dist/mcp.js +532 -0
  86. package/dist/mcp.js.map +1 -0
  87. package/dist/storage/index.d.ts +2 -0
  88. package/dist/storage/index.d.ts.map +1 -0
  89. package/dist/storage/index.js +2 -0
  90. package/dist/storage/index.js.map +1 -0
  91. package/dist/storage/sqlite.d.ts +116 -0
  92. package/dist/storage/sqlite.d.ts.map +1 -0
  93. package/dist/storage/sqlite.js +750 -0
  94. package/dist/storage/sqlite.js.map +1 -0
  95. package/dist/types/agent.d.ts +30 -0
  96. package/dist/types/agent.d.ts.map +1 -0
  97. package/dist/types/agent.js +23 -0
  98. package/dist/types/agent.js.map +1 -0
  99. package/dist/types/checkpoint.d.ts +50 -0
  100. package/dist/types/checkpoint.d.ts.map +1 -0
  101. package/dist/types/checkpoint.js +8 -0
  102. package/dist/types/checkpoint.js.map +1 -0
  103. package/dist/types/engram.d.ts +165 -0
  104. package/dist/types/engram.d.ts.map +1 -0
  105. package/dist/types/engram.js +8 -0
  106. package/dist/types/engram.js.map +1 -0
  107. package/dist/types/eval.d.ts +84 -0
  108. package/dist/types/eval.d.ts.map +1 -0
  109. package/dist/types/eval.js +11 -0
  110. package/dist/types/eval.js.map +1 -0
  111. package/dist/types/index.d.ts +5 -0
  112. package/dist/types/index.d.ts.map +1 -0
  113. package/dist/types/index.js +5 -0
  114. package/dist/types/index.js.map +1 -0
  115. package/package.json +55 -0
  116. package/src/api/index.ts +1 -0
  117. package/src/api/routes.ts +528 -0
  118. package/src/cli.ts +260 -0
  119. package/src/core/decay.ts +61 -0
  120. package/src/core/embeddings.ts +82 -0
  121. package/src/core/hebbian.ts +91 -0
  122. package/src/core/index.ts +3 -0
  123. package/src/core/query-expander.ts +64 -0
  124. package/src/core/reranker.ts +99 -0
  125. package/src/core/salience.ts +95 -0
  126. package/src/engine/activation.ts +577 -0
  127. package/src/engine/connections.ts +101 -0
  128. package/src/engine/consolidation-scheduler.ts +123 -0
  129. package/src/engine/consolidation.ts +443 -0
  130. package/src/engine/eval.ts +100 -0
  131. package/src/engine/eviction.ts +99 -0
  132. package/src/engine/index.ts +6 -0
  133. package/src/engine/retraction.ts +98 -0
  134. package/src/engine/staging.ts +72 -0
  135. package/src/index.ts +100 -0
  136. package/src/mcp.ts +635 -0
  137. package/src/storage/index.ts +1 -0
  138. package/src/storage/sqlite.ts +893 -0
  139. package/src/types/agent.ts +65 -0
  140. package/src/types/checkpoint.ts +44 -0
  141. package/src/types/engram.ts +194 -0
  142. package/src/types/eval.ts +98 -0
  143. package/src/types/index.ts +4 -0
@@ -0,0 +1,750 @@
1
+ /**
2
+ * SQLite storage layer — persistence for engrams, associations, and eval events.
3
+ *
4
+ * Uses better-sqlite3 for synchronous, fast, embedded storage.
5
+ * FTS5 provides BM25 full-text search for the activation pipeline.
6
+ */
7
+ import Database from 'better-sqlite3';
8
+ import { randomUUID } from 'node:crypto';
9
+ const DEFAULT_SALIENCE_FEATURES = {
10
+ surprise: 0, decisionMade: false, causalDepth: 0, resolutionEffort: 0, eventType: 'observation',
11
+ };
12
+ export class EngramStore {
13
+ db;
14
+ constructor(dbPath = 'memory.db') {
15
+ this.db = new Database(dbPath);
16
+ this.db.pragma('journal_mode = WAL');
17
+ this.db.pragma('foreign_keys = ON');
18
+ this.init();
19
+ }
20
+ init() {
21
+ this.db.exec(`
22
+ CREATE TABLE IF NOT EXISTS engrams (
23
+ id TEXT PRIMARY KEY,
24
+ agent_id TEXT NOT NULL,
25
+ concept TEXT NOT NULL,
26
+ content TEXT NOT NULL,
27
+ embedding BLOB,
28
+ confidence REAL NOT NULL DEFAULT 0.5,
29
+ salience REAL NOT NULL DEFAULT 0.5,
30
+ access_count INTEGER NOT NULL DEFAULT 0,
31
+ last_accessed TEXT NOT NULL,
32
+ created_at TEXT NOT NULL,
33
+ salience_features TEXT NOT NULL DEFAULT '{}',
34
+ reason_codes TEXT NOT NULL DEFAULT '[]',
35
+ stage TEXT NOT NULL DEFAULT 'active',
36
+ ttl INTEGER,
37
+ retracted INTEGER NOT NULL DEFAULT 0,
38
+ retracted_by TEXT,
39
+ retracted_at TEXT,
40
+ tags TEXT NOT NULL DEFAULT '[]'
41
+ );
42
+
43
+ CREATE INDEX IF NOT EXISTS idx_engrams_agent ON engrams(agent_id);
44
+ CREATE INDEX IF NOT EXISTS idx_engrams_stage ON engrams(agent_id, stage);
45
+ CREATE INDEX IF NOT EXISTS idx_engrams_concept ON engrams(concept);
46
+ CREATE INDEX IF NOT EXISTS idx_engrams_retracted ON engrams(agent_id, retracted);
47
+
48
+ CREATE TABLE IF NOT EXISTS associations (
49
+ id TEXT PRIMARY KEY,
50
+ from_engram_id TEXT NOT NULL REFERENCES engrams(id) ON DELETE CASCADE,
51
+ to_engram_id TEXT NOT NULL REFERENCES engrams(id) ON DELETE CASCADE,
52
+ weight REAL NOT NULL DEFAULT 0.1,
53
+ confidence REAL NOT NULL DEFAULT 0.5,
54
+ type TEXT NOT NULL DEFAULT 'hebbian',
55
+ activation_count INTEGER NOT NULL DEFAULT 0,
56
+ created_at TEXT NOT NULL,
57
+ last_activated TEXT NOT NULL
58
+ );
59
+
60
+ CREATE INDEX IF NOT EXISTS idx_assoc_from ON associations(from_engram_id);
61
+ CREATE INDEX IF NOT EXISTS idx_assoc_to ON associations(to_engram_id);
62
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_assoc_pair ON associations(from_engram_id, to_engram_id);
63
+
64
+ CREATE TABLE IF NOT EXISTS agents (
65
+ id TEXT PRIMARY KEY,
66
+ name TEXT NOT NULL,
67
+ created_at TEXT NOT NULL,
68
+ config TEXT NOT NULL DEFAULT '{}'
69
+ );
70
+
71
+ -- FTS5 for full-text search (BM25 ranking built in)
72
+ CREATE VIRTUAL TABLE IF NOT EXISTS engrams_fts USING fts5(
73
+ concept, content, tags,
74
+ content=engrams,
75
+ content_rowid=rowid
76
+ );
77
+
78
+ -- Triggers to keep FTS in sync
79
+ CREATE TRIGGER IF NOT EXISTS engrams_ai AFTER INSERT ON engrams BEGIN
80
+ INSERT INTO engrams_fts(rowid, concept, content, tags) VALUES (new.rowid, new.concept, new.content, new.tags);
81
+ END;
82
+ CREATE TRIGGER IF NOT EXISTS engrams_ad AFTER DELETE ON engrams BEGIN
83
+ INSERT INTO engrams_fts(engrams_fts, rowid, concept, content, tags) VALUES('delete', old.rowid, old.concept, old.content, old.tags);
84
+ END;
85
+ CREATE TRIGGER IF NOT EXISTS engrams_au AFTER UPDATE ON engrams BEGIN
86
+ INSERT INTO engrams_fts(engrams_fts, rowid, concept, content, tags) VALUES('delete', old.rowid, old.concept, old.content, old.tags);
87
+ INSERT INTO engrams_fts(rowid, concept, content, tags) VALUES (new.rowid, new.concept, new.content, new.tags);
88
+ END;
89
+
90
+ -- Eval event logs
91
+ CREATE TABLE IF NOT EXISTS activation_events (
92
+ id TEXT PRIMARY KEY,
93
+ agent_id TEXT NOT NULL,
94
+ timestamp TEXT NOT NULL,
95
+ context TEXT NOT NULL,
96
+ results_returned INTEGER NOT NULL,
97
+ top_score REAL,
98
+ latency_ms REAL NOT NULL,
99
+ engram_ids TEXT NOT NULL DEFAULT '[]'
100
+ );
101
+
102
+ CREATE TABLE IF NOT EXISTS staging_events (
103
+ engram_id TEXT NOT NULL,
104
+ agent_id TEXT NOT NULL,
105
+ action TEXT NOT NULL,
106
+ resonance_score REAL,
107
+ timestamp TEXT NOT NULL,
108
+ age_ms INTEGER NOT NULL
109
+ );
110
+
111
+ CREATE TABLE IF NOT EXISTS retrieval_feedback (
112
+ id TEXT PRIMARY KEY,
113
+ activation_event_id TEXT,
114
+ engram_id TEXT NOT NULL,
115
+ useful INTEGER NOT NULL,
116
+ context TEXT,
117
+ timestamp TEXT NOT NULL
118
+ );
119
+
120
+ CREATE TABLE IF NOT EXISTS episodes (
121
+ id TEXT PRIMARY KEY,
122
+ agent_id TEXT NOT NULL,
123
+ label TEXT NOT NULL,
124
+ embedding BLOB,
125
+ engram_count INTEGER NOT NULL DEFAULT 0,
126
+ start_time TEXT NOT NULL,
127
+ end_time TEXT NOT NULL,
128
+ created_at TEXT NOT NULL
129
+ );
130
+
131
+ CREATE INDEX IF NOT EXISTS idx_episodes_agent ON episodes(agent_id);
132
+ CREATE INDEX IF NOT EXISTS idx_episodes_time ON episodes(agent_id, end_time);
133
+ `);
134
+ // Migration: add episode_id column if missing
135
+ try {
136
+ this.db.prepare('SELECT episode_id FROM engrams LIMIT 0').get();
137
+ }
138
+ catch {
139
+ this.db.exec('ALTER TABLE engrams ADD COLUMN episode_id TEXT');
140
+ }
141
+ // Migration: add task management columns if missing
142
+ try {
143
+ this.db.prepare('SELECT task_status FROM engrams LIMIT 0').get();
144
+ }
145
+ catch {
146
+ this.db.exec(`
147
+ ALTER TABLE engrams ADD COLUMN task_status TEXT;
148
+ ALTER TABLE engrams ADD COLUMN task_priority TEXT;
149
+ ALTER TABLE engrams ADD COLUMN blocked_by TEXT;
150
+ `);
151
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_engrams_task ON engrams(agent_id, task_status)');
152
+ }
153
+ // Migration: add conscious_state table for checkpointing
154
+ this.db.exec(`
155
+ CREATE TABLE IF NOT EXISTS conscious_state (
156
+ agent_id TEXT PRIMARY KEY,
157
+ last_write_id TEXT,
158
+ last_recall_context TEXT,
159
+ last_recall_ids TEXT NOT NULL DEFAULT '[]',
160
+ last_activity_at TEXT NOT NULL DEFAULT (datetime('now')),
161
+ write_count_since_consolidation INTEGER NOT NULL DEFAULT 0,
162
+ recall_count_since_consolidation INTEGER NOT NULL DEFAULT 0,
163
+ execution_state TEXT,
164
+ checkpoint_at TEXT,
165
+ last_consolidation_at TEXT,
166
+ last_mini_consolidation_at TEXT,
167
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
168
+ )
169
+ `);
170
+ }
171
+ // --- Engram CRUD ---
172
+ createEngram(input) {
173
+ const now = new Date().toISOString();
174
+ const id = randomUUID();
175
+ const embeddingBlob = input.embedding
176
+ ? Buffer.from(new Float32Array(input.embedding).buffer)
177
+ : null;
178
+ this.db.prepare(`
179
+ INSERT INTO engrams (id, agent_id, concept, content, embedding, confidence, salience,
180
+ access_count, last_accessed, created_at, salience_features, reason_codes, stage, tags, episode_id,
181
+ ttl, task_status, task_priority, blocked_by)
182
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?)
183
+ `).run(id, input.agentId, input.concept, input.content, embeddingBlob, input.confidence ?? 0.5, input.salience ?? 0.5, now, now, JSON.stringify(input.salienceFeatures ?? DEFAULT_SALIENCE_FEATURES), JSON.stringify(input.reasonCodes ?? []), JSON.stringify(input.tags ?? []), input.episodeId ?? null, input.ttl ?? null, input.taskStatus ?? null, input.taskPriority ?? null, input.blockedBy ?? null);
184
+ return this.getEngram(id);
185
+ }
186
+ getEngram(id) {
187
+ const row = this.db.prepare('SELECT * FROM engrams WHERE id = ?').get(id);
188
+ return row ? this.rowToEngram(row) : null;
189
+ }
190
+ getEngramsByAgent(agentId, stage, includeRetracted = false) {
191
+ let query = 'SELECT * FROM engrams WHERE agent_id = ?';
192
+ const params = [agentId];
193
+ if (stage) {
194
+ query += ' AND stage = ?';
195
+ params.push(stage);
196
+ }
197
+ if (!includeRetracted) {
198
+ query += ' AND retracted = 0';
199
+ }
200
+ return this.db.prepare(query).all(...params).map(r => this.rowToEngram(r));
201
+ }
202
+ touchEngram(id) {
203
+ this.db.prepare(`
204
+ UPDATE engrams SET access_count = access_count + 1, last_accessed = ? WHERE id = ?
205
+ `).run(new Date().toISOString(), id);
206
+ }
207
+ updateStage(id, stage) {
208
+ this.db.prepare('UPDATE engrams SET stage = ? WHERE id = ?').run(stage, id);
209
+ }
210
+ updateConfidence(id, confidence) {
211
+ this.db.prepare('UPDATE engrams SET confidence = ? WHERE id = ?').run(Math.max(0, Math.min(1, confidence)), id);
212
+ }
213
+ updateEmbedding(id, embedding) {
214
+ const blob = Buffer.from(new Float32Array(embedding).buffer);
215
+ this.db.prepare('UPDATE engrams SET embedding = ? WHERE id = ?').run(blob, id);
216
+ }
217
+ retractEngram(id, retractedBy) {
218
+ this.db.prepare(`
219
+ UPDATE engrams SET retracted = 1, retracted_by = ?, retracted_at = ? WHERE id = ?
220
+ `).run(retractedBy, new Date().toISOString(), id);
221
+ }
222
+ deleteEngram(id) {
223
+ this.db.prepare('DELETE FROM engrams WHERE id = ?').run(id);
224
+ }
225
+ /**
226
+ * Time warp — shift all timestamps backward by ms milliseconds.
227
+ * Used for testing time-dependent behavior (decay, forgetting).
228
+ * Returns count of records shifted.
229
+ */
230
+ timeWarp(agentId, ms) {
231
+ let count = 0;
232
+ const shiftSec = Math.round(ms / 1000);
233
+ // Shift engram timestamps
234
+ const r1 = this.db.prepare(`
235
+ UPDATE engrams SET
236
+ created_at = datetime(created_at, '-${shiftSec} seconds'),
237
+ last_accessed = datetime(last_accessed, '-${shiftSec} seconds')
238
+ WHERE agent_id = ?
239
+ `).run(agentId);
240
+ count += r1.changes;
241
+ // Shift association timestamps
242
+ const r2 = this.db.prepare(`
243
+ UPDATE associations SET
244
+ created_at = datetime(created_at, '-${shiftSec} seconds'),
245
+ last_activated = datetime(last_activated, '-${shiftSec} seconds')
246
+ WHERE from_engram_id IN (SELECT id FROM engrams WHERE agent_id = ?)
247
+ OR to_engram_id IN (SELECT id FROM engrams WHERE agent_id = ?)
248
+ `).run(agentId, agentId);
249
+ count += r2.changes;
250
+ return count;
251
+ }
252
+ // --- Full-text search (BM25) ---
253
+ searchBM25(agentId, query, limit = 10) {
254
+ return this.searchBM25WithRank(agentId, query, limit).map(r => r.engram);
255
+ }
256
+ /**
257
+ * BM25 search returning rank scores alongside engrams.
258
+ * FTS5 rank is negative (lower = better match).
259
+ * We normalize to 0-1 where higher = better.
260
+ */
261
+ searchBM25WithRank(agentId, query, limit = 10) {
262
+ // Sanitize query for FTS5: quote each word to prevent column name interpretation
263
+ const sanitized = query
264
+ .replace(/[^\w\s]/g, '')
265
+ .split(/\s+/)
266
+ .filter(w => w.length > 1)
267
+ .map(w => `"${w}"`)
268
+ .join(' OR ');
269
+ if (!sanitized)
270
+ return [];
271
+ try {
272
+ const rows = this.db.prepare(`
273
+ SELECT e.*, rank FROM engrams e
274
+ JOIN engrams_fts ON e.rowid = engrams_fts.rowid
275
+ WHERE engrams_fts MATCH ? AND e.agent_id = ? AND e.retracted = 0
276
+ ORDER BY rank
277
+ LIMIT ?
278
+ `).all(sanitized, agentId, limit);
279
+ return rows.map(r => ({
280
+ engram: this.rowToEngram(r),
281
+ // Normalize: rank is negative, more negative = better match.
282
+ // |rank| / (1 + |rank|) gives 0-1 where higher = better.
283
+ bm25Score: Math.abs(r.rank ?? 0) / (1 + Math.abs(r.rank ?? 0)),
284
+ }));
285
+ }
286
+ catch {
287
+ return [];
288
+ }
289
+ }
290
+ // --- Diagnostic search (deterministic, not cognitive) ---
291
+ search(query) {
292
+ let sql = 'SELECT * FROM engrams WHERE agent_id = ?';
293
+ const params = [query.agentId];
294
+ if (query.text) {
295
+ sql += ' AND (content LIKE ? OR concept LIKE ?)';
296
+ params.push(`%${query.text}%`, `%${query.text}%`);
297
+ }
298
+ if (query.concept) {
299
+ sql += ' AND concept = ?';
300
+ params.push(query.concept);
301
+ }
302
+ if (query.stage) {
303
+ sql += ' AND stage = ?';
304
+ params.push(query.stage);
305
+ }
306
+ if (query.retracted !== undefined) {
307
+ sql += ' AND retracted = ?';
308
+ params.push(query.retracted ? 1 : 0);
309
+ }
310
+ if (query.tags && query.tags.length > 0) {
311
+ for (const tag of query.tags) {
312
+ sql += ' AND tags LIKE ?';
313
+ params.push(`%"${tag}"%`);
314
+ }
315
+ }
316
+ sql += ' ORDER BY last_accessed DESC';
317
+ sql += ` LIMIT ? OFFSET ?`;
318
+ params.push(query.limit ?? 50, query.offset ?? 0);
319
+ return this.db.prepare(sql).all(...params).map(r => this.rowToEngram(r));
320
+ }
321
+ /**
322
+ * Get the most recently created engram for an agent (for temporal adjacency edges).
323
+ */
324
+ getLatestEngram(agentId, excludeId) {
325
+ let sql = 'SELECT * FROM engrams WHERE agent_id = ? AND retracted = 0';
326
+ const params = [agentId];
327
+ if (excludeId) {
328
+ sql += ' AND id != ?';
329
+ params.push(excludeId);
330
+ }
331
+ sql += ' ORDER BY created_at DESC LIMIT 1';
332
+ const row = this.db.prepare(sql).get(...params);
333
+ return row ? this.rowToEngram(row) : null;
334
+ }
335
+ // --- Task management ---
336
+ updateTaskStatus(id, status) {
337
+ this.db.prepare('UPDATE engrams SET task_status = ? WHERE id = ?').run(status, id);
338
+ }
339
+ updateTaskPriority(id, priority) {
340
+ this.db.prepare('UPDATE engrams SET task_priority = ? WHERE id = ?').run(priority, id);
341
+ }
342
+ updateBlockedBy(id, blockedBy) {
343
+ this.db.prepare('UPDATE engrams SET blocked_by = ?, task_status = ? WHERE id = ?')
344
+ .run(blockedBy, blockedBy ? 'blocked' : 'open', id);
345
+ }
346
+ /**
347
+ * Get tasks for an agent, optionally filtered by status.
348
+ * Results ordered by priority (urgent > high > medium > low), then creation date.
349
+ */
350
+ getTasks(agentId, status) {
351
+ let sql = 'SELECT * FROM engrams WHERE agent_id = ? AND task_status IS NOT NULL AND retracted = 0';
352
+ const params = [agentId];
353
+ if (status) {
354
+ sql += ' AND task_status = ?';
355
+ params.push(status);
356
+ }
357
+ sql += ` ORDER BY
358
+ CASE task_priority
359
+ WHEN 'urgent' THEN 0
360
+ WHEN 'high' THEN 1
361
+ WHEN 'medium' THEN 2
362
+ WHEN 'low' THEN 3
363
+ ELSE 4
364
+ END,
365
+ created_at DESC`;
366
+ return this.db.prepare(sql).all(...params).map(r => this.rowToEngram(r));
367
+ }
368
+ /**
369
+ * Get the next actionable task — highest priority that's not blocked or done.
370
+ */
371
+ getNextTask(agentId) {
372
+ const row = this.db.prepare(`
373
+ SELECT * FROM engrams
374
+ WHERE agent_id = ? AND task_status IN ('open', 'in_progress') AND retracted = 0
375
+ ORDER BY
376
+ CASE task_status WHEN 'in_progress' THEN 0 ELSE 1 END,
377
+ CASE task_priority
378
+ WHEN 'urgent' THEN 0
379
+ WHEN 'high' THEN 1
380
+ WHEN 'medium' THEN 2
381
+ WHEN 'low' THEN 3
382
+ ELSE 4
383
+ END,
384
+ created_at ASC
385
+ LIMIT 1
386
+ `).get(agentId);
387
+ return row ? this.rowToEngram(row) : null;
388
+ }
389
+ // --- Associations ---
390
+ upsertAssociation(fromId, toId, weight, type = 'hebbian', confidence = 0.5) {
391
+ const now = new Date().toISOString();
392
+ const id = randomUUID();
393
+ this.db.prepare(`
394
+ INSERT INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type, activation_count, created_at, last_activated)
395
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
396
+ ON CONFLICT(from_engram_id, to_engram_id) DO UPDATE SET
397
+ weight = ?, confidence = ?, last_activated = ?, activation_count = activation_count + 1
398
+ `).run(id, fromId, toId, weight, confidence, type, now, now, weight, confidence, now);
399
+ return this.getAssociation(fromId, toId);
400
+ }
401
+ getAssociation(fromId, toId) {
402
+ const row = this.db.prepare('SELECT * FROM associations WHERE from_engram_id = ? AND to_engram_id = ?').get(fromId, toId);
403
+ return row ? this.rowToAssociation(row) : null;
404
+ }
405
+ getAssociationsFor(engramId) {
406
+ const rows = this.db.prepare('SELECT * FROM associations WHERE from_engram_id = ? OR to_engram_id = ?').all(engramId, engramId);
407
+ return rows.map(r => this.rowToAssociation(r));
408
+ }
409
+ countAssociationsFor(engramId) {
410
+ const row = this.db.prepare('SELECT COUNT(*) as count FROM associations WHERE from_engram_id = ?').get(engramId);
411
+ return row.count;
412
+ }
413
+ getWeakestAssociation(engramId) {
414
+ const row = this.db.prepare('SELECT * FROM associations WHERE from_engram_id = ? ORDER BY weight ASC LIMIT 1').get(engramId);
415
+ return row ? this.rowToAssociation(row) : null;
416
+ }
417
+ deleteAssociation(id) {
418
+ this.db.prepare('DELETE FROM associations WHERE id = ?').run(id);
419
+ }
420
+ getAllAssociations(agentId) {
421
+ const rows = this.db.prepare(`
422
+ SELECT a.* FROM associations a
423
+ JOIN engrams e ON a.from_engram_id = e.id
424
+ WHERE e.agent_id = ?
425
+ `).all(agentId);
426
+ return rows.map(r => this.rowToAssociation(r));
427
+ }
428
+ // --- Eviction ---
429
+ getEvictionCandidates(agentId, limit) {
430
+ // Lowest combined score: low salience + low access + low confidence + oldest
431
+ const rows = this.db.prepare(`
432
+ SELECT * FROM engrams
433
+ WHERE agent_id = ? AND stage = 'active' AND retracted = 0
434
+ ORDER BY (salience * 0.3 + confidence * 0.3 + (CAST(access_count AS REAL) / (access_count + 5)) * 0.2 +
435
+ (1.0 / (1.0 + (julianday('now') - julianday(last_accessed)))) * 0.2) ASC
436
+ LIMIT ?
437
+ `).all(agentId, limit);
438
+ return rows.map(r => this.rowToEngram(r));
439
+ }
440
+ getActiveCount(agentId) {
441
+ const row = this.db.prepare("SELECT COUNT(*) as count FROM engrams WHERE agent_id = ? AND stage = 'active'").get(agentId);
442
+ return row.count;
443
+ }
444
+ getStagingCount(agentId) {
445
+ const row = this.db.prepare("SELECT COUNT(*) as count FROM engrams WHERE agent_id = ? AND stage = 'staging'").get(agentId);
446
+ return row.count;
447
+ }
448
+ // --- Staging buffer ---
449
+ getExpiredStaging() {
450
+ const now = Date.now();
451
+ const rows = this.db.prepare(`
452
+ SELECT * FROM engrams WHERE stage = 'staging' AND ttl IS NOT NULL
453
+ `).all();
454
+ return rows
455
+ .map(r => this.rowToEngram(r))
456
+ .filter(e => e.ttl && (e.createdAt.getTime() + e.ttl) < now);
457
+ }
458
+ // --- Eval event logging ---
459
+ logActivationEvent(event) {
460
+ this.db.prepare(`
461
+ INSERT INTO activation_events (id, agent_id, timestamp, context, results_returned, top_score, latency_ms, engram_ids)
462
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
463
+ `).run(event.id, event.agentId, event.timestamp.toISOString(), event.context, event.resultsReturned, event.topScore, event.latencyMs, JSON.stringify(event.engramIds));
464
+ }
465
+ logStagingEvent(event) {
466
+ this.db.prepare(`
467
+ INSERT INTO staging_events (engram_id, agent_id, action, resonance_score, timestamp, age_ms)
468
+ VALUES (?, ?, ?, ?, ?, ?)
469
+ `).run(event.engramId, event.agentId, event.action, event.resonanceScore, event.timestamp.toISOString(), event.ageMs);
470
+ }
471
+ logRetrievalFeedback(activationEventId, engramId, useful, context) {
472
+ this.db.prepare(`
473
+ INSERT INTO retrieval_feedback (id, activation_event_id, engram_id, useful, context, timestamp)
474
+ VALUES (?, ?, ?, ?, ?, ?)
475
+ `).run(randomUUID(), activationEventId, engramId, useful ? 1 : 0, context, new Date().toISOString());
476
+ }
477
+ // --- Eval metrics queries ---
478
+ getRetrievalPrecision(agentId, windowHours = 24) {
479
+ const since = new Date(Date.now() - windowHours * 3600_000).toISOString();
480
+ const row = this.db.prepare(`
481
+ SELECT
482
+ COUNT(CASE WHEN useful = 1 THEN 1 END) as useful_count,
483
+ COUNT(*) as total_count
484
+ FROM retrieval_feedback rf
485
+ LEFT JOIN activation_events ae ON rf.activation_event_id = ae.id
486
+ JOIN engrams e ON rf.engram_id = e.id
487
+ WHERE e.agent_id = ? AND rf.timestamp > ?
488
+ `).get(agentId, since);
489
+ return row.total_count > 0 ? row.useful_count / row.total_count : 0;
490
+ }
491
+ getStagingMetrics(agentId) {
492
+ const row = this.db.prepare(`
493
+ SELECT
494
+ COUNT(CASE WHEN action = 'promoted' THEN 1 END) as promoted,
495
+ COUNT(CASE WHEN action = 'discarded' THEN 1 END) as discarded,
496
+ COUNT(CASE WHEN action = 'expired' THEN 1 END) as expired
497
+ FROM staging_events WHERE agent_id = ?
498
+ `).get(agentId);
499
+ return { promoted: row.promoted, discarded: row.discarded, expired: row.expired };
500
+ }
501
+ getActivationStats(agentId, windowHours = 24) {
502
+ const since = new Date(Date.now() - windowHours * 3600_000).toISOString();
503
+ const rows = this.db.prepare(`
504
+ SELECT latency_ms FROM activation_events
505
+ WHERE agent_id = ? AND timestamp > ?
506
+ ORDER BY latency_ms ASC
507
+ `).all(agentId, since);
508
+ if (rows.length === 0)
509
+ return { count: 0, avgLatencyMs: 0, p95LatencyMs: 0 };
510
+ const total = rows.reduce((s, r) => s + r.latency_ms, 0);
511
+ const p95Index = Math.min(Math.floor(rows.length * 0.95), rows.length - 1);
512
+ return {
513
+ count: rows.length,
514
+ avgLatencyMs: total / rows.length,
515
+ p95LatencyMs: rows[p95Index].latency_ms,
516
+ };
517
+ }
518
+ getConsolidatedCount(agentId) {
519
+ const row = this.db.prepare(`SELECT COUNT(*) as cnt FROM engrams WHERE agent_id = ? AND stage = 'consolidated'`).get(agentId);
520
+ return row.cnt;
521
+ }
522
+ // --- Helpers ---
523
+ rowToEngram(row) {
524
+ return {
525
+ id: row.id,
526
+ agentId: row.agent_id,
527
+ concept: row.concept,
528
+ content: row.content,
529
+ embedding: row.embedding
530
+ ? Array.from(new Float32Array(row.embedding.buffer ?? row.embedding))
531
+ : null,
532
+ confidence: row.confidence,
533
+ salience: row.salience,
534
+ accessCount: row.access_count,
535
+ lastAccessed: new Date(row.last_accessed),
536
+ createdAt: new Date(row.created_at),
537
+ salienceFeatures: JSON.parse(row.salience_features || '{}'),
538
+ reasonCodes: JSON.parse(row.reason_codes || '[]'),
539
+ stage: row.stage,
540
+ ttl: row.ttl,
541
+ retracted: !!row.retracted,
542
+ retractedBy: row.retracted_by,
543
+ retractedAt: row.retracted_at ? new Date(row.retracted_at) : null,
544
+ tags: JSON.parse(row.tags),
545
+ episodeId: row.episode_id ?? null,
546
+ taskStatus: row.task_status ?? null,
547
+ taskPriority: row.task_priority ?? null,
548
+ blockedBy: row.blocked_by ?? null,
549
+ };
550
+ }
551
+ rowToAssociation(row) {
552
+ return {
553
+ id: row.id,
554
+ fromEngramId: row.from_engram_id,
555
+ toEngramId: row.to_engram_id,
556
+ weight: row.weight,
557
+ confidence: row.confidence ?? 0.5,
558
+ type: row.type,
559
+ activationCount: row.activation_count ?? 0,
560
+ createdAt: new Date(row.created_at),
561
+ lastActivated: new Date(row.last_activated),
562
+ };
563
+ }
564
+ // --- Episodes ---
565
+ createEpisode(input) {
566
+ const now = new Date().toISOString();
567
+ const id = randomUUID();
568
+ const embeddingBlob = input.embedding
569
+ ? Buffer.from(new Float32Array(input.embedding).buffer)
570
+ : null;
571
+ this.db.prepare(`
572
+ INSERT INTO episodes (id, agent_id, label, embedding, engram_count, start_time, end_time, created_at)
573
+ VALUES (?, ?, ?, ?, 0, ?, ?, ?)
574
+ `).run(id, input.agentId, input.label, embeddingBlob, now, now, now);
575
+ return this.getEpisode(id);
576
+ }
577
+ getEpisode(id) {
578
+ const row = this.db.prepare('SELECT * FROM episodes WHERE id = ?').get(id);
579
+ return row ? this.rowToEpisode(row) : null;
580
+ }
581
+ getEpisodesByAgent(agentId) {
582
+ const rows = this.db.prepare('SELECT * FROM episodes WHERE agent_id = ? ORDER BY end_time DESC').all(agentId);
583
+ return rows.map(r => this.rowToEpisode(r));
584
+ }
585
+ getActiveEpisode(agentId, windowMs = 3600_000) {
586
+ // Find most recent episode that ended within the time window
587
+ const cutoff = new Date(Date.now() - windowMs).toISOString();
588
+ const row = this.db.prepare(`
589
+ SELECT * FROM episodes WHERE agent_id = ? AND end_time > ?
590
+ ORDER BY end_time DESC LIMIT 1
591
+ `).get(agentId, cutoff);
592
+ return row ? this.rowToEpisode(row) : null;
593
+ }
594
+ addEngramToEpisode(engramId, episodeId) {
595
+ this.db.prepare('UPDATE engrams SET episode_id = ? WHERE id = ?').run(episodeId, engramId);
596
+ this.db.prepare(`
597
+ UPDATE episodes SET
598
+ engram_count = engram_count + 1,
599
+ end_time = MAX(end_time, ?)
600
+ WHERE id = ?
601
+ `).run(new Date().toISOString(), episodeId);
602
+ }
603
+ getEngramsByEpisode(episodeId) {
604
+ const rows = this.db.prepare('SELECT * FROM engrams WHERE episode_id = ? AND retracted = 0 ORDER BY created_at ASC').all(episodeId);
605
+ return rows.map(r => this.rowToEngram(r));
606
+ }
607
+ updateEpisodeEmbedding(id, embedding) {
608
+ const blob = Buffer.from(new Float32Array(embedding).buffer);
609
+ this.db.prepare('UPDATE episodes SET embedding = ? WHERE id = ?').run(blob, id);
610
+ }
611
+ getEpisodeCount(agentId) {
612
+ const row = this.db.prepare('SELECT COUNT(*) as cnt FROM episodes WHERE agent_id = ?').get(agentId);
613
+ return row.cnt;
614
+ }
615
+ rowToEpisode(row) {
616
+ return {
617
+ id: row.id,
618
+ agentId: row.agent_id,
619
+ label: row.label,
620
+ embedding: row.embedding
621
+ ? Array.from(new Float32Array(row.embedding.buffer ?? row.embedding))
622
+ : null,
623
+ engramCount: row.engram_count,
624
+ startTime: new Date(row.start_time),
625
+ endTime: new Date(row.end_time),
626
+ createdAt: new Date(row.created_at),
627
+ };
628
+ }
629
+ /**
630
+ * Find engrams whose tags contain any of the given tag values.
631
+ * Used for entity-bridge retrieval: given entity tags from top results,
632
+ * find other engrams mentioning the same entities.
633
+ */
634
+ findEngramsByTags(agentId, tags, excludeIds) {
635
+ if (tags.length === 0)
636
+ return [];
637
+ // Build OR conditions for tag matching
638
+ const conditions = tags.map(() => 'tags LIKE ?').join(' OR ');
639
+ const params = [agentId, ...tags.map(t => `%"${t}"%`)];
640
+ let sql = `SELECT * FROM engrams WHERE agent_id = ? AND retracted = 0 AND (${conditions})`;
641
+ const rows = this.db.prepare(sql).all(...params);
642
+ const results = rows.map(r => this.rowToEngram(r));
643
+ if (excludeIds) {
644
+ return results.filter(e => !excludeIds.has(e.id));
645
+ }
646
+ return results;
647
+ }
648
+ // --- Checkpointing ---
649
+ updateAutoCheckpointWrite(agentId, engramId) {
650
+ const now = new Date().toISOString();
651
+ this.db.prepare(`
652
+ INSERT INTO conscious_state (agent_id, last_write_id, last_activity_at, write_count_since_consolidation, updated_at)
653
+ VALUES (?, ?, ?, 1, ?)
654
+ ON CONFLICT(agent_id) DO UPDATE SET
655
+ last_write_id = excluded.last_write_id,
656
+ last_activity_at = excluded.last_activity_at,
657
+ write_count_since_consolidation = write_count_since_consolidation + 1,
658
+ updated_at = excluded.updated_at
659
+ `).run(agentId, engramId, now, now);
660
+ }
661
+ updateAutoCheckpointRecall(agentId, context, engramIds) {
662
+ const now = new Date().toISOString();
663
+ this.db.prepare(`
664
+ INSERT INTO conscious_state (agent_id, last_recall_context, last_recall_ids, last_activity_at, recall_count_since_consolidation, updated_at)
665
+ VALUES (?, ?, ?, ?, 1, ?)
666
+ ON CONFLICT(agent_id) DO UPDATE SET
667
+ last_recall_context = excluded.last_recall_context,
668
+ last_recall_ids = excluded.last_recall_ids,
669
+ last_activity_at = excluded.last_activity_at,
670
+ recall_count_since_consolidation = recall_count_since_consolidation + 1,
671
+ updated_at = excluded.updated_at
672
+ `).run(agentId, context, JSON.stringify(engramIds), now, now);
673
+ }
674
+ touchActivity(agentId) {
675
+ const now = new Date().toISOString();
676
+ this.db.prepare(`
677
+ INSERT INTO conscious_state (agent_id, last_activity_at, updated_at)
678
+ VALUES (?, ?, ?)
679
+ ON CONFLICT(agent_id) DO UPDATE SET
680
+ last_activity_at = excluded.last_activity_at,
681
+ updated_at = excluded.updated_at
682
+ `).run(agentId, now, now);
683
+ }
684
+ saveCheckpoint(agentId, state) {
685
+ const now = new Date().toISOString();
686
+ this.db.prepare(`
687
+ INSERT INTO conscious_state (agent_id, execution_state, checkpoint_at, last_activity_at, updated_at)
688
+ VALUES (?, ?, ?, ?, ?)
689
+ ON CONFLICT(agent_id) DO UPDATE SET
690
+ execution_state = excluded.execution_state,
691
+ checkpoint_at = excluded.checkpoint_at,
692
+ last_activity_at = excluded.last_activity_at,
693
+ updated_at = excluded.updated_at
694
+ `).run(agentId, JSON.stringify(state), now, now, now);
695
+ }
696
+ getCheckpoint(agentId) {
697
+ const row = this.db.prepare('SELECT * FROM conscious_state WHERE agent_id = ?').get(agentId);
698
+ if (!row)
699
+ return null;
700
+ return {
701
+ agentId: row.agent_id,
702
+ auto: {
703
+ lastWriteId: row.last_write_id ?? null,
704
+ lastRecallContext: row.last_recall_context ?? null,
705
+ lastRecallIds: JSON.parse(row.last_recall_ids || '[]'),
706
+ lastActivityAt: new Date(row.last_activity_at),
707
+ writeCountSinceConsolidation: row.write_count_since_consolidation,
708
+ recallCountSinceConsolidation: row.recall_count_since_consolidation,
709
+ },
710
+ executionState: row.execution_state ? JSON.parse(row.execution_state) : null,
711
+ checkpointAt: row.checkpoint_at ? new Date(row.checkpoint_at) : null,
712
+ lastConsolidationAt: row.last_consolidation_at ? new Date(row.last_consolidation_at) : null,
713
+ lastMiniConsolidationAt: row.last_mini_consolidation_at ? new Date(row.last_mini_consolidation_at) : null,
714
+ updatedAt: new Date(row.updated_at),
715
+ };
716
+ }
717
+ markConsolidation(agentId, mini) {
718
+ const now = new Date().toISOString();
719
+ if (mini) {
720
+ this.db.prepare(`
721
+ UPDATE conscious_state SET last_mini_consolidation_at = ?, updated_at = ? WHERE agent_id = ?
722
+ `).run(now, now, agentId);
723
+ }
724
+ else {
725
+ this.db.prepare(`
726
+ UPDATE conscious_state SET
727
+ last_consolidation_at = ?,
728
+ last_mini_consolidation_at = ?,
729
+ write_count_since_consolidation = 0,
730
+ recall_count_since_consolidation = 0,
731
+ updated_at = ?
732
+ WHERE agent_id = ?
733
+ `).run(now, now, now, agentId);
734
+ }
735
+ }
736
+ getActiveAgents() {
737
+ const rows = this.db.prepare('SELECT * FROM conscious_state').all();
738
+ return rows.map(row => ({
739
+ agentId: row.agent_id,
740
+ lastActivityAt: new Date(row.last_activity_at),
741
+ writeCount: row.write_count_since_consolidation,
742
+ recallCount: row.recall_count_since_consolidation,
743
+ lastConsolidationAt: row.last_consolidation_at ? new Date(row.last_consolidation_at) : null,
744
+ }));
745
+ }
746
+ close() {
747
+ this.db.close();
748
+ }
749
+ }
750
+ //# sourceMappingURL=sqlite.js.map