agent-working-memory 0.7.17 → 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 (186) hide show
  1. package/README.md +156 -15
  2. package/dist/adapters/common.js +202 -202
  3. package/dist/adapters/http.js +22 -22
  4. package/dist/api/routes.d.ts +1 -1
  5. package/dist/api/routes.d.ts.map +1 -1
  6. package/dist/api/routes.js +258 -79
  7. package/dist/api/routes.js.map +1 -1
  8. package/dist/cli/migrate.d.ts +42 -0
  9. package/dist/cli/migrate.d.ts.map +1 -0
  10. package/dist/cli/migrate.js +268 -0
  11. package/dist/cli/migrate.js.map +1 -0
  12. package/dist/cli.js +147 -103
  13. package/dist/cli.js.map +1 -1
  14. package/dist/coordination/circuit-breaker.d.ts +24 -0
  15. package/dist/coordination/circuit-breaker.d.ts.map +1 -0
  16. package/dist/coordination/circuit-breaker.js +70 -0
  17. package/dist/coordination/circuit-breaker.js.map +1 -0
  18. package/dist/coordination/failure-modes.d.ts +21 -0
  19. package/dist/coordination/failure-modes.d.ts.map +1 -0
  20. package/dist/coordination/failure-modes.js +49 -0
  21. package/dist/coordination/failure-modes.js.map +1 -0
  22. package/dist/coordination/peer-decisions.js +7 -7
  23. package/dist/coordination/routes.d.ts.map +1 -1
  24. package/dist/coordination/routes.js +44 -1
  25. package/dist/coordination/routes.js.map +1 -1
  26. package/dist/coordination/schema.d.ts.map +1 -1
  27. package/dist/coordination/schema.js +18 -0
  28. package/dist/coordination/schema.js.map +1 -1
  29. package/dist/coordination/schemas.d.ts +9 -9
  30. package/dist/coordination/stale.d.ts +9 -1
  31. package/dist/coordination/stale.d.ts.map +1 -1
  32. package/dist/coordination/stale.js +53 -9
  33. package/dist/coordination/stale.js.map +1 -1
  34. package/dist/core/embeddings.d.ts +16 -10
  35. package/dist/core/embeddings.d.ts.map +1 -1
  36. package/dist/core/embeddings.js +54 -38
  37. package/dist/core/embeddings.js.map +1 -1
  38. package/dist/core/ml-worker-entry.d.ts +17 -0
  39. package/dist/core/ml-worker-entry.d.ts.map +1 -0
  40. package/dist/core/ml-worker-entry.js +193 -0
  41. package/dist/core/ml-worker-entry.js.map +1 -0
  42. package/dist/core/ml-worker.d.ts +59 -0
  43. package/dist/core/ml-worker.d.ts.map +1 -0
  44. package/dist/core/ml-worker.js +253 -0
  45. package/dist/core/ml-worker.js.map +1 -0
  46. package/dist/core/query-expander.d.ts +13 -15
  47. package/dist/core/query-expander.d.ts.map +1 -1
  48. package/dist/core/query-expander.js +48 -54
  49. package/dist/core/query-expander.js.map +1 -1
  50. package/dist/core/reranker.d.ts +9 -19
  51. package/dist/core/reranker.d.ts.map +1 -1
  52. package/dist/core/reranker.js +33 -35
  53. package/dist/core/reranker.js.map +1 -1
  54. package/dist/core/salience.d.ts +32 -5
  55. package/dist/core/salience.d.ts.map +1 -1
  56. package/dist/core/salience.js +201 -28
  57. package/dist/core/salience.js.map +1 -1
  58. package/dist/core/write-pipeline.d.ts +10 -2
  59. package/dist/core/write-pipeline.d.ts.map +1 -1
  60. package/dist/core/write-pipeline.js +218 -49
  61. package/dist/core/write-pipeline.js.map +1 -1
  62. package/dist/engine/activation.d.ts +2 -2
  63. package/dist/engine/activation.d.ts.map +1 -1
  64. package/dist/engine/activation.js +238 -115
  65. package/dist/engine/activation.js.map +1 -1
  66. package/dist/engine/confidence.d.ts +62 -0
  67. package/dist/engine/confidence.d.ts.map +1 -0
  68. package/dist/engine/confidence.js +100 -0
  69. package/dist/engine/confidence.js.map +1 -0
  70. package/dist/engine/connections.d.ts +47 -8
  71. package/dist/engine/connections.d.ts.map +1 -1
  72. package/dist/engine/connections.js +83 -22
  73. package/dist/engine/connections.js.map +1 -1
  74. package/dist/engine/consolidation-scheduler.d.ts +46 -10
  75. package/dist/engine/consolidation-scheduler.d.ts.map +1 -1
  76. package/dist/engine/consolidation-scheduler.js +170 -50
  77. package/dist/engine/consolidation-scheduler.js.map +1 -1
  78. package/dist/engine/consolidation.d.ts +16 -2
  79. package/dist/engine/consolidation.d.ts.map +1 -1
  80. package/dist/engine/consolidation.js +143 -44
  81. package/dist/engine/consolidation.js.map +1 -1
  82. package/dist/engine/eval.d.ts +2 -2
  83. package/dist/engine/eval.d.ts.map +1 -1
  84. package/dist/engine/eval.js +9 -9
  85. package/dist/engine/eval.js.map +1 -1
  86. package/dist/engine/eviction.d.ts +4 -4
  87. package/dist/engine/eviction.d.ts.map +1 -1
  88. package/dist/engine/eviction.js +15 -15
  89. package/dist/engine/eviction.js.map +1 -1
  90. package/dist/engine/retraction.d.ts +93 -5
  91. package/dist/engine/retraction.d.ts.map +1 -1
  92. package/dist/engine/retraction.js +238 -19
  93. package/dist/engine/retraction.js.map +1 -1
  94. package/dist/engine/staging.d.ts +1 -1
  95. package/dist/engine/staging.d.ts.map +1 -1
  96. package/dist/engine/staging.js +3 -3
  97. package/dist/engine/staging.js.map +1 -1
  98. package/dist/hooks/sidecar.d.ts +1 -1
  99. package/dist/hooks/sidecar.d.ts.map +1 -1
  100. package/dist/hooks/sidecar.js +4 -4
  101. package/dist/hooks/sidecar.js.map +1 -1
  102. package/dist/index.js +48 -31
  103. package/dist/index.js.map +1 -1
  104. package/dist/mcp.js +167 -141
  105. package/dist/mcp.js.map +1 -1
  106. package/dist/storage/factory.d.ts +55 -0
  107. package/dist/storage/factory.d.ts.map +1 -0
  108. package/dist/storage/factory.js +135 -0
  109. package/dist/storage/factory.js.map +1 -0
  110. package/dist/storage/pglite-schema.d.ts +20 -0
  111. package/dist/storage/pglite-schema.d.ts.map +1 -0
  112. package/dist/storage/pglite-schema.js +165 -0
  113. package/dist/storage/pglite-schema.js.map +1 -0
  114. package/dist/storage/pglite.d.ts +187 -0
  115. package/dist/storage/pglite.d.ts.map +1 -0
  116. package/dist/storage/pglite.js +1114 -0
  117. package/dist/storage/pglite.js.map +1 -0
  118. package/dist/storage/sqlite.d.ts +117 -0
  119. package/dist/storage/sqlite.d.ts.map +1 -1
  120. package/dist/storage/sqlite.js +357 -8
  121. package/dist/storage/sqlite.js.map +1 -1
  122. package/dist/storage/store.d.ts +63 -0
  123. package/dist/storage/store.d.ts.map +1 -0
  124. package/dist/storage/store.js +27 -0
  125. package/dist/storage/store.js.map +1 -0
  126. package/dist/types/engram.d.ts +92 -2
  127. package/dist/types/engram.d.ts.map +1 -1
  128. package/dist/types/engram.js.map +1 -1
  129. package/package.json +3 -1
  130. package/src/adapters/claude-code.ts +218 -218
  131. package/src/adapters/codex.ts +252 -252
  132. package/src/adapters/common.ts +411 -411
  133. package/src/adapters/cursor.ts +135 -135
  134. package/src/adapters/http.ts +93 -93
  135. package/src/adapters/index.ts +31 -31
  136. package/src/adapters/types.ts +75 -75
  137. package/src/api/index.ts +3 -3
  138. package/src/api/routes.ts +970 -714
  139. package/src/cli/migrate.ts +307 -0
  140. package/src/cli.ts +755 -719
  141. package/src/coordination/circuit-breaker.ts +83 -0
  142. package/src/coordination/events.ts +90 -90
  143. package/src/coordination/failure-modes.ts +50 -0
  144. package/src/coordination/peer-decisions.ts +105 -105
  145. package/src/coordination/plugin-loader.ts +60 -60
  146. package/src/coordination/plugin.ts +44 -44
  147. package/src/coordination/routes.ts +56 -1
  148. package/src/coordination/schema.ts +13 -0
  149. package/src/coordination/stale.ts +80 -11
  150. package/src/coordination/types.ts +311 -311
  151. package/src/coordination/write-mutex.ts +69 -69
  152. package/src/core/auto-tagger.ts +168 -168
  153. package/src/core/decay.ts +63 -63
  154. package/src/core/embeddings.ts +110 -93
  155. package/src/core/index.ts +5 -5
  156. package/src/core/logger.ts +36 -36
  157. package/src/core/ml-worker-entry.ts +194 -0
  158. package/src/core/ml-worker.ts +281 -0
  159. package/src/core/query-expander.ts +122 -128
  160. package/src/core/reranker.ts +119 -125
  161. package/src/core/salience.ts +200 -33
  162. package/src/core/write-pipeline.ts +521 -343
  163. package/src/engine/activation.ts +235 -109
  164. package/src/engine/confidence.ts +120 -0
  165. package/src/engine/connections.ts +162 -103
  166. package/src/engine/consolidation-scheduler.ts +242 -125
  167. package/src/engine/consolidation.ts +138 -45
  168. package/src/engine/eval.ts +102 -102
  169. package/src/engine/eviction.ts +101 -101
  170. package/src/engine/index.ts +8 -8
  171. package/src/engine/retraction.ts +366 -116
  172. package/src/engine/staging.ts +74 -74
  173. package/src/hooks/sidecar.ts +5 -5
  174. package/src/index.ts +226 -212
  175. package/src/mcp.ts +1145 -1121
  176. package/src/storage/factory.ts +147 -0
  177. package/src/storage/index.ts +3 -3
  178. package/src/storage/pglite-schema.ts +166 -0
  179. package/src/storage/pglite.ts +1363 -0
  180. package/src/storage/sqlite.ts +402 -7
  181. package/src/storage/store.ts +80 -0
  182. package/src/types/agent.ts +67 -67
  183. package/src/types/checkpoint.ts +46 -46
  184. package/src/types/engram.ts +101 -3
  185. package/src/types/eval.ts +100 -100
  186. package/src/types/index.ts +6 -6
