agent-working-memory 0.11.0 → 0.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +71 -297
  2. package/dist/adapters/claude-code.d.ts.map +1 -1
  3. package/dist/adapters/claude-code.js +63 -3
  4. package/dist/adapters/claude-code.js.map +1 -1
  5. package/dist/adapters/common.d.ts.map +1 -1
  6. package/dist/adapters/common.js +358 -306
  7. package/dist/adapters/common.js.map +1 -1
  8. package/dist/api/routes.d.ts.map +1 -1
  9. package/dist/api/routes.js +29 -7
  10. package/dist/api/routes.js.map +1 -1
  11. package/dist/coordination/routes.d.ts.map +1 -1
  12. package/dist/coordination/routes.js +174 -170
  13. package/dist/coordination/routes.js.map +1 -1
  14. package/dist/core/embeddings.d.ts.map +1 -1
  15. package/dist/core/embeddings.js +4 -1
  16. package/dist/core/embeddings.js.map +1 -1
  17. package/dist/core/entity-extract.d.ts +3 -0
  18. package/dist/core/entity-extract.d.ts.map +1 -0
  19. package/dist/core/entity-extract.js +47 -0
  20. package/dist/core/entity-extract.js.map +1 -0
  21. package/dist/core/format-recall.d.ts +16 -0
  22. package/dist/core/format-recall.d.ts.map +1 -0
  23. package/dist/core/format-recall.js +24 -0
  24. package/dist/core/format-recall.js.map +1 -0
  25. package/dist/core/query-expander.js +1 -1
  26. package/dist/core/query-expander.js.map +1 -1
  27. package/dist/core/reranker.js +1 -1
  28. package/dist/core/reranker.js.map +1 -1
  29. package/dist/core/salience.d.ts.map +1 -1
  30. package/dist/core/salience.js +14 -2
  31. package/dist/core/salience.js.map +1 -1
  32. package/dist/core/whoami.d.ts +24 -0
  33. package/dist/core/whoami.d.ts.map +1 -0
  34. package/dist/core/whoami.js +66 -0
  35. package/dist/core/whoami.js.map +1 -0
  36. package/dist/core/write-pipeline.d.ts +9 -0
  37. package/dist/core/write-pipeline.d.ts.map +1 -1
  38. package/dist/core/write-pipeline.js +109 -68
  39. package/dist/core/write-pipeline.js.map +1 -1
  40. package/dist/core/write-telemetry.d.ts +33 -0
  41. package/dist/core/write-telemetry.d.ts.map +1 -0
  42. package/dist/core/write-telemetry.js +110 -0
  43. package/dist/core/write-telemetry.js.map +1 -0
  44. package/dist/engine/activation.d.ts +22 -12
  45. package/dist/engine/activation.d.ts.map +1 -1
  46. package/dist/engine/activation.js +133 -17
  47. package/dist/engine/activation.js.map +1 -1
  48. package/dist/engine/consolidation-scheduler.d.ts +1 -1
  49. package/dist/engine/consolidation-scheduler.js +1 -1
  50. package/dist/engine/consolidation.d.ts +1 -0
  51. package/dist/engine/consolidation.d.ts.map +1 -1
  52. package/dist/engine/consolidation.js +18 -0
  53. package/dist/engine/consolidation.js.map +1 -1
  54. package/dist/engine/eval.d.ts.map +1 -1
  55. package/dist/engine/eval.js +5 -1
  56. package/dist/engine/eval.js.map +1 -1
  57. package/dist/hooks/sidecar.d.ts +26 -0
  58. package/dist/hooks/sidecar.d.ts.map +1 -1
  59. package/dist/hooks/sidecar.js +30 -0
  60. package/dist/hooks/sidecar.js.map +1 -1
  61. package/dist/index.js +20 -2
  62. package/dist/index.js.map +1 -1
  63. package/dist/mcp.d.ts +2 -1
  64. package/dist/mcp.d.ts.map +1 -1
  65. package/dist/mcp.js +222 -108
  66. package/dist/mcp.js.map +1 -1
  67. package/dist/recipes/index.d.ts +57 -0
  68. package/dist/recipes/index.d.ts.map +1 -0
  69. package/dist/recipes/index.js +81 -0
  70. package/dist/recipes/index.js.map +1 -0
  71. package/dist/storage/pglite-schema.d.ts.map +1 -1
  72. package/dist/storage/pglite-schema.js +27 -0
  73. package/dist/storage/pglite-schema.js.map +1 -1
  74. package/dist/storage/pglite.d.ts +5 -0
  75. package/dist/storage/pglite.d.ts.map +1 -1
  76. package/dist/storage/pglite.js +180 -138
  77. package/dist/storage/pglite.js.map +1 -1
  78. package/dist/storage/postgres.d.ts +5 -0
  79. package/dist/storage/postgres.d.ts.map +1 -1
  80. package/dist/storage/postgres.js +180 -138
  81. package/dist/storage/postgres.js.map +1 -1
  82. package/dist/storage/sqlite.d.ts +9 -0
  83. package/dist/storage/sqlite.d.ts.map +1 -1
  84. package/dist/storage/sqlite.js +394 -326
  85. package/dist/storage/sqlite.js.map +1 -1
  86. package/dist/types/engram.d.ts +14 -0
  87. package/dist/types/engram.d.ts.map +1 -1
  88. package/dist/types/engram.js.map +1 -1
  89. package/package.json +1 -1
  90. package/src/adapters/claude-code.ts +66 -3
  91. package/src/adapters/common.ts +567 -515
  92. package/src/api/routes.ts +999 -971
  93. package/src/coordination/routes.ts +2155 -2150
  94. package/src/core/embeddings.ts +4 -1
  95. package/src/core/entity-extract.ts +47 -0
  96. package/src/core/format-recall.ts +25 -0
  97. package/src/core/query-expander.ts +1 -1
  98. package/src/core/reranker.ts +1 -1
  99. package/src/core/salience.ts +529 -514
  100. package/src/core/whoami.ts +92 -0
  101. package/src/core/write-pipeline.ts +60 -8
  102. package/src/core/write-telemetry.ts +131 -0
  103. package/src/engine/activation.ts +1468 -1369
  104. package/src/engine/consolidation-scheduler.ts +1 -1
  105. package/src/engine/consolidation.ts +887 -869
  106. package/src/engine/eval.ts +6 -1
  107. package/src/hooks/sidecar.ts +55 -0
  108. package/src/index.ts +248 -227
  109. package/src/mcp.ts +1387 -1270
  110. package/src/recipes/index.ts +125 -0
  111. package/src/storage/pglite-schema.ts +27 -0
  112. package/src/storage/pglite.ts +1420 -1372
  113. package/src/storage/postgres.ts +1523 -1475
  114. package/src/storage/sqlite.ts +1936 -1861
  115. package/src/types/engram.ts +22 -0
@@ -154,119 +154,119 @@ export class EngramStore {
154
154
  }
155
155
  }
