agent-working-memory 0.7.16 → 0.8.5

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 (200) hide show
  1. package/README.md +156 -15
  2. package/dist/adapters/claude-code.d.ts.map +1 -1
  3. package/dist/adapters/claude-code.js +2 -16
  4. package/dist/adapters/claude-code.js.map +1 -1
  5. package/dist/adapters/codex.d.ts.map +1 -1
  6. package/dist/adapters/codex.js +2 -11
  7. package/dist/adapters/codex.js.map +1 -1
  8. package/dist/adapters/common.d.ts +18 -0
  9. package/dist/adapters/common.d.ts.map +1 -1
  10. package/dist/adapters/common.js +254 -141
  11. package/dist/adapters/common.js.map +1 -1
  12. package/dist/adapters/cursor.d.ts.map +1 -1
  13. package/dist/adapters/cursor.js +2 -15
  14. package/dist/adapters/cursor.js.map +1 -1
  15. package/dist/adapters/http.d.ts.map +1 -1
  16. package/dist/adapters/http.js +28 -34
  17. package/dist/adapters/http.js.map +1 -1
  18. package/dist/api/routes.d.ts +1 -1
  19. package/dist/api/routes.d.ts.map +1 -1
  20. package/dist/api/routes.js +280 -113
  21. package/dist/api/routes.js.map +1 -1
  22. package/dist/cli/migrate.d.ts +42 -0
  23. package/dist/cli/migrate.d.ts.map +1 -0
  24. package/dist/cli/migrate.js +268 -0
  25. package/dist/cli/migrate.js.map +1 -0
  26. package/dist/cli.js +45 -1
  27. package/dist/cli.js.map +1 -1
  28. package/dist/coordination/circuit-breaker.d.ts +24 -0
  29. package/dist/coordination/circuit-breaker.d.ts.map +1 -0
  30. package/dist/coordination/circuit-breaker.js +70 -0
  31. package/dist/coordination/circuit-breaker.js.map +1 -0
  32. package/dist/coordination/failure-modes.d.ts +21 -0
  33. package/dist/coordination/failure-modes.d.ts.map +1 -0
  34. package/dist/coordination/failure-modes.js +49 -0
  35. package/dist/coordination/failure-modes.js.map +1 -0
  36. package/dist/coordination/peer-decisions.js +7 -7
  37. package/dist/coordination/routes.d.ts.map +1 -1
  38. package/dist/coordination/routes.js +44 -1
  39. package/dist/coordination/routes.js.map +1 -1
  40. package/dist/coordination/schema.d.ts.map +1 -1
  41. package/dist/coordination/schema.js +18 -0
  42. package/dist/coordination/schema.js.map +1 -1
  43. package/dist/coordination/schemas.d.ts +9 -9
  44. package/dist/coordination/stale.d.ts +9 -1
  45. package/dist/coordination/stale.d.ts.map +1 -1
  46. package/dist/coordination/stale.js +53 -9
  47. package/dist/coordination/stale.js.map +1 -1
  48. package/dist/core/embeddings.d.ts +16 -10
  49. package/dist/core/embeddings.d.ts.map +1 -1
  50. package/dist/core/embeddings.js +54 -38
  51. package/dist/core/embeddings.js.map +1 -1
  52. package/dist/core/ml-worker-entry.d.ts +17 -0
  53. package/dist/core/ml-worker-entry.d.ts.map +1 -0
  54. package/dist/core/ml-worker-entry.js +193 -0
  55. package/dist/core/ml-worker-entry.js.map +1 -0
  56. package/dist/core/ml-worker.d.ts +59 -0
  57. package/dist/core/ml-worker.d.ts.map +1 -0
  58. package/dist/core/ml-worker.js +253 -0
  59. package/dist/core/ml-worker.js.map +1 -0
  60. package/dist/core/query-expander.d.ts +13 -15
  61. package/dist/core/query-expander.d.ts.map +1 -1
  62. package/dist/core/query-expander.js +48 -54
  63. package/dist/core/query-expander.js.map +1 -1
  64. package/dist/core/reranker.d.ts +9 -19
  65. package/dist/core/reranker.d.ts.map +1 -1
  66. package/dist/core/reranker.js +33 -35
  67. package/dist/core/reranker.js.map +1 -1
  68. package/dist/core/salience.d.ts +32 -5
  69. package/dist/core/salience.d.ts.map +1 -1
  70. package/dist/core/salience.js +201 -28
  71. package/dist/core/salience.js.map +1 -1
  72. package/dist/core/write-pipeline.d.ts +128 -0
  73. package/dist/core/write-pipeline.d.ts.map +1 -0
  74. package/dist/core/write-pipeline.js +405 -0
  75. package/dist/core/write-pipeline.js.map +1 -0
  76. package/dist/engine/activation.d.ts +2 -2
  77. package/dist/engine/activation.d.ts.map +1 -1
  78. package/dist/engine/activation.js +238 -115
  79. package/dist/engine/activation.js.map +1 -1
  80. package/dist/engine/confidence.d.ts +62 -0
  81. package/dist/engine/confidence.d.ts.map +1 -0
  82. package/dist/engine/confidence.js +100 -0
  83. package/dist/engine/confidence.js.map +1 -0
  84. package/dist/engine/connections.d.ts +47 -8
  85. package/dist/engine/connections.d.ts.map +1 -1
  86. package/dist/engine/connections.js +83 -22
  87. package/dist/engine/connections.js.map +1 -1
  88. package/dist/engine/consolidation-scheduler.d.ts +46 -10
  89. package/dist/engine/consolidation-scheduler.d.ts.map +1 -1
  90. package/dist/engine/consolidation-scheduler.js +170 -50
  91. package/dist/engine/consolidation-scheduler.js.map +1 -1
  92. package/dist/engine/consolidation.d.ts +16 -2
  93. package/dist/engine/consolidation.d.ts.map +1 -1
  94. package/dist/engine/consolidation.js +143 -44
  95. package/dist/engine/consolidation.js.map +1 -1
  96. package/dist/engine/eval.d.ts +2 -2
  97. package/dist/engine/eval.d.ts.map +1 -1
  98. package/dist/engine/eval.js +9 -9
  99. package/dist/engine/eval.js.map +1 -1
  100. package/dist/engine/eviction.d.ts +4 -4
  101. package/dist/engine/eviction.d.ts.map +1 -1
  102. package/dist/engine/eviction.js +15 -15
  103. package/dist/engine/eviction.js.map +1 -1
  104. package/dist/engine/retraction.d.ts +93 -5
  105. package/dist/engine/retraction.d.ts.map +1 -1
  106. package/dist/engine/retraction.js +238 -19
  107. package/dist/engine/retraction.js.map +1 -1
  108. package/dist/engine/staging.d.ts +1 -1
  109. package/dist/engine/staging.d.ts.map +1 -1
  110. package/dist/engine/staging.js +3 -3
  111. package/dist/engine/staging.js.map +1 -1
  112. package/dist/hooks/sidecar.d.ts +1 -1
  113. package/dist/hooks/sidecar.d.ts.map +1 -1
  114. package/dist/hooks/sidecar.js +4 -4
  115. package/dist/hooks/sidecar.js.map +1 -1
  116. package/dist/index.js +48 -31
  117. package/dist/index.js.map +1 -1
  118. package/dist/mcp.js +110 -155
  119. package/dist/mcp.js.map +1 -1
  120. package/dist/storage/factory.d.ts +55 -0
  121. package/dist/storage/factory.d.ts.map +1 -0
  122. package/dist/storage/factory.js +135 -0
  123. package/dist/storage/factory.js.map +1 -0
  124. package/dist/storage/pglite-schema.d.ts +20 -0
  125. package/dist/storage/pglite-schema.d.ts.map +1 -0
  126. package/dist/storage/pglite-schema.js +165 -0
  127. package/dist/storage/pglite-schema.js.map +1 -0
  128. package/dist/storage/pglite.d.ts +187 -0
  129. package/dist/storage/pglite.d.ts.map +1 -0
  130. package/dist/storage/pglite.js +1114 -0
  131. package/dist/storage/pglite.js.map +1 -0
  132. package/dist/storage/sqlite.d.ts +117 -0
  133. package/dist/storage/sqlite.d.ts.map +1 -1
  134. package/dist/storage/sqlite.js +357 -8
  135. package/dist/storage/sqlite.js.map +1 -1
  136. package/dist/storage/store.d.ts +63 -0
  137. package/dist/storage/store.d.ts.map +1 -0
  138. package/dist/storage/store.js +27 -0
  139. package/dist/storage/store.js.map +1 -0
  140. package/dist/types/engram.d.ts +92 -2
  141. package/dist/types/engram.d.ts.map +1 -1
  142. package/dist/types/engram.js.map +1 -1
  143. package/package.json +3 -1
  144. package/src/adapters/claude-code.ts +218 -234
  145. package/src/adapters/codex.ts +252 -262
  146. package/src/adapters/common.ts +411 -284
  147. package/src/adapters/cursor.ts +135 -150
  148. package/src/adapters/http.ts +93 -100
  149. package/src/adapters/index.ts +31 -31
  150. package/src/adapters/types.ts +75 -75
  151. package/src/api/index.ts +3 -3
  152. package/src/api/routes.ts +367 -120
  153. package/src/cli/migrate.ts +307 -0
  154. package/src/cli.ts +37 -1
  155. package/src/coordination/circuit-breaker.ts +83 -0
  156. package/src/coordination/events.ts +90 -90
  157. package/src/coordination/failure-modes.ts +50 -0
  158. package/src/coordination/peer-decisions.ts +105 -105
  159. package/src/coordination/plugin-loader.ts +60 -60
  160. package/src/coordination/plugin.ts +44 -44
  161. package/src/coordination/routes.ts +56 -1
  162. package/src/coordination/schema.ts +13 -0
  163. package/src/coordination/stale.ts +80 -11
  164. package/src/coordination/types.ts +311 -311
  165. package/src/coordination/write-mutex.ts +69 -69
  166. package/src/core/auto-tagger.ts +168 -168
  167. package/src/core/decay.ts +63 -63
  168. package/src/core/embeddings.ts +110 -93
  169. package/src/core/index.ts +5 -5
  170. package/src/core/logger.ts +36 -36
  171. package/src/core/ml-worker-entry.ts +194 -0
  172. package/src/core/ml-worker.ts +281 -0
  173. package/src/core/query-expander.ts +122 -128
  174. package/src/core/reranker.ts +119 -125
  175. package/src/core/salience.ts +200 -33
  176. package/src/core/write-pipeline.ts +521 -0
  177. package/src/engine/activation.ts +235 -109
  178. package/src/engine/confidence.ts +120 -0
  179. package/src/engine/connections.ts +162 -103
  180. package/src/engine/consolidation-scheduler.ts +242 -125
  181. package/src/engine/consolidation.ts +138 -45
  182. package/src/engine/eval.ts +102 -102
  183. package/src/engine/eviction.ts +101 -101
  184. package/src/engine/index.ts +8 -8
  185. package/src/engine/retraction.ts +366 -116
  186. package/src/engine/staging.ts +74 -74
  187. package/src/hooks/sidecar.ts +5 -5
  188. package/src/index.ts +42 -28
  189. package/src/mcp.ts +105 -152
  190. package/src/storage/factory.ts +147 -0
  191. package/src/storage/index.ts +3 -3
  192. package/src/storage/pglite-schema.ts +166 -0
  193. package/src/storage/pglite.ts +1363 -0
  194. package/src/storage/sqlite.ts +402 -7
  195. package/src/storage/store.ts +80 -0
  196. package/src/types/agent.ts +67 -67
  197. package/src/types/checkpoint.ts +46 -46
  198. package/src/types/engram.ts +101 -3
  199. package/src/types/eval.ts +100 -100
  200. package/src/types/index.ts +6 -6
