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
@@ -2,11 +2,14 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { createRequire } from 'module';
4
4
  import { normalizeScope, scopeValues } from '../../contracts/identity.js';
5
+ import { UniqueConstraintError } from '../../contracts/storage.js';
6
+ import { ConflictError } from '../../contracts/errors.js';
7
+ import { compareTemporalIds, normalizeTemporalId } from '../../contracts/temporal.js';
5
8
  import { estimateTokens } from '../../core/tokens.js';
6
9
  import { emitMemoryEvent } from '../../core/telemetry.js';
7
10
  import { matchesKnowledgeSearchOptions } from '../../core/retrieval.js';
8
- import { assertArchiveInput, nowSeconds, validateContextMonitorUpsert, validateNewCompactionLog, validateNewKnowledgeCandidate, validateNewKnowledgeEvidence, validateNewKnowledgeMemoryAudit, validateNewKnowledgeMemory, validateNewWorkItem, validateTimeRange, validateNewTurn, validateNewWorkingMemory, } from '../../core/validation.js';
9
- import { rowToCompactionLog, rowToContextMonitor, rowToKnowledgeCandidate, rowToKnowledgeEvidence, rowToKnowledgeMemory, rowToKnowledgeMemoryAudit, rowToTurn, rowToWorkItem, rowToWorkingMemory, serializeNumberArray, serializeStringArray, } from './mappers.js';
11
+ import { assertActorRef, assertArchiveInput, nowSeconds, validateContextMonitorUpsert, validateNewCompactionLog, validateNewKnowledgeCandidate, validateNewKnowledgeEvidence, validateNewKnowledgeMemoryAudit, validateNewKnowledgeMemory, validateNewWorkItem, validateTimeRange, validateNewTurn, validateNewWorkingMemory, } from '../../core/validation.js';
12
+ import { rowToCompactionLog, rowToContextMonitor, rowToKnowledgeCandidate, rowToKnowledgeEvidence, rowToKnowledgeMemory, rowToKnowledgeMemoryAudit, rowToMemoryEvent, rowToAssociation, rowToTurn, rowToPlaybook, rowToPlaybookRevision, rowToSessionStateProjection, rowToTemporalProjectionWatermark, rowToWorkItem, rowToWorkingMemory, serializeObject, serializeNumberArray, serializeStringArray, } from './mappers.js';
10
13
  import { createSQLiteEmbeddingAdapter } from './embeddings.js';
11
14
  import { createSQLiteSchema } from './schema.js';
12
15
  const SCOPE_WHERE = 'tenant_id = ? AND system_id = ? AND workspace_id = ? AND collaboration_id = ? AND scope_id = ?';
@@ -20,29 +23,30 @@ function loadBetterSqlite3() {
20
23
  }
21
24
  }
22
25
  function scopeWhereForLevel(scope, level) {
23
- const normalized = normalizeScope(scope);
24
26
  if (level === 'tenant')
25
27
  return 'tenant_id = ?';
26
28
  if (level === 'system')
27
29
  return 'tenant_id = ? AND system_id = ?';
28
- if (level === 'workspace') {
29
- return normalized.collaboration_id.length > 0
30
- ? 'tenant_id = ? AND collaboration_id = ?'
31
- : 'tenant_id = ? AND system_id = ? AND workspace_id = ?';
32
- }
30
+ if (level === 'workspace')
31
+ return 'tenant_id = ? AND workspace_id = ?';
33
32
  return SCOPE_WHERE;
34
33
  }
34
+ function scopeWhereForLevelWithPrefix(scope, level, prefix) {
35
+ return scopeWhereForLevel(scope, level)
36
+ .replaceAll('tenant_id', `${prefix}.tenant_id`)
37
+ .replaceAll('system_id', `${prefix}.system_id`)
38
+ .replaceAll('workspace_id', `${prefix}.workspace_id`)
39
+ .replaceAll('collaboration_id', `${prefix}.collaboration_id`)
40
+ .replaceAll('scope_id', `${prefix}.scope_id`);
41
+ }
35
42
  function scopeParamsForLevel(scope, level) {
36
43
  const normalized = normalizeScope(scope);
37
44
  if (level === 'tenant')
38
45
  return [normalized.tenant_id];
39
46
  if (level === 'system')
40
47
  return [normalized.tenant_id, normalized.system_id];
41
- if (level === 'workspace') {
42
- return normalized.collaboration_id.length > 0
43
- ? [normalized.tenant_id, normalized.collaboration_id]
44
- : [normalized.tenant_id, normalized.system_id, normalized.workspace_id];
45
- }
48
+ if (level === 'workspace')
49
+ return [normalized.tenant_id, normalized.workspace_id];
46
50
  return [...scopeValues(normalized)];
47
51
  }
48
52
  function timeRangeWhere(range, column = 'created_at') {
@@ -122,6 +126,20 @@ function resolvePaginationOptions(options) {
122
126
  cursor: options?.cursor ?? 0,
123
127
  };
124
128
  }