@@ -9,28 +9,28 @@
9
9
  import { existsSync } from 'node:fs';
10
10
  import { join } from 'node:path';
11
11
  import { AWM_INSTRUCTION_CONTENT, upsertAwmSection } from './common.js';
12
- const HTTP_ADDENDUM = `
13
- ### HTTP API (for tools without MCP support)
14
-
15
- If your tool doesn't support MCP, use the HTTP API directly:
16
-
17
- **Base URL:** \`http://127.0.0.1:8400\`
18
-
19
- | Method | Endpoint | Purpose |
20
- |--------|----------|---------|
21
- | GET | /memory/restore/:agentId | Restore context from prior sessions |
22
- | POST | /memory/activate | Cognitive recall (use \`context\` field) |
23
- | POST | /memory/write | Write a memory |
24
- | POST | /memory/feedback | Report if memory was useful |
25
- | POST | /memory/retract | Invalidate a wrong memory |
26
- | POST | /memory/checkpoint | Save execution state |
27
- | POST | /task/create | Create a task |
28
- | POST | /task/update | Update task status |
29
- | GET | /task/list/:agentId | List tasks |
30
- | GET | /task/next/:agentId | Get next actionable task |
31
- | GET | /health | Health check |
32
-
33
- Start the server with: \`awm serve\`
12
+ const HTTP_ADDENDUM = `
13
+ ### HTTP API (for tools without MCP support)
14
+
15
+ If your tool doesn't support MCP, use the HTTP API directly:
16
+
17
+ **Base URL:** \`http://127.0.0.1:8400\`
18
+
19
+ | Method | Endpoint | Purpose |
20
+ |--------|----------|---------|
21
+ | GET | /memory/restore/:agentId | Restore context from prior sessions |
22
+ | POST | /memory/activate | Cognitive recall (use \`context\` field) |
23
+ | POST | /memory/write | Write a memory |
24
+ | POST | /memory/feedback | Report if memory was useful |
25
+ | POST | /memory/retract | Invalidate a wrong memory |
26
+ | POST | /memory/checkpoint | Save execution state |
27
+ | POST | /task/create | Create a task |
28
+ | POST | /task/update | Update task status |
29
+ | GET | /task/list/:agentId | List tasks |
30
+ | GET | /task/next/:agentId | Get next actionable task |
31
+ | GET | /health | Health check |
32
+
33
+ Start the server with: \`awm serve\`
34
34
  `;