@@ -23,7 +23,8 @@
23
23
  import { cosineSimilarity } from '../core/embeddings.js';
24
24
  import { strengthenAssociation, decayAssociation } from '../core/hebbian.js';
25
25
  import type { Engram } from '../types/index.js';
26
- import type { EngramStore } from '../storage/sqlite.js';
26
+ import type { IEngramStore as EngramStore } from '../storage/store.js';
27
+ import type { ConnectionEngine } from './connections.js';
27
28
 
28
29
  /** Cosine similarity for initial candidate detection (single-link entry gate) */
29
30
  const SIMILARITY_THRESHOLD = 0.65;
@@ -81,6 +82,36 @@ const MAX_REDUNDANCY_PRUNE_PER_CYCLE = 25;
81
82
  /** Max confidence drift per consolidation cycle (prevents runaway) */
82
83
  const CONFIDENCE_DRIFT_CAP = 0.03;
83
84
 
85
+ /**
86
+ * Content fade — Paper 1 (PLOS Comp Biology, storage degradation).
87
+ * Memories whose content has not been recalled in a long time get coarsened:
88
+ * the content string is trimmed to FADE_KEEP_CHARS with a "[faded]" marker,
89
+ * while concept, tags, and embedding are preserved.
90
+ *
91
+ * Rationale: human memory degrades the detailed surface but retains the gist
92
+ * and the cue-association pathway. The faded engram still surfaces by tag/concept
93
+ * BM25 and by vector similarity (embedding preserved), but the full content body
94
+ * is gone — which is the correct behavior for a memory that has not been useful
95
+ * enough to rehearse in a long time.
96
+ *
97
+ * Fade comes BEFORE archive in the lifecycle (active → fading → archived → deleted),
98
+ * applied only to engrams that have been accessed at least once. Engrams that have
99
+ * never been accessed go straight to archive via the standard forgetting path —
100
+ * they have no surface worth preserving.
101
+ *
102
+ * Env: AWM_FADE_DAYS_SINCE_ACCESS (default 45),
103
+ * AWM_FADE_KEEP_CHARS (default 150),
104
+ * AWM_FADE_MIN_CONTENT_LEN (default 250),
105
+ * AWM_FADE_MAX_PER_CYCLE (default 25).
106
+ */
107
+ const FADE_DAYS_SINCE_ACCESS = Number(process.env.AWM_FADE_DAYS_SINCE_ACCESS ?? 45);
108
+ const FADE_KEEP_CHARS = Number(process.env.AWM_FADE_KEEP_CHARS ?? 150);
109
+ const FADE_MIN_CONTENT_LEN = Number(process.env.AWM_FADE_MIN_CONTENT_LEN ?? 250);
110
+ const FADE_MAX_PER_CYCLE = Number(process.env.AWM_FADE_MAX_PER_CYCLE ?? 25);
111
+ const FADE_PROTECTED_ACCESS_COUNT = 10;
112
+ const FADE_MARKER = '… [faded]';
113
+ const FADE_PROTECTED_CLASSES = new Set(['canonical', 'structural']);
114
+
84
115
  /** Days without recall before confidence starts drifting down */
