agent-working-memory 0.16.1 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +321 -318
  2. package/dist/adapters/common.d.ts +34 -0
  3. package/dist/adapters/common.d.ts.map +1 -1
  4. package/dist/adapters/common.js +528 -444
  5. package/dist/adapters/common.js.map +1 -1
  6. package/dist/api/routes.d.ts.map +1 -1
  7. package/dist/api/routes.js +19 -0
  8. package/dist/api/routes.js.map +1 -1
  9. package/dist/cli/migrate.d.ts.map +1 -1
  10. package/dist/cli/migrate.js +31 -29
  11. package/dist/cli/migrate.js.map +1 -1
  12. package/dist/core/asked-value.d.ts +37 -0
  13. package/dist/core/asked-value.d.ts.map +1 -0
  14. package/dist/core/asked-value.js +77 -0
  15. package/dist/core/asked-value.js.map +1 -0
  16. package/dist/core/conflicts.d.ts +89 -0
  17. package/dist/core/conflicts.d.ts.map +1 -0
  18. package/dist/core/conflicts.js +267 -0
  19. package/dist/core/conflicts.js.map +1 -0
  20. package/dist/core/embeddings.d.ts +14 -2
  21. package/dist/core/embeddings.d.ts.map +1 -1
  22. package/dist/core/embeddings.js +32 -10
  23. package/dist/core/embeddings.js.map +1 -1
  24. package/dist/core/format-recall.d.ts +21 -1
  25. package/dist/core/format-recall.d.ts.map +1 -1
  26. package/dist/core/format-recall.js +60 -5
  27. package/dist/core/format-recall.js.map +1 -1
  28. package/dist/core/ml-worker.d.ts +24 -1
  29. package/dist/core/ml-worker.d.ts.map +1 -1
  30. package/dist/core/ml-worker.js +165 -4
  31. package/dist/core/ml-worker.js.map +1 -1
  32. package/dist/core/model-daemon-client.d.ts +77 -0
  33. package/dist/core/model-daemon-client.d.ts.map +1 -0
  34. package/dist/core/model-daemon-client.js +383 -0
  35. package/dist/core/model-daemon-client.js.map +1 -0
  36. package/dist/core/model-daemon-server.d.ts +71 -0
  37. package/dist/core/model-daemon-server.d.ts.map +1 -0
  38. package/dist/core/model-daemon-server.js +363 -0
  39. package/dist/core/model-daemon-server.js.map +1 -0
  40. package/dist/core/model-daemon.d.ts +137 -0
  41. package/dist/core/model-daemon.d.ts.map +1 -0
  42. package/dist/core/model-daemon.js +231 -0
  43. package/dist/core/model-daemon.js.map +1 -0
  44. package/dist/core/query-expander.d.ts +5 -0
  45. package/dist/core/query-expander.d.ts.map +1 -1
  46. package/dist/core/query-expander.js +3 -2
  47. package/dist/core/query-expander.js.map +1 -1
  48. package/dist/core/recall-config.d.ts +1 -1
  49. package/dist/core/recall-config.d.ts.map +1 -1
  50. package/dist/core/recall-config.js +5 -0
  51. package/dist/core/recall-config.js.map +1 -1
  52. package/dist/core/rerank-window.d.ts +7 -2
  53. package/dist/core/rerank-window.d.ts.map +1 -1
  54. package/dist/core/rerank-window.js +9 -4
  55. package/dist/core/rerank-window.js.map +1 -1
  56. package/dist/core/rerank2.d.ts +52 -2
  57. package/dist/core/rerank2.d.ts.map +1 -1
  58. package/dist/core/rerank2.js +60 -3
  59. package/dist/core/rerank2.js.map +1 -1
  60. package/dist/core/reranker.d.ts +19 -0
  61. package/dist/core/reranker.d.ts.map +1 -1
  62. package/dist/core/reranker.js +76 -12
  63. package/dist/core/reranker.js.map +1 -1
  64. package/dist/core/salience.d.ts +2 -0
  65. package/dist/core/salience.d.ts.map +1 -1
  66. package/dist/core/salience.js +45 -18
  67. package/dist/core/salience.js.map +1 -1
  68. package/dist/core/trust.d.ts +32 -0
  69. package/dist/core/trust.d.ts.map +1 -0
  70. package/dist/core/trust.js +66 -0
  71. package/dist/core/trust.js.map +1 -0
  72. package/dist/core/validity.d.ts +21 -0
  73. package/dist/core/validity.d.ts.map +1 -0
  74. package/dist/core/validity.js +41 -0
  75. package/dist/core/validity.js.map +1 -0
  76. package/dist/core/whoami.d.ts +3 -0
  77. package/dist/core/whoami.d.ts.map +1 -1
  78. package/dist/core/whoami.js +14 -0
  79. package/dist/core/whoami.js.map +1 -1
  80. package/dist/core/write-pipeline.d.ts +48 -0
  81. package/dist/core/write-pipeline.d.ts.map +1 -1
  82. package/dist/core/write-pipeline.js +104 -4
  83. package/dist/core/write-pipeline.js.map +1 -1
  84. package/dist/engine/activation.d.ts.map +1 -1
  85. package/dist/engine/activation.js +27 -4
  86. package/dist/engine/activation.js.map +1 -1
  87. package/dist/engine/consolidation.d.ts.map +1 -1
  88. package/dist/engine/consolidation.js +67 -5
  89. package/dist/engine/consolidation.js.map +1 -1
  90. package/dist/hooks/prime-query.d.ts +57 -0
  91. package/dist/hooks/prime-query.d.ts.map +1 -0
  92. package/dist/hooks/prime-query.js +388 -0
  93. package/dist/hooks/prime-query.js.map +1 -0
  94. package/dist/hooks/prime.d.ts +8 -0
  95. package/dist/hooks/prime.d.ts.map +1 -1
  96. package/dist/hooks/prime.js +21 -2
  97. package/dist/hooks/prime.js.map +1 -1
  98. package/dist/hooks/sidecar.d.ts +7 -0
  99. package/dist/hooks/sidecar.d.ts.map +1 -1
  100. package/dist/hooks/sidecar.js +10 -2
  101. package/dist/hooks/sidecar.js.map +1 -1
  102. package/dist/index.js +11 -3
  103. package/dist/index.js.map +1 -1
  104. package/dist/mcp.js +148 -103
  105. package/dist/mcp.js.map +1 -1
  106. package/dist/model-daemon.d.ts +8 -0
  107. package/dist/model-daemon.d.ts.map +1 -0
  108. package/dist/model-daemon.js +45 -0
  109. package/dist/model-daemon.js.map +1 -0
  110. package/dist/storage/factory.d.ts.map +1 -1
  111. package/dist/storage/factory.js +5 -0
  112. package/dist/storage/factory.js.map +1 -1
  113. package/dist/storage/pglite-schema.d.ts.map +1 -1
  114. package/dist/storage/pglite-schema.js +176 -170
  115. package/dist/storage/pglite-schema.js.map +1 -1
  116. package/dist/storage/pglite.d.ts +5 -0
  117. package/dist/storage/pglite.d.ts.map +1 -1
  118. package/dist/storage/pglite.js +165 -153
  119. package/dist/storage/pglite.js.map +1 -1
  120. package/dist/storage/postgres.d.ts +5 -0
  121. package/dist/storage/postgres.d.ts.map +1 -1
  122. package/dist/storage/postgres.js +165 -153
  123. package/dist/storage/postgres.js.map +1 -1
  124. package/dist/storage/sqlite.d.ts +22 -0
  125. package/dist/storage/sqlite.d.ts.map +1 -1
  126. package/dist/storage/sqlite.js +400 -363
  127. package/dist/storage/sqlite.js.map +1 -1
  128. package/dist/types/engram.d.ts +33 -0
  129. package/dist/types/engram.d.ts.map +1 -1
  130. package/dist/types/engram.js.map +1 -1
  131. package/package.json +106 -106
  132. package/plugin/.claude-plugin/plugin.json +1 -1
  133. package/plugin/README.md +1 -1
  134. package/plugin/bin/awm-mcp-launcher.cjs +2 -2
  135. package/plugin/skills/awm-memory/SKILL.md +16 -5
  136. package/src/adapters/common.ts +881 -800
  137. package/src/api/routes.ts +20 -0
  138. package/src/cli/migrate.ts +309 -307
  139. package/src/core/asked-value.ts +74 -0
  140. package/src/core/conflicts.ts +280 -0
  141. package/src/core/embeddings.ts +35 -10
  142. package/src/core/format-recall.ts +83 -25
  143. package/src/core/ml-worker.ts +458 -281
  144. package/src/core/model-daemon-client.ts +374 -0
  145. package/src/core/model-daemon-server.ts +344 -0
  146. package/src/core/model-daemon.ts +243 -0
  147. package/src/core/query-expander.ts +127 -126
  148. package/src/core/recall-config.ts +5 -0
  149. package/src/core/rerank-window.ts +164 -159
  150. package/src/core/rerank2.ts +143 -82
  151. package/src/core/reranker.ts +78 -11
  152. package/src/core/salience.ts +45 -18
  153. package/src/core/trust.ts +65 -0
  154. package/src/core/validity.ts +47 -0
  155. package/src/core/whoami.ts +136 -120
  156. package/src/core/write-pipeline.ts +143 -4
  157. package/src/engine/activation.ts +27 -4
  158. package/src/engine/consolidation.ts +950 -887
  159. package/src/hooks/prime-query.ts +375 -0
  160. package/src/hooks/prime.ts +25 -2
  161. package/src/hooks/sidecar.ts +13 -3
  162. package/src/index.ts +255 -248
  163. package/src/mcp.ts +1521 -1475
  164. package/src/model-daemon.ts +48 -0
  165. package/src/storage/factory.ts +25 -19
  166. package/src/storage/pglite-schema.ts +199 -193
  167. package/src/storage/pglite.ts +1513 -1498
  168. package/src/storage/postgres.ts +1613 -1598
  169. package/src/storage/sqlite.ts +2056 -2019
  170. package/src/types/engram.ts +30 -0
