ai-memory-layer 2.0.1 → 3.0.0

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/CHANGELOG.md +19 -12
  2. package/README.md +435 -320
  3. package/bin/memory-server.mjs +0 -0
  4. package/dist/adapters/memory/embeddings.d.ts.map +1 -1
  5. package/dist/adapters/memory/embeddings.js +12 -1
  6. package/dist/adapters/memory/embeddings.js.map +1 -1
  7. package/dist/adapters/memory/index.d.ts.map +1 -1
  8. package/dist/adapters/memory/index.js +1281 -48
  9. package/dist/adapters/memory/index.js.map +1 -1
  10. package/dist/adapters/postgres/index.d.ts +1 -0
  11. package/dist/adapters/postgres/index.d.ts.map +1 -1
  12. package/dist/adapters/postgres/index.js +1770 -42
  13. package/dist/adapters/postgres/index.js.map +1 -1
  14. package/dist/adapters/sqlite/embeddings.d.ts.map +1 -1
  15. package/dist/adapters/sqlite/embeddings.js +49 -12
  16. package/dist/adapters/sqlite/embeddings.js.map +1 -1
  17. package/dist/adapters/sqlite/index.d.ts.map +1 -1
  18. package/dist/adapters/sqlite/index.js +1720 -38
  19. package/dist/adapters/sqlite/index.js.map +1 -1
  20. package/dist/adapters/sqlite/mappers.d.ts +39 -4
  21. package/dist/adapters/sqlite/mappers.d.ts.map +1 -1
  22. package/dist/adapters/sqlite/mappers.js +87 -0
  23. package/dist/adapters/sqlite/mappers.js.map +1 -1
  24. package/dist/adapters/sqlite/schema.d.ts +1 -1
  25. package/dist/adapters/sqlite/schema.d.ts.map +1 -1
  26. package/dist/adapters/sqlite/schema.js +297 -1
  27. package/dist/adapters/sqlite/schema.js.map +1 -1
  28. package/dist/adapters/sync-to-async.d.ts.map +1 -1
  29. package/dist/adapters/sync-to-async.js +54 -0
  30. package/dist/adapters/sync-to-async.js.map +1 -1
  31. package/dist/contracts/async-storage.d.ts +61 -1
  32. package/dist/contracts/async-storage.d.ts.map +1 -1
  33. package/dist/contracts/cognitive.d.ts +37 -0
  34. package/dist/contracts/cognitive.d.ts.map +1 -0
  35. package/dist/contracts/cognitive.js +24 -0
  36. package/dist/contracts/cognitive.js.map +1 -0
  37. package/dist/contracts/coordination.d.ts +101 -0
  38. package/dist/contracts/coordination.d.ts.map +1 -0
  39. package/dist/contracts/coordination.js +26 -0
  40. package/dist/contracts/coordination.js.map +1 -0
  41. package/dist/contracts/embedding.d.ts +1 -1
  42. package/dist/contracts/embedding.d.ts.map +1 -1
  43. package/dist/contracts/errors.d.ts +28 -0
  44. package/dist/contracts/errors.d.ts.map +1 -0
  45. package/dist/contracts/errors.js +41 -0
  46. package/dist/contracts/errors.js.map +1 -0
  47. package/dist/contracts/identity.d.ts +2 -0
  48. package/dist/contracts/identity.d.ts.map +1 -1
  49. package/dist/contracts/identity.js +26 -1
  50. package/dist/contracts/identity.js.map +1 -1
  51. package/dist/contracts/observability.d.ts +2 -1
  52. package/dist/contracts/observability.d.ts.map +1 -1
  53. package/dist/contracts/observability.js +11 -0
  54. package/dist/contracts/observability.js.map +1 -1
  55. package/dist/contracts/profile.d.ts +29 -0
  56. package/dist/contracts/profile.d.ts.map +1 -0
  57. package/dist/contracts/profile.js +2 -0
  58. package/dist/contracts/profile.js.map +1 -0
  59. package/dist/contracts/session-state.d.ts +10 -0
  60. package/dist/contracts/session-state.d.ts.map +1 -0
  61. package/dist/contracts/session-state.js +2 -0
  62. package/dist/contracts/session-state.js.map +1 -0
  63. package/dist/contracts/storage.d.ts +73 -1
  64. package/dist/contracts/storage.d.ts.map +1 -1
  65. package/dist/contracts/storage.js +16 -1
  66. package/dist/contracts/storage.js.map +1 -1
  67. package/dist/contracts/temporal.d.ts +112 -0
  68. package/dist/contracts/temporal.d.ts.map +1 -0
  69. package/dist/contracts/temporal.js +31 -0
  70. package/dist/contracts/temporal.js.map +1 -0
  71. package/dist/contracts/types.d.ts +135 -0
  72. package/dist/contracts/types.d.ts.map +1 -1
  73. package/dist/contracts/types.js +27 -0
  74. package/dist/contracts/types.js.map +1 -1
  75. package/dist/core/associations.d.ts +18 -0
  76. package/dist/core/associations.d.ts.map +1 -0
  77. package/dist/core/associations.js +185 -0
  78. package/dist/core/associations.js.map +1 -0
  79. package/dist/core/circuit-breaker.d.ts +9 -0
  80. package/dist/core/circuit-breaker.d.ts.map +1 -1
  81. package/dist/core/circuit-breaker.js +13 -1
  82. package/dist/core/circuit-breaker.js.map +1 -1
  83. package/dist/core/cognitive.d.ts +5 -0
  84. package/dist/core/cognitive.d.ts.map +1 -0
  85. package/dist/core/cognitive.js +120 -0
  86. package/dist/core/cognitive.js.map +1 -0
  87. package/dist/core/context.d.ts +72 -1
  88. package/dist/core/context.d.ts.map +1 -1
  89. package/dist/core/context.js +471 -45
  90. package/dist/core/context.js.map +1 -1
  91. package/dist/core/episodic.d.ts +28 -0
  92. package/dist/core/episodic.d.ts.map +1 -0
  93. package/dist/core/episodic.js +371 -0
  94. package/dist/core/episodic.js.map +1 -0
  95. package/dist/core/formatter.d.ts +4 -0
  96. package/dist/core/formatter.d.ts.map +1 -1
  97. package/dist/core/formatter.js +103 -0
  98. package/dist/core/formatter.js.map +1 -1
  99. package/dist/core/maintenance.d.ts +1 -0
  100. package/dist/core/maintenance.d.ts.map +1 -1
  101. package/dist/core/maintenance.js +75 -0
  102. package/dist/core/maintenance.js.map +1 -1
  103. package/dist/core/manager.d.ts +159 -7
  104. package/dist/core/manager.d.ts.map +1 -1
  105. package/dist/core/manager.js +740 -31
  106. package/dist/core/manager.js.map +1 -1
  107. package/dist/core/orchestrator.d.ts.map +1 -1
  108. package/dist/core/orchestrator.js +210 -178
  109. package/dist/core/orchestrator.js.map +1 -1
  110. package/dist/core/playbook.d.ts +35 -0
  111. package/dist/core/playbook.d.ts.map +1 -0
  112. package/dist/core/playbook.js +184 -0
  113. package/dist/core/playbook.js.map +1 -0
  114. package/dist/core/profile.d.ts +8 -0
  115. package/dist/core/profile.d.ts.map +1 -0
  116. package/dist/core/profile.js +103 -0
  117. package/dist/core/profile.js.map +1 -0
  118. package/dist/core/quick.d.ts +5 -0
  119. package/dist/core/quick.d.ts.map +1 -1
  120. package/dist/core/quick.js +10 -1
  121. package/dist/core/quick.js.map +1 -1
  122. package/dist/core/runtime.d.ts +17 -1
  123. package/dist/core/runtime.d.ts.map +1 -1
  124. package/dist/core/runtime.js +88 -5
  125. package/dist/core/runtime.js.map +1 -1
  126. package/dist/core/streaming.d.ts +1 -1
  127. package/dist/core/streaming.d.ts.map +1 -1
  128. package/dist/core/temporal.d.ts +29 -0
  129. package/dist/core/temporal.d.ts.map +1 -0
  130. package/dist/core/temporal.js +447 -0
  131. package/dist/core/temporal.js.map +1 -0
  132. package/dist/core/validation.d.ts +3 -0
  133. package/dist/core/validation.d.ts.map +1 -1
  134. package/dist/core/validation.js +25 -10
  135. package/dist/core/validation.js.map +1 -1
  136. package/dist/core/workspace-detect.d.ts +17 -0
  137. package/dist/core/workspace-detect.d.ts.map +1 -0
  138. package/dist/core/workspace-detect.js +55 -0
  139. package/dist/core/workspace-detect.js.map +1 -0
  140. package/dist/embeddings/resilience.d.ts.map +1 -1
  141. package/dist/embeddings/resilience.js +19 -8
  142. package/dist/embeddings/resilience.js.map +1 -1
  143. package/dist/index.d.ts +21 -4
  144. package/dist/index.d.ts.map +1 -1
  145. package/dist/index.js +9 -0
  146. package/dist/index.js.map +1 -1
  147. package/dist/integrations/claude-agent.d.ts +6 -0
  148. package/dist/integrations/claude-agent.d.ts.map +1 -1
  149. package/dist/integrations/claude-agent.js +5 -1
  150. package/dist/integrations/claude-agent.js.map +1 -1
  151. package/dist/integrations/claude-tools.d.ts +5 -4
  152. package/dist/integrations/claude-tools.d.ts.map +1 -1
  153. package/dist/integrations/claude-tools.js +155 -2
  154. package/dist/integrations/claude-tools.js.map +1 -1
  155. package/dist/integrations/middleware.d.ts +6 -0
  156. package/dist/integrations/middleware.d.ts.map +1 -1
  157. package/dist/integrations/middleware.js +11 -1
  158. package/dist/integrations/middleware.js.map +1 -1
  159. package/dist/integrations/openai-tools.d.ts +5 -4
  160. package/dist/integrations/openai-tools.d.ts.map +1 -1
  161. package/dist/integrations/openai-tools.js +170 -2
  162. package/dist/integrations/openai-tools.js.map +1 -1
  163. package/dist/integrations/vercel-ai.d.ts +6 -0
  164. package/dist/integrations/vercel-ai.d.ts.map +1 -1
  165. package/dist/integrations/vercel-ai.js +4 -0
  166. package/dist/integrations/vercel-ai.js.map +1 -1
  167. package/dist/server/http-server.d.ts +8 -0
  168. package/dist/server/http-server.d.ts.map +1 -1
  169. package/dist/server/http-server.js +976 -58
  170. package/dist/server/http-server.js.map +1 -1
  171. package/dist/server/mcp-server.d.ts +8 -0
  172. package/dist/server/mcp-server.d.ts.map +1 -1
  173. package/dist/server/mcp-server.js +1157 -37
  174. package/dist/server/mcp-server.js.map +1 -1
  175. package/dist/server/parsing.d.ts +12 -0
  176. package/dist/server/parsing.d.ts.map +1 -0
  177. package/dist/server/parsing.js +42 -0
  178. package/dist/server/parsing.js.map +1 -0
  179. package/dist/summarizers/prompts.d.ts +4 -0
  180. package/dist/summarizers/prompts.d.ts.map +1 -1
  181. package/dist/summarizers/prompts.js +42 -0
  182. package/dist/summarizers/prompts.js.map +1 -1
  183. package/docs/ULTIMATE_MEMORY_LAYER_ROADMAP.md +291 -0
  184. package/docs/prd.json +1498 -0
  185. package/openapi.yaml +1945 -112
  186. package/package.json +4 -2
