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
package/src/api/routes.ts CHANGED
@@ -34,7 +34,7 @@
34
34
  */
35
35
 
36
36
  import type { FastifyInstance } from 'fastify';
37
- import type { EngramStore } from '../storage/sqlite.js';
37
+ import type { IEngramStore as EngramStore } from '../storage/store.js';
38
38
  import type { ActivationEngine } from '../engine/activation.js';
39
39
  import type { ConnectionEngine } from '../engine/connections.js';
40
40
  import type { EvictionEngine } from '../engine/eviction.js';
@@ -44,6 +44,7 @@ import type { ConsolidationEngine } from '../engine/consolidation.js';
44
44
  import type { ConsolidationScheduler } from '../engine/consolidation-scheduler.js';
45
45
  import { evaluateSalience, computeNovelty } from '../core/salience.js';
46
46
  import type { SalienceEventType } from '../core/salience.js';
47
+ import { performWrite } from '../core/write-pipeline.js';
47
48
  import type { TaskStatus, TaskPriority } from '../types/engram.js';
48
49
  import type { ConsciousState } from '../types/checkpoint.js';
49
50
  import { DEFAULT_AGENT_CONFIG } from '../types/agent.js';
@@ -79,6 +80,34 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
79
80
  causalDepth?: number;
80
81
  resolutionEffort?: number;
81
82
  confidence?: number;
83
+ // Memory class — canonical bypasses salience filter; structural is
84
+ // for system-written event-log records (see 0.8 spec). Restored in
85
+ // 0.8.0 after the field was dropped from the HTTP body schema during
86
+ // the 0.7.x refactor — core/salience.ts:88-89,124,187 and
87
+ // core/write-pipeline.ts:77 still expect and honor it, so HTTP
88
+ // callers were silently losing the canonical-bypass signal.
89
+ memory_class?: 'canonical' | 'working' | 'ephemeral' | 'structural';
90
+ // Optional story-time / sequence ordering (0.8 Cluster A). NULL by
91
+ // default. Used by sortBy: "sequence" on /memory/search and by
92
+ // /memory/latest-by-tag (0.8 Cluster C).
93
+ sequence?: number;
94
+ // Force embedding for structural-class writes. Defaults to false for
95
+ // structural (deterministic retrieval only) — opt in here when cognitive
96
+ // recall over a structural engram is desired. (0.8 Cluster A)
97
+ embed?: boolean;
98
+ // Typed cross-record links (0.8 Cluster D). Each reference is
99
+ // {type, matchEngramId?, matchConcept?}. If matchConcept is given
100
+ // without matchEngramId, AWM resolves it to the most recent active
101
+ // engram at write time and stores BOTH on the reference (stable link).
102
+ // If no match found, stores just matchConcept (caller may be linking
103
+ // to a future or deleted engram). Types: advances | resolves |
104
+ // subverts | abandons | extends | supersedes.
105
+ references?: Array<{
106
+ type: 'advances' | 'resolves' | 'subverts' | 'abandons' | 'extends' | 'supersedes';
107
+ matchEngramId?: string;
108
+ matchConcept?: string;
109
+ matchTags?: string[];
110
+ }>;
82
111
  // Agent-provided metadata (stored as searchable tags)
83
112
  project?: string;
84
113
  topic?: string;
@@ -94,24 +123,6 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
94
123
  return reply.status(400).send({ error: 'agentId, concept, and content are required strings' });
95
124
  }
96
125
 
97
- const novelty = computeNovelty(store, body.agentId, body.concept, body.content);
98
-
99
- const salience = evaluateSalience({
100
- content: body.content,
101
- eventType: body.eventType,
102
- surprise: body.surprise,
103
- decisionMade: body.decisionMade,
104
- causalDepth: body.causalDepth,
105
- resolutionEffort: body.resolutionEffort,
106
- novelty,
107
- });
108
-
109
- // v0.5.4: No longer discard — store with low confidence for ranking.
110
- const isLowSalience = salience.disposition === 'discard';
111
- const confidence = isLowSalience
112
- ? 0.25
113
- : body.confidence ?? (salience.disposition === 'staging' ? 0.40 : 0.50);
114
-
115
126
  // Assemble tags: user-provided + agent metadata
116
127
  const userTags = body.tags ?? [];
117
128
  const metaTags: string[] = [];
@@ -121,62 +132,94 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
121
132
  if (body.confidenceLevel) metaTags.push(`conf=${body.confidenceLevel}`);
122
133
  if (body.sessionId) metaTags.push(`sid=${body.sessionId}`);
123
134
  if (body.intent) metaTags.push(`intent=${body.intent}`);
