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
@@ -115,6 +115,11 @@ function rowToEngram(row) {
115
115
  blockedBy: row.blocked_by ?? null,
116
116
  sequence: row.sequence == null ? null : Number(row.sequence),
117
117
  references: row.references_json ? JSON.parse(row.references_json) : null,
118
+ originClass: row.origin_class ?? null,
119
+ writerSession: row.writer_session ?? null,
120
+ recipeId: row.recipe_id ?? null,
121
+ validFrom: row.valid_from ?? null,
122
+ validTo: row.valid_to ?? null,
118
123
  };
119
124
  }
120
125
  function rowToAssociation(row) {
@@ -274,7 +279,7 @@ export class PostgresEngramStore {
274
279
  // ALWAYS the pool, never this.q(): this background flush is fire-and-forget and must
275
280
  // not route to a transaction's client (a future caller that logs an activation inside a
276
281
  // withTransaction would otherwise collide with the tx's in-flight query on one pg client).
277
- await this.pool.query(`INSERT INTO activation_events (id, agent_id, timestamp, context, results_returned, top_score, latency_ms, engram_ids)
282
+ await this.pool.query(`INSERT INTO activation_events (id, agent_id, timestamp, context, results_returned, top_score, latency_ms, engram_ids)
278
283
  VALUES ${values.join(',')}`, params);
279
284
  }
280
285
  catch {
@@ -321,18 +326,20 @@ export class PostgresEngramStore {
321
326
  await this.readyPromise;
322
327
  const id = input.id ?? randomUUID();
323
328
  const now = new Date().toISOString();
324
- await this.q(`INSERT INTO engrams (
325
- id, agent_id, concept, content, embedding, embedding_model,
326
- confidence, salience, access_count, last_accessed, created_at,
327
- salience_features, reason_codes, stage, ttl, retracted,
328
- tags, memory_type, memory_class, supersedes, episode_id,
329
- task_status, task_priority, blocked_by, sequence, references_json
330
- ) VALUES (
331
- $1, $2, $3, $4, $5::vector, $6,
332
- $7, $8, 0, $9, $10,
333
- $11, $12, 'active', $13, FALSE,
334
- $14, $15, $16, $17, $18,
335
- $19, $20, $21, $22, $23
329
+ await this.q(`INSERT INTO engrams (
330
+ id, agent_id, concept, content, embedding, embedding_model,
331
+ confidence, salience, access_count, last_accessed, created_at,
332
+ salience_features, reason_codes, stage, ttl, retracted,
333
+ tags, memory_type, memory_class, supersedes, episode_id,
334
+ task_status, task_priority, blocked_by, sequence, references_json,
335
+ origin_class, writer_session, recipe_id, valid_from, valid_to
336
+ ) VALUES (
337
+ $1, $2, $3, $4, $5::vector, $6,
338
+ $7, $8, 0, $9, $10,
339
+ $11, $12, 'active', $13, FALSE,
340
+ $14, $15, $16, $17, $18,
341
+ $19, $20, $21, $22, $23,
342
+ $24, $25, $26, $27, $28
336
343
  )`, [
337
344
  id,
338
345
  input.agentId,
@@ -357,6 +364,11 @@ export class PostgresEngramStore {
357
364
  input.sequence ?? null,
358
365
  input.references && input.references.length > 0
359
366
  ? JSON.stringify(input.references) : null,
367
+ input.originClass ?? null,
368
+ input.writerSession ?? null,
369
+ input.recipeId ?? null,
370
+ input.validFrom ?? null,
371
+ input.validTo ?? null,
360
372
  ]);
361
373
  const row = await this.getEngram(id);
362
374
  if (!row)
@@ -468,10 +480,10 @@ export class PostgresEngramStore {
468
480
  }
469
481
  async touchEngram(id) {
470
482
  await this.readyPromise;
471
- await this.q(`UPDATE engrams
472
- SET access_count = access_count + 1,
473
- last_accessed = $1,
474
- confidence = LEAST(0.85, confidence + 0.02 / (1.0 + sqrt(access_count::float)))
483
+ await this.q(`UPDATE engrams
484
+ SET access_count = access_count + 1,
485
+ last_accessed = $1,
486
+ confidence = LEAST(0.85, confidence + 0.02 / (1.0 + sqrt(access_count::float)))
475
487
  WHERE id = $2`, [new Date().toISOString(), id]);
476
488
  }
477
489
  async updateStage(id, stage) {
@@ -517,9 +529,9 @@ export class PostgresEngramStore {
517
529
  async timeWarp(agentId, ms) {
518
530
  await this.readyPromise;
519
531
  const seconds = Math.round(ms / 1000);
520
- const r1 = await this.q(`UPDATE engrams SET
521
- created_at = to_char(($1::timestamptz - interval '1 second' * $2), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
522
- last_accessed = to_char(($3::timestamptz - interval '1 second' * $2), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')
532
+ const r1 = await this.q(`UPDATE engrams SET
533
+ created_at = to_char(($1::timestamptz - interval '1 second' * $2), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
534
+ last_accessed = to_char(($3::timestamptz - interval '1 second' * $2), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')
523
535
  WHERE agent_id = $4`, ['now', seconds, 'now', agentId]);
524
536
  // node-postgres reports the affected-row count as `rowCount` (PGlite called it
525
537
  // `affectedRows` — the one result-shape field that differs between the drivers).
@@ -545,13 +557,13 @@ export class PostgresEngramStore {
545
557
  // Restrict to active + fading. Faded engrams (Paper 1: storage degradation)
546
558
  // retain their embedding so they still participate in semantic recall, even
547
559
  // though their content has been trimmed. Excludes staging/consolidated/archived.
548
- const result = await this.q(`SELECT *, (embedding <=> $2::vector) AS distance
549
- FROM engrams
550
- WHERE agent_id = $1
551
- AND embedding IS NOT NULL
552
- AND retracted = FALSE
553
- AND stage IN ('active', 'fading')
554
- ORDER BY distance ASC
560
+ const result = await this.q(`SELECT *, (embedding <=> $2::vector) AS distance
561
+ FROM engrams
562
+ WHERE agent_id = $1
563
+ AND embedding IS NOT NULL
564
+ AND retracted = FALSE
565
+ AND stage IN ('active', 'fading')
566
+ ORDER BY distance ASC
555
567
  LIMIT $3`, [agentId, vectorToLiteral(vec), limit]);
556
568
  return result.rows.map((r) => ({ engram: rowToEngram(r), distance: r.distance }));
557
569
  }
@@ -570,11 +582,11 @@ export class PostgresEngramStore {
570
582
  if (tokens.length === 0)
571
583
  return [];
572
584
  const websearchQuery = tokens.join(' OR ');
573
- const result = await this.q(`SELECT *, ts_rank_cd(fts, websearch_to_tsquery('english', $2)) AS rank
574
- FROM engrams
575
- WHERE agent_id = $1 AND retracted = FALSE
576
- AND fts @@ websearch_to_tsquery('english', $2)
577
- ORDER BY rank DESC
585
+ const result = await this.q(`SELECT *, ts_rank_cd(fts, websearch_to_tsquery('english', $2)) AS rank
586
+ FROM engrams
587
+ WHERE agent_id = $1 AND retracted = FALSE
588
+ AND fts @@ websearch_to_tsquery('english', $2)
589
+ ORDER BY rank DESC
578
590
  LIMIT $3`, [agentId, websearchQuery, limit]);
579
591
  return result.rows.map((r) => ({ engram: rowToEngram(r), bm25Score: calibrateBm25(Number(r.rank)) }));
580
592
  }
@@ -588,11 +600,11 @@ export class PostgresEngramStore {
588
600
  if (tokens.length === 0)
589
601
  return [];
590
602
  const websearchQuery = tokens.join(' OR ');
591
- const result = await this.q(`SELECT *, ts_rank_cd(fts, websearch_to_tsquery('english', $2)) AS rank
592
- FROM engrams
593
- WHERE agent_id = ANY($1::text[]) AND retracted = FALSE
594
- AND fts @@ websearch_to_tsquery('english', $2)
595
- ORDER BY rank DESC
603
+ const result = await this.q(`SELECT *, ts_rank_cd(fts, websearch_to_tsquery('english', $2)) AS rank
604
+ FROM engrams
605
+ WHERE agent_id = ANY($1::text[]) AND retracted = FALSE
606
+ AND fts @@ websearch_to_tsquery('english', $2)
607
+ ORDER BY rank DESC
596
608
  LIMIT $3`, [agentIds, websearchQuery, limit]);
597
609
  return result.rows.map((r) => ({ engram: rowToEngram(r), bm25Score: calibrateBm25(Number(r.rank)) }));
598
610
  }
@@ -673,32 +685,32 @@ export class PostgresEngramStore {
673
685
  sql += ` AND task_status = $${params.length + 1}`;
674
686
  params.push(status);
675
687
  }
676
- sql += ` ORDER BY
677
- CASE task_priority
678
- WHEN 'urgent' THEN 0
679
- WHEN 'high' THEN 1
680
- WHEN 'medium' THEN 2
681
- WHEN 'low' THEN 3
682
- ELSE 4
683
- END,
688
+ sql += ` ORDER BY
689
+ CASE task_priority
690
+ WHEN 'urgent' THEN 0
691
+ WHEN 'high' THEN 1
692
+ WHEN 'medium' THEN 2
693
+ WHEN 'low' THEN 3
694
+ ELSE 4
695
+ END,
684
696
  created_at DESC`;
685
697
  const result = await this.q(sql, params);
686
698
  return result.rows.map(rowToEngram);
687
699
  }
688
700
  async getNextTask(agentId) {
689
701
  await this.readyPromise;
690
- const result = await this.q(`SELECT * FROM engrams
691
- WHERE agent_id = $1 AND task_status IN ('open', 'in_progress') AND retracted = FALSE
692
- ORDER BY
693
- CASE task_status WHEN 'in_progress' THEN 0 ELSE 1 END,
694
- CASE task_priority
695
- WHEN 'urgent' THEN 0
696
- WHEN 'high' THEN 1
697
- WHEN 'medium' THEN 2
698
- WHEN 'low' THEN 3
699
- ELSE 4
700
- END,
701
- created_at ASC
702
+ const result = await this.q(`SELECT * FROM engrams
703
+ WHERE agent_id = $1 AND task_status IN ('open', 'in_progress') AND retracted = FALSE
704
+ ORDER BY
705
+ CASE task_status WHEN 'in_progress' THEN 0 ELSE 1 END,
706
+ CASE task_priority
707
+ WHEN 'urgent' THEN 0
708
+ WHEN 'high' THEN 1
709
+ WHEN 'medium' THEN 2
710
+ WHEN 'low' THEN 3
711
+ ELSE 4
712
+ END,
713
+ created_at ASC
702
714
  LIMIT 1`, [agentId]);
703
715
  return result.rows.length > 0 ? rowToEngram(result.rows[0]) : null;
704
716
  }
@@ -712,11 +724,11 @@ export class PostgresEngramStore {
712
724
  }
713
725
  async findActiveMatchByConcept(agentId, concept, requiredTags) {
714
726
  await this.readyPromise;
715
- let sql = `SELECT * FROM engrams
716
- WHERE agent_id = $1
717
- AND LOWER(TRIM(concept)) = LOWER(TRIM($2))
718
- AND stage = 'active'
719
- AND retracted = FALSE
727
+ let sql = `SELECT * FROM engrams
728
+ WHERE agent_id = $1
729
+ AND LOWER(TRIM(concept)) = LOWER(TRIM($2))
730
+ AND stage = 'active'
731
+ AND retracted = FALSE
720
732
  AND superseded_by IS NULL`;
721
733
  const params = [agentId, concept];
722
734
  if (requiredTags && requiredTags.length > 0) {
@@ -749,12 +761,12 @@ export class PostgresEngramStore {
749
761
  await this.readyPromise;
750
762
  const id = randomUUID();
751
763
  const now = new Date().toISOString();
752
- await this.q(`INSERT INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type, activation_count, created_at, last_activated)
753
- VALUES ($1, $2, $3, $4, $5, $6, 0, $7, $7)
754
- ON CONFLICT (from_engram_id, to_engram_id) DO UPDATE SET
755
- weight = EXCLUDED.weight,
756
- confidence = EXCLUDED.confidence,
757
- last_activated = EXCLUDED.last_activated,
764
+ await this.q(`INSERT INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type, activation_count, created_at, last_activated)
765
+ VALUES ($1, $2, $3, $4, $5, $6, 0, $7, $7)
766
+ ON CONFLICT (from_engram_id, to_engram_id) DO UPDATE SET
767
+ weight = EXCLUDED.weight,
768
+ confidence = EXCLUDED.confidence,
769
+ last_activated = EXCLUDED.last_activated,
758
770
  activation_count = associations.activation_count + 1`, [id, fromId, toId, weight, confidence, type, now]);
759
771
  const assoc = await this.getAssociation(fromId, toId);
760
772
  if (!assoc)
@@ -776,12 +788,12 @@ export class PostgresEngramStore {
776
788
  if (engramIds.length === 0)
777
789
  return result;
778
790
  await this.readyPromise;
779
- const r = await this.q(`SELECT id, SUM(cnt) AS count, SUM(sw) AS sum_weight FROM (
780
- SELECT from_engram_id AS id, 1 AS cnt, weight AS sw FROM associations WHERE from_engram_id = ANY($1::text[])
781
- UNION ALL
782
- SELECT to_engram_id AS id, 1 AS cnt, weight AS sw FROM associations WHERE to_engram_id = ANY($1::text[])
783
- ) t
784
- WHERE id = ANY($1::text[])
791
+ const r = await this.q(`SELECT id, SUM(cnt) AS count, SUM(sw) AS sum_weight FROM (
792
+ SELECT from_engram_id AS id, 1 AS cnt, weight AS sw FROM associations WHERE from_engram_id = ANY($1::text[])
793
+ UNION ALL
794
+ SELECT to_engram_id AS id, 1 AS cnt, weight AS sw FROM associations WHERE to_engram_id = ANY($1::text[])
795
+ ) t
796
+ WHERE id = ANY($1::text[])
785
797
  GROUP BY id`, [engramIds]);
786
798
  for (const row of r.rows) {
787
799
  result.set(row.id, { count: Number(row.count), sumWeight: Number(row.sum_weight) });
@@ -797,7 +809,7 @@ export class PostgresEngramStore {
797
809
  if (engramIds.length === 0)
798
810
  return result;
799
811
  await this.readyPromise;
800
- const r = await this.q(`SELECT * FROM associations
812
+ const r = await this.q(`SELECT * FROM associations
801
813
  WHERE from_engram_id = ANY($1::text[]) OR to_engram_id = ANY($1::text[])`, [engramIds]);
802
814
  for (const row of r.rows) {
803
815
  const a = rowToAssociation(row);
@@ -837,8 +849,8 @@ export class PostgresEngramStore {
837
849
  }
838
850
  async getAllAssociations(agentId) {
839
851
  await this.readyPromise;
840
- const result = await this.q(`SELECT a.* FROM associations a
841
- JOIN engrams e ON a.from_engram_id = e.id
852
+ const result = await this.q(`SELECT a.* FROM associations a
853
+ JOIN engrams e ON a.from_engram_id = e.id
842
854
  WHERE e.agent_id = $1`, [agentId]);
843
855
  return result.rows.map(rowToAssociation);
844
856
  }
@@ -847,11 +859,11 @@ export class PostgresEngramStore {
847
859
  // ============================================================
848
860
  async getEvictionCandidates(agentId, limit) {
849
861
  await this.readyPromise;
850
- const result = await this.q(`SELECT * FROM engrams
851
- WHERE agent_id = $1 AND stage = 'active' AND retracted = FALSE
852
- ORDER BY (salience * 0.3 + confidence * 0.3
853
- + (access_count::float / (access_count + 5)) * 0.2
854
- + (1.0 / (1.0 + EXTRACT(EPOCH FROM (now() - last_accessed::timestamptz)) / 86400.0)) * 0.2) ASC
862
+ const result = await this.q(`SELECT * FROM engrams
863
+ WHERE agent_id = $1 AND stage = 'active' AND retracted = FALSE
864
+ ORDER BY (salience * 0.3 + confidence * 0.3
865
+ + (access_count::float / (access_count + 5)) * 0.2
866
+ + (1.0 / (1.0 + EXTRACT(EPOCH FROM (now() - last_accessed::timestamptz)) / 86400.0)) * 0.2) ASC
855
867
  LIMIT $2`, [agentId, limit]);
856
868
  return result.rows.map(rowToEngram);
857
869
  }
@@ -886,7 +898,7 @@ export class PostgresEngramStore {
886
898
  }
887
899
  async logStagingEvent(event) {
888
900
  await this.readyPromise;
889
- await this.q(`INSERT INTO staging_events (engram_id, agent_id, action, resonance_score, timestamp, age_ms)
901
+ await this.q(`INSERT INTO staging_events (engram_id, agent_id, action, resonance_score, timestamp, age_ms)
890
902
  VALUES ($1, $2, $3, $4, $5, $6)`, [
891
903
  event.engramId, event.agentId, event.action,
892
904
  event.resonanceScore, event.timestamp.toISOString(), event.ageMs,
@@ -894,18 +906,18 @@ export class PostgresEngramStore {
894
906
  }
895
907
  async logRetrievalFeedback(activationEventId, engramId, useful, context) {
896
908
  await this.readyPromise;
897
- await this.q(`INSERT INTO retrieval_feedback (id, activation_event_id, engram_id, useful, context, timestamp)
909
+ await this.q(`INSERT INTO retrieval_feedback (id, activation_event_id, engram_id, useful, context, timestamp)
898
910
  VALUES ($1, $2, $3, $4, $5, $6)`, [randomUUID(), activationEventId, engramId, useful, context, new Date().toISOString()]);
899
911
  }
900
912
  async getRetrievalPrecision(agentId, windowHours = 24) {
901
913
  await this.readyPromise;
902
914
  const since = new Date(Date.now() - windowHours * 3600_000).toISOString();
903
- const result = await this.q(`SELECT
904
- COUNT(CASE WHEN useful = TRUE THEN 1 END) AS useful_count,
905
- COUNT(*) AS total_count
906
- FROM retrieval_feedback rf
907
- LEFT JOIN activation_events ae ON rf.activation_event_id = ae.id
908
- JOIN engrams e ON rf.engram_id = e.id
915
+ const result = await this.q(`SELECT
916
+ COUNT(CASE WHEN useful = TRUE THEN 1 END) AS useful_count,
917
+ COUNT(*) AS total_count
918
+ FROM retrieval_feedback rf
919
+ LEFT JOIN activation_events ae ON rf.activation_event_id = ae.id
920
+ JOIN engrams e ON rf.engram_id = e.id
909
921
  WHERE e.agent_id = $1 AND rf.timestamp > $2`, [agentId, since]);
910
922
  const row = result.rows[0];
911
923
  const total = Number(row?.total_count ?? 0);
@@ -914,10 +926,10 @@ export class PostgresEngramStore {
914
926
  }
915
927
  async getStagingMetrics(agentId) {
916
928
  await this.readyPromise;
917
- const result = await this.q(`SELECT
918
- COUNT(CASE WHEN action = 'promoted' THEN 1 END) AS promoted,
919
- COUNT(CASE WHEN action = 'discarded' THEN 1 END) AS discarded,
920
- COUNT(CASE WHEN action = 'expired' THEN 1 END) AS expired
929
+ const result = await this.q(`SELECT
930
+ COUNT(CASE WHEN action = 'promoted' THEN 1 END) AS promoted,
931
+ COUNT(CASE WHEN action = 'discarded' THEN 1 END) AS discarded,
932
+ COUNT(CASE WHEN action = 'expired' THEN 1 END) AS expired
921
933
  FROM staging_events WHERE agent_id = $1`, [agentId]);
922
934
  const row = result.rows[0] ?? { promoted: 0, discarded: 0, expired: 0 };
923
935
  return {
@@ -931,8 +943,8 @@ export class PostgresEngramStore {
931
943
  // Flush any buffered activation events so stats reflect the latest writes.
932
944
  await this.flushActivationEvents();
933
945
  const since = new Date(Date.now() - windowHours * 3600_000).toISOString();
934
- const result = await this.q(`SELECT latency_ms FROM activation_events
935
- WHERE agent_id = $1 AND timestamp > $2
946
+ const result = await this.q(`SELECT latency_ms FROM activation_events
947
+ WHERE agent_id = $1 AND timestamp > $2
936
948
  ORDER BY latency_ms ASC`, [agentId, since]);
937
949
  if (result.rows.length === 0)
938
950
  return { count: 0, avgLatencyMs: 0, p95LatencyMs: 0 };
@@ -957,7 +969,7 @@ export class PostgresEngramStore {
957
969
  await this.readyPromise;
958
970
  const id = randomUUID();
959
971
  const now = new Date().toISOString();
960
- await this.q(`INSERT INTO episodes (id, agent_id, label, embedding, engram_count, start_time, end_time, created_at)
972
+ await this.q(`INSERT INTO episodes (id, agent_id, label, embedding, engram_count, start_time, end_time, created_at)
961
973
  VALUES ($1, $2, $3, $4::vector, 0, $5, $5, $5)`, [id, input.agentId, input.label, vectorToLiteral(input.embedding ?? null), now]);
962
974
  const ep = await this.getEpisode(id);
963
975
  if (!ep)
@@ -983,9 +995,9 @@ export class PostgresEngramStore {
983
995
  async addEngramToEpisode(engramId, episodeId) {
984
996
  await this.readyPromise;
985
997
  await this.q(`UPDATE engrams SET episode_id = $1 WHERE id = $2`, [episodeId, engramId]);
986
- await this.q(`UPDATE episodes SET
987
- engram_count = engram_count + 1,
988
- end_time = GREATEST(end_time, $1)
998
+ await this.q(`UPDATE episodes SET
999
+ engram_count = engram_count + 1,
1000
+ end_time = GREATEST(end_time, $1)
989
1001
  WHERE id = $2`, [new Date().toISOString(), episodeId]);
990
1002
  }
991
1003
  async getEngramsByEpisode(episodeId) {
@@ -1024,46 +1036,76 @@ export class PostgresEngramStore {
1024
1036
  async updateAutoCheckpointWrite(agentId, engramId) {
1025
1037
  await this.readyPromise;
1026
1038
  const now = new Date().toISOString();
1027
- await this.q(`INSERT INTO conscious_state (agent_id, last_write_id, last_activity_at, write_count_since_consolidation, updated_at)
1028
- VALUES ($1, $2, $3, 1, $3)
1029
- ON CONFLICT(agent_id) DO UPDATE SET
1030
- last_write_id = EXCLUDED.last_write_id,
1031
- last_activity_at = EXCLUDED.last_activity_at,
1032
- write_count_since_consolidation = conscious_state.write_count_since_consolidation + 1,
1039
+ await this.q(`INSERT INTO conscious_state (agent_id, last_write_id, last_activity_at, write_count_since_consolidation, updated_at)
1040
+ VALUES ($1, $2, $3, 1, $3)
1041
+ ON CONFLICT(agent_id) DO UPDATE SET
1042
+ last_write_id = EXCLUDED.last_write_id,
1043
+ last_activity_at = EXCLUDED.last_activity_at,
1044
+ write_count_since_consolidation = conscious_state.write_count_since_consolidation + 1,
1033
1045
  updated_at = EXCLUDED.updated_at`, [agentId, engramId, now]);
1034
1046
  }
1035
1047
  async updateAutoCheckpointRecall(agentId, context, engramIds) {
1036
1048
  await this.readyPromise;
1037
1049
  const now = new Date().toISOString();
1038
- await this.q(`INSERT INTO conscious_state (agent_id, last_recall_context, last_recall_ids, last_activity_at, recall_count_since_consolidation, updated_at)
1039
- VALUES ($1, $2, $3, $4, 1, $4)
1040
- ON CONFLICT(agent_id) DO UPDATE SET
1041
- last_recall_context = EXCLUDED.last_recall_context,
1042
- last_recall_ids = EXCLUDED.last_recall_ids,
1043
- last_activity_at = EXCLUDED.last_activity_at,
1044
- recall_count_since_consolidation = conscious_state.recall_count_since_consolidation + 1,
1050
+ await this.q(`INSERT INTO conscious_state (agent_id, last_recall_context, last_recall_ids, last_activity_at, recall_count_since_consolidation, updated_at)
1051
+ VALUES ($1, $2, $3, $4, 1, $4)
1052
+ ON CONFLICT(agent_id) DO UPDATE SET
1053
+ last_recall_context = EXCLUDED.last_recall_context,
1054
+ last_recall_ids = EXCLUDED.last_recall_ids,
1055
+ last_activity_at = EXCLUDED.last_activity_at,
1056
+ recall_count_since_consolidation = conscious_state.recall_count_since_consolidation + 1,
1045
1057
  updated_at = EXCLUDED.updated_at`, [agentId, context, JSON.stringify(engramIds), now]);
1046
1058
  }
1047
1059
  async touchActivity(agentId) {
1048
1060
  await this.readyPromise;
1049
1061
  const now = new Date().toISOString();
1050
- await this.q(`INSERT INTO conscious_state (agent_id, last_activity_at, updated_at)
1051
- VALUES ($1, $2, $2)
1052
- ON CONFLICT(agent_id) DO UPDATE SET
1053
- last_activity_at = EXCLUDED.last_activity_at,
1062
+ await this.q(`INSERT INTO conscious_state (agent_id, last_activity_at, updated_at)
1063
+ VALUES ($1, $2, $2)
1064
+ ON CONFLICT(agent_id) DO UPDATE SET
1065
+ last_activity_at = EXCLUDED.last_activity_at,
1054
1066
  updated_at = EXCLUDED.updated_at`, [agentId, now]);
1055
1067
  }
1056
1068
  async saveCheckpoint(agentId, state) {
1057
1069
  await this.readyPromise;
1058
1070
  const now = new Date().toISOString();
1059
- await this.q(`INSERT INTO conscious_state (agent_id, execution_state, checkpoint_at, last_activity_at, updated_at)
1060
- VALUES ($1, $2, $3, $3, $3)
1061
- ON CONFLICT(agent_id) DO UPDATE SET
1062
- execution_state = EXCLUDED.execution_state,
1063
- checkpoint_at = EXCLUDED.checkpoint_at,
1064
- last_activity_at = EXCLUDED.last_activity_at,
1071
+ await this.q(`INSERT INTO conscious_state (agent_id, execution_state, checkpoint_at, last_activity_at, updated_at)
1072
+ VALUES ($1, $2, $3, $3, $3)
1073
+ ON CONFLICT(agent_id) DO UPDATE SET
1074
+ execution_state = EXCLUDED.execution_state,
1075
+ checkpoint_at = EXCLUDED.checkpoint_at,
1076
+ last_activity_at = EXCLUDED.last_activity_at,
1065
1077
  updated_at = EXCLUDED.updated_at`, [agentId, JSON.stringify(state), now]);
1066
1078
  }
1079
+ /** Distinct agent ids present in this store (D3 whoami; includes all stages). */
1080
+ async listAgentIds() {
1081
+ const res = await this.pool.query('SELECT DISTINCT agent_id FROM engrams ORDER BY agent_id');
1082
+ return res.rows.map(r => r.agent_id);
1083
+ }
1084
+ async recordEntityMentions(engramId, agentId, entities) {
1085
+ if (!entities || entities.length === 0)
1086
+ return;
1087
+ for (const entity of entities) {
1088
+ await this.pool.query('INSERT INTO entity_mentions (entity, engram_id, agent_id) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING', [entity, engramId, agentId]);
1089
+ }
1090
+ }
1091
+ async searchEntities(term, limit = 8) {
1092
+ const t = term.toLowerCase().replace(/[%_]/g, '');
1093
+ if (!t)
1094
+ return [];
1095
+ const res = await this.pool.query(`SELECT DISTINCT entity FROM (
1096
+ SELECT entity FROM entity_mentions WHERE entity LIKE $1
1097
+ UNION SELECT entity FROM entity_aliases WHERE alias LIKE $1
1098
+ ) u LIMIT $2`, [`%${t}%`, limit]);
1099
+ return res.rows.map(r => r.entity);
1100
+ }
1101
+ async getEngramIdsByEntity(entity, agentId) {
1102
+ const alias = await this.pool.query('SELECT entity FROM entity_aliases WHERE alias = $1', [entity.toLowerCase()]);
1103
+ const resolved = alias.rows[0]?.entity ?? entity.toLowerCase();
1104
+ const res = agentId
1105
+ ? await this.pool.query('SELECT engram_id FROM entity_mentions WHERE entity = $1 AND agent_id = $2', [resolved, agentId])
1106
+ : await this.pool.query('SELECT engram_id FROM entity_mentions WHERE entity = $1', [resolved]);
1107
+ return res.rows.map(r => r.engram_id);
1108
+ }
1067
1109
  async getCheckpoint(agentId) {
1068
1110
  await this.readyPromise;
1069
1111
  const result = await this.q(`SELECT * FROM conscious_state WHERE agent_id = $1`, [agentId]);
@@ -1094,13 +1136,13 @@ export class PostgresEngramStore {
1094
1136
  await this.q(`UPDATE conscious_state SET last_mini_consolidation_at = $1, updated_at = $1 WHERE agent_id = $2`, [now, agentId]);
1095
1137
  }
1096
1138
  else {
1097
- await this.q(`UPDATE conscious_state SET
1098
- last_consolidation_at = $1,
1099
- last_mini_consolidation_at = $1,
1100
- write_count_since_consolidation = 0,
1101
- recall_count_since_consolidation = 0,
1102
- consolidation_cycle_count = consolidation_cycle_count + 1,
1103
- updated_at = $1
1139
+ await this.q(`UPDATE conscious_state SET
1140
+ last_consolidation_at = $1,
1141
+ last_mini_consolidation_at = $1,
1142
+ write_count_since_consolidation = 0,
1143
+ recall_count_since_consolidation = 0,
1144
+ consolidation_cycle_count = consolidation_cycle_count + 1,
1145
+ updated_at = $1
1104
1146
  WHERE agent_id = $2`, [now, agentId]);
1105
1147
  }
1106
1148
  }
@@ -1125,10 +1167,10 @@ export class PostgresEngramStore {
1125
1167
  // ============================================================
1126
1168
  async getLatestByTag(opts) {
1127
1169
  await this.readyPromise;
1128
- let sql = `SELECT * FROM engrams
1129
- WHERE agent_id = $1
1130
- AND retracted = $2
1131
- AND stage = 'active'
1170
+ let sql = `SELECT * FROM engrams
1171
+ WHERE agent_id = $1
1172
+ AND retracted = $2
1173
+ AND stage = 'active'
1132
1174
  AND tags LIKE $3`;
1133
1175
  const params = [opts.agentId, opts.retracted ?? false, `%"${opts.tagKeyPrefix}%`];
1134
1176
  if (opts.scopeTagsAll && opts.scopeTagsAll.length > 0) {
@@ -1155,10 +1197,10 @@ export class PostgresEngramStore {
1155
1197
  }
1156
1198
  async getTopBy(opts) {
1157
1199
  await this.readyPromise;
1158
- let sql = `SELECT * FROM engrams
1159
- WHERE agent_id = $1
1160
- AND retracted = $2
1161
- AND stage = 'active'
1200
+ let sql = `SELECT * FROM engrams
1201
+ WHERE agent_id = $1
1202
+ AND retracted = $2
1203
+ AND stage = 'active'
1162
1204
  AND tags LIKE $3`;
1163
1205
  const params = [opts.agentId, opts.retracted ?? false, `%"${opts.sortField}%`];
1164
1206
  if (opts.filterTagsAll && opts.filterTagsAll.length > 0) {