129
+ function cloneValue(value) {
130
+ return structuredClone(value);
131
+ }
132
+ function resolveEventQuery(query) {
133
+ return {
134
+ sessionId: query?.sessionId ?? '',
135
+ entityKind: query?.entityKind ?? null,
136
+ entityId: query?.entityId ?? '',
137
+ startAt: query?.startAt ?? Number.NEGATIVE_INFINITY,
138
+ endAt: query?.endAt ?? Number.POSITIVE_INFINITY,
139
+ limit: query?.limit ?? 100,
140
+ cursor: query?.cursor != null ? normalizeTemporalId(query.cursor) : null,
141
+ };
142
+ }
125
143
  export function createSQLiteAdapter(dbPath, telemetry) {
126
144
  const db = openSQLiteDatabase(dbPath);
127
145
  return createAdapterFromDatabase(db, telemetry);
@@ -145,6 +163,256 @@ function openSQLiteDatabase(dbPath) {
145
163
  return db;
146
164
  }
147
165
  function createAdapterFromDatabase(db, telemetry) {
166
+ function readTemporalWatermark(projectionName = 'temporal') {
167
+ const row = db
168
+ .prepare('SELECT * FROM projection_watermarks WHERE projection_name = ?')
169
+ .get(projectionName);
170
+ return row ? rowToTemporalProjectionWatermark(row) : null;
171
+ }
172
+ function writeTemporalWatermark(input) {
173
+ const lastEventId = normalizeTemporalId(input.last_event_id);
174
+ const updatedAt = input.updated_at ?? nowSeconds();
175
+ db.prepare(`INSERT INTO projection_watermarks
176
+ (projection_name, last_event_id, updated_at, cutover_at, metadata)
177
+ VALUES (?, ?, ?, ?, ?)
178
+ ON CONFLICT(projection_name) DO UPDATE SET
179
+ last_event_id = excluded.last_event_id,
180
+ updated_at = excluded.updated_at,
181
+ cutover_at = excluded.cutover_at,
182
+ metadata = excluded.metadata`).run(input.projection_name, lastEventId, updatedAt, input.cutover_at ?? null, serializeObject(input.metadata ?? null));
183
+ return readTemporalWatermark(input.projection_name);
184
+ }
185
+ function insertMemoryEventInternal(input) {
186
+ const normalized = normalizeScope(input);
187
+ const createdAt = input.created_at ?? nowSeconds();
188
+ const result = db
189
+ .prepare(`INSERT INTO memory_event_log
190
+ (tenant_id, system_id, workspace_id, collaboration_id, scope_id, session_id, actor_id,
191
+ actor_kind, actor_system_id, actor_display_name, actor_metadata,
192
+ entity_kind, entity_id, event_type, payload, causation_id, correlation_id, created_at)
193
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
194
+ .run(normalized.tenant_id, normalized.system_id, normalized.workspace_id, normalized.collaboration_id, normalized.scope_id, input.session_id ?? null, input.actor_id ?? null, input.actor_kind ?? null, input.actor_system_id ?? null, input.actor_display_name ?? null, input.actor_metadata ? JSON.stringify(input.actor_metadata) : null, input.entity_kind, input.entity_id, input.event_type, JSON.stringify(input.payload ?? {}), input.causation_id ?? null, input.correlation_id ?? null, createdAt);
195
+ const eventId = normalizeTemporalId(result.lastInsertRowid);
196
+ writeTemporalWatermark({
197
+ projection_name: 'temporal',
198
+ last_event_id: eventId,
199
+ updated_at: createdAt,
200
+ cutover_at: readTemporalWatermark('temporal')?.cutover_at ?? createdAt,
201
+ metadata: readTemporalWatermark('temporal')?.metadata ?? null,
202
+ });
203
+ const row = db
204
+ .prepare('SELECT * FROM memory_event_log WHERE event_id = ?')
205
+ .get(eventId);
206
+ return rowToMemoryEvent(row);
207
+ }
208
+ function insertMemoryEventsBatchInternal(inputs) {
209
+ if (inputs.length === 0)
210
+ return [];
211
+ const tx = db.transaction((batch) => {
212
+ const normalizedBatch = batch.map((input) => ({
213
+ normalized: normalizeScope(input),
214
+ input,
215
+ createdAt: input.created_at ?? nowSeconds(),
216
+ }));
217
+ const values = normalizedBatch.map(() => '(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').join(', ');
218
+ const params = [];
219
+ for (const { normalized, input, createdAt } of normalizedBatch) {
220
+ params.push(normalized.tenant_id, normalized.system_id, normalized.workspace_id, normalized.collaboration_id, normalized.scope_id, input.session_id ?? null, input.actor_id ?? null, input.actor_kind ?? null, input.actor_system_id ?? null, input.actor_display_name ?? null, input.actor_metadata ? JSON.stringify(input.actor_metadata) : null, input.entity_kind, input.entity_id, input.event_type, JSON.stringify(input.payload ?? {}), input.causation_id ?? null, input.correlation_id ?? null, createdAt);
221
+ }
222
+ const result = db.prepare(`INSERT INTO memory_event_log
223
+ (tenant_id, system_id, workspace_id, collaboration_id, scope_id, session_id, actor_id,
224
+ actor_kind, actor_system_id, actor_display_name, actor_metadata,
225
+ entity_kind, entity_id, event_type, payload, causation_id, correlation_id, created_at)
226
+ VALUES ${values}`).run(...params);
227
+ const lastEventId = normalizeTemporalId(result.lastInsertRowid);
228
+ const firstEventId = normalizeTemporalId(BigInt(lastEventId) - BigInt(normalizedBatch.length) + 1n);
229
+ const records = db
230
+ .prepare(`SELECT * FROM memory_event_log
231
+ WHERE event_id BETWEEN ? AND ?
232
+ ORDER BY event_id ASC`)
233
+ .all(firstEventId, lastEventId).map(rowToMemoryEvent);
234
+ if (lastEventId != null) {
235
+ writeTemporalWatermark({
236
+ projection_name: 'temporal',
237
+ last_event_id: lastEventId,
238
+ updated_at: normalizedBatch[normalizedBatch.length - 1].createdAt,
239
+ cutover_at: readTemporalWatermark('temporal')?.cutover_at ??
240
+ normalizedBatch[normalizedBatch.length - 1].createdAt,
241
+ metadata: readTemporalWatermark('temporal')?.metadata ?? null,
242
+ });
243
+ }
244
+ return records;
245
+ });
246
+ return tx(inputs);
247
+ }
248
+ function listScopedMemoryEvents(scope, query) {
249
+ const resolved = resolveEventQuery(query);
250
+ const clauses = [SCOPE_WHERE, 'created_at >= ?', 'created_at <= ?'];
251
+ const params = [...scopeValues(scope), resolved.startAt, resolved.endAt];
252
+ if (resolved.cursor != null && compareTemporalIds(resolved.cursor, '0') > 0) {
253
+ clauses.push('event_id > ?');
254
+ params.push(resolved.cursor);
255
+ }
256
+ if (resolved.sessionId) {
257
+ clauses.push('session_id = ?');
258
+ params.push(resolved.sessionId);
259
+ }
260
+ if (resolved.entityKind) {
261
+ clauses.push('entity_kind = ?');
262
+ params.push(resolved.entityKind);
263
+ }
264
+ if (resolved.entityId) {
265
+ clauses.push('entity_id = ?');
266
+ params.push(resolved.entityId);
267
+ }
268
+ const rows = db
269
+ .prepare(`SELECT * FROM memory_event_log
270
+ WHERE ${clauses.join(' AND ')}
271
+ ORDER BY created_at ASC, event_id ASC
272
+ LIMIT ?`)
273
+ .all(...params, resolved.limit + 1);
274
+ const pageRows = rows.slice(0, resolved.limit).map(rowToMemoryEvent);
275
+ return {
276
+ events: pageRows,
277
+ nextCursor: rows.length > resolved.limit ? pageRows[pageRows.length - 1]?.event_id ?? null : null,
278
+ };
279
+ }
280
+ function listScopedMemoryEventsCrossScope(scope, level, query) {
281
+ const resolved = resolveEventQuery(query);
282
+ const clauses = [scopeWhereForLevel(scope, level), 'created_at >= ?', 'created_at <= ?'];
283
+ const params = [...scopeParamsForLevel(scope, level), resolved.startAt, resolved.endAt];
284
+ if (resolved.cursor != null && compareTemporalIds(resolved.cursor, '0') > 0) {
285
+ clauses.push('event_id > ?');
286
+ params.push(resolved.cursor);
287
+ }
288
+ if (resolved.sessionId) {
289
+ clauses.push('session_id = ?');
290
+ params.push(resolved.sessionId);
291
+ }
292
+ if (resolved.entityKind) {
293
+ clauses.push('entity_kind = ?');
294
+ params.push(resolved.entityKind);
295
+ }
296
+ if (resolved.entityId) {
297
+ clauses.push('entity_id = ?');
298
+ params.push(resolved.entityId);
299
+ }
300
+ const rows = db
301
+ .prepare(`SELECT * FROM memory_event_log
302
+ WHERE ${clauses.join(' AND ')}
303
+ ORDER BY created_at ASC, event_id ASC
304
+ LIMIT ?`)
305
+ .all(...params, resolved.limit + 1);
306
+ const pageRows = rows.slice(0, resolved.limit).map(rowToMemoryEvent);
307
+ return {
308
+ events: pageRows,
309
+ nextCursor: rows.length > resolved.limit ? pageRows[pageRows.length - 1]?.event_id ?? null : null,
310
+ };
311
+ }
312
+ function readSessionStateProjection(scope, sessionId) {
313
+ const row = db
314
+ .prepare(`SELECT * FROM session_state_current
315
+ WHERE ${SCOPE_WHERE} AND session_id = ?`)
316
+ .get(...scopeValues(scope), sessionId);
317
+ return row ? rowToSessionStateProjection(row) : null;
318
+ }
319
+ function writeSessionStateProjection(input) {
320
+ const normalized = normalizeScope(input);
321
+ db.prepare(`INSERT INTO session_state_current
322
+ (tenant_id, system_id, workspace_id, collaboration_id, scope_id, session_id,
323
+ current_objective, blockers, assumptions, pending_decisions, active_tools, recent_outputs,
324
+ updated_at, source_event_id)
325
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
326
+ ON CONFLICT(tenant_id, system_id, workspace_id, collaboration_id, scope_id, session_id) DO UPDATE SET
327
+ current_objective = excluded.current_objective,
328
+ blockers = excluded.blockers,
329
+ assumptions = excluded.assumptions,
330
+ pending_decisions = excluded.pending_decisions,
331
+ active_tools = excluded.active_tools,
332
+ recent_outputs = excluded.recent_outputs,
333
+ updated_at = excluded.updated_at,
334
+ source_event_id = excluded.source_event_id`).run(normalized.tenant_id, normalized.system_id, normalized.workspace_id, normalized.collaboration_id, normalized.scope_id, input.session_id, input.currentObjective, serializeStringArray(input.blockers), serializeStringArray(input.assumptions), serializeStringArray(input.pendingDecisions), serializeStringArray(input.activeTools), serializeStringArray(input.recentOutputs), input.updatedAt, input.source_event_id != null ? normalizeTemporalId(input.source_event_id) : null);
335
+ return readSessionStateProjection(normalized, input.session_id);
336
+ }
337
+ function serializeActorMetadata(actor) {
338
+ return [
339
+ actor.actor_kind,
340
+ actor.actor_id,
341
+ actor.system_id ?? null,
342
+ actor.display_name ?? null,
343
+ actor.metadata ? JSON.stringify(actor.metadata) : null,
344
+ ];
345
+ }
346
+ function parseActorRef(row, prefix = '') {
347
+ const value = (field) => row[`${prefix}${field}`];
348
+ let metadata = null;
349
+ if (value('actor_metadata') != null) {
350
+ try {
351
+ const parsed = JSON.parse(String(value('actor_metadata')));
352
+ metadata = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
353
+ ? parsed
354
+ : null;
355
+ }
356
+ catch {
357
+ metadata = null;
358
+ }
359
+ }
360
+ return {
361
+ actor_kind: String(value('actor_kind')),
362
+ actor_id: String(value('actor_id')),
363
+ system_id: value('actor_system_id') != null ? String(value('actor_system_id')) : null,
364
+ display_name: value('actor_display_name') != null ? String(value('actor_display_name')) : null,
365
+ metadata,
366
+ };
367
+ }
368
+ function mapWorkClaim(row) {
369
+ return {
370
+ id: Number(row.id),
371
+ tenant_id: String(row.tenant_id),
372
+ system_id: String(row.system_id),
373
+ workspace_id: String(row.workspace_id ?? ''),
374
+ collaboration_id: String(row.collaboration_id ?? ''),
375
+ scope_id: String(row.scope_id),
376
+ work_item_id: Number(row.work_item_id),
377
+ actor: parseActorRef(row),
378
+ session_id: row.session_id != null ? String(row.session_id) : null,
379
+ claim_token: String(row.claim_token),
380
+ status: String(row.status),
381
+ claimed_at: Number(row.claimed_at),
382
+ expires_at: Number(row.expires_at),
383
+ released_at: row.released_at != null ? Number(row.released_at) : null,
384
+ release_reason: row.release_reason != null ? String(row.release_reason) : null,
385
+ source_event_id: row.source_event_id != null ? String(row.source_event_id) : null,
386
+ visibility_class: row.visibility_class ?? 'private',
387
+ version: Number(row.version ?? 1),
388
+ };
389
+ }
390
+ function mapHandoff(row) {
391
+ return {
392
+ id: Number(row.id),
393
+ tenant_id: String(row.tenant_id),
394
+ system_id: String(row.system_id),
395
+ workspace_id: String(row.workspace_id ?? ''),
396
+ collaboration_id: String(row.collaboration_id ?? ''),
397
+ scope_id: String(row.scope_id),
398
+ work_item_id: Number(row.work_item_id),
399
+ from_actor: parseActorRef(row, 'from_'),
400
+ to_actor: parseActorRef(row, 'to_'),
401
+ session_id: row.session_id != null ? String(row.session_id) : null,
402
+ summary: String(row.summary),
403
+ context_bundle_ref: row.context_bundle_ref != null ? String(row.context_bundle_ref) : null,
404
+ status: String(row.status),
405
+ created_at: Number(row.created_at),
406
+ accepted_at: row.accepted_at != null ? Number(row.accepted_at) : null,
407
+ rejected_at: row.rejected_at != null ? Number(row.rejected_at) : null,
408
+ canceled_at: row.canceled_at != null ? Number(row.canceled_at) : null,
409
+ expires_at: row.expires_at != null ? Number(row.expires_at) : null,
410
+ decision_reason: row.decision_reason != null ? String(row.decision_reason) : null,
411
+ source_event_id: row.source_event_id != null ? String(row.source_event_id) : null,
412
+ visibility_class: row.visibility_class ?? 'private',
413
+ version: Number(row.version ?? 1),
414
+ };
415
+ }
148
416
  function getTurnById(id) {
149
417
  const row = db.prepare('SELECT * FROM turns WHERE id = ?').get(id);
150
418
  return row ? rowToTurn(row) : null;
@@ -155,6 +423,18 @@ function createAdapterFromDatabase(db, telemetry) {
155
423
  .get(id);
156
424
  return row ? rowToWorkingMemory(row) : null;
157
425
  }
426
+ function getExistingIds(table, ids) {
427
+ const uniqueIds = [...new Set(ids)];
428
+ if (uniqueIds.length === 0) {
429
+ return [];
430
+ }
431
+ const placeholders = uniqueIds.map(() => '?').join(', ');
432
+ const rows = db
433
+ .prepare(`SELECT id FROM ${table} WHERE id IN (${placeholders})`)
434
+ .all(...uniqueIds);
435
+ const existing = new Set(rows.map((row) => Number(row.id)));
436
+ return uniqueIds.filter((id) => existing.has(id));
437
+ }
158
438
  function getKnowledgeMemoryById(id) {
159
439
  const row = db
160
440
  .prepare('SELECT * FROM knowledge_memory WHERE id = ?')
@@ -207,7 +487,20 @@ function createAdapterFromDatabase(db, telemetry) {
207
487
  (session_id, tenant_id, system_id, workspace_id, collaboration_id, scope_id, actor, role, content, priority, token_estimate, created_at)
208
488
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
209
489
  .run(input.session_id, scope.tenant_id, scope.system_id, scope.workspace_id, scope.collaboration_id, scope.scope_id, input.actor, input.role, input.content, input.priority ?? (input.role === 'system' ? 1.5 : 1), tokenEstimate, createdAt);
210
- return getTurnById(Number(result.lastInsertRowid));
490
+ const turn = getTurnById(Number(result.lastInsertRowid));
491
+ insertMemoryEventInternal({
492
+ ...scope,
493
+ session_id: turn.session_id,
494
+ actor_id: turn.actor,
495
+ entity_kind: 'turn',
496
+ entity_id: String(turn.id),
497
+ event_type: 'turn.created',
498
+ payload: {
499
+ after: cloneValue(turn),
500
+ },
501
+ created_at: turn.created_at,
502
+ });
503
+ return turn;
211
504
  }
212
505
  function insertValidatedKnowledgeMemory(input) {
213
506
  const scope = validateNewKnowledgeMemory(input);
@@ -224,8 +517,140 @@ function createAdapterFromDatabase(db, telemetry) {
224
517
  contradiction_score, superseded_at, retired_at, created_at, last_accessed_at)
225
518
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
226
519
  .run(scope.tenant_id, scope.system_id, scope.workspace_id, scope.collaboration_id, scope.scope_id, input.fact, input.fact_type, input.knowledge_state ?? 'trusted', input.knowledge_class ?? 'project_fact', input.fact_subject ?? null, input.fact_attribute ?? null, input.fact_value ?? null, input.normalized_fact ?? null, input.slot_key ?? null, input.is_negated ? 1 : 0, input.source, input.confidence, input.confidence_score ?? 0.5, input.grounding_strength ?? 'moderate', input.evidence_count ?? Math.max(1, (input.source_turn_ids ?? []).length), input.trust_score ?? (input.confidence_score ?? 0.5), input.verification_status ?? 'unverified', input.verification_notes ?? null, input.last_verified_at ?? null, input.next_reverification_at ?? null, input.last_confirmed_at ?? null, input.confirmation_count ?? 0, input.source_system_id ?? scope.system_id, input.source_scope_id ?? scope.scope_id, input.source_collaboration_id ?? scope.collaboration_id, input.source_working_memory_id ?? null, serializeNumberArray(input.source_turn_ids ?? []), input.successful_use_count ?? 0, input.failed_use_count ?? 0, input.disputed_at ?? null, input.dispute_reason ?? null, input.contradiction_score ?? 0, input.superseded_at ?? null, input.retired_at ?? null, createdAt, createdAt);
227
- return getKnowledgeMemoryById(Number(result.lastInsertRowid));
520
+ const knowledge = getKnowledgeMemoryById(Number(result.lastInsertRowid));
521
+ insertMemoryEventInternal({
522
+ ...scope,
523
+ entity_kind: 'knowledge_memory',
524
+ entity_id: String(knowledge.id),
525
+ event_type: 'knowledge.created',
526
+ payload: {
527
+ after: cloneValue(knowledge),
528
+ },
529
+ created_at: knowledge.created_at,
530
+ });
531
+ return knowledge;
532
+ }
533
+ function bootstrapTemporalCutover() {
534
+ const existing = readTemporalWatermark('temporal');
535
+ if (existing?.cutover_at != null) {
536
+ return;
537
+ }
538
+ const cutoverAt = nowSeconds();
539
+ const correlationId = 'temporal-cutover-v1';
540
+ db.transaction(() => {
541
+ writeTemporalWatermark({
542
+ projection_name: 'temporal',
543
+ last_event_id: existing?.last_event_id ?? 0,
544
+ updated_at: cutoverAt,
545
+ cutover_at: cutoverAt,
546
+ metadata: {
547
+ correlation_id: correlationId,
548
+ },
549
+ });
550
+ const turnRows = db.prepare('SELECT * FROM turns ORDER BY created_at ASC, id ASC').all();
551
+ for (const row of turnRows.map(rowToTurn)) {
552
+ insertMemoryEventInternal({
553
+ ...normalizeScope(row),
554
+ session_id: row.session_id,
555
+ actor_id: row.actor,
556
+ entity_kind: 'turn',
557
+ entity_id: String(row.id),
558
+ event_type: 'turn.seeded',
559
+ payload: { after: cloneValue(row) },
560
+ correlation_id: correlationId,
561
+ created_at: cutoverAt,
562
+ });
563
+ }
564
+ const workingRows = db
565
+ .prepare('SELECT * FROM working_memory ORDER BY created_at ASC, id ASC')
566
+ .all();
567
+ for (const row of workingRows.map(rowToWorkingMemory)) {
568
+ insertMemoryEventInternal({
569
+ ...normalizeScope(row),
570
+ session_id: row.session_id,
571
+ entity_kind: 'working_memory',
572
+ entity_id: String(row.id),
573
+ event_type: 'working_memory.seeded',
574
+ payload: { after: cloneValue(row) },
575
+ correlation_id: correlationId,
576
+ created_at: cutoverAt,
577
+ });
578
+ }
579
+ const knowledgeRows = db
580
+ .prepare('SELECT * FROM knowledge_memory ORDER BY created_at ASC, id ASC')
581
+ .all();
582
+ for (const row of knowledgeRows.map(rowToKnowledgeMemory)) {
583
+ insertMemoryEventInternal({
584
+ ...normalizeScope(row),
585
+ entity_kind: 'knowledge_memory',
586
+ entity_id: String(row.id),
587
+ event_type: 'knowledge.seeded',
588
+ payload: { after: cloneValue(row) },
589
+ correlation_id: correlationId,
590
+ created_at: cutoverAt,
591
+ });
592
+ }
593
+ const workItemRows = db
594
+ .prepare('SELECT * FROM work_items ORDER BY created_at ASC, id ASC')
595
+ .all();
596
+ for (const row of workItemRows.map(rowToWorkItem)) {
597
+ insertMemoryEventInternal({
598
+ ...normalizeScope(row),
599
+ session_id: row.session_id,
600
+ entity_kind: 'work_item',
601
+ entity_id: String(row.id),
602
+ event_type: 'work_item.seeded',
603
+ payload: { after: cloneValue(row) },
604
+ correlation_id: correlationId,
605
+ created_at: cutoverAt,
606
+ });
607
+ }
608
+ const associationRows = db
609
+ .prepare('SELECT * FROM associations ORDER BY created_at ASC, id ASC')
610
+ .all();
611
+ for (const row of associationRows.map((item) => rowToAssociation({
612
+ id: Number(item.id),
613
+ tenant_id: String(item.tenant_id),
614
+ system_id: String(item.system_id),
615
+ workspace_id: String(item.workspace_id ?? ''),
616
+ collaboration_id: String(item.collaboration_id ?? item.workspace_id ?? ''),
617
+ scope_id: String(item.scope_id),
618
+ visibility_class: item.visibility_class ?? 'private',
619
+ source_kind: item.source_kind,
620
+ source_id: Number(item.source_id),
621
+ target_kind: item.target_kind,
622
+ target_id: Number(item.target_id),
623
+ association_type: item.association_type,
624
+ confidence: Number(item.confidence ?? 0),
625
+ auto_generated: Number(item.auto_generated ?? 0) === 1,
626
+ created_at: Number(item.created_at),
627
+ }))) {
628
+ insertMemoryEventInternal({
629
+ ...normalizeScope(row),
630
+ entity_kind: 'association',
631
+ entity_id: String(row.id),
632
+ event_type: 'association.seeded',
633
+ payload: { after: cloneValue(row) },
634
+ correlation_id: correlationId,
635
+ created_at: cutoverAt,
636
+ });
637
+ }
638
+ const playbookRows = db.prepare('SELECT * FROM playbooks ORDER BY created_at ASC, id ASC').all();
639
+ for (const row of playbookRows.map(rowToPlaybook)) {
640
+ insertMemoryEventInternal({
641
+ ...normalizeScope(row),
642
+ session_id: row.source_session_id,
643
+ entity_kind: 'playbook',
644
+ entity_id: String(row.id),
645
+ event_type: 'playbook.seeded',
646
+ payload: { after: cloneValue(row) },
647
+ correlation_id: correlationId,
648
+ created_at: cutoverAt,
649
+ });
650
+ }
651
+ })();
228
652
  }
653
+ bootstrapTemporalCutover();
229
654
  return {
230
655
  insertTurn(input) {
231
656
  return insertValidatedTurn(input);
@@ -272,17 +697,28 @@ function createAdapterFromDatabase(db, telemetry) {
272
697
  searchTurns(scope, query, options) {
273
698
  const startedAt = Date.now();
274
699
  const resolved = resolveSearchOptions(options);
700
+ const safeQuery = toSafeFtsQuery(query);
701
+ const executeSearch = (ftsQuery) => db.prepare(`SELECT turns.*, bm25(turns_fts) AS raw_rank
702
+ FROM turns_fts
703
+ JOIN turns ON turns_fts.rowid = turns.id
704
+ WHERE turns_fts MATCH ?
705
+ AND ${SCOPE_WHERE}
706
+ AND (? = 0 OR turns.archived_at IS NULL)
707
+ ORDER BY bm25(turns_fts)
708
+ LIMIT ?`).all(ftsQuery, ...scopeValues(scope), resolved.activeOnly ? 1 : 0, resolved.limit);
275
709
  try {
276
- const rows = db
277
- .prepare(`SELECT turns.*, bm25(turns_fts) AS raw_rank
278
- FROM turns_fts
279
- JOIN turns ON turns_fts.rowid = turns.id
280
- WHERE turns_fts MATCH ?
281
- AND ${SCOPE_WHERE}
282
- AND (? = 0 OR turns.archived_at IS NULL)
283
- ORDER BY bm25(turns_fts)
284
- LIMIT ?`)
285
- .all(query, ...scopeValues(scope), resolved.activeOnly ? 1 : 0, resolved.limit);
710
+ let rows;
711
+ try {
712
+ rows = executeSearch(query);
713
+ }
714
+ catch (error) {
715
+ if (safeQuery.length === 0 || safeQuery === query)
716
+ throw error;
717
+ rows = executeSearch(safeQuery);
718
+ }
719
+ if (rows.length === 0 && safeQuery.length > 0 && safeQuery !== query && !/["']/.test(query)) {
720
+ rows = executeSearch(safeQuery);
721
+ }
286
722
  const results = rows.map((row) => ({
287
723
  item: rowToTurn(row),
288
724
  rank: normalizeRank(row.raw_rank),
@@ -306,9 +742,30 @@ function createAdapterFromDatabase(db, telemetry) {
306
742
  },
307
743
  archiveTurn(id, archivedAt, compactionLogId) {
308
744
  assertArchiveInput(id, archivedAt, compactionLogId);
745
+ const before = getTurnById(id);
309
746
  db.prepare(`UPDATE turns
310
747
  SET archived_at = ?, compaction_log_id = ?
311
748
  WHERE id = ? AND archived_at IS NULL`).run(archivedAt, compactionLogId, id);
749
+ const after = getTurnById(id);
750
+ if (before && after && before.archived_at !== after.archived_at) {
751
+ insertMemoryEventInternal({
752
+ ...normalizeScope(after),
753
+ session_id: after.session_id,
754
+ actor_id: after.actor,
755
+ entity_kind: 'turn',
756
+ entity_id: String(after.id),
757
+ event_type: 'turn.archived',
758
+ payload: {
759
+ before: cloneValue(before),
760
+ after: cloneValue(after),
761
+ patch: {
762
+ archived_at: archivedAt,
763
+ compaction_log_id: compactionLogId,
764
+ },
765
+ },
766
+ created_at: archivedAt,
767
+ });
768
+ }
312
769
  },
313
770
  getArchivedTurnRange(sessionId, startId, endId, scope) {
314
771
  const query = `SELECT * FROM turns
@@ -327,12 +784,27 @@ function createAdapterFromDatabase(db, telemetry) {
327
784
  const result = db
328
785
  .prepare(`INSERT INTO working_memory
329
786
  (session_id, tenant_id, system_id, workspace_id, collaboration_id, scope_id, summary, key_entities, topic_tags,
330
- turn_id_start, turn_id_end, turn_count, compaction_trigger, created_at, expires_at)
331
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
332
- .run(input.session_id, scope.tenant_id, scope.system_id, scope.workspace_id, scope.collaboration_id, scope.scope_id, input.summary, serializeStringArray(input.key_entities), serializeStringArray(input.topic_tags), input.turn_id_start, input.turn_id_end, input.turn_count, input.compaction_trigger, createdAt, expiresAt);
333
- return getWorkingMemoryById(Number(result.lastInsertRowid));
787
+ turn_id_start, turn_id_end, turn_count, compaction_trigger, created_at, expires_at, episode_recap)
788
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
789
+ .run(input.session_id, scope.tenant_id, scope.system_id, scope.workspace_id, scope.collaboration_id, scope.scope_id, input.summary, serializeStringArray(input.key_entities), serializeStringArray(input.topic_tags), input.turn_id_start, input.turn_id_end, input.turn_count, input.compaction_trigger, createdAt, expiresAt, input.episode_recap ? JSON.stringify(input.episode_recap) : null);
790
+ const workingMemory = getWorkingMemoryById(Number(result.lastInsertRowid));
791
+ insertMemoryEventInternal({
792
+ ...scope,
793
+ session_id: workingMemory.session_id,
794
+ entity_kind: 'working_memory',
795
+ entity_id: String(workingMemory.id),
796
+ event_type: 'working_memory.created',
797
+ payload: {
798
+ after: cloneValue(workingMemory),
799
+ },
800
+ created_at: workingMemory.created_at,
801
+ });
802
+ return workingMemory;
334
803
  },
335
804
  getWorkingMemoryById,
805
+ getExistingWorkingMemoryIds(ids) {
806
+ return getExistingIds('working_memory', ids);
807
+ },
336
808
  getWorkingMemoryBySession(sessionId, scope) {
337
809
  const query = `SELECT * FROM working_memory
338
810
  WHERE session_id = ? AND ${SCOPE_WHERE}
@@ -375,10 +847,49 @@ function createAdapterFromDatabase(db, telemetry) {
375
847
  return rows.map(rowToWorkingMemory);
376
848
  },
377
849
  expireWorkingMemory(id) {
378
- db.prepare('UPDATE working_memory SET expires_at = ? WHERE id = ?').run(nowSeconds(), id);
850
+ const before = getWorkingMemoryById(id);
851
+ const expiredAt = nowSeconds();
852
+ db.prepare('UPDATE working_memory SET expires_at = ? WHERE id = ?').run(expiredAt, id);
853
+ const after = getWorkingMemoryById(id);
854
+ if (before && after) {
855
+ insertMemoryEventInternal({
856
+ ...normalizeScope(after),
857
+ session_id: after.session_id,
858
+ entity_kind: 'working_memory',
859
+ entity_id: String(after.id),
860
+ event_type: 'working_memory.expired',
861
+ payload: {
862
+ before: cloneValue(before),
863
+ after: cloneValue(after),
864
+ patch: {
865
+ expires_at: expiredAt,
866
+ },
867
+ },
868
+ created_at: expiredAt,
869
+ });
870
+ }
379
871
  },
380
872
  markWorkingMemoryPromoted(id, knowledgeMemoryId) {
873
+ const before = getWorkingMemoryById(id);
381
874
  db.prepare('UPDATE working_memory SET promoted_to_knowledge_id = ? WHERE id = ?').run(knowledgeMemoryId, id);
875
+ const after = getWorkingMemoryById(id);
876
+ if (before && after) {
877
+ insertMemoryEventInternal({
878
+ ...normalizeScope(after),
879
+ session_id: after.session_id,
880
+ entity_kind: 'working_memory',
881
+ entity_id: String(after.id),
882
+ event_type: 'working_memory.promoted',
883
+ payload: {
884
+ before: cloneValue(before),
885
+ after: cloneValue(after),
886
+ refs: {
887
+ knowledge_memory_id: knowledgeMemoryId,
888
+ },
889
+ },
890
+ created_at: nowSeconds(),
891
+ });
892
+ }
382
893
  },
383
894
  insertKnowledgeMemory(input) {
384
895
  return insertValidatedKnowledgeMemory(input);
@@ -448,6 +959,9 @@ function createAdapterFromDatabase(db, telemetry) {
448
959
  return knowledge;
449
960
  },
450
961
  getKnowledgeMemoryById,
962
+ getExistingKnowledgeMemoryIds(ids) {
963
+ return getExistingIds('knowledge_memory', ids);
964
+ },
451
965
  getActiveKnowledgeMemory(scope) {
452
966
  const rows = db
453
967
  .prepare(`SELECT * FROM knowledge_memory
@@ -695,6 +1209,7 @@ function createAdapterFromDatabase(db, telemetry) {
695
1209
  getRecentKnowledgeMemoryAudits,
696
1210
  getKnowledgeMemoryAuditsForKnowledge,
697
1211
  updateKnowledgeMemory(id, patch) {
1212
+ const before = getKnowledgeMemoryById(id);
698
1213
  const assignments = [];
699
1214
  const values = [];
700
1215
  const push = (column, value) => {
@@ -736,7 +1251,22 @@ function createAdapterFromDatabase(db, telemetry) {
736
1251
  return getKnowledgeMemoryById(id);
737
1252
  }
738
1253
  db.prepare(`UPDATE knowledge_memory SET ${assignments.join(', ')} WHERE id = ?`).run(...values, id);
739
- return getKnowledgeMemoryById(id);
1254
+ const after = getKnowledgeMemoryById(id);
1255
+ if (before && after) {
1256
+ insertMemoryEventInternal({
1257
+ ...normalizeScope(after),
1258
+ entity_kind: 'knowledge_memory',
1259
+ entity_id: String(after.id),
1260
+ event_type: 'knowledge.updated',
1261
+ payload: {
1262
+ before: cloneValue(before),
1263
+ after: cloneValue(after),
1264
+ patch: cloneValue(patch),
1265
+ },
1266
+ created_at: nowSeconds(),
1267
+ });
1268
+ }
1269
+ return after;
740
1270
  },
741
1271
  insertWorkItem(input) {
742
1272
  const scope = validateNewWorkItem(input);
@@ -744,13 +1274,25 @@ function createAdapterFromDatabase(db, telemetry) {
744
1274
  const result = db
745
1275
  .prepare(`INSERT INTO work_items
746
1276
  (session_id, tenant_id, system_id, workspace_id, collaboration_id, scope_id, kind, title, detail, status,
747
- source_working_memory_id, created_at, updated_at)
748
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
749
- .run(input.session_id ?? null, scope.tenant_id, scope.system_id, scope.workspace_id, scope.collaboration_id, scope.scope_id, input.kind, input.title, input.detail ?? null, input.status ?? 'open', input.source_working_memory_id ?? null, createdAt, createdAt);
1277
+ visibility_class, source_working_memory_id, version, created_at, updated_at)
1278
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1279
+ .run(input.session_id ?? null, scope.tenant_id, scope.system_id, scope.workspace_id, scope.collaboration_id, scope.scope_id, input.kind, input.title, input.detail ?? null, input.status ?? 'open', input.visibility_class ?? 'private', input.source_working_memory_id ?? null, 1, createdAt, createdAt);
750
1280
  const row = db
751
1281
  .prepare('SELECT * FROM work_items WHERE id = ?')
752
1282
  .get(Number(result.lastInsertRowid));
753
- return rowToWorkItem(row);
1283
+ const workItem = rowToWorkItem(row);
1284
+ insertMemoryEventInternal({
1285
+ ...scope,
1286
+ session_id: workItem.session_id,
1287
+ entity_kind: 'work_item',
1288
+ entity_id: String(workItem.id),
1289
+ event_type: 'work_item.created',
1290
+ payload: {
1291
+ after: cloneValue(workItem),
1292
+ },
1293
+ created_at: workItem.created_at,
1294
+ });
1295
+ return workItem;
754
1296
  },
755
1297
  getActiveWorkItems(scope) {
756
1298
  const rows = db
@@ -760,6 +1302,21 @@ function createAdapterFromDatabase(db, telemetry) {
760
1302
  .all(...scopeValues(scope));
761
1303
  return rows.map(rowToWorkItem);
762
1304
  },
1305
+ getWorkItemById(id) {
1306
+ const row = db.prepare('SELECT * FROM work_items WHERE id = ?').get(id);
1307
+ return row ? rowToWorkItem(row) : null;
1308
+ },
1309
+ getExistingWorkItemIds(ids) {
1310
+ return getExistingIds('work_items', ids);
1311
+ },
1312
+ getActiveWorkItemsCrossScope(scope, level) {
1313
+ const rows = db
1314
+ .prepare(`SELECT * FROM work_items
1315
+ WHERE ${scopeWhereForLevel(scope, level)} AND status != 'done'
1316
+ ORDER BY updated_at DESC`)
1317
+ .all(...scopeParamsForLevel(scope, level));
1318
+ return rows.map(rowToWorkItem);
1319
+ },
763
1320
  getWorkItemsByTimeRange(scope, range) {
764
1321
  const time = timeRangeWhere(range, 'created_at');
765
1322
  const rows = db
@@ -769,24 +1326,802 @@ function createAdapterFromDatabase(db, telemetry) {
769
1326
  .all(...scopeValues(scope), ...time.params);
770
1327
  return rows.map(rowToWorkItem);
771
1328
  },
1329
+ getWorkItemsByTimeRangeCrossScope(scope, level, range) {
1330
+ const time = timeRangeWhere(range, 'created_at');
1331
+ const rows = db
1332
+ .prepare(`SELECT * FROM work_items
1333
+ WHERE ${scopeWhereForLevel(scope, level)}${time.clause}
1334
+ ORDER BY created_at ASC`)
1335
+ .all(...scopeParamsForLevel(scope, level), ...time.params);
1336
+ return rows.map(rowToWorkItem);
1337
+ },
772
1338
  updateWorkItemStatus(id, status) {
773
- db.prepare('UPDATE work_items SET status = ?, updated_at = ? WHERE id = ?').run(status, nowSeconds(), id);
1339
+ const before = db
1340
+ .prepare('SELECT * FROM work_items WHERE id = ?')
1341
+ .get(id);
1342
+ const updatedAt = nowSeconds();
1343
+ db.prepare('UPDATE work_items SET status = ?, updated_at = ? WHERE id = ?').run(status, updatedAt, id);
1344
+ const after = db
1345
+ .prepare('SELECT * FROM work_items WHERE id = ?')
1346
+ .get(id);
1347
+ if (before && after) {
1348
+ const afterItem = rowToWorkItem(after);
1349
+ insertMemoryEventInternal({
1350
+ ...normalizeScope(afterItem),
1351
+ session_id: afterItem.session_id,
1352
+ entity_kind: 'work_item',
1353
+ entity_id: String(afterItem.id),
1354
+ event_type: 'work_item.status_changed',
1355
+ payload: {
1356
+ before: cloneValue(rowToWorkItem(before)),
1357
+ after: cloneValue(afterItem),
1358
+ patch: {
1359
+ status,
1360
+ updated_at: updatedAt,
1361
+ },
1362
+ },
1363
+ created_at: updatedAt,
1364
+ });
1365
+ }
1366
+ },
1367
+ updateWorkItem(id, patch, options) {
1368
+ const before = db
1369
+ .prepare('SELECT * FROM work_items WHERE id = ?')
1370
+ .get(id);
1371
+ if (!before)
1372
+ return null;
1373
+ const beforeItem = rowToWorkItem(before);
1374
+ if (options?.expectedVersion != null && beforeItem.version !== options.expectedVersion) {
1375
+ throw new ConflictError(`Work item ${id} version mismatch`);
1376
+ }
1377
+ const updatedAt = nowSeconds();
1378
+ const next = {
1379
+ title: patch.title ?? beforeItem.title,
1380
+ detail: patch.detail !== undefined ? patch.detail : beforeItem.detail,
1381
+ status: patch.status ?? beforeItem.status,
1382
+ visibility_class: patch.visibility_class ?? beforeItem.visibility_class,
1383
+ };
1384
+ db.prepare(`UPDATE work_items
1385
+ SET title = ?, detail = ?, status = ?, visibility_class = ?, version = version + 1, updated_at = ?
1386
+ WHERE id = ?`).run(next.title, next.detail ?? null, next.status, next.visibility_class, updatedAt, id);
1387
+ const after = db
1388
+ .prepare('SELECT * FROM work_items WHERE id = ?')
1389
+ .get(id);
1390
+ if (!after)
1391
+ return null;
1392
+ const afterItem = rowToWorkItem(after);
1393
+ insertMemoryEventInternal({
1394
+ ...normalizeScope(afterItem),
1395
+ session_id: afterItem.session_id,
1396
+ entity_kind: 'work_item',
1397
+ entity_id: String(afterItem.id),
1398
+ event_type: patch.visibility_class !== undefined &&
1399
+ patch.title === undefined &&
1400
+ patch.detail === undefined &&
1401
+ patch.status === undefined
1402
+ ? 'work_item.visibility_changed'
1403
+ : 'work_item.updated',
1404
+ payload: {
1405
+ before: cloneValue(beforeItem),
1406
+ after: cloneValue(afterItem),
1407
+ patch: cloneValue(patch),
1408
+ },
1409
+ created_at: updatedAt,
1410
+ });
1411
+ return afterItem;
774
1412
  },
775
1413
  deleteWorkItem(id) {
1414
+ const before = db
1415
+ .prepare('SELECT * FROM work_items WHERE id = ?')
1416
+ .get(id);
776
1417
  db.prepare('DELETE FROM work_items WHERE id = ?').run(id);
1418
+ if (before) {
1419
+ const item = rowToWorkItem(before);
1420
+ insertMemoryEventInternal({
1421
+ ...normalizeScope(item),
1422
+ session_id: item.session_id,
1423
+ entity_kind: 'work_item',
1424
+ entity_id: String(item.id),
1425
+ event_type: 'work_item.deleted',
1426
+ payload: {
1427
+ before: cloneValue(item),
1428
+ },
1429
+ created_at: nowSeconds(),
1430
+ });
1431
+ }
1432
+ },
1433
+ claimWorkItem(input) {
1434
+ assertActorRef(input.actor);
1435
+ const workItemRow = db.prepare('SELECT * FROM work_items WHERE id = ?').get(input.work_item_id);
1436
+ if (!workItemRow)
1437
+ throw new ConflictError(`Work item ${input.work_item_id} does not exist`);
1438
+ const workItem = rowToWorkItem(workItemRow);
1439
+ if (workItem.status === 'done')
1440
+ throw new ConflictError(`Work item ${input.work_item_id} is done`);
1441
+ const now = input.claimed_at ?? nowSeconds();
1442
+ const existingRow = db
1443
+ .prepare('SELECT * FROM work_claims_current WHERE work_item_id = ?')
1444
+ .get(input.work_item_id);
1445
+ if (existingRow) {
1446
+ const existing = mapWorkClaim(existingRow);
1447
+ if (existing.status === 'active' && existing.expires_at > now) {
1448
+ if (existing.actor.actor_kind !== input.actor.actor_kind ||
1449
+ existing.actor.actor_id !== input.actor.actor_id) {
1450
+ throw new ConflictError(`Work item ${input.work_item_id} is already claimed`);
1451
+ }
1452
+ return this.renewWorkClaim(existing.id, input.actor, input.lease_seconds ?? 300);
1453
+ }
1454
+ db.prepare(`UPDATE work_claims_current
1455
+ SET status = 'expired', released_at = ?, release_reason = 'expired', version = version + 1
1456
+ WHERE id = ?`).run(now, existing.id);
1457
+ const expired = mapWorkClaim(db.prepare('SELECT * FROM work_claims_current WHERE id = ?').get(existing.id));
1458
+ insertMemoryEventInternal({
1459
+ ...normalizeScope(expired),
1460
+ session_id: expired.session_id,
1461
+ actor_id: expired.actor.actor_id,
1462
+ actor_kind: expired.actor.actor_kind,
1463
+ actor_system_id: expired.actor.system_id,
1464
+ actor_display_name: expired.actor.display_name,
1465
+ actor_metadata: expired.actor.metadata,
1466
+ entity_kind: 'work_claim',
1467
+ entity_id: String(expired.id),
1468
+ event_type: 'work_claim.expired',
1469
+ payload: { after: cloneValue(expired) },
1470
+ created_at: now,
1471
+ });
1472
+ }
1473
+ const normalized = normalizeScope(input);
1474
+ const actorParts = serializeActorMetadata(input.actor);
1475
+ const claimToken = `claim-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
1476
+ const result = db.prepare(`INSERT INTO work_claims_current
1477
+ (tenant_id, system_id, workspace_id, collaboration_id, scope_id, work_item_id, session_id,
1478
+ actor_kind, actor_id, actor_system_id, actor_display_name, actor_metadata,
1479
+ claim_token, status, claimed_at, expires_at, visibility_class, version)
1480
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, 1)`).run(normalized.tenant_id, normalized.system_id, normalized.workspace_id, normalized.collaboration_id, normalized.scope_id, input.work_item_id, input.session_id ?? null, ...actorParts, claimToken, now, now + (input.lease_seconds ?? 300), input.visibility_class);
1481
+ const claim = mapWorkClaim(db.prepare('SELECT * FROM work_claims_current WHERE id = ?').get(Number(result.lastInsertRowid)));
1482
+ insertMemoryEventInternal({
1483
+ ...normalized,
1484
+ session_id: claim.session_id,
1485
+ actor_id: claim.actor.actor_id,
1486
+ actor_kind: claim.actor.actor_kind,
1487
+ actor_system_id: claim.actor.system_id,
1488
+ actor_display_name: claim.actor.display_name,
1489
+ actor_metadata: claim.actor.metadata,
1490
+ entity_kind: 'work_claim',
1491
+ entity_id: String(claim.id),
1492
+ event_type: 'work_claim.claimed',
1493
+ payload: { after: cloneValue(claim) },
1494
+ created_at: now,
1495
+ });
1496
+ return claim;
1497
+ },
1498
+ renewWorkClaim(claimId, actor, leaseSeconds = 300) {
1499
+ assertActorRef(actor);
1500
+ const before = db.prepare('SELECT * FROM work_claims_current WHERE id = ?').get(claimId);
1501
+ if (!before)
1502
+ return null;
1503
+ const claim = mapWorkClaim(before);
1504
+ if (claim.actor.actor_kind !== actor.actor_kind || claim.actor.actor_id !== actor.actor_id) {
1505
+ throw new ConflictError(`Claim ${claimId} is owned by another actor`);
1506
+ }
1507
+ const now = nowSeconds();
1508
+ if (claim.status !== 'active') {
1509
+ throw new ConflictError(`Claim ${claimId} is no longer active`);
1510
+ }
1511
+ if (claim.expires_at <= now) {
1512
+ db.prepare(`UPDATE work_claims_current
1513
+ SET status = 'expired', released_at = ?, release_reason = 'expired', version = version + 1
1514
+ WHERE id = ?`).run(now, claimId);
1515
+ const expired = mapWorkClaim(db.prepare('SELECT * FROM work_claims_current WHERE id = ?').get(claimId));
1516
+ insertMemoryEventInternal({
1517
+ ...normalizeScope(expired),
1518
+ session_id: expired.session_id,
1519
+ actor_id: expired.actor.actor_id,
1520
+ actor_kind: expired.actor.actor_kind,
1521
+ actor_system_id: expired.actor.system_id,
1522
+ actor_display_name: expired.actor.display_name,
1523
+ actor_metadata: expired.actor.metadata,
1524
+ entity_kind: 'work_claim',
1525
+ entity_id: String(expired.id),
1526
+ event_type: 'work_claim.expired',
1527
+ payload: { before: cloneValue(claim), after: cloneValue(expired) },
1528
+ created_at: now,
1529
+ });
1530
+ return null;
1531
+ }
1532
+ db.prepare(`UPDATE work_claims_current
1533
+ SET expires_at = ?, version = version + 1
1534
+ WHERE id = ?`).run(Math.max(claim.expires_at, now) + leaseSeconds, claimId);
1535
+ const after = mapWorkClaim(db.prepare('SELECT * FROM work_claims_current WHERE id = ?').get(claimId));
1536
+ insertMemoryEventInternal({
1537
+ ...normalizeScope(after),
1538
+ session_id: after.session_id,
1539
+ actor_id: after.actor.actor_id,
1540
+ actor_kind: after.actor.actor_kind,
1541
+ actor_system_id: after.actor.system_id,
1542
+ actor_display_name: after.actor.display_name,
1543
+ actor_metadata: after.actor.metadata,
1544
+ entity_kind: 'work_claim',
1545
+ entity_id: String(after.id),
1546
+ event_type: 'work_claim.renewed',
1547
+ payload: { before: cloneValue(claim), after: cloneValue(after) },
1548
+ created_at: now,
1549
+ });
1550
+ return after;
1551
+ },
1552
+ releaseWorkClaim(claimId, actor, reason) {
1553
+ assertActorRef(actor);
1554
+ const before = db.prepare('SELECT * FROM work_claims_current WHERE id = ?').get(claimId);
1555
+ if (!before)
1556
+ return null;
1557
+ const claim = mapWorkClaim(before);
1558
+ if (claim.actor.actor_kind !== actor.actor_kind || claim.actor.actor_id !== actor.actor_id) {
1559
+ throw new ConflictError(`Claim ${claimId} is owned by another actor`);
1560
+ }
1561
+ if (claim.status !== 'active') {
1562
+ throw new ConflictError(`Claim ${claimId} is no longer active`);
1563
+ }
1564
+ const now = nowSeconds();
1565
+ db.prepare(`UPDATE work_claims_current
1566
+ SET status = 'released', released_at = ?, release_reason = ?, version = version + 1
1567
+ WHERE id = ?`).run(now, reason ?? null, claimId);
1568
+ const after = mapWorkClaim(db.prepare('SELECT * FROM work_claims_current WHERE id = ?').get(claimId));
1569
+ insertMemoryEventInternal({
1570
+ ...normalizeScope(after),
1571
+ session_id: after.session_id,
1572
+ actor_id: after.actor.actor_id,
1573
+ actor_kind: after.actor.actor_kind,
1574
+ actor_system_id: after.actor.system_id,
1575
+ actor_display_name: after.actor.display_name,
1576
+ actor_metadata: after.actor.metadata,
1577
+ entity_kind: 'work_claim',
1578
+ entity_id: String(after.id),
1579
+ event_type: 'work_claim.released',
1580
+ payload: { before: cloneValue(claim), after: cloneValue(after) },
1581
+ created_at: now,
1582
+ });
1583
+ return after;
1584
+ },
1585
+ getActiveWorkClaim(workItemId) {
1586
+ const row = db
1587
+ .prepare(`SELECT * FROM work_claims_current
1588
+ WHERE work_item_id = ? AND status = 'active'
1589
+ ORDER BY id DESC LIMIT 1`)
1590
+ .get(workItemId);
1591
+ if (!row)
1592
+ return null;
1593
+ const claim = mapWorkClaim(row);
1594
+ if (claim.expires_at > nowSeconds())
1595
+ return claim;
1596
+ const expiredAt = nowSeconds();
1597
+ db.prepare(`UPDATE work_claims_current
1598
+ SET status = 'expired', released_at = ?, release_reason = 'expired', version = version + 1
1599
+ WHERE id = ?`).run(expiredAt, claim.id);
1600
+ const expired = mapWorkClaim(db.prepare('SELECT * FROM work_claims_current WHERE id = ?').get(claim.id));
1601
+ insertMemoryEventInternal({
1602
+ ...normalizeScope(expired),
1603
+ session_id: expired.session_id,
1604
+ actor_id: expired.actor.actor_id,
1605
+ actor_kind: expired.actor.actor_kind,
1606
+ actor_system_id: expired.actor.system_id,
1607
+ actor_display_name: expired.actor.display_name,
1608
+ actor_metadata: expired.actor.metadata,
1609
+ entity_kind: 'work_claim',
1610
+ entity_id: String(expired.id),
1611
+ event_type: 'work_claim.expired',
1612
+ payload: { before: cloneValue(claim), after: cloneValue(expired) },
1613
+ created_at: expiredAt,
1614
+ });
1615
+ return null;
1616
+ },
1617
+ listWorkClaims(scope, options) {
1618
+ const now = nowSeconds();
1619
+ const expiredRows = db
1620
+ .prepare(`SELECT * FROM work_claims_current
1621
+ WHERE ${SCOPE_WHERE} AND status = 'active' AND expires_at <= ?`)
1622
+ .all(...scopeValues(scope), now);
1623
+ for (const row of expiredRows) {
1624
+ const before = mapWorkClaim(row);
1625
+ db.prepare(`UPDATE work_claims_current
1626
+ SET status = 'expired', released_at = ?, release_reason = 'expired', version = version + 1
1627
+ WHERE id = ?`).run(now, before.id);
1628
+ const after = mapWorkClaim(db.prepare('SELECT * FROM work_claims_current WHERE id = ?').get(before.id));
1629
+ insertMemoryEventInternal({
1630
+ ...normalizeScope(after),
1631
+ session_id: after.session_id,
1632
+ actor_id: after.actor.actor_id,
1633
+ actor_kind: after.actor.actor_kind,
1634
+ actor_system_id: after.actor.system_id,
1635
+ actor_display_name: after.actor.display_name,
1636
+ actor_metadata: after.actor.metadata,
1637
+ entity_kind: 'work_claim',
1638
+ entity_id: String(after.id),
1639
+ event_type: 'work_claim.expired',
1640
+ payload: { before: cloneValue(before), after: cloneValue(after) },
1641
+ created_at: now,
1642
+ });
1643
+ }
1644
+ const rows = db
1645
+ .prepare(`SELECT * FROM work_claims_current WHERE ${SCOPE_WHERE} ORDER BY claimed_at DESC`)
1646
+ .all(...scopeValues(scope));
1647
+ return rows
1648
+ .map(mapWorkClaim)
1649
+ .filter((claim) => {
1650
+ if (!options?.includeExpired && claim.status === 'expired')
1651
+ return false;
1652
+ if (!options?.includeReleased && claim.status === 'released')
1653
+ return false;
1654
+ if (options?.sessionId && claim.session_id !== options.sessionId)
1655
+ return false;
1656
+ if (options?.visibilityClass && claim.visibility_class !== options.visibilityClass)
1657
+ return false;
1658
+ if (options?.actor) {
1659
+ return (claim.actor.actor_kind === options.actor.actor_kind &&
1660
+ claim.actor.actor_id === options.actor.actor_id);
1661
+ }
1662
+ return true;
1663
+ });
1664
+ },
1665
+ listWorkClaimsCrossScope(scope, level, options) {
1666
+ const now = nowSeconds();
1667
+ const expiredRows = db
1668
+ .prepare(`SELECT * FROM work_claims_current
1669
+ WHERE ${scopeWhereForLevel(scope, level)} AND status = 'active' AND expires_at <= ?`)
1670
+ .all(...scopeParamsForLevel(scope, level), now);
1671
+ for (const row of expiredRows) {
1672
+ const before = mapWorkClaim(row);
1673
+ db.prepare(`UPDATE work_claims_current
1674
+ SET status = 'expired', released_at = ?, release_reason = 'expired', version = version + 1
1675
+ WHERE id = ?`).run(now, before.id);
1676
+ const after = mapWorkClaim(db.prepare('SELECT * FROM work_claims_current WHERE id = ?').get(before.id));
1677
+ insertMemoryEventInternal({
1678
+ ...normalizeScope(after),
1679
+ session_id: after.session_id,
1680
+ actor_id: after.actor.actor_id,
1681
+ actor_kind: after.actor.actor_kind,
1682
+ actor_system_id: after.actor.system_id,
1683
+ actor_display_name: after.actor.display_name,
1684
+ actor_metadata: after.actor.metadata,
1685
+ entity_kind: 'work_claim',
1686
+ entity_id: String(after.id),
1687
+ event_type: 'work_claim.expired',
1688
+ payload: { before: cloneValue(before), after: cloneValue(after) },
1689
+ created_at: now,
1690
+ });
1691
+ }
1692
+ const rows = db
1693
+ .prepare(`SELECT * FROM work_claims_current WHERE ${scopeWhereForLevel(scope, level)} ORDER BY claimed_at DESC`)
1694
+ .all(...scopeParamsForLevel(scope, level));
1695
+ return rows
1696
+ .map(mapWorkClaim)
1697
+ .filter((claim) => {
1698
+ if (!options?.includeExpired && claim.status === 'expired')
1699
+ return false;
1700
+ if (!options?.includeReleased && claim.status === 'released')
1701
+ return false;
1702
+ if (options?.sessionId && claim.session_id !== options.sessionId)
1703
+ return false;
1704
+ if (options?.visibilityClass && claim.visibility_class !== options.visibilityClass)
1705
+ return false;
1706
+ if (options?.actor) {
1707
+ return (claim.actor.actor_kind === options.actor.actor_kind &&
1708
+ claim.actor.actor_id === options.actor.actor_id);
1709
+ }
1710
+ return true;
1711
+ });
1712
+ },
1713
+ createHandoff(input) {
1714
+ assertActorRef(input.from_actor, 'from_actor');
1715
+ assertActorRef(input.to_actor, 'to_actor');
1716
+ const normalized = normalizeScope(input);
1717
+ const createdAt = input.created_at ?? nowSeconds();
1718
+ const fromParts = serializeActorMetadata(input.from_actor);
1719
+ const toParts = serializeActorMetadata(input.to_actor);
1720
+ const result = db.prepare(`INSERT INTO handoff_records
1721
+ (tenant_id, system_id, workspace_id, collaboration_id, scope_id, work_item_id, session_id,
1722
+ from_actor_kind, from_actor_id, from_actor_system_id, from_actor_display_name, from_actor_metadata,
1723
+ to_actor_kind, to_actor_id, to_actor_system_id, to_actor_display_name, to_actor_metadata,
1724
+ summary, context_bundle_ref, status, created_at, expires_at, visibility_class, version)
1725
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?, 1)`).run(normalized.tenant_id, normalized.system_id, normalized.workspace_id, normalized.collaboration_id, normalized.scope_id, input.work_item_id, input.session_id ?? null, ...fromParts, ...toParts, input.summary, input.context_bundle_ref ?? null, createdAt, input.expires_at ?? null, input.visibility_class);
1726
+ const handoff = mapHandoff(db.prepare('SELECT * FROM handoff_records WHERE id = ?').get(Number(result.lastInsertRowid)));
1727
+ insertMemoryEventInternal({
1728
+ ...normalized,
1729
+ session_id: handoff.session_id,
1730
+ actor_id: handoff.from_actor.actor_id,
1731
+ actor_kind: handoff.from_actor.actor_kind,
1732
+ actor_system_id: handoff.from_actor.system_id,
1733
+ actor_display_name: handoff.from_actor.display_name,
1734
+ actor_metadata: handoff.from_actor.metadata,
1735
+ entity_kind: 'handoff',
1736
+ entity_id: String(handoff.id),
1737
+ event_type: 'handoff.created',
1738
+ payload: { after: cloneValue(handoff) },
1739
+ created_at: createdAt,
1740
+ });
1741
+ return handoff;
1742
+ },
1743
+ acceptHandoff(handoffId, actor, reason) {
1744
+ assertActorRef(actor);
1745
+ const before = db.prepare('SELECT * FROM handoff_records WHERE id = ?').get(handoffId);
1746
+ if (!before)
1747
+ return null;
1748
+ const handoff = mapHandoff(before);
1749
+ if (handoff.to_actor.actor_kind !== actor.actor_kind || handoff.to_actor.actor_id !== actor.actor_id) {
1750
+ throw new ConflictError(`Handoff ${handoffId} is assigned to another actor`);
1751
+ }
1752
+ const now = nowSeconds();
1753
+ if (handoff.status !== 'pending') {
1754
+ throw new ConflictError(`Handoff ${handoffId} is no longer pending`);
1755
+ }
1756
+ if (handoff.expires_at != null && handoff.expires_at <= now) {
1757
+ db.prepare(`UPDATE handoff_records
1758
+ SET status = 'expired', decision_reason = 'expired', version = version + 1
1759
+ WHERE id = ?`).run(handoffId);
1760
+ const expired = mapHandoff(db.prepare('SELECT * FROM handoff_records WHERE id = ?').get(handoffId));
1761
+ insertMemoryEventInternal({
1762
+ ...normalizeScope(expired),
1763
+ session_id: expired.session_id,
1764
+ actor_id: expired.to_actor.actor_id,
1765
+ actor_kind: expired.to_actor.actor_kind,
1766
+ actor_system_id: expired.to_actor.system_id,
1767
+ actor_display_name: expired.to_actor.display_name,
1768
+ actor_metadata: expired.to_actor.metadata,
1769
+ entity_kind: 'handoff',
1770
+ entity_id: String(expired.id),
1771
+ event_type: 'handoff.expired',
1772
+ payload: { before: cloneValue(handoff), after: cloneValue(expired) },
1773
+ created_at: now,
1774
+ });
1775
+ return null;
1776
+ }
1777
+ const activeClaim = this.getActiveWorkClaim(handoff.work_item_id);
1778
+ if (activeClaim &&
1779
+ !(activeClaim.actor.actor_kind === handoff.from_actor.actor_kind && activeClaim.actor.actor_id === handoff.from_actor.actor_id)) {
1780
+ throw new ConflictError(`Work item ${handoff.work_item_id} has another active owner`);
1781
+ }
1782
+ if (activeClaim) {
1783
+ this.releaseWorkClaim(activeClaim.id, handoff.from_actor, 'handoff_accepted');
1784
+ }
1785
+ this.claimWorkItem({
1786
+ ...normalizeScope(handoff),
1787
+ work_item_id: handoff.work_item_id,
1788
+ actor,
1789
+ session_id: handoff.session_id,
1790
+ visibility_class: handoff.visibility_class,
1791
+ });
1792
+ db.prepare(`UPDATE handoff_records
1793
+ SET status = 'accepted', accepted_at = ?, decision_reason = ?, version = version + 1
1794
+ WHERE id = ?`).run(now, reason ?? null, handoffId);
1795
+ const after = mapHandoff(db.prepare('SELECT * FROM handoff_records WHERE id = ?').get(handoffId));
1796
+ insertMemoryEventInternal({
1797
+ ...normalizeScope(after),
1798
+ session_id: after.session_id,
1799
+ actor_id: actor.actor_id,
1800
+ actor_kind: actor.actor_kind,
1801
+ actor_system_id: actor.system_id,
1802
+ actor_display_name: actor.display_name,
1803
+ actor_metadata: actor.metadata,
1804
+ entity_kind: 'handoff',
1805
+ entity_id: String(after.id),
1806
+ event_type: 'handoff.accepted',
1807
+ payload: { before: cloneValue(handoff), after: cloneValue(after) },
1808
+ created_at: now,
1809
+ });
1810
+ return after;
1811
+ },
1812
+ rejectHandoff(handoffId, actor, reason) {
1813
+ assertActorRef(actor);
1814
+ const before = db.prepare('SELECT * FROM handoff_records WHERE id = ?').get(handoffId);
1815
+ if (!before)
1816
+ return null;
1817
+ const handoff = mapHandoff(before);
1818
+ if (handoff.to_actor.actor_kind !== actor.actor_kind || handoff.to_actor.actor_id !== actor.actor_id) {
1819
+ throw new ConflictError(`Handoff ${handoffId} is assigned to another actor`);
1820
+ }
1821
+ const now = nowSeconds();
1822
+ if (handoff.status !== 'pending') {
1823
+ throw new ConflictError(`Handoff ${handoffId} is no longer pending`);
1824
+ }
1825
+ if (handoff.expires_at != null && handoff.expires_at <= now) {
1826
+ db.prepare(`UPDATE handoff_records
1827
+ SET status = 'expired', decision_reason = 'expired', version = version + 1
1828
+ WHERE id = ?`).run(handoffId);
1829
+ const expired = mapHandoff(db.prepare('SELECT * FROM handoff_records WHERE id = ?').get(handoffId));
1830
+ insertMemoryEventInternal({
1831
+ ...normalizeScope(expired),
1832
+ session_id: expired.session_id,
1833
+ actor_id: expired.to_actor.actor_id,
1834
+ actor_kind: expired.to_actor.actor_kind,
1835
+ actor_system_id: expired.to_actor.system_id,
1836
+ actor_display_name: expired.to_actor.display_name,
1837
+ actor_metadata: expired.to_actor.metadata,
1838
+ entity_kind: 'handoff',
1839
+ entity_id: String(expired.id),
1840
+ event_type: 'handoff.expired',
1841
+ payload: { before: cloneValue(handoff), after: cloneValue(expired) },
1842
+ created_at: now,
1843
+ });
1844
+ return null;
1845
+ }
1846
+ db.prepare(`UPDATE handoff_records
1847
+ SET status = 'rejected', rejected_at = ?, decision_reason = ?, version = version + 1
1848
+ WHERE id = ?`).run(now, reason ?? null, handoffId);
1849
+ const after = mapHandoff(db.prepare('SELECT * FROM handoff_records WHERE id = ?').get(handoffId));
1850
+ insertMemoryEventInternal({
1851
+ ...normalizeScope(after),
1852
+ session_id: after.session_id,
1853
+ actor_id: actor.actor_id,
1854
+ actor_kind: actor.actor_kind,
1855
+ actor_system_id: actor.system_id,
1856
+ actor_display_name: actor.display_name,
1857
+ actor_metadata: actor.metadata,
1858
+ entity_kind: 'handoff',
1859
+ entity_id: String(after.id),
1860
+ event_type: 'handoff.rejected',
1861
+ payload: { before: cloneValue(handoff), after: cloneValue(after) },
1862
+ created_at: now,
1863
+ });
1864
+ return after;
1865
+ },
1866
+ cancelHandoff(handoffId, actor, reason) {
1867
+ assertActorRef(actor);
1868
+ const before = db.prepare('SELECT * FROM handoff_records WHERE id = ?').get(handoffId);
1869
+ if (!before)
1870
+ return null;
1871
+ const handoff = mapHandoff(before);
1872
+ if (handoff.from_actor.actor_kind !== actor.actor_kind || handoff.from_actor.actor_id !== actor.actor_id) {
1873
+ throw new ConflictError(`Handoff ${handoffId} was created by another actor`);
1874
+ }
1875
+ const now = nowSeconds();
1876
+ if (handoff.status !== 'pending') {
1877
+ throw new ConflictError(`Handoff ${handoffId} is no longer pending`);
1878
+ }
1879
+ if (handoff.expires_at != null && handoff.expires_at <= now) {
1880
+ db.prepare(`UPDATE handoff_records
1881
+ SET status = 'expired', decision_reason = 'expired', version = version + 1
1882
+ WHERE id = ?`).run(handoffId);
1883
+ const expired = mapHandoff(db.prepare('SELECT * FROM handoff_records WHERE id = ?').get(handoffId));
1884
+ insertMemoryEventInternal({
1885
+ ...normalizeScope(expired),
1886
+ session_id: expired.session_id,
1887
+ actor_id: expired.from_actor.actor_id,
1888
+ actor_kind: expired.from_actor.actor_kind,
1889
+ actor_system_id: expired.from_actor.system_id,
1890
+ actor_display_name: expired.from_actor.display_name,
1891
+ actor_metadata: expired.from_actor.metadata,
1892
+ entity_kind: 'handoff',
1893
+ entity_id: String(expired.id),
1894
+ event_type: 'handoff.expired',
1895
+ payload: { before: cloneValue(handoff), after: cloneValue(expired) },
1896
+ created_at: now,
1897
+ });
1898
+ return null;
1899
+ }
1900
+ db.prepare(`UPDATE handoff_records
1901
+ SET status = 'canceled', canceled_at = ?, decision_reason = ?, version = version + 1
1902
+ WHERE id = ?`).run(now, reason ?? null, handoffId);
1903
+ const after = mapHandoff(db.prepare('SELECT * FROM handoff_records WHERE id = ?').get(handoffId));
1904
+ insertMemoryEventInternal({
1905
+ ...normalizeScope(after),
1906
+ session_id: after.session_id,
1907
+ actor_id: actor.actor_id,
1908
+ actor_kind: actor.actor_kind,
1909
+ actor_system_id: actor.system_id,
1910
+ actor_display_name: actor.display_name,
1911
+ actor_metadata: actor.metadata,
1912
+ entity_kind: 'handoff',
1913
+ entity_id: String(after.id),
1914
+ event_type: 'handoff.canceled',
1915
+ payload: { before: cloneValue(handoff), after: cloneValue(after) },
1916
+ created_at: now,
1917
+ });
1918
+ return after;
1919
+ },
1920
+ listHandoffs(scope, options) {
1921
+ const now = nowSeconds();
1922
+ const expiredRows = db
1923
+ .prepare(`SELECT * FROM handoff_records
1924
+ WHERE ${SCOPE_WHERE} AND status = 'pending' AND expires_at IS NOT NULL AND expires_at <= ?`)
1925
+ .all(...scopeValues(scope), now);
1926
+ for (const row of expiredRows) {
1927
+ const before = mapHandoff(row);
1928
+ db.prepare(`UPDATE handoff_records
1929
+ SET status = 'expired', decision_reason = 'expired', version = version + 1
1930
+ WHERE id = ?`).run(before.id);
1931
+ const after = mapHandoff(db.prepare('SELECT * FROM handoff_records WHERE id = ?').get(before.id));
1932
+ insertMemoryEventInternal({
1933
+ ...normalizeScope(after),
1934
+ session_id: after.session_id,
1935
+ actor_id: after.to_actor.actor_id,
1936
+ actor_kind: after.to_actor.actor_kind,
1937
+ actor_system_id: after.to_actor.system_id,
1938
+ actor_display_name: after.to_actor.display_name,
1939
+ actor_metadata: after.to_actor.metadata,
1940
+ entity_kind: 'handoff',
1941
+ entity_id: String(after.id),
1942
+ event_type: 'handoff.expired',
1943
+ payload: { before: cloneValue(before), after: cloneValue(after) },
1944
+ created_at: now,
1945
+ });
1946
+ }
1947
+ const rows = db
1948
+ .prepare(`SELECT * FROM handoff_records WHERE ${SCOPE_WHERE} ORDER BY created_at DESC`)
1949
+ .all(...scopeValues(scope));
1950
+ return rows.map(mapHandoff).filter((handoff) => {
1951
+ if (options?.sessionId && handoff.session_id !== options.sessionId)
1952
+ return false;
1953
+ if (options?.statuses && !options.statuses.includes(handoff.status))
1954
+ return false;
1955
+ if (!options?.actor)
1956
+ return true;
1957
+ const inbound = handoff.to_actor.actor_kind === options.actor.actor_kind &&
1958
+ handoff.to_actor.actor_id === options.actor.actor_id;
1959
+ const outbound = handoff.from_actor.actor_kind === options.actor.actor_kind &&
1960
+ handoff.from_actor.actor_id === options.actor.actor_id;
1961
+ if (options.direction === 'inbound')
1962
+ return inbound;
1963
+ if (options.direction === 'outbound')
1964
+ return outbound;
1965
+ return inbound || outbound;
1966
+ });
1967
+ },
1968
+ listHandoffsCrossScope(scope, level, options) {
1969
+ const now = nowSeconds();
1970
+ const expiredRows = db
1971
+ .prepare(`SELECT * FROM handoff_records
1972
+ WHERE ${scopeWhereForLevel(scope, level)} AND status = 'pending' AND expires_at IS NOT NULL AND expires_at <= ?`)
1973
+ .all(...scopeParamsForLevel(scope, level), now);
1974
+ for (const row of expiredRows) {
1975
+ const before = mapHandoff(row);
1976
+ db.prepare(`UPDATE handoff_records
1977
+ SET status = 'expired', decision_reason = 'expired', version = version + 1
1978
+ WHERE id = ?`).run(before.id);
1979
+ const after = mapHandoff(db.prepare('SELECT * FROM handoff_records WHERE id = ?').get(before.id));
1980
+ insertMemoryEventInternal({
1981
+ ...normalizeScope(after),
1982
+ session_id: after.session_id,
1983
+ actor_id: after.to_actor.actor_id,
1984
+ actor_kind: after.to_actor.actor_kind,
1985
+ actor_system_id: after.to_actor.system_id,
1986
+ actor_display_name: after.to_actor.display_name,
1987
+ actor_metadata: after.to_actor.metadata,
1988
+ entity_kind: 'handoff',
1989
+ entity_id: String(after.id),
1990
+ event_type: 'handoff.expired',
1991
+ payload: { before: cloneValue(before), after: cloneValue(after) },
1992
+ created_at: now,
1993
+ });
1994
+ }
1995
+ const rows = db
1996
+ .prepare(`SELECT * FROM handoff_records WHERE ${scopeWhereForLevel(scope, level)} ORDER BY created_at DESC`)
1997
+ .all(...scopeParamsForLevel(scope, level));
1998
+ return rows.map(mapHandoff).filter((handoff) => {
1999
+ if (options?.sessionId && handoff.session_id !== options.sessionId)
2000
+ return false;
2001
+ if (options?.statuses && !options.statuses.includes(handoff.status))
2002
+ return false;
2003
+ if (!options?.actor)
2004
+ return true;
2005
+ const inbound = handoff.to_actor.actor_kind === options.actor.actor_kind &&
2006
+ handoff.to_actor.actor_id === options.actor.actor_id;
2007
+ const outbound = handoff.from_actor.actor_kind === options.actor.actor_kind &&
2008
+ handoff.from_actor.actor_id === options.actor.actor_id;
2009
+ if (options.direction === 'inbound')
2010
+ return inbound;
2011
+ if (options.direction === 'outbound')
2012
+ return outbound;
2013
+ return inbound || outbound;
2014
+ });
777
2015
  },
778
2016
  touchKnowledgeMemory(id) {
2017
+ const before = getKnowledgeMemoryById(id);
2018
+ const touchedAt = nowSeconds();
2019
+ db.prepare(`UPDATE knowledge_memory
2020
+ SET last_accessed_at = ?, access_count = access_count + 1
2021
+ WHERE id = ?`).run(touchedAt, id);
2022
+ const after = getKnowledgeMemoryById(id);
2023
+ if (before && after) {
2024
+ insertMemoryEventInternal({
2025
+ ...normalizeScope(after),
2026
+ entity_kind: 'knowledge_memory',
2027
+ entity_id: String(after.id),
2028
+ event_type: 'knowledge.touched',
2029
+ payload: {
2030
+ before: cloneValue(before),
2031
+ after: cloneValue(after),
2032
+ patch: {
2033
+ last_accessed_at: touchedAt,
2034
+ access_count: after.access_count,
2035
+ },
2036
+ },
2037
+ created_at: touchedAt,
2038
+ });
2039
+ }
2040
+ },
2041
+ touchKnowledgeMemories(ids) {
2042
+ const uniqueIds = [...new Set(ids)].filter((id) => Number.isInteger(id) && id > 0);
2043
+ if (uniqueIds.length === 0)
2044
+ return;
2045
+ const placeholders = uniqueIds.map(() => '?').join(', ');
2046
+ const beforeRows = db
2047
+ .prepare(`SELECT * FROM knowledge_memory WHERE id IN (${placeholders})`)
2048
+ .all(...uniqueIds);
2049
+ if (beforeRows.length === 0)
2050
+ return;
2051
+ const touchedAt = nowSeconds();
779
2052
  db.prepare(`UPDATE knowledge_memory
780
2053
  SET last_accessed_at = ?, access_count = access_count + 1
781
- WHERE id = ?`).run(nowSeconds(), id);
2054
+ WHERE id IN (${placeholders})`).run(touchedAt, ...uniqueIds);
2055
+ const afterRows = db
2056
+ .prepare(`SELECT * FROM knowledge_memory WHERE id IN (${placeholders})`)
2057
+ .all(...uniqueIds);
2058
+ const afterById = new Map(afterRows.map((row) => [Number(row.id), rowToKnowledgeMemory(row)]));
2059
+ insertMemoryEventsBatchInternal(beforeRows.flatMap((row) => {
2060
+ const before = rowToKnowledgeMemory(row);
2061
+ const after = afterById.get(before.id);
2062
+ if (!after)
2063
+ return [];
2064
+ return [{
2065
+ ...normalizeScope(after),
2066
+ entity_kind: 'knowledge_memory',
2067
+ entity_id: String(after.id),
2068
+ event_type: 'knowledge.touched',
2069
+ payload: {
2070
+ before: cloneValue(before),
2071
+ after: cloneValue(after),
2072
+ patch: {
2073
+ last_accessed_at: touchedAt,
2074
+ access_count: after.access_count,
2075
+ },
2076
+ },
2077
+ created_at: touchedAt,
2078
+ }];
2079
+ }));
782
2080
  },
783
2081
  retireKnowledgeMemory(id, retiredAt = nowSeconds()) {
2082
+ const before = getKnowledgeMemoryById(id);
784
2083
  db.prepare('UPDATE knowledge_memory SET retired_at = ? WHERE id = ?').run(retiredAt, id);
2084
+ const after = getKnowledgeMemoryById(id);
2085
+ if (before && after) {
2086
+ insertMemoryEventInternal({
2087
+ ...normalizeScope(after),
2088
+ entity_kind: 'knowledge_memory',
2089
+ entity_id: String(after.id),
2090
+ event_type: 'knowledge.retired',
2091
+ payload: {
2092
+ before: cloneValue(before),
2093
+ after: cloneValue(after),
2094
+ patch: {
2095
+ retired_at: retiredAt,
2096
+ },
2097
+ },
2098
+ created_at: retiredAt,
2099
+ });
2100
+ }
785
2101
  },
786
2102
  supersedeKnowledgeMemory(oldId, newId) {
2103
+ const before = getKnowledgeMemoryById(oldId);
2104
+ const supersededAt = nowSeconds();
787
2105
  db.prepare(`UPDATE knowledge_memory
788
2106
  SET superseded_by_id = ?, superseded_at = ?, knowledge_state = 'superseded'
789
- WHERE id = ?`).run(newId, nowSeconds(), oldId);
2107
+ WHERE id = ?`).run(newId, supersededAt, oldId);
2108
+ const after = getKnowledgeMemoryById(oldId);
2109
+ if (before && after) {
2110
+ insertMemoryEventInternal({
2111
+ ...normalizeScope(after),
2112
+ entity_kind: 'knowledge_memory',
2113
+ entity_id: String(after.id),
2114
+ event_type: 'knowledge.superseded',
2115
+ payload: {
2116
+ before: cloneValue(before),
2117
+ after: cloneValue(after),
2118
+ refs: {
2119
+ new_id: newId,
2120
+ },
2121
+ },
2122
+ created_at: supersededAt,
2123
+ });
2124
+ }
790
2125
  },
791
2126
  upsertContextMonitor(input) {
792
2127
  const scope = validateContextMonitorUpsert(input);
@@ -828,6 +2163,353 @@ function createAdapterFromDatabase(db, telemetry) {
828
2163
  .all(...scopeValues(scope), limit);
829
2164
  return rows.map(rowToCompactionLog);
830
2165
  },
2166
+ insertPlaybook(input) {
2167
+ const scope = normalizeScope(input);
2168
+ const now = nowSeconds();
2169
+ const result = db
2170
+ .prepare(`INSERT INTO playbooks
2171
+ (tenant_id, system_id, workspace_id, collaboration_id, scope_id, title, description, instructions,
2172
+ references_json, templates, scripts, assets, tags, status, source_session_id, source_working_memory_id, created_at, updated_at)
2173
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
2174
+ .run(scope.tenant_id, scope.system_id, scope.workspace_id, scope.collaboration_id, scope.scope_id, input.title, input.description, input.instructions, serializeStringArray(input.references ?? []), serializeStringArray(input.templates ?? []), serializeStringArray(input.scripts ?? []), serializeStringArray(input.assets ?? []), serializeStringArray(input.tags ?? []), input.status ?? 'draft', input.source_session_id ?? null, input.source_working_memory_id ?? null, input.created_at ?? now, now);
2175
+ const playbook = this.getPlaybookById(Number(result.lastInsertRowid));
2176
+ insertMemoryEventInternal({
2177
+ ...scope,
2178
+ session_id: playbook.source_session_id,
2179
+ entity_kind: 'playbook',
2180
+ entity_id: String(playbook.id),
2181
+ event_type: 'playbook.created',
2182
+ payload: {
2183
+ after: cloneValue(playbook),
2184
+ },
2185
+ created_at: playbook.created_at,
2186
+ });
2187
+ return playbook;
2188
+ },
2189
+ getPlaybookById(id) {
2190
+ const row = db.prepare('SELECT * FROM playbooks WHERE id = ?').get(id);
2191
+ return row ? rowToPlaybook(row) : null;
2192
+ },
2193
+ getExistingPlaybookIds(ids) {
2194
+ return getExistingIds('playbooks', ids);
2195
+ },
2196
+ getActivePlaybooks(scope) {
2197
+ const rows = db
2198
+ .prepare(`SELECT * FROM playbooks WHERE ${SCOPE_WHERE} AND status IN ('draft', 'active') ORDER BY id DESC`)
2199
+ .all(...scopeValues(scope));
2200
+ return rows.map(rowToPlaybook);
2201
+ },
2202
+ getActivePlaybooksCrossScope(scope, level) {
2203
+ const rows = db
2204
+ .prepare(`SELECT * FROM playbooks
2205
+ WHERE ${scopeWhereForLevel(scope, level)} AND status IN ('draft', 'active')
2206
+ ORDER BY id DESC`)
2207
+ .all(...scopeParamsForLevel(scope, level));
2208
+ return rows.map(rowToPlaybook);
2209
+ },
2210
+ searchPlaybooks(scope, query, options) {
2211
+ const limit = options?.limit ?? 20;
2212
+ const activeOnly = options?.activeOnly ?? true;
2213
+ const safeQuery = toSafeFtsQuery(query);
2214
+ if (!safeQuery)
2215
+ return [];
2216
+ const statusFilter = activeOnly
2217
+ ? `AND p.status NOT IN ('archived', 'deprecated')`
2218
+ : '';
2219
+ try {
2220
+ const rows = db
2221
+ .prepare(`SELECT p.* FROM playbooks p
2222
+ INNER JOIN playbooks_fts f ON f.rowid = p.id
2223
+ WHERE p.${SCOPE_WHERE} ${statusFilter}
2224
+ AND playbooks_fts MATCH ?
2225
+ ORDER BY rank LIMIT ?`)
2226
+ .all(...scopeValues(scope), safeQuery, limit);
2227
+ return rows.map((row, index) => ({ item: rowToPlaybook(row), rank: index }));
2228
+ }
2229
+ catch {
2230
+ return [];
2231
+ }
2232
+ },
2233
+ searchPlaybooksCrossScope(scope, level, query, options) {
2234
+ const limit = options?.limit ?? 20;
2235
+ const activeOnly = options?.activeOnly ?? true;
2236
+ const safeQuery = toSafeFtsQuery(query);
2237
+ if (!safeQuery)
2238
+ return [];
2239
+ const statusFilter = activeOnly
2240
+ ? `AND p.status NOT IN ('archived', 'deprecated')`
2241
+ : '';
2242
+ try {
2243
+ const rows = db
2244
+ .prepare(`SELECT p.* FROM playbooks p
2245
+ INNER JOIN playbooks_fts f ON f.rowid = p.id
2246
+ WHERE ${scopeWhereForLevelWithPrefix(scope, level, 'p')} ${statusFilter}
2247
+ AND playbooks_fts MATCH ?
2248
+ ORDER BY rank LIMIT ?`)
2249
+ .all(...scopeParamsForLevel(scope, level), safeQuery, limit);
2250
+ return rows.map((row, index) => ({ item: rowToPlaybook(row), rank: index }));
2251
+ }
2252
+ catch {
2253
+ return [];
2254
+ }
2255
+ },
2256
+ updatePlaybook(id, patch) {
2257
+ const before = this.getPlaybookById(id);
2258
+ const sets = [];
2259
+ const values = [];
2260
+ if (patch.title != null) {
2261
+ sets.push('title = ?');
2262
+ values.push(patch.title);
2263
+ }
2264
+ if (patch.description != null) {
2265
+ sets.push('description = ?');
2266
+ values.push(patch.description);
2267
+ }
2268
+ if (patch.instructions != null) {
2269
+ sets.push('instructions = ?');
2270
+ values.push(patch.instructions);
2271
+ }
2272
+ if (patch.references != null) {
2273
+ sets.push('references_json = ?');
2274
+ values.push(serializeStringArray(patch.references));
2275
+ }
2276
+ if (patch.templates != null) {
2277
+ sets.push('templates = ?');
2278
+ values.push(serializeStringArray(patch.templates));
2279
+ }
2280
+ if (patch.scripts != null) {
2281
+ sets.push('scripts = ?');
2282
+ values.push(serializeStringArray(patch.scripts));
2283
+ }
2284
+ if (patch.assets != null) {
2285
+ sets.push('assets = ?');
2286
+ values.push(serializeStringArray(patch.assets));
2287
+ }
2288
+ if (patch.tags != null) {
2289
+ sets.push('tags = ?');
2290
+ values.push(serializeStringArray(patch.tags));
2291
+ }
2292
+ if (patch.status != null) {
2293
+ sets.push('status = ?');
2294
+ values.push(patch.status);
2295
+ }
2296
+ if (sets.length === 0)
2297
+ return this.getPlaybookById(id);
2298
+ sets.push('updated_at = ?');
2299
+ values.push(nowSeconds());
2300
+ values.push(id);
2301
+ db.prepare(`UPDATE playbooks SET ${sets.join(', ')} WHERE id = ?`).run(...values);
2302
+ const after = this.getPlaybookById(id);
2303
+ if (before && after) {
2304
+ insertMemoryEventInternal({
2305
+ ...normalizeScope(after),
2306
+ session_id: after.source_session_id,
2307
+ entity_kind: 'playbook',
2308
+ entity_id: String(after.id),
2309
+ event_type: 'playbook.updated',
2310
+ payload: {
2311
+ before: cloneValue(before),
2312
+ after: cloneValue(after),
2313
+ patch: cloneValue(patch),
2314
+ },
2315
+ created_at: after.updated_at,
2316
+ });
2317
+ }
2318
+ return after;
2319
+ },
2320
+ recordPlaybookUse(id) {
2321
+ const before = this.getPlaybookById(id);
2322
+ const usedAt = nowSeconds();
2323
+ db.prepare('UPDATE playbooks SET use_count = use_count + 1, last_used_at = ? WHERE id = ?').run(usedAt, id);
2324
+ const after = this.getPlaybookById(id);
2325
+ if (before && after) {
2326
+ insertMemoryEventInternal({
2327
+ ...normalizeScope(after),
2328
+ session_id: after.source_session_id,
2329
+ entity_kind: 'playbook',
2330
+ entity_id: String(after.id),
2331
+ event_type: 'playbook.used',
2332
+ payload: {
2333
+ before: cloneValue(before),
2334
+ after: cloneValue(after),
2335
+ refs: {
2336
+ use_count: after.use_count,
2337
+ },
2338
+ },
2339
+ created_at: usedAt,
2340
+ });
2341
+ }
2342
+ },
2343
+ insertPlaybookRevision(input) {
2344
+ const playbook = this.getPlaybookById(input.playbook_id);
2345
+ if (!playbook) {
2346
+ throw new Error(`Playbook ${input.playbook_id} not found`);
2347
+ }
2348
+ const now = nowSeconds();
2349
+ const result = db
2350
+ .prepare(`INSERT INTO playbook_revisions
2351
+ (tenant_id, system_id, workspace_id, collaboration_id, scope_id, playbook_id, instructions, revision_reason, source_session_id, created_at)
2352
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
2353
+ .run(playbook.tenant_id, playbook.system_id, playbook.workspace_id, playbook.collaboration_id, playbook.scope_id, input.playbook_id, input.instructions, input.revision_reason, input.source_session_id ?? null, input.created_at ?? now);
2354
+ db.prepare('UPDATE playbooks SET revision_count = revision_count + 1 WHERE id = ?').run(input.playbook_id);
2355
+ const row = db.prepare('SELECT * FROM playbook_revisions WHERE id = ?').get(Number(result.lastInsertRowid));
2356
+ const revision = rowToPlaybookRevision(row);
2357
+ insertMemoryEventInternal({
2358
+ ...normalizeScope(revision),
2359
+ session_id: revision.source_session_id,
2360
+ entity_kind: 'playbook_revision',
2361
+ entity_id: String(revision.id),
2362
+ event_type: 'playbook.revised',
2363
+ payload: {
2364
+ after: cloneValue(revision),
2365
+ refs: {
2366
+ playbook_id: revision.playbook_id,
2367
+ },
2368
+ },
2369
+ created_at: revision.created_at,
2370
+ });
2371
+ return revision;
2372
+ },
2373
+ getPlaybookRevisions(playbookId) {
2374
+ const rows = db
2375
+ .prepare('SELECT * FROM playbook_revisions WHERE playbook_id = ? ORDER BY created_at DESC')
2376
+ .all(playbookId);
2377
+ return rows.map(rowToPlaybookRevision);
2378
+ },
2379
+ insertAssociation(input) {
2380
+ const scope = normalizeScope(input);
2381
+ const now = nowSeconds();
2382
+ let result;
2383
+ try {
2384
+ result = db
2385
+ .prepare(`INSERT INTO associations
2386
+ (tenant_id, system_id, workspace_id, collaboration_id, scope_id,
2387
+ source_kind, source_id, target_kind, target_id, association_type, confidence, auto_generated, created_at)
2388
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
2389
+ .run(scope.tenant_id, scope.system_id, scope.workspace_id, scope.collaboration_id, scope.scope_id, input.source_kind, input.source_id, input.target_kind, input.target_id, input.association_type, input.confidence ?? 0.5, input.auto_generated ? 1 : 0, input.created_at ?? now);
2390
+ }
2391
+ catch (err) {
2392
+ if (err &&
2393
+ typeof err === 'object' &&
2394
+ err.code === 'SQLITE_CONSTRAINT_UNIQUE') {
2395
+ throw new UniqueConstraintError(`Association already exists: ${input.source_kind}:${input.source_id} -> ${input.target_kind}:${input.target_id} (${input.association_type})`, err);
2396
+ }
2397
+ throw err;
2398
+ }
2399
+ const row = db.prepare('SELECT * FROM associations WHERE id = ?').get(Number(result.lastInsertRowid));
2400
+ const association = {
2401
+ ...row,
2402
+ collaboration_id: row.collaboration_id ?? row.workspace_id,
2403
+ auto_generated: row.auto_generated === 1,
2404
+ };
2405
+ insertMemoryEventInternal({
2406
+ ...scope,
2407
+ entity_kind: 'association',
2408
+ entity_id: String(association.id),
2409
+ event_type: 'association.created',
2410
+ payload: {
2411
+ after: cloneValue(association),
2412
+ },
2413
+ created_at: association.created_at,
2414
+ });
2415
+ return association;
2416
+ },
2417
+ getAssociationById(id) {
2418
+ const row = db.prepare('SELECT * FROM associations WHERE id = ?').get(id);
2419
+ if (!row)
2420
+ return null;
2421
+ return {
2422
+ ...row,
2423
+ collaboration_id: row.collaboration_id ?? row.workspace_id,
2424
+ auto_generated: row.auto_generated === 1,
2425
+ };
2426
+ },
2427
+ getAssociationsFrom(kind, id, scope) {
2428
+ const rows = db
2429
+ .prepare(`SELECT * FROM associations WHERE source_kind = ? AND source_id = ? AND ${SCOPE_WHERE} ORDER BY id DESC`)
2430
+ .all(kind, id, ...scopeValues(scope));
2431
+ return rows.map((row) => ({
2432
+ ...row,
2433
+ collaboration_id: row.collaboration_id ?? row.workspace_id,
2434
+ auto_generated: row.auto_generated === 1,
2435
+ }));
2436
+ },
2437
+ getAssociationsTo(kind, id, scope) {
2438
+ const rows = db
2439
+ .prepare(`SELECT * FROM associations WHERE target_kind = ? AND target_id = ? AND ${SCOPE_WHERE} ORDER BY id DESC`)
2440
+ .all(kind, id, ...scopeValues(scope));
2441
+ return rows.map((row) => ({
2442
+ ...row,
2443
+ collaboration_id: row.collaboration_id ?? row.workspace_id,
2444
+ auto_generated: row.auto_generated === 1,
2445
+ }));
2446
+ },
2447
+ listAssociations(scope) {
2448
+ const rows = db
2449
+ .prepare(`SELECT * FROM associations WHERE ${SCOPE_WHERE} ORDER BY id DESC`)
2450
+ .all(...scopeValues(scope));
2451
+ return rows.map((row) => ({
2452
+ ...row,
2453
+ collaboration_id: row.collaboration_id ?? row.workspace_id,
2454
+ auto_generated: row.auto_generated === 1,
2455
+ }));
2456
+ },
2457
+ deleteAssociation(id) {
2458
+ const before = this.getAssociationById(id);
2459
+ db.prepare('DELETE FROM associations WHERE id = ?').run(id);
2460
+ if (before) {
2461
+ insertMemoryEventInternal({
2462
+ ...normalizeScope(before),
2463
+ entity_kind: 'association',
2464
+ entity_id: String(before.id),
2465
+ event_type: 'association.deleted',
2466
+ payload: {
2467
+ before: cloneValue(before),
2468
+ },
2469
+ created_at: nowSeconds(),
2470
+ });
2471
+ }
2472
+ },
2473
+ insertMemoryEvent(input) {
2474
+ return insertMemoryEventInternal(input);
2475
+ },
2476
+ listMemoryEvents(scope, query) {
2477
+ return listScopedMemoryEvents(scope, query);
2478
+ },
2479
+ listMemoryEventsCrossScope(scope, level, query) {
2480
+ return listScopedMemoryEventsCrossScope(scope, level, query);
2481
+ },
2482
+ getMemoryEventsByEntity(scope, entityKind, entityId, query) {
2483
+ return listScopedMemoryEvents(scope, {
2484
+ ...query,
2485
+ entityKind,
2486
+ entityId,
2487
+ });
2488
+ },
2489
+ getMemoryEventsBySession(scope, sessionId, query) {
2490
+ return listScopedMemoryEvents(scope, {
2491
+ ...query,
2492
+ sessionId,
2493
+ });
2494
+ },
2495
+ getSessionState: readSessionStateProjection,
2496
+ upsertSessionState(input) {
2497
+ const projection = writeSessionStateProjection(input);
2498
+ insertMemoryEventInternal({
2499
+ ...normalizeScope(projection),
2500
+ session_id: projection.session_id,
2501
+ entity_kind: 'session_state',
2502
+ entity_id: projection.session_id,
2503
+ event_type: 'session_state.updated',
2504
+ payload: {
2505
+ after: cloneValue(projection),
2506
+ },
2507
+ created_at: projection.updatedAt,
2508
+ });
2509
+ return projection;
2510
+ },
2511
+ getTemporalWatermark: readTemporalWatermark,
2512
+ upsertTemporalWatermark: writeTemporalWatermark,
831
2513
  transaction(fn) {
832
2514
  return db.transaction(fn)();
833
2515
  },