124
- const allTags = isLowSalience
125
- ? [...userTags, ...metaTags, 'low-salience']
126
- : [...userTags, ...metaTags];
127
135
 
128
- const engram = store.createEngram({
136
+ // Resolve references (0.8 Cluster D): if matchConcept is given without
137
+ // matchEngramId, look up the most recent active engram with that concept
138
+ // (+ optional matchTags) and store both. Stable link survives concept
139
+ // edits. No match found → store just matchConcept so the intent
140
+ // (link-to-future or link-to-deleted) is preserved.
141
+ const resolvedRefs = await Promise.all((body.references ?? []).map(async ref => {
142
+ if (!ref.matchEngramId && ref.matchConcept) {
143
+ const matched = await store.findActiveMatchByConcept(
144
+ body.agentId, ref.matchConcept, ref.matchTags,
145
+ );
146
+ if (matched) {
147
+ return { type: ref.type, matchEngramId: matched.id, matchConcept: ref.matchConcept };
148
+ }
149
+ }
150
+ return { type: ref.type, matchEngramId: ref.matchEngramId, matchConcept: ref.matchConcept };
151
+ }));
152
+
153
+ const result = await performWrite({ store, connectionEngine }, {
129
154
  agentId: body.agentId,
130
155
  concept: body.concept,
131
156
  content: body.content,
132
- tags: allTags,
133
- salience: salience.score,
134
- confidence,
135
- salienceFeatures: salience.features,
136
- reasonCodes: salience.reasonCodes,
137
- ttl: salience.disposition === 'staging' ? DEFAULT_AGENT_CONFIG.stagingTtlMs : undefined,
157
+ tags: [...userTags, ...metaTags],
158
+ memoryClass: body.memory_class,
159
+ eventType: body.eventType,
160
+ surprise: body.surprise,
161
+ decisionMade: body.decisionMade,
162
+ causalDepth: body.causalDepth,
163
+ resolutionEffort: body.resolutionEffort,
164
+ confidence: body.confidence,
165
+ sequence: body.sequence,
166
+ embed: body.embed,
167
+ references: resolvedRefs.length > 0 ? resolvedRefs : undefined,
138
168
  });
139
169
 
140
- if (salience.disposition === 'staging') {
141
- store.updateStage(engram.id, 'staging');
170
+ // Auto-checkpoint always (covers create, reinforce, and supersede).
171
+ try { await store.updateAutoCheckpointWrite(body.agentId, result.engram.id); } catch { /* non-fatal */ }
172
+
173
+ if (result.action === 'reinforce') {
174
+ return reply.code(200).send({
175
+ stored: false,
176
+ action: 'reinforce',
177
+ disposition: 'reinforced',
178
+ engram: result.engram,
179
+ reinforce: result.reinforce,
180
+ novelty: result.noveltyResult.novelty,
181
+ });
142
182
  }
143
183
 
144
- // Create temporal adjacency edge to previous memory (conversation thread graph)
145
- // This enables multi-hop graph walk through conversation sequences
146
- try {
147
- const prev = store.getLatestEngram(body.agentId, engram.id);
148
- if (prev) {
149
- store.upsertAssociation(prev.id, engram.id, 0.3, 'temporal', 0.8);
150
- }
151
- } catch { /* Temporal edge creation is non-fatal */ }
152
-
153
- if (salience.disposition === 'active' || isLowSalience) {
154
- connectionEngine.enqueue(engram.id);
155
-
156
- // Auto-assign to episode (1-hour window per agent)
184
+ // create / supersede paths follow legacy temporal-edge + episode logic.
185
+ // Structural-class writes (0.8 Cluster A) skip these: they're system-written
186
+ // event-log records, not conversational beats, so keeping them out of the
187
+ // temporal graph + episode index keeps cognitive retrieval clean.
188
+ const isStructural = body.memory_class === 'structural';
189
+ if (!isStructural) {
157
190
  try {
158
- let episode = store.getActiveEpisode(body.agentId, 3600_000);
159
- if (!episode) {
160
- episode = store.createEpisode({ agentId: body.agentId, label: body.concept });
191
+ const prev = await store.getLatestEngram(body.agentId, result.engram.id);
192
+ if (prev) {
193
+ await store.upsertAssociation(prev.id, result.engram.id, 0.3, 'temporal', 0.8);
161
194
  }
162
- store.addEngramToEpisode(engram.id, episode.id);
163
- } catch { /* Episode assignment is non-fatal */ }
195
+ } catch { /* Temporal edge creation is non-fatal */ }
196
+
197
+ if (result.salience
198
+ && (result.salience.disposition === 'active' || result.salience.disposition === 'discard')) {
199
+ try {
200
+ let episode = await store.getActiveEpisode(body.agentId, 3600_000);
201
+ if (!episode) {
202
+ episode = await store.createEpisode({ agentId: body.agentId, label: body.concept });
203
+ }
204
+ await store.addEngramToEpisode(result.engram.id, episode.id);
205
+ } catch { /* Episode assignment is non-fatal */ }
206
+ }
164
207
  }
165
208
 
166
- // Generate embedding asynchronously (don't block response)
167
- embed(`${body.concept} ${body.content}`).then(vec => {
168
- store.updateEmbedding(engram.id, vec);
169
- }).catch(() => {}); // Embedding failure is non-fatal
170
-
171
- // Auto-checkpoint: track write for consolidation scheduling
172
- try { store.updateAutoCheckpointWrite(body.agentId, engram.id); } catch { /* non-fatal */ }
173
-
209
+ const isLowSalience = result.salience?.disposition === 'discard';
174
210
  return reply.code(201).send({
175
211
  stored: true,
176
- disposition: isLowSalience ? 'low-salience' : salience.disposition,
177
- salience: salience.score,
178
- reasonCodes: salience.reasonCodes,
179
- engram,
212
+ action: result.action,
213
+ // disposition: legacy field — 'low-salience' is returned for discard so
214
+ // callers know the engram was kept but marked low-value. The raw inner
215
+ // salience decision is exposed as `salienceDisposition` for callers
216
+ // (and tests) that want the unmapped value.
217
+ disposition: isLowSalience ? 'low-salience' : (result.salience?.disposition ?? 'active'),
218
+ salienceDisposition: result.salience?.disposition ?? null,
219
+ salience: result.salience?.score ?? 0,
220
+ reasonCodes: result.salience?.reasonCodes ?? [],
221
+ engram: result.engram,
222
+ supersedeOf: result.supersedeOf,
180
223
  });
181
224
  });
182
225
 
@@ -210,7 +253,7 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
210
253
  const memTags = [...(mem.tags ?? [])];
211
254
  if (sid) memTags.push(`sid=${sid}`);
212
255
 
213
- const engram = store.createEngram({
256
+ const engram = await store.createEngram({
214
257
  agentId: body.agentId,
215
258
  concept: mem.concept,
216
259
  content: mem.content,
@@ -222,9 +265,9 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
222
265
 
223
266
  // Handle supersession inline — archive superseded memory to remove from active pool
224
267
  if (mem.supersedes) {
225
- store.supersedeEngram(mem.supersedes, engram.id);
226
- store.updateConfidence(mem.supersedes, 0.1);
227
- store.updateStage(mem.supersedes, 'archived'); // Remove from active search pool
268
+ await store.supersedeEngram(mem.supersedes, engram.id);
269
+ await store.updateConfidence(mem.supersedes, 0.1);
270
+ await store.updateStage(mem.supersedes, 'archived'); // Remove from active search pool
228
271
  }
229
272
 
230
273
  results.push({ id: engram.id, concept: mem.concept, disposition: 'active' });
@@ -236,12 +279,12 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
236
279
  const vecs = await embedBatch(texts);
237
280
  for (let i = 0; i < vecs.length; i++) {
238
281
  if (results[i]) {
239
- store.updateEmbedding(results[i].id, vecs[i]);
282
+ await store.updateEmbedding(results[i].id, vecs[i]);
240
283
  }
241
284
  }
242
285
  } catch { /* Embedding failure is non-fatal */ }
243
286
 
244
- try { store.updateAutoCheckpointWrite(body.agentId, results[results.length - 1]?.id ?? ''); } catch {}
287
+ try { await store.updateAutoCheckpointWrite(body.agentId, results[results.length - 1]?.id ?? ''); } catch {}
245
288
 
246
289
  return reply.code(201).send({
247
290
  stored: results.length,
@@ -259,8 +302,10 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
259
302
  useReranker?: boolean;
260
303
  useExpansion?: boolean;
261
304
  abstentionThreshold?: number;
305
+ requireConfidence?: number;
262
306
  workspace?: string;
263
307
  bm25Only?: boolean;
308
+ granularity?: 'full' | 'compact' | 'auto';
264
309
  };
265
310
 
266
311
  const results = await activationEngine.activate({
@@ -272,17 +317,23 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
272
317
  useReranker: body.useReranker,
273
318
  useExpansion: body.useExpansion,
274
319
  abstentionThreshold: body.abstentionThreshold,
320
+ requireConfidence: body.requireConfidence,
275
321
  workspace: body.workspace,
276
322
  bm25Only: body.bm25Only,
323
+ granularity: body.granularity,
277
324
  });
278
325
 
279
326
  // Auto-checkpoint: track recall for consolidation scheduling
280
327
  try {
281
328
  const ids = results.map(r => r.engram.id);
282
- store.updateAutoCheckpointRecall(body.agentId, body.context, ids);
329
+ await store.updateAutoCheckpointRecall(body.agentId, body.context, ids);
283
330
  } catch { /* non-fatal */ }
284
331
 
285
- return reply.send({ results });
332
+ // Surface recall confidence as a top-level field too — same value is on
333
+ // every result, but it describes the recall as a whole, so exposing it
334
+ // once is easier for consumers (and lets them inspect 0-result recalls).
335
+ const confidence = results[0]?.confidence ?? 0;
336
+ return reply.send({ results, confidence });
286
337
  });
287
338
 
288
339
  app.post('/memory/feedback', async (req, reply) => {
@@ -293,7 +344,7 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
293
344
  context?: string;
294
345
  };
295
346
 
296
- store.logRetrievalFeedback(
347
+ await store.logRetrievalFeedback(
297
348
  body.activationEventId ?? null,
298
349
  body.engramId,
299
350
  body.useful,
@@ -301,18 +352,18 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
301
352
  );
302
353
 
303
354
  // Update engram confidence based on feedback
304
- const engram = store.getEngram(body.engramId);
355
+ const engram = await store.getEngram(body.engramId);
305
356
  if (engram) {
306
357
  const config = DEFAULT_AGENT_CONFIG;
307
358
  const delta = body.useful
308
359
  ? config.feedbackPositiveBoost
309
360
  : -config.feedbackNegativePenalty;
310
- store.updateConfidence(engram.id, engram.confidence + delta);
361
+ await store.updateConfidence(engram.id, engram.confidence + delta);
311
362
  }
312
363
 
313
364
  // Touch activity for consolidation scheduling
314
365
  if (engram) {
315
- try { store.touchActivity(engram.agentId); } catch { /* non-fatal */ }
366
+ try { await store.touchActivity(engram.agentId); } catch { /* non-fatal */ }
316
367
  }
317
368
 
318
369
  return reply.send({ recorded: true });
@@ -326,7 +377,7 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
326
377
  counterContent?: string;
327
378
  };
328
379
 
329
- const result = retractionEngine.retract({
380
+ const result = await retractionEngine.retract({
330
381
  agentId: body.agentId,
331
382
  targetEngramId: body.targetEngramId,
332
383
  reason: body.reason,
@@ -334,38 +385,105 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
334
385
  });
335
386
 
336
387
  // Touch activity for consolidation scheduling
337
- try { store.touchActivity(body.agentId); } catch { /* non-fatal */ }
388
+ try { await store.touchActivity(body.agentId); } catch { /* non-fatal */ }
338
389
 
339
390
  return reply.send(result);
340
391
  });
341
392
 
342
393
  app.post('/memory/supersede', async (req, reply) => {
343
394
  const body = req.body as {
344
- oldEngramId: string;
345
- newEngramId: string;
395
+ // ── Form A — supersede by existing engram IDs (pre-0.8 behavior) ──
396
+ oldEngramId?: string;
397
+ newEngramId?: string;
398
+ // ── Form B — atomic write-and-supersede by concept match (0.8 Cluster D) ──
399
+ agentId?: string;
400
+ matchConcept?: string;
401
+ matchTags?: string[];
402
+ newEngram?: {
403
+ concept: string;
404
+ content: string;
405
+ tags?: string[];
406
+ memory_class?: 'canonical' | 'working' | 'ephemeral' | 'structural';
407
+ sequence?: number;
408
+ eventType?: SalienceEventType;
409
+ };
410
+ // Common
346
411
  reason?: string;
347
412
  };
348
413
 
349
- const oldEngram = store.getEngram(body.oldEngramId);
350
- const newEngram = store.getEngram(body.newEngramId);
351
- if (!oldEngram) return reply.code(404).send({ error: `Old engram ${body.oldEngramId} not found` });
352
- if (!newEngram) return reply.code(404).send({ error: `New engram ${body.newEngramId} not found` });
414
+ const isFormA = !!(body.oldEngramId && body.newEngramId);
415
+ const isFormB = !!(body.matchConcept && body.newEngram && body.agentId);
353
416
 
354
- // Create causal association (new old)
355
- store.upsertAssociation(body.newEngramId, body.oldEngramId, 0.8, 'causal', 1.0);
417
+ if (isFormA && isFormB) {
418
+ return reply.code(400).send({
419
+ error: 'Pass either {oldEngramId, newEngramId} (Form A) OR ' +
420
+ '{agentId, matchConcept, newEngram} (Form B), not both.',
421
+ });
422
+ }
423
+ if (!isFormA && !isFormB) {
424
+ return reply.code(400).send({
425
+ error: 'Missing required fields. Form A: {oldEngramId, newEngramId}. ' +
426
+ 'Form B: {agentId, matchConcept, newEngram}.',
427
+ });
428
+ }
429
+
430
+ // ── Form A — by IDs ──
431
+ if (isFormA) {
432
+ const oldEngram = await store.getEngram(body.oldEngramId!);
433
+ const newEngram = await store.getEngram(body.newEngramId!);
434
+ if (!oldEngram) return reply.code(404).send({ error: `Old engram ${body.oldEngramId} not found` });
435
+ if (!newEngram) return reply.code(404).send({ error: `New engram ${body.newEngramId} not found` });
436
+
437
+ await store.upsertAssociation(body.newEngramId!, body.oldEngramId!, 0.8, 'causal', 1.0);
438
+ await store.updateConfidence(body.oldEngramId!, oldEngram.confidence * 0.2);
439
+ await store.supersedeEngram(body.oldEngramId!, body.newEngramId!);
440
+ try { await store.touchActivity(oldEngram.agentId); } catch { /* non-fatal */ }
441
+
442
+ return reply.send({
443
+ superseded: body.oldEngramId,
444
+ supersededBy: body.newEngramId,
445
+ reason: body.reason ?? 'outdated',
446
+ });
447
+ }
356
448
 
357
- // Reduce old engram confidence to 20% (keep for historical reference)
358
- store.updateConfidence(body.oldEngramId, oldEngram.confidence * 0.2);
449
+ // ── Form B atomic write-and-supersede by concept match ──
450
+ // Find old (most recent active match by concept + optional tags), write
451
+ // new engram via performWrite, link them — all in one SQL transaction.
452
+ // If no match: write new anyway, return { superseded: null }.
453
+ // AWM 0.8.x P4b follow-up: Form B atomicity via withTransaction.
454
+ // Holds the SQLite/PGlite lock across the async write + supersede pair
455
+ // so callers never observe a half-completed state.
456
+ const result = await (store.withTransaction(async () => {
457
+ const matched = await store.findActiveMatchByConcept(
458
+ body.agentId!, body.matchConcept!, body.matchTags,
459
+ );
460
+
461
+ const writeRes = await performWrite({ store, connectionEngine }, {
462
+ agentId: body.agentId!,
463
+ concept: body.newEngram!.concept,
464
+ content: body.newEngram!.content,
465
+ tags: body.newEngram!.tags ?? [],
466
+ memoryClass: body.newEngram!.memory_class,
467
+ sequence: body.newEngram!.sequence,
468
+ eventType: body.newEngram!.eventType,
469
+ enableReinforcement: false,
470
+ });
359
471
 
360
- // Mark supersession via store method
361
- store.supersedeEngram(body.oldEngramId, body.newEngramId);
472
+ if (matched) {
473
+ await store.upsertAssociation(writeRes.engram.id, matched.id, 0.8, 'causal', 1.0);
474
+ await store.updateConfidence(matched.id, matched.confidence * 0.2);
475
+ await store.supersedeEngram(matched.id, writeRes.engram.id);
476
+ }
477
+ return { writeRes, matched };
478
+ }) as Promise<{ writeRes: Awaited<ReturnType<typeof performWrite>>; matched: any }>);
362
479
 
363
- try { store.touchActivity(oldEngram.agentId); } catch { /* non-fatal */ }
480
+ try { await store.touchActivity(body.agentId!); } catch { /* non-fatal */ }
364
481
 
365
- return reply.send({
366
- superseded: body.oldEngramId,
367
- supersededBy: body.newEngramId,
368
- reason: body.reason ?? 'outdated',
482
+ return reply.code(201).send({
483
+ newEngram: result.writeRes.engram,
484
+ superseded: result.matched ? result.matched.id : null,
485
+ supersededBy: result.writeRes.engram.id,
486
+ reason: body.reason ?? 'resolved by concept match',
369
487
  });
370
488
  });
371
489
 
@@ -378,42 +496,166 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
378
496
  agentId: string;
379
497
  text?: string;
380
498
  concept?: string;
381
- tags?: string[];
499
+ tags?: string[]; // legacy AND-filter — preserved, equivalent to tagsAll
500
+ tagsAll?: string[]; // 0.8 Cluster B — explicit AND
501
+ tagsAny?: string[]; // 0.8 Cluster B — OR (at least one)
502
+ tagsNone?: string[]; // 0.8 Cluster B — NOT (exclude all)
382
503
  stage?: string;
383
504
  retracted?: boolean;
384
505
  limit?: number;
385
506
  offset?: number;
507
+ sortBy?: 'createdAt' | 'sequence' | 'salience' | 'confidence' | 'lastAccessed';
508
+ sortOrder?: 'asc' | 'desc';
386
509
  };
387
510
 
388
- const results = store.search({
511
+ const results = await store.search({
389
512
  agentId: body.agentId,
390
513
  text: body.text,
391
514
  concept: body.concept,
392
515
  tags: body.tags,
516
+ tagsAll: body.tagsAll,
517
+ tagsAny: body.tagsAny,
518
+ tagsNone: body.tagsNone,
393
519
  stage: body.stage as any,
394
520
  retracted: body.retracted,
395
521
  limit: body.limit,
396
522
  offset: body.offset,
523
+ sortBy: body.sortBy,
524
+ sortOrder: body.sortOrder,
397
525
  });
398
526
 
399
527
  return reply.send({ results, count: results.length });
400
528
  });
401
529
 
530
+ // ============================================================
531
+ // 0.8 Cluster C — materialized-view + atomic-counter endpoints
532
+ // ============================================================
533
+
534
+ /**
535
+ * For each distinct value of `tagKey`, return the most recent active
536
+ * engram. Used by NovelForge for "latest emotional state per character",
537
+ * "latest motif phase per motif", etc.
538
+ */
539
+ app.post('/memory/latest-by-tag', async (req, reply) => {
540
+ const body = req.body as {
541
+ agentId: string;
542
+ tagKey: string; // e.g. "character=", "motif="
543
+ scopeTagsAll?: string[]; // optional narrowing
544
+ retracted?: boolean;
545
+ sortBy?: 'createdAt' | 'sequence';
546
+ limit?: number;
547
+ };
548
+ if (!body.agentId || !body.tagKey) {
549
+ return reply.code(400).send({ error: 'agentId and tagKey are required' });
550
+ }
551
+ const results = await store.getLatestByTag({
552
+ agentId: body.agentId,
553
+ tagKeyPrefix: body.tagKey,
554
+ scopeTagsAll: body.scopeTagsAll,
555
+ retracted: body.retracted ?? false,
556
+ sortBy: body.sortBy,
557
+ limit: body.limit,
558
+ });
559
+ return reply.send({ results, count: results.length });
560
+ });
561
+
562
+ /**
563
+ * Filter by tag-set operators, sort by numeric value extracted from a
564
+ * tag prefix, return top N. Used by NovelForge for "top N active
565
+ * promises by weight".
566
+ */
567
+ app.post('/memory/top-by', async (req, reply) => {
568
+ const body = req.body as {
569
+ agentId: string;
570
+ sortField: string; // tag prefix, e.g. "weight="
571
+ order?: 'asc' | 'desc'; // default desc
572
+ filterTagsAll?: string[];
573
+ filterTagsAny?: string[];
574
+ filterTagsNone?: string[];
575
+ retracted?: boolean;
576
+ limit?: number;
577
+ };
578
+ if (!body.agentId || !body.sortField) {
579
+ return reply.code(400).send({ error: 'agentId and sortField are required' });
580
+ }
581
+ const results = await store.getTopBy({
582
+ agentId: body.agentId,
583
+ sortField: body.sortField,
584
+ order: body.order ?? 'desc',
585
+ filterTagsAll: body.filterTagsAll,
586
+ filterTagsAny: body.filterTagsAny,
587
+ filterTagsNone: body.filterTagsNone,
588
+ retracted: body.retracted ?? false,
589
+ limit: body.limit,
590
+ });
591
+ return reply.send({ results, count: results.length });
592
+ });
593
+
594
+ /**
595
+ * Compute effective state of an engram from referenced events. Two
596
+ * targeting modes: by ID, or by concept match (same semantics as Form B's
597
+ * findActiveMatchByConcept).
598
+ */
599
+ app.post('/memory/resolve', async (req, reply) => {
600
+ const body = req.body as {
601
+ agentId: string;
602
+ targetEngramId?: string;
603
+ matchConcept?: string;
604
+ matchTags?: string[];
605
+ };
606
+ if (!body.agentId) {
607
+ return reply.code(400).send({ error: 'agentId is required' });
608
+ }
609
+
610
+ let targetId = body.targetEngramId;
611
+ if (!targetId && body.matchConcept) {
612
+ const matched = await store.findActiveMatchByConcept(
613
+ body.agentId, body.matchConcept, body.matchTags,
614
+ );
615
+ if (!matched) {
616
+ return reply.code(404).send({
617
+ error: `No active engram matches concept "${body.matchConcept}"`,
618
+ });
619
+ }
620
+ targetId = matched.id;
621
+ }
622
+ if (!targetId) {
623
+ return reply.code(400).send({
624
+ error: 'Provide either targetEngramId or matchConcept',
625
+ });
626
+ }
627
+
628
+ const result = await store.resolveEffectiveState(targetId);
629
+ if (!result) return reply.code(404).send({ error: `Engram ${targetId} not found` });
630
+ return reply.send(result);
631
+ });
632
+
633
+ /**
634
+ * Race-free next-sequence allocator. Caller writes the engram with the
635
+ * returned value in `sequence`. Doesn't reserve — concurrent allocations
636
+ * always serialize via BEGIN IMMEDIATE.
637
+ */
638
+ app.get('/memory/sequence/:agentId/next', async (req, reply) => {
639
+ const { agentId } = req.params as { agentId: string };
640
+ const next = await store.allocateNextSequence(agentId);
641
+ return reply.send({ agentId, next });
642
+ });
643
+
402
644
  app.get('/memory/:id', async (req, reply) => {
403
645
  const { id } = req.params as { id: string };
404
- const engram = store.getEngram(id);
646
+ const engram = await store.getEngram(id);
405
647
  if (!engram) return reply.code(404).send({ error: 'Not found' });
406
648
 
407
- const associations = store.getAssociationsFor(id);
649
+ const associations = await store.getAssociationsFor(id);
408
650
  return reply.send({ engram, associations });
409
651
  });
410
652
 
411
653
  app.get('/agent/:id/stats', async (req, reply) => {
412
654
  const { id } = req.params as { id: string };
413
- const active = store.getEngramsByAgent(id, 'active');
414
- const staging = store.getEngramsByAgent(id, 'staging');
415
- const retracted = store.getEngramsByAgent(id, undefined, true).filter(e => e.retracted);
416
- const associations = store.getAllAssociations(id);
655
+ const active = await store.getEngramsByAgent(id, 'active');
656
+ const staging = await store.getEngramsByAgent(id, 'staging');
657
+ const retracted = (await store.getEngramsByAgent(id, undefined, true)).filter(e => e.retracted);
658
+ const associations = await store.getAllAssociations(id);
417
659
 
418
660
  return reply.send({
419
661
  agentId: id,
@@ -433,7 +675,7 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
433
675
  app.get('/agent/:id/metrics', async (req, reply) => {
434
676
  const { id } = req.params as { id: string };
435
677
  const windowHours = parseInt((req.query as any).window ?? '24', 10);
436
- const metrics = evalEngine.computeMetrics(id, windowHours);
678
+ const metrics = await evalEngine.computeMetrics(id, windowHours);
437
679
  return reply.send({ metrics });
438
680
  });
439
681
 
@@ -453,13 +695,13 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
453
695
 
454
696
  app.post('/system/evict', async (req, reply) => {
455
697
  const body = req.body as { agentId: string };
456
- const result = evictionEngine.enforceCapacity(body.agentId, DEFAULT_AGENT_CONFIG);
698
+ const result = await evictionEngine.enforceCapacity(body.agentId, DEFAULT_AGENT_CONFIG);
457
699
  return reply.send(result);
458
700
  });
459
701
 
460
702
  app.post('/system/decay', async (req, reply) => {
461
703
  const body = req.body as { agentId: string; halfLifeDays?: number };
462
- const decayed = evictionEngine.decayEdges(body.agentId, body.halfLifeDays);
704
+ const decayed = await evictionEngine.decayEdges(body.agentId, body.halfLifeDays);
463
705
  return reply.send({ edgesDecayed: decayed });
464
706
  });
465
707
 
@@ -501,7 +743,7 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
501
743
 
502
744
  app.get('/memory/restore/:agentId', async (req, reply) => {
503
745
  const { agentId } = req.params as { agentId: string };
504
- const checkpoint = store.getCheckpoint(agentId);
746
+ const checkpoint = await store.getCheckpoint(agentId);
505
747
 
506
748
  const now = Date.now();
507
749
  const idleMs = checkpoint
@@ -511,7 +753,7 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
511
753
  // Get last written engram for context
512
754
  let lastWrite: { id: string; concept: string; content: string } | null = null;
513
755
  if (checkpoint?.auto.lastWriteId) {
514
- const engram = store.getEngram(checkpoint.auto.lastWriteId);
756
+ const engram = await store.getEngram(checkpoint.auto.lastWriteId);
515
757
  if (engram) {
516
758
  lastWrite = { id: engram.id, concept: engram.concept, content: engram.content };
517
759
  }
@@ -574,7 +816,7 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
574
816
  blockedBy?: string;
575
817
  };
576
818
 
577
- const engram = store.createEngram({
819
+ const engram = await store.createEngram({
578
820
  agentId: body.agentId,
579
821
  concept: body.concept,
580
822
  content: body.content,
@@ -592,8 +834,8 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
592
834
  });
593
835
 
594
836
  connectionEngine.enqueue(engram.id);
595
- embed(`${body.concept} ${body.content}`).then(vec => {
596
- store.updateEmbedding(engram.id, vec);
837
+ embed(`${body.concept} ${body.content}`).then(async vec => {
838
+ await store.updateEmbedding(engram.id, vec);
597
839
  }).catch(() => {});
598
840
 
599
841
  return reply.send(engram);
@@ -607,29 +849,29 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
607
849
  blockedBy?: string | null;
608
850
  };
609
851
 
610
- const engram = store.getEngram(body.taskId);
852
+ const engram = await store.getEngram(body.taskId);
611
853
  if (!engram || !engram.taskStatus) {
612
854
  return reply.code(404).send({ error: 'Task not found' });
613
855
  }
614
856
 
615
857
  if (body.blockedBy !== undefined) {
616
- store.updateBlockedBy(body.taskId, body.blockedBy);
858
+ await store.updateBlockedBy(body.taskId, body.blockedBy);
617
859
  }
618
860
  if (body.status) {
619
- store.updateTaskStatus(body.taskId, body.status);
861
+ await store.updateTaskStatus(body.taskId, body.status);
620
862
  }
621
863
  if (body.priority) {
622
- store.updateTaskPriority(body.taskId, body.priority);
864
+ await store.updateTaskPriority(body.taskId, body.priority);
623
865
  }
624
866
 
625
- return reply.send(store.getEngram(body.taskId));
867
+ return reply.send(await store.getEngram(body.taskId));
626
868
  });
627
869
 
628
870
  app.get('/task/list/:agentId', async (req, reply) => {
629
871
  const { agentId } = req.params as { agentId: string };
630
872
  const { status, includeDone } = req.query as { status?: TaskStatus; includeDone?: string };
631
873
 
632
- let tasks = store.getTasks(agentId, status);
874
+ let tasks = await store.getTasks(agentId, status);
633
875
  if (includeDone !== 'true' && !status) {
634
876
  tasks = tasks.filter(t => t.taskStatus !== 'done');
635
877
  }
@@ -639,7 +881,7 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
639
881
 
640
882
  app.get('/task/next/:agentId', async (req, reply) => {
641
883
  const { agentId } = req.params as { agentId: string };
642
- const next = store.getNextTask(agentId);
884
+ const next = await store.getNextTask(agentId);
643
885
  return reply.send(next ? { task: next } : { task: null, message: 'No actionable tasks' });
644
886
  });
645
887
 
@@ -647,7 +889,7 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
647
889
  app.post('/system/time-warp', async (req, reply) => {
648
890
  const body = req.body as { agentId: string; days: number };
649
891
  const ms = body.days * 24 * 60 * 60 * 1000;
650
- const shifted = store.timeWarp(body.agentId, ms);
892
+ const shifted = await store.timeWarp(body.agentId, ms);
651
893
  return reply.send({ shifted, days: body.days });
652
894
  });
653
895
 
@@ -656,7 +898,12 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
656
898
  app.get('/memory/export', async (req, reply) => {
657
899
  const { agentId, all } = req.query as { agentId?: string; all?: string };
658
900
  const includeAll = all === 'true';
659
- const db = store.getDb();
901
+ // /memory/export uses raw SQL — SQLite-only. On PGlite, callers should use
902
+ // the awm CLI export/merge tools instead.
903
+ if (typeof (store as any).getDb !== 'function') {
904
+ return reply.code(501).send({ error: 'export endpoint requires the SQLite backend' });
905
+ }
906
+ const db = (store as any).getDb();
660
907
 
661
908
  let engramSql = `SELECT id, agent_id, concept, content, confidence, salience, access_count,
662
909
  last_accessed, created_at, salience_features, reason_codes, stage, ttl,
@@ -705,12 +952,12 @@ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
705
952
  const base: Record<string, unknown> = {
706
953
  status: 'ok',
707
954
  timestamp: new Date().toISOString(),
708
- version: '0.7.16',
955
+ version: '0.8.5',
709
956
  coordination: coordEnabled,
710
957
  };
711
- if (coordEnabled) {
958
+ if (coordEnabled && typeof (deps.store as any).getDb === 'function') {
712
959
  try {
713
- const db = deps.store.getDb();
960
+ const db = (deps.store as any).getDb();
714
961
  const stats = db.prepare(`SELECT
715
962
  (SELECT COUNT(*) FROM coord_agents WHERE status != 'dead') AS agents_alive,
716
963
  (SELECT COUNT(*) FROM coord_assignments WHERE status = 'pending') AS pending_tasks,