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,17 +1,25 @@
1
+ import { matchesScope, normalizeScope } from '../contracts/identity.js';
2
+ import { ProviderUnavailableError, ResourceNotFoundError, ScopeMismatchError, ValidationError, } from '../contracts/errors.js';
1
3
  import { DEFAULT_CONTEXT_POLICY, DEFAULT_MONITOR_POLICY, } from '../contracts/policy.js';
2
- import { buildMemoryContext } from './context.js';
4
+ import { buildDerivedShortTermState, buildMemoryContext, getContextWorkItems, resolveContextScopeLevel, resolveVisibleHandoffs, resolveVisibleKnowledge, resolveVisiblePlaybooks, resolveVisibleWorkClaims, resolveVisibleWorkItems, } from './context.js';
3
5
  import { compactTurns, extractKnowledge, } from './orchestrator.js';
4
6
  import { assessContext } from './monitor.js';
5
7
  import { runMaintenance } from './maintenance.js';
6
8
  import { emitMemoryEvent } from './telemetry.js';
7
9
  import { wrapSyncAdapter } from '../adapters/sync-to-async.js';
8
10
  import { estimateTokens } from './tokens.js';
9
- import { createCircuitBreaker } from './circuit-breaker.js';
11
+ import { createCircuitBreaker, } from './circuit-breaker.js';
10
12
  import { DEFAULT_EXTRACTION_POLICY } from '../contracts/policy.js';
11
13
  import { assessKnowledgeReverification } from './trust.js';
12
14
  import { matchesKnowledgeSearchOptions, rankKnowledge } from './retrieval.js';
13
15
  import { computeNextReverificationAt, getDueReverificationKnowledge, resolveMaintenancePolicy, } from './knowledge-lifecycle.js';
