agent-working-memory 0.8.5 → 0.8.7

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 (53) hide show
  1. package/README.md +4 -2
  2. package/dist/adapters/common.d.ts.map +1 -1
  3. package/dist/adapters/common.js +108 -8
  4. package/dist/adapters/common.js.map +1 -1
  5. package/dist/api/routes.js +1 -1
  6. package/dist/cli/migrate.js +29 -29
  7. package/dist/cli.js +1 -1
  8. package/dist/coordination/circuit-breaker.js +23 -23
  9. package/dist/core/lite-compress.d.ts +26 -0
  10. package/dist/core/lite-compress.d.ts.map +1 -0
  11. package/dist/core/lite-compress.js +105 -0
  12. package/dist/core/lite-compress.js.map +1 -0
  13. package/dist/mcp.d.ts +5 -1
  14. package/dist/mcp.d.ts.map +1 -1
  15. package/dist/mcp.js +58 -4
  16. package/dist/mcp.js.map +1 -1
  17. package/dist/storage/pglite-schema.js +143 -143
  18. package/dist/storage/pglite.js +138 -138
  19. package/package.json +4 -3
  20. package/src/adapters/common.ts +108 -8
  21. package/src/api/index.ts +3 -3
  22. package/src/api/routes.ts +1 -1
  23. package/src/cli/migrate.ts +307 -307
  24. package/src/cli.ts +1 -1
  25. package/src/coordination/circuit-breaker.ts +83 -83
  26. package/src/coordination/failure-modes.ts +50 -50
  27. package/src/core/decay.ts +63 -63
  28. package/src/core/embeddings.ts +110 -110
  29. package/src/core/index.ts +5 -5
  30. package/src/core/lite-compress.ts +129 -0
  31. package/src/core/logger.ts +36 -36
  32. package/src/core/ml-worker-entry.ts +194 -194
  33. package/src/core/ml-worker.ts +281 -281
  34. package/src/core/query-expander.ts +122 -122
  35. package/src/core/reranker.ts +119 -119
  36. package/src/engine/confidence.ts +120 -120
  37. package/src/engine/connections.ts +162 -162
  38. package/src/engine/consolidation-scheduler.ts +242 -242
  39. package/src/engine/eval.ts +102 -102
  40. package/src/engine/eviction.ts +101 -101
  41. package/src/engine/index.ts +8 -8
  42. package/src/engine/retraction.ts +366 -366
  43. package/src/engine/staging.ts +74 -74
  44. package/src/mcp.ts +70 -4
  45. package/src/storage/factory.ts +147 -147
  46. package/src/storage/index.ts +3 -3
  47. package/src/storage/pglite-schema.ts +166 -166
  48. package/src/storage/pglite.ts +1363 -1363
  49. package/src/storage/store.ts +80 -80
  50. package/src/types/agent.ts +67 -67
  51. package/src/types/checkpoint.ts +46 -46
  52. package/src/types/eval.ts +100 -100
  53. package/src/types/index.ts +6 -6
@@ -184,7 +184,7 @@ export class PGliteEngramStore {
184
184
  params.push(e.id, e.agentId, e.timestamp.toISOString(), e.context, e.resultsReturned, e.topScore, e.latencyMs, JSON.stringify(e.engramIds));
185
185
  }
