ai-memory-layer 2.0.0 → 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 +7 -5
@@ -1,48 +1,16 @@
1
- import { normalizeScope } from '../../contracts/identity.js';
1
+ import { matchesScope, matchesScopeLevel, normalizeScope, } from '../../contracts/identity.js';
2
+ import { UniqueConstraintError } from '../../contracts/storage.js';
3
+ import { ConflictError } from '../../contracts/errors.js';
4
+ import { compareTemporalIds, normalizeTemporalId, } from '../../contracts/temporal.js';
2
5
  import { estimateTokens } from '../../core/tokens.js';
3
6
  import { emitMemoryEvent } from '../../core/telemetry.js';
4
7
  import { matchesKnowledgeSearchOptions } from '../../core/retrieval.js';
5
- import { assertArchiveInput, nowSeconds, validateContextMonitorUpsert, validateNewCompactionLog, validateNewKnowledgeCandidate, validateNewKnowledgeEvidence, validateNewKnowledgeMemory, validateNewKnowledgeMemoryAudit, validateNewTurn, validateNewWorkItem, validateNewWorkingMemory, validateTimeRange, } from '../../core/validation.js';
8
+ import { assertActorRef, assertArchiveInput, assertMemoryVisibilityClass, nowSeconds, validateContextMonitorUpsert, validateNewCompactionLog, validateNewKnowledgeCandidate, validateNewKnowledgeEvidence, validateNewKnowledgeMemory, validateNewKnowledgeMemoryAudit, validateNewTurn, validateNewWorkItem, validateNewWorkingMemory, validateTimeRange, } from '../../core/validation.js';
6
9
  import { createInMemoryEmbeddingAdapter } from './embeddings.js';
7
10
  const SCHEMA_VERSION = 1;
8
- function matchesScope(item, scope) {
9
- const left = normalizeScope(item);
10
- const right = normalizeScope(scope);
11
- return (left.tenant_id === right.tenant_id &&
12
- left.system_id === right.system_id &&
13
- left.workspace_id === right.workspace_id &&
14
- left.collaboration_id === right.collaboration_id &&
15
- left.scope_id === right.scope_id);
16
- }
17
11
  function matchesScopedSession(item, scope, sessionId) {
18
12
  return matchesScope(item, scope) && (sessionId == null || item.session_id === sessionId);
19
13
  }
20
- function matchesLevel(item, scope, level) {
21
- const left = normalizeScope(item);
22
- const right = normalizeScope(scope);
23
- if (left.tenant_id !== right.tenant_id)
24
- return false;
25
- if (level === 'tenant')
26
- return true;
27
- const explicitCollaboration = left.collaboration_id.length > 0 && right.collaboration_id.length > 0;
28
- if (level === 'workspace' && explicitCollaboration) {
29
- return left.collaboration_id === right.collaboration_id;
30
- }
31
- if (left.system_id !== right.system_id)
32
- return false;
33
- if (level === 'system')
34
- return true;
35
- if (explicitCollaboration) {
36
- if (left.collaboration_id !== right.collaboration_id)
37
- return false;
38
- }
39
- else if (left.workspace_id !== right.workspace_id) {
40
- return false;
41
- }
42
- if (level === 'workspace')
43
- return true;
44
- return left.scope_id === right.scope_id;
45
- }
46
14
  function inRange(createdAt, range) {
47
15
  validateTimeRange(range);
48
16
  if (range.start_at !== undefined && createdAt < range.start_at)
@@ -105,6 +73,66 @@ function paginateItems(items, options) {
105
73
  nextCursor: hasMore ? itemsPage[itemsPage.length - 1]?.id ?? null : null,
106
74
  };
107
75
  }