35
35
  const adapter = {
36
36
  id: 'http',
@@ -31,7 +31,7 @@
31
31
  * GET /health — health check
32
32
  */
33
33
  import type { FastifyInstance } from 'fastify';
34
- import type { EngramStore } from '../storage/sqlite.js';
34
+ import type { IEngramStore as EngramStore } from '../storage/store.js';
35
35
  import type { ActivationEngine } from '../engine/activation.js';
36
36
  import type { ConnectionEngine } from '../engine/connections.js';
37
37
  import type { EvictionEngine } from '../engine/eviction.js';
@@ -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;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,CA0oB3E"}
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"}
@@ -62,7 +62,21 @@ export function registerRoutes(app, deps) {
62
62
  metaTags.push(`sid=${body.sessionId}`);
63
63
  if (body.intent)
64
64
  metaTags.push(`intent=${body.intent}`);
65
- const result = performWrite({ store, connectionEngine }, {
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 }, {
66
80
  agentId: body.agentId,
67
81
  concept: body.concept,
68
82
  content: body.content,
@@ -74,10 +88,13 @@ export function registerRoutes(app, deps) {
74
88
  causalDepth: body.causalDepth,
75
89
  resolutionEffort: body.resolutionEffort,
76
90
  confidence: body.confidence,
91
+ sequence: body.sequence,
92
+ embed: body.embed,
93
+ references: resolvedRefs.length > 0 ? resolvedRefs : undefined,
77
94
  });
78
95
  // Auto-checkpoint always (covers create, reinforce, and supersede).
79
96
  try {
80
- store.updateAutoCheckpointWrite(body.agentId, result.engram.id);
97
+ await store.updateAutoCheckpointWrite(body.agentId, result.engram.id);
81
98
  }
82
99
  catch { /* non-fatal */ }
83
100
  if (result.action === 'reinforce') {
@@ -91,29 +108,40 @@ export function registerRoutes(app, deps) {
91
108
  });
92
109
  }
93
110
  // create / supersede paths follow legacy temporal-edge + episode logic.
94
- try {
95
- const prev = store.getLatestEngram(body.agentId, result.engram.id);
96
- if (prev) {
97
- store.upsertAssociation(prev.id, result.engram.id, 0.3, 'temporal', 0.8);
98
- }
99
- }
100
- catch { /* Temporal edge creation is non-fatal */ }
101
- if (result.salience
102
- && (result.salience.disposition === 'active' || result.salience.disposition === 'discard')) {
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) {
103
116
  try {
104
- let episode = store.getActiveEpisode(body.agentId, 3600_000);
105
- if (!episode) {
106
- 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);
120
+ }
121
+ }
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);
107
131
  }