@@ -154,123 +154,123 @@ 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
- -- Serves getTopAccessedTags: the recall path's pronoun branch needs the ten
185
- -- most-accessed engrams, and without this it filters then sorts the whole
186
- -- agent partition on every query containing "it", "this" or "that".
187
- CREATE INDEX IF NOT EXISTS idx_engrams_access ON engrams(agent_id, stage, access_count DESC);
188
-
189
- CREATE TABLE IF NOT EXISTS associations (
190
- id TEXT PRIMARY KEY,
191
- from_engram_id TEXT NOT NULL REFERENCES engrams(id) ON DELETE CASCADE,
192
- to_engram_id TEXT NOT NULL REFERENCES engrams(id) ON DELETE CASCADE,
193
- weight REAL NOT NULL DEFAULT 0.1,
194
- confidence REAL NOT NULL DEFAULT 0.5,
195
- type TEXT NOT NULL DEFAULT 'hebbian',
196
- activation_count INTEGER NOT NULL DEFAULT 0,
197
- created_at TEXT NOT NULL,
198
- last_activated TEXT NOT NULL
199
- );
200
-
201
- CREATE INDEX IF NOT EXISTS idx_assoc_from ON associations(from_engram_id);
202
- CREATE INDEX IF NOT EXISTS idx_assoc_to ON associations(to_engram_id);
203
- CREATE UNIQUE INDEX IF NOT EXISTS idx_assoc_pair ON associations(from_engram_id, to_engram_id);
204
-
205
- CREATE TABLE IF NOT EXISTS agents (
206
- id TEXT PRIMARY KEY,
207
- name TEXT NOT NULL,
208
- created_at TEXT NOT NULL,
209
- config TEXT NOT NULL DEFAULT '{}'
210
- );
211
-
212
- -- FTS5 for full-text search (BM25 ranking built in)
213
- CREATE VIRTUAL TABLE IF NOT EXISTS engrams_fts USING fts5(
214
- concept, content, tags,
215
- content=engrams,
216
- content_rowid=rowid
217
- );
218
-
219
- -- Triggers to keep FTS in sync
220
- CREATE TRIGGER IF NOT EXISTS engrams_ai AFTER INSERT ON engrams BEGIN
221
- INSERT INTO engrams_fts(rowid, concept, content, tags) VALUES (new.rowid, new.concept, new.content, new.tags);
222
- END;
223
- CREATE TRIGGER IF NOT EXISTS engrams_ad AFTER DELETE ON engrams BEGIN
224
- INSERT INTO engrams_fts(engrams_fts, rowid, concept, content, tags) VALUES('delete', old.rowid, old.concept, old.content, old.tags);
225
- END;
226
- CREATE TRIGGER IF NOT EXISTS engrams_au AFTER UPDATE ON engrams BEGIN
227
- INSERT INTO engrams_fts(engrams_fts, rowid, concept, content, tags) VALUES('delete', old.rowid, old.concept, old.content, old.tags);
228
- INSERT INTO engrams_fts(rowid, concept, content, tags) VALUES (new.rowid, new.concept, new.content, new.tags);
229
- END;
230
-
231
- -- Eval event logs
232
- CREATE TABLE IF NOT EXISTS activation_events (
233
- id TEXT PRIMARY KEY,
234
- agent_id TEXT NOT NULL,
235
- timestamp TEXT NOT NULL,
236
- context TEXT NOT NULL,
237
- results_returned INTEGER NOT NULL,
238
- top_score REAL,
239
- latency_ms REAL NOT NULL,
240
- engram_ids TEXT NOT NULL DEFAULT '[]'
241
- );
242
-
243
- CREATE TABLE IF NOT EXISTS staging_events (
244
- engram_id TEXT NOT NULL,
245
- agent_id TEXT NOT NULL,
246
- action TEXT NOT NULL,
247
- resonance_score REAL,
248
- timestamp TEXT NOT NULL,
249
- age_ms INTEGER NOT NULL
250
- );
251
-
252
- CREATE TABLE IF NOT EXISTS retrieval_feedback (
253
- id TEXT PRIMARY KEY,
254
- activation_event_id TEXT,
255
- engram_id TEXT NOT NULL,
256
- useful INTEGER NOT NULL,
257
- context TEXT,
258
- timestamp TEXT NOT NULL
259
- );
260
-
261
- CREATE TABLE IF NOT EXISTS episodes (
262
- id TEXT PRIMARY KEY,
263
- agent_id TEXT NOT NULL,
264
- label TEXT NOT NULL,
265
- embedding BLOB,
266
- engram_count INTEGER NOT NULL DEFAULT 0,
267
- start_time TEXT NOT NULL,
268
- end_time TEXT NOT NULL,
269
- created_at TEXT NOT NULL
270
- );
271
-
272
- CREATE INDEX IF NOT EXISTS idx_episodes_agent ON episodes(agent_id);
273
- 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
+ -- Serves getTopAccessedTags: the recall path's pronoun branch needs the ten
185
+ -- most-accessed engrams, and without this it filters then sorts the whole
186
+ -- agent partition on every query containing "it", "this" or "that".
187
+ CREATE INDEX IF NOT EXISTS idx_engrams_access ON engrams(agent_id, stage, access_count DESC);
188
+
189
+ CREATE TABLE IF NOT EXISTS associations (
190
+ id TEXT PRIMARY KEY,
191
+ from_engram_id TEXT NOT NULL REFERENCES engrams(id) ON DELETE CASCADE,
192
+ to_engram_id TEXT NOT NULL REFERENCES engrams(id) ON DELETE CASCADE,
193
+ weight REAL NOT NULL DEFAULT 0.1,
194
+ confidence REAL NOT NULL DEFAULT 0.5,
195
+ type TEXT NOT NULL DEFAULT 'hebbian',
196
+ activation_count INTEGER NOT NULL DEFAULT 0,
197
+ created_at TEXT NOT NULL,
198
+ last_activated TEXT NOT NULL
199
+ );
200
+
201
+ CREATE INDEX IF NOT EXISTS idx_assoc_from ON associations(from_engram_id);
202
+ CREATE INDEX IF NOT EXISTS idx_assoc_to ON associations(to_engram_id);
203
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_assoc_pair ON associations(from_engram_id, to_engram_id);
204
+
205
+ CREATE TABLE IF NOT EXISTS agents (
206
+ id TEXT PRIMARY KEY,
207
+ name TEXT NOT NULL,
208
+ created_at TEXT NOT NULL,
209
+ config TEXT NOT NULL DEFAULT '{}'
210
+ );
211
+
212
+ -- FTS5 for full-text search (BM25 ranking built in)
213
+ CREATE VIRTUAL TABLE IF NOT EXISTS engrams_fts USING fts5(
214
+ concept, content, tags,
215
+ content=engrams,
216
+ content_rowid=rowid
217
+ );
218
+
219
+ -- Triggers to keep FTS in sync
220
+ CREATE TRIGGER IF NOT EXISTS engrams_ai AFTER INSERT ON engrams BEGIN
221
+ INSERT INTO engrams_fts(rowid, concept, content, tags) VALUES (new.rowid, new.concept, new.content, new.tags);
222
+ END;
223
+ CREATE TRIGGER IF NOT EXISTS engrams_ad AFTER DELETE ON engrams BEGIN
224
+ INSERT INTO engrams_fts(engrams_fts, rowid, concept, content, tags) VALUES('delete', old.rowid, old.concept, old.content, old.tags);
225
+ END;
226
+ CREATE TRIGGER IF NOT EXISTS engrams_au AFTER UPDATE ON engrams BEGIN
227
+ INSERT INTO engrams_fts(engrams_fts, rowid, concept, content, tags) VALUES('delete', old.rowid, old.concept, old.content, old.tags);
228
+ INSERT INTO engrams_fts(rowid, concept, content, tags) VALUES (new.rowid, new.concept, new.content, new.tags);
229
+ END;
230
+
231
+ -- Eval event logs
232
+ CREATE TABLE IF NOT EXISTS activation_events (
233
+ id TEXT PRIMARY KEY,
234
+ agent_id TEXT NOT NULL,
235
+ timestamp TEXT NOT NULL,
236
+ context TEXT NOT NULL,
237
+ results_returned INTEGER NOT NULL,
238
+ top_score REAL,
239
+ latency_ms REAL NOT NULL,
240
+ engram_ids TEXT NOT NULL DEFAULT '[]'
241
+ );
242
+
243
+ CREATE TABLE IF NOT EXISTS staging_events (
244
+ engram_id TEXT NOT NULL,
245
+ agent_id TEXT NOT NULL,
246
+ action TEXT NOT NULL,
247
+ resonance_score REAL,
248
+ timestamp TEXT NOT NULL,
249
+ age_ms INTEGER NOT NULL
250
+ );
251
+
252
+ CREATE TABLE IF NOT EXISTS retrieval_feedback (
253
+ id TEXT PRIMARY KEY,
254
+ activation_event_id TEXT,
255
+ engram_id TEXT NOT NULL,
256
+ useful INTEGER NOT NULL,
257
+ context TEXT,
258
+ timestamp TEXT NOT NULL
259
+ );
260
+
261
+ CREATE TABLE IF NOT EXISTS episodes (
262
+ id TEXT PRIMARY KEY,
263
+ agent_id TEXT NOT NULL,
264
+ label TEXT NOT NULL,
265
+ embedding BLOB,
266
+ engram_count INTEGER NOT NULL DEFAULT 0,
267
+ start_time TEXT NOT NULL,
268
+ end_time TEXT NOT NULL,
269
+ created_at TEXT NOT NULL
270
+ );
271
+
272
+ CREATE INDEX IF NOT EXISTS idx_episodes_agent ON episodes(agent_id);
273
+ CREATE INDEX IF NOT EXISTS idx_episodes_time ON episodes(agent_id, end_time);
274
274
  `);
275
275
  // Migration: add episode_id column if missing
276
276
  try {
@@ -284,10 +284,10 @@ export class EngramStore {
284
284
  this.db.prepare('SELECT task_status FROM engrams LIMIT 0').get();
285
285
  }
286
286
  catch {
287
- this.db.exec(`
288
- ALTER TABLE engrams ADD COLUMN task_status TEXT;
289
- ALTER TABLE engrams ADD COLUMN task_priority TEXT;
290
- ALTER TABLE engrams ADD COLUMN blocked_by TEXT;
287
+ this.db.exec(`
288
+ ALTER TABLE engrams ADD COLUMN task_status TEXT;
289
+ ALTER TABLE engrams ADD COLUMN task_priority TEXT;
290
+ ALTER TABLE engrams ADD COLUMN blocked_by TEXT;
291
291
  `);