156
156
  init() {
157
- this.db.exec(`
158
- CREATE TABLE IF NOT EXISTS engrams (
159
- id TEXT PRIMARY KEY,
160
- agent_id TEXT NOT NULL,
161
- concept TEXT NOT NULL,
162
- content TEXT NOT NULL,
163
- embedding BLOB,
164
- confidence REAL NOT NULL DEFAULT 0.5,
165
- salience REAL NOT NULL DEFAULT 0.5,
166
- access_count INTEGER NOT NULL DEFAULT 0,
167
- last_accessed TEXT NOT NULL,
168
- created_at TEXT NOT NULL,
169
- salience_features TEXT NOT NULL DEFAULT '{}',
170
- reason_codes TEXT NOT NULL DEFAULT '[]',
171
- stage TEXT NOT NULL DEFAULT 'active',
172
- ttl INTEGER,
173
- retracted INTEGER NOT NULL DEFAULT 0,
174
- retracted_by TEXT,
175
- retracted_at TEXT,
176
- tags TEXT NOT NULL DEFAULT '[]',
177
- memory_type TEXT NOT NULL DEFAULT 'unclassified'
178
- );
179
-
180
- CREATE INDEX IF NOT EXISTS idx_engrams_agent ON engrams(agent_id);
181
- CREATE INDEX IF NOT EXISTS idx_engrams_stage ON engrams(agent_id, stage);
182
- CREATE INDEX IF NOT EXISTS idx_engrams_concept ON engrams(concept);
183
- CREATE INDEX IF NOT EXISTS idx_engrams_retracted ON engrams(agent_id, retracted);
184
-
185
- CREATE TABLE IF NOT EXISTS associations (
186
- id TEXT PRIMARY KEY,
187
- from_engram_id TEXT NOT NULL REFERENCES engrams(id) ON DELETE CASCADE,
188
- to_engram_id TEXT NOT NULL REFERENCES engrams(id) ON DELETE CASCADE,
189
- weight REAL NOT NULL DEFAULT 0.1,
190
- confidence REAL NOT NULL DEFAULT 0.5,
191
- type TEXT NOT NULL DEFAULT 'hebbian',
192
- activation_count INTEGER NOT NULL DEFAULT 0,
193
- created_at TEXT NOT NULL,
194
- last_activated TEXT NOT NULL
195
- );
196
-
197
- CREATE INDEX IF NOT EXISTS idx_assoc_from ON associations(from_engram_id);
198
- CREATE INDEX IF NOT EXISTS idx_assoc_to ON associations(to_engram_id);
199
- CREATE UNIQUE INDEX IF NOT EXISTS idx_assoc_pair ON associations(from_engram_id, to_engram_id);
200
-
201
- CREATE TABLE IF NOT EXISTS agents (
202
- id TEXT PRIMARY KEY,
203
- name TEXT NOT NULL,
204
- created_at TEXT NOT NULL,
205
- config TEXT NOT NULL DEFAULT '{}'
206
- );
207
-
208
- -- FTS5 for full-text search (BM25 ranking built in)
209
- CREATE VIRTUAL TABLE IF NOT EXISTS engrams_fts USING fts5(
210
- concept, content, tags,
211
- content=engrams,
212
- content_rowid=rowid
213
- );
214
-
215
- -- Triggers to keep FTS in sync
216
- CREATE TRIGGER IF NOT EXISTS engrams_ai AFTER INSERT ON engrams BEGIN
217
- INSERT INTO engrams_fts(rowid, concept, content, tags) VALUES (new.rowid, new.concept, new.content, new.tags);
218
- END;
219
- CREATE TRIGGER IF NOT EXISTS engrams_ad AFTER DELETE ON engrams BEGIN
220
- INSERT INTO engrams_fts(engrams_fts, rowid, concept, content, tags) VALUES('delete', old.rowid, old.concept, old.content, old.tags);
221
- END;
222
- CREATE TRIGGER IF NOT EXISTS engrams_au AFTER UPDATE ON engrams BEGIN
223
- INSERT INTO engrams_fts(engrams_fts, rowid, concept, content, tags) VALUES('delete', old.rowid, old.concept, old.content, old.tags);
224
- INSERT INTO engrams_fts(rowid, concept, content, tags) VALUES (new.rowid, new.concept, new.content, new.tags);
225
- END;
226
-
227
- -- Eval event logs
228
- CREATE TABLE IF NOT EXISTS activation_events (
229
- id TEXT PRIMARY KEY,
230
- agent_id TEXT NOT NULL,
231
- timestamp TEXT NOT NULL,
232
- context TEXT NOT NULL,
233
- results_returned INTEGER NOT NULL,
234
- top_score REAL,
235
- latency_ms REAL NOT NULL,
236
- engram_ids TEXT NOT NULL DEFAULT '[]'
237
- );
238
-
239
- CREATE TABLE IF NOT EXISTS staging_events (
240
- engram_id TEXT NOT NULL,
241
- agent_id TEXT NOT NULL,
242
- action TEXT NOT NULL,
243
- resonance_score REAL,
244
- timestamp TEXT NOT NULL,
245
- age_ms INTEGER NOT NULL
246
- );
247
-
248
- CREATE TABLE IF NOT EXISTS retrieval_feedback (
249
- id TEXT PRIMARY KEY,
250
- activation_event_id TEXT,
251
- engram_id TEXT NOT NULL,
252
- useful INTEGER NOT NULL,
253
- context TEXT,
254
- timestamp TEXT NOT NULL
255
- );
256
-
257
- CREATE TABLE IF NOT EXISTS episodes (
258
- id TEXT PRIMARY KEY,
259
- agent_id TEXT NOT NULL,
260
- label TEXT NOT NULL,
261
- embedding BLOB,
262
- engram_count INTEGER NOT NULL DEFAULT 0,
263
- start_time TEXT NOT NULL,
264
- end_time TEXT NOT NULL,
265
- created_at TEXT NOT NULL
266
- );
267
-
268
- CREATE INDEX IF NOT EXISTS idx_episodes_agent ON episodes(agent_id);
269
- CREATE INDEX IF NOT EXISTS idx_episodes_time ON episodes(agent_id, end_time);
157
+ this.db.exec(`
158
+ CREATE TABLE IF NOT EXISTS engrams (
159
+ id TEXT PRIMARY KEY,
160
+ agent_id TEXT NOT NULL,
161
+ concept TEXT NOT NULL,
162
+ content TEXT NOT NULL,
163
+ embedding BLOB,
164
+ confidence REAL NOT NULL DEFAULT 0.5,
165
+ salience REAL NOT NULL DEFAULT 0.5,
166
+ access_count INTEGER NOT NULL DEFAULT 0,
167
+ last_accessed TEXT NOT NULL,
168
+ created_at TEXT NOT NULL,
169
+ salience_features TEXT NOT NULL DEFAULT '{}',
170
+ reason_codes TEXT NOT NULL DEFAULT '[]',
171
+ stage TEXT NOT NULL DEFAULT 'active',
172
+ ttl INTEGER,
173
+ retracted INTEGER NOT NULL DEFAULT 0,
174
+ retracted_by TEXT,
175
+ retracted_at TEXT,
176
+ tags TEXT NOT NULL DEFAULT '[]',
177
+ memory_type TEXT NOT NULL DEFAULT 'unclassified'
178
+ );
179
+
180
+ CREATE INDEX IF NOT EXISTS idx_engrams_agent ON engrams(agent_id);
181
+ CREATE INDEX IF NOT EXISTS idx_engrams_stage ON engrams(agent_id, stage);
182
+ CREATE INDEX IF NOT EXISTS idx_engrams_concept ON engrams(concept);
183
+ CREATE INDEX IF NOT EXISTS idx_engrams_retracted ON engrams(agent_id, retracted);
184
+
185
+ CREATE TABLE IF NOT EXISTS associations (
186
+ id TEXT PRIMARY KEY,
187
+ from_engram_id TEXT NOT NULL REFERENCES engrams(id) ON DELETE CASCADE,
188
+ to_engram_id TEXT NOT NULL REFERENCES engrams(id) ON DELETE CASCADE,
189
+ weight REAL NOT NULL DEFAULT 0.1,
190
+ confidence REAL NOT NULL DEFAULT 0.5,
191
+ type TEXT NOT NULL DEFAULT 'hebbian',
192
+ activation_count INTEGER NOT NULL DEFAULT 0,
193
+ created_at TEXT NOT NULL,
194
+ last_activated TEXT NOT NULL
195
+ );
196
+
197
+ CREATE INDEX IF NOT EXISTS idx_assoc_from ON associations(from_engram_id);
198
+ CREATE INDEX IF NOT EXISTS idx_assoc_to ON associations(to_engram_id);
199
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_assoc_pair ON associations(from_engram_id, to_engram_id);
200
+
201
+ CREATE TABLE IF NOT EXISTS agents (
202
+ id TEXT PRIMARY KEY,
203
+ name TEXT NOT NULL,
204
+ created_at TEXT NOT NULL,
205
+ config TEXT NOT NULL DEFAULT '{}'
206
+ );
207
+
208
+ -- FTS5 for full-text search (BM25 ranking built in)
209
+ CREATE VIRTUAL TABLE IF NOT EXISTS engrams_fts USING fts5(
210
+ concept, content, tags,
211
+ content=engrams,
212
+ content_rowid=rowid
213
+ );
214
+
215
+ -- Triggers to keep FTS in sync
216
+ CREATE TRIGGER IF NOT EXISTS engrams_ai AFTER INSERT ON engrams BEGIN
217
+ INSERT INTO engrams_fts(rowid, concept, content, tags) VALUES (new.rowid, new.concept, new.content, new.tags);
218
+ END;
219
+ CREATE TRIGGER IF NOT EXISTS engrams_ad AFTER DELETE ON engrams BEGIN
220
+ INSERT INTO engrams_fts(engrams_fts, rowid, concept, content, tags) VALUES('delete', old.rowid, old.concept, old.content, old.tags);
221
+ END;
222
+ CREATE TRIGGER IF NOT EXISTS engrams_au AFTER UPDATE ON engrams BEGIN
223
+ INSERT INTO engrams_fts(engrams_fts, rowid, concept, content, tags) VALUES('delete', old.rowid, old.concept, old.content, old.tags);
224
+ INSERT INTO engrams_fts(rowid, concept, content, tags) VALUES (new.rowid, new.concept, new.content, new.tags);
225
+ END;
226
+
227
+ -- Eval event logs
228
+ CREATE TABLE IF NOT EXISTS activation_events (
229
+ id TEXT PRIMARY KEY,
230
+ agent_id TEXT NOT NULL,
231
+ timestamp TEXT NOT NULL,
232
+ context TEXT NOT NULL,
233
+ results_returned INTEGER NOT NULL,
234
+ top_score REAL,
235
+ latency_ms REAL NOT NULL,
236
+ engram_ids TEXT NOT NULL DEFAULT '[]'
237
+ );
238
+
239
+ CREATE TABLE IF NOT EXISTS staging_events (
240
+ engram_id TEXT NOT NULL,
241
+ agent_id TEXT NOT NULL,
242
+ action TEXT NOT NULL,
243
+ resonance_score REAL,
244
+ timestamp TEXT NOT NULL,
245
+ age_ms INTEGER NOT NULL
246
+ );
247
+
248
+ CREATE TABLE IF NOT EXISTS retrieval_feedback (
249
+ id TEXT PRIMARY KEY,
250
+ activation_event_id TEXT,
251
+ engram_id TEXT NOT NULL,
252
+ useful INTEGER NOT NULL,
253
+ context TEXT,
254
+ timestamp TEXT NOT NULL
255
+ );
256
+
257
+ CREATE TABLE IF NOT EXISTS episodes (
258
+ id TEXT PRIMARY KEY,
259
+ agent_id TEXT NOT NULL,
260
+ label TEXT NOT NULL,
261
+ embedding BLOB,
262
+ engram_count INTEGER NOT NULL DEFAULT 0,
263
+ start_time TEXT NOT NULL,
264
+ end_time TEXT NOT NULL,
265
+ created_at TEXT NOT NULL
266
+ );
267
+
268
+ CREATE INDEX IF NOT EXISTS idx_episodes_agent ON episodes(agent_id);
269
+ CREATE INDEX IF NOT EXISTS idx_episodes_time ON episodes(agent_id, end_time);
270
270
  `);
271
271
  // Migration: add episode_id column if missing
272
272
  try {
@@ -280,10 +280,10 @@ export class EngramStore {
280
280
  this.db.prepare('SELECT task_status FROM engrams LIMIT 0').get();
281
281
  }
282
282
  catch {
283
- this.db.exec(`
284
- ALTER TABLE engrams ADD COLUMN task_status TEXT;
285
- ALTER TABLE engrams ADD COLUMN task_priority TEXT;
286
- ALTER TABLE engrams ADD COLUMN blocked_by TEXT;
283
+ this.db.exec(`
284
+ ALTER TABLE engrams ADD COLUMN task_status TEXT;
285
+ ALTER TABLE engrams ADD COLUMN task_priority TEXT;
286
+ ALTER TABLE engrams ADD COLUMN blocked_by TEXT;
287
287
  `);
288
288
  this.db.exec('CREATE INDEX IF NOT EXISTS idx_engrams_task ON engrams(agent_id, task_status)');
289
289
  }
@@ -292,10 +292,10 @@ export class EngramStore {
292
292
  this.db.prepare('SELECT memory_class FROM engrams LIMIT 0').get();
293
293
  }
294
294
  catch {
295
- this.db.exec(`
296
- ALTER TABLE engrams ADD COLUMN memory_class TEXT NOT NULL DEFAULT 'working';
297
- ALTER TABLE engrams ADD COLUMN superseded_by TEXT;
298
- ALTER TABLE engrams ADD COLUMN supersedes TEXT;
295
+ this.db.exec(`
296
+ ALTER TABLE engrams ADD COLUMN memory_class TEXT NOT NULL DEFAULT 'working';
297
+ ALTER TABLE engrams ADD COLUMN superseded_by TEXT;
298
+ ALTER TABLE engrams ADD COLUMN supersedes TEXT;
299
299
  `);
300
300
  }
301
301
  // Migration: add embedding_model for version tracking (prevents drift on model change)
@@ -305,23 +305,51 @@ export class EngramStore {
305
305
  catch {
306
306
  this.db.exec(`ALTER TABLE engrams ADD COLUMN embedding_model TEXT`);
307
307
  }
308
+ // Entity inverted index + alias table (D9, 2026-07-30) — write-time
309
+ // bookkeeping only; retrieval unchanged until D11. Idempotent CREATEs.
310
+ this.db.exec(`
311
+ CREATE TABLE IF NOT EXISTS entity_mentions (
312
+ entity TEXT NOT NULL,
313
+ engram_id TEXT NOT NULL,
314
+ agent_id TEXT NOT NULL,
315
+ PRIMARY KEY (entity, engram_id)
316
+ );
317
+ CREATE INDEX IF NOT EXISTS idx_entity_mentions_agent ON entity_mentions(agent_id, entity);
318
+ CREATE TABLE IF NOT EXISTS entity_aliases (
319
+ alias TEXT PRIMARY KEY,
320
+ entity TEXT NOT NULL
321
+ );
322
+ `);
323
+ // Migration: memory-spine provenance + temporal validity (D5/D8, 2026-07-30)
324
+ try {
325
+ this.db.prepare('SELECT origin_class FROM engrams LIMIT 0').get();
326
+ }
327
+ catch {
328
+ this.db.exec(`
329
+ ALTER TABLE engrams ADD COLUMN origin_class TEXT;
330
+ ALTER TABLE engrams ADD COLUMN writer_session TEXT;
331
+ ALTER TABLE engrams ADD COLUMN recipe_id TEXT;
332
+ ALTER TABLE engrams ADD COLUMN valid_from TEXT;
333
+ ALTER TABLE engrams ADD COLUMN valid_to TEXT;
334
+ `);
335
+ }
308
336
  // Migration: add conscious_state table for checkpointing
309
- this.db.exec(`
310
- CREATE TABLE IF NOT EXISTS conscious_state (
311
- agent_id TEXT PRIMARY KEY,
312
- last_write_id TEXT,
313
- last_recall_context TEXT,
314
- last_recall_ids TEXT NOT NULL DEFAULT '[]',
315
- last_activity_at TEXT NOT NULL DEFAULT (datetime('now')),
316
- write_count_since_consolidation INTEGER NOT NULL DEFAULT 0,
317
- recall_count_since_consolidation INTEGER NOT NULL DEFAULT 0,
318
- execution_state TEXT,
319
- checkpoint_at TEXT,
320
- last_consolidation_at TEXT,
321
- last_mini_consolidation_at TEXT,
322
- consolidation_cycle_count INTEGER NOT NULL DEFAULT 0,
323
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
324
- )
337
+ this.db.exec(`
338
+ CREATE TABLE IF NOT EXISTS conscious_state (
339
+ agent_id TEXT PRIMARY KEY,
340
+ last_write_id TEXT,
341
+ last_recall_context TEXT,
342
+ last_recall_ids TEXT NOT NULL DEFAULT '[]',
343
+ last_activity_at TEXT NOT NULL DEFAULT (datetime('now')),
344
+ write_count_since_consolidation INTEGER NOT NULL DEFAULT 0,
345
+ recall_count_since_consolidation INTEGER NOT NULL DEFAULT 0,
346
+ execution_state TEXT,
347
+ checkpoint_at TEXT,
348
+ last_consolidation_at TEXT,
349
+ last_mini_consolidation_at TEXT,
350
+ consolidation_cycle_count INTEGER NOT NULL DEFAULT 0,
351
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
352
+ )
325
353
  `);
326
354
  // Migration: add consolidation_cycle_count if missing (existing DBs)
327
355
  try {
@@ -342,7 +370,7 @@ export class EngramStore {
342
370
  }
343
371
  catch {
344
372
  this.db.exec(`ALTER TABLE engrams ADD COLUMN sequence INTEGER`);
345
- this.db.exec(`CREATE INDEX IF NOT EXISTS idx_engrams_agent_sequence
373
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idx_engrams_agent_sequence
346
374
  ON engrams(agent_id, sequence) WHERE sequence IS NOT NULL`);
347
375
  }
348
376
  // Migration (0.8 Cluster A): references_json column for typed cross-record
@@ -361,14 +389,14 @@ export class EngramStore {
361
389
  const embeddingBlob = input.embedding
362
390
  ? Buffer.from(new Float32Array(input.embedding).buffer)
363
391
  : null;
364
- this.db.prepare(`
365
- INSERT INTO engrams (id, agent_id, concept, content, embedding, confidence, salience,
366
- access_count, last_accessed, created_at, salience_features, reason_codes, stage, tags, episode_id,
367
- ttl, memory_class, supersedes, task_status, task_priority, blocked_by, memory_type,
368
- sequence, references_json)
369
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
392
+ this.db.prepare(`
393
+ INSERT INTO engrams (id, agent_id, concept, content, embedding, confidence, salience,
394
+ access_count, last_accessed, created_at, salience_features, reason_codes, stage, tags, episode_id,
395
+ ttl, memory_class, supersedes, task_status, task_priority, blocked_by, memory_type,
396
+ sequence, references_json, origin_class, writer_session, recipe_id, valid_from, valid_to)
397
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
370
398
  `).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.memoryClass ?? 'working', input.supersedes ?? null, input.taskStatus ?? null, input.taskPriority ?? null, input.blockedBy ?? null, input.memoryType ?? 'unclassified', input.sequence ?? null, input.references && input.references.length > 0
371
- ? JSON.stringify(input.references) : null);
399
+ ? JSON.stringify(input.references) : null, input.originClass ?? null, input.writerSession ?? null, input.recipeId ?? null, input.validFrom ?? null, input.validTo ?? null);
372
400
  // Add to slim cache (skip if not yet populated — first slim fetch will load it)
373
401
  if (this.slimCachePopulated) {
374
402
  this.cacheAdd({
@@ -536,15 +564,15 @@ export class EngramStore {
536
564
  try {
537
565
  const placeholders = agentIds.map(() => '?').join(',');
538
566
  const innerLimit = Math.max(limit * 5, 50);
539
- const rows = this.db.prepare(`
540
- WITH top_fts AS (
541
- SELECT rowid, rank FROM engrams_fts WHERE engrams_fts MATCH ? ORDER BY rank LIMIT ?
542
- )
543
- SELECT e.*, top_fts.rank FROM top_fts
544
- JOIN engrams e ON e.rowid = top_fts.rowid
545
- WHERE e.agent_id IN (${placeholders}) AND e.retracted = 0
546
- ORDER BY top_fts.rank
547
- LIMIT ?
567
+ const rows = this.db.prepare(`
568
+ WITH top_fts AS (
569
+ SELECT rowid, rank FROM engrams_fts WHERE engrams_fts MATCH ? ORDER BY rank LIMIT ?
570
+ )
571
+ SELECT e.*, top_fts.rank FROM top_fts
572
+ JOIN engrams e ON e.rowid = top_fts.rowid
573
+ WHERE e.agent_id IN (${placeholders}) AND e.retracted = 0
574
+ ORDER BY top_fts.rank
575
+ LIMIT ?
548
576
  `).all(sanitized, innerLimit, ...agentIds, limit);
549
577
  return rows.map(r => ({
550
578
  engram: this.rowToEngram(r),
@@ -583,12 +611,12 @@ export class EngramStore {
583
611
  * Boost: +0.02 per access, scaled by 1/sqrt(accessCount+1), capped at 0.85.
584
612
  */
585
613
  touchEngram(id) {
586
- this.db.prepare(`
587
- UPDATE engrams
588
- SET access_count = access_count + 1,
589
- last_accessed = ?,
590
- confidence = MIN(0.85, confidence + 0.02 / (1.0 + sqrt(access_count)))
591
- WHERE id = ?
614
+ this.db.prepare(`
615
+ UPDATE engrams
616
+ SET access_count = access_count + 1,
617
+ last_accessed = ?,
618
+ confidence = MIN(0.85, confidence + 0.02 / (1.0 + sqrt(access_count)))
619
+ WHERE id = ?
592
620
  `).run(new Date().toISOString(), id);
593
621
  }
594
622
  updateStage(id, stage) {
@@ -618,8 +646,8 @@ export class EngramStore {
618
646
  this.cacheUpdateEmbedding(id, embedding);
619
647
  }
620
648
  retractEngram(id, retractedBy) {
621
- this.db.prepare(`
622
- UPDATE engrams SET retracted = 1, retracted_by = ?, retracted_at = ? WHERE id = ?
649
+ this.db.prepare(`
650
+ UPDATE engrams SET retracted = 1, retracted_by = ?, retracted_at = ? WHERE id = ?
623
651
  `).run(retractedBy, new Date().toISOString(), id);
624
652
  this.cacheRetract(id);
625
653
  }
@@ -636,20 +664,20 @@ export class EngramStore {
636
664
  let count = 0;
637
665
  const shiftSec = Math.round(ms / 1000);
638
666
  // Shift engram timestamps
639
- const r1 = this.db.prepare(`
640
- UPDATE engrams SET
641
- created_at = datetime(created_at, '-${shiftSec} seconds'),
642
- last_accessed = datetime(last_accessed, '-${shiftSec} seconds')
643
- WHERE agent_id = ?
667
+ const r1 = this.db.prepare(`
668
+ UPDATE engrams SET
669
+ created_at = datetime(created_at, '-${shiftSec} seconds'),
670
+ last_accessed = datetime(last_accessed, '-${shiftSec} seconds')
671
+ WHERE agent_id = ?
644
672
  `).run(agentId);
645
673
  count += r1.changes;
646
674
  // Shift association timestamps
647
- const r2 = this.db.prepare(`
648
- UPDATE associations SET
649
- created_at = datetime(created_at, '-${shiftSec} seconds'),
650
- last_activated = datetime(last_activated, '-${shiftSec} seconds')
651
- WHERE from_engram_id IN (SELECT id FROM engrams WHERE agent_id = ?)
652
- OR to_engram_id IN (SELECT id FROM engrams WHERE agent_id = ?)
675
+ const r2 = this.db.prepare(`
676
+ UPDATE associations SET
677
+ created_at = datetime(created_at, '-${shiftSec} seconds'),
678
+ last_activated = datetime(last_activated, '-${shiftSec} seconds')
679
+ WHERE from_engram_id IN (SELECT id FROM engrams WHERE agent_id = ?)
680
+ OR to_engram_id IN (SELECT id FROM engrams WHERE agent_id = ?)
653
681
  `).run(agentId, agentId);
654
682
  count += r2.changes;
655
683
  return count;
@@ -728,15 +756,15 @@ export class EngramStore {
728
756
  // dominant, low retracted rate).
729
757
  try {
730
758
  const innerLimit = Math.max(limit * 5, 50);
731
- const rows = this.db.prepare(`
732
- WITH top_fts AS (
733
- SELECT rowid, rank FROM engrams_fts WHERE engrams_fts MATCH ? ORDER BY rank LIMIT ?
734
- )
735
- SELECT e.*, top_fts.rank FROM top_fts
736
- JOIN engrams e ON e.rowid = top_fts.rowid
737
- WHERE e.agent_id = ? AND e.retracted = 0
738
- ORDER BY top_fts.rank
739
- LIMIT ?
759
+ const rows = this.db.prepare(`
760
+ WITH top_fts AS (
761
+ SELECT rowid, rank FROM engrams_fts WHERE engrams_fts MATCH ? ORDER BY rank LIMIT ?
762
+ )
763
+ SELECT e.*, top_fts.rank FROM top_fts
764
+ JOIN engrams e ON e.rowid = top_fts.rowid
765
+ WHERE e.agent_id = ? AND e.retracted = 0
766
+ ORDER BY top_fts.rank
767
+ LIMIT ?
740
768
  `).all(sanitized, innerLimit, agentId, limit);
741
769
  return rows.map(r => ({
742
770
  engram: this.rowToEngram(r),
@@ -851,14 +879,14 @@ export class EngramStore {
851
879
  sql += ' AND task_status = ?';
852
880
  params.push(status);
853
881
  }
854
- sql += ` ORDER BY
855
- CASE task_priority
856
- WHEN 'urgent' THEN 0
857
- WHEN 'high' THEN 1
858
- WHEN 'medium' THEN 2
859
- WHEN 'low' THEN 3
860
- ELSE 4
861
- END,
882
+ sql += ` ORDER BY
883
+ CASE task_priority
884
+ WHEN 'urgent' THEN 0
885
+ WHEN 'high' THEN 1
886
+ WHEN 'medium' THEN 2
887
+ WHEN 'low' THEN 3
888
+ ELSE 4
889
+ END,
862
890
  created_at DESC`;
863
891
  return this.db.prepare(sql).all(...params).map(r => this.rowToEngram(r));
864
892
  }
@@ -866,20 +894,20 @@ export class EngramStore {
866
894
  * Get the next actionable task — highest priority that's not blocked or done.
867
895
  */
868
896
  getNextTask(agentId) {
869
- const row = this.db.prepare(`
870
- SELECT * FROM engrams
871
- WHERE agent_id = ? AND task_status IN ('open', 'in_progress') AND retracted = 0
872
- ORDER BY
873
- CASE task_status WHEN 'in_progress' THEN 0 ELSE 1 END,
874
- CASE task_priority
875
- WHEN 'urgent' THEN 0
876
- WHEN 'high' THEN 1
877
- WHEN 'medium' THEN 2
878
- WHEN 'low' THEN 3
879
- ELSE 4
880
- END,
881
- created_at ASC
882
- LIMIT 1
897
+ const row = this.db.prepare(`
898
+ SELECT * FROM engrams
899
+ WHERE agent_id = ? AND task_status IN ('open', 'in_progress') AND retracted = 0
900
+ ORDER BY
901
+ CASE task_status WHEN 'in_progress' THEN 0 ELSE 1 END,
902
+ CASE task_priority
903
+ WHEN 'urgent' THEN 0
904
+ WHEN 'high' THEN 1
905
+ WHEN 'medium' THEN 2
906
+ WHEN 'low' THEN 3
907
+ ELSE 4
908
+ END,
909
+ created_at ASC
910
+ LIMIT 1
883
911
  `).get(agentId);
884
912
  return row ? this.rowToEngram(row) : null;
885
913
  }
@@ -901,11 +929,11 @@ export class EngramStore {
901
929
  * Returns null if no match.
902
930
  */
903
931
  findActiveMatchByConcept(agentId, concept, requiredTags) {
904
- let sql = `SELECT * FROM engrams
905
- WHERE agent_id = ?
906
- AND LOWER(TRIM(concept)) = LOWER(TRIM(?))
907
- AND stage = 'active'
908
- AND retracted = 0
932
+ let sql = `SELECT * FROM engrams
933
+ WHERE agent_id = ?
934
+ AND LOWER(TRIM(concept)) = LOWER(TRIM(?))
935
+ AND stage = 'active'
936
+ AND retracted = 0
909
937
  AND superseded_by IS NULL`;
910
938
  const params = [agentId, concept];
911
939
  if (requiredTags && requiredTags.length > 0) {
@@ -935,11 +963,11 @@ export class EngramStore {
935
963
  upsertAssociation(fromId, toId, weight, type = 'hebbian', confidence = 0.5) {
936
964
  const now = new Date().toISOString();
937
965
  const id = randomUUID();
938
- this.db.prepare(`
939
- INSERT INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type, activation_count, created_at, last_activated)
940
- VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
941
- ON CONFLICT(from_engram_id, to_engram_id) DO UPDATE SET
942
- weight = ?, confidence = ?, last_activated = ?, activation_count = activation_count + 1
966
+ this.db.prepare(`
967
+ INSERT INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type, activation_count, created_at, last_activated)
968
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
969
+ ON CONFLICT(from_engram_id, to_engram_id) DO UPDATE SET
970
+ weight = ?, confidence = ?, last_activated = ?, activation_count = activation_count + 1
943
971
  `).run(id, fromId, toId, weight, confidence, type, now, now, weight, confidence, now);
944
972
  return this.getAssociation(fromId, toId);
945
973
  }
@@ -979,12 +1007,12 @@ export class EngramStore {
979
1007
  // associations under both their from and to engram. Self-loops would be
980
1008
  // double-counted, but they're rare in practice and the prior code handled them
981
1009
  // identically.
982
- const rows = this.db.prepare(`SELECT id, SUM(cnt) AS count, SUM(sw) AS sum_weight FROM (
983
- SELECT from_engram_id AS id, 1 AS cnt, weight AS sw FROM associations WHERE from_engram_id IN (${placeholders})
984
- UNION ALL
985
- SELECT to_engram_id AS id, 1 AS cnt, weight AS sw FROM associations WHERE to_engram_id IN (${placeholders})
986
- )
987
- WHERE id IN (${placeholders})
1010
+ const rows = this.db.prepare(`SELECT id, SUM(cnt) AS count, SUM(sw) AS sum_weight FROM (
1011
+ SELECT from_engram_id AS id, 1 AS cnt, weight AS sw FROM associations WHERE from_engram_id IN (${placeholders})
1012
+ UNION ALL
1013
+ SELECT to_engram_id AS id, 1 AS cnt, weight AS sw FROM associations WHERE to_engram_id IN (${placeholders})
1014
+ )
1015
+ WHERE id IN (${placeholders})
988
1016
  GROUP BY id`).all(...chunk, ...chunk, ...chunk);
989
1017
  for (const r of rows) {
990
1018
  result.set(r.id, { count: r.count, sumWeight: r.sum_weight });
@@ -1015,7 +1043,7 @@ export class EngramStore {
1015
1043
  for (let i = 0; i < engramIds.length; i += CHUNK) {
1016
1044
  const chunk = engramIds.slice(i, i + CHUNK);
1017
1045
  const placeholders = chunk.map(() => '?').join(',');
1018
- const rows = this.db.prepare(`SELECT * FROM associations
1046
+ const rows = this.db.prepare(`SELECT * FROM associations
1019
1047
  WHERE from_engram_id IN (${placeholders}) OR to_engram_id IN (${placeholders})`).all(...chunk, ...chunk);
1020
1048
  for (const r of rows) {
1021
1049
  const a = this.rowToAssociation(r);
@@ -1054,22 +1082,22 @@ export class EngramStore {
1054
1082
  this.db.prepare('DELETE FROM associations WHERE id = ?').run(id);
1055
1083
  }
1056
1084
  getAllAssociations(agentId) {
1057
- const rows = this.db.prepare(`
1058
- SELECT a.* FROM associations a
1059
- JOIN engrams e ON a.from_engram_id = e.id
1060
- WHERE e.agent_id = ?
1085
+ const rows = this.db.prepare(`
1086
+ SELECT a.* FROM associations a
1087
+ JOIN engrams e ON a.from_engram_id = e.id
1088
+ WHERE e.agent_id = ?
1061
1089
  `).all(agentId);
1062
1090
  return rows.map(r => this.rowToAssociation(r));
1063
1091
  }
1064
1092
  // --- Eviction ---
1065
1093
  getEvictionCandidates(agentId, limit) {
1066
1094
  // Lowest combined score: low salience + low access + low confidence + oldest
1067
- const rows = this.db.prepare(`
1068
- SELECT * FROM engrams
1069
- WHERE agent_id = ? AND stage = 'active' AND retracted = 0
1070
- ORDER BY (salience * 0.3 + confidence * 0.3 + (CAST(access_count AS REAL) / (access_count + 5)) * 0.2 +
1071
- (1.0 / (1.0 + (julianday('now') - julianday(last_accessed)))) * 0.2) ASC
1072
- LIMIT ?
1095
+ const rows = this.db.prepare(`
1096
+ SELECT * FROM engrams
1097
+ WHERE agent_id = ? AND stage = 'active' AND retracted = 0
1098
+ ORDER BY (salience * 0.3 + confidence * 0.3 + (CAST(access_count AS REAL) / (access_count + 5)) * 0.2 +
1099
+ (1.0 / (1.0 + (julianday('now') - julianday(last_accessed)))) * 0.2) ASC
1100
+ LIMIT ?
1073
1101
  `).all(agentId, limit);
1074
1102
  return rows.map(r => this.rowToEngram(r));
1075
1103
  }
@@ -1084,8 +1112,8 @@ export class EngramStore {
1084
1112
  // --- Staging buffer ---
1085
1113
  getExpiredStaging() {
1086
1114
  const now = Date.now();
1087
- const rows = this.db.prepare(`
1088
- SELECT * FROM engrams WHERE stage = 'staging' AND ttl IS NOT NULL
1115
+ const rows = this.db.prepare(`
1116
+ SELECT * FROM engrams WHERE stage = 'staging' AND ttl IS NOT NULL
1089
1117
  `).all();
1090
1118
  return rows
1091
1119
  .map(r => this.rowToEngram(r))
@@ -1093,53 +1121,53 @@ export class EngramStore {
1093
1121
  }
1094
1122
  // --- Eval event logging ---
1095
1123
  logActivationEvent(event) {
1096
- this.db.prepare(`
1097
- INSERT INTO activation_events (id, agent_id, timestamp, context, results_returned, top_score, latency_ms, engram_ids)
1098
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1124
+ this.db.prepare(`
1125
+ INSERT INTO activation_events (id, agent_id, timestamp, context, results_returned, top_score, latency_ms, engram_ids)
1126
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1099
1127
  `).run(event.id, event.agentId, event.timestamp.toISOString(), event.context, event.resultsReturned, event.topScore, event.latencyMs, JSON.stringify(event.engramIds));
1100
1128
  }
1101
1129
  logStagingEvent(event) {
1102
- this.db.prepare(`
1103
- INSERT INTO staging_events (engram_id, agent_id, action, resonance_score, timestamp, age_ms)
1104
- VALUES (?, ?, ?, ?, ?, ?)
1130
+ this.db.prepare(`
1131
+ INSERT INTO staging_events (engram_id, agent_id, action, resonance_score, timestamp, age_ms)
1132
+ VALUES (?, ?, ?, ?, ?, ?)
1105
1133
  `).run(event.engramId, event.agentId, event.action, event.resonanceScore, event.timestamp.toISOString(), event.ageMs);
1106
1134
  }
1107
1135
  logRetrievalFeedback(activationEventId, engramId, useful, context) {
1108
- this.db.prepare(`
1109
- INSERT INTO retrieval_feedback (id, activation_event_id, engram_id, useful, context, timestamp)
1110
- VALUES (?, ?, ?, ?, ?, ?)
1136
+ this.db.prepare(`
1137
+ INSERT INTO retrieval_feedback (id, activation_event_id, engram_id, useful, context, timestamp)
1138
+ VALUES (?, ?, ?, ?, ?, ?)
1111
1139
  `).run(randomUUID(), activationEventId, engramId, useful ? 1 : 0, context, new Date().toISOString());
1112
1140
  }
1113
1141
  // --- Eval metrics queries ---
1114
1142
  getRetrievalPrecision(agentId, windowHours = 24) {
1115
1143
  const since = new Date(Date.now() - windowHours * 3600_000).toISOString();
1116
- const row = this.db.prepare(`
1117
- SELECT
1118
- COUNT(CASE WHEN useful = 1 THEN 1 END) as useful_count,
1119
- COUNT(*) as total_count
1120
- FROM retrieval_feedback rf
1121
- LEFT JOIN activation_events ae ON rf.activation_event_id = ae.id
1122
- JOIN engrams e ON rf.engram_id = e.id
1123
- WHERE e.agent_id = ? AND rf.timestamp > ?
1144
+ const row = this.db.prepare(`
1145
+ SELECT
1146
+ COUNT(CASE WHEN useful = 1 THEN 1 END) as useful_count,
1147
+ COUNT(*) as total_count
1148
+ FROM retrieval_feedback rf
1149
+ LEFT JOIN activation_events ae ON rf.activation_event_id = ae.id
1150
+ JOIN engrams e ON rf.engram_id = e.id
1151
+ WHERE e.agent_id = ? AND rf.timestamp > ?
1124
1152
  `).get(agentId, since);
1125
1153
  return row.total_count > 0 ? row.useful_count / row.total_count : 0;
1126
1154
  }
1127
1155
  getStagingMetrics(agentId) {
1128
- const row = this.db.prepare(`
1129
- SELECT
1130
- COUNT(CASE WHEN action = 'promoted' THEN 1 END) as promoted,
1131
- COUNT(CASE WHEN action = 'discarded' THEN 1 END) as discarded,
1132
- COUNT(CASE WHEN action = 'expired' THEN 1 END) as expired
1133
- FROM staging_events WHERE agent_id = ?
1156
+ const row = this.db.prepare(`
1157
+ SELECT
1158
+ COUNT(CASE WHEN action = 'promoted' THEN 1 END) as promoted,
1159
+ COUNT(CASE WHEN action = 'discarded' THEN 1 END) as discarded,
1160
+ COUNT(CASE WHEN action = 'expired' THEN 1 END) as expired
1161
+ FROM staging_events WHERE agent_id = ?
1134
1162
  `).get(agentId);
1135
1163
  return { promoted: row.promoted, discarded: row.discarded, expired: row.expired };
1136
1164
  }
1137
1165
  getActivationStats(agentId, windowHours = 24) {
1138
1166
  const since = new Date(Date.now() - windowHours * 3600_000).toISOString();
1139
- const rows = this.db.prepare(`
1140
- SELECT latency_ms FROM activation_events
1141
- WHERE agent_id = ? AND timestamp > ?
1142
- ORDER BY latency_ms ASC
1167
+ const rows = this.db.prepare(`
1168
+ SELECT latency_ms FROM activation_events
1169
+ WHERE agent_id = ? AND timestamp > ?
1170
+ ORDER BY latency_ms ASC
1143
1171
  `).all(agentId, since);
1144
1172
  if (rows.length === 0)
1145
1173
  return { count: 0, avgLatencyMs: 0, p95LatencyMs: 0 };
@@ -1188,6 +1216,11 @@ export class EngramStore {
1188
1216
  blockedBy: row.blocked_by ?? null,
1189
1217
  sequence: row.sequence == null ? null : Number(row.sequence),
1190
1218
  references: row.references_json ? JSON.parse(row.references_json) : null,
1219
+ originClass: row.origin_class ?? null,
1220
+ writerSession: row.writer_session ?? null,
1221
+ recipeId: row.recipe_id ?? null,
1222
+ validFrom: row.valid_from ?? null,
1223
+ validTo: row.valid_to ?? null,
1191
1224
  };
1192
1225
  }
1193
1226
  rowToAssociation(row) {
@@ -1210,9 +1243,9 @@ export class EngramStore {
1210
1243
  const embeddingBlob = input.embedding
1211
1244
  ? Buffer.from(new Float32Array(input.embedding).buffer)
1212
1245
  : null;
1213
- this.db.prepare(`
1214
- INSERT INTO episodes (id, agent_id, label, embedding, engram_count, start_time, end_time, created_at)
1215
- VALUES (?, ?, ?, ?, 0, ?, ?, ?)
1246
+ this.db.prepare(`
1247
+ INSERT INTO episodes (id, agent_id, label, embedding, engram_count, start_time, end_time, created_at)
1248
+ VALUES (?, ?, ?, ?, 0, ?, ?, ?)
1216
1249
  `).run(id, input.agentId, input.label, embeddingBlob, now, now, now);
1217
1250
  return this.getEpisode(id);
1218
1251
  }
@@ -1227,19 +1260,19 @@ export class EngramStore {
1227
1260
  getActiveEpisode(agentId, windowMs = 3600_000) {
1228
1261
  // Find most recent episode that ended within the time window
1229
1262
  const cutoff = new Date(Date.now() - windowMs).toISOString();
1230
- const row = this.db.prepare(`
1231
- SELECT * FROM episodes WHERE agent_id = ? AND end_time > ?
1232
- ORDER BY end_time DESC LIMIT 1
1263
+ const row = this.db.prepare(`
1264
+ SELECT * FROM episodes WHERE agent_id = ? AND end_time > ?
1265
+ ORDER BY end_time DESC LIMIT 1
1233
1266
  `).get(agentId, cutoff);
1234
1267
  return row ? this.rowToEpisode(row) : null;
1235
1268
  }
1236
1269
  addEngramToEpisode(engramId, episodeId) {
1237
1270
  this.db.prepare('UPDATE engrams SET episode_id = ? WHERE id = ?').run(episodeId, engramId);
1238
- this.db.prepare(`
1239
- UPDATE episodes SET
1240
- engram_count = engram_count + 1,
1241
- end_time = MAX(end_time, ?)
1242
- WHERE id = ?
1271
+ this.db.prepare(`
1272
+ UPDATE episodes SET
1273
+ engram_count = engram_count + 1,
1274
+ end_time = MAX(end_time, ?)
1275
+ WHERE id = ?
1243
1276
  `).run(new Date().toISOString(), episodeId);
1244
1277
  }
1245
1278
  getEngramsByEpisode(episodeId) {
@@ -1290,51 +1323,86 @@ export class EngramStore {
1290
1323
  // --- Checkpointing ---
1291
1324
  updateAutoCheckpointWrite(agentId, engramId) {
1292
1325
  const now = new Date().toISOString();
1293
- this.db.prepare(`
1294
- INSERT INTO conscious_state (agent_id, last_write_id, last_activity_at, write_count_since_consolidation, updated_at)
1295
- VALUES (?, ?, ?, 1, ?)
1296
- ON CONFLICT(agent_id) DO UPDATE SET
1297
- last_write_id = excluded.last_write_id,
1298
- last_activity_at = excluded.last_activity_at,
1299
- write_count_since_consolidation = write_count_since_consolidation + 1,
1300
- updated_at = excluded.updated_at
1326
+ this.db.prepare(`
1327
+ INSERT INTO conscious_state (agent_id, last_write_id, last_activity_at, write_count_since_consolidation, updated_at)
1328
+ VALUES (?, ?, ?, 1, ?)
1329
+ ON CONFLICT(agent_id) DO UPDATE SET
1330
+ last_write_id = excluded.last_write_id,
1331
+ last_activity_at = excluded.last_activity_at,
1332
+ write_count_since_consolidation = write_count_since_consolidation + 1,
1333
+ updated_at = excluded.updated_at
1301
1334
  `).run(agentId, engramId, now, now);
1302
1335
  }
1303
1336
  updateAutoCheckpointRecall(agentId, context, engramIds) {
1304
1337
  const now = new Date().toISOString();
1305
- this.db.prepare(`
1306
- INSERT INTO conscious_state (agent_id, last_recall_context, last_recall_ids, last_activity_at, recall_count_since_consolidation, updated_at)
1307
- VALUES (?, ?, ?, ?, 1, ?)
1308
- ON CONFLICT(agent_id) DO UPDATE SET
1309
- last_recall_context = excluded.last_recall_context,
1310
- last_recall_ids = excluded.last_recall_ids,
1311
- last_activity_at = excluded.last_activity_at,
1312
- recall_count_since_consolidation = recall_count_since_consolidation + 1,
1313
- updated_at = excluded.updated_at
1338
+ this.db.prepare(`
1339
+ INSERT INTO conscious_state (agent_id, last_recall_context, last_recall_ids, last_activity_at, recall_count_since_consolidation, updated_at)
1340
+ VALUES (?, ?, ?, ?, 1, ?)
1341
+ ON CONFLICT(agent_id) DO UPDATE SET
1342
+ last_recall_context = excluded.last_recall_context,
1343
+ last_recall_ids = excluded.last_recall_ids,
1344
+ last_activity_at = excluded.last_activity_at,
1345
+ recall_count_since_consolidation = recall_count_since_consolidation + 1,
1346
+ updated_at = excluded.updated_at
1314
1347
  `).run(agentId, context, JSON.stringify(engramIds), now, now);
1315
1348
  }
1316
1349
  touchActivity(agentId) {
1317
1350
  const now = new Date().toISOString();
1318
- this.db.prepare(`
1319
- INSERT INTO conscious_state (agent_id, last_activity_at, updated_at)
1320
- VALUES (?, ?, ?)
1321
- ON CONFLICT(agent_id) DO UPDATE SET
1322
- last_activity_at = excluded.last_activity_at,
1323
- updated_at = excluded.updated_at
1351
+ this.db.prepare(`
1352
+ INSERT INTO conscious_state (agent_id, last_activity_at, updated_at)
1353
+ VALUES (?, ?, ?)
1354
+ ON CONFLICT(agent_id) DO UPDATE SET
1355
+ last_activity_at = excluded.last_activity_at,
1356
+ updated_at = excluded.updated_at
1324
1357
  `).run(agentId, now, now);
1325
1358
  }
1326
1359
  saveCheckpoint(agentId, state) {
1327
1360
  const now = new Date().toISOString();
1328
- this.db.prepare(`
1329
- INSERT INTO conscious_state (agent_id, execution_state, checkpoint_at, last_activity_at, updated_at)
1330
- VALUES (?, ?, ?, ?, ?)
1331
- ON CONFLICT(agent_id) DO UPDATE SET
1332
- execution_state = excluded.execution_state,
1333
- checkpoint_at = excluded.checkpoint_at,
1334
- last_activity_at = excluded.last_activity_at,
1335
- updated_at = excluded.updated_at
1361
+ this.db.prepare(`
1362
+ INSERT INTO conscious_state (agent_id, execution_state, checkpoint_at, last_activity_at, updated_at)
1363
+ VALUES (?, ?, ?, ?, ?)
1364
+ ON CONFLICT(agent_id) DO UPDATE SET
1365
+ execution_state = excluded.execution_state,
1366
+ checkpoint_at = excluded.checkpoint_at,
1367
+ last_activity_at = excluded.last_activity_at,
1368
+ updated_at = excluded.updated_at
1336
1369
  `).run(agentId, JSON.stringify(state), now, now, now);
1337
1370
  }
1371
+ /** Record entity mentions for an engram (D9 inverted index; idempotent). */
1372
+ recordEntityMentions(engramId, agentId, entities) {
1373
+ if (!entities || entities.length === 0)
1374
+ return;
1375
+ const stmt = this.db.prepare('INSERT OR IGNORE INTO entity_mentions (entity, engram_id, agent_id) VALUES (?, ?, ?)');
1376
+ for (const entity of entities)
1377
+ stmt.run(entity, engramId, agentId);
1378
+ }
1379
+ /** Indexed entities whose normalized key:value — or a registered alias — contains the term
1380
+ * (D11 query-side lookup). Alias matches resolve to their target entity. */
1381
+ searchEntities(term, limit = 8) {
1382
+ const t = term.toLowerCase().replace(/[%_]/g, '');
1383
+ if (!t)
1384
+ return [];
1385
+ const rows = this.db.prepare(`
1386
+ SELECT DISTINCT entity FROM (
1387
+ SELECT entity FROM entity_mentions WHERE entity LIKE ?
1388
+ UNION SELECT entity FROM entity_aliases WHERE alias LIKE ?
1389
+ ) LIMIT ?`).all(`%${t}%`, `%${t}%`, limit);
1390
+ return rows.map(r => r.entity);
1391
+ }
1392
+ /** Engram ids mentioning an entity (alias-resolved), optionally agent-scoped. */
1393
+ getEngramIdsByEntity(entity, agentId) {
1394
+ const resolved = this.db.prepare('SELECT entity FROM entity_aliases WHERE alias = ?')
1395
+ .get(entity.toLowerCase())?.entity ?? entity.toLowerCase();
1396
+ const rows = agentId
1397
+ ? this.db.prepare('SELECT engram_id FROM entity_mentions WHERE entity = ? AND agent_id = ?').all(resolved, agentId)
1398
+ : this.db.prepare('SELECT engram_id FROM entity_mentions WHERE entity = ?').all(resolved);
1399
+ return rows.map(r => r.engram_id);
1400
+ }
1401
+ /** Distinct agent ids present in this store (D3 whoami; includes all stages). */
1402
+ listAgentIds() {
1403
+ const rows = this.db.prepare('SELECT DISTINCT agent_id FROM engrams ORDER BY agent_id').all();
1404
+ return rows.map(r => r.agent_id);
1405
+ }
1338
1406
  getCheckpoint(agentId) {
1339
1407
  const row = this.db.prepare('SELECT * FROM conscious_state WHERE agent_id = ?').get(agentId);
1340
1408
  if (!row)
@@ -1359,20 +1427,20 @@ export class EngramStore {
1359
1427
  markConsolidation(agentId, mini) {
1360
1428
  const now = new Date().toISOString();
1361
1429
  if (mini) {
1362
- this.db.prepare(`
1363
- UPDATE conscious_state SET last_mini_consolidation_at = ?, updated_at = ? WHERE agent_id = ?
1430
+ this.db.prepare(`
1431
+ UPDATE conscious_state SET last_mini_consolidation_at = ?, updated_at = ? WHERE agent_id = ?
1364
1432
  `).run(now, now, agentId);
1365
1433
  }
1366
1434
  else {
1367
- this.db.prepare(`
1368
- UPDATE conscious_state SET
1369
- last_consolidation_at = ?,
1370
- last_mini_consolidation_at = ?,
1371
- write_count_since_consolidation = 0,
1372
- recall_count_since_consolidation = 0,
1373
- consolidation_cycle_count = consolidation_cycle_count + 1,
1374
- updated_at = ?
1375
- WHERE agent_id = ?
1435
+ this.db.prepare(`
1436
+ UPDATE conscious_state SET
1437
+ last_consolidation_at = ?,
1438
+ last_mini_consolidation_at = ?,
1439
+ write_count_since_consolidation = 0,
1440
+ recall_count_since_consolidation = 0,
1441
+ consolidation_cycle_count = consolidation_cycle_count + 1,
1442
+ updated_at = ?
1443
+ WHERE agent_id = ?
1376
1444
  `).run(now, now, now, agentId);
1377
1445
  }
1378
1446
  }
@@ -1443,10 +1511,10 @@ export class EngramStore {
1443
1511
  * sequence are excluded (they have no story-time anchor).
1444
1512
  */
1445
1513
  getLatestByTag(opts) {
1446
- let sql = `SELECT * FROM engrams
1447
- WHERE agent_id = ?
1448
- AND retracted = ?
1449
- AND stage = 'active'
1514
+ let sql = `SELECT * FROM engrams
1515
+ WHERE agent_id = ?
1516
+ AND retracted = ?
1517
+ AND stage = 'active'
1450
1518
  AND tags LIKE ?`;
1451
1519
  const params = [
1452
1520
  opts.agentId,
@@ -1486,10 +1554,10 @@ export class EngramStore {
1486
1554
  */
1487
1555
  getTopBy(opts) {
1488
1556
  // Build the base query inline (mirrors search() but adds the topBy sort).
1489
- let sql = `SELECT * FROM engrams
1490
- WHERE agent_id = ?
1491
- AND retracted = ?
1492
- AND stage = 'active'
1557
+ let sql = `SELECT * FROM engrams
1558
+ WHERE agent_id = ?
1559
+ AND retracted = ?
1560
+ AND stage = 'active'
1493
1561
  AND tags LIKE ?`;
1494
1562
  const params = [
1495
1563
  opts.agentId,
@@ -1557,11 +1625,11 @@ export class EngramStore {
1557
1625
  // Find candidate referencing engrams. LIKE-filter first to narrow,
1558
1626
  // then JSON.parse each candidate's references_json to verify the
1559
1627
  // matchEngramId and type. Cheap enough at NovelForge's scale.
1560
- const candidates = this.db.prepare(`SELECT id, references_json, created_at FROM engrams
1561
- WHERE agent_id = ?
1562
- AND retracted = 0
1563
- AND stage = 'active'
1564
- AND references_json IS NOT NULL
1628
+ const candidates = this.db.prepare(`SELECT id, references_json, created_at FROM engrams
1629
+ WHERE agent_id = ?
1630
+ AND retracted = 0
1631
+ AND stage = 'active'
1632
+ AND references_json IS NOT NULL
1565
1633
  AND references_json LIKE ?`).all(target.agentId, `%"${targetEngramId}"%`);
1566
1634
  const terminalTypes = new Set(['resolves', 'subverts', 'abandons']);
1567
1635
  const resolvingEvents = [];