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
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/api/routes.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACtE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAQnF,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,CAAC;IACnB,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,cAAc,EAAE,cAAc,CAAC;IAC/B,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,UAAU,EAAE,UAAU,CAAC;IACvB,mBAAmB,EAAE,mBAAmB,CAAC;IACzC,sBAAsB,EAAE,sBAAsB,CAAC;CAChD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI,CAopB3E"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/api/routes.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,IAAI,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACtE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AASnF,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,CAAC;IACnB,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,cAAc,EAAE,cAAc,CAAC;IAC/B,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,UAAU,EAAE,UAAU,CAAC;IACvB,mBAAmB,EAAE,mBAAmB,CAAC;IACzC,sBAAsB,EAAE,sBAAsB,CAAC;CAChD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI,CA04B3E"}
@@ -32,7 +32,7 @@
32
32
  * POST /system/consolidate — run sleep cycle (strengthen, decay, sweep)
33
33
  * GET /health — health check
34
34
  */
35
- import { evaluateSalience, computeNovelty } from '../core/salience.js';
35
+ import { performWrite } from '../core/write-pipeline.js';
36
36
  import { DEFAULT_AGENT_CONFIG } from '../types/agent.js';
37
37
  import { embed, embedBatch } from '../core/embeddings.js';
38
38
  export function registerRoutes(app, deps) {
@@ -47,21 +47,6 @@ export function registerRoutes(app, deps) {
47
47
  !body.content || typeof body.content !== 'string') {
48
48
  return reply.status(400).send({ error: 'agentId, concept, and content are required strings' });
49
49
  }
50
- const novelty = computeNovelty(store, body.agentId, body.concept, body.content);
51
- const salience = evaluateSalience({
52
- content: body.content,
53
- eventType: body.eventType,
54
- surprise: body.surprise,
55
- decisionMade: body.decisionMade,
56
- causalDepth: body.causalDepth,
57
- resolutionEffort: body.resolutionEffort,
58
- novelty,
59
- });
60
- // v0.5.4: No longer discard — store with low confidence for ranking.
61
- const isLowSalience = salience.disposition === 'discard';
62
- const confidence = isLowSalience
63
- ? 0.25
64
- : body.confidence ?? (salience.disposition === 'staging' ? 0.40 : 0.50);
65
50
  // Assemble tags: user-provided + agent metadata
66
51
  const userTags = body.tags ?? [];
67
52
  const metaTags = [];
@@ -77,59 +62,90 @@ export function registerRoutes(app, deps) {
77
62
  metaTags.push(`sid=${body.sessionId}`);
78
63
  if (body.intent)
79
64
  metaTags.push(`intent=${body.intent}`);
80
- const allTags = isLowSalience
81
- ? [...userTags, ...metaTags, 'low-salience']
82
- : [...userTags, ...metaTags];
83
- const engram = store.createEngram({
65
+ // Resolve references (0.8 Cluster D): if matchConcept is given without
66
+ // matchEngramId, look up the most recent active engram with that concept
67
+ // (+ optional matchTags) and store both. Stable link survives concept
68
+ // edits. No match found → store just matchConcept so the intent
69
+ // (link-to-future or link-to-deleted) is preserved.
70
+ const resolvedRefs = await Promise.all((body.references ?? []).map(async (ref) => {
71
+ if (!ref.matchEngramId && ref.matchConcept) {
72
+ const matched = await store.findActiveMatchByConcept(body.agentId, ref.matchConcept, ref.matchTags);
73
+ if (matched) {
74
+ return { type: ref.type, matchEngramId: matched.id, matchConcept: ref.matchConcept };
75
+ }
76
+ }
77
+ return { type: ref.type, matchEngramId: ref.matchEngramId, matchConcept: ref.matchConcept };
78
+ }));
79
+ const result = await performWrite({ store, connectionEngine }, {
84
80
  agentId: body.agentId,
85
81
  concept: body.concept,
86
82
  content: body.content,
87
- tags: allTags,
88
- salience: salience.score,
89
- confidence,
90
- salienceFeatures: salience.features,
91
- reasonCodes: salience.reasonCodes,
92
- ttl: salience.disposition === 'staging' ? DEFAULT_AGENT_CONFIG.stagingTtlMs : undefined,
83
+ tags: [...userTags, ...metaTags],
84
+ memoryClass: body.memory_class,
85
+ eventType: body.eventType,
86
+ surprise: body.surprise,
87
+ decisionMade: body.decisionMade,
88
+ causalDepth: body.causalDepth,
89
+ resolutionEffort: body.resolutionEffort,
90
+ confidence: body.confidence,
91
+ sequence: body.sequence,
92
+ embed: body.embed,
93
+ references: resolvedRefs.length > 0 ? resolvedRefs : undefined,
93
94
  });
94
- if (salience.disposition === 'staging') {
95
- store.updateStage(engram.id, 'staging');
96
- }
97
- // Create temporal adjacency edge to previous memory (conversation thread graph)
98
- // This enables multi-hop graph walk through conversation sequences
95
+ // Auto-checkpoint always (covers create, reinforce, and supersede).
99
96
  try {
100
- const prev = store.getLatestEngram(body.agentId, engram.id);
101
- if (prev) {
102
- store.upsertAssociation(prev.id, engram.id, 0.3, 'temporal', 0.8);
103
- }
97
+ await store.updateAutoCheckpointWrite(body.agentId, result.engram.id);
98
+ }
99
+ catch { /* non-fatal */ }
100
+ if (result.action === 'reinforce') {
101
+ return reply.code(200).send({
102
+ stored: false,
103
+ action: 'reinforce',
104
+ disposition: 'reinforced',
105
+ engram: result.engram,
106
+ reinforce: result.reinforce,
107
+ novelty: result.noveltyResult.novelty,
108
+ });
104
109
  }
105
- catch { /* Temporal edge creation is non-fatal */ }
106
- if (salience.disposition === 'active' || isLowSalience) {
107
- connectionEngine.enqueue(engram.id);
108
- // Auto-assign to episode (1-hour window per agent)
110
+ // create / supersede paths follow legacy temporal-edge + episode logic.
111
+ // Structural-class writes (0.8 Cluster A) skip these: they're system-written
112
+ // event-log records, not conversational beats, so keeping them out of the
113
+ // temporal graph + episode index keeps cognitive retrieval clean.
114
+ const isStructural = body.memory_class === 'structural';
115
+ if (!isStructural) {
109
116
  try {
110
- let episode = store.getActiveEpisode(body.agentId, 3600_000);
111
- if (!episode) {
112
- episode = store.createEpisode({ agentId: body.agentId, label: body.concept });
117
+ const prev = await store.getLatestEngram(body.agentId, result.engram.id);
118
+ if (prev) {
119
+ await store.upsertAssociation(prev.id, result.engram.id, 0.3, 'temporal', 0.8);
113
120
  }
114
- store.addEngramToEpisode(engram.id, episode.id);
115
121
  }
116
- catch { /* Episode assignment is non-fatal */ }
117
- }
118
- // Generate embedding asynchronously (don't block response)
119
- embed(`${body.concept} ${body.content}`).then(vec => {
120
- store.updateEmbedding(engram.id, vec);
121
- }).catch(() => { }); // Embedding failure is non-fatal
122
- // Auto-checkpoint: track write for consolidation scheduling
123
- try {
124
- store.updateAutoCheckpointWrite(body.agentId, engram.id);
122
+ catch { /* Temporal edge creation is non-fatal */ }
123
+ if (result.salience
124
+ && (result.salience.disposition === 'active' || result.salience.disposition === 'discard')) {
125
+ try {
126
+ let episode = await store.getActiveEpisode(body.agentId, 3600_000);
127
+ if (!episode) {
128
+ episode = await store.createEpisode({ agentId: body.agentId, label: body.concept });
129
+ }
130
+ await store.addEngramToEpisode(result.engram.id, episode.id);
131
+ }
132
+ catch { /* Episode assignment is non-fatal */ }
133
+ }
125
134
  }
126
- catch { /* non-fatal */ }
135
+ const isLowSalience = result.salience?.disposition === 'discard';
127
136
  return reply.code(201).send({
128
137
  stored: true,
129
- disposition: isLowSalience ? 'low-salience' : salience.disposition,
130
- salience: salience.score,
131
- reasonCodes: salience.reasonCodes,
132
- engram,
138
+ action: result.action,
139
+ // disposition: legacy field — 'low-salience' is returned for discard so
140
+ // callers know the engram was kept but marked low-value. The raw inner
141
+ // salience decision is exposed as `salienceDisposition` for callers
142
+ // (and tests) that want the unmapped value.
143
+ disposition: isLowSalience ? 'low-salience' : (result.salience?.disposition ?? 'active'),
144
+ salienceDisposition: result.salience?.disposition ?? null,
145
+ salience: result.salience?.score ?? 0,
146
+ reasonCodes: result.salience?.reasonCodes ?? [],
147
+ engram: result.engram,
148
+ supersedeOf: result.supersedeOf,
133
149
  });
134
150
  });
135
151
  /**
@@ -149,7 +165,7 @@ export function registerRoutes(app, deps) {
149
165
  const memTags = [...(mem.tags ?? [])];
150
166
  if (sid)
151
167
  memTags.push(`sid=${sid}`);
152
- const engram = store.createEngram({
168
+ const engram = await store.createEngram({
153
169
  agentId: body.agentId,
154
170
  concept: mem.concept,
155
171
  content: mem.content,
@@ -160,9 +176,9 @@ export function registerRoutes(app, deps) {
160
176
  });
161
177
  // Handle supersession inline — archive superseded memory to remove from active pool
162
178
  if (mem.supersedes) {
163
- store.supersedeEngram(mem.supersedes, engram.id);
164
- store.updateConfidence(mem.supersedes, 0.1);
165
- store.updateStage(mem.supersedes, 'archived'); // Remove from active search pool
179
+ await store.supersedeEngram(mem.supersedes, engram.id);
180
+ await store.updateConfidence(mem.supersedes, 0.1);
181
+ await store.updateStage(mem.supersedes, 'archived'); // Remove from active search pool
166
182
  }
167
183
  results.push({ id: engram.id, concept: mem.concept, disposition: 'active' });
168
184
  }
@@ -172,13 +188,13 @@ export function registerRoutes(app, deps) {
172
188
  const vecs = await embedBatch(texts);
173
189
  for (let i = 0; i < vecs.length; i++) {
174
190
  if (results[i]) {
175
- store.updateEmbedding(results[i].id, vecs[i]);
191
+ await store.updateEmbedding(results[i].id, vecs[i]);
176
192
  }
177
193
  }
178
194
  }
179
195
  catch { /* Embedding failure is non-fatal */ }
180
196
  try {
181
- store.updateAutoCheckpointWrite(body.agentId, results[results.length - 1]?.id ?? '');
197
+ await store.updateAutoCheckpointWrite(body.agentId, results[results.length - 1]?.id ?? '');
182
198
  }
183
199
  catch { }
184
200
  return reply.code(201).send({
@@ -197,33 +213,39 @@ export function registerRoutes(app, deps) {
197
213
  useReranker: body.useReranker,
198
214
  useExpansion: body.useExpansion,
199
215
  abstentionThreshold: body.abstentionThreshold,
216
+ requireConfidence: body.requireConfidence,
200
217
  workspace: body.workspace,
201
218
  bm25Only: body.bm25Only,
219
+ granularity: body.granularity,
202
220
  });
203
221
  // Auto-checkpoint: track recall for consolidation scheduling
204
222
  try {
205
223
  const ids = results.map(r => r.engram.id);
206
- store.updateAutoCheckpointRecall(body.agentId, body.context, ids);
224
+ await store.updateAutoCheckpointRecall(body.agentId, body.context, ids);
207
225
  }
208
226
  catch { /* non-fatal */ }
209
- return reply.send({ results });
227
+ // Surface recall confidence as a top-level field too — same value is on
228
+ // every result, but it describes the recall as a whole, so exposing it
229
+ // once is easier for consumers (and lets them inspect 0-result recalls).
230
+ const confidence = results[0]?.confidence ?? 0;
231
+ return reply.send({ results, confidence });
210
232
  });
211
233
  app.post('/memory/feedback', async (req, reply) => {
212
234
  const body = req.body;
213
- store.logRetrievalFeedback(body.activationEventId ?? null, body.engramId, body.useful, body.context ?? '');
235
+ await store.logRetrievalFeedback(body.activationEventId ?? null, body.engramId, body.useful, body.context ?? '');
214
236
  // Update engram confidence based on feedback
215
- const engram = store.getEngram(body.engramId);
237
+ const engram = await store.getEngram(body.engramId);
216
238
  if (engram) {
217
239
  const config = DEFAULT_AGENT_CONFIG;
218
240
  const delta = body.useful
219
241
  ? config.feedbackPositiveBoost
220
242
  : -config.feedbackNegativePenalty;
221
- store.updateConfidence(engram.id, engram.confidence + delta);
243
+ await store.updateConfidence(engram.id, engram.confidence + delta);
222
244
  }
223
245
  // Touch activity for consolidation scheduling
224
246
  if (engram) {
225
247
  try {
226
- store.touchActivity(engram.agentId);
248
+ await store.touchActivity(engram.agentId);
227
249
  }
228
250
  catch { /* non-fatal */ }
229
251
  }
@@ -231,7 +253,7 @@ export function registerRoutes(app, deps) {
231
253
  });
232
254
  app.post('/memory/retract', async (req, reply) => {
233
255
  const body = req.body;
234
- const result = retractionEngine.retract({
256
+ const result = await retractionEngine.retract({
235
257
  agentId: body.agentId,
236
258
  targetEngramId: body.targetEngramId,
237
259
  reason: body.reason,
@@ -239,33 +261,83 @@ export function registerRoutes(app, deps) {
239
261
  });
240
262
  // Touch activity for consolidation scheduling
241
263
  try {
242
- store.touchActivity(body.agentId);
264
+ await store.touchActivity(body.agentId);
243
265
  }
244
266
  catch { /* non-fatal */ }
245
267
  return reply.send(result);
246
268
  });
247
269
  app.post('/memory/supersede', async (req, reply) => {
248
270
  const body = req.body;
249
- const oldEngram = store.getEngram(body.oldEngramId);
250
- const newEngram = store.getEngram(body.newEngramId);
251
- if (!oldEngram)
252
- return reply.code(404).send({ error: `Old engram ${body.oldEngramId} not found` });
253
- if (!newEngram)
254
- return reply.code(404).send({ error: `New engram ${body.newEngramId} not found` });
255
- // Create causal association (new → old)
256
- store.upsertAssociation(body.newEngramId, body.oldEngramId, 0.8, 'causal', 1.0);
257
- // Reduce old engram confidence to 20% (keep for historical reference)
258
- store.updateConfidence(body.oldEngramId, oldEngram.confidence * 0.2);
259
- // Mark supersession via store method
260
- store.supersedeEngram(body.oldEngramId, body.newEngramId);
271
+ const isFormA = !!(body.oldEngramId && body.newEngramId);
272
+ const isFormB = !!(body.matchConcept && body.newEngram && body.agentId);
273
+ if (isFormA && isFormB) {
274
+ return reply.code(400).send({
275
+ error: 'Pass either {oldEngramId, newEngramId} (Form A) OR ' +
276
+ '{agentId, matchConcept, newEngram} (Form B), not both.',
277
+ });
278
+ }
279
+ if (!isFormA && !isFormB) {
280
+ return reply.code(400).send({
281
+ error: 'Missing required fields. Form A: {oldEngramId, newEngramId}. ' +
282
+ 'Form B: {agentId, matchConcept, newEngram}.',
283
+ });
284
+ }
285
+ // ── Form A — by IDs ──
286
+ if (isFormA) {
287
+ const oldEngram = await store.getEngram(body.oldEngramId);
288
+ const newEngram = await store.getEngram(body.newEngramId);
289
+ if (!oldEngram)
290
+ return reply.code(404).send({ error: `Old engram ${body.oldEngramId} not found` });
291
+ if (!newEngram)
292
+ return reply.code(404).send({ error: `New engram ${body.newEngramId} not found` });
293
+ await store.upsertAssociation(body.newEngramId, body.oldEngramId, 0.8, 'causal', 1.0);
294
+ await store.updateConfidence(body.oldEngramId, oldEngram.confidence * 0.2);
295
+ await store.supersedeEngram(body.oldEngramId, body.newEngramId);
296
+ try {
297
+ await store.touchActivity(oldEngram.agentId);
298
+ }
299
+ catch { /* non-fatal */ }
300
+ return reply.send({
301
+ superseded: body.oldEngramId,
302
+ supersededBy: body.newEngramId,
303
+ reason: body.reason ?? 'outdated',
304
+ });
305
+ }
306
+ // ── Form B — atomic write-and-supersede by concept match ──
307
+ // Find old (most recent active match by concept + optional tags), write
308
+ // new engram via performWrite, link them — all in one SQL transaction.
309
+ // If no match: write new anyway, return { superseded: null }.
310
+ // AWM 0.8.x P4b follow-up: Form B atomicity via withTransaction.
311
+ // Holds the SQLite/PGlite lock across the async write + supersede pair
312
+ // so callers never observe a half-completed state.
313
+ const result = await store.withTransaction(async () => {
314
+ const matched = await store.findActiveMatchByConcept(body.agentId, body.matchConcept, body.matchTags);
315
+ const writeRes = await performWrite({ store, connectionEngine }, {
316
+ agentId: body.agentId,
317
+ concept: body.newEngram.concept,
318
+ content: body.newEngram.content,
319
+ tags: body.newEngram.tags ?? [],
320
+ memoryClass: body.newEngram.memory_class,
321
+ sequence: body.newEngram.sequence,
322
+ eventType: body.newEngram.eventType,
323
+ enableReinforcement: false,
324
+ });
325
+ if (matched) {
326
+ await store.upsertAssociation(writeRes.engram.id, matched.id, 0.8, 'causal', 1.0);
327
+ await store.updateConfidence(matched.id, matched.confidence * 0.2);
328
+ await store.supersedeEngram(matched.id, writeRes.engram.id);
329
+ }
330
+ return { writeRes, matched };
331
+ });
261
332
  try {
262
- store.touchActivity(oldEngram.agentId);
333
+ await store.touchActivity(body.agentId);
263
334
  }
264
335
  catch { /* non-fatal */ }
265
- return reply.send({
266
- superseded: body.oldEngramId,
267
- supersededBy: body.newEngramId,
268
- reason: body.reason ?? 'outdated',
336
+ return reply.code(201).send({
337
+ newEngram: result.writeRes.engram,
338
+ superseded: result.matched ? result.matched.id : null,
339
+ supersededBy: result.writeRes.engram.id,
340
+ reason: body.reason ?? 'resolved by concept match',
269
341
  });
270
342
  });
271
343
  // ============================================================
@@ -273,32 +345,122 @@ export function registerRoutes(app, deps) {
273
345
  // ============================================================
274
346
  app.post('/memory/search', async (req, reply) => {
275
347
  const body = req.body;
276
- const results = store.search({
348
+ const results = await store.search({
277
349
  agentId: body.agentId,
278
350
  text: body.text,
279
351
  concept: body.concept,
280
352
  tags: body.tags,
353
+ tagsAll: body.tagsAll,
354
+ tagsAny: body.tagsAny,
355
+ tagsNone: body.tagsNone,
281
356
  stage: body.stage,
282
357
  retracted: body.retracted,
283
358
  limit: body.limit,
284
359
  offset: body.offset,
360
+ sortBy: body.sortBy,
361
+ sortOrder: body.sortOrder,
285
362
  });
286
363
  return reply.send({ results, count: results.length });
287
364
  });
365
+ // ============================================================
366
+ // 0.8 Cluster C — materialized-view + atomic-counter endpoints
367
+ // ============================================================
368
+ /**
369
+ * For each distinct value of `tagKey`, return the most recent active
370
+ * engram. Used by NovelForge for "latest emotional state per character",
371
+ * "latest motif phase per motif", etc.
372
+ */
373
+ app.post('/memory/latest-by-tag', async (req, reply) => {
374
+ const body = req.body;
375
+ if (!body.agentId || !body.tagKey) {
376
+ return reply.code(400).send({ error: 'agentId and tagKey are required' });
377
+ }
378
+ const results = await store.getLatestByTag({
379
+ agentId: body.agentId,
380
+ tagKeyPrefix: body.tagKey,
381
+ scopeTagsAll: body.scopeTagsAll,
382
+ retracted: body.retracted ?? false,
383
+ sortBy: body.sortBy,
384
+ limit: body.limit,
385
+ });
386
+ return reply.send({ results, count: results.length });
387
+ });
388
+ /**
389
+ * Filter by tag-set operators, sort by numeric value extracted from a
390
+ * tag prefix, return top N. Used by NovelForge for "top N active
391
+ * promises by weight".
392
+ */
393
+ app.post('/memory/top-by', async (req, reply) => {
394
+ const body = req.body;
395
+ if (!body.agentId || !body.sortField) {
396
+ return reply.code(400).send({ error: 'agentId and sortField are required' });
397
+ }
398
+ const results = await store.getTopBy({
399
+ agentId: body.agentId,
400
+ sortField: body.sortField,
401
+ order: body.order ?? 'desc',
402
+ filterTagsAll: body.filterTagsAll,
403
+ filterTagsAny: body.filterTagsAny,
404
+ filterTagsNone: body.filterTagsNone,
405
+ retracted: body.retracted ?? false,
406
+ limit: body.limit,
407
+ });
408
+ return reply.send({ results, count: results.length });
409
+ });
410
+ /**
411
+ * Compute effective state of an engram from referenced events. Two
412
+ * targeting modes: by ID, or by concept match (same semantics as Form B's
413
+ * findActiveMatchByConcept).
414
+ */
415
+ app.post('/memory/resolve', async (req, reply) => {
416
+ const body = req.body;
417
+ if (!body.agentId) {
418
+ return reply.code(400).send({ error: 'agentId is required' });
419
+ }
420
+ let targetId = body.targetEngramId;
421
+ if (!targetId && body.matchConcept) {
422
+ const matched = await store.findActiveMatchByConcept(body.agentId, body.matchConcept, body.matchTags);
423
+ if (!matched) {
424
+ return reply.code(404).send({
425
+ error: `No active engram matches concept "${body.matchConcept}"`,
426
+ });
427
+ }
428
+ targetId = matched.id;
429
+ }
430
+ if (!targetId) {
431
+ return reply.code(400).send({
432
+ error: 'Provide either targetEngramId or matchConcept',
433
+ });
434
+ }
435
+ const result = await store.resolveEffectiveState(targetId);
436
+ if (!result)
437
+ return reply.code(404).send({ error: `Engram ${targetId} not found` });
438
+ return reply.send(result);
439
+ });
440
+ /**
441
+ * Race-free next-sequence allocator. Caller writes the engram with the
442
+ * returned value in `sequence`. Doesn't reserve — concurrent allocations
443
+ * always serialize via BEGIN IMMEDIATE.
444
+ */
445
+ app.get('/memory/sequence/:agentId/next', async (req, reply) => {
446
+ const { agentId } = req.params;
447
+ const next = await store.allocateNextSequence(agentId);
448
+ return reply.send({ agentId, next });
449
+ });
288
450
  app.get('/memory/:id', async (req, reply) => {
289
451
  const { id } = req.params;
290
- const engram = store.getEngram(id);
452
+ const engram = await store.getEngram(id);
291
453
  if (!engram)
292
454
  return reply.code(404).send({ error: 'Not found' });
293
- const associations = store.getAssociationsFor(id);
455
+ const associations = await store.getAssociationsFor(id);
294
456
  return reply.send({ engram, associations });
295
457
  });
296
458
  app.get('/agent/:id/stats', async (req, reply) => {
297
459
  const { id } = req.params;
298
- const active = store.getEngramsByAgent(id, 'active');
299
- const staging = store.getEngramsByAgent(id, 'staging');
300
- const retracted = store.getEngramsByAgent(id, undefined, true).filter(e => e.retracted);
301
- const associations = store.getAllAssociations(id);
460
+ const active = await store.getEngramsByAgent(id, 'active');
461
+ const staging = await store.getEngramsByAgent(id, 'staging');
462
+ const retracted = (await store.getEngramsByAgent(id, undefined, true)).filter(e => e.retracted);
463
+ const associations = await store.getAllAssociations(id);
302
464
  return reply.send({
303
465
  agentId: id,
304
466
  engrams: {
@@ -316,7 +478,7 @@ export function registerRoutes(app, deps) {
316
478
  app.get('/agent/:id/metrics', async (req, reply) => {
317
479
  const { id } = req.params;
318
480
  const windowHours = parseInt(req.query.window ?? '24', 10);
319
- const metrics = evalEngine.computeMetrics(id, windowHours);
481
+ const metrics = await evalEngine.computeMetrics(id, windowHours);
320
482
  return reply.send({ metrics });
321
483
  });
322
484
  app.post('/agent/register', async (req, reply) => {
@@ -333,12 +495,12 @@ export function registerRoutes(app, deps) {
333
495
  // ============================================================
334
496
  app.post('/system/evict', async (req, reply) => {
335
497
  const body = req.body;
336
- const result = evictionEngine.enforceCapacity(body.agentId, DEFAULT_AGENT_CONFIG);
498
+ const result = await evictionEngine.enforceCapacity(body.agentId, DEFAULT_AGENT_CONFIG);
337
499
  return reply.send(result);
338
500
  });
339
501
  app.post('/system/decay', async (req, reply) => {
340
502
  const body = req.body;
341
- const decayed = evictionEngine.decayEdges(body.agentId, body.halfLifeDays);
503
+ const decayed = await evictionEngine.decayEdges(body.agentId, body.halfLifeDays);
342
504
  return reply.send({ edgesDecayed: decayed });
343
505
  });
344
506
  app.post('/system/consolidate', async (req, reply) => {
@@ -365,7 +527,7 @@ export function registerRoutes(app, deps) {
365
527
  });
366
528
  app.get('/memory/restore/:agentId', async (req, reply) => {
367
529
  const { agentId } = req.params;
368
- const checkpoint = store.getCheckpoint(agentId);
530
+ const checkpoint = await store.getCheckpoint(agentId);
369
531
  const now = Date.now();
370
532
  const idleMs = checkpoint
371
533
  ? now - checkpoint.auto.lastActivityAt.getTime()
@@ -373,7 +535,7 @@ export function registerRoutes(app, deps) {
373
535
  // Get last written engram for context
374
536
  let lastWrite = null;
375
537
  if (checkpoint?.auto.lastWriteId) {
376
- const engram = store.getEngram(checkpoint.auto.lastWriteId);
538
+ const engram = await store.getEngram(checkpoint.auto.lastWriteId);
377
539
  if (engram) {
378
540
  lastWrite = { id: engram.id, concept: engram.concept, content: engram.content };
379
541
  }
@@ -423,7 +585,7 @@ export function registerRoutes(app, deps) {
423
585
  // ============================================================
424
586
  app.post('/task/create', async (req, reply) => {
425
587
  const body = req.body;
426
- const engram = store.createEngram({
588
+ const engram = await store.createEngram({
427
589
  agentId: body.agentId,
428
590
  concept: body.concept,
429
591
  content: body.content,
@@ -440,32 +602,32 @@ export function registerRoutes(app, deps) {
440
602
  blockedBy: body.blockedBy,
441
603
  });
442
604
  connectionEngine.enqueue(engram.id);
443
- embed(`${body.concept} ${body.content}`).then(vec => {
444
- store.updateEmbedding(engram.id, vec);
605
+ embed(`${body.concept} ${body.content}`).then(async (vec) => {
606
+ await store.updateEmbedding(engram.id, vec);
445
607
  }).catch(() => { });
446
608
  return reply.send(engram);
447
609
  });
448
610
  app.post('/task/update', async (req, reply) => {
449
611
  const body = req.body;
450
- const engram = store.getEngram(body.taskId);
612
+ const engram = await store.getEngram(body.taskId);
451
613
  if (!engram || !engram.taskStatus) {
452
614
  return reply.code(404).send({ error: 'Task not found' });
453
615
  }
454
616
  if (body.blockedBy !== undefined) {
455
- store.updateBlockedBy(body.taskId, body.blockedBy);
617
+ await store.updateBlockedBy(body.taskId, body.blockedBy);
456
618
  }
457
619
  if (body.status) {
458
- store.updateTaskStatus(body.taskId, body.status);
620
+ await store.updateTaskStatus(body.taskId, body.status);
459
621
  }
460
622
  if (body.priority) {
461
- store.updateTaskPriority(body.taskId, body.priority);
623
+ await store.updateTaskPriority(body.taskId, body.priority);
462
624
  }
463
- return reply.send(store.getEngram(body.taskId));
625
+ return reply.send(await store.getEngram(body.taskId));
464
626
  });
465
627
  app.get('/task/list/:agentId', async (req, reply) => {
466
628
  const { agentId } = req.params;
467
629
  const { status, includeDone } = req.query;
468
- let tasks = store.getTasks(agentId, status);
630
+ let tasks = await store.getTasks(agentId, status);
469
631
  if (includeDone !== 'true' && !status) {
470
632
  tasks = tasks.filter(t => t.taskStatus !== 'done');
471
633
  }
@@ -473,20 +635,25 @@ export function registerRoutes(app, deps) {
473
635
  });
474
636
  app.get('/task/next/:agentId', async (req, reply) => {
475
637
  const { agentId } = req.params;
476
- const next = store.getNextTask(agentId);
638
+ const next = await store.getNextTask(agentId);
477
639
  return reply.send(next ? { task: next } : { task: null, message: 'No actionable tasks' });
478
640
  });
479
641
  // Time warp — shift all timestamps backward by N days (for testing)
480
642
  app.post('/system/time-warp', async (req, reply) => {
481
643
  const body = req.body;
482
644
  const ms = body.days * 24 * 60 * 60 * 1000;
483
- const shifted = store.timeWarp(body.agentId, ms);
645
+ const shifted = await store.timeWarp(body.agentId, ms);
484
646
  return reply.send({ shifted, days: body.days });
485
647
  });
486
648
  // ─── Export ─────────────────────────────────────────────────────────────
487
649
  app.get('/memory/export', async (req, reply) => {
488
650
  const { agentId, all } = req.query;
489
651
  const includeAll = all === 'true';
652
+ // /memory/export uses raw SQL — SQLite-only. On PGlite, callers should use
653
+ // the awm CLI export/merge tools instead.
654
+ if (typeof store.getDb !== 'function') {
655
+ return reply.code(501).send({ error: 'export endpoint requires the SQLite backend' });
656
+ }
490
657
  const db = store.getDb();
491
658
  let engramSql = `SELECT id, agent_id, concept, content, confidence, salience, access_count,
492
659
  last_accessed, created_at, salience_features, reason_codes, stage, ttl,
@@ -527,10 +694,10 @@ export function registerRoutes(app, deps) {
527
694
  const base = {
528
695
  status: 'ok',
529
696
  timestamp: new Date().toISOString(),
530
- version: '0.7.16',
697
+ version: '0.8.5',
531
698
  coordination: coordEnabled,
532
699
  };
533
- if (coordEnabled) {
700
+ if (coordEnabled && typeof deps.store.getDb === 'function') {
534
701
  try {
535
702
  const db = deps.store.getDb();
536
703
  const stats = db.prepare(`SELECT