108
- store.addEngramToEpisode(result.engram.id, episode.id);
132
+ catch { /* Episode assignment is non-fatal */ }
109
133
  }
110
- catch { /* Episode assignment is non-fatal */ }
111
134
  }
112
135
  const isLowSalience = result.salience?.disposition === 'discard';
113
136
  return reply.code(201).send({
114
137
  stored: true,
115
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.
116
143
  disposition: isLowSalience ? 'low-salience' : (result.salience?.disposition ?? 'active'),
144
+ salienceDisposition: result.salience?.disposition ?? null,
117
145
  salience: result.salience?.score ?? 0,
118
146
  reasonCodes: result.salience?.reasonCodes ?? [],
119
147
  engram: result.engram,
@@ -137,7 +165,7 @@ export function registerRoutes(app, deps) {
137
165
  const memTags = [...(mem.tags ?? [])];
138
166
  if (sid)
139
167
  memTags.push(`sid=${sid}`);
140
- const engram = store.createEngram({
168
+ const engram = await store.createEngram({
141
169
  agentId: body.agentId,
142
170
  concept: mem.concept,
143
171
  content: mem.content,
@@ -148,9 +176,9 @@ export function registerRoutes(app, deps) {
148
176
  });
149
177
  // Handle supersession inline — archive superseded memory to remove from active pool
150
178
  if (mem.supersedes) {
151
- store.supersedeEngram(mem.supersedes, engram.id);
152
- store.updateConfidence(mem.supersedes, 0.1);
153
- 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
154
182
  }
155
183
  results.push({ id: engram.id, concept: mem.concept, disposition: 'active' });
156
184
  }
@@ -160,13 +188,13 @@ export function registerRoutes(app, deps) {
160
188
  const vecs = await embedBatch(texts);
161
189
  for (let i = 0; i < vecs.length; i++) {
162
190
  if (results[i]) {
163
- store.updateEmbedding(results[i].id, vecs[i]);
191
+ await store.updateEmbedding(results[i].id, vecs[i]);
164
192
  }
165
193
  }
166
194
  }
167
195
  catch { /* Embedding failure is non-fatal */ }
168
196
  try {
169
- store.updateAutoCheckpointWrite(body.agentId, results[results.length - 1]?.id ?? '');
197
+ await store.updateAutoCheckpointWrite(body.agentId, results[results.length - 1]?.id ?? '');
170
198
  }
171
199
  catch { }
172
200
  return reply.code(201).send({
@@ -185,33 +213,39 @@ export function registerRoutes(app, deps) {
185
213
  useReranker: body.useReranker,
186
214
  useExpansion: body.useExpansion,
187
215
  abstentionThreshold: body.abstentionThreshold,
216
+ requireConfidence: body.requireConfidence,
188
217
  workspace: body.workspace,
189
218
  bm25Only: body.bm25Only,
219
+ granularity: body.granularity,
190
220
  });
191
221
  // Auto-checkpoint: track recall for consolidation scheduling
192
222
  try {
193
223
  const ids = results.map(r => r.engram.id);
194
- store.updateAutoCheckpointRecall(body.agentId, body.context, ids);
224
+ await store.updateAutoCheckpointRecall(body.agentId, body.context, ids);
195
225
  }
196
226
  catch { /* non-fatal */ }
197
- 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 });
198
232
  });
199
233
  app.post('/memory/feedback', async (req, reply) => {
200
234
  const body = req.body;
201
- store.logRetrievalFeedback(body.activationEventId ?? null, body.engramId, body.useful, body.context ?? '');
235
+ await store.logRetrievalFeedback(body.activationEventId ?? null, body.engramId, body.useful, body.context ?? '');
202
236
  // Update engram confidence based on feedback
203
- const engram = store.getEngram(body.engramId);
237
+ const engram = await store.getEngram(body.engramId);
204
238
  if (engram) {
205
239
  const config = DEFAULT_AGENT_CONFIG;
206
240
  const delta = body.useful
207
241
  ? config.feedbackPositiveBoost
208
242
  : -config.feedbackNegativePenalty;
209
- store.updateConfidence(engram.id, engram.confidence + delta);
243
+ await store.updateConfidence(engram.id, engram.confidence + delta);
210
244
  }
211
245
  // Touch activity for consolidation scheduling
212
246
  if (engram) {
213
247
  try {
214
- store.touchActivity(engram.agentId);
248
+ await store.touchActivity(engram.agentId);
215
249
  }
216
250
  catch { /* non-fatal */ }
217
251
  }
@@ -219,7 +253,7 @@ export function registerRoutes(app, deps) {
219
253
  });
220
254
  app.post('/memory/retract', async (req, reply) => {
221
255
  const body = req.body;
222
- const result = retractionEngine.retract({
256
+ const result = await retractionEngine.retract({
223
257
  agentId: body.agentId,
224
258
  targetEngramId: body.targetEngramId,
225
259
  reason: body.reason,
@@ -227,33 +261,83 @@ export function registerRoutes(app, deps) {
227
261
  });
228
262
  // Touch activity for consolidation scheduling
229
263
  try {
230
- store.touchActivity(body.agentId);
264
+ await store.touchActivity(body.agentId);
231
265
  }
232
266
  catch { /* non-fatal */ }
233
267
  return reply.send(result);
234
268
  });
235
269
  app.post('/memory/supersede', async (req, reply) => {
236
270
  const body = req.body;
237
- const oldEngram = store.getEngram(body.oldEngramId);
238
- const newEngram = store.getEngram(body.newEngramId);
239
- if (!oldEngram)
240
- return reply.code(404).send({ error: `Old engram ${body.oldEngramId} not found` });
241
- if (!newEngram)
242
- return reply.code(404).send({ error: `New engram ${body.newEngramId} not found` });
243
- // Create causal association (new → old)
244
- store.upsertAssociation(body.newEngramId, body.oldEngramId, 0.8, 'causal', 1.0);
245
- // Reduce old engram confidence to 20% (keep for historical reference)
246
- store.updateConfidence(body.oldEngramId, oldEngram.confidence * 0.2);
247
- // Mark supersession via store method
248
- 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
+ });
249
332
  try {
250
- store.touchActivity(oldEngram.agentId);
333
+ await store.touchActivity(body.agentId);
251
334
  }
252
335
  catch { /* non-fatal */ }
253
- return reply.send({
254
- superseded: body.oldEngramId,
255
- supersededBy: body.newEngramId,
256
- 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',
257
341
  });
258
342
  });
259
343
  // ============================================================
@@ -261,32 +345,122 @@ export function registerRoutes(app, deps) {
261
345
  // ============================================================
262
346
  app.post('/memory/search', async (req, reply) => {
263
347
  const body = req.body;
264
- const results = store.search({
348
+ const results = await store.search({
265
349
  agentId: body.agentId,
266
350
  text: body.text,
267
351
  concept: body.concept,
268
352
  tags: body.tags,
353
+ tagsAll: body.tagsAll,
354
+ tagsAny: body.tagsAny,
355
+ tagsNone: body.tagsNone,
269
356
  stage: body.stage,
270
357
  retracted: body.retracted,
271
358
  limit: body.limit,
272
359
  offset: body.offset,
360
+ sortBy: body.sortBy,
361
+ sortOrder: body.sortOrder,
362
+ });
363
+ return reply.send({ results, count: results.length });
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,
273
385
  });
274
386
  return reply.send({ results, count: results.length });
275
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
+ });
276
450
  app.get('/memory/:id', async (req, reply) => {
277
451
  const { id } = req.params;
278
- const engram = store.getEngram(id);
452
+ const engram = await store.getEngram(id);
279
453
  if (!engram)
280
454
  return reply.code(404).send({ error: 'Not found' });
281
- const associations = store.getAssociationsFor(id);
455
+ const associations = await store.getAssociationsFor(id);
282
456
  return reply.send({ engram, associations });
283
457
  });
284
458
  app.get('/agent/:id/stats', async (req, reply) => {
285
459
  const { id } = req.params;
286
- const active = store.getEngramsByAgent(id, 'active');
287
- const staging = store.getEngramsByAgent(id, 'staging');
288
- const retracted = store.getEngramsByAgent(id, undefined, true).filter(e => e.retracted);
289
- 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);
290
464
  return reply.send({
291
465
  agentId: id,
292
466
  engrams: {
@@ -304,7 +478,7 @@ export function registerRoutes(app, deps) {
304
478
  app.get('/agent/:id/metrics', async (req, reply) => {
305
479
  const { id } = req.params;
306
480
  const windowHours = parseInt(req.query.window ?? '24', 10);
307
- const metrics = evalEngine.computeMetrics(id, windowHours);
481
+ const metrics = await evalEngine.computeMetrics(id, windowHours);
308
482
  return reply.send({ metrics });
309
483
  });
310
484
  app.post('/agent/register', async (req, reply) => {
@@ -321,12 +495,12 @@ export function registerRoutes(app, deps) {
321
495
  // ============================================================
322
496
  app.post('/system/evict', async (req, reply) => {
323
497
  const body = req.body;
324
- const result = evictionEngine.enforceCapacity(body.agentId, DEFAULT_AGENT_CONFIG);
498
+ const result = await evictionEngine.enforceCapacity(body.agentId, DEFAULT_AGENT_CONFIG);
325
499
  return reply.send(result);
326
500
  });
327
501
  app.post('/system/decay', async (req, reply) => {
328
502
  const body = req.body;
329
- const decayed = evictionEngine.decayEdges(body.agentId, body.halfLifeDays);
503
+ const decayed = await evictionEngine.decayEdges(body.agentId, body.halfLifeDays);
330
504
  return reply.send({ edgesDecayed: decayed });
331
505
  });
332
506
  app.post('/system/consolidate', async (req, reply) => {
@@ -353,7 +527,7 @@ export function registerRoutes(app, deps) {
353
527
  });
354
528
  app.get('/memory/restore/:agentId', async (req, reply) => {
355
529
  const { agentId } = req.params;
356
- const checkpoint = store.getCheckpoint(agentId);
530
+ const checkpoint = await store.getCheckpoint(agentId);
357
531
  const now = Date.now();
358
532
  const idleMs = checkpoint
359
533
  ? now - checkpoint.auto.lastActivityAt.getTime()
@@ -361,7 +535,7 @@ export function registerRoutes(app, deps) {
361
535
  // Get last written engram for context
362
536
  let lastWrite = null;
363
537
  if (checkpoint?.auto.lastWriteId) {
364
- const engram = store.getEngram(checkpoint.auto.lastWriteId);
538
+ const engram = await store.getEngram(checkpoint.auto.lastWriteId);
365
539
  if (engram) {
366
540
  lastWrite = { id: engram.id, concept: engram.concept, content: engram.content };
367
541
  }
@@ -411,7 +585,7 @@ export function registerRoutes(app, deps) {
411
585
  // ============================================================
412
586
  app.post('/task/create', async (req, reply) => {
413
587
  const body = req.body;
414
- const engram = store.createEngram({
588
+ const engram = await store.createEngram({
415
589
  agentId: body.agentId,
416
590
  concept: body.concept,
417
591
  content: body.content,
@@ -428,32 +602,32 @@ export function registerRoutes(app, deps) {
428
602
  blockedBy: body.blockedBy,
429
603
  });
430
604
  connectionEngine.enqueue(engram.id);
431
- embed(`${body.concept} ${body.content}`).then(vec => {
432
- store.updateEmbedding(engram.id, vec);
605
+ embed(`${body.concept} ${body.content}`).then(async (vec) => {
606
+ await store.updateEmbedding(engram.id, vec);
433
607
  }).catch(() => { });
434
608
  return reply.send(engram);
435
609
  });
436
610
  app.post('/task/update', async (req, reply) => {
437
611
  const body = req.body;
438
- const engram = store.getEngram(body.taskId);
612
+ const engram = await store.getEngram(body.taskId);
439
613
  if (!engram || !engram.taskStatus) {
440
614
  return reply.code(404).send({ error: 'Task not found' });
441
615
  }
442
616
  if (body.blockedBy !== undefined) {
443
- store.updateBlockedBy(body.taskId, body.blockedBy);
617
+ await store.updateBlockedBy(body.taskId, body.blockedBy);
444
618
  }
445
619
  if (body.status) {
446
- store.updateTaskStatus(body.taskId, body.status);
620
+ await store.updateTaskStatus(body.taskId, body.status);
447
621
  }
448
622
  if (body.priority) {
449
- store.updateTaskPriority(body.taskId, body.priority);
623
+ await store.updateTaskPriority(body.taskId, body.priority);
450
624
  }
451
- return reply.send(store.getEngram(body.taskId));
625
+ return reply.send(await store.getEngram(body.taskId));
452
626
  });
453
627
  app.get('/task/list/:agentId', async (req, reply) => {
454
628
  const { agentId } = req.params;
455
629
  const { status, includeDone } = req.query;
456
- let tasks = store.getTasks(agentId, status);
630
+ let tasks = await store.getTasks(agentId, status);
457
631
  if (includeDone !== 'true' && !status) {
458
632
  tasks = tasks.filter(t => t.taskStatus !== 'done');
459
633
  }
@@ -461,24 +635,29 @@ export function registerRoutes(app, deps) {
461
635
  });
462
636
  app.get('/task/next/:agentId', async (req, reply) => {
463
637
  const { agentId } = req.params;
464
- const next = store.getNextTask(agentId);
638
+ const next = await store.getNextTask(agentId);
465
639
  return reply.send(next ? { task: next } : { task: null, message: 'No actionable tasks' });
466
640
  });
467
641
  // Time warp — shift all timestamps backward by N days (for testing)
468
642
  app.post('/system/time-warp', async (req, reply) => {
469
643
  const body = req.body;
470
644
  const ms = body.days * 24 * 60 * 60 * 1000;
471
- const shifted = store.timeWarp(body.agentId, ms);
645
+ const shifted = await store.timeWarp(body.agentId, ms);
472
646
  return reply.send({ shifted, days: body.days });
473
647
  });
474
648
  // ─── Export ─────────────────────────────────────────────────────────────
475
649
  app.get('/memory/export', async (req, reply) => {
476
650
  const { agentId, all } = req.query;
477
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
+ }
478
657
  const db = store.getDb();
479
- let engramSql = `SELECT id, agent_id, concept, content, confidence, salience, access_count,
480
- last_accessed, created_at, salience_features, reason_codes, stage, ttl,
481
- retracted, retracted_by, retracted_at, tags
658
+ let engramSql = `SELECT id, agent_id, concept, content, confidence, salience, access_count,
659
+ last_accessed, created_at, salience_features, reason_codes, stage, ttl,
660
+ retracted, retracted_by, retracted_at, tags
482
661
  FROM engrams`;
483
662
  const conditions = [];
484
663
  const params = [];
@@ -496,7 +675,7 @@ export function registerRoutes(app, deps) {
496
675
  engramSql += ' ORDER BY created_at ASC';
497
676
  const engrams = db.prepare(engramSql).all(...params);
498
677
  const engramIds = new Set(engrams.map(e => e.id));
499
- const allAssocs = db.prepare(`SELECT id, from_engram_id, to_engram_id, weight, confidence, type, activation_count, created_at, last_activated
678
+ const allAssocs = db.prepare(`SELECT id, from_engram_id, to_engram_id, weight, confidence, type, activation_count, created_at, last_activated
500
679
  FROM associations`).all();
501
680
  const associations = allAssocs.filter(a => engramIds.has(a.from_engram_id) && engramIds.has(a.to_engram_id));
502
681
  return reply.send({
@@ -515,15 +694,15 @@ export function registerRoutes(app, deps) {
515
694
  const base = {
516
695
  status: 'ok',
517
696
  timestamp: new Date().toISOString(),
518
- version: '0.7.17',
697
+ version: '0.8.5',
519
698
  coordination: coordEnabled,
520
699
  };
521
- if (coordEnabled) {
700
+ if (coordEnabled && typeof deps.store.getDb === 'function') {
522
701
  try {
523
702
  const db = deps.store.getDb();
524
- const stats = db.prepare(`SELECT
525
- (SELECT COUNT(*) FROM coord_agents WHERE status != 'dead') AS agents_alive,
526
- (SELECT COUNT(*) FROM coord_assignments WHERE status = 'pending') AS pending_tasks,
703
+ const stats = db.prepare(`SELECT
704
+ (SELECT COUNT(*) FROM coord_agents WHERE status != 'dead') AS agents_alive,
705
+ (SELECT COUNT(*) FROM coord_assignments WHERE status = 'pending') AS pending_tasks,
527
706
  (SELECT COUNT(*) FROM coord_locks) AS active_locks`).get();
528
707
  Object.assign(base, stats);
529
708
  }