186
186
  try {
187
- await this.db.query(`INSERT INTO activation_events (id, agent_id, timestamp, context, results_returned, top_score, latency_ms, engram_ids)
187
+ await this.db.query(`INSERT INTO activation_events (id, agent_id, timestamp, context, results_returned, top_score, latency_ms, engram_ids)
188
188
  VALUES ${values.join(',')}`, params);
189
189
  }
190
190
  catch {
@@ -222,18 +222,18 @@ export class PGliteEngramStore {
222
222
  await this.readyPromise;
223
223
  const id = input.id ?? randomUUID();
224
224
  const now = new Date().toISOString();
225
- await this.db.query(`INSERT INTO engrams (
226
- id, agent_id, concept, content, embedding, embedding_model,
227
- confidence, salience, access_count, last_accessed, created_at,
228
- salience_features, reason_codes, stage, ttl, retracted,
229
- tags, memory_type, memory_class, supersedes, episode_id,
230
- task_status, task_priority, blocked_by, sequence, references_json
231
- ) VALUES (
232
- $1, $2, $3, $4, $5::vector, $6,
233
- $7, $8, 0, $9, $10,
234
- $11, $12, 'active', $13, FALSE,
235
- $14, $15, $16, $17, $18,
236
- $19, $20, $21, $22, $23
225
+ await this.db.query(`INSERT INTO engrams (
226
+ id, agent_id, concept, content, embedding, embedding_model,
227
+ confidence, salience, access_count, last_accessed, created_at,
228
+ salience_features, reason_codes, stage, ttl, retracted,
229
+ tags, memory_type, memory_class, supersedes, episode_id,
230
+ task_status, task_priority, blocked_by, sequence, references_json
231
+ ) VALUES (
232
+ $1, $2, $3, $4, $5::vector, $6,
233
+ $7, $8, 0, $9, $10,
234
+ $11, $12, 'active', $13, FALSE,
235
+ $14, $15, $16, $17, $18,
236
+ $19, $20, $21, $22, $23
237
237
  )`, [
238
238
  id,
239
239
  input.agentId,
@@ -362,10 +362,10 @@ export class PGliteEngramStore {
362
362
  }
363
363
  async touchEngram(id) {
364
364
  await this.readyPromise;
365
- await this.db.query(`UPDATE engrams
366
- SET access_count = access_count + 1,
367
- last_accessed = $1,
368
- confidence = LEAST(0.85, confidence + 0.02 / (1.0 + sqrt(access_count::float)))
365
+ await this.db.query(`UPDATE engrams
366
+ SET access_count = access_count + 1,
367
+ last_accessed = $1,
368
+ confidence = LEAST(0.85, confidence + 0.02 / (1.0 + sqrt(access_count::float)))
369
369
  WHERE id = $2`, [new Date().toISOString(), id]);
370
370
  }
371
371
  async updateStage(id, stage) {
@@ -411,9 +411,9 @@ export class PGliteEngramStore {
411
411
  async timeWarp(agentId, ms) {
412
412
  await this.readyPromise;
413
413
  const seconds = Math.round(ms / 1000);
414
- const r1 = await this.db.query(`UPDATE engrams SET
415
- created_at = to_char(($1::timestamptz - interval '1 second' * $2), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
416
- last_accessed = to_char(($3::timestamptz - interval '1 second' * $2), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')
414
+ const r1 = await this.db.query(`UPDATE engrams SET
415
+ created_at = to_char(($1::timestamptz - interval '1 second' * $2), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
416
+ last_accessed = to_char(($3::timestamptz - interval '1 second' * $2), 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')
417
417
  WHERE agent_id = $4`, ['now', seconds, 'now', agentId]);
418
418
  // Simpler: just update with relative arithmetic on stored ISO strings.
419
419
  // PGlite doesn't support all date ops cleanly; fall through to JS-side calculation.
@@ -439,13 +439,13 @@ export class PGliteEngramStore {
439
439
  // Restrict to active + fading. Faded engrams (Paper 1: storage degradation)
440
440
  // retain their embedding so they still participate in semantic recall, even
441
441
  // though their content has been trimmed. Excludes staging/consolidated/archived.
442
- const result = await this.db.query(`SELECT *, (embedding <=> $2::vector) AS distance
443
- FROM engrams
444
- WHERE agent_id = $1
445
- AND embedding IS NOT NULL
446
- AND retracted = FALSE
447
- AND stage IN ('active', 'fading')
448
- ORDER BY distance ASC
442
+ const result = await this.db.query(`SELECT *, (embedding <=> $2::vector) AS distance
443
+ FROM engrams
444
+ WHERE agent_id = $1
445
+ AND embedding IS NOT NULL
446
+ AND retracted = FALSE
447
+ AND stage IN ('active', 'fading')
448
+ ORDER BY distance ASC
449
449
  LIMIT $3`, [agentId, vectorToLiteral(vec), limit]);
450
450
  return result.rows.map((r) => ({ engram: rowToEngram(r), distance: r.distance }));
451
451
  }
@@ -464,11 +464,11 @@ export class PGliteEngramStore {
464
464
  if (tokens.length === 0)
465
465
  return [];
466
466
  const websearchQuery = tokens.join(' OR ');
467
- const result = await this.db.query(`SELECT *, ts_rank_cd(fts, websearch_to_tsquery('english', $2)) AS rank
468
- FROM engrams
469
- WHERE agent_id = $1 AND retracted = FALSE
470
- AND fts @@ websearch_to_tsquery('english', $2)
471
- ORDER BY rank DESC
467
+ const result = await this.db.query(`SELECT *, ts_rank_cd(fts, websearch_to_tsquery('english', $2)) AS rank
468
+ FROM engrams
469
+ WHERE agent_id = $1 AND retracted = FALSE
470
+ AND fts @@ websearch_to_tsquery('english', $2)
471
+ ORDER BY rank DESC
472
472
  LIMIT $3`, [agentId, websearchQuery, limit]);
473
473
  return result.rows.map((r) => ({ engram: rowToEngram(r), bm25Score: calibrateBm25(Number(r.rank)) }));
474
474
  }
@@ -482,11 +482,11 @@ export class PGliteEngramStore {
482
482
  if (tokens.length === 0)
483
483
  return [];
484
484
  const websearchQuery = tokens.join(' OR ');
485
- const result = await this.db.query(`SELECT *, ts_rank_cd(fts, websearch_to_tsquery('english', $2)) AS rank
486
- FROM engrams
487
- WHERE agent_id = ANY($1::text[]) AND retracted = FALSE
488
- AND fts @@ websearch_to_tsquery('english', $2)
489
- ORDER BY rank DESC
485
+ const result = await this.db.query(`SELECT *, ts_rank_cd(fts, websearch_to_tsquery('english', $2)) AS rank
486
+ FROM engrams
487
+ WHERE agent_id = ANY($1::text[]) AND retracted = FALSE
488
+ AND fts @@ websearch_to_tsquery('english', $2)
489
+ ORDER BY rank DESC
490
490
  LIMIT $3`, [agentIds, websearchQuery, limit]);
491
491
  return result.rows.map((r) => ({ engram: rowToEngram(r), bm25Score: calibrateBm25(Number(r.rank)) }));
492
492
  }
@@ -567,32 +567,32 @@ export class PGliteEngramStore {
567
567
  sql += ` AND task_status = $${params.length + 1}`;
568
568
  params.push(status);
569
569
  }
570
- sql += ` ORDER BY
571
- CASE task_priority
572
- WHEN 'urgent' THEN 0
573
- WHEN 'high' THEN 1
574
- WHEN 'medium' THEN 2
575
- WHEN 'low' THEN 3
576
- ELSE 4
577
- END,
570
+ sql += ` ORDER BY
571
+ CASE task_priority
572
+ WHEN 'urgent' THEN 0
573
+ WHEN 'high' THEN 1
574
+ WHEN 'medium' THEN 2
575
+ WHEN 'low' THEN 3
576
+ ELSE 4
577
+ END,
578
578
  created_at DESC`;
579
579
  const result = await this.db.query(sql, params);
580
580
  return result.rows.map(rowToEngram);
581
581
  }
582
582
  async getNextTask(agentId) {
583
583
  await this.readyPromise;
584
- const result = await this.db.query(`SELECT * FROM engrams
585
- WHERE agent_id = $1 AND task_status IN ('open', 'in_progress') AND retracted = FALSE
586
- ORDER BY
587
- CASE task_status WHEN 'in_progress' THEN 0 ELSE 1 END,
588
- CASE task_priority
589
- WHEN 'urgent' THEN 0
590
- WHEN 'high' THEN 1
591
- WHEN 'medium' THEN 2
592
- WHEN 'low' THEN 3
593
- ELSE 4
594
- END,
595
- created_at ASC
584
+ const result = await this.db.query(`SELECT * FROM engrams
585
+ WHERE agent_id = $1 AND task_status IN ('open', 'in_progress') AND retracted = FALSE
586
+ ORDER BY
587
+ CASE task_status WHEN 'in_progress' THEN 0 ELSE 1 END,
588
+ CASE task_priority
589
+ WHEN 'urgent' THEN 0
590
+ WHEN 'high' THEN 1
591
+ WHEN 'medium' THEN 2
592
+ WHEN 'low' THEN 3
593
+ ELSE 4
594
+ END,
595
+ created_at ASC
596
596
  LIMIT 1`, [agentId]);
597
597
  return result.rows.length > 0 ? rowToEngram(result.rows[0]) : null;
598
598
  }
@@ -606,11 +606,11 @@ export class PGliteEngramStore {
606
606
  }
607
607
  async findActiveMatchByConcept(agentId, concept, requiredTags) {
608
608
  await this.readyPromise;
609
- let sql = `SELECT * FROM engrams
610
- WHERE agent_id = $1
611
- AND LOWER(TRIM(concept)) = LOWER(TRIM($2))
612
- AND stage = 'active'
613
- AND retracted = FALSE
609
+ let sql = `SELECT * FROM engrams
610
+ WHERE agent_id = $1
611
+ AND LOWER(TRIM(concept)) = LOWER(TRIM($2))
612
+ AND stage = 'active'
613
+ AND retracted = FALSE
614
614
  AND superseded_by IS NULL`;
615
615
  const params = [agentId, concept];
616
616
  if (requiredTags && requiredTags.length > 0) {
@@ -643,12 +643,12 @@ export class PGliteEngramStore {
643
643
  await this.readyPromise;
644
644
  const id = randomUUID();
645
645
  const now = new Date().toISOString();
646
- await this.db.query(`INSERT INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type, activation_count, created_at, last_activated)
647
- VALUES ($1, $2, $3, $4, $5, $6, 0, $7, $7)
648
- ON CONFLICT (from_engram_id, to_engram_id) DO UPDATE SET
649
- weight = EXCLUDED.weight,
650
- confidence = EXCLUDED.confidence,
651
- last_activated = EXCLUDED.last_activated,
646
+ await this.db.query(`INSERT INTO associations (id, from_engram_id, to_engram_id, weight, confidence, type, activation_count, created_at, last_activated)
647
+ VALUES ($1, $2, $3, $4, $5, $6, 0, $7, $7)
648
+ ON CONFLICT (from_engram_id, to_engram_id) DO UPDATE SET
649
+ weight = EXCLUDED.weight,
650
+ confidence = EXCLUDED.confidence,
651
+ last_activated = EXCLUDED.last_activated,
652
652
  activation_count = associations.activation_count + 1`, [id, fromId, toId, weight, confidence, type, now]);
653
653
  const assoc = await this.getAssociation(fromId, toId);
654
654
  if (!assoc)
@@ -670,12 +670,12 @@ export class PGliteEngramStore {
670
670
  if (engramIds.length === 0)
671
671
  return result;
672
672
  await this.readyPromise;
673
- const r = await this.db.query(`SELECT id, SUM(cnt) AS count, SUM(sw) AS sum_weight FROM (
674
- SELECT from_engram_id AS id, 1 AS cnt, weight AS sw FROM associations WHERE from_engram_id = ANY($1::text[])
675
- UNION ALL
676
- SELECT to_engram_id AS id, 1 AS cnt, weight AS sw FROM associations WHERE to_engram_id = ANY($1::text[])
677
- ) t
678
- WHERE id = ANY($1::text[])
673
+ const r = await this.db.query(`SELECT id, SUM(cnt) AS count, SUM(sw) AS sum_weight FROM (
674
+ SELECT from_engram_id AS id, 1 AS cnt, weight AS sw FROM associations WHERE from_engram_id = ANY($1::text[])
675
+ UNION ALL
676
+ SELECT to_engram_id AS id, 1 AS cnt, weight AS sw FROM associations WHERE to_engram_id = ANY($1::text[])
677
+ ) t
678
+ WHERE id = ANY($1::text[])
679
679
  GROUP BY id`, [engramIds]);
680
680
  for (const row of r.rows) {
681
681
  result.set(row.id, { count: Number(row.count), sumWeight: Number(row.sum_weight) });
@@ -691,7 +691,7 @@ export class PGliteEngramStore {
691
691
  if (engramIds.length === 0)
692
692
  return result;
693
693
  await this.readyPromise;
694
- const r = await this.db.query(`SELECT * FROM associations
694
+ const r = await this.db.query(`SELECT * FROM associations
695
695
  WHERE from_engram_id = ANY($1::text[]) OR to_engram_id = ANY($1::text[])`, [engramIds]);
696
696
  for (const row of r.rows) {
697
697
  const a = rowToAssociation(row);
@@ -731,8 +731,8 @@ export class PGliteEngramStore {
731
731
  }
732
732
  async getAllAssociations(agentId) {
733
733
  await this.readyPromise;
734
- const result = await this.db.query(`SELECT a.* FROM associations a
735
- JOIN engrams e ON a.from_engram_id = e.id
734
+ const result = await this.db.query(`SELECT a.* FROM associations a
735
+ JOIN engrams e ON a.from_engram_id = e.id
736
736
  WHERE e.agent_id = $1`, [agentId]);
737
737
  return result.rows.map(rowToAssociation);
738
738
  }
@@ -741,11 +741,11 @@ export class PGliteEngramStore {
741
741
  // ============================================================
742
742
  async getEvictionCandidates(agentId, limit) {
743
743
  await this.readyPromise;
744
- const result = await this.db.query(`SELECT * FROM engrams
745
- WHERE agent_id = $1 AND stage = 'active' AND retracted = FALSE
746
- ORDER BY (salience * 0.3 + confidence * 0.3
747
- + (access_count::float / (access_count + 5)) * 0.2
748
- + (1.0 / (1.0 + EXTRACT(EPOCH FROM (now() - last_accessed::timestamptz)) / 86400.0)) * 0.2) ASC
744
+ const result = await this.db.query(`SELECT * FROM engrams
745
+ WHERE agent_id = $1 AND stage = 'active' AND retracted = FALSE
746
+ ORDER BY (salience * 0.3 + confidence * 0.3
747
+ + (access_count::float / (access_count + 5)) * 0.2
748
+ + (1.0 / (1.0 + EXTRACT(EPOCH FROM (now() - last_accessed::timestamptz)) / 86400.0)) * 0.2) ASC
749
749
  LIMIT $2`, [agentId, limit]);
750
750
  return result.rows.map(rowToEngram);
751
751
  }
@@ -780,7 +780,7 @@ export class PGliteEngramStore {
780
780
  }
781
781
  async logStagingEvent(event) {
782
782
  await this.readyPromise;
783
- await this.db.query(`INSERT INTO staging_events (engram_id, agent_id, action, resonance_score, timestamp, age_ms)
783
+ await this.db.query(`INSERT INTO staging_events (engram_id, agent_id, action, resonance_score, timestamp, age_ms)
784
784
  VALUES ($1, $2, $3, $4, $5, $6)`, [
785
785
  event.engramId, event.agentId, event.action,
786
786
  event.resonanceScore, event.timestamp.toISOString(), event.ageMs,
@@ -788,18 +788,18 @@ export class PGliteEngramStore {
788
788
  }
789
789
  async logRetrievalFeedback(activationEventId, engramId, useful, context) {
790
790
  await this.readyPromise;
791
- await this.db.query(`INSERT INTO retrieval_feedback (id, activation_event_id, engram_id, useful, context, timestamp)
791
+ await this.db.query(`INSERT INTO retrieval_feedback (id, activation_event_id, engram_id, useful, context, timestamp)
792
792
  VALUES ($1, $2, $3, $4, $5, $6)`, [randomUUID(), activationEventId, engramId, useful, context, new Date().toISOString()]);
793
793
  }
794
794
  async getRetrievalPrecision(agentId, windowHours = 24) {
795
795
  await this.readyPromise;
796
796
  const since = new Date(Date.now() - windowHours * 3600_000).toISOString();
797
- const result = await this.db.query(`SELECT
798
- COUNT(CASE WHEN useful = TRUE THEN 1 END) AS useful_count,
799
- COUNT(*) AS total_count
800
- FROM retrieval_feedback rf
801
- LEFT JOIN activation_events ae ON rf.activation_event_id = ae.id
802
- JOIN engrams e ON rf.engram_id = e.id
797
+ const result = await this.db.query(`SELECT
798
+ COUNT(CASE WHEN useful = TRUE THEN 1 END) AS useful_count,
799
+ COUNT(*) AS total_count
800
+ FROM retrieval_feedback rf
801
+ LEFT JOIN activation_events ae ON rf.activation_event_id = ae.id
802
+ JOIN engrams e ON rf.engram_id = e.id
803
803
  WHERE e.agent_id = $1 AND rf.timestamp > $2`, [agentId, since]);
804
804
  const row = result.rows[0];
805
805
  const total = Number(row?.total_count ?? 0);
@@ -808,10 +808,10 @@ export class PGliteEngramStore {
808
808
  }
809
809
  async getStagingMetrics(agentId) {
810
810
  await this.readyPromise;
811
- const result = await this.db.query(`SELECT
812
- COUNT(CASE WHEN action = 'promoted' THEN 1 END) AS promoted,
813
- COUNT(CASE WHEN action = 'discarded' THEN 1 END) AS discarded,
814
- COUNT(CASE WHEN action = 'expired' THEN 1 END) AS expired
811
+ const result = await this.db.query(`SELECT
812
+ COUNT(CASE WHEN action = 'promoted' THEN 1 END) AS promoted,
813
+ COUNT(CASE WHEN action = 'discarded' THEN 1 END) AS discarded,
814
+ COUNT(CASE WHEN action = 'expired' THEN 1 END) AS expired
815
815
  FROM staging_events WHERE agent_id = $1`, [agentId]);
816
816
  const row = result.rows[0] ?? { promoted: 0, discarded: 0, expired: 0 };
817
817
  return {
@@ -825,8 +825,8 @@ export class PGliteEngramStore {
825
825
  // Flush any buffered activation events so stats reflect the latest writes.
826
826
  await this.flushActivationEvents();
827
827
  const since = new Date(Date.now() - windowHours * 3600_000).toISOString();
828
- const result = await this.db.query(`SELECT latency_ms FROM activation_events
829
- WHERE agent_id = $1 AND timestamp > $2
828
+ const result = await this.db.query(`SELECT latency_ms FROM activation_events
829
+ WHERE agent_id = $1 AND timestamp > $2
830
830
  ORDER BY latency_ms ASC`, [agentId, since]);
831
831
  if (result.rows.length === 0)
832
832
  return { count: 0, avgLatencyMs: 0, p95LatencyMs: 0 };
@@ -851,7 +851,7 @@ export class PGliteEngramStore {
851
851
  await this.readyPromise;
852
852
  const id = randomUUID();
853
853
  const now = new Date().toISOString();
854
- await this.db.query(`INSERT INTO episodes (id, agent_id, label, embedding, engram_count, start_time, end_time, created_at)
854
+ await this.db.query(`INSERT INTO episodes (id, agent_id, label, embedding, engram_count, start_time, end_time, created_at)
855
855
  VALUES ($1, $2, $3, $4::vector, 0, $5, $5, $5)`, [id, input.agentId, input.label, vectorToLiteral(input.embedding ?? null), now]);
856
856
  const ep = await this.getEpisode(id);
857
857
  if (!ep)
@@ -877,9 +877,9 @@ export class PGliteEngramStore {
877
877
  async addEngramToEpisode(engramId, episodeId) {
878
878
  await this.readyPromise;
879
879
  await this.db.query(`UPDATE engrams SET episode_id = $1 WHERE id = $2`, [episodeId, engramId]);
880
- await this.db.query(`UPDATE episodes SET
881
- engram_count = engram_count + 1,
882
- end_time = GREATEST(end_time, $1)
880
+ await this.db.query(`UPDATE episodes SET
881
+ engram_count = engram_count + 1,
882
+ end_time = GREATEST(end_time, $1)
883
883
  WHERE id = $2`, [new Date().toISOString(), episodeId]);
884
884
  }
885
885
  async getEngramsByEpisode(episodeId) {
@@ -918,44 +918,44 @@ export class PGliteEngramStore {
918
918
  async updateAutoCheckpointWrite(agentId, engramId) {
919
919
  await this.readyPromise;
920
920
  const now = new Date().toISOString();
921
- await this.db.query(`INSERT INTO conscious_state (agent_id, last_write_id, last_activity_at, write_count_since_consolidation, updated_at)
922
- VALUES ($1, $2, $3, 1, $3)
923
- ON CONFLICT(agent_id) DO UPDATE SET
924
- last_write_id = EXCLUDED.last_write_id,
925
- last_activity_at = EXCLUDED.last_activity_at,
926
- write_count_since_consolidation = conscious_state.write_count_since_consolidation + 1,
921
+ await this.db.query(`INSERT INTO conscious_state (agent_id, last_write_id, last_activity_at, write_count_since_consolidation, updated_at)
922
+ VALUES ($1, $2, $3, 1, $3)
923
+ ON CONFLICT(agent_id) DO UPDATE SET
924
+ last_write_id = EXCLUDED.last_write_id,
925
+ last_activity_at = EXCLUDED.last_activity_at,
926
+ write_count_since_consolidation = conscious_state.write_count_since_consolidation + 1,
927
927
  updated_at = EXCLUDED.updated_at`, [agentId, engramId, now]);
928
928
  }
929
929
  async updateAutoCheckpointRecall(agentId, context, engramIds) {
930
930
  await this.readyPromise;
931
931
  const now = new Date().toISOString();
932
- await this.db.query(`INSERT INTO conscious_state (agent_id, last_recall_context, last_recall_ids, last_activity_at, recall_count_since_consolidation, updated_at)
933
- VALUES ($1, $2, $3, $4, 1, $4)
934
- ON CONFLICT(agent_id) DO UPDATE SET
935
- last_recall_context = EXCLUDED.last_recall_context,
936
- last_recall_ids = EXCLUDED.last_recall_ids,
937
- last_activity_at = EXCLUDED.last_activity_at,
938
- recall_count_since_consolidation = conscious_state.recall_count_since_consolidation + 1,
932
+ await this.db.query(`INSERT INTO conscious_state (agent_id, last_recall_context, last_recall_ids, last_activity_at, recall_count_since_consolidation, updated_at)
933
+ VALUES ($1, $2, $3, $4, 1, $4)
934
+ ON CONFLICT(agent_id) DO UPDATE SET
935
+ last_recall_context = EXCLUDED.last_recall_context,
936
+ last_recall_ids = EXCLUDED.last_recall_ids,
937
+ last_activity_at = EXCLUDED.last_activity_at,
938
+ recall_count_since_consolidation = conscious_state.recall_count_since_consolidation + 1,
939
939
  updated_at = EXCLUDED.updated_at`, [agentId, context, JSON.stringify(engramIds), now]);
940
940
  }
941
941
  async touchActivity(agentId) {
942
942
  await this.readyPromise;
943
943
  const now = new Date().toISOString();
944
- await this.db.query(`INSERT INTO conscious_state (agent_id, last_activity_at, updated_at)
945
- VALUES ($1, $2, $2)
946
- ON CONFLICT(agent_id) DO UPDATE SET
947
- last_activity_at = EXCLUDED.last_activity_at,
944
+ await this.db.query(`INSERT INTO conscious_state (agent_id, last_activity_at, updated_at)
945
+ VALUES ($1, $2, $2)
946
+ ON CONFLICT(agent_id) DO UPDATE SET
947
+ last_activity_at = EXCLUDED.last_activity_at,
948
948
  updated_at = EXCLUDED.updated_at`, [agentId, now]);
949
949
  }
950
950
  async saveCheckpoint(agentId, state) {
951
951
  await this.readyPromise;
952
952
  const now = new Date().toISOString();
953
- await this.db.query(`INSERT INTO conscious_state (agent_id, execution_state, checkpoint_at, last_activity_at, updated_at)
954
- VALUES ($1, $2, $3, $3, $3)
955
- ON CONFLICT(agent_id) DO UPDATE SET
956
- execution_state = EXCLUDED.execution_state,
957
- checkpoint_at = EXCLUDED.checkpoint_at,
958
- last_activity_at = EXCLUDED.last_activity_at,
953
+ await this.db.query(`INSERT INTO conscious_state (agent_id, execution_state, checkpoint_at, last_activity_at, updated_at)
954
+ VALUES ($1, $2, $3, $3, $3)
955
+ ON CONFLICT(agent_id) DO UPDATE SET
956
+ execution_state = EXCLUDED.execution_state,
957
+ checkpoint_at = EXCLUDED.checkpoint_at,
958
+ last_activity_at = EXCLUDED.last_activity_at,
959
959
  updated_at = EXCLUDED.updated_at`, [agentId, JSON.stringify(state), now]);
960
960
  }
961
961
  async getCheckpoint(agentId) {
@@ -988,13 +988,13 @@ export class PGliteEngramStore {
988
988
  await this.db.query(`UPDATE conscious_state SET last_mini_consolidation_at = $1, updated_at = $1 WHERE agent_id = $2`, [now, agentId]);
989
989
  }
990
990
  else {
991
- await this.db.query(`UPDATE conscious_state SET
992
- last_consolidation_at = $1,
993
- last_mini_consolidation_at = $1,
994
- write_count_since_consolidation = 0,
995
- recall_count_since_consolidation = 0,
996
- consolidation_cycle_count = consolidation_cycle_count + 1,
997
- updated_at = $1
991
+ await this.db.query(`UPDATE conscious_state SET
992
+ last_consolidation_at = $1,
993
+ last_mini_consolidation_at = $1,
994
+ write_count_since_consolidation = 0,
995
+ recall_count_since_consolidation = 0,
996
+ consolidation_cycle_count = consolidation_cycle_count + 1,
997
+ updated_at = $1
998
998
  WHERE agent_id = $2`, [now, agentId]);
999
999
  }
1000
1000
  }
@@ -1019,10 +1019,10 @@ export class PGliteEngramStore {
1019
1019
  // ============================================================
1020
1020
  async getLatestByTag(opts) {
1021
1021
  await this.readyPromise;
1022
- let sql = `SELECT * FROM engrams
1023
- WHERE agent_id = $1
1024
- AND retracted = $2
1025
- AND stage = 'active'
1022
+ let sql = `SELECT * FROM engrams
1023
+ WHERE agent_id = $1
1024
+ AND retracted = $2
1025
+ AND stage = 'active'
1026
1026
  AND tags LIKE $3`;
1027
1027
  const params = [opts.agentId, opts.retracted ?? false, `%"${opts.tagKeyPrefix}%`];
1028
1028
  if (opts.scopeTagsAll && opts.scopeTagsAll.length > 0) {
@@ -1049,10 +1049,10 @@ export class PGliteEngramStore {
1049
1049
  }
1050
1050
  async getTopBy(opts) {
1051
1051
  await this.readyPromise;
1052
- let sql = `SELECT * FROM engrams
1053
- WHERE agent_id = $1
1054
- AND retracted = $2
1055
- AND stage = 'active'
1052
+ let sql = `SELECT * FROM engrams
1053
+ WHERE agent_id = $1
1054
+ AND retracted = $2
1055
+ AND stage = 'active'
1056
1056
  AND tags LIKE $3`;
1057
1057
  const params = [opts.agentId, opts.retracted ?? false, `%"${opts.sortField}%`];
1058
1058
  if (opts.filterTagsAll && opts.filterTagsAll.length > 0) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-working-memory",
3
- "version": "0.8.5",
4
- "description": "Cognitive memory layer for AI agents activation-based retrieval, salience filtering, associative connections",
3
+ "version": "0.8.7",
4
+ "description": "Cognitive memory layer for AI agents — activation-based retrieval, salience filtering, associative connections",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
@@ -31,7 +31,7 @@
31
31
  "test:all": "vitest run && tsx tests/mcp-smoke.ts"
32
32
  },
33
33
  "engines": {
34
- "node": ">=20.0.0"
34
+ "node": ">=22.0.0"
35
35
  },
36
36
  "license": "Apache-2.0",
37
37
  "files": [
@@ -45,6 +45,7 @@
45
45
  "@electric-sql/pglite": "^0.4.6",
46
46
  "@huggingface/transformers": "^3.8.1",
47
47
  "@modelcontextprotocol/sdk": "^1.27.1",
48
+ "@toon-format/toon": "^2.3.0",
48
49
  "better-sqlite3": "^12.6.2",
49
50
  "fastify": "^5.8.2",
50
51
  "zod": "^4.3.6"
@@ -352,6 +352,33 @@ When it isn't:
352
352
  return nothing, the memory probably isn't there — read the code instead of
353
353
  burning more recalls.
354
354
 
355
+ ### Recall tuning (0.8.x — opt-in parameters for higher-quality recall)
356
+ Default \`memory_recall\` is tuned for the common case. The 0.8.x recall pipeline
357
+ exposes four opt-in parameters that change the cost/quality tradeoff. Use them
358
+ when the default doesn't match what you actually need.
359
+
360
+ - **\`granularity: 'compact'\`** — every result carries a 200-char \`summary\`
361
+ field with a query-aware snippet (the densest window of query terms in the
362
+ content). Use this when you expect to scan 5+ results to find one — saves
363
+ ~70% of recall output tokens. The full content stays available in
364
+ \`engram.content\` if you want to drill into a specific result.
365
+ - **\`granularity: 'auto'\`** — confidence-adaptive. If the top result is a clear
366
+ winner, it gets a longer summary while the rest are compact. If confidence
367
+ is uniform across results, everything is compact. Use when you don't know
368
+ in advance whether one result will dominate.
369
+ - **\`require_confidence: 0.10 | 0.25 | 0.40\`** — opt-in abstention. AWM
370
+ returns \`[]\` instead of low-confidence noise. Use when you're about to ACT
371
+ on the recalled fact (grounding a decision, citing the memory verbatim,
372
+ contradicting a prior assumption). Thresholds: \`0.10\` strict — only abstain
373
+ on garbage; \`0.25\` balanced; \`0.40\` aggressive — prefer "I don't know"
374
+ over "best of bad." When abstention fires (empty result), treat it as a
375
+ signal — either the memory genuinely isn't there (read the code) or your
376
+ query missed (reformulate). Don't retry without the threshold.
377
+ - **\`workspace: "<name>"\`** — hive-mode recall across all agents in the
378
+ workspace. Use when other agents may have written canonical knowledge you
379
+ need. Default is agent-scoped (your own memories only). Can also be set
380
+ globally via the \`AWM_WORKSPACE\` env var.
381
+
355
382
  ### Keep memory fresh
356
383
  - After recalling a memory, if you observe the real state is different → call
357
384
  \`memory_supersede\` immediately with the corrected version.
@@ -362,6 +389,24 @@ When it isn't:
362
389
  drifts out of date. The system relies on you to keep it current. This is the #1
363
390
  failure mode.**
364
391
 
392
+ ### Content fade — write-and-forget is safe (0.8.x)
393
+ Un-recalled engrams gradually fade their content while preserving cue pathways
394
+ (concept + tags + embedding stay intact). This is Paper 1 — storage
395
+ degradation. Practical implications:
396
+
397
+ - **Don't manually purge memories** to "save space." The system already
398
+ compresses unused content. Old memories stay findable via cue match even
399
+ when their body has decayed.
400
+ - **Don't over-pin with \`memory_class: canonical\`** to fight fade. Canonical
401
+ only changes salience gating at write time, not fade behavior. Fade
402
+ affects un-recalled engrams of any class.
403
+ - **Recall keeps content alive.** Every recall touches the engram and resets
404
+ its fade clock. Frequently-recalled memories stay full-fidelity automatically.
405
+ - **Supersede is the right tool for stale facts.** When you observe a memory
406
+ is outdated, call \`memory_supersede\` — the new version inherits the old
407
+ one's coherent associations (counter-narrative replacement, 0.8.x) so cue
408
+ pathways carry forward to the replacement.
409
+
365
410
  ### Example — good vs bad memory_write
366
411
 
367
412
  **BAD** (no prefix tags, vague concept, can't be recalled by future queries):
@@ -392,19 +437,74 @@ memory_write(
392
437
  - AWM is shared across all agents in real time. When any agent writes or supersedes a
393
438
  memory, every other agent can recall it immediately.
394
439
 
440
+ ### Output compression (token efficiency, output-only)
441
+ When a tool returns a LARGE STRUCTURED result you need to keep in context — a JSON
442
+ array of records, query rows, a log dump, an API response — pass it through
443
+ \`compress_output\` first. It re-encodes the data as TOON (a compact, lossless,
444
+ schema-aware tabular form of JSON), cutting ~50-65% of the tokens at no
445
+ comprehension cost. This is output-only: it never changes the data or your memories.
446
+ - Use it on big STRUCTURED outputs, not on prose. Prose is returned unchanged —
447
+ for trimming memory prose, use recall \`granularity: 'compact'\` instead.
448
+ - It returns a \`ref\`; call \`retrieve_original(ref)\` if you later need the exact
449
+ verbatim source (e.g. to hand it to another tool unchanged).
450
+ - Don't bother for small outputs — it only compresses when the saving is worthwhile
451
+ and falls back to plain JSON if TOON wouldn't reproduce the data exactly.
452
+
453
+ ### Backend (SQLite vs PGlite, 0.8.x)
454
+ AWM ships two storage backends. The installer picks SQLite by default; both
455
+ are functionally equivalent for cognitive workloads, but differ in operational
456
+ guarantees:
457
+
458
+ - **SQLite** (default) — embedded, **multi-process safe** via WAL mode. Best
459
+ for single-machine setups and MCP scenarios where multiple Claude Code
460
+ sessions may open the same database concurrently.
461
+ - **PGlite** — embedded Postgres (WASM) with pgvector. **Single-process only**
462
+ — two MCP processes against the same \`memory-pglite/\` directory will
463
+ abort the second. Pick via \`AWM_STORE_BACKEND=pglite\` and
464
+ \`AWM_DB_PATH=path/to/memory-pglite\`.
465
+ - **Auto-detect** — if \`AWM_DB_PATH\` points to a directory that already
466
+ exists, AWM detects PGlite; a file → SQLite. No explicit
467
+ \`AWM_STORE_BACKEND\` needed when an existing DB is present.
468
+
469
+ For the comparison table (recall quality parity, BM25 vs \`ts_rank_cd\`,
470
+ multi-process guarantees), see \`docs/pglite-feature-parity.md\`.
471
+
395
472
  ### Diagnostics / escape hatches (env vars, only if you know why)
396
- The 0.7.6→0.7.14 work cut recall latency from 11s to ~300ms. Each optimization
397
- is gated by an env-var so it can be disabled for A/B testing if a regression
398
- appears in your workload:
473
+ The 0.7.6→0.7.14 work cut recall latency from 11s to ~300ms. The 0.8.x work
474
+ added the write-path rewrite (per-write 300+ ms under 10ms) and PGlite
475
+ parity tuning. Each optimization is gated by an env-var so it can be disabled
476
+ for A/B testing if a regression appears in your workload:
399
477
 
400
- - \`AWM_DISABLE_POOL_FILTER=1\` (0.7.7+) — disables the candidate pool reduction
478
+ Recall pipeline (0.7.x):
479
+ - \`AWM_DISABLE_POOL_FILTER=1\` — disables the candidate pool reduction
401
480
  pre-filter in recall. Reverts to scoring all active candidates.
402
- - \`AWM_DISABLE_SLIM_CACHE=1\` (0.7.10+) — disables the in-memory slim cache.
481
+ - \`AWM_DISABLE_SLIM_CACHE=1\` — disables the in-memory slim cache.
403
482
  Reverts to per-recall SQL fetch + Buffer→Float32Array conversion.
404
- - \`AWM_DISABLE_RERANK_SKIP=1\` (0.7.10+) — disables the cross-encoder skip on
483
+ - \`AWM_DISABLE_RERANK_SKIP=1\` — disables the cross-encoder skip on
405
484
  clear-winner queries. Forces every recall through the reranker.
406
- - \`AWM_DISABLE_EXPANSION_CACHE=1\` (0.7.11+) — disables the query expansion
407
- skip heuristic + LRU cache. Forces every recall through flan-t5-small.
485
+ - \`AWM_DISABLE_EXPANSION_CACHE=1\` — disables the query expansion skip
486
+ heuristic + LRU cache. Forces every recall through flan-t5-small.
487
+
488
+ Write pipeline + lifecycle (0.8.x):
489
+ - \`AWM_REINFORCE_MAX_CONTENT_LEN=1500\` — max chars an engram's content
490
+ can grow to via merge-on-reinforce (drop-oldest on overflow). Higher =
491
+ preserves more reinforced detail; lower = leaner recall output.
492
+ - \`AWM_REINFORCE_MERGE_CONTENT=0\` — disable content merge on reinforce.
493
+ Reverts to pre-0.8.5 behavior (discard new content, only bump confidence).
494
+ - \`AWM_NOVELTY_EMBED=0\` — disable the cosine channel in novelty
495
+ computation. BM25-only fallback. Reverts to pre-0.8.5 novelty.
496
+ - \`AWM_GRANULARITY_COMPACT_LEN=200\` — char budget for query-aware snippet
497
+ in \`granularity: 'compact'\` mode.
498
+ - \`AWM_GRANULARITY_FULL_LEN=1000\` — char budget for the top result in
499
+ \`granularity: 'auto'\` mode when there's a clear winner.
500
+
501
+ PGlite backend (0.8.x):
502
+ - \`AWM_PGLITE_BM25_M=1\` — multiplier on PGlite \`ts_rank_cd\` to calibrate
503
+ against SQLite FTS5 BM25 distribution. M=1 (default) is passthrough;
504
+ higher M boosts PGlite scores at the cost of recall-ranking precision
505
+ (see CHANGELOG 0.8.5 follow-up).
506
+ - \`AWM_IVFFLAT_PROBES=5\` — pgvector ivfflat probes per query. Higher =
507
+ more accurate, slower.
408
508
 
409
509
  In production, leave these all unset. Use only when diagnosing a suspected
410
510
  recall-quality regression.
package/src/api/index.ts CHANGED
@@ -1,3 +1,3 @@
1
- // Copyright 2026 Robert Winter / Complete Ideas
2
- // SPDX-License-Identifier: Apache-2.0
3
- export * from './routes.js';
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ export * from './routes.js';
package/src/api/routes.ts CHANGED
@@ -952,7 +952,7 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
952
952
  const base: Record<string, unknown> = {
953
953
  status: 'ok',
954
954
  timestamp: new Date().toISOString(),
955
- version: '0.8.5',
955
+ version: '0.8.7',
956
956
  coordination: coordEnabled,
957
957
  };
958
958
  if (coordEnabled && typeof (deps.store as any).getDb === 'function') {