76
+ function cloneValue(value) {
77
+ return structuredClone(value);
78
+ }
79
+ function filterExistingIds(items, ids) {
80
+ const existing = new Set(items.map((item) => item.id));
81
+ const uniqueIds = [...new Set(ids)];
82
+ return uniqueIds.filter((id) => existing.has(id));
83
+ }
84
+ function normalizeEventQuery(query) {
85
+ return {
86
+ sessionId: query?.sessionId ?? '',
87
+ entityKind: query?.entityKind ?? null,
88
+ entityId: query?.entityId ?? '',
89
+ startAt: query?.startAt ?? Number.NEGATIVE_INFINITY,
90
+ endAt: query?.endAt ?? Number.POSITIVE_INFINITY,
91
+ limit: query?.limit ?? 100,
92
+ cursor: query?.cursor != null ? normalizeTemporalId(query.cursor) : null,
93
+ };
94
+ }
95
+ function matchesEventScope(item, scope) {
96
+ return matchesScope(item, scope);
97
+ }
98
+ function matchesActor(actor, target) {
99
+ return actor.actor_kind === target.actor_kind && actor.actor_id === target.actor_id;
100
+ }
101
+ function makeClaimToken() {
102
+ return `claim-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
103
+ }
104
+ function isClaimExpired(claim, now = nowSeconds()) {
105
+ return claim.status === 'active' && claim.expires_at <= now;
106
+ }
107
+ function isHandoffExpired(handoff, now = nowSeconds()) {
108
+ return handoff.status === 'pending' && handoff.expires_at != null && handoff.expires_at <= now;
109
+ }
110
+ function matchesEventQuery(item, query) {
111
+ const resolved = normalizeEventQuery(query);
112
+ if (resolved.cursor != null && compareTemporalIds(item.event_id, resolved.cursor) <= 0) {
113
+ return false;
114
+ }
115
+ if (item.created_at < resolved.startAt || item.created_at > resolved.endAt)
116
+ return false;
117
+ if (resolved.sessionId && item.session_id !== resolved.sessionId)
118
+ return false;
119
+ if (resolved.entityKind && item.entity_kind !== resolved.entityKind)
120
+ return false;
121
+ if (resolved.entityId && item.entity_id !== resolved.entityId)
122
+ return false;
123
+ return true;
124
+ }
125
+ function paginateEvents(items, query) {
126
+ const resolved = normalizeEventQuery(query);
127
+ const ordered = [...items].sort((a, b) => a.created_at - b.created_at || compareTemporalIds(a.event_id, b.event_id));
128
+ const page = ordered.slice(0, resolved.limit + 1);
129
+ const hasMore = page.length > resolved.limit;
130
+ const events = hasMore ? page.slice(0, resolved.limit) : page;
131
+ return {
132
+ events,
133
+ nextCursor: hasMore ? events[events.length - 1]?.event_id ?? null : null,
134
+ };
135
+ }
108
136
  export function createInMemoryAdapter(telemetry) {
109
137
  const state = {
110
138
  turns: [],
@@ -114,8 +142,24 @@ export function createInMemoryAdapter(telemetry) {
114
142
  knowledgeEvidence: [],
115
143
  knowledgeAudits: [],
116
144
  workItems: [],
145
+ workClaims: [],
146
+ handoffs: [],
117
147
  contextMonitors: [],
118
148
  compactionLogs: [],
149
+ playbooks: [],
150
+ playbookRevisions: [],
151
+ associations: [],
152
+ memoryEvents: [],
153
+ sessionStates: [],
154
+ projectionWatermarks: [
155
+ {
156
+ projection_name: 'temporal',
157
+ last_event_id: '0',
158
+ updated_at: nowSeconds(),
159
+ cutover_at: nowSeconds(),
160
+ metadata: null,
161
+ },
162
+ ],
119
163
  };
120
164
  const ids = {
121
165
  turn: 1,
@@ -125,8 +169,14 @@ export function createInMemoryAdapter(telemetry) {
125
169
  knowledgeEvidence: 1,
126
170
  knowledgeAudit: 1,
127
171
  workItem: 1,
172
+ workClaim: 1,
173
+ handoff: 1,
128
174
  contextMonitor: 1,
129
175
  compactionLog: 1,
176
+ playbook: 1,
177
+ playbookRevision: 1,
178
+ association: 1,
179
+ memoryEvent: 1,
130
180
  };
131
181
  return {
132
182
  insertTurn(input) {
@@ -146,6 +196,18 @@ export function createInMemoryAdapter(telemetry) {
146
196
  schema_version: SCHEMA_VERSION,
147
197
  };
148
198
  state.turns.push(turn);
199
+ this.insertMemoryEvent({
200
+ ...scope,
201
+ session_id: turn.session_id,
202
+ actor_id: turn.actor,
203
+ entity_kind: 'turn',
204
+ entity_id: String(turn.id),
205
+ event_type: 'turn.created',
206
+ payload: {
207
+ after: cloneValue(turn),
208
+ },
209
+ created_at: turn.created_at,
210
+ });
149
211
  return turn;
150
212
  },
151
213
  insertTurns(inputs) {
@@ -187,8 +249,26 @@ export function createInMemoryAdapter(telemetry) {
187
249
  const turn = state.turns.find((item) => item.id === id);
188
250
  if (!turn)
189
251
  return;
252
+ const before = cloneValue(turn);
190
253
  turn.archived_at = archivedAt;
191
254
  turn.compaction_log_id = compactionLogId;
255
+ this.insertMemoryEvent({
256
+ ...normalizeScope(turn),
257
+ session_id: turn.session_id,
258
+ actor_id: turn.actor,
259
+ entity_kind: 'turn',
260
+ entity_id: String(turn.id),
261
+ event_type: 'turn.archived',
262
+ payload: {
263
+ before,
264
+ after: cloneValue(turn),
265
+ patch: {
266
+ archived_at: archivedAt,
267
+ compaction_log_id: compactionLogId,
268
+ },
269
+ },
270
+ created_at: archivedAt,
271
+ });
192
272
  },
193
273
  getArchivedTurnRange(sessionId, startId, endId, scope) {
194
274
  return state.turns.filter((turn) => turn.session_id === sessionId &&
@@ -214,14 +294,29 @@ export function createInMemoryAdapter(telemetry) {
214
294
  created_at: createdAt,
215
295
  expires_at: input.expires_at ?? createdAt + 86400,
216
296
  promoted_to_knowledge_id: null,
297
+ episode_recap: input.episode_recap ?? null,
217
298
  schema_version: SCHEMA_VERSION,
218
299
  };
219
300
  state.workingMemory.push(record);
301
+ this.insertMemoryEvent({
302
+ ...scope,
303
+ session_id: record.session_id,
304
+ entity_kind: 'working_memory',
305
+ entity_id: String(record.id),
306
+ event_type: 'working_memory.created',
307
+ payload: {
308
+ after: cloneValue(record),
309
+ },
310
+ created_at: record.created_at,
311
+ });
220
312
  return record;
221
313
  },
222
314
  getWorkingMemoryById(id) {
223
315
  return state.workingMemory.find((item) => item.id === id) ?? null;
224
316
  },
317
+ getExistingWorkingMemoryIds(ids) {
318
+ return filterExistingIds(state.workingMemory, ids);
319
+ },
225
320
  getWorkingMemoryBySession(sessionId, scope) {
226
321
  return state.workingMemory.filter((item) => item.session_id === sessionId && matchesScope(item, scope));
227
322
  },
@@ -238,13 +333,48 @@ export function createInMemoryAdapter(telemetry) {
238
333
  },
239
334
  expireWorkingMemory(id) {
240
335
  const item = state.workingMemory.find((entry) => entry.id === id);
241
- if (item)
242
- item.expires_at = nowSeconds();
336
+ if (!item)
337
+ return;
338
+ const before = cloneValue(item);
339
+ const expiredAt = nowSeconds();
340
+ item.expires_at = expiredAt;
341
+ this.insertMemoryEvent({
342
+ ...normalizeScope(item),
343
+ session_id: item.session_id,
344
+ entity_kind: 'working_memory',
345
+ entity_id: String(item.id),
346
+ event_type: 'working_memory.expired',
347
+ payload: {
348
+ before,
349
+ after: cloneValue(item),
350
+ patch: {
351
+ expires_at: expiredAt,
352
+ },
353
+ },
354
+ created_at: expiredAt,
355
+ });
243
356
  },
244
357
  markWorkingMemoryPromoted(id, knowledgeMemoryId) {
245
358
  const item = state.workingMemory.find((entry) => entry.id === id);
246
- if (item)
247
- item.promoted_to_knowledge_id = knowledgeMemoryId;
359
+ if (!item)
360
+ return;
361
+ const before = cloneValue(item);
362
+ item.promoted_to_knowledge_id = knowledgeMemoryId;
363
+ this.insertMemoryEvent({
364
+ ...normalizeScope(item),
365
+ session_id: item.session_id,
366
+ entity_kind: 'working_memory',
367
+ entity_id: String(item.id),
368
+ event_type: 'working_memory.promoted',
369
+ payload: {
370
+ before,
371
+ after: cloneValue(item),
372
+ refs: {
373
+ knowledge_memory_id: knowledgeMemoryId,
374
+ },
375
+ },
376
+ created_at: nowSeconds(),
377
+ });
248
378
  },
249
379
  insertKnowledgeMemory(input) {
250
380
  const scope = validateNewKnowledgeMemory(input);
@@ -252,6 +382,7 @@ export function createInMemoryAdapter(telemetry) {
252
382
  const record = {
253
383
  ...scope,
254
384
  id: ids.knowledgeMemory++,
385
+ visibility_class: input.visibility_class ?? 'private',
255
386
  fact: input.fact,
256
387
  fact_type: input.fact_type,
257
388
  knowledge_state: input.knowledge_state ?? 'trusted',
@@ -293,6 +424,16 @@ export function createInMemoryAdapter(telemetry) {
293
424
  schema_version: SCHEMA_VERSION,
294
425
  };
295
426
  state.knowledgeMemory.push(record);
427
+ this.insertMemoryEvent({
428
+ ...scope,
429
+ entity_kind: 'knowledge_memory',
430
+ entity_id: String(record.id),
431
+ event_type: 'knowledge.created',
432
+ payload: {
433
+ after: cloneValue(record),
434
+ },
435
+ created_at: record.created_at,
436
+ });
296
437
  return record;
297
438
  },
298
439
  insertKnowledgeMemories(inputs) {
@@ -377,6 +518,9 @@ export function createInMemoryAdapter(telemetry) {
377
518
  getKnowledgeMemoryById(id) {
378
519
  return state.knowledgeMemory.find((item) => item.id === id) ?? null;
379
520
  },
521
+ getExistingKnowledgeMemoryIds(ids) {
522
+ return filterExistingIds(state.knowledgeMemory, ids);
523
+ },
380
524
  getActiveKnowledgeMemory(scope) {
381
525
  return state.knowledgeMemory.filter((item) => matchesScope(item, scope) && item.superseded_by_id === null && item.retired_at === null);
382
526
  },
@@ -386,12 +530,12 @@ export function createInMemoryAdapter(telemetry) {
386
530
  item.retired_at === null), options);
387
531
  },
388
532
  getActiveKnowledgeCrossScope(scope, level) {
389
- return state.knowledgeMemory.filter((item) => matchesLevel(item, scope, level) &&
533
+ return state.knowledgeMemory.filter((item) => matchesScopeLevel(item, scope, level) &&
390
534
  item.superseded_by_id === null &&
391
535
  item.retired_at === null);
392
536
  },
393
537
  getKnowledgeSince(scope, level, since) {
394
- return state.knowledgeMemory.filter((item) => matchesLevel(item, scope, level) &&
538
+ return state.knowledgeMemory.filter((item) => matchesScopeLevel(item, scope, level) &&
395
539
  item.created_at >= since &&
396
540
  item.superseded_by_id === null &&
397
541
  item.retired_at === null);
@@ -425,7 +569,7 @@ export function createInMemoryAdapter(telemetry) {
425
569
  const startedAt = Date.now();
426
570
  const resolved = resolveSearchOptions(options);
427
571
  const results = state.knowledgeMemory
428
- .filter((item) => matchesLevel(item, scope, level) &&
572
+ .filter((item) => matchesScopeLevel(item, scope, level) &&
429
573
  (!resolved.activeOnly ||
430
574
  (item.superseded_by_id === null && item.retired_at === null)))
431
575
  .filter((item) => matchesKnowledgeSearchOptions(item, resolved))
@@ -488,6 +632,7 @@ export function createInMemoryAdapter(telemetry) {
488
632
  const item = state.knowledgeMemory.find((entry) => entry.id === id);
489
633
  if (!item)
490
634
  return null;
635
+ const before = cloneValue(item);
491
636
  if (patch.knowledge_state !== undefined)
492
637
  item.knowledge_state = patch.knowledge_state;
493
638
  if (patch.knowledge_class !== undefined)
@@ -519,26 +664,91 @@ export function createInMemoryAdapter(telemetry) {
519
664
  item.successful_use_count = patch.successful_use_count;
520
665
  if (patch.failed_use_count !== undefined)
521
666
  item.failed_use_count = patch.failed_use_count;
667
+ this.insertMemoryEvent({
668
+ ...normalizeScope(item),
669
+ entity_kind: 'knowledge_memory',
670
+ entity_id: String(item.id),
671
+ event_type: 'knowledge.updated',
672
+ payload: {
673
+ before,
674
+ after: cloneValue(item),
675
+ patch: cloneValue(patch),
676
+ },
677
+ created_at: nowSeconds(),
678
+ });
522
679
  return item;
523
680
  },
524
681
  touchKnowledgeMemory(id) {
525
682
  const item = state.knowledgeMemory.find((entry) => entry.id === id);
526
683
  if (!item)
527
684
  return;
685
+ const before = cloneValue(item);
528
686
  item.last_accessed_at = nowSeconds();
529
687
  item.access_count += 1;
688
+ this.insertMemoryEvent({
689
+ ...normalizeScope(item),
690
+ entity_kind: 'knowledge_memory',
691
+ entity_id: String(item.id),
692
+ event_type: 'knowledge.touched',
693
+ payload: {
694
+ before,
695
+ after: cloneValue(item),
696
+ patch: {
697
+ last_accessed_at: item.last_accessed_at,
698
+ access_count: item.access_count,
699
+ },
700
+ },
701
+ created_at: item.last_accessed_at,
702
+ });
703
+ },
704
+ touchKnowledgeMemories(ids) {
705
+ const uniqueIds = [...new Set(ids)].filter((id) => Number.isInteger(id) && id > 0);
706
+ for (const id of uniqueIds) {
707
+ this.touchKnowledgeMemory(id);
708
+ }
530
709
  },
531
710
  retireKnowledgeMemory(id, retiredAt = nowSeconds()) {
532
711
  const item = state.knowledgeMemory.find((entry) => entry.id === id);
533
- if (item)
534
- item.retired_at = retiredAt;
712
+ if (!item)
713
+ return;
714
+ const before = cloneValue(item);
715
+ item.retired_at = retiredAt;
716
+ this.insertMemoryEvent({
717
+ ...normalizeScope(item),
718
+ entity_kind: 'knowledge_memory',
719
+ entity_id: String(item.id),
720
+ event_type: 'knowledge.retired',
721
+ payload: {
722
+ before,
723
+ after: cloneValue(item),
724
+ patch: {
725
+ retired_at: retiredAt,
726
+ },
727
+ },
728
+ created_at: retiredAt,
729
+ });
535
730
  },
536
731
  supersedeKnowledgeMemory(oldId, newId) {
537
732
  const item = state.knowledgeMemory.find((entry) => entry.id === oldId);
538
733
  if (item) {
734
+ const before = cloneValue(item);
539
735
  item.superseded_by_id = newId;
540
736
  item.superseded_at = nowSeconds();
541
737
  item.knowledge_state = 'superseded';
738
+ this.insertMemoryEvent({
739
+ ...normalizeScope(item),
740
+ entity_kind: 'knowledge_memory',
741
+ entity_id: String(item.id),
742
+ event_type: 'knowledge.superseded',
743
+ payload: {
744
+ before,
745
+ after: cloneValue(item),
746
+ refs: {
747
+ new_id: newId,
748
+ },
749
+ },
750
+ created_at: item.superseded_at,
751
+ });
542
752
  }
543
753
  },
544
754
  insertWorkItem(input) {
@@ -548,34 +758,697 @@ export function createInMemoryAdapter(telemetry) {
548
758
  ...scope,
549
759
  id: ids.workItem++,
550
760
  session_id: input.session_id ?? null,
761
+ visibility_class: input.visibility_class ?? 'private',
551
762
  kind: input.kind,
552
763
  title: input.title,
553
764
  detail: input.detail ?? null,
554
765
  status: input.status ?? 'open',
555
766
  source_working_memory_id: input.source_working_memory_id ?? null,
767
+ version: 1,
556
768
  created_at: createdAt,
557
769
  updated_at: createdAt,
558
770
  };
559
771
  state.workItems.push(item);
772
+ this.insertMemoryEvent({
773
+ ...scope,
774
+ session_id: item.session_id,
775
+ entity_kind: 'work_item',
776
+ entity_id: String(item.id),
777
+ event_type: 'work_item.created',
778
+ payload: {
779
+ after: cloneValue(item),
780
+ },
781
+ created_at: item.created_at,
782
+ });
560
783
  return item;
561
784
  },
785
+ getWorkItemById(id) {
786
+ const item = state.workItems.find((entry) => entry.id === id);
787
+ return item ? cloneValue(item) : null;
788
+ },
789
+ getExistingWorkItemIds(ids) {
790
+ return filterExistingIds(state.workItems, ids);
791
+ },
562
792
  getActiveWorkItems(scope) {
563
793
  return state.workItems.filter((item) => matchesScope(item, scope) && item.status !== 'done');
564
794
  },
795
+ getActiveWorkItemsCrossScope(scope, level) {
796
+ return state.workItems.filter((item) => matchesScopeLevel(item, scope, level) && item.status !== 'done');
797
+ },
565
798
  getWorkItemsByTimeRange(scope, range) {
566
799
  return state.workItems.filter((item) => matchesScope(item, scope) && inRange(item.created_at, range));
567
800
  },
801
+ getWorkItemsByTimeRangeCrossScope(scope, level, range) {
802
+ return state.workItems.filter((item) => matchesScopeLevel(item, scope, level) && inRange(item.created_at, range));
803
+ },
568
804
  updateWorkItemStatus(id, status) {
569
805
  const item = state.workItems.find((entry) => entry.id === id);
570
806
  if (!item)
571
807
  return;
808
+ const before = cloneValue(item);
572
809
  item.status = status;
573
810
  item.updated_at = nowSeconds();
811
+ item.version += 1;
812
+ this.insertMemoryEvent({
813
+ ...normalizeScope(item),
814
+ session_id: item.session_id,
815
+ entity_kind: 'work_item',
816
+ entity_id: String(item.id),
817
+ event_type: 'work_item.status_changed',
818
+ payload: {
819
+ before,
820
+ after: cloneValue(item),
821
+ patch: {
822
+ status,
823
+ updated_at: item.updated_at,
824
+ },
825
+ },
826
+ created_at: item.updated_at,
827
+ });
828
+ },
829
+ updateWorkItem(id, patch, options) {
830
+ const item = state.workItems.find((entry) => entry.id === id);
831
+ if (!item)
832
+ return null;
833
+ if (options?.expectedVersion != null && item.version !== options.expectedVersion) {
834
+ throw new ConflictError(`Work item ${id} version mismatch`);
835
+ }
836
+ const before = cloneValue(item);
837
+ if (patch.title !== undefined)
838
+ item.title = patch.title;
839
+ if (patch.detail !== undefined)
840
+ item.detail = patch.detail ?? null;
841
+ if (patch.status !== undefined)
842
+ item.status = patch.status;
843
+ if (patch.visibility_class !== undefined) {
844
+ assertMemoryVisibilityClass(patch.visibility_class);
845
+ item.visibility_class = patch.visibility_class;
846
+ }
847
+ item.updated_at = nowSeconds();
848
+ item.version += 1;
849
+ this.insertMemoryEvent({
850
+ ...normalizeScope(item),
851
+ session_id: item.session_id,
852
+ entity_kind: 'work_item',
853
+ entity_id: String(item.id),
854
+ event_type: patch.visibility_class !== undefined &&
855
+ patch.title === undefined &&
856
+ patch.detail === undefined &&
857
+ patch.status === undefined
858
+ ? 'work_item.visibility_changed'
859
+ : 'work_item.updated',
860
+ payload: {
861
+ before,
862
+ after: cloneValue(item),
863
+ patch: cloneValue(patch),
864
+ },
865
+ created_at: item.updated_at,
866
+ });
867
+ return cloneValue(item);
574
868
  },
575
869
  deleteWorkItem(id) {
576
870
  const index = state.workItems.findIndex((item) => item.id === id);
577
- if (index >= 0)
578
- state.workItems.splice(index, 1);
871
+ if (index < 0)
872
+ return;
873
+ const [item] = state.workItems.splice(index, 1);
874
+ this.insertMemoryEvent({
875
+ ...normalizeScope(item),
876
+ session_id: item.session_id,
877
+ entity_kind: 'work_item',
878
+ entity_id: String(item.id),
879
+ event_type: 'work_item.deleted',
880
+ payload: {
881
+ before: cloneValue(item),
882
+ },
883
+ created_at: nowSeconds(),
884
+ });
885
+ },
886
+ claimWorkItem(input) {
887
+ assertActorRef(input.actor);
888
+ const workItem = state.workItems.find((item) => item.id === input.work_item_id);
889
+ if (!workItem) {
890
+ throw new ConflictError(`Work item ${input.work_item_id} does not exist`);
891
+ }
892
+ if (workItem.status === 'done') {
893
+ throw new ConflictError(`Work item ${input.work_item_id} is already done`);
894
+ }
895
+ const now = input.claimed_at ?? nowSeconds();
896
+ const existing = state.workClaims.find((claim) => claim.work_item_id === input.work_item_id);
897
+ if (existing && isClaimExpired(existing, now)) {
898
+ const before = cloneValue(existing);
899
+ existing.status = 'expired';
900
+ existing.released_at = now;
901
+ existing.release_reason = 'expired';
902
+ existing.version += 1;
903
+ this.insertMemoryEvent({
904
+ ...normalizeScope(existing),
905
+ session_id: existing.session_id,
906
+ actor_id: existing.actor.actor_id,
907
+ actor_kind: existing.actor.actor_kind,
908
+ actor_system_id: existing.actor.system_id,
909
+ actor_display_name: existing.actor.display_name,
910
+ actor_metadata: existing.actor.metadata,
911
+ entity_kind: 'work_claim',
912
+ entity_id: String(existing.id),
913
+ event_type: 'work_claim.expired',
914
+ payload: { before, after: cloneValue(existing) },
915
+ created_at: now,
916
+ });
917
+ }
918
+ const active = state.workClaims.find((claim) => claim.work_item_id === input.work_item_id && claim.status === 'active');
919
+ if (active) {
920
+ if (!matchesActor(input.actor, active.actor)) {
921
+ throw new ConflictError(`Work item ${input.work_item_id} is already claimed`);
922
+ }
923
+ active.expires_at = Math.max(active.expires_at, now) + (input.lease_seconds ?? 300);
924
+ active.version += 1;
925
+ this.insertMemoryEvent({
926
+ ...normalizeScope(active),
927
+ session_id: active.session_id,
928
+ actor_id: active.actor.actor_id,
929
+ actor_kind: active.actor.actor_kind,
930
+ actor_system_id: active.actor.system_id,
931
+ actor_display_name: active.actor.display_name,
932
+ actor_metadata: active.actor.metadata,
933
+ entity_kind: 'work_claim',
934
+ entity_id: String(active.id),
935
+ event_type: 'work_claim.renewed',
936
+ payload: { after: cloneValue(active) },
937
+ created_at: now,
938
+ });
939
+ return cloneValue(active);
940
+ }
941
+ const normalized = normalizeScope(input);
942
+ const claim = {
943
+ ...normalized,
944
+ id: ids.workClaim++,
945
+ work_item_id: input.work_item_id,
946
+ actor: cloneValue(input.actor),
947
+ session_id: input.session_id ?? null,
948
+ claim_token: makeClaimToken(),
949
+ status: 'active',
950
+ claimed_at: now,
951
+ expires_at: now + (input.lease_seconds ?? 300),
952
+ released_at: null,
953
+ release_reason: null,
954
+ source_event_id: null,
955
+ visibility_class: input.visibility_class,
956
+ version: 1,
957
+ };
958
+ state.workClaims = state.workClaims.filter((entry) => !(entry.work_item_id === claim.work_item_id && entry.status === 'active'));
959
+ state.workClaims.push(claim);
960
+ this.insertMemoryEvent({
961
+ ...normalized,
962
+ session_id: claim.session_id,
963
+ actor_id: claim.actor.actor_id,
964
+ actor_kind: claim.actor.actor_kind,
965
+ actor_system_id: claim.actor.system_id,
966
+ actor_display_name: claim.actor.display_name,
967
+ actor_metadata: claim.actor.metadata,
968
+ entity_kind: 'work_claim',
969
+ entity_id: String(claim.id),
970
+ event_type: 'work_claim.claimed',
971
+ payload: { after: cloneValue(claim) },
972
+ created_at: now,
973
+ });
974
+ return cloneValue(claim);
975
+ },
976
+ renewWorkClaim(claimId, actor, leaseSeconds = 300) {
977
+ assertActorRef(actor);
978
+ const claim = state.workClaims.find((entry) => entry.id === claimId);
979
+ if (!claim)
980
+ return null;
981
+ if (!matchesActor(actor, claim.actor)) {
982
+ throw new ConflictError(`Claim ${claimId} is owned by another actor`);
983
+ }
984
+ const now = nowSeconds();
985
+ if (isClaimExpired(claim, now)) {
986
+ claim.status = 'expired';
987
+ claim.released_at = now;
988
+ claim.release_reason = 'expired';
989
+ claim.version += 1;
990
+ this.insertMemoryEvent({
991
+ ...normalizeScope(claim),
992
+ session_id: claim.session_id,
993
+ actor_id: claim.actor.actor_id,
994
+ actor_kind: claim.actor.actor_kind,
995
+ actor_system_id: claim.actor.system_id,
996
+ actor_display_name: claim.actor.display_name,
997
+ actor_metadata: claim.actor.metadata,
998
+ entity_kind: 'work_claim',
999
+ entity_id: String(claim.id),
1000
+ event_type: 'work_claim.expired',
1001
+ payload: { after: cloneValue(claim) },
1002
+ created_at: now,
1003
+ });
1004
+ return null;
1005
+ }
1006
+ if (claim.status !== 'active') {
1007
+ throw new ConflictError(`Claim ${claimId} is no longer active`);
1008
+ }
1009
+ claim.expires_at = Math.max(claim.expires_at, now) + leaseSeconds;
1010
+ claim.version += 1;
1011
+ this.insertMemoryEvent({
1012
+ ...normalizeScope(claim),
1013
+ session_id: claim.session_id,
1014
+ actor_id: claim.actor.actor_id,
1015
+ actor_kind: claim.actor.actor_kind,
1016
+ actor_system_id: claim.actor.system_id,
1017
+ actor_display_name: claim.actor.display_name,
1018
+ actor_metadata: claim.actor.metadata,
1019
+ entity_kind: 'work_claim',
1020
+ entity_id: String(claim.id),
1021
+ event_type: 'work_claim.renewed',
1022
+ payload: { after: cloneValue(claim) },
1023
+ created_at: now,
1024
+ });
1025
+ return cloneValue(claim);
1026
+ },
1027
+ releaseWorkClaim(claimId, actor, reason) {
1028
+ assertActorRef(actor);
1029
+ const claim = state.workClaims.find((entry) => entry.id === claimId);
1030
+ if (!claim)
1031
+ return null;
1032
+ if (!matchesActor(actor, claim.actor)) {
1033
+ throw new ConflictError(`Claim ${claimId} is owned by another actor`);
1034
+ }
1035
+ if (claim.status !== 'active') {
1036
+ throw new ConflictError(`Claim ${claimId} is no longer active`);
1037
+ }
1038
+ const now = nowSeconds();
1039
+ claim.status = 'released';
1040
+ claim.released_at = now;
1041
+ claim.release_reason = reason ?? null;
1042
+ claim.version += 1;
1043
+ this.insertMemoryEvent({
1044
+ ...normalizeScope(claim),
1045
+ session_id: claim.session_id,
1046
+ actor_id: claim.actor.actor_id,
1047
+ actor_kind: claim.actor.actor_kind,
1048
+ actor_system_id: claim.actor.system_id,
1049
+ actor_display_name: claim.actor.display_name,
1050
+ actor_metadata: claim.actor.metadata,
1051
+ entity_kind: 'work_claim',
1052
+ entity_id: String(claim.id),
1053
+ event_type: 'work_claim.released',
1054
+ payload: { after: cloneValue(claim) },
1055
+ created_at: now,
1056
+ });
1057
+ return cloneValue(claim);
1058
+ },
1059
+ getActiveWorkClaim(workItemId) {
1060
+ const claim = state.workClaims.find((entry) => entry.work_item_id === workItemId && entry.status === 'active');
1061
+ if (claim && isClaimExpired(claim)) {
1062
+ claim.status = 'expired';
1063
+ claim.released_at = nowSeconds();
1064
+ claim.release_reason = 'expired';
1065
+ claim.version += 1;
1066
+ this.insertMemoryEvent({
1067
+ ...normalizeScope(claim),
1068
+ session_id: claim.session_id,
1069
+ actor_id: claim.actor.actor_id,
1070
+ actor_kind: claim.actor.actor_kind,
1071
+ actor_system_id: claim.actor.system_id,
1072
+ actor_display_name: claim.actor.display_name,
1073
+ actor_metadata: claim.actor.metadata,
1074
+ entity_kind: 'work_claim',
1075
+ entity_id: String(claim.id),
1076
+ event_type: 'work_claim.expired',
1077
+ payload: { after: cloneValue(claim) },
1078
+ created_at: claim.released_at,
1079
+ });
1080
+ return null;
1081
+ }
1082
+ return claim ? cloneValue(claim) : null;
1083
+ },
1084
+ listWorkClaims(scope, options) {
1085
+ const currentNow = nowSeconds();
1086
+ for (const claim of state.workClaims) {
1087
+ if (matchesScope(claim, scope) && isClaimExpired(claim, currentNow)) {
1088
+ claim.status = 'expired';
1089
+ claim.released_at = currentNow;
1090
+ claim.release_reason = 'expired';
1091
+ claim.version += 1;
1092
+ this.insertMemoryEvent({
1093
+ ...normalizeScope(claim),
1094
+ session_id: claim.session_id,
1095
+ actor_id: claim.actor.actor_id,
1096
+ actor_kind: claim.actor.actor_kind,
1097
+ actor_system_id: claim.actor.system_id,
1098
+ actor_display_name: claim.actor.display_name,
1099
+ actor_metadata: claim.actor.metadata,
1100
+ entity_kind: 'work_claim',
1101
+ entity_id: String(claim.id),
1102
+ event_type: 'work_claim.expired',
1103
+ payload: { after: cloneValue(claim) },
1104
+ created_at: currentNow,
1105
+ });
1106
+ }
1107
+ }
1108
+ return state.workClaims.filter((claim) => {
1109
+ if (!matchesScope(claim, scope))
1110
+ return false;
1111
+ if (!options?.includeExpired && claim.status === 'expired')
1112
+ return false;
1113
+ if (!options?.includeReleased && claim.status === 'released')
1114
+ return false;
1115
+ if (options?.sessionId && claim.session_id !== options.sessionId)
1116
+ return false;
1117
+ if (options?.visibilityClass && claim.visibility_class !== options.visibilityClass)
1118
+ return false;
1119
+ if (options?.actor && !matchesActor(options.actor, claim.actor))
1120
+ return false;
1121
+ return true;
1122
+ }).map(cloneValue);
1123
+ },
1124
+ listWorkClaimsCrossScope(scope, level, options) {
1125
+ const currentNow = nowSeconds();
1126
+ for (const claim of state.workClaims) {
1127
+ if (matchesScopeLevel(claim, scope, level) && isClaimExpired(claim, currentNow)) {
1128
+ claim.status = 'expired';
1129
+ claim.released_at = currentNow;
1130
+ claim.release_reason = 'expired';
1131
+ claim.version += 1;
1132
+ this.insertMemoryEvent({
1133
+ ...normalizeScope(claim),
1134
+ session_id: claim.session_id,
1135
+ actor_id: claim.actor.actor_id,
1136
+ actor_kind: claim.actor.actor_kind,
1137
+ actor_system_id: claim.actor.system_id,
1138
+ actor_display_name: claim.actor.display_name,
1139
+ actor_metadata: claim.actor.metadata,
1140
+ entity_kind: 'work_claim',
1141
+ entity_id: String(claim.id),
1142
+ event_type: 'work_claim.expired',
1143
+ payload: { after: cloneValue(claim) },
1144
+ created_at: currentNow,
1145
+ });
1146
+ }
1147
+ }
1148
+ return state.workClaims.filter((claim) => {
1149
+ if (!matchesScopeLevel(claim, scope, level))
1150
+ return false;
1151
+ if (!options?.includeExpired && claim.status === 'expired')
1152
+ return false;
1153
+ if (!options?.includeReleased && claim.status === 'released')
1154
+ return false;
1155
+ if (options?.sessionId && claim.session_id !== options.sessionId)
1156
+ return false;
1157
+ if (options?.visibilityClass && claim.visibility_class !== options.visibilityClass)
1158
+ return false;
1159
+ if (options?.actor && !matchesActor(options.actor, claim.actor))
1160
+ return false;
1161
+ return true;
1162
+ }).map(cloneValue);
1163
+ },
1164
+ createHandoff(input) {
1165
+ assertActorRef(input.from_actor, 'from_actor');
1166
+ assertActorRef(input.to_actor, 'to_actor');
1167
+ const normalized = normalizeScope(input);
1168
+ const handoff = {
1169
+ ...normalized,
1170
+ id: ids.handoff++,
1171
+ work_item_id: input.work_item_id,
1172
+ from_actor: cloneValue(input.from_actor),
1173
+ to_actor: cloneValue(input.to_actor),
1174
+ session_id: input.session_id ?? null,
1175
+ summary: input.summary,
1176
+ context_bundle_ref: input.context_bundle_ref ?? null,
1177
+ status: 'pending',
1178
+ created_at: input.created_at ?? nowSeconds(),
1179
+ accepted_at: null,
1180
+ rejected_at: null,
1181
+ canceled_at: null,
1182
+ expires_at: input.expires_at ?? null,
1183
+ decision_reason: null,
1184
+ source_event_id: null,
1185
+ visibility_class: input.visibility_class,
1186
+ version: 1,
1187
+ };
1188
+ state.handoffs.push(handoff);
1189
+ this.insertMemoryEvent({
1190
+ ...normalized,
1191
+ session_id: handoff.session_id,
1192
+ actor_id: handoff.from_actor.actor_id,
1193
+ actor_kind: handoff.from_actor.actor_kind,
1194
+ actor_system_id: handoff.from_actor.system_id,
1195
+ actor_display_name: handoff.from_actor.display_name,
1196
+ actor_metadata: handoff.from_actor.metadata,
1197
+ entity_kind: 'handoff',
1198
+ entity_id: String(handoff.id),
1199
+ event_type: 'handoff.created',
1200
+ payload: { after: cloneValue(handoff) },
1201
+ created_at: handoff.created_at,
1202
+ });
1203
+ return cloneValue(handoff);
1204
+ },
1205
+ acceptHandoff(handoffId, actor, reason) {
1206
+ assertActorRef(actor);
1207
+ const handoff = state.handoffs.find((entry) => entry.id === handoffId);
1208
+ if (!handoff)
1209
+ return null;
1210
+ if (!matchesActor(actor, handoff.to_actor)) {
1211
+ throw new ConflictError(`Handoff ${handoffId} is assigned to another actor`);
1212
+ }
1213
+ if (isHandoffExpired(handoff)) {
1214
+ handoff.status = 'expired';
1215
+ handoff.decision_reason = 'expired';
1216
+ handoff.version += 1;
1217
+ this.insertMemoryEvent({
1218
+ ...normalizeScope(handoff),
1219
+ session_id: handoff.session_id,
1220
+ actor_id: handoff.to_actor.actor_id,
1221
+ actor_kind: handoff.to_actor.actor_kind,
1222
+ actor_system_id: handoff.to_actor.system_id,
1223
+ actor_display_name: handoff.to_actor.display_name,
1224
+ actor_metadata: handoff.to_actor.metadata,
1225
+ entity_kind: 'handoff',
1226
+ entity_id: String(handoff.id),
1227
+ event_type: 'handoff.expired',
1228
+ payload: { after: cloneValue(handoff) },
1229
+ created_at: nowSeconds(),
1230
+ });
1231
+ return null;
1232
+ }
1233
+ if (handoff.status !== 'pending') {
1234
+ throw new ConflictError(`Handoff ${handoffId} is no longer pending`);
1235
+ }
1236
+ const activeClaim = state.workClaims.find((claim) => claim.work_item_id === handoff.work_item_id && claim.status === 'active' && !isClaimExpired(claim));
1237
+ if (activeClaim && !matchesActor(handoff.from_actor, activeClaim.actor)) {
1238
+ throw new ConflictError(`Work item ${handoff.work_item_id} has another active owner`);
1239
+ }
1240
+ const now = nowSeconds();
1241
+ if (activeClaim && matchesActor(handoff.from_actor, activeClaim.actor)) {
1242
+ this.releaseWorkClaim(activeClaim.id, handoff.from_actor, 'handoff_accepted');
1243
+ }
1244
+ this.claimWorkItem({
1245
+ ...normalizeScope(handoff),
1246
+ work_item_id: handoff.work_item_id,
1247
+ actor,
1248
+ session_id: handoff.session_id,
1249
+ visibility_class: handoff.visibility_class,
1250
+ });
1251
+ handoff.status = 'accepted';
1252
+ handoff.accepted_at = now;
1253
+ handoff.decision_reason = reason ?? null;
1254
+ handoff.version += 1;
1255
+ this.insertMemoryEvent({
1256
+ ...normalizeScope(handoff),
1257
+ session_id: handoff.session_id,
1258
+ actor_id: actor.actor_id,
1259
+ actor_kind: actor.actor_kind,
1260
+ actor_system_id: actor.system_id,
1261
+ actor_display_name: actor.display_name,
1262
+ actor_metadata: actor.metadata,
1263
+ entity_kind: 'handoff',
1264
+ entity_id: String(handoff.id),
1265
+ event_type: 'handoff.accepted',
1266
+ payload: { after: cloneValue(handoff) },
1267
+ created_at: now,
1268
+ });
1269
+ return cloneValue(handoff);
1270
+ },
1271
+ rejectHandoff(handoffId, actor, reason) {
1272
+ assertActorRef(actor);
1273
+ const handoff = state.handoffs.find((entry) => entry.id === handoffId);
1274
+ if (!handoff)
1275
+ return null;
1276
+ if (!matchesActor(actor, handoff.to_actor)) {
1277
+ throw new ConflictError(`Handoff ${handoffId} is assigned to another actor`);
1278
+ }
1279
+ if (isHandoffExpired(handoff)) {
1280
+ handoff.status = 'expired';
1281
+ handoff.decision_reason = 'expired';
1282
+ handoff.version += 1;
1283
+ this.insertMemoryEvent({
1284
+ ...normalizeScope(handoff),
1285
+ session_id: handoff.session_id,
1286
+ actor_id: handoff.to_actor.actor_id,
1287
+ actor_kind: handoff.to_actor.actor_kind,
1288
+ actor_system_id: handoff.to_actor.system_id,
1289
+ actor_display_name: handoff.to_actor.display_name,
1290
+ actor_metadata: handoff.to_actor.metadata,
1291
+ entity_kind: 'handoff',
1292
+ entity_id: String(handoff.id),
1293
+ event_type: 'handoff.expired',
1294
+ payload: { after: cloneValue(handoff) },
1295
+ created_at: nowSeconds(),
1296
+ });
1297
+ return null;
1298
+ }
1299
+ if (handoff.status !== 'pending') {
1300
+ throw new ConflictError(`Handoff ${handoffId} is no longer pending`);
1301
+ }
1302
+ handoff.status = 'rejected';
1303
+ handoff.rejected_at = nowSeconds();
1304
+ handoff.decision_reason = reason ?? null;
1305
+ handoff.version += 1;
1306
+ this.insertMemoryEvent({
1307
+ ...normalizeScope(handoff),
1308
+ session_id: handoff.session_id,
1309
+ actor_id: actor.actor_id,
1310
+ actor_kind: actor.actor_kind,
1311
+ actor_system_id: actor.system_id,
1312
+ actor_display_name: actor.display_name,
1313
+ actor_metadata: actor.metadata,
1314
+ entity_kind: 'handoff',
1315
+ entity_id: String(handoff.id),
1316
+ event_type: 'handoff.rejected',
1317
+ payload: { after: cloneValue(handoff) },
1318
+ created_at: handoff.rejected_at,
1319
+ });
1320
+ return cloneValue(handoff);
1321
+ },
1322
+ cancelHandoff(handoffId, actor, reason) {
1323
+ assertActorRef(actor);
1324
+ const handoff = state.handoffs.find((entry) => entry.id === handoffId);
1325
+ if (!handoff)
1326
+ return null;
1327
+ if (!matchesActor(actor, handoff.from_actor)) {
1328
+ throw new ConflictError(`Handoff ${handoffId} was created by another actor`);
1329
+ }
1330
+ if (isHandoffExpired(handoff)) {
1331
+ handoff.status = 'expired';
1332
+ handoff.decision_reason = 'expired';
1333
+ handoff.version += 1;
1334
+ this.insertMemoryEvent({
1335
+ ...normalizeScope(handoff),
1336
+ session_id: handoff.session_id,
1337
+ actor_id: handoff.from_actor.actor_id,
1338
+ actor_kind: handoff.from_actor.actor_kind,
1339
+ actor_system_id: handoff.from_actor.system_id,
1340
+ actor_display_name: handoff.from_actor.display_name,
1341
+ actor_metadata: handoff.from_actor.metadata,
1342
+ entity_kind: 'handoff',
1343
+ entity_id: String(handoff.id),
1344
+ event_type: 'handoff.expired',
1345
+ payload: { after: cloneValue(handoff) },
1346
+ created_at: nowSeconds(),
1347
+ });
1348
+ return null;
1349
+ }
1350
+ if (handoff.status !== 'pending') {
1351
+ throw new ConflictError(`Handoff ${handoffId} is no longer pending`);
1352
+ }
1353
+ handoff.status = 'canceled';
1354
+ handoff.canceled_at = nowSeconds();
1355
+ handoff.decision_reason = reason ?? null;
1356
+ handoff.version += 1;
1357
+ this.insertMemoryEvent({
1358
+ ...normalizeScope(handoff),
1359
+ session_id: handoff.session_id,
1360
+ actor_id: actor.actor_id,
1361
+ actor_kind: actor.actor_kind,
1362
+ actor_system_id: actor.system_id,
1363
+ actor_display_name: actor.display_name,
1364
+ actor_metadata: actor.metadata,
1365
+ entity_kind: 'handoff',
1366
+ entity_id: String(handoff.id),
1367
+ event_type: 'handoff.canceled',
1368
+ payload: { after: cloneValue(handoff) },
1369
+ created_at: handoff.canceled_at,
1370
+ });
1371
+ return cloneValue(handoff);
1372
+ },
1373
+ listHandoffs(scope, options) {
1374
+ const currentNow = nowSeconds();
1375
+ for (const handoff of state.handoffs) {
1376
+ if (matchesScope(handoff, scope) && isHandoffExpired(handoff, currentNow)) {
1377
+ handoff.status = 'expired';
1378
+ handoff.decision_reason = 'expired';
1379
+ handoff.version += 1;
1380
+ this.insertMemoryEvent({
1381
+ ...normalizeScope(handoff),
1382
+ session_id: handoff.session_id,
1383
+ actor_id: handoff.to_actor.actor_id,
1384
+ actor_kind: handoff.to_actor.actor_kind,
1385
+ actor_system_id: handoff.to_actor.system_id,
1386
+ actor_display_name: handoff.to_actor.display_name,
1387
+ actor_metadata: handoff.to_actor.metadata,
1388
+ entity_kind: 'handoff',
1389
+ entity_id: String(handoff.id),
1390
+ event_type: 'handoff.expired',
1391
+ payload: { after: cloneValue(handoff) },
1392
+ created_at: currentNow,
1393
+ });
1394
+ }
1395
+ }
1396
+ return state.handoffs.filter((handoff) => {
1397
+ if (!matchesScope(handoff, scope))
1398
+ return false;
1399
+ if (options?.sessionId && handoff.session_id !== options.sessionId)
1400
+ return false;
1401
+ if (options?.statuses && !options.statuses.includes(handoff.status))
1402
+ return false;
1403
+ if (!options?.actor)
1404
+ return true;
1405
+ if (options.direction === 'inbound')
1406
+ return matchesActor(options.actor, handoff.to_actor);
1407
+ if (options.direction === 'outbound')
1408
+ return matchesActor(options.actor, handoff.from_actor);
1409
+ return (matchesActor(options.actor, handoff.to_actor) ||
1410
+ matchesActor(options.actor, handoff.from_actor));
1411
+ }).map(cloneValue);
1412
+ },
1413
+ listHandoffsCrossScope(scope, level, options) {
1414
+ const currentNow = nowSeconds();
1415
+ for (const handoff of state.handoffs) {
1416
+ if (matchesScopeLevel(handoff, scope, level) && isHandoffExpired(handoff, currentNow)) {
1417
+ handoff.status = 'expired';
1418
+ handoff.decision_reason = 'expired';
1419
+ handoff.version += 1;
1420
+ this.insertMemoryEvent({
1421
+ ...normalizeScope(handoff),
1422
+ session_id: handoff.session_id,
1423
+ actor_id: handoff.to_actor.actor_id,
1424
+ actor_kind: handoff.to_actor.actor_kind,
1425
+ actor_system_id: handoff.to_actor.system_id,
1426
+ actor_display_name: handoff.to_actor.display_name,
1427
+ actor_metadata: handoff.to_actor.metadata,
1428
+ entity_kind: 'handoff',
1429
+ entity_id: String(handoff.id),
1430
+ event_type: 'handoff.expired',
1431
+ payload: { after: cloneValue(handoff) },
1432
+ created_at: currentNow,
1433
+ });
1434
+ }
1435
+ }
1436
+ return state.handoffs.filter((handoff) => {
1437
+ if (!matchesScopeLevel(handoff, scope, level))
1438
+ return false;
1439
+ if (options?.sessionId && handoff.session_id !== options.sessionId)
1440
+ return false;
1441
+ if (options?.statuses && !options.statuses.includes(handoff.status))
1442
+ return false;
1443
+ if (!options?.actor)
1444
+ return true;
1445
+ if (options.direction === 'inbound')
1446
+ return matchesActor(options.actor, handoff.to_actor);
1447
+ if (options.direction === 'outbound')
1448
+ return matchesActor(options.actor, handoff.from_actor);
1449
+ return (matchesActor(options.actor, handoff.to_actor) ||
1450
+ matchesActor(options.actor, handoff.from_actor));
1451
+ }).map(cloneValue);
579
1452
  },
580
1453
  upsertContextMonitor(input) {
581
1454
  const scope = validateContextMonitorUpsert(input);
@@ -636,6 +1509,366 @@ export function createInMemoryAdapter(telemetry) {
636
1509
  .sort((a, b) => b.id - a.id)
637
1510
  .slice(0, limit);
638
1511
  },
1512
+ insertPlaybook(input) {
1513
+ const scope = normalizeScope(input);
1514
+ const now = nowSeconds();
1515
+ const record = {
1516
+ ...scope,
1517
+ id: ids.playbook++,
1518
+ visibility_class: input.visibility_class ?? 'private',
1519
+ title: input.title,
1520
+ description: input.description,
1521
+ instructions: input.instructions,
1522
+ references: input.references ? [...input.references] : [],
1523
+ templates: input.templates ? [...input.templates] : [],
1524
+ scripts: input.scripts ? [...input.scripts] : [],
1525
+ assets: input.assets ? [...input.assets] : [],
1526
+ tags: input.tags ? [...input.tags] : [],
1527
+ status: input.status ?? 'draft',
1528
+ source_session_id: input.source_session_id ?? null,
1529
+ source_working_memory_id: input.source_working_memory_id ?? null,
1530
+ revision_count: 0,
1531
+ last_used_at: null,
1532
+ use_count: 0,
1533
+ created_at: input.created_at ?? now,
1534
+ updated_at: now,
1535
+ schema_version: SCHEMA_VERSION,
1536
+ };
1537
+ state.playbooks.push(record);
1538
+ this.insertMemoryEvent({
1539
+ ...scope,
1540
+ session_id: record.source_session_id,
1541
+ entity_kind: 'playbook',
1542
+ entity_id: String(record.id),
1543
+ event_type: 'playbook.created',
1544
+ payload: {
1545
+ after: cloneValue(record),
1546
+ },
1547
+ created_at: record.created_at,
1548
+ });
1549
+ return record;
1550
+ },
1551
+ getPlaybookById(id) {
1552
+ return state.playbooks.find((p) => p.id === id) ?? null;
1553
+ },
1554
+ getExistingPlaybookIds(ids) {
1555
+ return filterExistingIds(state.playbooks, ids);
1556
+ },
1557
+ getActivePlaybooks(scope) {
1558
+ return state.playbooks.filter((p) => matchesScope(p, scope) && (p.status === 'draft' || p.status === 'active'));
1559
+ },
1560
+ getActivePlaybooksCrossScope(scope, level) {
1561
+ return state.playbooks.filter((p) => matchesScopeLevel(p, scope, level) && (p.status === 'draft' || p.status === 'active'));
1562
+ },
1563
+ searchPlaybooks(scope, query, options) {
1564
+ const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
1565
+ if (tokens.length === 0)
1566
+ return [];
1567
+ const limit = options?.limit ?? 20;
1568
+ const activeOnly = options?.activeOnly ?? true;
1569
+ return state.playbooks
1570
+ .filter((p) => {
1571
+ if (!matchesScope(p, scope))
1572
+ return false;
1573
+ if (activeOnly && (p.status === 'archived' || p.status === 'deprecated'))
1574
+ return false;
1575
+ const text = `${p.title} ${p.description} ${p.instructions}`.toLowerCase();
1576
+ return tokens.every((token) => text.includes(token));
1577
+ })
1578
+ .slice(0, limit)
1579
+ .map((item, index) => ({ item, rank: index }));
1580
+ },
1581
+ searchPlaybooksCrossScope(scope, level, query, options) {
1582
+ const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
1583
+ if (tokens.length === 0)
1584
+ return [];
1585
+ const limit = options?.limit ?? 20;
1586
+ const activeOnly = options?.activeOnly ?? true;
1587
+ return state.playbooks
1588
+ .filter((p) => {
1589
+ if (!matchesScopeLevel(p, scope, level))
1590
+ return false;
1591
+ if (activeOnly && (p.status === 'archived' || p.status === 'deprecated'))
1592
+ return false;
1593
+ const text = `${p.title} ${p.description} ${p.instructions}`.toLowerCase();
1594
+ return tokens.every((token) => text.includes(token));
1595
+ })
1596
+ .slice(0, limit)
1597
+ .map((item, index) => ({ item, rank: index }));
1598
+ },
1599
+ updatePlaybook(id, patch) {
1600
+ const playbook = state.playbooks.find((p) => p.id === id);
1601
+ if (!playbook)
1602
+ return null;
1603
+ const before = cloneValue(playbook);
1604
+ if (patch.title != null)
1605
+ playbook.title = patch.title;
1606
+ if (patch.description != null)
1607
+ playbook.description = patch.description;
1608
+ if (patch.instructions != null)
1609
+ playbook.instructions = patch.instructions;
1610
+ if (patch.references != null)
1611
+ playbook.references = [...patch.references];
1612
+ if (patch.templates != null)
1613
+ playbook.templates = [...patch.templates];
1614
+ if (patch.scripts != null)
1615
+ playbook.scripts = [...patch.scripts];
1616
+ if (patch.assets != null)
1617
+ playbook.assets = [...patch.assets];
1618
+ if (patch.tags != null)
1619
+ playbook.tags = [...patch.tags];
1620
+ if (patch.status != null)
1621
+ playbook.status = patch.status;
1622
+ playbook.updated_at = nowSeconds();
1623
+ this.insertMemoryEvent({
1624
+ ...normalizeScope(playbook),
1625
+ session_id: playbook.source_session_id,
1626
+ entity_kind: 'playbook',
1627
+ entity_id: String(playbook.id),
1628
+ event_type: 'playbook.updated',
1629
+ payload: {
1630
+ before,
1631
+ after: cloneValue(playbook),
1632
+ patch: cloneValue(patch),
1633
+ },
1634
+ created_at: playbook.updated_at,
1635
+ });
1636
+ return playbook;
1637
+ },
1638
+ recordPlaybookUse(id) {
1639
+ const playbook = state.playbooks.find((p) => p.id === id);
1640
+ if (playbook) {
1641
+ const before = cloneValue(playbook);
1642
+ playbook.use_count += 1;
1643
+ playbook.last_used_at = nowSeconds();
1644
+ this.insertMemoryEvent({
1645
+ ...normalizeScope(playbook),
1646
+ session_id: playbook.source_session_id,
1647
+ entity_kind: 'playbook',
1648
+ entity_id: String(playbook.id),
1649
+ event_type: 'playbook.used',
1650
+ payload: {
1651
+ before,
1652
+ after: cloneValue(playbook),
1653
+ refs: {
1654
+ use_count: playbook.use_count,
1655
+ },
1656
+ },
1657
+ created_at: playbook.last_used_at ?? nowSeconds(),
1658
+ });
1659
+ }
1660
+ },
1661
+ insertPlaybookRevision(input) {
1662
+ const playbook = state.playbooks.find((p) => p.id === input.playbook_id);
1663
+ if (!playbook) {
1664
+ throw new Error(`Playbook ${input.playbook_id} not found`);
1665
+ }
1666
+ const now = nowSeconds();
1667
+ const record = {
1668
+ tenant_id: playbook.tenant_id,
1669
+ system_id: playbook.system_id,
1670
+ workspace_id: playbook.workspace_id,
1671
+ collaboration_id: playbook.collaboration_id,
1672
+ scope_id: playbook.scope_id,
1673
+ id: ids.playbookRevision++,
1674
+ playbook_id: input.playbook_id,
1675
+ instructions: input.instructions,
1676
+ revision_reason: input.revision_reason,
1677
+ source_session_id: input.source_session_id ?? null,
1678
+ created_at: input.created_at ?? now,
1679
+ };
1680
+ state.playbookRevisions.push(record);
1681
+ playbook.revision_count += 1;
1682
+ this.insertMemoryEvent({
1683
+ ...normalizeScope(record),
1684
+ session_id: record.source_session_id,
1685
+ entity_kind: 'playbook_revision',
1686
+ entity_id: String(record.id),
1687
+ event_type: 'playbook.revised',
1688
+ payload: {
1689
+ after: cloneValue(record),
1690
+ refs: {
1691
+ playbook_id: record.playbook_id,
1692
+ },
1693
+ },
1694
+ created_at: record.created_at,
1695
+ });
1696
+ return record;
1697
+ },
1698
+ getPlaybookRevisions(playbookId) {
1699
+ return state.playbookRevisions
1700
+ .filter((r) => r.playbook_id === playbookId)
1701
+ .sort((a, b) => b.created_at - a.created_at);
1702
+ },
1703
+ insertAssociation(input) {
1704
+ const scope = normalizeScope(input);
1705
+ // Enforce unique constraint
1706
+ const existing = state.associations.find((a) => a.source_kind === input.source_kind &&
1707
+ a.source_id === input.source_id &&
1708
+ a.target_kind === input.target_kind &&
1709
+ a.target_id === input.target_id &&
1710
+ a.association_type === input.association_type);
1711
+ if (existing) {
1712
+ throw new UniqueConstraintError(`Association already exists: ${input.source_kind}:${input.source_id} -> ${input.target_kind}:${input.target_id} (${input.association_type})`);
1713
+ }
1714
+ const record = {
1715
+ ...scope,
1716
+ id: ids.association++,
1717
+ visibility_class: input.visibility_class ?? 'private',
1718
+ source_kind: input.source_kind,
1719
+ source_id: input.source_id,
1720
+ target_kind: input.target_kind,
1721
+ target_id: input.target_id,
1722
+ association_type: input.association_type,
1723
+ confidence: input.confidence ?? 0.5,
1724
+ auto_generated: input.auto_generated ?? false,
1725
+ created_at: input.created_at ?? nowSeconds(),
1726
+ };
1727
+ state.associations.push(record);
1728
+ this.insertMemoryEvent({
1729
+ ...scope,
1730
+ entity_kind: 'association',
1731
+ entity_id: String(record.id),
1732
+ event_type: 'association.created',
1733
+ payload: {
1734
+ after: cloneValue(record),
1735
+ },
1736
+ created_at: record.created_at,
1737
+ });
1738
+ return record;
1739
+ },
1740
+ getAssociationById(id) {
1741
+ return state.associations.find((a) => a.id === id) ?? null;
1742
+ },
1743
+ getAssociationsFrom(kind, id, scope) {
1744
+ return state.associations.filter((a) => a.source_kind === kind && a.source_id === id && matchesScope(a, scope));
1745
+ },
1746
+ getAssociationsTo(kind, id, scope) {
1747
+ return state.associations.filter((a) => a.target_kind === kind && a.target_id === id && matchesScope(a, scope));
1748
+ },
1749
+ listAssociations(scope) {
1750
+ return state.associations.filter((association) => matchesScope(association, scope));
1751
+ },
1752
+ deleteAssociation(id) {
1753
+ const idx = state.associations.findIndex((a) => a.id === id);
1754
+ if (idx === -1)
1755
+ return;
1756
+ const [association] = state.associations.splice(idx, 1);
1757
+ this.insertMemoryEvent({
1758
+ ...normalizeScope(association),
1759
+ entity_kind: 'association',
1760
+ entity_id: String(association.id),
1761
+ event_type: 'association.deleted',
1762
+ payload: {
1763
+ before: cloneValue(association),
1764
+ },
1765
+ created_at: nowSeconds(),
1766
+ });
1767
+ },
1768
+ insertMemoryEvent(input) {
1769
+ const normalized = normalizeScope(input);
1770
+ const event = {
1771
+ ...normalized,
1772
+ event_id: String(ids.memoryEvent++),
1773
+ session_id: input.session_id ?? null,
1774
+ actor_id: input.actor_id ?? null,
1775
+ actor_kind: input.actor_kind ?? null,
1776
+ actor_system_id: input.actor_system_id ?? null,
1777
+ actor_display_name: input.actor_display_name ?? null,
1778
+ actor_metadata: input.actor_metadata ? cloneValue(input.actor_metadata) : null,
1779
+ entity_kind: input.entity_kind,
1780
+ entity_id: input.entity_id,
1781
+ event_type: input.event_type,
1782
+ payload: cloneValue(input.payload),
1783
+ causation_id: input.causation_id ?? null,
1784
+ correlation_id: input.correlation_id ?? null,
1785
+ created_at: input.created_at ?? nowSeconds(),
1786
+ };
1787
+ state.memoryEvents.push(event);
1788
+ const existing = state.projectionWatermarks.find((item) => item.projection_name === 'temporal');
1789
+ if (existing) {
1790
+ existing.last_event_id = event.event_id;
1791
+ existing.updated_at = event.created_at;
1792
+ }
1793
+ return cloneValue(event);
1794
+ },
1795
+ listMemoryEvents(scope, query) {
1796
+ return paginateEvents(state.memoryEvents.filter((item) => matchesEventScope(item, scope) && matchesEventQuery(item, query)), query);
1797
+ },
1798
+ listMemoryEventsCrossScope(scope, level, query) {
1799
+ return paginateEvents(state.memoryEvents.filter((item) => matchesScopeLevel(item, scope, level) && matchesEventQuery(item, query)), query);
1800
+ },
1801
+ getMemoryEventsByEntity(scope, entityKind, entityId, query) {
1802
+ return paginateEvents(state.memoryEvents.filter((item) => matchesEventScope(item, scope) &&
1803
+ item.entity_kind === entityKind &&
1804
+ item.entity_id === entityId &&
1805
+ matchesEventQuery(item, query)), query);
1806
+ },
1807
+ getMemoryEventsBySession(scope, sessionId, query) {
1808
+ return paginateEvents(state.memoryEvents.filter((item) => matchesEventScope(item, scope) &&
1809
+ item.session_id === sessionId &&
1810
+ matchesEventQuery(item, query)), query);
1811
+ },
1812
+ getSessionState(scope, sessionId) {
1813
+ const item = state.sessionStates.find((entry) => matchesScope(entry, scope) && entry.session_id === sessionId) ?? null;
1814
+ return item ? cloneValue(item) : null;
1815
+ },
1816
+ upsertSessionState(input) {
1817
+ const normalized = normalizeScope(input);
1818
+ const existing = state.sessionStates.find((entry) => matchesScope(entry, normalized) && entry.session_id === input.session_id);
1819
+ const next = {
1820
+ ...normalized,
1821
+ session_id: input.session_id,
1822
+ currentObjective: input.currentObjective,
1823
+ blockers: [...input.blockers],
1824
+ assumptions: [...input.assumptions],
1825
+ pendingDecisions: [...input.pendingDecisions],
1826
+ activeTools: [...input.activeTools],
1827
+ recentOutputs: [...input.recentOutputs],
1828
+ updatedAt: input.updatedAt,
1829
+ source_event_id: input.source_event_id != null ? normalizeTemporalId(input.source_event_id) : null,
1830
+ };
1831
+ if (existing) {
1832
+ Object.assign(existing, next);
1833
+ }
1834
+ else {
1835
+ state.sessionStates.push(next);
1836
+ }
1837
+ this.insertMemoryEvent({
1838
+ ...normalized,
1839
+ session_id: input.session_id,
1840
+ entity_kind: 'session_state',
1841
+ entity_id: input.session_id,
1842
+ event_type: 'session_state.updated',
1843
+ payload: {
1844
+ after: cloneValue(next),
1845
+ },
1846
+ created_at: input.updatedAt,
1847
+ });
1848
+ return cloneValue(next);
1849
+ },
1850
+ getTemporalWatermark(projectionName = 'temporal') {
1851
+ const watermark = state.projectionWatermarks.find((item) => item.projection_name === projectionName) ?? null;
1852
+ return watermark ? cloneValue(watermark) : null;
1853
+ },
1854
+ upsertTemporalWatermark(input) {
1855
+ const updatedAt = input.updated_at ?? nowSeconds();
1856
+ const next = {
1857
+ projection_name: input.projection_name,
1858
+ last_event_id: normalizeTemporalId(input.last_event_id),
1859
+ updated_at: updatedAt,
1860
+ cutover_at: input.cutover_at ?? null,
1861
+ metadata: input.metadata ? cloneValue(input.metadata) : null,
1862
+ };
1863
+ const existing = state.projectionWatermarks.find((item) => item.projection_name === input.projection_name);
1864
+ if (existing) {
1865
+ Object.assign(existing, next);
1866
+ }
1867
+ else {
1868
+ state.projectionWatermarks.push(next);
1869
+ }
1870
+ return cloneValue(next);
1871
+ },
639
1872
  transaction(fn) {
640
1873
  return fn();
641
1874
  },