@@ -1,8 +1,14 @@
1
+ import { createHash, timingSafeEqual } from 'crypto';
1
2
  import { createServer } from 'http';
2
3
  import { createMemoryWithAsyncAdapter, } from '../core/quick.js';
3
4
  import { normalizeScope } from '../contracts/identity.js';
5
+ import { isMemoryDomainError } from '../contracts/errors.js';
6
+ import { MEMORY_EVENT_TYPES, } from '../contracts/observability.js';
7
+ import { EPISODE_DETAIL_LEVELS, PLAYBOOK_STATUSES, ASSOCIATION_TYPES, ASSOCIATION_TARGET_KINDS } from '../contracts/types.js';
8
+ import { ACTOR_KINDS, CONTEXT_VIEW_POLICIES, MEMORY_VISIBILITY_CLASSES } from '../contracts/coordination.js';
4
9
  import { createSQLiteAdapterWithEmbeddings } from '../adapters/sqlite/index.js';
5
10
  import { wrapSyncAdapter } from '../adapters/sync-to-async.js';
11
+ import { parseOptionalFiniteInteger, parseOptionalFiniteNumber, parseOptionalTemporalIdValue, } from './parsing.js';
6
12
  class HttpRequestError extends Error {
7
13
  status;
8
14
  constructor(status, message) {
@@ -10,6 +16,40 @@ class HttpRequestError extends Error {
10
16
  this.status = status;
11
17
  }
12
18
  }
19
+ const SESSION_SNAPSHOT_LIMIT = 1000;
20
+ const PER_SCOPE_SESSION_SNAPSHOT_LIMIT = 10;
21
+ const MANAGER_CACHE_LIMIT = 256;
22
+ const SESSION_MANAGER_CACHE_LIMIT = 256;
23
+ const MAX_LIST_LIMIT = 100;
24
+ const DEFAULT_DIFF_MAX_EVENTS = 5000;
25
+ const MAX_DIFF_MAX_EVENTS = 20000;
26
+ function resolveDiffEventCaps(defaultMaxEvents, maxMaxEvents) {
27
+ const resolvedMax = maxMaxEvents ?? MAX_DIFF_MAX_EVENTS;
28
+ const resolvedDefault = defaultMaxEvents ?? DEFAULT_DIFF_MAX_EVENTS;
29
+ if (!Number.isInteger(resolvedMax) || resolvedMax < 1) {
30
+ throw new Error('memory-layer: maxDiffMaxEvents must be a positive integer');
31
+ }
32
+ if (!Number.isInteger(resolvedDefault) || resolvedDefault < 1) {
33
+ throw new Error('memory-layer: defaultDiffMaxEvents must be a positive integer');
34
+ }
35
+ if (resolvedDefault > resolvedMax) {
36
+ throw new Error('memory-layer: defaultDiffMaxEvents must not exceed maxDiffMaxEvents');
37
+ }
38
+ return {
39
+ defaultDiffMaxEvents: resolvedDefault,
40
+ maxDiffMaxEvents: resolvedMax,
41
+ };
42
+ }
43
+ function failHttpValidation(message) {
44
+ throw new HttpRequestError(400, message);
45
+ }
46
+ function safeSecretEquals(provided, expected) {
47
+ if (typeof provided !== 'string')
48
+ return false;
49
+ const providedBuffer = createHash('sha256').update(provided).digest();
50
+ const expectedBuffer = createHash('sha256').update(expected).digest();
51
+ return timingSafeEqual(providedBuffer, expectedBuffer) && provided.length === expected.length;
52
+ }
13
53
  function writeJson(res, status, data) {
14
54
  const body = JSON.stringify(data);
15
55
  res.writeHead(status, {
@@ -21,6 +61,143 @@ function writeJson(res, status, data) {
21
61
  function writeError(res, status, message) {
22
62
  writeJson(res, status, { error: message });
23
63
  }
64
+ function serializeContextResponse(context, options = {}) {
65
+ return {
66
+ currentObjective: context.currentObjective,
67
+ sessionState: context.sessionState,
68
+ activeTurnCount: context.activeTurns.length,
69
+ workingMemory: context.workingMemory
70
+ ? {
71
+ summary: context.workingMemory.summary,
72
+ key_entities: context.workingMemory.key_entities,
73
+ topic_tags: context.workingMemory.topic_tags,
74
+ }
75
+ : null,
76
+ relevantKnowledge: context.relevantKnowledge.map((knowledge) => ({
77
+ id: knowledge.id,
78
+ fact: knowledge.fact,
79
+ fact_type: knowledge.fact_type,
80
+ confidence: knowledge.confidence,
81
+ })),
82
+ activeObjectives: context.activeObjectives.map((objective) => ({
83
+ id: objective.id,
84
+ title: objective.title,
85
+ status: objective.status,
86
+ visibility_class: objective.visibility_class,
87
+ })),
88
+ associatedKnowledge: options.includeAssociatedKnowledge === false
89
+ ? undefined
90
+ : context.associatedKnowledge.map((knowledge) => ({
91
+ id: knowledge.id,
92
+ fact: knowledge.fact,
93
+ fact_type: knowledge.fact_type,
94
+ knowledge_class: knowledge.knowledge_class,
95
+ trust_score: knowledge.trust_score,
96
+ })),
97
+ unresolvedWork: context.unresolvedWork,
98
+ coordinationState: context.coordinationState
99
+ ? {
100
+ ownedClaims: context.coordinationState.ownedClaims.map(serializeWorkClaim),
101
+ pendingInboundHandoffs: context.coordinationState.pendingInboundHandoffs.map(serializeHandoffRecord),
102
+ pendingOutboundHandoffs: context.coordinationState.pendingOutboundHandoffs.map(serializeHandoffRecord),
103
+ sharedWorkItems: context.coordinationState.sharedWorkItems.map((item) => ({
104
+ id: item.id,
105
+ title: item.title,
106
+ status: item.status,
107
+ visibility_class: item.visibility_class,
108
+ })),
109
+ }
110
+ : null,
111
+ tokenEstimate: context.tokenEstimate,
112
+ ...(options.includeDebug
113
+ ? {
114
+ debugTrace: context.debugTrace,
115
+ knowledgeSelectionReasons: context.knowledgeSelectionReasons,
116
+ }
117
+ : {}),
118
+ };
119
+ }
120
+ function serializeActorRef(actor) {
121
+ return {
122
+ actor_kind: actor.actor_kind,
123
+ actor_id: actor.actor_id,
124
+ system_id: actor.system_id,
125
+ display_name: actor.display_name,
126
+ metadata: actor.metadata,
127
+ };
128
+ }
129
+ function serializeWorkClaim(claim) {
130
+ return {
131
+ id: claim.id,
132
+ work_item_id: claim.work_item_id,
133
+ actor: serializeActorRef(claim.actor),
134
+ session_id: claim.session_id,
135
+ claim_token: claim.claim_token,
136
+ status: claim.status,
137
+ claimed_at: claim.claimed_at,
138
+ expires_at: claim.expires_at,
139
+ released_at: claim.released_at,
140
+ release_reason: claim.release_reason,
141
+ source_event_id: claim.source_event_id,
142
+ visibility_class: claim.visibility_class,
143
+ version: claim.version,
144
+ };
145
+ }
146
+ function serializeHandoffRecord(handoff) {
147
+ return {
148
+ id: handoff.id,
149
+ work_item_id: handoff.work_item_id,
150
+ from_actor: serializeActorRef(handoff.from_actor),
151
+ to_actor: serializeActorRef(handoff.to_actor),
152
+ session_id: handoff.session_id,
153
+ summary: handoff.summary,
154
+ context_bundle_ref: handoff.context_bundle_ref,
155
+ status: handoff.status,
156
+ created_at: handoff.created_at,
157
+ accepted_at: handoff.accepted_at,
158
+ rejected_at: handoff.rejected_at,
159
+ canceled_at: handoff.canceled_at,
160
+ expires_at: handoff.expires_at,
161
+ decision_reason: handoff.decision_reason,
162
+ source_event_id: handoff.source_event_id,
163
+ visibility_class: handoff.visibility_class,
164
+ version: handoff.version,
165
+ };
166
+ }
167
+ function serializeTimelineResult(result) {
168
+ return {
169
+ events: result.events,
170
+ nextCursor: result.nextCursor,
171
+ };
172
+ }
173
+ function serializeTemporalState(state, options = {}) {
174
+ return {
175
+ asOf: state.asOf,
176
+ exact: state.exact,
177
+ cutoverAt: state.cutoverAt,
178
+ watermarkEventId: state.watermarkEventId,
179
+ context: serializeContextResponse(state.context, {
180
+ includeDebug: options.includeDebug,
181
+ }),
182
+ sessionState: state.sessionState,
183
+ turns: state.turns,
184
+ workingMemory: state.workingMemory,
185
+ knowledge: state.knowledge,
186
+ workItems: state.workItems,
187
+ workClaims: state.workClaims.map(serializeWorkClaim),
188
+ handoffs: state.handoffs.map(serializeHandoffRecord),
189
+ coordinationState: state.coordinationState
190
+ ? {
191
+ ownedClaims: state.coordinationState.ownedClaims.map(serializeWorkClaim),
192
+ pendingInboundHandoffs: state.coordinationState.pendingInboundHandoffs.map(serializeHandoffRecord),
193
+ pendingOutboundHandoffs: state.coordinationState.pendingOutboundHandoffs.map(serializeHandoffRecord),
194
+ sharedWorkItems: state.coordinationState.sharedWorkItems,
195
+ }
196
+ : null,
197
+ associations: state.associations,
198
+ playbooks: state.playbooks,
199
+ };
200
+ }
24
201
  async function readBody(req, limitBytes = 1_048_576) {
25
202
  return new Promise((resolve, reject) => {
26
203
  const chunks = [];
@@ -80,6 +257,9 @@ function parseOptionalInteger(value) {
80
257
  const parsed = Number(value);
81
258
  return Number.isInteger(parsed) ? parsed : undefined;
82
259
  }
260
+ function parseOptionalTemporalId(value) {
261
+ return parseOptionalTemporalIdValue(value, 'cursor', failHttpValidation);
262
+ }
83
263
  function normalizePath(path) {
84
264
  if (path.length > 1 && path.endsWith('/')) {
85
265
  return path.slice(0, -1);
@@ -109,13 +289,51 @@ function requireEnum(value, allowed, name) {
109
289
  }
110
290
  return value;
111
291
  }
292
+ function parseContextViewPolicy(value, name = 'view') {
293
+ return value ? requireEnum(value, CONTEXT_VIEW_POLICIES, name) : undefined;
294
+ }
295
+ function parseViewerFromQuery(query) {
296
+ if (query.viewer_actor_id == null && query.viewer_actor_kind == null)
297
+ return undefined;
298
+ return parseActorRef({
299
+ actor_kind: query.viewer_actor_kind,
300
+ actor_id: query.viewer_actor_id,
301
+ system_id: query.viewer_system_id,
302
+ display_name: query.viewer_display_name,
303
+ }, 'viewer');
304
+ }
305
+ function parseActorRef(value, name = 'actor') {
306
+ if (value == null)
307
+ return undefined;
308
+ if (!isRecord(value)) {
309
+ throw new HttpRequestError(400, `Invalid field: ${name}`);
310
+ }
311
+ return {
312
+ actor_kind: requireEnum(value.actor_kind, ACTOR_KINDS, `${name}.actor_kind`),
313
+ actor_id: requireString(value.actor_id, `${name}.actor_id`),
314
+ system_id: value.system_id == null ? null : requireString(value.system_id, `${name}.system_id`),
315
+ display_name: value.display_name == null ? null : requireString(value.display_name, `${name}.display_name`),
316
+ metadata: isRecord(value.metadata) ? value.metadata : null,
317
+ };
318
+ }
112
319
  function parseLimit(value) {
113
320
  const parsed = parseOptionalInteger(value);
114
321
  if (value != null && parsed == null) {
115
322
  throw new HttpRequestError(400, 'Invalid limit parameter');
116
323
  }
324
+ if (parsed != null && parsed > MAX_LIST_LIMIT) {
325
+ throw new HttpRequestError(400, `Limit parameter exceeds maximum of ${MAX_LIST_LIMIT}`);
326
+ }
117
327
  return parsed;
118
328
  }
329
+ function parseOptionalNonNegativeInteger(value, name) {
330
+ if (value === undefined || value === null)
331
+ return undefined;
332
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
333
+ throw new HttpRequestError(400, `Invalid field: ${name} (must be a non-negative integer)`);
334
+ }
335
+ return value;
336
+ }
119
337
  function parseScopeLevel(value, name, allowed = ['scope', 'workspace', 'system', 'tenant']) {
120
338
  if (value == null || value === '')
121
339
  return undefined;
@@ -141,21 +359,11 @@ function resolvePartialScope(source, labels) {
141
359
  function parseEventTypes(value) {
142
360
  if (!value)
143
361
  return undefined;
144
- const allowed = [
145
- 'manager',
146
- 'search',
147
- 'compaction',
148
- 'extraction',
149
- 'promotion',
150
- 'knowledge_change',
151
- 'context_assembly',
152
- 'semantic_search',
153
- ];
154
362
  return new Set(value
155
363
  .split(',')
156
364
  .map((entry) => entry.trim())
157
365
  .filter(Boolean)
158
- .map((entry) => requireEnum(entry, allowed, 'event_types')));
366
+ .map((entry) => requireEnum(entry, MEMORY_EVENT_TYPES, 'event_types')));
159
367
  }
160
368
  function resolveRequestScope(fallbackScope, req, query, body) {
161
369
  const bodyScope = body?.scope;
@@ -190,6 +398,18 @@ function resolveRequestScope(fallbackScope, req, query, body) {
190
398
  }
191
399
  return fallbackScope ?? 'default';
192
400
  }
401
+ function materializeScope(scopeInput) {
402
+ return typeof scopeInput === 'string'
403
+ ? {
404
+ tenant_id: 'default',
405
+ system_id: 'default',
406
+ scope_id: scopeInput,
407
+ }
408
+ : scopeInput;
409
+ }
410
+ function cloneSnapshotValue(value) {
411
+ return structuredClone(value);
412
+ }
193
413
  function matchesEventScope(event, scope, level) {
194
414
  const left = normalizeScope(event.scope);
195
415
  const right = normalizeScope(scope);
@@ -197,6 +417,9 @@ function matchesEventScope(event, scope, level) {
197
417
  return false;
198
418
  if (level === 'tenant')
199
419
  return true;
420
+ // Collaboration scope is the explicit shared-memory boundary across systems,
421
+ // so workspace-level collaboration listeners intentionally fan out across
422
+ // system_id values when both sides are bound to the same collaboration.
200
423
  if (level === 'workspace' && left.collaboration_id && right.collaboration_id) {
201
424
  return left.collaboration_id === right.collaboration_id;
202
425
  }
@@ -233,7 +456,49 @@ export async function startHttpServer(config = {}) {
233
456
  const adminApiKey = config.adminApiKey ?? process.env.MEMORY_ADMIN_API_KEY;
234
457
  const enableCors = config.cors ?? true;
235
458
  const bodyLimitBytes = config.bodyLimitBytes ?? 1_048_576;
459
+ const { defaultDiffMaxEvents, maxDiffMaxEvents } = resolveDiffEventCaps(config.defaultDiffMaxEvents, config.maxDiffMaxEvents);
236
460
  const managers = new Map();
461
+ // Managers keyed by (scope, sessionId) for snapshot endpoints that must
462
+ // honor the URL-path session instead of the scope's bound default session.
463
+ const sessionManagers = new Map();
464
+ const sessionSnapshots = new Map();
465
+ function touchManagerCache(cache, key, manager, limit) {
466
+ cache.delete(key);
467
+ cache.set(key, manager);
468
+ while (cache.size > limit) {
469
+ const oldestKey = cache.keys().next().value;
470
+ if (!oldestKey)
471
+ break;
472
+ cache.delete(oldestKey);
473
+ }
474
+ }
475
+ function touchSnapshot(key, snapshot) {
476
+ const cachedSnapshot = cloneSnapshotValue(snapshot);
477
+ sessionSnapshots.delete(key);
478
+ sessionSnapshots.set(key, cachedSnapshot);
479
+ const scopeEntries = [...sessionSnapshots.entries()].filter(([, value]) => value.scopeKey === cachedSnapshot.scopeKey);
480
+ while (scopeEntries.length > PER_SCOPE_SESSION_SNAPSHOT_LIMIT) {
481
+ const [oldestKey] = scopeEntries.shift() ?? [];
482
+ if (!oldestKey)
483
+ break;
484
+ sessionSnapshots.delete(oldestKey);
485
+ }
486
+ while (sessionSnapshots.size > SESSION_SNAPSHOT_LIMIT) {
487
+ const oldest = sessionSnapshots.keys().next().value;
488
+ if (oldest === undefined)
489
+ break;
490
+ sessionSnapshots.delete(oldest);
491
+ }
492
+ }
493
+ function readSnapshot(key) {
494
+ const snapshot = sessionSnapshots.get(key);
495
+ if (!snapshot)
496
+ return undefined;
497
+ // Refresh LRU recency
498
+ sessionSnapshots.delete(key);
499
+ sessionSnapshots.set(key, snapshot);
500
+ return cloneSnapshotValue(snapshot);
501
+ }
237
502
  const databaseUrl = config.databaseUrl ?? process.env.MEMORY_DATABASE_URL;
238
503
  const adapterResources = databaseUrl
239
504
  ? await (async () => {
@@ -244,7 +509,7 @@ export async function startHttpServer(config = {}) {
244
509
  const { createPostgresAdapter, createPostgresEmbeddingAdapter } = await import('../adapters/postgres/index.js');
245
510
  const Pool = pgModule.Pool ?? pgModule.default?.Pool;
246
511
  const pool = new Pool({ connectionString: databaseUrl });
247
- const asyncAdapter = createPostgresAdapter(pool);
512
+ const asyncAdapter = createPostgresAdapter(pool, { ownsPool: false });
248
513
  return {
249
514
  asyncAdapter,
250
515
  embeddingAdapter: createPostgresEmbeddingAdapter(pool),
@@ -264,11 +529,10 @@ export async function startHttpServer(config = {}) {
264
529
  };
265
530
  })();
266
531
  const sseClients = new Set();
267
- function createHostedManager(scopeInput) {
268
- const baseOptions = {
269
- adapter: 'sqlite',
270
- path: config.dbPath ?? ':memory:',
532
+ function buildHostedManagerOptions(scopeInput, sessionId) {
533
+ return {
271
534
  scope: scopeInput,
535
+ ...(sessionId ? { sessionId } : {}),
272
536
  summarizer: config.summarizer ?? 'extractive',
273
537
  extractor: config.extractor ?? 'regex',
274
538
  preset: config.preset,
@@ -276,6 +540,13 @@ export async function startHttpServer(config = {}) {
276
540
  qualityMode: config.qualityMode,
277
541
  qualityTier: config.qualityTier,
278
542
  crossScopeLevel: config.crossScopeLevel,
543
+ autoDetectWorkspace: config.autoDetectWorkspace,
544
+ structuredClient: config.structuredClient,
545
+ };
546
+ }
547
+ function createHostedManager(scopeInput) {
548
+ const baseOptions = {
549
+ ...buildHostedManagerOptions(scopeInput),
279
550
  onEvent: (event) => {
280
551
  if (sseClients.size === 0)
281
552
  return;
@@ -295,6 +566,7 @@ export async function startHttpServer(config = {}) {
295
566
  ...baseOptions,
296
567
  asyncAdapter: adapterResources.asyncAdapter,
297
568
  embeddingAdapter: adapterResources.embeddingAdapter,
569
+ closeAdapter: false,
298
570
  });
299
571
  }
300
572
  function getManager(scopeInput) {
@@ -303,10 +575,38 @@ export async function startHttpServer(config = {}) {
303
575
  : JSON.stringify(normalizeScope(scopeInput));
304
576
  const existing = managers.get(key);
305
577
  if (existing) {
578
+ touchManagerCache(managers, key, existing, MANAGER_CACHE_LIMIT);
306
579
  return existing;
307
580
  }
308
581
  const manager = createHostedManager(scopeInput);
309
- managers.set(key, manager);
582
+ touchManagerCache(managers, key, manager, MANAGER_CACHE_LIMIT);
583
+ return manager;
584
+ }
585
+ function scopeKeyFor(scopeInput) {
586
+ return typeof scopeInput === 'string'
587
+ ? `scope:${scopeInput}`
588
+ : JSON.stringify(normalizeScope(scopeInput));
589
+ }
590
+ /**
591
+ * Get a manager bound to a specific sessionId under the given scope.
592
+ * Snapshot endpoints use this so POST/GET/REFRESH against different URL
593
+ * :sessionId values read from the correct session, not the scope's default.
594
+ */
595
+ function getSessionManager(scopeInput, sessionId) {
596
+ const key = `${scopeKeyFor(scopeInput)}|session:${sessionId}`;
597
+ const existing = sessionManagers.get(key);
598
+ if (existing) {
599
+ touchManagerCache(sessionManagers, key, existing, SESSION_MANAGER_CACHE_LIMIT);
600
+ return existing;
601
+ }
602
+ const baseOptions = buildHostedManagerOptions(scopeInput, sessionId);
603
+ const manager = createMemoryWithAsyncAdapter({
604
+ ...baseOptions,
605
+ asyncAdapter: adapterResources.asyncAdapter,
606
+ embeddingAdapter: adapterResources.embeddingAdapter,
607
+ closeAdapter: false,
608
+ });
609
+ touchManagerCache(sessionManagers, key, manager, SESSION_MANAGER_CACHE_LIMIT);
310
610
  return manager;
311
611
  }
312
612
  const manager = getManager(config.scope ?? 'default');
@@ -325,7 +625,7 @@ export async function startHttpServer(config = {}) {
325
625
  // Auth
326
626
  if (apiKey) {
327
627
  const auth = req.headers.authorization;
328
- if (!auth || auth !== `Bearer ${apiKey}`) {
628
+ if (!safeSecretEquals(auth, `Bearer ${apiKey}`)) {
329
629
  writeError(res, 401, 'Unauthorized');
330
630
  return;
331
631
  }
@@ -357,30 +657,132 @@ export async function startHttpServer(config = {}) {
357
657
  // GET /v1/context
358
658
  if (path === '/v1/context' && req.method === 'GET') {
359
659
  const requestManager = getManager(resolveRequestScope(config.scope, req, query));
360
- const context = await requestManager.getContext(query.query || undefined);
361
- writeJson(res, 200, {
362
- currentObjective: context.currentObjective,
363
- activeTurnCount: context.activeTurns.length,
364
- workingMemory: context.workingMemory
365
- ? {
366
- summary: context.workingMemory.summary,
367
- key_entities: context.workingMemory.key_entities,
368
- topic_tags: context.workingMemory.topic_tags,
369
- }
370
- : null,
371
- relevantKnowledge: context.relevantKnowledge.map((k) => ({
372
- id: k.id,
373
- fact: k.fact,
374
- fact_type: k.fact_type,
375
- confidence: k.confidence,
376
- })),
377
- activeObjectives: context.activeObjectives.map((o) => ({
378
- title: o.title,
379
- status: o.status,
380
- })),
381
- unresolvedWork: context.unresolvedWork,
382
- tokenEstimate: context.tokenEstimate,
660
+ const context = await requestManager.getContext(query.query || undefined, {
661
+ view: parseContextViewPolicy(query.view),
662
+ viewer: parseViewerFromQuery(query),
663
+ includeCoordinationState: query.include_coordination === 'true',
664
+ });
665
+ writeJson(res, 200, serializeContextResponse(context, {
666
+ includeDebug: query.debug === 'true',
667
+ }));
668
+ return;
669
+ }
670
+ // GET /v1/state
671
+ if (path === '/v1/state' && req.method === 'GET') {
672
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query));
673
+ const asOf = parseOptionalFiniteNumber(query.as_of, { name: 'as_of' }, failHttpValidation);
674
+ if (asOf == null) {
675
+ writeError(res, 400, 'Missing or invalid as_of parameter');
676
+ return;
677
+ }
678
+ const state = await requestManager.getStateAt(asOf, {
679
+ relevanceQuery: query.query || undefined,
680
+ view: parseContextViewPolicy(query.view),
681
+ viewer: parseViewerFromQuery(query),
682
+ includeCoordinationState: query.include_coordination === 'true',
683
+ });
684
+ writeJson(res, 200, serializeTemporalState(state, {
685
+ includeDebug: query.include_debug === 'true',
686
+ }));
687
+ return;
688
+ }
689
+ // GET /v1/timeline
690
+ if (path === '/v1/timeline' && req.method === 'GET') {
691
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query));
692
+ const startAt = parseOptionalFiniteNumber(query.start_at, { name: 'start_at' }, failHttpValidation);
693
+ const endAt = parseOptionalFiniteNumber(query.end_at, { name: 'end_at' }, failHttpValidation);
694
+ const cursor = parseOptionalTemporalId(query.cursor);
695
+ const limit = parseLimit(query.limit);
696
+ const timeline = await requestManager.getTimeline({
697
+ sessionId: query.session_id || undefined,
698
+ entityKind: query.entity_kind,
699
+ entityId: query.entity_id || undefined,
700
+ startAt,
701
+ endAt,
702
+ limit,
703
+ cursor,
704
+ });
705
+ writeJson(res, 200, serializeTimelineResult(timeline));
706
+ return;
707
+ }
708
+ // GET /v1/state/diff
709
+ if (path === '/v1/state/diff' && req.method === 'GET') {
710
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query));
711
+ const from = parseOptionalFiniteNumber(query.from, { name: 'from' }, failHttpValidation);
712
+ const to = parseOptionalFiniteNumber(query.to, { name: 'to' }, failHttpValidation);
713
+ const maxEvents = parseOptionalFiniteInteger(query.max_events, { name: 'max_events', min: 1, max: maxDiffMaxEvents }, failHttpValidation);
714
+ if (from == null || to == null) {
715
+ writeError(res, 400, 'Missing or invalid from/to parameters');
716
+ return;
717
+ }
718
+ const diff = await requestManager.diffState(from, to, {
719
+ sessionId: query.session_id || undefined,
720
+ entityKind: query.entity_kind,
721
+ entityId: query.entity_id || undefined,
722
+ maxEvents: maxEvents ?? defaultDiffMaxEvents,
723
+ });
724
+ writeJson(res, 200, diff);
725
+ return;
726
+ }
727
+ // GET /v1/events/log
728
+ if (path === '/v1/events/log' && req.method === 'GET') {
729
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query));
730
+ const startAt = parseOptionalFiniteNumber(query.start_at, { name: 'start_at' }, failHttpValidation);
731
+ const endAt = parseOptionalFiniteNumber(query.end_at, { name: 'end_at' }, failHttpValidation);
732
+ const cursor = parseOptionalTemporalId(query.cursor);
733
+ const limit = parseLimit(query.limit);
734
+ const events = await requestManager.listMemoryEvents({
735
+ sessionId: query.session_id || undefined,
736
+ entityKind: query.entity_kind,
737
+ entityId: query.entity_id || undefined,
738
+ startAt,
739
+ endAt,
740
+ limit,
741
+ cursor,
383
742
  });
743
+ writeJson(res, 200, serializeTimelineResult(events));
744
+ return;
745
+ }
746
+ // GET /v1/changes/stream
747
+ if (path === '/v1/changes/stream' && req.method === 'GET') {
748
+ res.writeHead(200, {
749
+ 'Content-Type': 'text/event-stream',
750
+ 'Cache-Control': 'no-cache',
751
+ Connection: 'keep-alive',
752
+ });
753
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query));
754
+ let closed = false;
755
+ const abortController = new AbortController();
756
+ req.on('close', () => {
757
+ closed = true;
758
+ abortController.abort();
759
+ });
760
+ const cursor = parseOptionalTemporalId(query.cursor);
761
+ const initialCursor = await requestManager.resolveChangeStreamCursor(cursor);
762
+ const iterator = requestManager.streamChanges({
763
+ cursor,
764
+ sessionId: query.session_id || undefined,
765
+ entityKind: query.entity_kind,
766
+ entityId: query.entity_id || undefined,
767
+ pollIntervalMs: 250,
768
+ signal: abortController.signal,
769
+ });
770
+ void (async () => {
771
+ res.write(`data: ${JSON.stringify({ type: 'connected', cursor: initialCursor })}\n\n`);
772
+ try {
773
+ for await (const event of iterator) {
774
+ if (closed)
775
+ break;
776
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
777
+ }
778
+ }
779
+ catch (error) {
780
+ if (!closed) {
781
+ res.write(`data: ${JSON.stringify({ type: 'error', error: String(error) })}\n\n`);
782
+ res.end();
783
+ }
784
+ }
785
+ })();
384
786
  return;
385
787
  }
386
788
  // GET /v1/search
@@ -435,7 +837,7 @@ export async function startHttpServer(config = {}) {
435
837
  // GET /v1/inspect/knowledge
436
838
  if (path === '/v1/inspect/knowledge' && req.method === 'GET') {
437
839
  const requestManager = getManager(resolveRequestScope(config.scope, req, query));
438
- const limit = parseOptionalInteger(query.limit);
840
+ const limit = parseLimit(query.limit);
439
841
  const cursor = parseOptionalInteger(query.cursor);
440
842
  if ((query.limit && limit == null) || (query.cursor && cursor == null)) {
441
843
  writeError(res, 400, 'Invalid pagination parameters');
@@ -463,7 +865,7 @@ export async function startHttpServer(config = {}) {
463
865
  if (path === '/v1/inspect/audits' && req.method === 'GET') {
464
866
  const requestManager = getManager(resolveRequestScope(config.scope, req, query));
465
867
  const knowledgeId = parseOptionalInteger(query.knowledge_id);
466
- const limit = parseOptionalInteger(query.limit);
868
+ const limit = parseLimit(query.limit);
467
869
  if ((query.knowledge_id && knowledgeId == null) || (query.limit && limit == null)) {
468
870
  writeError(res, 400, 'Invalid audit inspection parameters');
469
871
  return;
@@ -478,14 +880,17 @@ export async function startHttpServer(config = {}) {
478
880
  // GET /v1/inspect/monitor
479
881
  if (path === '/v1/inspect/monitor' && req.method === 'GET') {
480
882
  const requestManager = getManager(resolveRequestScope(config.scope, req, query));
481
- const monitor = await requestManager.getContextMonitor();
482
- writeJson(res, 200, { monitor });
883
+ const [monitor, diagnostics] = await Promise.all([
884
+ requestManager.getContextMonitor(),
885
+ requestManager.getRuntimeDiagnostics(),
886
+ ]);
887
+ writeJson(res, 200, { monitor, diagnostics });
483
888
  return;
484
889
  }
485
890
  // GET /v1/inspect/compactions
486
891
  if (path === '/v1/inspect/compactions' && req.method === 'GET') {
487
892
  const requestManager = getManager(resolveRequestScope(config.scope, req, query));
488
- const limit = parseOptionalInteger(query.limit);
893
+ const limit = parseLimit(query.limit);
489
894
  if (query.limit && limit == null) {
490
895
  writeError(res, 400, 'Invalid compaction inspection parameters');
491
896
  return;
@@ -494,6 +899,40 @@ export async function startHttpServer(config = {}) {
494
899
  writeJson(res, 200, { logs });
495
900
  return;
496
901
  }
902
+ // GET /v1/inspect/context
903
+ if (path === '/v1/inspect/context' && req.method === 'GET') {
904
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query));
905
+ const asOf = parseOptionalFiniteNumber(query.as_of, { name: 'as_of' }, failHttpValidation);
906
+ const context = asOf != null
907
+ ? await requestManager.getContextAt(asOf, query.query || undefined)
908
+ : await requestManager.getContext(query.query || undefined);
909
+ writeJson(res, 200, serializeContextResponse(context, { includeDebug: true }));
910
+ return;
911
+ }
912
+ // GET /v1/inspect/session-state
913
+ if (path === '/v1/inspect/session-state' && req.method === 'GET') {
914
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query));
915
+ const asOf = parseOptionalFiniteNumber(query.as_of, { name: 'as_of' }, failHttpValidation);
916
+ const context = asOf != null
917
+ ? await requestManager.getContextAt(asOf, query.query || undefined)
918
+ : await requestManager.getContext(query.query || undefined);
919
+ writeJson(res, 200, { sessionState: context.sessionState });
920
+ return;
921
+ }
922
+ // GET /v1/inspect/retrieval
923
+ if (path === '/v1/inspect/retrieval' && req.method === 'GET') {
924
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query));
925
+ const asOf = parseOptionalFiniteNumber(query.as_of, { name: 'as_of' }, failHttpValidation);
926
+ const context = asOf != null
927
+ ? await requestManager.getContextAt(asOf, query.query || undefined)
928
+ : await requestManager.getContext(query.query || undefined);
929
+ writeJson(res, 200, {
930
+ sessionState: context.sessionState,
931
+ knowledgeSelectionReasons: context.knowledgeSelectionReasons,
932
+ debugTrace: context.debugTrace,
933
+ });
934
+ return;
935
+ }
497
936
  // GET /v1/inspect/reverification
498
937
  if (path === '/v1/inspect/reverification' && req.method === 'GET') {
499
938
  const requestManager = getManager(resolveRequestScope(config.scope, req, query));
@@ -520,13 +959,134 @@ export async function startHttpServer(config = {}) {
520
959
  if (path === '/v1/work' && req.method === 'POST') {
521
960
  const body = await readBody(req, bodyLimitBytes);
522
961
  const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
523
- const item = await requestManager.trackWorkItem(requireString(body.title, 'title'), requireEnum(body.kind ?? 'objective', ['objective', 'unresolved_work', 'constraint'], 'kind'), requireEnum(body.status ?? 'open', ['open', 'in_progress', 'blocked', 'done'], 'status'), optionalString(body.detail, 'detail'));
962
+ const item = await requestManager.trackWorkItem(requireString(body.title, 'title'), requireEnum(body.kind ?? 'objective', ['objective', 'unresolved_work', 'constraint'], 'kind'), requireEnum(body.status ?? 'open', ['open', 'in_progress', 'blocked', 'done'], 'status'), optionalString(body.detail, 'detail'), {
963
+ visibilityClass: body.visibility_class == null
964
+ ? undefined
965
+ : requireEnum(body.visibility_class, MEMORY_VISIBILITY_CLASSES, 'visibility_class'),
966
+ });
524
967
  writeJson(res, 201, { workItemId: item.id });
525
968
  return;
526
969
  }
970
+ const workItemMatch = path.match(/^\/v1\/work-items\/(\d+)$/);
971
+ if (workItemMatch && req.method === 'POST') {
972
+ const body = await readBody(req, bodyLimitBytes);
973
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
974
+ const item = await requestManager.updateWorkItem(Number(workItemMatch[1]), {
975
+ title: body.title != null ? requireString(body.title, 'title') : undefined,
976
+ detail: body.detail != null ? optionalString(body.detail, 'detail') ?? null : undefined,
977
+ status: body.status != null
978
+ ? requireEnum(body.status, ['open', 'in_progress', 'blocked', 'done'], 'status')
979
+ : undefined,
980
+ visibility_class: body.visibility_class != null
981
+ ? requireEnum(body.visibility_class, MEMORY_VISIBILITY_CLASSES, 'visibility_class')
982
+ : undefined,
983
+ }, {
984
+ expectedVersion: parseOptionalFiniteInteger(body.expectedVersion, { name: 'expectedVersion', min: 0 }, failHttpValidation),
985
+ });
986
+ writeJson(res, 200, { workItem: item });
987
+ return;
988
+ }
989
+ const claimMatch = path.match(/^\/v1\/work-items\/(\d+)\/claim$/);
990
+ if (claimMatch && req.method === 'POST') {
991
+ const body = await readBody(req, bodyLimitBytes);
992
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
993
+ const actor = parseActorRef(body.actor, 'actor');
994
+ if (!actor) {
995
+ writeError(res, 400, 'Missing required field: actor');
996
+ return;
997
+ }
998
+ const claim = await requestManager.claimWorkItem({
999
+ workItemId: Number(claimMatch[1]),
1000
+ actor,
1001
+ leaseSeconds: parseOptionalFiniteInteger(body.leaseSeconds, { name: 'leaseSeconds', min: 1 }, failHttpValidation),
1002
+ });
1003
+ writeJson(res, 200, { claim: serializeWorkClaim(claim) });
1004
+ return;
1005
+ }
1006
+ const renewMatch = path.match(/^\/v1\/work-claims\/(\d+)\/renew$/);
1007
+ if (renewMatch && req.method === 'POST') {
1008
+ const body = await readBody(req, bodyLimitBytes);
1009
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
1010
+ const actor = parseActorRef(body.actor, 'actor');
1011
+ if (!actor) {
1012
+ writeError(res, 400, 'Missing required field: actor');
1013
+ return;
1014
+ }
1015
+ const claim = await requestManager.renewWorkClaim(Number(renewMatch[1]), actor, parseOptionalFiniteInteger(body.leaseSeconds, { name: 'leaseSeconds', min: 1 }, failHttpValidation));
1016
+ writeJson(res, 200, { claim: claim ? serializeWorkClaim(claim) : null });
1017
+ return;
1018
+ }
1019
+ const releaseMatch = path.match(/^\/v1\/work-claims\/(\d+)\/release$/);
1020
+ if (releaseMatch && req.method === 'POST') {
1021
+ const body = await readBody(req, bodyLimitBytes);
1022
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
1023
+ const actor = parseActorRef(body.actor, 'actor');
1024
+ if (!actor) {
1025
+ writeError(res, 400, 'Missing required field: actor');
1026
+ return;
1027
+ }
1028
+ const claim = await requestManager.releaseWorkClaim(Number(releaseMatch[1]), actor, optionalString(body.reason, 'reason'));
1029
+ writeJson(res, 200, { claim: claim ? serializeWorkClaim(claim) : null });
1030
+ return;
1031
+ }
1032
+ if (path === '/v1/work-claims' && req.method === 'GET') {
1033
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query));
1034
+ const claims = await requestManager.listWorkClaims();
1035
+ writeJson(res, 200, { claims: claims.map(serializeWorkClaim) });
1036
+ return;
1037
+ }
1038
+ const handoffCreateMatch = path.match(/^\/v1\/work-items\/(\d+)\/handoffs$/);
1039
+ if (handoffCreateMatch && req.method === 'POST') {
1040
+ const body = await readBody(req, bodyLimitBytes);
1041
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
1042
+ const fromActor = parseActorRef(body.from_actor, 'from_actor');
1043
+ const toActor = parseActorRef(body.to_actor, 'to_actor');
1044
+ if (!fromActor || !toActor) {
1045
+ writeError(res, 400, 'Missing required field: from_actor/to_actor');
1046
+ return;
1047
+ }
1048
+ const handoff = await requestManager.handoffWorkItem({
1049
+ workItemId: Number(handoffCreateMatch[1]),
1050
+ fromActor,
1051
+ toActor,
1052
+ summary: requireString(body.summary, 'summary'),
1053
+ contextBundleRef: optionalString(body.context_bundle_ref, 'context_bundle_ref') ?? null,
1054
+ expiresAt: parseOptionalFiniteInteger(body.expires_at, { name: 'expires_at', min: 0 }, failHttpValidation) ?? null,
1055
+ });
1056
+ writeJson(res, 201, { handoff: serializeHandoffRecord(handoff) });
1057
+ return;
1058
+ }
1059
+ const handoffActionMatch = path.match(/^\/v1\/handoffs\/(\d+)\/(accept|reject|cancel)$/);
1060
+ if (handoffActionMatch && req.method === 'POST') {
1061
+ const body = await readBody(req, bodyLimitBytes);
1062
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
1063
+ const actor = parseActorRef(body.actor, 'actor');
1064
+ if (!actor) {
1065
+ writeError(res, 400, 'Missing required field: actor');
1066
+ return;
1067
+ }
1068
+ const id = Number(handoffActionMatch[1]);
1069
+ const action = handoffActionMatch[2];
1070
+ const reason = optionalString(body.reason, 'reason');
1071
+ const handoff = action === 'accept'
1072
+ ? await requestManager.acceptHandoff(id, actor, reason)
1073
+ : action === 'reject'
1074
+ ? await requestManager.rejectHandoff(id, actor, reason)
1075
+ : await requestManager.cancelHandoff(id, actor, reason);
1076
+ writeJson(res, 200, { handoff: handoff ? serializeHandoffRecord(handoff) : null });
1077
+ return;
1078
+ }
1079
+ if (path === '/v1/handoffs' && req.method === 'GET') {
1080
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query));
1081
+ const handoffs = await requestManager.listPendingHandoffs({
1082
+ direction: query.direction ?? 'all',
1083
+ });
1084
+ writeJson(res, 200, { handoffs: handoffs.map(serializeHandoffRecord) });
1085
+ return;
1086
+ }
527
1087
  // POST /v1/compact
528
1088
  if (path === '/v1/compact' && req.method === 'POST') {
529
- if (adminApiKey && req.headers['x-admin-key'] !== adminApiKey) {
1089
+ if (adminApiKey && !safeSecretEquals(req.headers['x-admin-key'], adminApiKey)) {
530
1090
  writeError(res, 403, 'Admin key required');
531
1091
  return;
532
1092
  }
@@ -542,13 +1102,18 @@ export async function startHttpServer(config = {}) {
542
1102
  // GET /v1/health
543
1103
  if (path === '/v1/health' && req.method === 'GET') {
544
1104
  const requestManager = getManager(resolveRequestScope(config.scope, req, query));
545
- const context = await requestManager.getContext();
1105
+ const [context, diagnostics] = await Promise.all([
1106
+ requestManager.getContext(),
1107
+ requestManager.getRuntimeDiagnostics(),
1108
+ ]);
546
1109
  writeJson(res, 200, {
547
1110
  activeTurnCount: context.activeTurns.length,
548
1111
  tokenEstimate: context.tokenEstimate,
549
1112
  knowledgeCount: context.relevantKnowledge.length,
550
1113
  objectiveCount: context.activeObjectives.length,
551
1114
  unresolvedWorkCount: context.unresolvedWork.length,
1115
+ sessionStateUpdatedAt: context.sessionState.updatedAt,
1116
+ circuitBreakers: diagnostics.circuitBreakers,
552
1117
  });
553
1118
  return;
554
1119
  }
@@ -558,7 +1123,7 @@ export async function startHttpServer(config = {}) {
558
1123
  }
559
1124
  // POST /v1/maintenance
560
1125
  if (path === '/v1/maintenance' && req.method === 'POST') {
561
- if (adminApiKey && req.headers['x-admin-key'] !== adminApiKey) {
1126
+ if (adminApiKey && !safeSecretEquals(req.headers['x-admin-key'], adminApiKey)) {
562
1127
  writeError(res, 403, 'Admin key required');
563
1128
  return;
564
1129
  }
@@ -569,12 +1134,13 @@ export async function startHttpServer(config = {}) {
569
1134
  expiredWorkingMemory: report.expiredWorkingMemoryIds.length,
570
1135
  retiredKnowledge: report.retiredKnowledgeIds.length,
571
1136
  deletedWorkItems: report.deletedWorkItemIds.length,
1137
+ deletedAssociationIds: report.deletedAssociationIds,
572
1138
  });
573
1139
  return;
574
1140
  }
575
1141
  const reverificationMatch = path.match(/^\/v1\/reverification\/(\d+)$/);
576
1142
  if (reverificationMatch && req.method === 'POST') {
577
- if (adminApiKey && req.headers['x-admin-key'] !== adminApiKey) {
1143
+ if (adminApiKey && !safeSecretEquals(req.headers['x-admin-key'], adminApiKey)) {
578
1144
  writeError(res, 403, 'Admin key required');
579
1145
  return;
580
1146
  }
@@ -585,7 +1151,7 @@ export async function startHttpServer(config = {}) {
585
1151
  }
586
1152
  // POST /v1/reverification/run
587
1153
  if (path === '/v1/reverification/run' && req.method === 'POST') {
588
- if (adminApiKey && req.headers['x-admin-key'] !== adminApiKey) {
1154
+ if (adminApiKey && !safeSecretEquals(req.headers['x-admin-key'], adminApiKey)) {
589
1155
  writeError(res, 403, 'Admin key required');
590
1156
  return;
591
1157
  }
@@ -637,7 +1203,7 @@ export async function startHttpServer(config = {}) {
637
1203
  const scope = resolveRequestScope(config.scope, req, query);
638
1204
  sseClients.add({
639
1205
  response: res,
640
- scope: typeof scope === 'string' ? undefined : scope,
1206
+ scope: materializeScope(scope),
641
1207
  scopeLevel: parseScopeLevel(query.scope_level, 'scope_level') ?? 'scope',
642
1208
  eventTypes: parseEventTypes(query.event_types),
643
1209
  });
@@ -650,6 +1216,352 @@ export async function startHttpServer(config = {}) {
650
1216
  });
651
1217
  return;
652
1218
  }
1219
+ // GET /v1/episodes
1220
+ if (path === '/v1/episodes' && req.method === 'GET') {
1221
+ if (!query.q) {
1222
+ writeError(res, 400, 'Missing required query parameter: q');
1223
+ return;
1224
+ }
1225
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query));
1226
+ const detailLevel = query.detail
1227
+ ? requireEnum(query.detail, EPISODE_DETAIL_LEVELS, 'detail')
1228
+ : undefined;
1229
+ const episodeStartAt = parseOptionalFiniteNumber(query.start_at, { name: 'start_at' }, failHttpValidation);
1230
+ const episodeEndAt = parseOptionalFiniteNumber(query.end_at, { name: 'end_at' }, failHttpValidation);
1231
+ const episodes = await requestManager.searchEpisodes({
1232
+ query: query.q,
1233
+ detailLevel,
1234
+ limit: parseLimit(query.limit),
1235
+ timeRange: episodeStartAt != null || episodeEndAt != null
1236
+ ? {
1237
+ start_at: episodeStartAt,
1238
+ end_at: episodeEndAt,
1239
+ }
1240
+ : undefined,
1241
+ });
1242
+ writeJson(res, 200, { episodes });
1243
+ return;
1244
+ }
1245
+ // POST /v1/episodes/summarize
1246
+ if (path === '/v1/episodes/summarize' && req.method === 'POST') {
1247
+ const body = await readBody(req, bodyLimitBytes);
1248
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
1249
+ const sessionId = requireString(body.session_id, 'session_id');
1250
+ const detailLevel = body.detailLevel
1251
+ ? requireEnum(body.detailLevel, EPISODE_DETAIL_LEVELS, 'detailLevel')
1252
+ : undefined;
1253
+ const summary = await requestManager.summarizeEpisode(sessionId, { detailLevel });
1254
+ writeJson(res, 200, { episode: summary });
1255
+ return;
1256
+ }
1257
+ // POST /v1/reflect
1258
+ if (path === '/v1/reflect' && req.method === 'POST') {
1259
+ const body = await readBody(req, bodyLimitBytes);
1260
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
1261
+ const reflectQuery = requireString(body.query, 'query');
1262
+ const detailLevel = body.detailLevel
1263
+ ? requireEnum(body.detailLevel, EPISODE_DETAIL_LEVELS, 'detailLevel')
1264
+ : undefined;
1265
+ const includeDeclarative = body.includeDeclarative != null ? Boolean(body.includeDeclarative) : undefined;
1266
+ const includeEpisodic = body.includeEpisodic != null ? Boolean(body.includeEpisodic) : undefined;
1267
+ const reflectLimit = parseOptionalNonNegativeInteger(body.limit, 'limit');
1268
+ const timeRange = isRecord(body.timeRange)
1269
+ ? {
1270
+ start_at: parseOptionalFiniteNumber(body.timeRange.start_at, { name: 'timeRange.start_at' }, failHttpValidation),
1271
+ end_at: parseOptionalFiniteNumber(body.timeRange.end_at, { name: 'timeRange.end_at' }, failHttpValidation),
1272
+ }
1273
+ : undefined;
1274
+ const result = await requestManager.reflect({
1275
+ query: reflectQuery,
1276
+ detailLevel,
1277
+ includeDeclarative,
1278
+ includeEpisodic,
1279
+ limit: reflectLimit,
1280
+ timeRange,
1281
+ });
1282
+ writeJson(res, 200, result);
1283
+ return;
1284
+ }
1285
+ // GET /v1/memory
1286
+ if (path === '/v1/memory' && req.method === 'GET') {
1287
+ if (!query.q) {
1288
+ writeError(res, 400, 'Missing required query parameter: q');
1289
+ return;
1290
+ }
1291
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query));
1292
+ const types = query.types
1293
+ ? query.types.split(',').map((t) => t.trim()).filter(Boolean)
1294
+ : undefined;
1295
+ const cogMinTrust = parseOptionalFiniteNumber(query.minimumTrustScore, { name: 'minimumTrustScore', min: 0, max: 1 }, failHttpValidation);
1296
+ const cogActiveOnly = query.activeOnly != null
1297
+ ? query.activeOnly === 'true'
1298
+ : undefined;
1299
+ const result = await requestManager.searchCognitive({
1300
+ query: query.q,
1301
+ types,
1302
+ limit: parseLimit(query.limit),
1303
+ minimumTrustScore: cogMinTrust,
1304
+ activeOnly: cogActiveOnly,
1305
+ });
1306
+ writeJson(res, 200, result);
1307
+ return;
1308
+ }
1309
+ // GET /v1/profile
1310
+ if (path === '/v1/profile' && req.method === 'GET') {
1311
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query));
1312
+ const validViews = ['user', 'operator', 'workspace'];
1313
+ const view = query.view
1314
+ ? requireEnum(query.view, validViews, 'view')
1315
+ : undefined;
1316
+ const validSections = ['identity', 'preferences', 'communication', 'constraints', 'workflows'];
1317
+ const sections = query.sections
1318
+ ? query.sections.split(',').map((s) => requireEnum(s.trim(), validSections, 'sections'))
1319
+ : undefined;
1320
+ const minTrust = parseOptionalFiniteNumber(query.min_trust, { name: 'min_trust', min: 0, max: 1 }, failHttpValidation);
1321
+ const includeProvisional = query.includeProvisional === 'true' ? true : undefined;
1322
+ const includeDisputed = query.includeDisputed === 'true' ? true : undefined;
1323
+ const profile = await requestManager.getProfile({
1324
+ view,
1325
+ sections,
1326
+ minimumTrustScore: minTrust,
1327
+ includeProvisional,
1328
+ includeDisputed,
1329
+ });
1330
+ writeJson(res, 200, { profile });
1331
+ return;
1332
+ }
1333
+ // POST /v1/playbooks
1334
+ if (path === '/v1/playbooks' && req.method === 'POST') {
1335
+ const body = await readBody(req, bodyLimitBytes);
1336
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
1337
+ const playbook = await requestManager.createPlaybook({
1338
+ title: requireString(body.title, 'title'),
1339
+ description: requireString(body.description, 'description'),
1340
+ instructions: requireString(body.instructions, 'instructions'),
1341
+ references: Array.isArray(body.references) ? body.references.map(String) : undefined,
1342
+ templates: Array.isArray(body.templates) ? body.templates.map(String) : undefined,
1343
+ scripts: Array.isArray(body.scripts) ? body.scripts.map(String) : undefined,
1344
+ assets: Array.isArray(body.assets) ? body.assets.map(String) : undefined,
1345
+ tags: Array.isArray(body.tags) ? body.tags.map(String) : undefined,
1346
+ status: body.status ? requireEnum(body.status, PLAYBOOK_STATUSES, 'status') : undefined,
1347
+ });
1348
+ writeJson(res, 201, { playbook });
1349
+ return;
1350
+ }
1351
+ // GET /v1/playbooks
1352
+ if (path === '/v1/playbooks' && req.method === 'GET') {
1353
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query));
1354
+ if (query.q) {
1355
+ const results = await requestManager.searchPlaybooks(query.q, query.limit ? { limit: parseLimit(query.limit) } : undefined);
1356
+ writeJson(res, 200, {
1357
+ playbooks: results.map((r) => ({ ...r.item, rank: r.rank })),
1358
+ });
1359
+ }
1360
+ else {
1361
+ const playbooks = await requestManager.listPlaybooks();
1362
+ writeJson(res, 200, { playbooks });
1363
+ }
1364
+ return;
1365
+ }
1366
+ // POST /v1/playbooks/from-task
1367
+ if (path === '/v1/playbooks/from-task' && req.method === 'POST') {
1368
+ const body = await readBody(req, bodyLimitBytes);
1369
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
1370
+ const playbook = await requestManager.createPlaybookFromTask({
1371
+ title: requireString(body.title, 'title'),
1372
+ description: requireString(body.description, 'description'),
1373
+ sessionId: requireString(body.sessionId, 'sessionId'),
1374
+ tags: Array.isArray(body.tags) ? body.tags.map(String) : undefined,
1375
+ sourceWorkingMemoryId: parseOptionalFiniteInteger(body.sourceWorkingMemoryId, { name: 'sourceWorkingMemoryId', min: 1 }, failHttpValidation),
1376
+ });
1377
+ writeJson(res, 201, { playbook });
1378
+ return;
1379
+ }
1380
+ // GET /v1/playbooks/:id
1381
+ const playbookGetMatch = path.match(/^\/v1\/playbooks\/(\d+)$/);
1382
+ if (playbookGetMatch && req.method === 'GET') {
1383
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query));
1384
+ const playbook = await requestManager.getPlaybook(Number(playbookGetMatch[1]));
1385
+ if (!playbook) {
1386
+ writeError(res, 404, 'Playbook not found');
1387
+ return;
1388
+ }
1389
+ writeJson(res, 200, { playbook });
1390
+ return;
1391
+ }
1392
+ // PUT /v1/playbooks/:id
1393
+ if (playbookGetMatch && req.method === 'PUT') {
1394
+ const body = await readBody(req, bodyLimitBytes);
1395
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
1396
+ const patch = {};
1397
+ if (body.title != null)
1398
+ patch.title = requireString(body.title, 'title');
1399
+ if (body.description != null)
1400
+ patch.description = requireString(body.description, 'description');
1401
+ if (body.instructions != null)
1402
+ patch.instructions = requireString(body.instructions, 'instructions');
1403
+ if (Array.isArray(body.references))
1404
+ patch.references = body.references.map(String);
1405
+ if (Array.isArray(body.templates))
1406
+ patch.templates = body.templates.map(String);
1407
+ if (Array.isArray(body.scripts))
1408
+ patch.scripts = body.scripts.map(String);
1409
+ if (Array.isArray(body.assets))
1410
+ patch.assets = body.assets.map(String);
1411
+ if (Array.isArray(body.tags))
1412
+ patch.tags = body.tags.map(String);
1413
+ if (body.status != null)
1414
+ patch.status = requireEnum(body.status, PLAYBOOK_STATUSES, 'status');
1415
+ const updated = await requestManager.updatePlaybook(Number(playbookGetMatch[1]), patch);
1416
+ if (!updated) {
1417
+ writeError(res, 404, 'Playbook not found');
1418
+ return;
1419
+ }
1420
+ writeJson(res, 200, { playbook: updated });
1421
+ return;
1422
+ }
1423
+ // POST /v1/playbooks/:id/revise
1424
+ const playbookReviseMatch = path.match(/^\/v1\/playbooks\/(\d+)\/revise$/);
1425
+ if (playbookReviseMatch && req.method === 'POST') {
1426
+ const body = await readBody(req, bodyLimitBytes);
1427
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
1428
+ const result = await requestManager.revisePlaybook(Number(playbookReviseMatch[1]), requireString(body.instructions, 'instructions'), requireString(body.revisionReason, 'revisionReason'), optionalString(body.sourceSessionId, 'sourceSessionId'));
1429
+ writeJson(res, 200, result);
1430
+ return;
1431
+ }
1432
+ // POST /v1/playbooks/:id/use
1433
+ const playbookUseMatch = path.match(/^\/v1\/playbooks\/(\d+)\/use$/);
1434
+ if (playbookUseMatch && req.method === 'POST') {
1435
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query));
1436
+ const playbookId = Number(playbookUseMatch[1]);
1437
+ await requestManager.recordPlaybookUse(playbookId);
1438
+ const playbook = await requestManager.getPlaybook(playbookId);
1439
+ writeJson(res, 200, { recorded: true, playbook });
1440
+ return;
1441
+ }
1442
+ // POST /v1/associations
1443
+ if (path === '/v1/associations' && req.method === 'POST') {
1444
+ const body = await readBody(req, bodyLimitBytes);
1445
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
1446
+ const sourceId = Number.isInteger(body.source_id) && body.source_id > 0
1447
+ ? body.source_id
1448
+ : (() => { throw new HttpRequestError(400, 'Missing or invalid field: source_id (must be positive integer)'); })();
1449
+ const targetId = Number.isInteger(body.target_id) && body.target_id > 0
1450
+ ? body.target_id
1451
+ : (() => { throw new HttpRequestError(400, 'Missing or invalid field: target_id (must be positive integer)'); })();
1452
+ let confidence;
1453
+ if (body.confidence !== undefined && body.confidence !== null) {
1454
+ if (typeof body.confidence !== 'number' || Number.isNaN(body.confidence) || body.confidence < 0 || body.confidence > 1) {
1455
+ throw new HttpRequestError(400, 'Invalid field: confidence (must be a number in [0, 1])');
1456
+ }
1457
+ confidence = body.confidence;
1458
+ }
1459
+ const association = await requestManager.addAssociation({
1460
+ source_kind: requireEnum(body.source_kind, ASSOCIATION_TARGET_KINDS, 'source_kind'),
1461
+ source_id: sourceId,
1462
+ target_kind: requireEnum(body.target_kind, ASSOCIATION_TARGET_KINDS, 'target_kind'),
1463
+ target_id: targetId,
1464
+ association_type: requireEnum(body.association_type, ASSOCIATION_TYPES, 'association_type'),
1465
+ confidence,
1466
+ auto_generated: typeof body.auto_generated === 'boolean' ? body.auto_generated : undefined,
1467
+ });
1468
+ writeJson(res, 201, { association });
1469
+ return;
1470
+ }
1471
+ // GET /v1/associations/:kind/:id
1472
+ const assocGetMatch = path.match(/^\/v1\/associations\/([a-z_]+)\/(\d+)$/);
1473
+ if (assocGetMatch && req.method === 'GET') {
1474
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query));
1475
+ const kind = requireEnum(assocGetMatch[1], ASSOCIATION_TARGET_KINDS, 'kind');
1476
+ const targetId = Number(assocGetMatch[2]);
1477
+ const result = await requestManager.getAssociations(kind, targetId);
1478
+ writeJson(res, 200, result);
1479
+ return;
1480
+ }
1481
+ // POST /v1/associations/traverse
1482
+ if (path === '/v1/associations/traverse' && req.method === 'POST') {
1483
+ const body = await readBody(req, bodyLimitBytes);
1484
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
1485
+ const kind = requireEnum(body.kind, ASSOCIATION_TARGET_KINDS, 'kind');
1486
+ const id = Number.isInteger(body.id) && body.id > 0
1487
+ ? body.id
1488
+ : (() => { throw new HttpRequestError(400, 'Missing or invalid field: id (must be positive integer)'); })();
1489
+ const maxDepth = parseOptionalNonNegativeInteger(body.maxDepth, 'maxDepth');
1490
+ const maxNodes = parseOptionalNonNegativeInteger(body.maxNodes, 'maxNodes');
1491
+ const graph = await requestManager.traverseAssociations(kind, id, { maxDepth, maxNodes });
1492
+ writeJson(res, 200, graph);
1493
+ return;
1494
+ }
1495
+ // DELETE /v1/associations/:id
1496
+ const assocDeleteMatch = path.match(/^\/v1\/associations\/(\d+)$/);
1497
+ if (assocDeleteMatch && req.method === 'DELETE') {
1498
+ const requestManager = getManager(resolveRequestScope(config.scope, req, query));
1499
+ await requestManager.removeAssociation(Number(assocDeleteMatch[1]));
1500
+ writeJson(res, 200, { deleted: true });
1501
+ return;
1502
+ }
1503
+ // POST /v1/sessions/:sessionId/snapshot — capture a frozen snapshot
1504
+ const snapshotCaptureMatch = path.match(/^\/v1\/sessions\/([^/]+)\/snapshot$/);
1505
+ if (snapshotCaptureMatch && req.method === 'POST') {
1506
+ const body = await readBody(req, bodyLimitBytes);
1507
+ const sessionId = decodeURIComponent(snapshotCaptureMatch[1]);
1508
+ const scopeInput = resolveRequestScope(config.scope, req, query, body);
1509
+ // Use session-aware manager so getContext/getSessionBootstrap read
1510
+ // the session named in the URL, not the scope's bound default.
1511
+ const requestManager = getSessionManager(scopeInput, sessionId);
1512
+ const scopeKey = scopeKeyFor(scopeInput);
1513
+ const relevanceQuery = typeof body.relevanceQuery === 'string' ? body.relevanceQuery : undefined;
1514
+ const snapshotData = await requestManager.captureSnapshot(relevanceQuery);
1515
+ const snapshot = {
1516
+ scopeKey,
1517
+ snapshotId: `snap-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
1518
+ bootstrap: snapshotData.bootstrap,
1519
+ context: snapshotData.context,
1520
+ frozenAt: snapshotData.frozenAt,
1521
+ watermarkEventId: snapshotData.watermarkEventId,
1522
+ };
1523
+ touchSnapshot(`${scopeKey}:${sessionId}`, snapshot);
1524
+ const { scopeKey: _scopeKey, ...publicSnapshot } = snapshot;
1525
+ writeJson(res, 201, { snapshot: { ...publicSnapshot, sessionId } });
1526
+ return;
1527
+ }
1528
+ // GET /v1/sessions/:sessionId/snapshot — fetch cached snapshot
1529
+ if (snapshotCaptureMatch && req.method === 'GET') {
1530
+ const sessionId = decodeURIComponent(snapshotCaptureMatch[1]);
1531
+ const scopeInput = resolveRequestScope(config.scope, req, query);
1532
+ const scopeKey = scopeKeyFor(scopeInput);
1533
+ const snapshot = readSnapshot(`${scopeKey}:${sessionId}`);
1534
+ if (!snapshot) {
1535
+ writeError(res, 404, 'Snapshot not found');
1536
+ return;
1537
+ }
1538
+ const { scopeKey: _scopeKey, ...publicSnapshot } = snapshot;
1539
+ writeJson(res, 200, { snapshot: { ...publicSnapshot, sessionId } });
1540
+ return;
1541
+ }
1542
+ // POST /v1/sessions/:sessionId/refresh — re-capture and replace
1543
+ const snapshotRefreshMatch = path.match(/^\/v1\/sessions\/([^/]+)\/refresh$/);
1544
+ if (snapshotRefreshMatch && req.method === 'POST') {
1545
+ const body = await readBody(req, bodyLimitBytes);
1546
+ const sessionId = decodeURIComponent(snapshotRefreshMatch[1]);
1547
+ const scopeInput = resolveRequestScope(config.scope, req, query, body);
1548
+ const requestManager = getSessionManager(scopeInput, sessionId);
1549
+ const scopeKey = scopeKeyFor(scopeInput);
1550
+ const relevanceQuery = typeof body.relevanceQuery === 'string' ? body.relevanceQuery : undefined;
1551
+ const snapshotData = await requestManager.captureSnapshot(relevanceQuery);
1552
+ const snapshot = {
1553
+ scopeKey,
1554
+ snapshotId: `snap-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
1555
+ bootstrap: snapshotData.bootstrap,
1556
+ context: snapshotData.context,
1557
+ frozenAt: snapshotData.frozenAt,
1558
+ watermarkEventId: snapshotData.watermarkEventId,
1559
+ };
1560
+ touchSnapshot(`${scopeKey}:${sessionId}`, snapshot);
1561
+ const { scopeKey: _scopeKey, ...publicSnapshot } = snapshot;
1562
+ writeJson(res, 200, { snapshot: { ...publicSnapshot, sessionId } });
1563
+ return;
1564
+ }
653
1565
  writeError(res, 404, `Not found: ${req.method} ${path}`);
654
1566
  }
655
1567
  catch (error) {
@@ -657,7 +1569,12 @@ export async function startHttpServer(config = {}) {
657
1569
  writeError(res, error.status, error.message);
658
1570
  return;
659
1571
  }
660
- writeError(res, 500, error instanceof Error ? error.message : String(error));
1572
+ if (isMemoryDomainError(error)) {
1573
+ writeError(res, error.status, error.message);
1574
+ return;
1575
+ }
1576
+ const message = error instanceof Error ? error.message : String(error);
1577
+ writeError(res, 500, message);
661
1578
  }
662
1579
  });
663
1580
  return new Promise((resolve) => {
@@ -670,11 +1587,12 @@ export async function startHttpServer(config = {}) {
670
1587
  client.response.end();
671
1588
  }
672
1589
  sseClients.clear();
673
- server.close();
674
- for (const cachedManager of managers.values()) {
675
- await cachedManager.close();
676
- }
1590
+ await new Promise((resolveClose) => {
1591
+ server.close(() => resolveClose());
1592
+ });
677
1593
  managers.clear();
1594
+ sessionManagers.clear();
1595
+ sessionSnapshots.clear();
678
1596
  await adapterResources.close();
679
1597
  },
680
1598
  });