14
- import { normalizeScope } from '../contracts/identity.js';
16
+ import { compareTemporalIds, normalizeTemporalId } from '../contracts/temporal.js';
17
+ import { searchEpisodes, summarizeEpisode, reflect } from './episodic.js';
18
+ import { searchCognitive } from './cognitive.js';
19
+ import { traverseAssociations } from './associations.js';
20
+ import { buildProfileFromKnowledge, getProfile } from './profile.js';
21
+ import { createPlaybookFromTask, revisePlaybook, findRelevantPlaybooks, } from './playbook.js';
22
+ import { createTemporalReplayAdapter, foldTemporalState, listAllMemoryEvents, listAllMemoryEventsBounded, listAllMemoryEventsCrossScope, normalizeReplayedTemporalState, normalizeHandoffAt, normalizeWorkClaimAt, } from './temporal.js';
15
23
  function resolveAdapter(config) {
16
24
  if (config.asyncAdapter) {
17
25
  return config.asyncAdapter;
@@ -19,7 +27,7 @@ function resolveAdapter(config) {
19
27
  if (config.adapter) {
20
28
  return wrapSyncAdapter(config.adapter);
21
29
  }
22
- throw new Error("MemoryManagerConfig requires either 'adapter' or 'asyncAdapter'");
30
+ throw new ValidationError("MemoryManagerConfig requires either 'adapter' or 'asyncAdapter'");
23
31
  }
24
32
  function manualKnowledgeClassForFactType(factType) {
25
33
  switch (factType) {
@@ -35,6 +43,79 @@ function manualKnowledgeClassForFactType(factType) {
35
43
  return 'project_fact';
36
44
  }
37
45
  }
46
+ /**
47
+ * Resolve an association endpoint (source or target) and verify it exists
48
+ * and belongs to the caller's normalized scope. Throws a descriptive error
49
+ * if the node is missing or cross-scope. This is the sole authority on
50
+ * association ID validity; HTTP/MCP layers should NOT rely on their own
51
+ * type checks for scope safety.
52
+ */
53
+ async function assertAssociationEndpointInScope(adapter, norm, kind, id, role) {
54
+ const scopedMatch = (record) => record.tenant_id === norm.tenant_id &&
55
+ record.system_id === norm.system_id &&
56
+ record.workspace_id === norm.workspace_id &&
57
+ record.collaboration_id === norm.collaboration_id &&
58
+ record.scope_id === norm.scope_id;
59
+ if (kind === 'knowledge') {
60
+ const km = await adapter.getKnowledgeMemoryById(id);
61
+ if (!km) {
62
+ throw new ResourceNotFoundError(`addAssociation: ${role} knowledge ${id} does not exist`);
63
+ }
64
+ if (!scopedMatch(km)) {
65
+ throw new ScopeMismatchError(`addAssociation: ${role} knowledge ${id} is not in the current scope`);
66
+ }
67
+ return;
68
+ }
69
+ if (kind === 'playbook') {
70
+ const pb = await adapter.getPlaybookById(id);
71
+ if (!pb) {
72
+ throw new ResourceNotFoundError(`addAssociation: ${role} playbook ${id} does not exist`);
73
+ }
74
+ if (!scopedMatch(pb)) {
75
+ throw new ScopeMismatchError(`addAssociation: ${role} playbook ${id} is not in the current scope`);
76
+ }
77
+ return;
78
+ }
79
+ if (kind === 'working_memory') {
80
+ const wm = await adapter.getWorkingMemoryById(id);
81
+ if (!wm) {
82
+ throw new ResourceNotFoundError(`addAssociation: ${role} working_memory ${id} does not exist`);
83
+ }
84
+ if (!scopedMatch(wm)) {
85
+ throw new ScopeMismatchError(`addAssociation: ${role} working_memory ${id} is not in the current scope`);
86
+ }
87
+ return;
88
+ }
89
+ if (kind === 'work_item') {
90
+ const match = await adapter.getWorkItemById(id);
91
+ if (!match) {
92
+ throw new ResourceNotFoundError(`addAssociation: ${role} work_item ${id} does not exist in the current scope`);
93
+ }
94
+ if (match.tenant_id !== norm.tenant_id ||
95
+ match.system_id !== norm.system_id ||
96
+ match.workspace_id !== norm.workspace_id ||
97
+ match.collaboration_id !== norm.collaboration_id ||
98
+ match.scope_id !== norm.scope_id) {
99
+ throw new ScopeMismatchError(`addAssociation: ${role} work_item ${id} does not exist in the current scope`);
100
+ }
101
+ return;
102
+ }
103
+ // Exhaustiveness: AssociationTargetKind has no other members.
104
+ throw new ValidationError(`addAssociation: unknown ${role} kind '${kind}'`);
105
+ }
106
+ /**
107
+ * Merge archived and active turns by id, preserving order by turn id.
108
+ * Partially compacted sessions have both sets; summarizing from only one
109
+ * drops context, so callers should always pass the union through this.
110
+ */
111
+ function mergeTurnsById(archived, active) {
112
+ const byId = new Map();
113
+ for (const t of archived)
114
+ byId.set(t.id, t);
115
+ for (const t of active)
116
+ byId.set(t.id, t);
117
+ return Array.from(byId.values()).sort((a, b) => a.id - b.id);
118
+ }
38
119
  function knowledgeMatchesScope(knowledge, scope) {
39
120
  const normalized = normalizeScope(scope);
40
121
  return (knowledge.tenant_id === normalized.tenant_id &&
@@ -43,6 +124,9 @@ function knowledgeMatchesScope(knowledge, scope) {
43
124
  knowledge.collaboration_id === normalized.collaboration_id &&
44
125
  knowledge.scope_id === normalized.scope_id);
45
126
  }
127
+ function delay(ms) {
128
+ return new Promise((resolve) => setTimeout(resolve, ms));
129
+ }
46
130
  export function createMemoryManager(config) {
47
131
  const asyncAdapter = resolveAdapter(config);
48
132
  const autoCompact = config.autoCompact ?? true;
@@ -87,6 +171,14 @@ export function createMemoryManager(config) {
87
171
  ...detail,
88
172
  });
89
173
  }
174
+ function emitRetrievalFallback(reason, detail = {}) {
175
+ emitMemoryEvent('manager', config.scope, { logger: config.logger, onEvent }, 0, {
176
+ action: 'retrieval_fallback',
177
+ reason,
178
+ strategy: 'lexical_only',
179
+ ...detail,
180
+ });
181
+ }
90
182
  async function withFailurePolicy(kind, run, fallback) {
91
183
  const strategy = config.failurePolicy?.[kind] ??
92
184
  (kind === 'extractor' ? 'disable_auto_extract' : 'throw');
@@ -138,7 +230,13 @@ export function createMemoryManager(config) {
138
230
  });
139
231
  }
140
232
  async function buildQueryVector(input) {
141
- if (!config.embeddingGenerator || input.trim().length === 0) {
233
+ if (input.trim().length === 0) {
234
+ return undefined;
235
+ }
236
+ if (!config.embeddingGenerator) {
237
+ emitRetrievalFallback('embedding_generator_unavailable', {
238
+ stage: 'query_vector',
239
+ });
142
240
  return undefined;
143
241
  }
144
242
  try {
@@ -153,6 +251,10 @@ export function createMemoryManager(config) {
153
251
  stage: 'query_vector',
154
252
  error: String(error),
155
253
  });
254
+ emitRetrievalFallback('query_vector_unavailable', {
255
+ stage: 'query_vector',
256
+ error: String(error),
257
+ });
156
258
  return undefined;
157
259
  }
158
260
  }
@@ -198,10 +300,18 @@ export function createMemoryManager(config) {
198
300
  : await asyncAdapter.searchKnowledgeCrossScope(config.scope, level, query, options);
199
301
  const filteredLexical = lexical.filter((result) => matchesKnowledgeSearchOptions(result.item, options));
200
302
  if (!config.embeddingAdapter) {
303
+ emitRetrievalFallback('embedding_adapter_unavailable', {
304
+ stage: 'semantic_search',
305
+ scopeLevel: level,
306
+ });
201
307
  return filteredLexical;
202
308
  }
203
309
  const queryVector = await buildQueryVector(query);
204
310
  if (!queryVector) {
311
+ emitRetrievalFallback('query_vector_unavailable', {
312
+ stage: 'semantic_search',
313
+ scopeLevel: level,
314
+ });
205
315
  return filteredLexical;
206
316
  }
207
317
  let semantic;
@@ -227,6 +337,11 @@ export function createMemoryManager(config) {
227
337
  error: String(error),
228
338
  scopeLevel: level,
229
339
  });
340
+ emitRetrievalFallback('semantic_search_failed', {
341
+ stage: 'semantic_search',
342
+ error: String(error),
343
+ scopeLevel: level,
344
+ });
230
345
  return filteredLexical;
231
346
  }
232
347
  const lexicalRanks = new Map();
@@ -267,16 +382,17 @@ export function createMemoryManager(config) {
267
382
  .sort((a, b) => b.rank - a.rank || b.item.last_accessed_at - a.item.last_accessed_at)
268
383
  .slice(0, options?.limit ?? 10);
269
384
  if (config.contextPolicy?.touchSelectedKnowledge ?? true) {
270
- for (const result of results) {
271
- await asyncAdapter.touchKnowledgeMemory(result.item.id);
272
- }
385
+ await asyncAdapter.touchKnowledgeMemories(results.map((result) => result.item.id));
273
386
  }
274
387
  return results;
275
388
  }
276
- async function getContextInternal(relevanceQuery, asOf) {
389
+ async function getContextInternal(relevanceQuery, asOf, options) {
277
390
  const activeTurns = await asyncAdapter.getActiveTurns(config.scope, config.sessionId);
391
+ const relevantTurns = asOf == null
392
+ ? activeTurns
393
+ : activeTurns.filter((turn) => turn.created_at <= asOf);
278
394
  const queryVector = await buildQueryVector(relevanceQuery ??
279
- activeTurns
395
+ relevantTurns
280
396
  .slice(-4)
281
397
  .map((turn) => turn.content)
282
398
  .join('\n'));
@@ -289,10 +405,192 @@ export function createMemoryManager(config) {
289
405
  policy: config.contextPolicy,
290
406
  tokenEstimator,
291
407
  asOf,
408
+ view: options?.view,
409
+ viewer: options?.viewer,
410
+ includeCoordinationState: options?.includeCoordinationState,
292
411
  logger: config.logger,
293
412
  onEvent,
294
413
  });
295
414
  }
415
+ async function refreshSessionStateProjection() {
416
+ try {
417
+ const [activeTurns, workingMemoryCandidates, contextWorkItems, watermark] = await Promise.all([
418
+ asyncAdapter.getActiveTurns(config.scope, config.sessionId),
419
+ asyncAdapter.getActiveWorkingMemory(config.scope, config.sessionId),
420
+ // Session-state projection stays scope-local even when wider
421
+ // retrieval is configured; it is a fast resume artifact, not a full
422
+ // cross-scope context assembly.
423
+ getContextWorkItems(asyncAdapter, config.scope, undefined, 'scope'),
424
+ asyncAdapter.getTemporalWatermark('temporal'),
425
+ ]);
426
+ const shortTermState = buildDerivedShortTermState({
427
+ activeTurns,
428
+ workingMemoryCandidates,
429
+ contextWorkItems,
430
+ maxRecentSummaries: config.contextPolicy?.maxRecentSummaries ??
431
+ DEFAULT_CONTEXT_POLICY.maxRecentSummaries,
432
+ });
433
+ await asyncAdapter.upsertSessionState({
434
+ ...normalizeScope(config.scope),
435
+ session_id: config.sessionId,
436
+ ...shortTermState.sessionState,
437
+ source_event_id: watermark?.last_event_id ?? null,
438
+ });
439
+ }
440
+ catch (error) {
441
+ config.logger?.warn?.('memory.session_state_projection_refresh_failed', {
442
+ error: String(error),
443
+ sessionId: config.sessionId,
444
+ });
445
+ emitMemoryEvent('manager', config.scope, { logger: config.logger, onEvent }, 0, {
446
+ action: 'session_state_projection_refresh_failed',
447
+ sessionId: config.sessionId,
448
+ error: String(error),
449
+ });
450
+ }
451
+ }
452
+ async function collectBestEffortTemporalState(asOf, options) {
453
+ const view = options?.view;
454
+ const effectiveScopeLevel = resolveContextScopeLevel(config.crossScopeLevel, view);
455
+ const [turns, workingMemory, knowledge, contextWorkItems, playbooks, associations, rawWorkClaims, rawHandoffs,] = await Promise.all([
456
+ asyncAdapter.getTurnsByTimeRange(config.scope, { end_at: asOf }),
457
+ asyncAdapter.getWorkingMemoryByTimeRange(config.scope, { end_at: asOf }),
458
+ effectiveScopeLevel && effectiveScopeLevel !== 'scope'
459
+ ? asyncAdapter.getActiveKnowledgeCrossScope(config.scope, effectiveScopeLevel)
460
+ : asyncAdapter.getActiveKnowledgeMemory(config.scope),
461
+ getContextWorkItems(asyncAdapter, config.scope, asOf, effectiveScopeLevel),
462
+ effectiveScopeLevel && effectiveScopeLevel !== 'scope'
463
+ ? asyncAdapter.getActivePlaybooksCrossScope(config.scope, effectiveScopeLevel)
464
+ : asyncAdapter.getActivePlaybooks(config.scope),
465
+ asyncAdapter.listAssociations(config.scope),
466
+ effectiveScopeLevel && effectiveScopeLevel !== 'scope'
467
+ ? asyncAdapter.listWorkClaimsCrossScope(config.scope, effectiveScopeLevel, {
468
+ includeExpired: true,
469
+ includeReleased: true,
470
+ })
471
+ : asyncAdapter.listWorkClaims(config.scope, {
472
+ includeExpired: true,
473
+ includeReleased: true,
474
+ }),
475
+ effectiveScopeLevel && effectiveScopeLevel !== 'scope'
476
+ ? asyncAdapter.listHandoffsCrossScope(config.scope, effectiveScopeLevel)
477
+ : asyncAdapter.listHandoffs(config.scope),
478
+ ]);
479
+ const visibleKnowledge = (view
480
+ ? resolveVisibleKnowledge(knowledge.filter((item) => item.created_at <= asOf), config.scope, view)
481
+ : knowledge.filter((item) => item.created_at <= asOf)).sort((a, b) => a.created_at - b.created_at || a.id - b.id);
482
+ const visibleWorkItems = (view ? resolveVisibleWorkItems(contextWorkItems, config.scope, view) : contextWorkItems).sort((a, b) => a.updated_at - b.updated_at || a.created_at - b.created_at || a.id - b.id);
483
+ const visiblePlaybooks = (view
484
+ ? resolveVisiblePlaybooks(playbooks.filter((item) => item.created_at <= asOf), config.scope, view)
485
+ : playbooks.filter((item) => item.created_at <= asOf)).sort((a, b) => a.updated_at - b.updated_at || a.created_at - b.created_at || a.id - b.id);
486
+ const visibleWorkClaims = (view
487
+ ? resolveVisibleWorkClaims(rawWorkClaims.map((claim) => normalizeWorkClaimAt(claim, asOf)), config.scope, view)
488
+ : rawWorkClaims.map((claim) => normalizeWorkClaimAt(claim, asOf))).sort((a, b) => a.claimed_at - b.claimed_at || a.id - b.id);
489
+ const visibleHandoffs = (view
490
+ ? resolveVisibleHandoffs(rawHandoffs.map((handoff) => normalizeHandoffAt(handoff, asOf)), config.scope, view)
491
+ : rawHandoffs.map((handoff) => normalizeHandoffAt(handoff, asOf))).sort((a, b) => a.created_at - b.created_at || a.id - b.id);
492
+ return {
493
+ turns: turns
494
+ .filter((turn) => turn.session_id === config.sessionId)
495
+ .sort((a, b) => a.created_at - b.created_at || a.id - b.id),
496
+ workingMemory: workingMemory
497
+ .filter((item) => item.session_id === config.sessionId)
498
+ .sort((a, b) => a.created_at - b.created_at || a.id - b.id),
499
+ knowledge: visibleKnowledge,
500
+ workItems: visibleWorkItems,
501
+ workClaims: visibleWorkClaims,
502
+ handoffs: visibleHandoffs,
503
+ associations: associations
504
+ .filter((association) => association.created_at <= asOf)
505
+ .sort((a, b) => a.created_at - b.created_at || a.id - b.id),
506
+ playbooks: visiblePlaybooks,
507
+ };
508
+ }
509
+ async function getTemporalCutoverAt() {
510
+ const watermark = await asyncAdapter.getTemporalWatermark('temporal');
511
+ return watermark?.cutover_at ?? null;
512
+ }
513
+ async function resolveChangeStreamCursorInternal(cursor) {
514
+ if (cursor != null)
515
+ return normalizeTemporalId(cursor);
516
+ return (await asyncAdapter.getTemporalWatermark('temporal'))?.last_event_id ?? '0';
517
+ }
518
+ async function buildReplayedContext(asOf, relevanceQuery, options, replayCutoff) {
519
+ const cutoverAt = await getTemporalCutoverAt();
520
+ if (cutoverAt == null || asOf < cutoverAt) {
521
+ return {
522
+ // Pre-cutover replay is best-effort only. The historical filters still
523
+ // apply, but semantic retrieval may consult the live embedding index
524
+ // because that index has no temporal dimension before the cutover.
525
+ context: await getContextInternal(relevanceQuery, asOf, options),
526
+ events: [],
527
+ state: null,
528
+ watermarkEventId: null,
529
+ exact: false,
530
+ cutoverAt,
531
+ };
532
+ }
533
+ const replayScopeLevel = resolveContextScopeLevel(config.crossScopeLevel, options?.view);
534
+ const events = replayScopeLevel != null && replayScopeLevel !== 'scope'
535
+ ? await listAllMemoryEventsCrossScope(asyncAdapter, config.scope, replayScopeLevel, {
536
+ endAt: asOf,
537
+ limit: 500,
538
+ })
539
+ : await listAllMemoryEvents(asyncAdapter, config.scope, {
540
+ endAt: asOf,
541
+ limit: 500,
542
+ });
543
+ const filteredEvents = replayCutoff?.throughEventId != null
544
+ ? events.filter((event) => compareTemporalIds(event.event_id, replayCutoff.throughEventId) <= 0)
545
+ : events;
546
+ const replayed = normalizeReplayedTemporalState(foldTemporalState(filteredEvents, { sessionId: config.sessionId }), asOf);
547
+ const replayAdapter = createTemporalReplayAdapter(replayed, asOf);
548
+ const inferredRelevanceQuery = replayed.turns
549
+ .slice(-4)
550
+ .map((turn) => turn.content)
551
+ .join('\n')
552
+ .trim();
553
+ const resolvedRelevanceQuery = relevanceQuery ?? (inferredRelevanceQuery || undefined);
554
+ // Exact replay stays on the replay adapter only, so no live semantic data
555
+ // can leak into the assembled historical context after temporal cutover.
556
+ const context = await buildMemoryContext(replayAdapter, config.scope, {
557
+ sessionId: config.sessionId,
558
+ relevanceQuery: resolvedRelevanceQuery,
559
+ crossScopeLevel: config.crossScopeLevel,
560
+ policy: config.contextPolicy,
561
+ tokenEstimator,
562
+ view: options?.view,
563
+ viewer: options?.viewer,
564
+ includeCoordinationState: options?.includeCoordinationState,
565
+ logger: config.logger,
566
+ onEvent,
567
+ });
568
+ return {
569
+ context,
570
+ events: filteredEvents,
571
+ state: replayed,
572
+ watermarkEventId: replayCutoff?.throughEventId ?? replayed.watermarkEventId,
573
+ exact: true,
574
+ cutoverAt,
575
+ };
576
+ }
577
+ function buildSessionBootstrapPayload(context, profile) {
578
+ return {
579
+ currentObjective: context.currentObjective,
580
+ sessionState: context.sessionState,
581
+ workingMemory: context.workingMemory,
582
+ relevantKnowledge: context.relevantKnowledge,
583
+ recentSummaries: context.recentSummaries,
584
+ activeObjectives: context.activeObjectives,
585
+ unresolvedWork: context.unresolvedWork,
586
+ coordinationState: context.coordinationState,
587
+ profile,
588
+ };
589
+ }
590
+ async function getHistoricalProfileAt(asOf) {
591
+ const bestEffort = await collectBestEffortTemporalState(asOf);
592
+ return buildProfileFromKnowledge(bestEffort.knowledge.filter((item) => matchesScope(item, config.scope)));
593
+ }
296
594
  async function executeCompaction(turns, trigger, retainedTurnCount, score) {
297
595
  await persistMonitorState('compacting', score, turns);
298
596
  const result = await withFailurePolicy('summarizer', () => compactTurns(asyncAdapter, config.scope, config.sessionId, turns, config.summarizer, trigger, retainedTurnCount, { logger: config.logger, onEvent }), () => null);
@@ -316,6 +614,7 @@ export function createMemoryManager(config) {
316
614
  await maybeEmbedKnowledge(extracted);
317
615
  extracted.forEach((knowledge) => emitKnowledgeChange('promoted', knowledge));
318
616
  }
617
+ await refreshSessionStateProjection();
319
618
  return result;
320
619
  }
321
620
  async function runCompaction(turns) {
@@ -342,9 +641,9 @@ export function createMemoryManager(config) {
342
641
  }
343
642
  return executeCompaction(turns, report.recommendation.action, Math.max(0, Math.min(report.recommendation.post_compaction_target_turns, turns.length - 1)), report.score_breakdown.total);
344
643
  }
345
- async function insertManagedTurn(role, content, actor) {
644
+ async function insertManagedTurnRecord(role, content, actor) {
346
645
  const redactedContent = config.redactText ? config.redactText({ kind: 'turn', text: content }) : content;
347
- const turn = await asyncAdapter.insertTurn({
646
+ return asyncAdapter.insertTurn({
348
647
  ...config.scope,
349
648
  session_id: config.sessionId,
350
649
  actor,
@@ -352,6 +651,9 @@ export function createMemoryManager(config) {
352
651
  content: redactedContent,
353
652
  token_estimate: tokenEstimator(redactedContent),
354
653
  });
654
+ }
655
+ async function insertManagedTurn(role, content, actor) {
656
+ const turn = await insertManagedTurnRecord(role, content, actor);
355
657
  emitMemoryEvent('manager', config.scope, { logger: config.logger, onEvent }, 0, {
356
658
  action: 'process_turn',
357
659
  role,
@@ -366,35 +668,175 @@ export function createMemoryManager(config) {
366
668
  const activeTurns = await asyncAdapter.getActiveTurns(config.scope, config.sessionId);
367
669
  await runCompaction(activeTurns);
368
670
  }
671
+ await refreshSessionStateProjection();
369
672
  return turn;
370
673
  },
371
674
  async processExchange(userContent, assistantContent, actors) {
372
- const userTurn = await insertManagedTurn('user', userContent, actors?.user ?? 'user');
373
- const assistantTurn = await insertManagedTurn('assistant', assistantContent, actors?.assistant ?? 'assistant');
675
+ const [userTurn, assistantTurn] = await asyncAdapter.transaction(async () => {
676
+ const createdUserTurn = await insertManagedTurnRecord('user', userContent, actors?.user ?? 'user');
677
+ const createdAssistantTurn = await insertManagedTurnRecord('assistant', assistantContent, actors?.assistant ?? 'assistant');
678
+ return [createdUserTurn, createdAssistantTurn];
679
+ });
680
+ emitMemoryEvent('manager', config.scope, { logger: config.logger, onEvent }, 0, {
681
+ action: 'process_turn',
682
+ role: 'user',
683
+ turnId: userTurn.id,
684
+ });
685
+ emitMemoryEvent('manager', config.scope, { logger: config.logger, onEvent }, 0, {
686
+ action: 'process_turn',
687
+ role: 'assistant',
688
+ turnId: assistantTurn.id,
689
+ });
374
690
  const compactionResult = autoCompact
375
691
  ? await runCompaction(await asyncAdapter.getActiveTurns(config.scope, config.sessionId))
376
692
  : null;
693
+ await refreshSessionStateProjection();
377
694
  return {
378
695
  userTurn,
379
696
  assistantTurn,
380
697
  compactionResult,
381
698
  };
382
699
  },
383
- async getContext(relevanceQuery) {
384
- return getContextInternal(relevanceQuery);
700
+ async getContext(relevanceQuery, options) {
701
+ return getContextInternal(relevanceQuery, undefined, options);
702
+ },
703
+ async getContextAt(asOf, relevanceQuery, options) {
704
+ return (await buildReplayedContext(asOf, relevanceQuery, options)).context;
705
+ },
706
+ async getStateAt(asOf, options) {
707
+ const replay = await buildReplayedContext(asOf, options?.relevanceQuery, options);
708
+ const replayed = replay.exact
709
+ ? replay.state
710
+ : await collectBestEffortTemporalState(asOf, options);
711
+ return {
712
+ asOf,
713
+ exact: replay.exact,
714
+ cutoverAt: replay.cutoverAt,
715
+ watermarkEventId: replay.watermarkEventId,
716
+ context: replay.context,
717
+ sessionState: replay.context.sessionState,
718
+ turns: replayed.turns,
719
+ workingMemory: replayed.workingMemory,
720
+ knowledge: replayed.knowledge,
721
+ workItems: replayed.workItems,
722
+ workClaims: replayed.workClaims,
723
+ handoffs: replayed.handoffs,
724
+ coordinationState: replay.context.coordinationState,
725
+ associations: replayed.associations,
726
+ playbooks: replayed.playbooks,
727
+ };
728
+ },
729
+ async getTimeline(options) {
730
+ return asyncAdapter.listMemoryEvents(config.scope, {
731
+ sessionId: options?.sessionId,
732
+ entityKind: options?.entityKind,
733
+ entityId: options?.entityId,
734
+ startAt: options?.startAt,
735
+ endAt: options?.endAt,
736
+ limit: options?.limit,
737
+ cursor: options?.cursor,
738
+ });
385
739
  },
386
- async getContextAt(asOf, relevanceQuery) {
387
- return getContextInternal(relevanceQuery, asOf);
740
+ async diffState(from, to, options) {
741
+ const cutoverAt = await getTemporalCutoverAt();
742
+ const eventQuery = {
743
+ sessionId: options?.sessionId,
744
+ entityKind: options?.entityKind,
745
+ entityId: options?.entityId,
746
+ startAt: from + 1,
747
+ endAt: to,
748
+ limit: 500,
749
+ };
750
+ const events = options?.maxEvents != null
751
+ ? await listAllMemoryEventsBounded(asyncAdapter, config.scope, options.maxEvents, eventQuery)
752
+ : await listAllMemoryEvents(asyncAdapter, config.scope, eventQuery);
753
+ const byEntityKind = {};
754
+ const byEventType = {};
755
+ for (const event of events) {
756
+ byEntityKind[event.entity_kind] = (byEntityKind[event.entity_kind] ?? 0) + 1;
757
+ byEventType[event.event_type] = (byEventType[event.event_type] ?? 0) + 1;
758
+ }
759
+ return {
760
+ from,
761
+ to,
762
+ exact: cutoverAt != null && from >= cutoverAt && to >= cutoverAt,
763
+ cutoverAt,
764
+ watermarkRange: {
765
+ fromEventId: events[0]?.event_id ?? null,
766
+ toEventId: events[events.length - 1]?.event_id ?? null,
767
+ },
768
+ events,
769
+ summary: {
770
+ totalEvents: events.length,
771
+ byEntityKind,
772
+ byEventType,
773
+ },
774
+ };
388
775
  },
389
- async getSessionBootstrap(relevanceQuery) {
390
- const context = await getContextInternal(relevanceQuery);
776
+ async listMemoryEvents(options) {
777
+ const timeline = await asyncAdapter.listMemoryEvents(config.scope, {
778
+ sessionId: options?.sessionId,
779
+ entityKind: options?.entityKind,
780
+ entityId: options?.entityId,
781
+ startAt: options?.startAt,
782
+ endAt: options?.endAt,
783
+ limit: options?.limit,
784
+ cursor: options?.cursor,
785
+ });
391
786
  return {
392
- currentObjective: context.currentObjective,
393
- workingMemory: context.workingMemory,
394
- relevantKnowledge: context.relevantKnowledge,
395
- recentSummaries: context.recentSummaries,
396
- activeObjectives: context.activeObjectives,
397
- unresolvedWork: context.unresolvedWork,
787
+ events: [...timeline.events].reverse(),
788
+ nextCursor: timeline.nextCursor,
789
+ };
790
+ },
791
+ async getSessionBootstrap(relevanceQuery, options) {
792
+ const context = await getContextInternal(relevanceQuery, undefined, options);
793
+ const profile = await getProfile(asyncAdapter, config.scope);
794
+ return buildSessionBootstrapPayload(context, profile);
795
+ },
796
+ async getSessionBootstrapAt(asOf, relevanceQuery, options) {
797
+ const replay = await buildReplayedContext(asOf, relevanceQuery, options);
798
+ const profile = replay.exact && replay.state
799
+ ? buildProfileFromKnowledge(replay.state.knowledge.filter((item) => matchesScope(item, config.scope)))
800
+ : await getHistoricalProfileAt(asOf);
801
+ return buildSessionBootstrapPayload(replay.context, profile);
802
+ },
803
+ async captureSnapshot(relevanceQuery, options) {
804
+ const frozenAt = Math.floor(Date.now() / 1000);
805
+ const watermark = await asyncAdapter.getTemporalWatermark('temporal');
806
+ if (!watermark || watermark.last_event_id === '0') {
807
+ const [context, profile] = await Promise.all([
808
+ getContextInternal(relevanceQuery, undefined, options),
809
+ getProfile(asyncAdapter, config.scope),
810
+ ]);
811
+ return {
812
+ bootstrap: buildSessionBootstrapPayload(context, profile),
813
+ context,
814
+ frozenAt,
815
+ watermarkEventId: null,
816
+ profile,
817
+ };
818
+ }
819
+ const replay = await buildReplayedContext(watermark.updated_at, relevanceQuery, options, {
820
+ throughEventId: watermark.last_event_id,
821
+ });
822
+ const profile = replay.exact && replay.state
823
+ ? buildProfileFromKnowledge(replay.state.knowledge.filter((item) => matchesScope(item, config.scope)))
824
+ : await getHistoricalProfileAt(watermark.updated_at);
825
+ return {
826
+ bootstrap: buildSessionBootstrapPayload(replay.context, profile),
827
+ context: replay.context,
828
+ frozenAt,
829
+ watermarkEventId: replay.exact ? watermark.last_event_id : null,
830
+ profile,
831
+ };
832
+ },
833
+ async getRuntimeDiagnostics() {
834
+ return {
835
+ circuitBreakers: {
836
+ summarizer: circuitBreakers.summarizer.getSnapshot(),
837
+ extractor: circuitBreakers.extractor.getSnapshot(),
838
+ embeddings: circuitBreakers.embeddings.getSnapshot(),
839
+ },
398
840
  };
399
841
  },
400
842
  async recall(timeRange) {
@@ -461,10 +903,11 @@ export function createMemoryManager(config) {
461
903
  emitKnowledgeChange('learned', knowledge);
462
904
  return knowledge;
463
905
  },
464
- async trackWorkItem(title, kind = 'objective', status = 'open', detail) {
465
- return asyncAdapter.insertWorkItem({
906
+ async trackWorkItem(title, kind = 'objective', status = 'open', detail, options) {
907
+ const workItem = await asyncAdapter.insertWorkItem({
466
908
  ...config.scope,
467
909
  session_id: config.sessionId,
910
+ visibility_class: options?.visibilityClass ?? 'private',
468
911
  title: config.redactText ? config.redactText({ kind: 'work_item', text: title }) : title,
469
912
  kind,
470
913
  status,
@@ -472,6 +915,88 @@ export function createMemoryManager(config) {
472
915
  ? config.redactText({ kind: 'work_item', text: detail })
473
916
  : detail,
474
917
  });
918
+ await refreshSessionStateProjection();
919
+ return workItem;
920
+ },
921
+ async updateWorkItem(id, patch, options) {
922
+ const workItem = await asyncAdapter.updateWorkItem(id, patch, options);
923
+ await refreshSessionStateProjection();
924
+ return workItem;
925
+ },
926
+ async claimWorkItem(input) {
927
+ const workItem = await asyncAdapter.getWorkItemById(input.workItemId);
928
+ return asyncAdapter.claimWorkItem({
929
+ ...normalizeScope(config.scope),
930
+ work_item_id: input.workItemId,
931
+ actor: input.actor,
932
+ session_id: config.sessionId,
933
+ lease_seconds: input.leaseSeconds,
934
+ visibility_class: workItem?.visibility_class ?? 'private',
935
+ });
936
+ },
937
+ async renewWorkClaim(claimId, actor, leaseSeconds) {
938
+ return asyncAdapter.renewWorkClaim(claimId, actor, leaseSeconds);
939
+ },
940
+ async releaseWorkClaim(claimId, actor, reason) {
941
+ return asyncAdapter.releaseWorkClaim(claimId, actor, reason);
942
+ },
943
+ async listWorkClaims(options) {
944
+ return asyncAdapter.listWorkClaims(config.scope, {
945
+ actor: options?.actor,
946
+ sessionId: options?.sessionId,
947
+ });
948
+ },
949
+ async handoffWorkItem(input) {
950
+ const workItem = await asyncAdapter.getWorkItemById(input.workItemId);
951
+ return asyncAdapter.createHandoff({
952
+ ...normalizeScope(config.scope),
953
+ work_item_id: input.workItemId,
954
+ from_actor: input.fromActor,
955
+ to_actor: input.toActor,
956
+ session_id: config.sessionId,
957
+ summary: input.summary,
958
+ context_bundle_ref: input.contextBundleRef ?? null,
959
+ expires_at: input.expiresAt ?? null,
960
+ visibility_class: workItem?.visibility_class ?? 'private',
961
+ });
962
+ },
963
+ async acceptHandoff(handoffId, actor, reason) {
964
+ return asyncAdapter.acceptHandoff(handoffId, actor, reason);
965
+ },
966
+ async rejectHandoff(handoffId, actor, reason) {
967
+ return asyncAdapter.rejectHandoff(handoffId, actor, reason);
968
+ },
969
+ async cancelHandoff(handoffId, actor, reason) {
970
+ return asyncAdapter.cancelHandoff(handoffId, actor, reason);
971
+ },
972
+ async listPendingHandoffs(options) {
973
+ return asyncAdapter.listHandoffs(config.scope, {
974
+ actor: options?.actor,
975
+ direction: options?.direction,
976
+ statuses: ['pending'],
977
+ });
978
+ },
979
+ async resolveChangeStreamCursor(cursor) {
980
+ return resolveChangeStreamCursorInternal(cursor);
981
+ },
982
+ async *streamChanges(options) {
983
+ let cursor = await resolveChangeStreamCursorInternal(options?.cursor);
984
+ while (!options?.signal?.aborted) {
985
+ const page = await asyncAdapter.listMemoryEvents(config.scope, {
986
+ cursor,
987
+ sessionId: options?.sessionId,
988
+ entityKind: options?.entityKind,
989
+ entityId: options?.entityId,
990
+ limit: 100,
991
+ });
992
+ for (const event of page.events) {
993
+ cursor = event.event_id;
994
+ yield event;
995
+ }
996
+ if (options?.signal?.aborted)
997
+ break;
998
+ await delay(options?.pollIntervalMs ?? 250);
999
+ }
475
1000
  },
476
1001
  async inspectKnowledge(id) {
477
1002
  const knowledge = await asyncAdapter.getKnowledgeMemoryById(id);
@@ -506,10 +1031,10 @@ export function createMemoryManager(config) {
506
1031
  async reverifyKnowledge(id) {
507
1032
  const knowledge = await asyncAdapter.getKnowledgeMemoryById(id);
508
1033
  if (!knowledge) {
509
- throw new Error(`Memory validation: knowledge memory ${id} was not found`);
1034
+ throw new ResourceNotFoundError(`Memory validation: knowledge memory ${id} was not found`);
510
1035
  }
511
1036
  if (!knowledgeMatchesScope(knowledge, config.scope)) {
512
- throw new Error(`Memory validation: knowledge memory ${id} does not belong to the requested scope`);
1037
+ throw new ScopeMismatchError(`Memory validation: knowledge memory ${id} does not belong to the requested scope`);
513
1038
  }
514
1039
  const evidence = await asyncAdapter.listKnowledgeEvidenceForKnowledge(id);
515
1040
  const policy = {
@@ -627,13 +1152,197 @@ export function createMemoryManager(config) {
627
1152
  expiredWorkingMemoryCount: report.expiredWorkingMemoryIds.length,
628
1153
  retiredKnowledgeCount: report.retiredKnowledgeIds.length,
629
1154
  deletedWorkItemCount: report.deletedWorkItemIds.length,
1155
+ deletedAssociationCount: report.deletedAssociationIds.length,
630
1156
  reverifiedKnowledgeCount: report.reverifiedKnowledgeIds.length,
631
1157
  demotedKnowledgeCount: report.demotedKnowledgeIds.length,
632
1158
  });
1159
+ await refreshSessionStateProjection();
633
1160
  return report;
634
1161
  },
1162
+ async searchEpisodes(options) {
1163
+ if (!config.structuredClient) {
1164
+ throw new ProviderUnavailableError('searchEpisodes requires a structuredClient in MemoryManagerConfig');
1165
+ }
1166
+ return searchEpisodes({
1167
+ adapter: asyncAdapter,
1168
+ scope: config.scope,
1169
+ client: config.structuredClient,
1170
+ telemetry: { logger: config.logger, onEvent },
1171
+ }, options);
1172
+ },
1173
+ async summarizeEpisode(sessionId, options) {
1174
+ if (!config.structuredClient) {
1175
+ throw new ProviderUnavailableError('summarizeEpisode requires a structuredClient in MemoryManagerConfig');
1176
+ }
1177
+ const detailLevel = options?.detailLevel ?? 'overview';
1178
+ // Fetch both active and all session working memories. Partially
1179
+ // compacted sessions have BOTH archived history (covered by working
1180
+ // memory turn ranges) and active turns; a recap built from only the
1181
+ // active fragment silently drops earlier context, so we always merge
1182
+ // archived + active and dedupe by turn id.
1183
+ const activeTurns = await asyncAdapter.getActiveTurns(config.scope, sessionId);
1184
+ const allSessionWm = await asyncAdapter.getWorkingMemoryBySession(sessionId, config.scope);
1185
+ let archivedTurns = [];
1186
+ if (allSessionWm.length > 0) {
1187
+ const minStart = Math.min(...allSessionWm.map((wm) => wm.turn_id_start));
1188
+ const maxEnd = Math.max(...allSessionWm.map((wm) => wm.turn_id_end));
1189
+ archivedTurns = await asyncAdapter.getArchivedTurnRange(sessionId, minStart, maxEnd, config.scope);
1190
+ }
1191
+ const turns = mergeTurnsById(archivedTurns, activeTurns);
1192
+ return summarizeEpisode({
1193
+ adapter: asyncAdapter,
1194
+ scope: config.scope,
1195
+ client: config.structuredClient,
1196
+ telemetry: { logger: config.logger, onEvent },
1197
+ }, { turns, workingMemories: allSessionWm, sessionId, detailLevel, client: config.structuredClient });
1198
+ },
1199
+ async reflect(options) {
1200
+ if (!config.structuredClient) {
1201
+ throw new ProviderUnavailableError('reflect requires a structuredClient in MemoryManagerConfig');
1202
+ }
1203
+ return reflect({
1204
+ adapter: asyncAdapter,
1205
+ scope: config.scope,
1206
+ client: config.structuredClient,
1207
+ telemetry: { logger: config.logger, onEvent },
1208
+ }, options);
1209
+ },
1210
+ async searchCognitive(options) {
1211
+ return searchCognitive(asyncAdapter, config.scope, options);
1212
+ },
1213
+ async getProfile(options) {
1214
+ return getProfile(asyncAdapter, config.scope, options);
1215
+ },
1216
+ async createPlaybook(input) {
1217
+ return asyncAdapter.insertPlaybook({ ...input, ...config.scope });
1218
+ },
1219
+ async createPlaybookFromTask(input) {
1220
+ if (!config.structuredClient) {
1221
+ throw new ProviderUnavailableError('createPlaybookFromTask requires a structuredClient in MemoryManagerConfig');
1222
+ }
1223
+ return createPlaybookFromTask({ adapter: asyncAdapter, scope: config.scope, client: config.structuredClient }, input);
1224
+ },
1225
+ async revisePlaybook(playbookId, newInstructions, revisionReason, sourceSessionId) {
1226
+ return revisePlaybook(asyncAdapter, config.scope, playbookId, newInstructions, revisionReason, sourceSessionId);
1227
+ },
1228
+ async getPlaybook(id) {
1229
+ const playbook = await asyncAdapter.getPlaybookById(id);
1230
+ if (!playbook)
1231
+ return null;
1232
+ const norm = normalizeScope(config.scope);
1233
+ if (playbook.tenant_id !== norm.tenant_id ||
1234
+ playbook.system_id !== norm.system_id ||
1235
+ playbook.workspace_id !== norm.workspace_id ||
1236
+ playbook.collaboration_id !== norm.collaboration_id ||
1237
+ playbook.scope_id !== norm.scope_id) {
1238
+ return null;
1239
+ }
1240
+ return playbook;
1241
+ },
1242
+ async listPlaybooks() {
1243
+ return asyncAdapter.getActivePlaybooks(config.scope);
1244
+ },
1245
+ async searchPlaybooks(query, options) {
1246
+ return findRelevantPlaybooks(asyncAdapter, config.scope, query, options);
1247
+ },
1248
+ async updatePlaybook(id, patch) {
1249
+ const playbook = await asyncAdapter.getPlaybookById(id);
1250
+ if (!playbook)
1251
+ return null;
1252
+ const norm = normalizeScope(config.scope);
1253
+ if (playbook.tenant_id !== norm.tenant_id ||
1254
+ playbook.system_id !== norm.system_id ||
1255
+ playbook.workspace_id !== norm.workspace_id ||
1256
+ playbook.collaboration_id !== norm.collaboration_id ||
1257
+ playbook.scope_id !== norm.scope_id) {
1258
+ return null;
1259
+ }
1260
+ return asyncAdapter.updatePlaybook(id, patch);
1261
+ },
1262
+ async recordPlaybookUse(id) {
1263
+ const playbook = await asyncAdapter.getPlaybookById(id);
1264
+ if (!playbook) {
1265
+ throw new ResourceNotFoundError(`Playbook ${id} not found`);
1266
+ }
1267
+ const norm = normalizeScope(config.scope);
1268
+ if (playbook.tenant_id !== norm.tenant_id ||
1269
+ playbook.system_id !== norm.system_id ||
1270
+ playbook.workspace_id !== norm.workspace_id ||
1271
+ playbook.collaboration_id !== norm.collaboration_id ||
1272
+ playbook.scope_id !== norm.scope_id) {
1273
+ throw new ScopeMismatchError(`Playbook ${id} does not belong to the requested scope`);
1274
+ }
1275
+ return asyncAdapter.recordPlaybookUse(id);
1276
+ },
1277
+ async addAssociation(input) {
1278
+ // Validate source/target IDs are positive integers. Callers (HTTP/MCP)
1279
+ // only check typeof number, so this is the authoritative guard.
1280
+ if (!Number.isInteger(input.source_id) || input.source_id <= 0) {
1281
+ throw new ValidationError(`addAssociation: source_id must be a positive integer, got ${input.source_id}`);
1282
+ }
1283
+ if (!Number.isInteger(input.target_id) || input.target_id <= 0) {
1284
+ throw new ValidationError(`addAssociation: target_id must be a positive integer, got ${input.target_id}`);
1285
+ }
1286
+ if (input.source_kind === input.target_kind && input.source_id === input.target_id) {
1287
+ throw new ValidationError('addAssociation: self-referential associations are not allowed');
1288
+ }
1289
+ // Validate confidence is in [0, 1] when provided.
1290
+ if (input.confidence !== undefined) {
1291
+ if (typeof input.confidence !== 'number' ||
1292
+ Number.isNaN(input.confidence) ||
1293
+ input.confidence < 0 ||
1294
+ input.confidence > 1) {
1295
+ throw new ValidationError(`addAssociation: confidence must be a number in [0, 1], got ${input.confidence}`);
1296
+ }
1297
+ }
1298
+ // Resolve source and target: both must exist and belong to the caller's
1299
+ // scope. Without this, callers can create orphaned or cross-scope edges,
1300
+ // polluting the graph and weakening isolation guarantees.
1301
+ const norm = normalizeScope(config.scope);
1302
+ await assertAssociationEndpointInScope(asyncAdapter, norm, input.source_kind, input.source_id, 'source');
1303
+ await assertAssociationEndpointInScope(asyncAdapter, norm, input.target_kind, input.target_id, 'target');
1304
+ return asyncAdapter.insertAssociation({
1305
+ ...input,
1306
+ ...norm,
1307
+ });
1308
+ },
1309
+ async getAssociations(kind, id) {
1310
+ const [from, to] = await Promise.all([
1311
+ asyncAdapter.getAssociationsFrom(kind, id, config.scope),
1312
+ asyncAdapter.getAssociationsTo(kind, id, config.scope),
1313
+ ]);
1314
+ return { from, to };
1315
+ },
1316
+ async traverseAssociations(kind, id, options) {
1317
+ return traverseAssociations(asyncAdapter, config.scope, kind, id, options);
1318
+ },
1319
+ async removeAssociation(id) {
1320
+ // Scope safety: verify the association belongs to the current scope by
1321
+ // checking the association row's own scope columns. Scanning through
1322
+ // active knowledge/playbooks/WM/work items would incorrectly reject
1323
+ // associations attached to archived/expired/orphaned nodes, leaving
1324
+ // stale edges permanently in the graph.
1325
+ if (!Number.isInteger(id) || id <= 0) {
1326
+ throw new ValidationError(`removeAssociation: id must be a positive integer, got ${id}`);
1327
+ }
1328
+ const association = await asyncAdapter.getAssociationById(id);
1329
+ if (!association) {
1330
+ throw new ResourceNotFoundError(`Association ${id} not found`);
1331
+ }
1332
+ const norm = normalizeScope(config.scope);
1333
+ if (association.tenant_id !== norm.tenant_id ||
1334
+ association.system_id !== norm.system_id ||
1335
+ association.workspace_id !== norm.workspace_id ||
1336
+ association.collaboration_id !== norm.collaboration_id ||
1337
+ association.scope_id !== norm.scope_id) {
1338
+ throw new ScopeMismatchError(`Association ${id} not found in the current scope`);
1339
+ }
1340
+ await asyncAdapter.deleteAssociation(id);
1341
+ },
635
1342
  async close() {
636
- await asyncAdapter.close();
1343
+ if (config.closeAdapter !== false) {
1344
+ await asyncAdapter.close();
1345
+ }
637
1346
  },
638
1347
  };
639
1348
  }