292
292
  this.db.exec('CREATE INDEX IF NOT EXISTS idx_engrams_task ON engrams(agent_id, task_status)');
293
293
  }
@@ -296,10 +296,10 @@ export class EngramStore {
296
296
  this.db.prepare('SELECT memory_class FROM engrams LIMIT 0').get();
297
297
  }
298
298
  catch {
299
- this.db.exec(`
300
- ALTER TABLE engrams ADD COLUMN memory_class TEXT NOT NULL DEFAULT 'working';
301
- ALTER TABLE engrams ADD COLUMN superseded_by TEXT;
302
- ALTER TABLE engrams ADD COLUMN supersedes TEXT;
299
+ this.db.exec(`
300
+ ALTER TABLE engrams ADD COLUMN memory_class TEXT NOT NULL DEFAULT 'working';
301
+ ALTER TABLE engrams ADD COLUMN superseded_by TEXT;
302
+ ALTER TABLE engrams ADD COLUMN supersedes TEXT;
303
303
  `);
304
304
  }
305
305
  // Migration: add embedding_model for version tracking (prevents drift on model change)
@@ -311,49 +311,57 @@ export class EngramStore {
311
311
  }
312
312
  // Entity inverted index + alias table (D9, 2026-07-30) — write-time
313
313
  // bookkeeping only; retrieval unchanged until D11. Idempotent CREATEs.
314
- this.db.exec(`
315
- CREATE TABLE IF NOT EXISTS entity_mentions (
316
- entity TEXT NOT NULL,
317
- engram_id TEXT NOT NULL,
318
- agent_id TEXT NOT NULL,
319
- PRIMARY KEY (entity, engram_id)
320
- );
321
- CREATE INDEX IF NOT EXISTS idx_entity_mentions_agent ON entity_mentions(agent_id, entity);
322
- CREATE TABLE IF NOT EXISTS entity_aliases (
323
- alias TEXT PRIMARY KEY,
324
- entity TEXT NOT NULL
325
- );
314
+ this.db.exec(`
315
+ CREATE TABLE IF NOT EXISTS entity_mentions (
316
+ entity TEXT NOT NULL,
317
+ engram_id TEXT NOT NULL,
318
+ agent_id TEXT NOT NULL,
319
+ PRIMARY KEY (entity, engram_id)
320
+ );
321
+ CREATE INDEX IF NOT EXISTS idx_entity_mentions_agent ON entity_mentions(agent_id, entity);
322
+ CREATE TABLE IF NOT EXISTS entity_aliases (
323
+ alias TEXT PRIMARY KEY,
324
+ entity TEXT NOT NULL
325
+ );
326
326
  `);
327
327
  // Migration: memory-spine provenance + temporal validity (D5/D8, 2026-07-30)
328
328
  try {
329
329
  this.db.prepare('SELECT origin_class FROM engrams LIMIT 0').get();
330
330
  }
331
331
  catch {
332
- this.db.exec(`
333
- ALTER TABLE engrams ADD COLUMN origin_class TEXT;
334
- ALTER TABLE engrams ADD COLUMN writer_session TEXT;
335
- ALTER TABLE engrams ADD COLUMN recipe_id TEXT;
336
- ALTER TABLE engrams ADD COLUMN valid_from TEXT;
337
- ALTER TABLE engrams ADD COLUMN valid_to TEXT;
332
+ this.db.exec(`
333
+ ALTER TABLE engrams ADD COLUMN origin_class TEXT;
334
+ ALTER TABLE engrams ADD COLUMN writer_session TEXT;
335
+ ALTER TABLE engrams ADD COLUMN recipe_id TEXT;
336
+ ALTER TABLE engrams ADD COLUMN valid_from TEXT;
337
+ ALTER TABLE engrams ADD COLUMN valid_to TEXT;
338
338
  `);
339
339
  }
340
+ // Migration: stage-transition timestamp (A-4, 2026-10-12). Additive, nullable,
341
+ // no backfill — null means "never transitioned", so readers fall back to created_at.
342
+ try {
343
+ this.db.prepare('SELECT stage_changed_at FROM engrams LIMIT 0').get();
344
+ }
345
+ catch {
346
+ this.db.exec(`ALTER TABLE engrams ADD COLUMN stage_changed_at TEXT`);
347
+ }
340
348
  // Migration: add conscious_state table for checkpointing
341
- this.db.exec(`
342
- CREATE TABLE IF NOT EXISTS conscious_state (
343
- agent_id TEXT PRIMARY KEY,
344
- last_write_id TEXT,
345
- last_recall_context TEXT,
346
- last_recall_ids TEXT NOT NULL DEFAULT '[]',
347
- last_activity_at TEXT NOT NULL DEFAULT (datetime('now')),
348
- write_count_since_consolidation INTEGER NOT NULL DEFAULT 0,
349
- recall_count_since_consolidation INTEGER NOT NULL DEFAULT 0,
350
- execution_state TEXT,
351
- checkpoint_at TEXT,
352
- last_consolidation_at TEXT,
353
- last_mini_consolidation_at TEXT,
354
- consolidation_cycle_count INTEGER NOT NULL DEFAULT 0,
355
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
356
- )
349
+ this.db.exec(`
350
+ CREATE TABLE IF NOT EXISTS conscious_state (
351
+ agent_id TEXT PRIMARY KEY,
352
+ last_write_id TEXT,
353
+ last_recall_context TEXT,
354
+ last_recall_ids TEXT NOT NULL DEFAULT '[]',
355
+ last_activity_at TEXT NOT NULL DEFAULT (datetime('now')),
356
+ write_count_since_consolidation INTEGER NOT NULL DEFAULT 0,
357
+ recall_count_since_consolidation INTEGER NOT NULL DEFAULT 0,
358
+ execution_state TEXT,
359
+ checkpoint_at TEXT,
360
+ last_consolidation_at TEXT,
361
+ last_mini_consolidation_at TEXT,
362
+ consolidation_cycle_count INTEGER NOT NULL DEFAULT 0,
363
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
364
+ )
357
365
  `);
358
366
  // Migration: add consolidation_cycle_count if missing (existing DBs)
359
367
  try {
@@ -374,7 +382,7 @@ export class EngramStore {
374
382
  }
375
383
  catch {
376
384
  this.db.exec(`ALTER TABLE engrams ADD COLUMN sequence INTEGER`);
377
- this.db.exec(`CREATE INDEX IF NOT EXISTS idx_engrams_agent_sequence
385
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idx_engrams_agent_sequence
378
386
  ON engrams(agent_id, sequence) WHERE sequence IS NOT NULL`);
379
387
  }
380
388
  // Migration (0.8 Cluster A): references_json column for typed cross-record
@@ -393,12 +401,12 @@ export class EngramStore {
393
401
  const embeddingBlob = input.embedding
394
402
  ? Buffer.from(new Float32Array(input.embedding).buffer)
395
403
  : null;
396
- this.db.prepare(`
397
- INSERT INTO engrams (id, agent_id, concept, content, embedding, confidence, salience,
398
- access_count, last_accessed, created_at, salience_features, reason_codes, stage, tags, episode_id,
399
- ttl, memory_class, supersedes, task_status, task_priority, blocked_by, memory_type,
400
- sequence, references_json, origin_class, writer_session, recipe_id, valid_from, valid_to)
401
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
404
+ this.db.prepare(`
405
+ INSERT INTO engrams (id, agent_id, concept, content, embedding, confidence, salience,
406
+ access_count, last_accessed, created_at, salience_features, reason_codes, stage, tags, episode_id,
407
+ ttl, memory_class, supersedes, task_status, task_priority, blocked_by, memory_type,
408
+ sequence, references_json, origin_class, writer_session, recipe_id, valid_from, valid_to)
409
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
402
410
  `).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
403
411
  ? JSON.stringify(input.references) : null, input.originClass ?? null, input.writerSession ?? null, input.recipeId ?? null, input.validFrom ?? null, input.validTo ?? null);
404
412
  // Add to slim cache (skip if not yet populated — first slim fetch will load it)
@@ -578,11 +586,11 @@ export class EngramStore {
578
586
  getTopAccessedTags(agentIds, topN, stage = 'active') {
579
587
  if (agentIds.length === 0 || topN <= 0)
580
588
  return [];
581
- const stmt = this.db.prepare(`
582
- SELECT tags, access_count AS accessCount, id FROM engrams
583
- WHERE agent_id = ? AND stage = ? AND retracted = 0
584
- ORDER BY access_count DESC, id
585
- LIMIT ?
589
+ const stmt = this.db.prepare(`
590
+ SELECT tags, access_count AS accessCount, id FROM engrams
591
+ WHERE agent_id = ? AND stage = ? AND retracted = 0
592
+ ORDER BY access_count DESC, id
593
+ LIMIT ?
586
594
  `);
587
595
  const rows = [];
588
596
  for (const agentId of agentIds) {
@@ -620,15 +628,15 @@ export class EngramStore {
620
628
  try {
621
629
  const placeholders = agentIds.map(() => '?').join(',');
622
630
  const innerLimit = Math.max(limit * 5, 50);
623
- const rows = this.db.prepare(`
624
- WITH top_fts AS (
625
- SELECT rowid, rank FROM engrams_fts WHERE engrams_fts MATCH ? ORDER BY rank LIMIT ?
626
- )
627
- SELECT e.*, top_fts.rank FROM top_fts
628
- JOIN engrams e ON e.rowid = top_fts.rowid
629
- WHERE e.agent_id IN (${placeholders}) AND e.retracted = 0
630
- ORDER BY top_fts.rank
631
- LIMIT ?
631
+ const rows = this.db.prepare(`
632
+ WITH top_fts AS (
633
+ SELECT rowid, rank FROM engrams_fts WHERE engrams_fts MATCH ? ORDER BY rank LIMIT ?
634
+ )
635
+ SELECT e.*, top_fts.rank FROM top_fts
636
+ JOIN engrams e ON e.rowid = top_fts.rowid
637
+ WHERE e.agent_id IN (${placeholders}) AND e.retracted = 0
638
+ ORDER BY top_fts.rank
639
+ LIMIT ?
632
640
  `).all(sanitized, innerLimit, ...agentIds, limit);
633
641
  return rows.map(r => ({
634
642
  engram: this.rowToEngram(r),
@@ -667,16 +675,22 @@ export class EngramStore {
667
675
  * Boost: +0.02 per access, scaled by 1/sqrt(accessCount+1), capped at 0.85.
668
676
  */
669
677
  touchEngram(id) {
670
- this.db.prepare(`
671
- UPDATE engrams
672
- SET access_count = access_count + 1,
673
- last_accessed = ?,
674
- confidence = MIN(0.85, confidence + 0.02 / (1.0 + sqrt(access_count)))
675
- WHERE id = ?
678
+ this.db.prepare(`
679
+ UPDATE engrams
680
+ SET access_count = access_count + 1,
681
+ last_accessed = ?,
682
+ confidence = MIN(0.85, confidence + 0.02 / (1.0 + sqrt(access_count)))
683
+ WHERE id = ?
676
684
  `).run(new Date().toISOString(), id);
677
685
  }
686
+ /**
687
+ * Move an engram to a new stage, recording WHEN. The timestamp is written here
688
+ * rather than at the six consolidation call sites so a future transition cannot
689
+ * forget it; see `Engram.stageChangedAt` for why the column exists.
690
+ */
678
691
  updateStage(id, stage) {
679
- this.db.prepare('UPDATE engrams SET stage = ? WHERE id = ?').run(stage, id);
692
+ this.db.prepare('UPDATE engrams SET stage = ?, stage_changed_at = ? WHERE id = ?')
693
+ .run(stage, new Date().toISOString(), id);
680
694
  this.cacheUpdateStage(id, stage);
681
695
  }
682
696
  /**
@@ -702,13 +716,35 @@ export class EngramStore {
702
716
  this.cacheUpdateEmbedding(id, embedding);
703
717
  }
704
718
  retractEngram(id, retractedBy) {
705
- this.db.prepare(`
706
- UPDATE engrams SET retracted = 1, retracted_by = ?, retracted_at = ? WHERE id = ?
719
+ this.db.prepare(`
720
+ UPDATE engrams SET retracted = 1, retracted_by = ?, retracted_at = ? WHERE id = ?
707
721
  `).run(retractedBy, new Date().toISOString(), id);
708
722
  this.cacheRetract(id);
709
723
  }
724
+ /**
725
+ * Delete an engram and everything that points at it.
726
+ *
727
+ * `associations` cascades (its FKs declare ON DELETE CASCADE), but
728
+ * `retrieval_feedback`, `entity_mentions` and `staging_events` carry a bare
729
+ * `engram_id TEXT` with no foreign key, so deleting an engram used to strand their
730
+ * rows. Measured on the live store 2026-10-12: **571 orphaned retrieval_feedback
731
+ * rows and 3 orphaned entity_mentions**, present in the pre-purge backup too, so
732
+ * they accumulated over time rather than in one incident. Inert for recall, but
733
+ * they corrupt any join that assumes referential integrity — including the
734
+ * feedback-weighted scoring channel's own counts.
735
+ *
736
+ * One transaction, so a crash cannot leave the half-deleted state this is fixing.
737
+ * `activation_events` is deliberately NOT touched: its `engram_ids` is a JSON array
738
+ * describing a recall that really happened, and editing history to remove one id
739
+ * would falsify the event rather than tidy it.
740
+ */
710
741
  deleteEngram(id) {
711
- this.db.prepare('DELETE FROM engrams WHERE id = ?').run(id);
742
+ this.db.transaction(() => {
743
+ this.db.prepare('DELETE FROM retrieval_feedback WHERE engram_id = ?').run(id);
744
+ this.db.prepare('DELETE FROM entity_mentions WHERE engram_id = ?').run(id);
745
+ this.db.prepare('DELETE FROM staging_events WHERE engram_id = ?').run(id);
746
+ this.db.prepare('DELETE FROM engrams WHERE id = ?').run(id);
747
+ })();
712
748
  this.cacheRemove(id);
713
749
  }
714
750
  /**
@@ -720,20 +756,20 @@ export class EngramStore {
720
756
  let count = 0;
721
757
  const shiftSec = Math.round(ms / 1000);
722
758
  // Shift engram timestamps
723
- const r1 = this.db.prepare(`
724
- UPDATE engrams SET
725
- created_at = datetime(created_at, '-${shiftSec} seconds'),
726
- last_accessed = datetime(last_accessed, '-${shiftSec} seconds')
727
- WHERE agent_id = ?
759
+ const r1 = this.db.prepare(`
760
+ UPDATE engrams SET
761
+ created_at = datetime(created_at, '-${shiftSec} seconds'),
762
+ last_accessed = datetime(last_accessed, '-${shiftSec} seconds')
763
+ WHERE agent_id = ?
728
764
  `).run(agentId);
729
765
  count += r1.changes;
730
766
  // Shift association timestamps
731
- const r2 = this.db.prepare(`
732
- UPDATE associations SET
733
- created_at = datetime(created_at, '-${shiftSec} seconds'),
734
- last_activated = datetime(last_activated, '-${shiftSec} seconds')
735
- WHERE from_engram_id IN (SELECT id FROM engrams WHERE agent_id = ?)
736
- OR to_engram_id IN (SELECT id FROM engrams WHERE agent_id = ?)
767
+ const r2 = this.db.prepare(`
768
+ UPDATE associations SET
769
+ created_at = datetime(created_at, '-${shiftSec} seconds'),
770
+ last_activated = datetime(last_activated, '-${shiftSec} seconds')
771
+ WHERE from_engram_id IN (SELECT id FROM engrams WHERE agent_id = ?)
772
+ OR to_engram_id IN (SELECT id FROM engrams WHERE agent_id = ?)
737
773
  `).run(agentId, agentId);
738
774
  count += r2.changes;
739
775
  return count;
@@ -812,15 +848,15 @@ export class EngramStore {
812
848
  // dominant, low retracted rate).
813
849
  try {
814
850
  const innerLimit = Math.max(limit * 5, 50);
815
- const rows = this.db.prepare(`
816
- WITH top_fts AS (
817
- SELECT rowid, rank FROM engrams_fts WHERE engrams_fts MATCH ? ORDER BY rank LIMIT ?
818
- )
819
- SELECT e.*, top_fts.rank FROM top_fts
820
- JOIN engrams e ON e.rowid = top_fts.rowid
821
- WHERE e.agent_id = ? AND e.retracted = 0
822
- ORDER BY top_fts.rank
823
- LIMIT ?
851
+ const rows = this.db.prepare(`
852
+ WITH top_fts AS (
853
+ SELECT rowid, rank FROM engrams_fts WHERE engrams_fts MATCH ? ORDER BY rank LIMIT ?
854
+ )
855
+ SELECT e.*, top_fts.rank FROM top_fts
856
+ JOIN engrams e ON e.rowid = top_fts.rowid
857
+ WHERE e.agent_id = ? AND e.retracted = 0
858
+ ORDER BY top_fts.rank
859
+ LIMIT ?
824
860
  `).all(sanitized, innerLimit, agentId, limit);
825
861
  return rows.map(r => ({
826
862
  engram: this.rowToEngram(r),
@@ -940,14 +976,14 @@ export class EngramStore {
940
976
  sql += ' AND task_status = ?';
941
977
  params.push(status);
942
978
  }
943
- sql += ` ORDER BY
944
- CASE task_priority
945
- WHEN 'urgent' THEN 0
946
- WHEN 'high' THEN 1
947
- WHEN 'medium' THEN 2
948
- WHEN 'low' THEN 3
949
- ELSE 4
950
- END,
979
+ sql += ` ORDER BY
980
+ CASE task_priority
981
+ WHEN 'urgent' THEN 0
982
+ WHEN 'high' THEN 1
983
+ WHEN 'medium' THEN 2
984
+ WHEN 'low' THEN 3
985
+ ELSE 4
986
+ END,
951
987
  created_at DESC`;
952
988
  return this.db.prepare(sql).all(...params).map(r => this.rowToEngram(r));
953
989
  }
@@ -955,20 +991,20 @@ export class EngramStore {
955
991
  * Get the next actionable task — highest priority that's not blocked or done.
956
992
  */
957
993
  getNextTask(agentId) {
958
- const row = this.db.prepare(`
959
- SELECT * FROM engrams
960
- WHERE agent_id = ? AND task_status IN ('open', 'in_progress') AND retracted = 0
961
- ORDER BY
962
- CASE task_status WHEN 'in_progress' THEN 0 ELSE 1 END,
963
- CASE task_priority
964
- WHEN 'urgent' THEN 0
965
- WHEN 'high' THEN 1
966
- WHEN 'medium' THEN 2
967
- WHEN 'low' THEN 3
968
- ELSE 4
969
- END,
970
- created_at ASC
971
- LIMIT 1
994
+ const row = this.db.prepare(`
995
+ SELECT * FROM engrams
996
+ WHERE agent_id = ? AND task_status IN ('open', 'in_progress') AND retracted = 0
997
+ ORDER BY
998
+ CASE task_status WHEN 'in_progress' THEN 0 ELSE 1 END,
999
+ CASE task_priority
1000
+ WHEN 'urgent' THEN 0
1001
+ WHEN 'high' THEN 1
1002
+ WHEN 'medium' THEN 2
1003
+ WHEN 'low' THEN 3
1004
+ ELSE 4
1005
+ END,
1006
+ created_at ASC
1007
+ LIMIT 1
972
1008
  `).get(agentId);
973
1009
  return row ? this.rowToEngram(row) : null;
974
1010
  }
@@ -990,11 +1026,11 @@ export class EngramStore {
990
1026
  * Returns null if no match.
991
1027
  */
992
1028
  findActiveMatchByConcept(agentId, concept, requiredTags) {
993
- let sql = `SELECT * FROM engrams
994
- WHERE agent_id = ?
995
- AND LOWER(TRIM(concept)) = LOWER(TRIM(?))
996
- AND stage = 'active'
997
- AND retracted = 0
1029
+ let sql = `SELECT * FROM engrams
1030
+ WHERE agent_id = ?
1031
+ AND LOWER(TRIM(concept)) = LOWER(TRIM(?))
1032
+ AND stage = 'active'
1033
+ AND retracted = 0
998
1034
  AND superseded_by IS NULL`;
999
1035
  const params = [agentId, concept];
1000
1036
  if (requiredTags && requiredTags.length > 0) {
@@ -1029,11 +1065,11 @@ export class EngramStore {
1029
1065
  upsertAssociation(fromId, toId, weight, type = 'hebbian', confidence = 0.5) {
1030
1066
  const now = new Date().toISOString();
1031
1067
  const id = randomUUID();
1032
- this.db.prepare(`
1033
- INSERT INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type, activation_count, created_at, last_activated)
1034
- VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
1035
- ON CONFLICT(from_engram_id, to_engram_id) DO UPDATE SET
1036
- weight = ?, confidence = ?, last_activated = ?, activation_count = activation_count + 1
1068
+ this.db.prepare(`
1069
+ INSERT INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type, activation_count, created_at, last_activated)
1070
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
1071
+ ON CONFLICT(from_engram_id, to_engram_id) DO UPDATE SET
1072
+ weight = ?, confidence = ?, last_activated = ?, activation_count = activation_count + 1
1037
1073
  `).run(id, fromId, toId, weight, confidence, type, now, now, weight, confidence, now);
1038
1074
  return this.getAssociation(fromId, toId);
1039
1075
  }
@@ -1073,12 +1109,12 @@ export class EngramStore {
1073
1109
  // associations under both their from and to engram. Self-loops would be
1074
1110
  // double-counted, but they're rare in practice and the prior code handled them
1075
1111
  // identically.
1076
- const rows = this.db.prepare(`SELECT id, SUM(cnt) AS count, SUM(sw) AS sum_weight FROM (
1077
- SELECT from_engram_id AS id, 1 AS cnt, weight AS sw FROM associations WHERE from_engram_id IN (${placeholders})
1078
- UNION ALL
1079
- SELECT to_engram_id AS id, 1 AS cnt, weight AS sw FROM associations WHERE to_engram_id IN (${placeholders})
1080
- )
1081
- WHERE id IN (${placeholders})
1112
+ const rows = this.db.prepare(`SELECT id, SUM(cnt) AS count, SUM(sw) AS sum_weight FROM (
1113
+ SELECT from_engram_id AS id, 1 AS cnt, weight AS sw FROM associations WHERE from_engram_id IN (${placeholders})
1114
+ UNION ALL
1115
+ SELECT to_engram_id AS id, 1 AS cnt, weight AS sw FROM associations WHERE to_engram_id IN (${placeholders})
1116
+ )
1117
+ WHERE id IN (${placeholders})
1082
1118
  GROUP BY id`).all(...chunk, ...chunk, ...chunk);
1083
1119
  for (const r of rows) {
1084
1120
  result.set(r.id, { count: r.count, sumWeight: r.sum_weight });
@@ -1109,7 +1145,7 @@ export class EngramStore {
1109
1145
  for (let i = 0; i < engramIds.length; i += CHUNK) {
1110
1146
  const chunk = engramIds.slice(i, i + CHUNK);
1111
1147
  const placeholders = chunk.map(() => '?').join(',');
1112
- const rows = this.db.prepare(`SELECT * FROM associations
1148
+ const rows = this.db.prepare(`SELECT * FROM associations
1113
1149
  WHERE from_engram_id IN (${placeholders}) OR to_engram_id IN (${placeholders})`).all(...chunk, ...chunk);
1114
1150
  for (const r of rows) {
1115
1151
  const a = this.rowToAssociation(r);
@@ -1148,22 +1184,22 @@ export class EngramStore {
1148
1184
  this.db.prepare('DELETE FROM associations WHERE id = ?').run(id);
1149
1185
  }
1150
1186
  getAllAssociations(agentId) {
1151
- const rows = this.db.prepare(`
1152
- SELECT a.* FROM associations a
1153
- JOIN engrams e ON a.from_engram_id = e.id
1154
- WHERE e.agent_id = ?
1187
+ const rows = this.db.prepare(`
1188
+ SELECT a.* FROM associations a
1189
+ JOIN engrams e ON a.from_engram_id = e.id
1190
+ WHERE e.agent_id = ?
1155
1191
  `).all(agentId);
1156
1192
  return rows.map(r => this.rowToAssociation(r));
1157
1193
  }
1158
1194
  // --- Eviction ---
1159
1195
  getEvictionCandidates(agentId, limit) {
1160
1196
  // Lowest combined score: low salience + low access + low confidence + oldest
1161
- const rows = this.db.prepare(`
1162
- SELECT * FROM engrams
1163
- WHERE agent_id = ? AND stage = 'active' AND retracted = 0
1164
- ORDER BY (salience * 0.3 + confidence * 0.3 + (CAST(access_count AS REAL) / (access_count + 5)) * 0.2 +
1165
- (1.0 / (1.0 + (julianday('now') - julianday(last_accessed)))) * 0.2) ASC
1166
- LIMIT ?
1197
+ const rows = this.db.prepare(`
1198
+ SELECT * FROM engrams
1199
+ WHERE agent_id = ? AND stage = 'active' AND retracted = 0
1200
+ ORDER BY (salience * 0.3 + confidence * 0.3 + (CAST(access_count AS REAL) / (access_count + 5)) * 0.2 +
1201
+ (1.0 / (1.0 + (julianday('now') - julianday(last_accessed)))) * 0.2) ASC
1202
+ LIMIT ?
1167
1203
  `).all(agentId, limit);
1168
1204
  return rows.map(r => this.rowToEngram(r));
1169
1205
  }
@@ -1178,8 +1214,8 @@ export class EngramStore {
1178
1214
  // --- Staging buffer ---
1179
1215
  getExpiredStaging() {
1180
1216
  const now = Date.now();
1181
- const rows = this.db.prepare(`
1182
- SELECT * FROM engrams WHERE stage = 'staging' AND ttl IS NOT NULL
1217
+ const rows = this.db.prepare(`
1218
+ SELECT * FROM engrams WHERE stage = 'staging' AND ttl IS NOT NULL
1183
1219
  `).all();
1184
1220
  return rows
1185
1221
  .map(r => this.rowToEngram(r))
@@ -1187,53 +1223,53 @@ export class EngramStore {
1187
1223
  }
1188
1224
  // --- Eval event logging ---
1189
1225
  logActivationEvent(event) {
1190
- this.db.prepare(`
1191
- INSERT INTO activation_events (id, agent_id, timestamp, context, results_returned, top_score, latency_ms, engram_ids)
1192
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1226
+ this.db.prepare(`
1227
+ INSERT INTO activation_events (id, agent_id, timestamp, context, results_returned, top_score, latency_ms, engram_ids)
1228
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1193
1229
  `).run(event.id, event.agentId, event.timestamp.toISOString(), event.context, event.resultsReturned, event.topScore, event.latencyMs, JSON.stringify(event.engramIds));
1194
1230
  }
1195
1231
  logStagingEvent(event) {
1196
- this.db.prepare(`
1197
- INSERT INTO staging_events (engram_id, agent_id, action, resonance_score, timestamp, age_ms)
1198
- VALUES (?, ?, ?, ?, ?, ?)
1232
+ this.db.prepare(`
1233
+ INSERT INTO staging_events (engram_id, agent_id, action, resonance_score, timestamp, age_ms)
1234
+ VALUES (?, ?, ?, ?, ?, ?)
1199
1235
  `).run(event.engramId, event.agentId, event.action, event.resonanceScore, event.timestamp.toISOString(), event.ageMs);
1200
1236
  }
1201
1237
  logRetrievalFeedback(activationEventId, engramId, useful, context) {
1202
- this.db.prepare(`
1203
- INSERT INTO retrieval_feedback (id, activation_event_id, engram_id, useful, context, timestamp)
1204
- VALUES (?, ?, ?, ?, ?, ?)
1238
+ this.db.prepare(`
1239
+ INSERT INTO retrieval_feedback (id, activation_event_id, engram_id, useful, context, timestamp)
1240
+ VALUES (?, ?, ?, ?, ?, ?)
1205
1241
  `).run(randomUUID(), activationEventId, engramId, useful ? 1 : 0, context, new Date().toISOString());
1206
1242
  }
1207
1243
  // --- Eval metrics queries ---
1208
1244
  getRetrievalPrecision(agentId, windowHours = 24) {
1209
1245
  const since = new Date(Date.now() - windowHours * 3600_000).toISOString();
1210
- const row = this.db.prepare(`
1211
- SELECT
1212
- COUNT(CASE WHEN useful = 1 THEN 1 END) as useful_count,
1213
- COUNT(*) as total_count
1214
- FROM retrieval_feedback rf
1215
- LEFT JOIN activation_events ae ON rf.activation_event_id = ae.id
1216
- JOIN engrams e ON rf.engram_id = e.id
1217
- WHERE e.agent_id = ? AND rf.timestamp > ?
1246
+ const row = this.db.prepare(`
1247
+ SELECT
1248
+ COUNT(CASE WHEN useful = 1 THEN 1 END) as useful_count,
1249
+ COUNT(*) as total_count
1250
+ FROM retrieval_feedback rf
1251
+ LEFT JOIN activation_events ae ON rf.activation_event_id = ae.id
1252
+ JOIN engrams e ON rf.engram_id = e.id
1253
+ WHERE e.agent_id = ? AND rf.timestamp > ?
1218
1254
  `).get(agentId, since);
1219
1255
  return row.total_count > 0 ? row.useful_count / row.total_count : 0;
1220
1256
  }
1221
1257
  getStagingMetrics(agentId) {
1222
- const row = this.db.prepare(`
1223
- SELECT
1224
- COUNT(CASE WHEN action = 'promoted' THEN 1 END) as promoted,
1225
- COUNT(CASE WHEN action = 'discarded' THEN 1 END) as discarded,
1226
- COUNT(CASE WHEN action = 'expired' THEN 1 END) as expired
1227
- FROM staging_events WHERE agent_id = ?
1258
+ const row = this.db.prepare(`
1259
+ SELECT
1260
+ COUNT(CASE WHEN action = 'promoted' THEN 1 END) as promoted,
1261
+ COUNT(CASE WHEN action = 'discarded' THEN 1 END) as discarded,
1262
+ COUNT(CASE WHEN action = 'expired' THEN 1 END) as expired
1263
+ FROM staging_events WHERE agent_id = ?
1228
1264
  `).get(agentId);
1229
1265
  return { promoted: row.promoted, discarded: row.discarded, expired: row.expired };
1230
1266
  }
1231
1267
  getActivationStats(agentId, windowHours = 24) {
1232
1268
  const since = new Date(Date.now() - windowHours * 3600_000).toISOString();
1233
- const rows = this.db.prepare(`
1234
- SELECT latency_ms FROM activation_events
1235
- WHERE agent_id = ? AND timestamp > ?
1236
- ORDER BY latency_ms ASC
1269
+ const rows = this.db.prepare(`
1270
+ SELECT latency_ms FROM activation_events
1271
+ WHERE agent_id = ? AND timestamp > ?
1272
+ ORDER BY latency_ms ASC
1237
1273
  `).all(agentId, since);
1238
1274
  if (rows.length === 0)
1239
1275
  return { count: 0, avgLatencyMs: 0, p50LatencyMs: 0, p90LatencyMs: 0, p95LatencyMs: 0 };
@@ -1258,11 +1294,11 @@ export class EngramStore {
1258
1294
  */
1259
1295
  getLinkedFeedbackStats(agentId, windowHours = 24 * 7) {
1260
1296
  const since = new Date(Date.now() - windowHours * 3600_000).toISOString();
1261
- const row = this.db.prepare(`
1262
- SELECT COUNT(*) AS total, COUNT(CASE WHEN rf.useful = 1 THEN 1 END) AS useful
1263
- FROM retrieval_feedback rf
1264
- JOIN activation_events ae ON ae.id = rf.activation_event_id
1265
- WHERE ae.agent_id = ? AND rf.timestamp > ?
1297
+ const row = this.db.prepare(`
1298
+ SELECT COUNT(*) AS total, COUNT(CASE WHEN rf.useful = 1 THEN 1 END) AS useful
1299
+ FROM retrieval_feedback rf
1300
+ JOIN activation_events ae ON ae.id = rf.activation_event_id
1301
+ WHERE ae.agent_id = ? AND rf.timestamp > ?
1266
1302
  `).get(agentId, since);
1267
1303
  return { total: row.total, useful: row.useful };
1268
1304
  }
@@ -1308,6 +1344,7 @@ export class EngramStore {
1308
1344
  recipeId: row.recipe_id ?? null,
1309
1345
  validFrom: row.valid_from ?? null,
1310
1346
  validTo: row.valid_to ?? null,
1347
+ stageChangedAt: row.stage_changed_at ?? null,
1311
1348
  };
1312
1349
  }
1313
1350
  rowToAssociation(row) {
@@ -1330,9 +1367,9 @@ export class EngramStore {
1330
1367
  const embeddingBlob = input.embedding
1331
1368
  ? Buffer.from(new Float32Array(input.embedding).buffer)
1332
1369
  : null;
1333
- this.db.prepare(`
1334
- INSERT INTO episodes (id, agent_id, label, embedding, engram_count, start_time, end_time, created_at)
1335
- VALUES (?, ?, ?, ?, 0, ?, ?, ?)
1370
+ this.db.prepare(`
1371
+ INSERT INTO episodes (id, agent_id, label, embedding, engram_count, start_time, end_time, created_at)
1372
+ VALUES (?, ?, ?, ?, 0, ?, ?, ?)
1336
1373
  `).run(id, input.agentId, input.label, embeddingBlob, now, now, now);
1337
1374
  return this.getEpisode(id);
1338
1375
  }
@@ -1347,19 +1384,19 @@ export class EngramStore {
1347
1384
  getActiveEpisode(agentId, windowMs = 3600_000) {
1348
1385
  // Find most recent episode that ended within the time window
1349
1386
  const cutoff = new Date(Date.now() - windowMs).toISOString();
1350
- const row = this.db.prepare(`
1351
- SELECT * FROM episodes WHERE agent_id = ? AND end_time > ?
1352
- ORDER BY end_time DESC LIMIT 1
1387
+ const row = this.db.prepare(`
1388
+ SELECT * FROM episodes WHERE agent_id = ? AND end_time > ?
1389
+ ORDER BY end_time DESC LIMIT 1
1353
1390
  `).get(agentId, cutoff);
1354
1391
  return row ? this.rowToEpisode(row) : null;
1355
1392
  }
1356
1393
  addEngramToEpisode(engramId, episodeId) {
1357
1394
  this.db.prepare('UPDATE engrams SET episode_id = ? WHERE id = ?').run(episodeId, engramId);
1358
- this.db.prepare(`
1359
- UPDATE episodes SET
1360
- engram_count = engram_count + 1,
1361
- end_time = MAX(end_time, ?)
1362
- WHERE id = ?
1395
+ this.db.prepare(`
1396
+ UPDATE episodes SET
1397
+ engram_count = engram_count + 1,
1398
+ end_time = MAX(end_time, ?)
1399
+ WHERE id = ?
1363
1400
  `).run(new Date().toISOString(), episodeId);
1364
1401
  }
1365
1402
  getEngramsByEpisode(episodeId) {
@@ -1410,49 +1447,49 @@ export class EngramStore {
1410
1447
  // --- Checkpointing ---
1411
1448
  updateAutoCheckpointWrite(agentId, engramId) {
1412
1449
  const now = new Date().toISOString();
1413
- this.db.prepare(`
1414
- INSERT INTO conscious_state (agent_id, last_write_id, last_activity_at, write_count_since_consolidation, updated_at)
1415
- VALUES (?, ?, ?, 1, ?)
1416
- ON CONFLICT(agent_id) DO UPDATE SET
1417
- last_write_id = excluded.last_write_id,
1418
- last_activity_at = excluded.last_activity_at,
1419
- write_count_since_consolidation = write_count_since_consolidation + 1,
1420
- updated_at = excluded.updated_at
1450
+ this.db.prepare(`
1451
+ INSERT INTO conscious_state (agent_id, last_write_id, last_activity_at, write_count_since_consolidation, updated_at)
1452
+ VALUES (?, ?, ?, 1, ?)
1453
+ ON CONFLICT(agent_id) DO UPDATE SET
1454
+ last_write_id = excluded.last_write_id,
1455
+ last_activity_at = excluded.last_activity_at,
1456
+ write_count_since_consolidation = write_count_since_consolidation + 1,
1457
+ updated_at = excluded.updated_at
1421
1458
  `).run(agentId, engramId, now, now);
1422
1459
  }
1423
1460
  updateAutoCheckpointRecall(agentId, context, engramIds) {
1424
1461
  const now = new Date().toISOString();
1425
- this.db.prepare(`
1426
- INSERT INTO conscious_state (agent_id, last_recall_context, last_recall_ids, last_activity_at, recall_count_since_consolidation, updated_at)
1427
- VALUES (?, ?, ?, ?, 1, ?)
1428
- ON CONFLICT(agent_id) DO UPDATE SET
1429
- last_recall_context = excluded.last_recall_context,
1430
- last_recall_ids = excluded.last_recall_ids,
1431
- last_activity_at = excluded.last_activity_at,
1432
- recall_count_since_consolidation = recall_count_since_consolidation + 1,
1433
- updated_at = excluded.updated_at
1462
+ this.db.prepare(`
1463
+ INSERT INTO conscious_state (agent_id, last_recall_context, last_recall_ids, last_activity_at, recall_count_since_consolidation, updated_at)
1464
+ VALUES (?, ?, ?, ?, 1, ?)
1465
+ ON CONFLICT(agent_id) DO UPDATE SET
1466
+ last_recall_context = excluded.last_recall_context,
1467
+ last_recall_ids = excluded.last_recall_ids,
1468
+ last_activity_at = excluded.last_activity_at,
1469
+ recall_count_since_consolidation = recall_count_since_consolidation + 1,
1470
+ updated_at = excluded.updated_at
1434
1471
  `).run(agentId, context, JSON.stringify(engramIds), now, now);
1435
1472
  }
1436
1473
  touchActivity(agentId) {
1437
1474
  const now = new Date().toISOString();
1438
- this.db.prepare(`
1439
- INSERT INTO conscious_state (agent_id, last_activity_at, updated_at)
1440
- VALUES (?, ?, ?)
1441
- ON CONFLICT(agent_id) DO UPDATE SET
1442
- last_activity_at = excluded.last_activity_at,
1443
- updated_at = excluded.updated_at
1475
+ this.db.prepare(`
1476
+ INSERT INTO conscious_state (agent_id, last_activity_at, updated_at)
1477
+ VALUES (?, ?, ?)
1478
+ ON CONFLICT(agent_id) DO UPDATE SET
1479
+ last_activity_at = excluded.last_activity_at,
1480
+ updated_at = excluded.updated_at
1444
1481
  `).run(agentId, now, now);
1445
1482
  }
1446
1483
  saveCheckpoint(agentId, state) {
1447
1484
  const now = new Date().toISOString();
1448
- this.db.prepare(`
1449
- INSERT INTO conscious_state (agent_id, execution_state, checkpoint_at, last_activity_at, updated_at)
1450
- VALUES (?, ?, ?, ?, ?)
1451
- ON CONFLICT(agent_id) DO UPDATE SET
1452
- execution_state = excluded.execution_state,
1453
- checkpoint_at = excluded.checkpoint_at,
1454
- last_activity_at = excluded.last_activity_at,
1455
- updated_at = excluded.updated_at
1485
+ this.db.prepare(`
1486
+ INSERT INTO conscious_state (agent_id, execution_state, checkpoint_at, last_activity_at, updated_at)
1487
+ VALUES (?, ?, ?, ?, ?)
1488
+ ON CONFLICT(agent_id) DO UPDATE SET
1489
+ execution_state = excluded.execution_state,
1490
+ checkpoint_at = excluded.checkpoint_at,
1491
+ last_activity_at = excluded.last_activity_at,
1492
+ updated_at = excluded.updated_at
1456
1493
  `).run(agentId, JSON.stringify(state), now, now, now);
1457
1494
  }
1458
1495
  /** Record entity mentions for an engram (D9 inverted index; idempotent). */
@@ -1469,10 +1506,10 @@ export class EngramStore {
1469
1506
  const t = term.toLowerCase().replace(/[%_]/g, '');
1470
1507
  if (!t)
1471
1508
  return [];
1472
- const rows = this.db.prepare(`
1473
- SELECT DISTINCT entity FROM (
1474
- SELECT entity FROM entity_mentions WHERE entity LIKE ?
1475
- UNION SELECT entity FROM entity_aliases WHERE alias LIKE ?
1509
+ const rows = this.db.prepare(`
1510
+ SELECT DISTINCT entity FROM (
1511
+ SELECT entity FROM entity_mentions WHERE entity LIKE ?
1512
+ UNION SELECT entity FROM entity_aliases WHERE alias LIKE ?
1476
1513
  ) LIMIT ?`).all(`%${t}%`, `%${t}%`, limit);
1477
1514
  return rows.map(r => r.entity);
1478
1515
  }
@@ -1514,20 +1551,20 @@ export class EngramStore {
1514
1551
  markConsolidation(agentId, mini) {
1515
1552
  const now = new Date().toISOString();
1516
1553
  if (mini) {
1517
- this.db.prepare(`
1518
- UPDATE conscious_state SET last_mini_consolidation_at = ?, updated_at = ? WHERE agent_id = ?
1554
+ this.db.prepare(`
1555
+ UPDATE conscious_state SET last_mini_consolidation_at = ?, updated_at = ? WHERE agent_id = ?
1519
1556
  `).run(now, now, agentId);
1520
1557
  }
1521
1558
  else {
1522
- this.db.prepare(`
1523
- UPDATE conscious_state SET
1524
- last_consolidation_at = ?,
1525
- last_mini_consolidation_at = ?,
1526
- write_count_since_consolidation = 0,
1527
- recall_count_since_consolidation = 0,
1528
- consolidation_cycle_count = consolidation_cycle_count + 1,
1529
- updated_at = ?
1530
- WHERE agent_id = ?
1559
+ this.db.prepare(`
1560
+ UPDATE conscious_state SET
1561
+ last_consolidation_at = ?,
1562
+ last_mini_consolidation_at = ?,
1563
+ write_count_since_consolidation = 0,
1564
+ recall_count_since_consolidation = 0,
1565
+ consolidation_cycle_count = consolidation_cycle_count + 1,
1566
+ updated_at = ?
1567
+ WHERE agent_id = ?
1531
1568
  `).run(now, now, now, agentId);
1532
1569
  }
1533
1570
  }
@@ -1598,10 +1635,10 @@ export class EngramStore {
1598
1635
  * sequence are excluded (they have no story-time anchor).
1599
1636
  */
1600
1637
  getLatestByTag(opts) {
1601
- let sql = `SELECT * FROM engrams
1602
- WHERE agent_id = ?
1603
- AND retracted = ?
1604
- AND stage = 'active'
1638
+ let sql = `SELECT * FROM engrams
1639
+ WHERE agent_id = ?
1640
+ AND retracted = ?
1641
+ AND stage = 'active'
1605
1642
  AND tags LIKE ?`;
1606
1643
  const params = [
1607
1644
  opts.agentId,
@@ -1641,10 +1678,10 @@ export class EngramStore {
1641
1678
  */
1642
1679
  getTopBy(opts) {
1643
1680
  // Build the base query inline (mirrors search() but adds the topBy sort).
1644
- let sql = `SELECT * FROM engrams
1645
- WHERE agent_id = ?
1646
- AND retracted = ?
1647
- AND stage = 'active'
1681
+ let sql = `SELECT * FROM engrams
1682
+ WHERE agent_id = ?
1683
+ AND retracted = ?
1684
+ AND stage = 'active'
1648
1685
  AND tags LIKE ?`;
1649
1686
  const params = [
1650
1687
  opts.agentId,
@@ -1712,11 +1749,11 @@ export class EngramStore {
1712
1749
  // Find candidate referencing engrams. LIKE-filter first to narrow,
1713
1750
  // then JSON.parse each candidate's references_json to verify the
1714
1751
  // matchEngramId and type. Cheap enough at NovelForge's scale.
1715
- const candidates = this.db.prepare(`SELECT id, references_json, created_at FROM engrams
1716
- WHERE agent_id = ?
1717
- AND retracted = 0
1718
- AND stage = 'active'
1719
- AND references_json IS NOT NULL
1752
+ const candidates = this.db.prepare(`SELECT id, references_json, created_at FROM engrams
1753
+ WHERE agent_id = ?
1754
+ AND retracted = 0
1755
+ AND stage = 'active'
1756
+ AND references_json IS NOT NULL
1720
1757
  AND references_json LIKE ?`).all(target.agentId, `%"${targetEngramId}"%`);
1721
1758
  const terminalTypes = new Set(['resolves', 'subverts', 'abandons']);
1722
1759
  const resolvingEvents = [];