85
116
  const CONFIDENCE_NEGLECT_DAYS = 30;
86
117
 
@@ -94,6 +125,7 @@ export interface ConsolidationResult {
94
125
  edgesNormalized: number;
95
126
  memoriesForgotten: number;
96
127
  memoriesArchived: number;
128
+ memoriesFaded: number;
97
129
  redundancyPruned: number;
98
130
  confidenceAdjusted: number;
99
131
  stagingPromoted: number;
@@ -114,24 +146,45 @@ const SYNTH_STOPWORDS = new Set(['the', 'is', 'a', 'an', 'and', 'or', 'of', 'to'
114
146
 
115
147
  export class ConsolidationEngine {
116
148
  private store: EngramStore;
149
+ private connectionEngine: ConnectionEngine | null;
117
150
 
118
- constructor(store: EngramStore) {
151
+ /**
152
+ * Optional `connectionEngine` is drained at the start of each
153
+ * consolidation pass (v0.8.2). Connection discovery is no longer
154
+ * triggered per write — the work batches into the existing sleep cycle
155
+ * so writes return in ~50 ms instead of being pegged by an inline
156
+ * embed + rerank cycle (~200-500 ms/write under load).
157
+ */
158
+ constructor(store: EngramStore, connectionEngine?: ConnectionEngine) {
119
159
  this.store = store;
160
+ this.connectionEngine = connectionEngine ?? null;
120
161
  }
121
162
 
122
163
  /**
123
164
  * Run a full sleep cycle for an agent.
124
165
  *
166
+ * Phase 0: Connection drain — discover associations for engrams enqueued
167
+ * by writes since the last consolidation (v0.8.2)
125
168
  * Phase 1: Replay — find clusters of semantically similar memories
126
169
  * Phase 2: Strengthen — reinforce edges within clusters (access-weighted)
127
170
  * Phase 3: Bridge — create cross-cluster shortcuts
128
171
  * Phase 4: Decay — weaken unused edges, prune dead ones
129
172
  * Phase 5: Homeostasis — normalize outgoing edge weights per node
173
+ * Phase 5.5: Content fade — coarsen un-recalled engrams (Paper 1)
130
174
  * Phase 6: Forget — archive/delete memories never retrieved (age-gated)
175
+ * Phase 6.5: Redundancy prune
131
176
  * Phase 6.7: Confidence drift — adjust confidence based on structural signals
132
177
  * Phase 7: Sweep — check staging buffer for resonance
133
178
  */
134
179
  async consolidate(agentId: string): Promise<ConsolidationResult> {
180
+ // --- Phase 0: Connection drain ---
181
+ // Discover associations for engrams enqueued by writes since the last
182
+ // consolidation. Per-write inline discovery was removed in v0.8.2
183
+ // because each findConnections() call runs a full activation cycle
184
+ // (embed + rerank, ~200-500 ms event-loop block per write).
185
+ if (this.connectionEngine) {
186
+ try { await this.connectionEngine.processQueue(); } catch { /* best-effort */ }
187
+ }
135
188
  const result: ConsolidationResult = {
136
189
  clustersFound: 0,
137
190
  edgesStrengthened: 0,
@@ -142,6 +195,7 @@ export class ConsolidationEngine {
142
195
  edgesNormalized: 0,
143
196
  memoriesForgotten: 0,
144
197
  memoriesArchived: 0,
198
+ memoriesFaded: 0,
145
199
  redundancyPruned: 0,
146
200
  confidenceAdjusted: 0,
147
201
  stagingPromoted: 0,
@@ -152,7 +206,7 @@ export class ConsolidationEngine {
152
206
 
153
207
  // --- Phase 1: Replay ---
154
208
  // Get all active engrams, backfill missing embeddings
155
- const allActive = this.store.getEngramsByAgent(agentId, 'active');
209
+ const allActive = await this.store.getEngramsByAgent(agentId, 'active');
156
210
  const needsEmbedding = allActive.filter(e => !e.embedding || e.embedding.length === 0);
157
211
  if (needsEmbedding.length > 0) {
158
212
  try {
@@ -165,7 +219,7 @@ export class ConsolidationEngine {
165
219
  const texts = batch.map(e => `${e.concept} ${e.content}`);
166
220
  const vecs = await embedBatch(texts);
167
221
  for (let j = 0; j < batch.length; j++) {
168
- this.store.updateEmbedding(batch[j].id, vecs[j], modelId);
222
+ await this.store.updateEmbedding(batch[j].id, vecs[j], modelId);
169
223
  batch[j].embedding = vecs[j];
170
224
  }
171
225
  } catch { /* batch failed, skip — non-fatal */ }
@@ -178,10 +232,10 @@ export class ConsolidationEngine {
178
232
  const engrams = allActive.filter(e => e.embedding && e.embedding.length > 0);
179
233
 
180
234
  result.engramsProcessed = engrams.length;
181
- if (engrams.length < 2) return result;
182
-
183
- // Find clusters of related memories
184
- const clusters = this.findClusters(engrams);
235
+ // Clustering requires at least two engrams to form pairs. Per-engram phases
236
+ // (fade, forget, confidence drift, staging sweep) still run for singletons —
237
+ // they don't need a graph.
238
+ const clusters = engrams.length >= 2 ? this.findClusters(engrams) : [];
185
239
  result.clustersFound = clusters.length;
186
240
 
187
241
  // --- Phase 2: Strengthen (access-weighted) ---
@@ -200,17 +254,17 @@ export class ConsolidationEngine {
200
254
  0.3 + 0.7 * Math.log1p(a.accessCount + b.accessCount) / Math.log1p(20),
201
255
  );
202
256
 
203
- const existing = this.store.getAssociation(a.id, b.id);
257
+ const existing = await this.store.getAssociation(a.id, b.id);
204
258
  if (existing) {
205
259
  const newWeight = strengthenAssociation(
206
260
  existing.weight, CONSOLIDATION_SIGNAL * accessFactor, 0.25,
207
261
  );
208
- this.store.upsertAssociation(
262
+ await this.store.upsertAssociation(
209
263
  a.id, b.id, newWeight, existing.type, existing.confidence,
210
264
  );
211
265
  result.edgesStrengthened++;
212
266
  } else if (newEdges < MAX_NEW_EDGES_PER_CYCLE) {
213
- this.store.upsertAssociation(
267
+ await this.store.upsertAssociation(
214
268
  a.id, b.id, INITIAL_EDGE_WEIGHT * accessFactor, 'connection',
215
269
  );
216
270
  newEdges++;
@@ -283,7 +337,7 @@ export class ConsolidationEngine {
283
337
  `Discussed: ${keyTerms.slice(8).join(', ')}.`,
284
338
  ].join(' ');
285
339
 
286
- const synthEngram = this.store.createEngram({
340
+ const synthEngram = await this.store.createEngram({
287
341
  agentId,
288
342
  concept: `session: ${tag} (${keyTerms.slice(0, 3).join(', ')})`,
289
343
  content: synthContent,
@@ -296,12 +350,12 @@ export class ConsolidationEngine {
296
350
  // Set embedding to group centroid
297
351
  const centroid = this.computeCentroid(group);
298
352
  if (centroid.length > 0) {
299
- this.store.updateEmbedding(synthEngram.id, centroid);
353
+ await this.store.updateEmbedding(synthEngram.id, centroid);
300
354
  }
301
355
 
302
356
  // Link to sources
303
357
  for (const source of group.slice(0, 10)) { // Cap links to prevent explosion
304
- this.store.upsertAssociation(synthEngram.id, source.id, 0.4, 'causal');
358
+ await this.store.upsertAssociation(synthEngram.id, source.id, 0.4, 'causal');
305
359
  }
306
360
 
307
361
  synthCount++;
@@ -344,7 +398,7 @@ export class ConsolidationEngine {
344
398
  `Common themes: ${keyTerms.join(', ')}.`,
345
399
  ].join(' ');
346
400
 
347
- const synthEngram = this.store.createEngram({
401
+ const synthEngram = await this.store.createEngram({
348
402
  agentId,
349
403
  concept: `pattern: ${keyTerms.slice(0, 3).join(', ')}`,
350
404
  content: synthContent,
@@ -356,11 +410,11 @@ export class ConsolidationEngine {
356
410
 
357
411
  const centroid = this.computeCentroid(cluster);
358
412
  if (centroid.length > 0) {
359
- this.store.updateEmbedding(synthEngram.id, centroid);
413
+ await this.store.updateEmbedding(synthEngram.id, centroid);
360
414
  }
361
415
 
362
416
  for (const source of cluster.slice(0, 8)) {
363
- this.store.upsertAssociation(synthEngram.id, source.id, 0.3, 'bridge');
417
+ await this.store.upsertAssociation(synthEngram.id, source.id, 0.3, 'bridge');
364
418
  }
365
419
 
366
420
  synthCount++;
@@ -386,10 +440,10 @@ export class ConsolidationEngine {
386
440
  }
387
441
  }
388
442
  if (bestA && bestB && bestSim > MIN_BRIDGE_SIM) {
389
- const existing = this.store.getAssociation(bestA.id, bestB.id);
443
+ const existing = await this.store.getAssociation(bestA.id, bestB.id);
390
444
  if (!existing) {
391
- this.store.upsertAssociation(bestA.id, bestB.id, bestSim, 'bridge');
392
- this.store.upsertAssociation(bestB.id, bestA.id, bestSim, 'bridge');
445
+ await this.store.upsertAssociation(bestA.id, bestB.id, bestSim, 'bridge');
446
+ await this.store.upsertAssociation(bestB.id, bestA.id, bestSim, 'bridge');
393
447
  bridges++;
394
448
  result.bridgesCreated++;
395
449
  }
@@ -404,7 +458,7 @@ export class ConsolidationEngine {
404
458
  // practiced memories are more resistant to forgetting in the brain.
405
459
  // Base half-life: 7 days. High-confidence (0.8+) gets up to 30 days.
406
460
  const engramMap = new Map(engrams.map(e => [e.id, e]));
407
- const associations = this.store.getAllAssociations(agentId);
461
+ const associations = await this.store.getAllAssociations(agentId);
408
462
  for (const assoc of associations) {
409
463
  const daysSince =
410
464
  (Date.now() - assoc.lastActivated.getTime()) / (1000 * 60 * 60 * 24);
@@ -424,10 +478,10 @@ export class ConsolidationEngine {
424
478
 
425
479
  const newWeight = decayAssociation(assoc.weight, daysSince, halfLifeDays);
426
480
  if (newWeight < PRUNE_THRESHOLD) {
427
- this.store.deleteAssociation(assoc.id);
481
+ await this.store.deleteAssociation(assoc.id);
428
482
  result.edgesPruned++;
429
483
  } else if (Math.abs(newWeight - assoc.weight) > 0.001) {
430
- this.store.upsertAssociation(
484
+ await this.store.upsertAssociation(
431
485
  assoc.fromEngramId, assoc.toEngramId,
432
486
  newWeight, assoc.type, assoc.confidence,
433
487
  );
@@ -440,17 +494,17 @@ export class ConsolidationEngine {
440
494
  // Nodes with many strong edges get scaled down so relative weights stay meaningful.
441
495
  const engramIds = new Set(engrams.map(e => e.id));
442
496
  for (const id of engramIds) {
443
- const outgoing = this.store.getOutgoingAssociations(id);
497
+ const outgoing = await this.store.getOutgoingAssociations(id);
444
498
  const totalWeight = outgoing.reduce((sum, a) => sum + a.weight, 0);
445
499
  if (totalWeight > HOMEOSTASIS_TARGET) {
446
500
  const scale = HOMEOSTASIS_TARGET / totalWeight;
447
501
  for (const edge of outgoing) {
448
502
  const newWeight = edge.weight * scale;
449
503
  if (newWeight < PRUNE_THRESHOLD) {
450
- this.store.deleteAssociation(edge.id);
504
+ await this.store.deleteAssociation(edge.id);
451
505
  result.edgesPruned++;
452
506
  } else {
453
- this.store.upsertAssociation(
507
+ await this.store.upsertAssociation(
454
508
  edge.fromEngramId, edge.toEngramId,
455
509
  newWeight, edge.type, edge.confidence,
456
510
  );
@@ -460,6 +514,37 @@ export class ConsolidationEngine {
460
514
  }
461
515
  }
462
516
 
517
+ // --- Phase 5.5: Content fade (Paper 1: storage degradation) ---
518
+ // Coarsens engrams that have been accessed before but have gone stale.
519
+ // Trims content surface area while preserving concept, tags, and embedding so
520
+ // the engram still participates in BM25 + vector recall — just with less
521
+ // body to score against. This models how human memory loses surface detail
522
+ // while retaining cue-association pathways.
523
+ //
524
+ // Fade applies only to accessed engrams (the never-accessed ones go straight
525
+ // to archive via Phase 6). Heavily-used (accessCount >= FADE_PROTECTED_ACCESS_COUNT),
526
+ // canonical, structural, and short-content engrams are excluded.
527
+ let fadeCount = 0;
528
+ for (const engram of engrams) {
529
+ if (fadeCount >= FADE_MAX_PER_CYCLE) break;
530
+ if (engram.accessCount === 0) continue;
531
+ if (engram.accessCount >= FADE_PROTECTED_ACCESS_COUNT) continue;
532
+ if (FADE_PROTECTED_CLASSES.has(engram.memoryClass)) continue;
533
+ if (engram.retracted) continue;
534
+ if (engram.content.length < FADE_MIN_CONTENT_LEN) continue;
535
+ if (engram.content.endsWith(FADE_MARKER)) continue; // Already faded
536
+ const daysSinceAccess = (Date.now() - engram.lastAccessed.getTime()) / (1000 * 60 * 60 * 24);
537
+ if (daysSinceAccess < FADE_DAYS_SINCE_ACCESS) continue;
538
+
539
+ const trimmed = engram.content.slice(0, FADE_KEEP_CHARS).trimEnd() + FADE_MARKER;
540
+ await this.store.updateContent(engram.id, trimmed);
541
+ await this.store.updateStage(engram.id, 'fading');
542
+ engram.content = trimmed;
543
+ engram.stage = 'fading';
544
+ result.memoriesFaded++;
545
+ fadeCount++;
546
+ }
547
+
463
548
  // --- Phase 6: Forgetting (age-gated) ---
464
549
  // Models how human memory actually works:
465
550
  // - New memories get a grace period (too new to judge)
@@ -474,19 +559,27 @@ export class ConsolidationEngine {
474
559
  // Compute edge count percentile for relative protection threshold.
475
560
  // With avg 12 edges/node, an absolute threshold of 3 protects everything.
476
561
  // Use 25th percentile so "weakly connected" is relative to actual graph density.
477
- const edgeCounts = engrams.map(e => this.store.countAssociationsFor(e.id));
562
+ const edgeCounts = await Promise.all(engrams.map(async e => await this.store.countAssociationsFor(e.id)));
478
563
  edgeCounts.sort((a, b) => a - b);
479
564
  const percentileIdx = Math.floor(edgeCounts.length * EDGE_PROTECTION_PERCENTILE);
480
565
  const baseEdgeThreshold = edgeCounts.length > 0 ? edgeCounts[percentileIdx] : 3;
481
566
 
482
567
  // Get consolidation cycle count for cycle-based archiving
483
- const cycleCount = this.store.getConsolidationCycleCount(agentId);
568
+ const cycleCount = await this.store.getConsolidationCycleCount(agentId);
484
569
 
485
570
  for (const engram of engrams) {
571
+ // Skip engrams that just transitioned to 'fading' in Phase 5.5 this cycle.
572
+ // Fade is intended as an intermediate stage between active and archived —
573
+ // the engram should live in 'fading' for at least one more recall window
574
+ // before being eligible for archival. Without this guard, a same-cycle
575
+ // fade-then-archive sequence collapses the active pool to zero (observed
576
+ // in the stress test scale phase around cycle 90).
577
+ if (engram.stage !== 'active') continue;
578
+
486
579
  const ageDays = (Date.now() - engram.createdAt.getTime()) / (1000 * 60 * 60 * 24);
487
580
  if (ageDays < FORGET_GRACE_DAYS) continue; // Grace period — too new to judge
488
581
 
489
- const edgeCount = this.store.countAssociationsFor(engram.id);
582
+ const edgeCount = await this.store.countAssociationsFor(engram.id);
490
583
 
491
584
  // Use relative threshold (percentile-based) instead of absolute.
492
585
  // High-confidence memories need fewer edges to survive.
@@ -499,7 +592,7 @@ export class ConsolidationEngine {
499
592
  // Cycle-based archive: 0-access memories archived after N cycles
500
593
  // regardless of age. Handles small pools where time thresholds are too generous.
501
594
  if (engram.accessCount === 0 && cycleCount >= FORGET_CYCLE_THRESHOLD) {
502
- this.store.updateStage(engram.id, 'archived');
595
+ await this.store.updateStage(engram.id, 'archived');
503
596
  result.memoriesArchived++;
504
597
  continue;
505
598
  }
@@ -522,24 +615,24 @@ export class ConsolidationEngine {
522
615
 
523
616
  if (engram.accessCount === 0 && ageDays > FORGET_ARCHIVE_DAYS) {
524
617
  // Never retrieved, old, weakly connected → archive
525
- this.store.updateStage(engram.id, 'archived');
618
+ await this.store.updateStage(engram.id, 'archived');
526
619
  result.memoriesArchived++;
527
620
  } else if (engram.accessCount > 0 && daysSinceAccess > effectiveArchiveDays) {
528
621
  // Accessed before but not recently enough given its strength — archive
529
- this.store.updateStage(engram.id, 'archived');
622
+ await this.store.updateStage(engram.id, 'archived');
530
623
  result.memoriesArchived++;
531
624
  }
532
625
  }
533
626
 
534
627
  // Check archived memories for deletion — only truly orphaned ancient ones
535
- const archived = this.store.getEngramsByAgent(agentId, 'archived');
628
+ const archived = await this.store.getEngramsByAgent(agentId, 'archived');
536
629
  for (const engram of archived) {
537
630
  const ageDays = (Date.now() - engram.createdAt.getTime()) / (1000 * 60 * 60 * 24);
538
- const edgeCount = this.store.countAssociationsFor(engram.id);
631
+ const edgeCount = await this.store.countAssociationsFor(engram.id);
539
632
 
540
633
  if (engram.accessCount === 0 && ageDays > FORGET_DELETE_DAYS && edgeCount === 0) {
541
634
  // Very old, never accessed, completely isolated → truly forgotten
542
- this.store.deleteEngram(engram.id);
635
+ await this.store.deleteEngram(engram.id);
543
636
  result.memoriesForgotten++;
544
637
  }
545
638
  // Otherwise: stay archived — still searchable, just not in active recall
@@ -577,11 +670,11 @@ export class ConsolidationEngine {
577
670
  const prunedId = sortedLow[j].id;
578
671
 
579
672
  // Transfer associations from pruned memory to survivor
580
- const prunedEdges = this.store.getAssociationsFor(prunedId);
673
+ const prunedEdges = await this.store.getAssociationsFor(prunedId);
581
674
  for (const edge of prunedEdges) {
582
675
  const peerId = edge.fromEngramId === prunedId ? edge.toEngramId : edge.fromEngramId;
583
676
  if (peerId === survivorId) continue; // Skip self-loops
584
- this.store.upsertAssociation(survivorId, peerId, edge.weight, edge.type, edge.confidence);
677
+ await this.store.upsertAssociation(survivorId, peerId, edge.weight, edge.type, edge.confidence);
585
678
  }
586
679
 
587
680
  // Merge tags from pruned to survivor
@@ -589,11 +682,11 @@ export class ConsolidationEngine {
589
682
  const prunedMem = sortedLow[j];
590
683
  const mergedTags = [...new Set([...survivor.tags, ...prunedMem.tags])];
591
684
  if (mergedTags.length > survivor.tags.length) {
592
- this.store.updateTags(survivorId, mergedTags);
685
+ await this.store.updateTags(survivorId, mergedTags);
593
686
  }
594
687
 
595
688
  // Archive the lower-quality duplicate
596
- this.store.updateStage(prunedId, 'archived');
689
+ await this.store.updateStage(prunedId, 'archived');
597
690
  pruned.add(prunedId);
598
691
  redundancyCount++;
599
692
  }
@@ -623,7 +716,7 @@ export class ConsolidationEngine {
623
716
 
624
717
  for (const engram of engrams) {
625
718
  let drift = 0;
626
- const edgeCount = this.store.countAssociationsFor(engram.id);
719
+ const edgeCount = await this.store.countAssociationsFor(engram.id);
627
720
  const daysSinceAccess = (Date.now() - engram.lastAccessed.getTime()) / (1000 * 60 * 60 * 24);
628
721
 
629
722
  // Signal 1: Cluster membership → small boost
@@ -646,14 +739,14 @@ export class ConsolidationEngine {
646
739
  drift = Math.max(-CONFIDENCE_DRIFT_CAP, Math.min(CONFIDENCE_DRIFT_CAP, drift));
647
740
  const newConf = Math.max(0.15, Math.min(0.85, engram.confidence + drift));
648
741
  if (Math.abs(newConf - engram.confidence) > 0.001) {
649
- this.store.updateConfidence(engram.id, newConf);
742
+ await this.store.updateConfidence(engram.id, newConf);
650
743
  result.confidenceAdjusted++;
651
744
  }
652
745
  }
653
746
  }
654
747
 
655
748
  // --- Phase 7: Sweep staging ---
656
- const staging = this.store.getEngramsByAgent(agentId, 'staging')
749
+ const staging = (await this.store.getEngramsByAgent(agentId, 'staging'))
657
750
  .filter(e => e.embedding && e.embedding.length > 0);
658
751
 
659
752
  for (const staged of staging) {
@@ -669,12 +762,12 @@ export class ConsolidationEngine {
669
762
 
670
763
  if (maxSim >= 0.6) {
671
764
  // Resonates — promote to active with low confidence (barely made it)
672
- this.store.updateStage(staged.id, 'active');
673
- this.store.updateConfidence(staged.id, 0.40);
765
+ await this.store.updateStage(staged.id, 'active');
766
+ await this.store.updateConfidence(staged.id, 0.40);
674
767
  result.stagingPromoted++;
675
768
  } else if (ageMs > 24 * 60 * 60 * 1000) {
676
769
  // Over 24h and no resonance — discard
677
- this.store.deleteEngram(staged.id);
770
+ await this.store.deleteEngram(staged.id);
678
771
  result.stagingDiscarded++;
679
772
  }
680
773
  // Otherwise: leave in staging, maybe next cycle
@@ -1,102 +1,102 @@
1
- // Copyright 2026 Robert Winter / Complete Ideas
2
- // SPDX-License-Identifier: Apache-2.0
3
- /**
4
- * Evaluation Engine — measures whether memory actually helps.
5
- *
6
- * Four dimensions (from Codex):
7
- * 1. Retrieval quality — precision@k, latency
8
- * 2. Connection quality — edge utility, stability
9
- * 3. Staging accuracy — promotion precision, discard regret
10
- * 4. Memory health — contamination tracking, confidence distribution
11
- *
12
- * Task impact (with/without memory) is measured externally via TaskTrial records.
13
- */
14
-
15
- import type { EngramStore } from '../storage/sqlite.js';
16
- import type { EvalMetrics } from '../types/index.js';
17
-
18
- export class EvalEngine {
19
- private store: EngramStore;
20
-
21
- constructor(store: EngramStore) {
22
- this.store = store;
23
- }
24
-
25
- /**
26
- * Compute aggregate metrics for an agent over a time window.
27
- */
28
- computeMetrics(agentId: string, windowHours: number = 24): EvalMetrics {
29
- const window = windowHours <= 24 ? '24h' : `${Math.round(windowHours / 24)}d`;
30
-
31
- // Retrieval quality
32
- const precision = this.store.getRetrievalPrecision(agentId, windowHours);
33
-
34
- // Staging accuracy
35
- const stagingMetrics = this.store.getStagingMetrics(agentId);
36
- const totalStaged = stagingMetrics.promoted + stagingMetrics.discarded + stagingMetrics.expired;
37
- const promotionPrecision = totalStaged > 0 ? stagingMetrics.promoted / totalStaged : 0;
38
-
39
- // Memory health
40
- const activeEngrams = this.store.getEngramsByAgent(agentId, 'active');
41
- const stagingEngrams = this.store.getEngramsByAgent(agentId, 'staging');
42
- const retractedEngrams = this.store.getEngramsByAgent(agentId, undefined, true)
43
- .filter(e => e.retracted);
44
- const allAssociations = this.store.getAllAssociations(agentId);
45
-
46
- const avgConfidence = activeEngrams.length > 0
47
- ? activeEngrams.reduce((sum, e) => sum + e.confidence, 0) / activeEngrams.length
48
- : 0;
49
-
50
- // Edge utility — % of edges that have been used in activation
51
- const usedEdges = allAssociations.filter(a => a.activationCount > 0);
52
- const edgeUtility = allAssociations.length > 0
53
- ? usedEdges.length / allAssociations.length
54
- : 0;
55
-
56
- // Edge survival — average age of edges that are still above minimum weight
57
- const livingEdges = allAssociations.filter(a => a.weight > 0.01);
58
- const avgSurvival = livingEdges.length > 0
59
- ? livingEdges.reduce((sum, a) =>
60
- sum + (Date.now() - a.createdAt.getTime()) / (1000 * 60 * 60 * 24), 0
61
- ) / livingEdges.length
62
- : 0;
63
-
64
- // Activation performance stats
65
- const activationStats = this.store.getActivationStats(agentId, windowHours);
66
-
67
- // Consolidated count
68
- const consolidatedCount = this.store.getConsolidatedCount(agentId);
69
-
70
- return {
71
- agentId,
72
- timestamp: new Date(),
73
- window,
74
-
75
- activationCount: activationStats.count,
76
- avgPrecisionAtK: precision,
77
- avgLatencyMs: activationStats.avgLatencyMs,
78
- p95LatencyMs: activationStats.p95LatencyMs,
79
-
80
- totalEdges: allAssociations.length,
81
- edgesUsedInActivation: usedEdges.length,
82
- edgeUtilityRate: edgeUtility,
83
- avgEdgeSurvivalDays: avgSurvival,
84
-
85
- totalStaged: totalStaged,
86
- promotedCount: stagingMetrics.promoted,
87
- discardedCount: stagingMetrics.discarded,
88
- promotionPrecision,
89
- discardRegret: 0, // Requires tracking discarded-then-rediscovered items
90
-
91
- activeEngramCount: activeEngrams.length,
92
- stagingEngramCount: stagingEngrams.length,
93
- retractedCount: retractedEngrams.length,
94
- consolidatedCount,
95
- avgConfidence,
96
-
97
- staleUsageCount: 0, // Requires per-activation age/confidence tracking
98
- retractionRate: retractedEngrams.length /
99
- Math.max(activeEngrams.length + retractedEngrams.length, 1),
100
- };
101
- }
102
- }
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Evaluation Engine — measures whether memory actually helps.
5
+ *
6
+ * Four dimensions (from Codex):
7
+ * 1. Retrieval quality — precision@k, latency
8
+ * 2. Connection quality — edge utility, stability
9
+ * 3. Staging accuracy — promotion precision, discard regret
10
+ * 4. Memory health — contamination tracking, confidence distribution
11
+ *
12
+ * Task impact (with/without memory) is measured externally via TaskTrial records.
13
+ */
14
+
15
+ import type { IEngramStore as EngramStore } from '../storage/store.js';
16
+ import type { EvalMetrics } from '../types/index.js';
17
+
18
+ export class EvalEngine {
19
+ private store: EngramStore;
20
+
21
+ constructor(store: EngramStore) {
22
+ this.store = store;
23
+ }
24
+
25
+ /**
26
+ * Compute aggregate metrics for an agent over a time window.
27
+ */
28
+ async computeMetrics(agentId: string, windowHours: number = 24): Promise<EvalMetrics> {
29
+ const window = windowHours <= 24 ? '24h' : `${Math.round(windowHours / 24)}d`;
30
+
31
+ // Retrieval quality
32
+ const precision = await this.store.getRetrievalPrecision(agentId, windowHours);
33
+
34
+ // Staging accuracy
35
+ const stagingMetrics = await this.store.getStagingMetrics(agentId);
36
+ const totalStaged = stagingMetrics.promoted + stagingMetrics.discarded + stagingMetrics.expired;
37
+ const promotionPrecision = totalStaged > 0 ? stagingMetrics.promoted / totalStaged : 0;
38
+
39
+ // Memory health
40
+ const activeEngrams = await this.store.getEngramsByAgent(agentId, 'active');
41
+ const stagingEngrams = await this.store.getEngramsByAgent(agentId, 'staging');
42
+ const retractedEngrams = (await this.store.getEngramsByAgent(agentId, undefined, true))
43
+ .filter(e => e.retracted);
44
+ const allAssociations = await this.store.getAllAssociations(agentId);
45
+
46
+ const avgConfidence = activeEngrams.length > 0
47
+ ? activeEngrams.reduce((sum, e) => sum + e.confidence, 0) / activeEngrams.length
48
+ : 0;
49
+
50
+ // Edge utility — % of edges that have been used in activation
51
+ const usedEdges = allAssociations.filter(a => a.activationCount > 0);
52
+ const edgeUtility = allAssociations.length > 0
53
+ ? usedEdges.length / allAssociations.length
54
+ : 0;
55
+
56
+ // Edge survival — average age of edges that are still above minimum weight
57
+ const livingEdges = allAssociations.filter(a => a.weight > 0.01);
58
+ const avgSurvival = livingEdges.length > 0
59
+ ? livingEdges.reduce((sum, a) =>
60
+ sum + (Date.now() - a.createdAt.getTime()) / (1000 * 60 * 60 * 24), 0
61
+ ) / livingEdges.length
62
+ : 0;
63
+
64
+ // Activation performance stats
65
+ const activationStats = await this.store.getActivationStats(agentId, windowHours);
66
+
67
+ // Consolidated count
68
+ const consolidatedCount = await this.store.getConsolidatedCount(agentId);
69
+
70
+ return {
71
+ agentId,
72
+ timestamp: new Date(),
73
+ window,
74
+
75
+ activationCount: activationStats.count,
76
+ avgPrecisionAtK: precision,
77
+ avgLatencyMs: activationStats.avgLatencyMs,
78
+ p95LatencyMs: activationStats.p95LatencyMs,
79
+
80
+ totalEdges: allAssociations.length,
81
+ edgesUsedInActivation: usedEdges.length,
82
+ edgeUtilityRate: edgeUtility,
83
+ avgEdgeSurvivalDays: avgSurvival,
84
+
85
+ totalStaged: totalStaged,
86
+ promotedCount: stagingMetrics.promoted,
87
+ discardedCount: stagingMetrics.discarded,
88
+ promotionPrecision,
89
+ discardRegret: 0, // Requires tracking discarded-then-rediscovered items
90
+
91
+ activeEngramCount: activeEngrams.length,
92
+ stagingEngramCount: stagingEngrams.length,
93
+ retractedCount: retractedEngrams.length,
94
+ consolidatedCount,
95
+ avgConfidence,
96
+
97
+ staleUsageCount: 0, // Requires per-activation age/confidence tracking
98
+ retractionRate: retractedEngrams.length /
99
+ Math.max(activeEngrams.length + retractedEngrams.length, 1),
100
+ };
101
+ }
102
+ }