ai-memory-layer 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/CHANGELOG.md +19 -12
  2. package/README.md +435 -320
  3. package/bin/memory-server.mjs +0 -0
  4. package/dist/adapters/memory/embeddings.d.ts.map +1 -1
  5. package/dist/adapters/memory/embeddings.js +12 -1
  6. package/dist/adapters/memory/embeddings.js.map +1 -1
  7. package/dist/adapters/memory/index.d.ts.map +1 -1
  8. package/dist/adapters/memory/index.js +1281 -48
  9. package/dist/adapters/memory/index.js.map +1 -1
  10. package/dist/adapters/postgres/index.d.ts +1 -0
  11. package/dist/adapters/postgres/index.d.ts.map +1 -1
  12. package/dist/adapters/postgres/index.js +1770 -42
  13. package/dist/adapters/postgres/index.js.map +1 -1
  14. package/dist/adapters/sqlite/embeddings.d.ts.map +1 -1
  15. package/dist/adapters/sqlite/embeddings.js +49 -12
  16. package/dist/adapters/sqlite/embeddings.js.map +1 -1
  17. package/dist/adapters/sqlite/index.d.ts.map +1 -1
  18. package/dist/adapters/sqlite/index.js +1720 -38
  19. package/dist/adapters/sqlite/index.js.map +1 -1
  20. package/dist/adapters/sqlite/mappers.d.ts +39 -4
  21. package/dist/adapters/sqlite/mappers.d.ts.map +1 -1
  22. package/dist/adapters/sqlite/mappers.js +87 -0
  23. package/dist/adapters/sqlite/mappers.js.map +1 -1
  24. package/dist/adapters/sqlite/schema.d.ts +1 -1
  25. package/dist/adapters/sqlite/schema.d.ts.map +1 -1
  26. package/dist/adapters/sqlite/schema.js +297 -1
  27. package/dist/adapters/sqlite/schema.js.map +1 -1
  28. package/dist/adapters/sync-to-async.d.ts.map +1 -1
  29. package/dist/adapters/sync-to-async.js +54 -0
  30. package/dist/adapters/sync-to-async.js.map +1 -1
  31. package/dist/contracts/async-storage.d.ts +61 -1
  32. package/dist/contracts/async-storage.d.ts.map +1 -1
  33. package/dist/contracts/cognitive.d.ts +37 -0
  34. package/dist/contracts/cognitive.d.ts.map +1 -0
  35. package/dist/contracts/cognitive.js +24 -0
  36. package/dist/contracts/cognitive.js.map +1 -0
  37. package/dist/contracts/coordination.d.ts +101 -0
  38. package/dist/contracts/coordination.d.ts.map +1 -0
  39. package/dist/contracts/coordination.js +26 -0
  40. package/dist/contracts/coordination.js.map +1 -0
  41. package/dist/contracts/embedding.d.ts +1 -1
  42. package/dist/contracts/embedding.d.ts.map +1 -1
  43. package/dist/contracts/errors.d.ts +28 -0
  44. package/dist/contracts/errors.d.ts.map +1 -0
  45. package/dist/contracts/errors.js +41 -0
  46. package/dist/contracts/errors.js.map +1 -0
  47. package/dist/contracts/identity.d.ts +2 -0
  48. package/dist/contracts/identity.d.ts.map +1 -1
  49. package/dist/contracts/identity.js +26 -1
  50. package/dist/contracts/identity.js.map +1 -1
  51. package/dist/contracts/observability.d.ts +2 -1
  52. package/dist/contracts/observability.d.ts.map +1 -1
  53. package/dist/contracts/observability.js +11 -0
  54. package/dist/contracts/observability.js.map +1 -1
  55. package/dist/contracts/profile.d.ts +29 -0
  56. package/dist/contracts/profile.d.ts.map +1 -0
  57. package/dist/contracts/profile.js +2 -0
  58. package/dist/contracts/profile.js.map +1 -0
  59. package/dist/contracts/session-state.d.ts +10 -0
  60. package/dist/contracts/session-state.d.ts.map +1 -0
  61. package/dist/contracts/session-state.js +2 -0
  62. package/dist/contracts/session-state.js.map +1 -0
  63. package/dist/contracts/storage.d.ts +73 -1
  64. package/dist/contracts/storage.d.ts.map +1 -1
  65. package/dist/contracts/storage.js +16 -1
  66. package/dist/contracts/storage.js.map +1 -1
  67. package/dist/contracts/temporal.d.ts +112 -0
  68. package/dist/contracts/temporal.d.ts.map +1 -0
  69. package/dist/contracts/temporal.js +31 -0
  70. package/dist/contracts/temporal.js.map +1 -0
  71. package/dist/contracts/types.d.ts +135 -0
  72. package/dist/contracts/types.d.ts.map +1 -1
  73. package/dist/contracts/types.js +27 -0
  74. package/dist/contracts/types.js.map +1 -1
  75. package/dist/core/associations.d.ts +18 -0
  76. package/dist/core/associations.d.ts.map +1 -0
  77. package/dist/core/associations.js +185 -0
  78. package/dist/core/associations.js.map +1 -0
  79. package/dist/core/circuit-breaker.d.ts +9 -0
  80. package/dist/core/circuit-breaker.d.ts.map +1 -1
  81. package/dist/core/circuit-breaker.js +13 -1
  82. package/dist/core/circuit-breaker.js.map +1 -1
  83. package/dist/core/cognitive.d.ts +5 -0
  84. package/dist/core/cognitive.d.ts.map +1 -0
  85. package/dist/core/cognitive.js +120 -0
  86. package/dist/core/cognitive.js.map +1 -0
  87. package/dist/core/context.d.ts +72 -1
  88. package/dist/core/context.d.ts.map +1 -1
  89. package/dist/core/context.js +471 -45
  90. package/dist/core/context.js.map +1 -1
  91. package/dist/core/episodic.d.ts +28 -0
  92. package/dist/core/episodic.d.ts.map +1 -0
  93. package/dist/core/episodic.js +371 -0
  94. package/dist/core/episodic.js.map +1 -0
  95. package/dist/core/formatter.d.ts +4 -0
  96. package/dist/core/formatter.d.ts.map +1 -1
  97. package/dist/core/formatter.js +103 -0
  98. package/dist/core/formatter.js.map +1 -1
  99. package/dist/core/maintenance.d.ts +1 -0
  100. package/dist/core/maintenance.d.ts.map +1 -1
  101. package/dist/core/maintenance.js +75 -0
  102. package/dist/core/maintenance.js.map +1 -1
  103. package/dist/core/manager.d.ts +159 -7
  104. package/dist/core/manager.d.ts.map +1 -1
  105. package/dist/core/manager.js +740 -31
  106. package/dist/core/manager.js.map +1 -1
  107. package/dist/core/orchestrator.d.ts.map +1 -1
  108. package/dist/core/orchestrator.js +210 -178
  109. package/dist/core/orchestrator.js.map +1 -1
  110. package/dist/core/playbook.d.ts +35 -0
  111. package/dist/core/playbook.d.ts.map +1 -0
  112. package/dist/core/playbook.js +184 -0
  113. package/dist/core/playbook.js.map +1 -0
  114. package/dist/core/profile.d.ts +8 -0
  115. package/dist/core/profile.d.ts.map +1 -0
  116. package/dist/core/profile.js +103 -0
  117. package/dist/core/profile.js.map +1 -0
  118. package/dist/core/quick.d.ts +5 -0
  119. package/dist/core/quick.d.ts.map +1 -1
  120. package/dist/core/quick.js +10 -1
  121. package/dist/core/quick.js.map +1 -1
  122. package/dist/core/runtime.d.ts +17 -1
  123. package/dist/core/runtime.d.ts.map +1 -1
  124. package/dist/core/runtime.js +88 -5
  125. package/dist/core/runtime.js.map +1 -1
  126. package/dist/core/streaming.d.ts +1 -1
  127. package/dist/core/streaming.d.ts.map +1 -1
  128. package/dist/core/temporal.d.ts +29 -0
  129. package/dist/core/temporal.d.ts.map +1 -0
  130. package/dist/core/temporal.js +447 -0
  131. package/dist/core/temporal.js.map +1 -0
  132. package/dist/core/validation.d.ts +3 -0
  133. package/dist/core/validation.d.ts.map +1 -1
  134. package/dist/core/validation.js +25 -10
  135. package/dist/core/validation.js.map +1 -1
  136. package/dist/core/workspace-detect.d.ts +17 -0
  137. package/dist/core/workspace-detect.d.ts.map +1 -0
  138. package/dist/core/workspace-detect.js +55 -0
  139. package/dist/core/workspace-detect.js.map +1 -0
  140. package/dist/embeddings/resilience.d.ts.map +1 -1
  141. package/dist/embeddings/resilience.js +19 -8
  142. package/dist/embeddings/resilience.js.map +1 -1
  143. package/dist/index.d.ts +21 -4
  144. package/dist/index.d.ts.map +1 -1
  145. package/dist/index.js +9 -0
  146. package/dist/index.js.map +1 -1
  147. package/dist/integrations/claude-agent.d.ts +6 -0
  148. package/dist/integrations/claude-agent.d.ts.map +1 -1
  149. package/dist/integrations/claude-agent.js +5 -1
  150. package/dist/integrations/claude-agent.js.map +1 -1
  151. package/dist/integrations/claude-tools.d.ts +5 -4
  152. package/dist/integrations/claude-tools.d.ts.map +1 -1
  153. package/dist/integrations/claude-tools.js +155 -2
  154. package/dist/integrations/claude-tools.js.map +1 -1
  155. package/dist/integrations/middleware.d.ts +6 -0
  156. package/dist/integrations/middleware.d.ts.map +1 -1
  157. package/dist/integrations/middleware.js +11 -1
  158. package/dist/integrations/middleware.js.map +1 -1
  159. package/dist/integrations/openai-tools.d.ts +5 -4
  160. package/dist/integrations/openai-tools.d.ts.map +1 -1
  161. package/dist/integrations/openai-tools.js +170 -2
  162. package/dist/integrations/openai-tools.js.map +1 -1
  163. package/dist/integrations/vercel-ai.d.ts +6 -0
  164. package/dist/integrations/vercel-ai.d.ts.map +1 -1
  165. package/dist/integrations/vercel-ai.js +4 -0
  166. package/dist/integrations/vercel-ai.js.map +1 -1
  167. package/dist/server/http-server.d.ts +8 -0
  168. package/dist/server/http-server.d.ts.map +1 -1
  169. package/dist/server/http-server.js +976 -58
  170. package/dist/server/http-server.js.map +1 -1
  171. package/dist/server/mcp-server.d.ts +8 -0
  172. package/dist/server/mcp-server.d.ts.map +1 -1
  173. package/dist/server/mcp-server.js +1157 -37
  174. package/dist/server/mcp-server.js.map +1 -1
  175. package/dist/server/parsing.d.ts +12 -0
  176. package/dist/server/parsing.d.ts.map +1 -0
  177. package/dist/server/parsing.js +42 -0
  178. package/dist/server/parsing.js.map +1 -0
  179. package/dist/summarizers/prompts.d.ts +4 -0
  180. package/dist/summarizers/prompts.d.ts.map +1 -1
  181. package/dist/summarizers/prompts.js +42 -0
  182. package/dist/summarizers/prompts.js.map +1 -1
  183. package/docs/ULTIMATE_MEMORY_LAYER_ROADMAP.md +291 -0
  184. package/docs/prd.json +1498 -0
  185. package/openapi.yaml +1945 -112
  186. package/package.json +7 -5
@@ -1,4 +1,7 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { UniqueConstraintError } from '../../contracts/storage.js';
3
+ import { ConflictError } from '../../contracts/errors.js';
4
+ import { compareTemporalIds, normalizeTemporalId } from '../../contracts/temporal.js';
2
5
  import { normalizeScope } from '../../contracts/identity.js';
3
6
  import { matchesKnowledgeSearchOptions } from '../../core/retrieval.js';
4
7
  import { estimateTokens } from '../../core/tokens.js';
@@ -12,16 +15,13 @@ function scopeWhere(prefix = '') {
12
15
  }
13
16
  function wideScopeWhere(scope, level, prefix = '') {
14
17
  const p = prefix ? `${prefix}.` : '';
15
- const normalized = normalizeScope(scope);
16
18
  switch (level) {
17
19
  case 'tenant':
18
20
  return `${p}tenant_id = $1`;
19
21
  case 'system':
20
22
  return `${p}tenant_id = $1 AND ${p}system_id = $2`;
21
23
  case 'workspace':
22
- return normalized.collaboration_id.length > 0
23
- ? `${p}tenant_id = $1 AND ${p}collaboration_id = $2`
24
- : `${p}tenant_id = $1 AND ${p}system_id = $2 AND ${p}workspace_id = $3`;
24
+ return `${p}tenant_id = $1 AND ${p}workspace_id = $2`;
25
25
  default:
26
26
  return scopeWhere(prefix);
27
27
  }
@@ -34,9 +34,7 @@ function wideScopeParams(scope, level) {
34
34
  case 'system':
35
35
  return [n.tenant_id, n.system_id];
36
36
  case 'workspace':
37
- return n.collaboration_id.length > 0
38
- ? [n.tenant_id, n.collaboration_id]
39
- : [n.tenant_id, n.system_id, n.workspace_id];
37
+ return [n.tenant_id, n.workspace_id];
40
38
  default:
41
39
  return scopeParams(scope);
42
40
  }
@@ -118,6 +116,7 @@ function mapWorkingMemory(row) {
118
116
  compaction_trigger: row.compaction_trigger,
119
117
  expires_at: row.expires_at != null ? Number(row.expires_at) : null,
120
118
  promoted_to_knowledge_id: row.promoted_to_knowledge_id != null ? Number(row.promoted_to_knowledge_id) : null,
119
+ episode_recap: row.episode_recap != null ? (typeof row.episode_recap === 'string' ? JSON.parse(row.episode_recap) : row.episode_recap) : null,
121
120
  created_at: Number(row.created_at),
122
121
  schema_version: Number(row.schema_version ?? 1),
123
122
  };
@@ -130,6 +129,7 @@ function mapKnowledgeMemory(row) {
130
129
  workspace_id: String(row.workspace_id ?? ''),
131
130
  collaboration_id: String(row.collaboration_id ?? ''),
132
131
  scope_id: String(row.scope_id),
132
+ visibility_class: row.visibility_class ?? 'private',
133
133
  fact: String(row.fact),
134
134
  fact_type: row.fact_type,
135
135
  knowledge_state: row.knowledge_state ?? 'trusted',
@@ -182,15 +182,182 @@ function mapWorkItem(row) {
182
182
  collaboration_id: String(row.collaboration_id ?? ''),
183
183
  scope_id: String(row.scope_id),
184
184
  session_id: row.session_id != null ? String(row.session_id) : null,
185
+ visibility_class: row.visibility_class ?? 'private',
185
186
  title: String(row.title),
186
187
  kind: row.kind,
187
188
  status: row.status,
188
189
  detail: row.detail != null ? String(row.detail) : null,
189
190
  source_working_memory_id: row.source_working_memory_id != null ? Number(row.source_working_memory_id) : null,
191
+ version: Number(row.version ?? 1),
190
192
  created_at: Number(row.created_at),
191
193
  updated_at: Number(row.updated_at),
192
194
  };
193
195
  }
196
+ function parseJsonStringArray(value) {
197
+ if (Array.isArray(value))
198
+ return value.filter((v) => typeof v === 'string');
199
+ if (typeof value === 'string') {
200
+ try {
201
+ const parsed = JSON.parse(value);
202
+ return Array.isArray(parsed) ? parsed.filter((v) => typeof v === 'string') : [];
203
+ }
204
+ catch {
205
+ return [];
206
+ }
207
+ }
208
+ return [];
209
+ }
210
+ function parseJsonObject(value) {
211
+ if (value == null)
212
+ return null;
213
+ if (typeof value === 'object' && !Array.isArray(value)) {
214
+ return value;
215
+ }
216
+ if (typeof value === 'string') {
217
+ try {
218
+ const parsed = JSON.parse(value);
219
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
220
+ ? parsed
221
+ : null;
222
+ }
223
+ catch {
224
+ return null;
225
+ }
226
+ }
227
+ return null;
228
+ }
229
+ function serializeActorMetadata(actor) {
230
+ return [
231
+ actor.actor_kind,
232
+ actor.actor_id,
233
+ actor.system_id ?? null,
234
+ actor.display_name ?? null,
235
+ actor.metadata ? JSON.stringify(actor.metadata) : null,
236
+ ];
237
+ }
238
+ function parseActorRef(row, prefix) {
239
+ const base = `${prefix}_`;
240
+ return {
241
+ actor_kind: String(row[`${base}kind`]),
242
+ actor_id: String(row[`${base}id`]),
243
+ system_id: row[`${base}system_id`] != null ? String(row[`${base}system_id`]) : null,
244
+ display_name: row[`${base}display_name`] != null ? String(row[`${base}display_name`]) : null,
245
+ metadata: parseJsonObject(row[`${base}metadata`]) ?? null,
246
+ };
247
+ }
248
+ function sameActor(actor, other) {
249
+ return actor.actor_kind === other.actor_kind && actor.actor_id === other.actor_id;
250
+ }
251
+ function mapPlaybook(row) {
252
+ return {
253
+ id: Number(row.id),
254
+ tenant_id: String(row.tenant_id),
255
+ system_id: String(row.system_id),
256
+ workspace_id: String(row.workspace_id ?? ''),
257
+ collaboration_id: String(row.collaboration_id ?? ''),
258
+ scope_id: String(row.scope_id),
259
+ visibility_class: row.visibility_class ?? 'private',
260
+ title: String(row.title),
261
+ description: String(row.description),
262
+ instructions: String(row.instructions),
263
+ references: parseJsonStringArray(row.references_json),
264
+ templates: parseJsonStringArray(row.templates),
265
+ scripts: parseJsonStringArray(row.scripts),
266
+ assets: parseJsonStringArray(row.assets),
267
+ tags: parseJsonStringArray(row.tags),
268
+ status: row.status,
269
+ source_session_id: row.source_session_id != null ? String(row.source_session_id) : null,
270
+ source_working_memory_id: row.source_working_memory_id != null ? Number(row.source_working_memory_id) : null,
271
+ revision_count: Number(row.revision_count ?? 0),
272
+ last_used_at: row.last_used_at != null ? Number(row.last_used_at) : null,
273
+ use_count: Number(row.use_count ?? 0),
274
+ created_at: Number(row.created_at),
275
+ updated_at: Number(row.updated_at),
276
+ schema_version: Number(row.schema_version ?? 1),
277
+ };
278
+ }
279
+ function mapPlaybookRevision(row) {
280
+ return {
281
+ id: Number(row.id),
282
+ tenant_id: String(row.tenant_id),
283
+ system_id: String(row.system_id),
284
+ workspace_id: String(row.workspace_id ?? ''),
285
+ collaboration_id: String(row.collaboration_id ?? ''),
286
+ scope_id: String(row.scope_id),
287
+ playbook_id: Number(row.playbook_id),
288
+ instructions: String(row.instructions),
289
+ revision_reason: String(row.revision_reason),
290
+ source_session_id: row.source_session_id != null ? String(row.source_session_id) : null,
291
+ created_at: Number(row.created_at),
292
+ };
293
+ }
294
+ function mapAssociation(row) {
295
+ return {
296
+ id: Number(row.id),
297
+ tenant_id: String(row.tenant_id),
298
+ system_id: String(row.system_id),
299
+ workspace_id: String(row.workspace_id ?? ''),
300
+ collaboration_id: String(row.collaboration_id ?? ''),
301
+ scope_id: String(row.scope_id),
302
+ visibility_class: row.visibility_class ?? 'private',
303
+ source_kind: row.source_kind,
304
+ source_id: Number(row.source_id),
305
+ target_kind: row.target_kind,
306
+ target_id: Number(row.target_id),
307
+ association_type: row.association_type,
308
+ confidence: Number(row.confidence),
309
+ auto_generated: Boolean(row.auto_generated),
310
+ created_at: Number(row.created_at),
311
+ };
312
+ }
313
+ function mapWorkClaim(row) {
314
+ return {
315
+ id: Number(row.id),
316
+ tenant_id: String(row.tenant_id),
317
+ system_id: String(row.system_id),
318
+ workspace_id: String(row.workspace_id ?? ''),
319
+ collaboration_id: String(row.collaboration_id ?? ''),
320
+ scope_id: String(row.scope_id),
321
+ work_item_id: Number(row.work_item_id),
322
+ actor: parseActorRef(row, 'actor'),
323
+ session_id: row.session_id != null ? String(row.session_id) : null,
324
+ claim_token: String(row.claim_token),
325
+ status: row.status,
326
+ claimed_at: Number(row.claimed_at),
327
+ expires_at: Number(row.expires_at),
328
+ released_at: row.released_at != null ? Number(row.released_at) : null,
329
+ release_reason: row.release_reason != null ? String(row.release_reason) : null,
330
+ source_event_id: row.source_event_id != null ? String(row.source_event_id) : null,
331
+ visibility_class: row.visibility_class,
332
+ version: Number(row.version ?? 1),
333
+ };
334
+ }
335
+ function mapHandoff(row) {
336
+ return {
337
+ id: Number(row.id),
338
+ tenant_id: String(row.tenant_id),
339
+ system_id: String(row.system_id),
340
+ workspace_id: String(row.workspace_id ?? ''),
341
+ collaboration_id: String(row.collaboration_id ?? ''),
342
+ scope_id: String(row.scope_id),
343
+ work_item_id: Number(row.work_item_id),
344
+ from_actor: parseActorRef(row, 'from_actor'),
345
+ to_actor: parseActorRef(row, 'to_actor'),
346
+ session_id: row.session_id != null ? String(row.session_id) : null,
347
+ summary: String(row.summary),
348
+ context_bundle_ref: row.context_bundle_ref != null ? String(row.context_bundle_ref) : null,
349
+ status: row.status,
350
+ created_at: Number(row.created_at),
351
+ accepted_at: row.accepted_at != null ? Number(row.accepted_at) : null,
352
+ rejected_at: row.rejected_at != null ? Number(row.rejected_at) : null,
353
+ canceled_at: row.canceled_at != null ? Number(row.canceled_at) : null,
354
+ expires_at: row.expires_at != null ? Number(row.expires_at) : null,
355
+ decision_reason: row.decision_reason != null ? String(row.decision_reason) : null,
356
+ source_event_id: row.source_event_id != null ? String(row.source_event_id) : null,
357
+ visibility_class: row.visibility_class,
358
+ version: Number(row.version ?? 1),
359
+ };
360
+ }
194
361
  function mapContextMonitor(row) {
195
362
  return {
196
363
  id: Number(row.id),
@@ -307,6 +474,56 @@ function mapKnowledgeEvidenceRow(row) {
307
474
  created_at: Number(row.created_at),
308
475
  };
309
476
  }
477
+ function mapMemoryEventRecord(row) {
478
+ return {
479
+ event_id: String(row.event_id),
480
+ tenant_id: String(row.tenant_id),
481
+ system_id: String(row.system_id),
482
+ workspace_id: String(row.workspace_id ?? ''),
483
+ collaboration_id: String(row.collaboration_id ?? ''),
484
+ scope_id: String(row.scope_id),
485
+ session_id: row.session_id != null ? String(row.session_id) : null,
486
+ actor_id: row.actor_id != null ? String(row.actor_id) : null,
487
+ actor_kind: row.actor_kind != null ? String(row.actor_kind) : null,
488
+ actor_system_id: row.actor_system_id != null ? String(row.actor_system_id) : null,
489
+ actor_display_name: row.actor_display_name != null ? String(row.actor_display_name) : null,
490
+ actor_metadata: parseJsonObject(row.actor_metadata) ?? null,
491
+ entity_kind: row.entity_kind,
492
+ entity_id: String(row.entity_id),
493
+ event_type: row.event_type,
494
+ payload: parseJsonObject(row.payload) ?? {},
495
+ causation_id: row.causation_id != null ? String(row.causation_id) : null,
496
+ correlation_id: row.correlation_id != null ? String(row.correlation_id) : null,
497
+ created_at: Number(row.created_at),
498
+ };
499
+ }
500
+ function mapSessionStateProjection(row) {
501
+ return {
502
+ tenant_id: String(row.tenant_id),
503
+ system_id: String(row.system_id),
504
+ workspace_id: String(row.workspace_id ?? ''),
505
+ collaboration_id: String(row.collaboration_id ?? ''),
506
+ scope_id: String(row.scope_id),
507
+ session_id: String(row.session_id),
508
+ currentObjective: row.current_objective != null ? String(row.current_objective) : null,
509
+ blockers: parseJsonStringArray(row.blockers),
510
+ assumptions: parseJsonStringArray(row.assumptions),
511
+ pendingDecisions: parseJsonStringArray(row.pending_decisions),
512
+ activeTools: parseJsonStringArray(row.active_tools),
513
+ recentOutputs: parseJsonStringArray(row.recent_outputs),
514
+ updatedAt: Number(row.updated_at),
515
+ source_event_id: row.source_event_id != null ? String(row.source_event_id) : null,
516
+ };
517
+ }
518
+ function mapTemporalProjectionWatermark(row) {
519
+ return {
520
+ projection_name: String(row.projection_name),
521
+ last_event_id: String(row.last_event_id),
522
+ updated_at: Number(row.updated_at),
523
+ cutover_at: row.cutover_at != null ? Number(row.cutover_at) : null,
524
+ metadata: parseJsonObject(row.metadata),
525
+ };
526
+ }
310
527
  /**
311
528
  * Creates a PostgreSQL-backed AsyncStorageAdapter.
312
529
  *
@@ -323,19 +540,373 @@ function mapKnowledgeEvidenceRow(row) {
323
540
  export function createPostgresAdapter(pool, options) {
324
541
  const now = nowSeconds;
325
542
  const txStorage = new AsyncLocalStorage();
326
- const rootQuery = pool.query.bind(pool);
327
- pool.query = ((text, values) => {
543
+ const rootPool = pool;
544
+ const rootQuery = rootPool.query.bind(rootPool);
545
+ const scopedQuery = ((text, values) => {
328
546
  const context = txStorage.getStore();
329
547
  return context ? context.client.query(text, values) : rootQuery(text, values);
330
548
  });
549
+ pool = new Proxy(rootPool, {
550
+ get(target, prop, receiver) {
551
+ if (prop === 'query')
552
+ return scopedQuery;
553
+ return Reflect.get(target, prop, receiver);
554
+ },
555
+ });
556
+ let temporalInitPromise = null;
557
+ function resolveEventQuery(query) {
558
+ return {
559
+ sessionId: query?.sessionId ?? '',
560
+ entityKind: query?.entityKind ?? null,
561
+ entityId: query?.entityId ?? '',
562
+ startAt: query?.startAt ?? Number.NEGATIVE_INFINITY,
563
+ endAt: query?.endAt ?? Number.POSITIVE_INFINITY,
564
+ limit: query?.limit ?? 100,
565
+ cursor: query?.cursor != null ? normalizeTemporalId(query.cursor) : null,
566
+ };
567
+ }
568
+ async function getExistingIds(table, ids) {
569
+ const uniqueIds = [...new Set(ids)];
570
+ if (uniqueIds.length === 0) {
571
+ return [];
572
+ }
573
+ const { rows } = await pool.query(`SELECT id FROM ${table} WHERE id = ANY($1::int[])`, [
574
+ uniqueIds,
575
+ ]);
576
+ const existing = new Set(rows.map((row) => Number(row.id)));
577
+ return uniqueIds.filter((id) => existing.has(id));
578
+ }
579
+ async function ensureTemporalCutover() {
580
+ if (!temporalInitPromise) {
581
+ temporalInitPromise = (async () => {
582
+ const current = await readTemporalWatermark('temporal');
583
+ if (current?.cutover_at != null)
584
+ return;
585
+ const cutoverAt = now();
586
+ await writeTemporalWatermark({
587
+ projection_name: 'temporal',
588
+ last_event_id: current?.last_event_id ?? '0',
589
+ updated_at: cutoverAt,
590
+ cutover_at: cutoverAt,
591
+ metadata: current?.metadata ?? null,
592
+ });
593
+ })();
594
+ }
595
+ return temporalInitPromise;
596
+ }
597
+ async function readTemporalWatermark(projectionName = 'temporal') {
598
+ const { rows } = await pool.query('SELECT * FROM projection_watermarks WHERE projection_name = $1', [projectionName]);
599
+ return rows[0] ? mapTemporalProjectionWatermark(rows[0]) : null;
600
+ }
601
+ async function writeTemporalWatermark(input) {
602
+ const lastEventId = normalizeTemporalId(input.last_event_id);
603
+ const updatedAt = input.updated_at ?? now();
604
+ const { rows } = await pool.query(`INSERT INTO projection_watermarks
605
+ (projection_name, last_event_id, updated_at, cutover_at, metadata)
606
+ VALUES ($1, $2, $3, $4, $5::jsonb)
607
+ ON CONFLICT (projection_name) DO UPDATE SET
608
+ last_event_id = EXCLUDED.last_event_id,
609
+ updated_at = EXCLUDED.updated_at,
610
+ cutover_at = EXCLUDED.cutover_at,
611
+ metadata = EXCLUDED.metadata
612
+ RETURNING *`, [
613
+ input.projection_name,
614
+ lastEventId,
615
+ updatedAt,
616
+ input.cutover_at ?? null,
617
+ input.metadata ? JSON.stringify(input.metadata) : null,
618
+ ]);
619
+ if (rows[0]) {
620
+ return mapTemporalProjectionWatermark(rows[0]);
621
+ }
622
+ return {
623
+ projection_name: input.projection_name,
624
+ last_event_id: lastEventId,
625
+ updated_at: updatedAt,
626
+ cutover_at: input.cutover_at ?? null,
627
+ metadata: input.metadata ?? null,
628
+ };
629
+ }
630
+ async function insertMemoryEventInternal(input) {
631
+ await ensureTemporalCutover();
632
+ const normalized = normalizeScope(input);
633
+ const createdAt = input.created_at ?? now();
634
+ const previousWatermark = await readTemporalWatermark('temporal');
635
+ const { rows } = await pool.query(`INSERT INTO memory_event_log
636
+ (tenant_id, system_id, workspace_id, collaboration_id, scope_id, session_id, actor_id,
637
+ actor_kind, actor_system_id, actor_display_name, actor_metadata,
638
+ entity_kind, entity_id, event_type, payload, causation_id, correlation_id, created_at)
639
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, $12, $13, $14, $15::jsonb, $16, $17, $18)
640
+ RETURNING *`, [
641
+ normalized.tenant_id,
642
+ normalized.system_id,
643
+ normalized.workspace_id,
644
+ normalized.collaboration_id,
645
+ normalized.scope_id,
646
+ input.session_id ?? null,
647
+ input.actor_id ?? null,
648
+ input.actor_kind ?? null,
649
+ input.actor_system_id ?? null,
650
+ input.actor_display_name ?? null,
651
+ input.actor_metadata ? JSON.stringify(input.actor_metadata) : null,
652
+ input.entity_kind,
653
+ input.entity_id,
654
+ input.event_type,
655
+ JSON.stringify(input.payload ?? {}),
656
+ input.causation_id ?? null,
657
+ input.correlation_id ?? null,
658
+ createdAt,
659
+ ]);
660
+ const record = rows[0]
661
+ ? mapMemoryEventRecord(rows[0])
662
+ : {
663
+ event_id: normalizeTemporalId(BigInt(previousWatermark?.last_event_id ?? '0') + 1n),
664
+ tenant_id: normalized.tenant_id,
665
+ system_id: normalized.system_id,
666
+ workspace_id: normalized.workspace_id,
667
+ collaboration_id: normalized.collaboration_id,
668
+ scope_id: normalized.scope_id,
669
+ session_id: input.session_id ?? null,
670
+ actor_id: input.actor_id ?? null,
671
+ actor_kind: input.actor_kind ?? null,
672
+ actor_system_id: input.actor_system_id ?? null,
673
+ actor_display_name: input.actor_display_name ?? null,
674
+ actor_metadata: input.actor_metadata ?? null,
675
+ entity_kind: input.entity_kind,
676
+ entity_id: input.entity_id,
677
+ event_type: input.event_type,
678
+ payload: input.payload ?? {},
679
+ causation_id: input.causation_id ?? null,
680
+ correlation_id: input.correlation_id ?? null,
681
+ created_at: createdAt,
682
+ };
683
+ await writeTemporalWatermark({
684
+ projection_name: 'temporal',
685
+ last_event_id: record.event_id,
686
+ updated_at: createdAt,
687
+ cutover_at: previousWatermark?.cutover_at ?? createdAt,
688
+ metadata: previousWatermark?.metadata ?? null,
689
+ });
690
+ return record;
691
+ }
692
+ async function insertMemoryEventsBatchInternal(inputs) {
693
+ if (inputs.length === 0)
694
+ return [];
695
+ await ensureTemporalCutover();
696
+ const previousWatermark = await readTemporalWatermark('temporal');
697
+ const values = [];
698
+ const params = [];
699
+ let nextParam = 1;
700
+ for (const input of inputs) {
701
+ const normalized = normalizeScope(input);
702
+ const createdAt = input.created_at ?? now();
703
+ values.push(`($${nextParam}, $${nextParam + 1}, $${nextParam + 2}, $${nextParam + 3}, $${nextParam + 4}, $${nextParam + 5}, $${nextParam + 6}, $${nextParam + 7}, $${nextParam + 8}, $${nextParam + 9}, $${nextParam + 10}::jsonb, $${nextParam + 11}, $${nextParam + 12}, $${nextParam + 13}, $${nextParam + 14}::jsonb, $${nextParam + 15}, $${nextParam + 16}, $${nextParam + 17})`);
704
+ 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);
705
+ nextParam += 18;
706
+ }
707
+ const { rows } = await pool.query(`INSERT INTO memory_event_log
708
+ (tenant_id, system_id, workspace_id, collaboration_id, scope_id, session_id, actor_id,
709
+ actor_kind, actor_system_id, actor_display_name, actor_metadata,
710
+ entity_kind, entity_id, event_type, payload, causation_id, correlation_id, created_at)
711
+ VALUES ${values.join(', ')}
712
+ RETURNING *`, params);
713
+ const records = rows
714
+ .map(mapMemoryEventRecord)
715
+ .sort((a, b) => a.created_at - b.created_at || compareTemporalIds(a.event_id, b.event_id));
716
+ const lastRecord = records[records.length - 1];
717
+ if (lastRecord) {
718
+ await writeTemporalWatermark({
719
+ projection_name: 'temporal',
720
+ last_event_id: lastRecord.event_id,
721
+ updated_at: lastRecord.created_at,
722
+ cutover_at: previousWatermark?.cutover_at ?? lastRecord.created_at,
723
+ metadata: previousWatermark?.metadata ?? null,
724
+ });
725
+ }
726
+ return records;
727
+ }
728
+ async function listScopedMemoryEvents(scope, query) {
729
+ await ensureTemporalCutover();
730
+ const resolved = resolveEventQuery(query);
731
+ const params = [...scopeParams(scope), resolved.startAt, resolved.endAt];
732
+ const clauses = [`${scopeWhere()}`, `created_at >= $6`, `created_at <= $7`];
733
+ let nextParam = 8;
734
+ if (resolved.cursor != null && compareTemporalIds(resolved.cursor, '0') > 0) {
735
+ clauses.push(`event_id > $${nextParam}::bigint`);
736
+ params.push(resolved.cursor);
737
+ nextParam += 1;
738
+ }
739
+ if (resolved.sessionId) {
740
+ clauses.push(`session_id = $${nextParam}`);
741
+ params.push(resolved.sessionId);
742
+ nextParam += 1;
743
+ }
744
+ if (resolved.entityKind) {
745
+ clauses.push(`entity_kind = $${nextParam}`);
746
+ params.push(resolved.entityKind);
747
+ nextParam += 1;
748
+ }
749
+ if (resolved.entityId) {
750
+ clauses.push(`entity_id = $${nextParam}`);
751
+ params.push(resolved.entityId);
752
+ nextParam += 1;
753
+ }
754
+ params.push(resolved.limit + 1);
755
+ const { rows } = await pool.query(`SELECT * FROM memory_event_log
756
+ WHERE ${clauses.join(' AND ')}
757
+ ORDER BY created_at ASC, event_id ASC
758
+ LIMIT $${nextParam}`, params);
759
+ const items = rows.slice(0, resolved.limit).map(mapMemoryEventRecord);
760
+ return {
761
+ events: items,
762
+ nextCursor: rows.length > resolved.limit ? items[items.length - 1]?.event_id ?? null : null,
763
+ };
764
+ }
765
+ async function listScopedMemoryEventsCrossScope(scope, level, query) {
766
+ await ensureTemporalCutover();
767
+ const resolved = resolveEventQuery(query);
768
+ const params = [...wideScopeParams(scope, level), resolved.startAt, resolved.endAt];
769
+ const clauses = [`${wideScopeWhere(scope, level)}`, `created_at >= $${params.length - 1}`, `created_at <= $${params.length}`];
770
+ let nextParam = params.length + 1;
771
+ if (resolved.cursor != null && compareTemporalIds(resolved.cursor, '0') > 0) {
772
+ clauses.push(`event_id > $${nextParam}::bigint`);
773
+ params.push(resolved.cursor);
774
+ nextParam += 1;
775
+ }
776
+ if (resolved.sessionId) {
777
+ clauses.push(`session_id = $${nextParam}`);
778
+ params.push(resolved.sessionId);
779
+ nextParam += 1;
780
+ }
781
+ if (resolved.entityKind) {
782
+ clauses.push(`entity_kind = $${nextParam}`);
783
+ params.push(resolved.entityKind);
784
+ nextParam += 1;
785
+ }
786
+ if (resolved.entityId) {
787
+ clauses.push(`entity_id = $${nextParam}`);
788
+ params.push(resolved.entityId);
789
+ nextParam += 1;
790
+ }
791
+ params.push(resolved.limit + 1);
792
+ const { rows } = await pool.query(`SELECT * FROM memory_event_log
793
+ WHERE ${clauses.join(' AND ')}
794
+ ORDER BY created_at ASC, event_id ASC
795
+ LIMIT $${nextParam}`, params);
796
+ const items = rows.slice(0, resolved.limit).map(mapMemoryEventRecord);
797
+ return {
798
+ events: items,
799
+ nextCursor: rows.length > resolved.limit ? items[items.length - 1]?.event_id ?? null : null,
800
+ };
801
+ }
802
+ async function readSessionStateProjection(scope, sessionId) {
803
+ const { rows } = await pool.query(`SELECT * FROM session_state_current WHERE ${scopeWhere()} AND session_id = $6`, [...scopeParams(scope), sessionId]);
804
+ return rows[0] ? mapSessionStateProjection(rows[0]) : null;
805
+ }
806
+ async function writeSessionStateProjection(input) {
807
+ const normalized = normalizeScope(input);
808
+ const { rows } = await pool.query(`INSERT INTO session_state_current
809
+ (tenant_id, system_id, workspace_id, collaboration_id, scope_id, session_id,
810
+ current_objective, blockers, assumptions, pending_decisions, active_tools, recent_outputs,
811
+ updated_at, source_event_id)
812
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9::jsonb, $10::jsonb, $11::jsonb, $12::jsonb, $13, $14)
813
+ ON CONFLICT (tenant_id, system_id, workspace_id, collaboration_id, scope_id, session_id) DO UPDATE SET
814
+ current_objective = EXCLUDED.current_objective,
815
+ blockers = EXCLUDED.blockers,
816
+ assumptions = EXCLUDED.assumptions,
817
+ pending_decisions = EXCLUDED.pending_decisions,
818
+ active_tools = EXCLUDED.active_tools,
819
+ recent_outputs = EXCLUDED.recent_outputs,
820
+ updated_at = EXCLUDED.updated_at,
821
+ source_event_id = EXCLUDED.source_event_id
822
+ RETURNING *`, [
823
+ normalized.tenant_id,
824
+ normalized.system_id,
825
+ normalized.workspace_id,
826
+ normalized.collaboration_id,
827
+ normalized.scope_id,
828
+ input.session_id,
829
+ input.currentObjective,
830
+ JSON.stringify(input.blockers),
831
+ JSON.stringify(input.assumptions),
832
+ JSON.stringify(input.pendingDecisions),
833
+ JSON.stringify(input.activeTools),
834
+ JSON.stringify(input.recentOutputs),
835
+ input.updatedAt,
836
+ input.source_event_id != null ? normalizeTemporalId(input.source_event_id) : null,
837
+ ]);
838
+ return mapSessionStateProjection(rows[0]);
839
+ }
840
+ async function expireClaimRecord(row, expiredAt = now()) {
841
+ const { rows } = await pool.query(`UPDATE work_claims_current
842
+ SET status = 'expired', released_at = $2, release_reason = 'expired', version = COALESCE(version, 1) + 1
843
+ WHERE id = $1
844
+ RETURNING *`, [Number(row.id), expiredAt]);
845
+ const expired = mapWorkClaim(rows[0] ?? row);
846
+ await insertMemoryEventInternal({
847
+ ...normalizeScope(expired),
848
+ session_id: expired.session_id,
849
+ actor_id: expired.actor.actor_id,
850
+ actor_kind: expired.actor.actor_kind,
851
+ actor_system_id: expired.actor.system_id,
852
+ actor_display_name: expired.actor.display_name,
853
+ actor_metadata: expired.actor.metadata,
854
+ entity_kind: 'work_claim',
855
+ entity_id: String(expired.id),
856
+ event_type: 'work_claim.expired',
857
+ payload: { after: expired },
858
+ created_at: expiredAt,
859
+ });
860
+ return expired;
861
+ }
862
+ async function expireHandoffRecord(row, expiredAt = now()) {
863
+ const { rows } = await pool.query(`UPDATE handoff_records
864
+ SET status = 'expired', decision_reason = 'expired', version = COALESCE(version, 1) + 1
865
+ WHERE id = $1
866
+ RETURNING *`, [Number(row.id)]);
867
+ const expired = mapHandoff(rows[0] ?? row);
868
+ await insertMemoryEventInternal({
869
+ ...normalizeScope(expired),
870
+ session_id: expired.session_id,
871
+ actor_id: expired.to_actor.actor_id,
872
+ actor_kind: expired.to_actor.actor_kind,
873
+ actor_system_id: expired.to_actor.system_id,
874
+ actor_display_name: expired.to_actor.display_name,
875
+ actor_metadata: expired.to_actor.metadata,
876
+ entity_kind: 'handoff',
877
+ entity_id: String(expired.id),
878
+ event_type: 'handoff.expired',
879
+ payload: { after: expired },
880
+ created_at: expiredAt,
881
+ });
882
+ return expired;
883
+ }
884
+ async function getAnyClaimRowByWorkItem(workItemId) {
885
+ const { rows } = await pool.query('SELECT * FROM work_claims_current WHERE work_item_id = $1 ORDER BY id DESC LIMIT 1', [workItemId]);
886
+ return rows[0] ?? null;
887
+ }
331
888
  return {
332
889
  async insertTurn(input) {
333
890
  const n = normalizeScope(input);
891
+ const createdAt = now();
334
892
  const tokenEst = input.token_estimate ?? estimateTokens(input.content);
335
893
  const { rows } = await pool.query(`INSERT INTO turns (tenant_id, system_id, workspace_id, collaboration_id, scope_id, session_id, actor, role, content, priority, token_estimate, created_at)
336
894
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
337
- RETURNING *`, [n.tenant_id, n.system_id, n.workspace_id, n.collaboration_id, n.scope_id, input.session_id, input.actor, input.role, input.content, input.priority ?? (input.role === 'system' ? 1.5 : 1), tokenEst, now()]);
338
- return mapTurn(rows[0]);
895
+ RETURNING *`, [n.tenant_id, n.system_id, n.workspace_id, n.collaboration_id, n.scope_id, input.session_id, input.actor, input.role, input.content, input.priority ?? (input.role === 'system' ? 1.5 : 1), tokenEst, createdAt]);
896
+ const turn = mapTurn(rows[0]);
897
+ await insertMemoryEventInternal({
898
+ ...n,
899
+ session_id: turn.session_id,
900
+ actor_id: turn.actor,
901
+ entity_kind: 'turn',
902
+ entity_id: String(turn.id),
903
+ event_type: 'turn.created',
904
+ payload: {
905
+ after: turn,
906
+ },
907
+ created_at: createdAt,
908
+ });
909
+ return turn;
339
910
  },
340
911
  async insertTurns(inputs) {
341
912
  return this.transaction(async () => {
@@ -394,23 +965,45 @@ export function createPostgresAdapter(pool, options) {
394
965
  return rows.map(mapTurn);
395
966
  },
396
967
  async searchTurns(scope, queryText, searchOptions) {
968
+ // scopeParams occupies $1..$5. queryText binds at $6 and limit at $7.
397
969
  const params = scopeParams(scope);
398
970
  const limit = searchOptions?.limit ?? 10;
399
971
  params.push(queryText, limit);
400
972
  const activeClause = searchOptions?.activeOnly ? ` AND status = 'active'` : '';
401
- const { rows } = await pool.query(`SELECT *, ts_rank(search_vector, plainto_tsquery('english', $5)) AS rank
973
+ const { rows } = await pool.query(`SELECT *, ts_rank(search_vector, plainto_tsquery('english', $6)) AS rank
402
974
  FROM turns
403
975
  WHERE ${scopeWhere()} ${activeClause}
404
- AND search_vector @@ plainto_tsquery('english', $5)
976
+ AND search_vector @@ plainto_tsquery('english', $6)
405
977
  ORDER BY rank DESC
406
- LIMIT $6`, params);
978
+ LIMIT $7`, params);
407
979
  return rows.map((row) => ({
408
980
  item: mapTurn(row),
409
981
  rank: Number(row.rank),
410
982
  }));
411
983
  },
412
984
  async archiveTurn(id, archivedAt, compactionLogId) {
985
+ const before = await this.getTurnById(id);
413
986
  await pool.query(`UPDATE turns SET status = 'archived', archived_at = $2, compaction_log_id = $3 WHERE id = $1`, [id, archivedAt, compactionLogId]);
987
+ const after = await this.getTurnById(id);
988
+ if (before && after) {
989
+ await insertMemoryEventInternal({
990
+ ...normalizeScope(after),
991
+ session_id: after.session_id,
992
+ actor_id: after.actor,
993
+ entity_kind: 'turn',
994
+ entity_id: String(after.id),
995
+ event_type: 'turn.archived',
996
+ payload: {
997
+ before,
998
+ after,
999
+ patch: {
1000
+ archived_at: archivedAt,
1001
+ compaction_log_id: compactionLogId,
1002
+ },
1003
+ },
1004
+ created_at: archivedAt,
1005
+ });
1006
+ }
414
1007
  },
415
1008
  async getArchivedTurnRange(sessionId, startId, endId, scope) {
416
1009
  const n = normalizeScope(scope);
@@ -432,17 +1025,34 @@ export function createPostgresAdapter(pool, options) {
432
1025
  },
433
1026
  async insertWorkingMemory(input) {
434
1027
  const n = normalizeScope(input);
435
- const { rows } = await pool.query(`INSERT INTO working_memory (tenant_id, system_id, workspace_id, collaboration_id, scope_id, session_id, summary, key_entities, topic_tags, turn_id_start, turn_id_end, turn_count, compaction_trigger, created_at)
436
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
1028
+ const createdAt = now();
1029
+ const { rows } = await pool.query(`INSERT INTO working_memory (tenant_id, system_id, workspace_id, collaboration_id, scope_id, session_id, summary, key_entities, topic_tags, turn_id_start, turn_id_end, turn_count, compaction_trigger, created_at, episode_recap)
1030
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
437
1031
  RETURNING *`, [n.tenant_id, n.system_id, n.workspace_id, n.collaboration_id, n.scope_id, input.session_id, input.summary,
438
1032
  JSON.stringify(input.key_entities), JSON.stringify(input.topic_tags),
439
- input.turn_id_start, input.turn_id_end, input.turn_count, input.compaction_trigger, now()]);
440
- return mapWorkingMemory(rows[0]);
1033
+ input.turn_id_start, input.turn_id_end, input.turn_count, input.compaction_trigger, createdAt,
1034
+ input.episode_recap ? JSON.stringify(input.episode_recap) : null]);
1035
+ const workingMemory = mapWorkingMemory(rows[0]);
1036
+ await insertMemoryEventInternal({
1037
+ ...n,
1038
+ session_id: workingMemory.session_id,
1039
+ entity_kind: 'working_memory',
1040
+ entity_id: String(workingMemory.id),
1041
+ event_type: 'working_memory.created',
1042
+ payload: {
1043
+ after: workingMemory,
1044
+ },
1045
+ created_at: createdAt,
1046
+ });
1047
+ return workingMemory;
441
1048
  },
442
1049
  async getWorkingMemoryById(id) {
443
1050
  const { rows } = await pool.query('SELECT * FROM working_memory WHERE id = $1', [id]);
444
1051
  return rows[0] ? mapWorkingMemory(rows[0]) : null;
445
1052
  },
1053
+ async getExistingWorkingMemoryIds(ids) {
1054
+ return getExistingIds('working_memory', ids);
1055
+ },
446
1056
  async getWorkingMemoryBySession(sessionId, scope) {
447
1057
  const params = [sessionId, ...scopeParams(scope)];
448
1058
  const { rows } = await pool.query(`SELECT * FROM working_memory WHERE session_id = $1 AND tenant_id = $2 AND system_id = $3 AND workspace_id = $4 AND collaboration_id = $5 AND scope_id = $6 ORDER BY id DESC`, params);
@@ -474,15 +1084,55 @@ export function createPostgresAdapter(pool, options) {
474
1084
  return rows.map(mapWorkingMemory);
475
1085
  },
476
1086
  async expireWorkingMemory(id) {
477
- await pool.query(`UPDATE working_memory SET status = 'expired', expires_at = $2 WHERE id = $1`, [id, now()]);
1087
+ const before = await this.getWorkingMemoryById(id);
1088
+ const expiredAt = now();
1089
+ await pool.query(`UPDATE working_memory SET status = 'expired', expires_at = $2 WHERE id = $1`, [id, expiredAt]);
1090
+ const after = await this.getWorkingMemoryById(id);
1091
+ if (before && after) {
1092
+ await insertMemoryEventInternal({
1093
+ ...normalizeScope(after),
1094
+ session_id: after.session_id,
1095
+ entity_kind: 'working_memory',
1096
+ entity_id: String(after.id),
1097
+ event_type: 'working_memory.expired',
1098
+ payload: {
1099
+ before,
1100
+ after,
1101
+ patch: {
1102
+ expires_at: expiredAt,
1103
+ },
1104
+ },
1105
+ created_at: expiredAt,
1106
+ });
1107
+ }
478
1108
  },
479
1109
  async markWorkingMemoryPromoted(id, knowledgeMemoryId) {
1110
+ const before = await this.getWorkingMemoryById(id);
480
1111
  await pool.query(`UPDATE working_memory SET promoted_to_knowledge_id = $2 WHERE id = $1`, [id, knowledgeMemoryId]);
1112
+ const after = await this.getWorkingMemoryById(id);
1113
+ if (before && after) {
1114
+ await insertMemoryEventInternal({
1115
+ ...normalizeScope(after),
1116
+ session_id: after.session_id,
1117
+ entity_kind: 'working_memory',
1118
+ entity_id: String(after.id),
1119
+ event_type: 'working_memory.promoted',
1120
+ payload: {
1121
+ before,
1122
+ after,
1123
+ refs: {
1124
+ knowledge_memory_id: knowledgeMemoryId,
1125
+ },
1126
+ },
1127
+ created_at: now(),
1128
+ });
1129
+ }
481
1130
  },
482
1131
  async insertKnowledgeMemory(input) {
483
1132
  const n = normalizeScope(input);
484
- const { rows } = await pool.query(`INSERT INTO knowledge_memory (tenant_id, system_id, workspace_id, collaboration_id, scope_id, fact, fact_type, knowledge_state, knowledge_class, fact_subject, fact_attribute, fact_value, normalized_fact, slot_key, is_negated, source, confidence, confidence_score, grounding_strength, evidence_count, trust_score, verification_status, verification_notes, last_verified_at, next_reverification_at, last_confirmed_at, confirmation_count, source_working_memory_id, source_turn_ids, successful_use_count, failed_use_count, disputed_at, dispute_reason, contradiction_score, superseded_at, created_at, last_accessed_at)
485
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $37)
1133
+ const createdAt = now();
1134
+ const { rows } = await pool.query(`INSERT INTO knowledge_memory (tenant_id, system_id, workspace_id, collaboration_id, scope_id, fact, fact_type, knowledge_state, knowledge_class, fact_subject, fact_attribute, fact_value, normalized_fact, slot_key, is_negated, source, confidence, confidence_score, grounding_strength, evidence_count, trust_score, verification_status, verification_notes, last_verified_at, next_reverification_at, last_confirmed_at, confirmation_count, source_system_id, source_scope_id, source_collaboration_id, source_working_memory_id, source_turn_ids, successful_use_count, failed_use_count, disputed_at, dispute_reason, contradiction_score, superseded_at, retired_at, created_at, last_accessed_at)
1135
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $40)
486
1136
  RETURNING *`, [n.tenant_id, n.system_id, n.workspace_id, n.collaboration_id, n.scope_id, input.fact, input.fact_type,
487
1137
  input.knowledge_state ?? 'trusted', input.knowledge_class ?? 'project_fact',
488
1138
  input.fact_subject ?? null, input.fact_attribute ?? null, input.fact_value ?? null,
@@ -494,11 +1144,24 @@ export function createPostgresAdapter(pool, options) {
494
1144
  input.verification_status ?? 'unverified', input.verification_notes ?? null,
495
1145
  input.last_verified_at ?? null, input.next_reverification_at ?? null,
496
1146
  input.last_confirmed_at ?? null, input.confirmation_count ?? 0,
497
- input.source_working_memory_id ?? null, input.source_turn_ids ?? [],
1147
+ input.source_system_id ?? n.system_id, input.source_scope_id ?? n.scope_id,
1148
+ input.source_collaboration_id ?? n.collaboration_id,
1149
+ input.source_working_memory_id ?? null, JSON.stringify(input.source_turn_ids ?? []),
498
1150
  input.successful_use_count ?? 0, input.failed_use_count ?? 0,
499
1151
  input.disputed_at ?? null, input.dispute_reason ?? null, input.contradiction_score ?? 0,
500
- input.superseded_at ?? null, now()]);
501
- return mapKnowledgeMemory(rows[0]);
1152
+ input.superseded_at ?? null, input.retired_at ?? null, createdAt]);
1153
+ const knowledge = mapKnowledgeMemory(rows[0]);
1154
+ await insertMemoryEventInternal({
1155
+ ...n,
1156
+ entity_kind: 'knowledge_memory',
1157
+ entity_id: String(knowledge.id),
1158
+ event_type: 'knowledge.created',
1159
+ payload: {
1160
+ after: knowledge,
1161
+ },
1162
+ created_at: createdAt,
1163
+ });
1164
+ return knowledge;
502
1165
  },
503
1166
  async insertKnowledgeMemories(inputs) {
504
1167
  return this.transaction(async () => {
@@ -604,6 +1267,9 @@ export function createPostgresAdapter(pool, options) {
604
1267
  const { rows } = await pool.query('SELECT * FROM knowledge_memory WHERE id = $1', [id]);
605
1268
  return rows[0] ? mapKnowledgeMemory(rows[0]) : null;
606
1269
  },
1270
+ async getExistingKnowledgeMemoryIds(ids) {
1271
+ return getExistingIds('knowledge_memory', ids);
1272
+ },
607
1273
  async getActiveKnowledgeMemory(scope) {
608
1274
  const { rows } = await pool.query(`SELECT * FROM knowledge_memory WHERE ${scopeWhere()} AND superseded_by_id IS NULL AND retired_at IS NULL ORDER BY last_accessed_at DESC`, scopeParams(scope));
609
1275
  return rows.map(mapKnowledgeMemory);
@@ -662,16 +1328,17 @@ export function createPostgresAdapter(pool, options) {
662
1328
  return rows.map(mapKnowledgeMemory);
663
1329
  },
664
1330
  async searchKnowledge(scope, queryText, searchOptions) {
1331
+ // scopeParams occupies $1..$5. queryText binds at $6 and limit at $7.
665
1332
  const params = scopeParams(scope);
666
1333
  const limit = searchOptions?.limit ?? 10;
667
1334
  const activeClause = searchOptions?.activeOnly ? ' AND superseded_by_id IS NULL AND retired_at IS NULL' : '';
668
1335
  params.push(queryText, limit);
669
- const { rows } = await pool.query(`SELECT *, ts_rank(search_vector, plainto_tsquery('english', $5)) AS rank
1336
+ const { rows } = await pool.query(`SELECT *, ts_rank(search_vector, plainto_tsquery('english', $6)) AS rank
670
1337
  FROM knowledge_memory
671
1338
  WHERE ${scopeWhere()} ${activeClause}
672
- AND search_vector @@ plainto_tsquery('english', $5)
1339
+ AND search_vector @@ plainto_tsquery('english', $6)
673
1340
  ORDER BY rank DESC
674
- LIMIT $6`, params);
1341
+ LIMIT $7`, params);
675
1342
  return rows
676
1343
  .map((row) => ({
677
1344
  item: mapKnowledgeMemory(row),
@@ -727,6 +1394,7 @@ export function createPostgresAdapter(pool, options) {
727
1394
  return rows.map(mapKnowledgeMemoryAudit);
728
1395
  },
729
1396
  async updateKnowledgeMemory(id, patch) {
1397
+ const before = await this.getKnowledgeMemoryById(id);
730
1398
  const assignments = [];
731
1399
  const values = [];
732
1400
  const push = (column, value) => {
@@ -769,31 +1437,166 @@ export function createPostgresAdapter(pool, options) {
769
1437
  }
770
1438
  values.push(id);
771
1439
  const { rows } = await pool.query(`UPDATE knowledge_memory SET ${assignments.join(', ')} WHERE id = $${values.length} RETURNING *`, values);
772
- return rows[0] ? mapKnowledgeMemory(rows[0]) : null;
1440
+ const after = rows[0] ? mapKnowledgeMemory(rows[0]) : null;
1441
+ if (before && after) {
1442
+ await insertMemoryEventInternal({
1443
+ ...normalizeScope(after),
1444
+ entity_kind: 'knowledge_memory',
1445
+ entity_id: String(after.id),
1446
+ event_type: 'knowledge.updated',
1447
+ payload: {
1448
+ before,
1449
+ after,
1450
+ patch,
1451
+ },
1452
+ created_at: now(),
1453
+ });
1454
+ }
1455
+ return after;
773
1456
  },
774
1457
  async touchKnowledgeMemory(id) {
775
- await pool.query(`UPDATE knowledge_memory SET access_count = access_count + 1, last_accessed_at = $2 WHERE id = $1`, [id, now()]);
1458
+ const before = await this.getKnowledgeMemoryById(id);
1459
+ const touchedAt = now();
1460
+ await pool.query(`UPDATE knowledge_memory SET access_count = access_count + 1, last_accessed_at = $2 WHERE id = $1`, [id, touchedAt]);
1461
+ const after = await this.getKnowledgeMemoryById(id);
1462
+ if (before && after) {
1463
+ await insertMemoryEventInternal({
1464
+ ...normalizeScope(after),
1465
+ entity_kind: 'knowledge_memory',
1466
+ entity_id: String(after.id),
1467
+ event_type: 'knowledge.touched',
1468
+ payload: {
1469
+ before,
1470
+ after,
1471
+ patch: {
1472
+ last_accessed_at: touchedAt,
1473
+ access_count: after.access_count,
1474
+ },
1475
+ },
1476
+ created_at: touchedAt,
1477
+ });
1478
+ }
1479
+ },
1480
+ async touchKnowledgeMemories(ids) {
1481
+ const uniqueIds = [...new Set(ids)].filter((id) => Number.isInteger(id) && id > 0);
1482
+ if (uniqueIds.length === 0)
1483
+ return;
1484
+ const { rows: beforeRows } = await pool.query('SELECT * FROM knowledge_memory WHERE id = ANY($1::int[])', [uniqueIds]);
1485
+ if (beforeRows.length === 0)
1486
+ return;
1487
+ const touchedAt = now();
1488
+ const before = beforeRows.map(mapKnowledgeMemory);
1489
+ const { rows: afterRows } = await pool.query(`UPDATE knowledge_memory
1490
+ SET access_count = access_count + 1, last_accessed_at = $2
1491
+ WHERE id = ANY($1::int[])
1492
+ RETURNING *`, [uniqueIds, touchedAt]);
1493
+ const afterById = new Map(afterRows.map((row) => {
1494
+ const mapped = mapKnowledgeMemory(row);
1495
+ return [mapped.id, mapped];
1496
+ }));
1497
+ await insertMemoryEventsBatchInternal(before.flatMap((item) => {
1498
+ const after = afterById.get(item.id);
1499
+ if (!after)
1500
+ return [];
1501
+ return [{
1502
+ ...normalizeScope(after),
1503
+ entity_kind: 'knowledge_memory',
1504
+ entity_id: String(after.id),
1505
+ event_type: 'knowledge.touched',
1506
+ payload: {
1507
+ before: item,
1508
+ after,
1509
+ patch: {
1510
+ last_accessed_at: touchedAt,
1511
+ access_count: after.access_count,
1512
+ },
1513
+ },
1514
+ created_at: touchedAt,
1515
+ }];
1516
+ }));
776
1517
  },
777
1518
  async retireKnowledgeMemory(id, retiredAt) {
778
- await pool.query(`UPDATE knowledge_memory SET retired_at = $2 WHERE id = $1`, [id, retiredAt ?? now()]);
1519
+ const before = await this.getKnowledgeMemoryById(id);
1520
+ const effectiveRetiredAt = retiredAt ?? now();
1521
+ await pool.query(`UPDATE knowledge_memory SET retired_at = $2 WHERE id = $1`, [id, effectiveRetiredAt]);
1522
+ const after = await this.getKnowledgeMemoryById(id);
1523
+ if (before && after) {
1524
+ await insertMemoryEventInternal({
1525
+ ...normalizeScope(after),
1526
+ entity_kind: 'knowledge_memory',
1527
+ entity_id: String(after.id),
1528
+ event_type: 'knowledge.retired',
1529
+ payload: {
1530
+ before,
1531
+ after,
1532
+ patch: {
1533
+ retired_at: effectiveRetiredAt,
1534
+ },
1535
+ },
1536
+ created_at: effectiveRetiredAt,
1537
+ });
1538
+ }
779
1539
  },
780
1540
  async supersedeKnowledgeMemory(oldId, newId) {
1541
+ const before = await this.getKnowledgeMemoryById(oldId);
1542
+ const supersededAt = now();
781
1543
  await pool.query(`UPDATE knowledge_memory
782
1544
  SET superseded_by_id = $2, superseded_at = $3, knowledge_state = 'superseded', retired_at = $3
783
- WHERE id = $1`, [oldId, newId, now()]);
1545
+ WHERE id = $1`, [oldId, newId, supersededAt]);
1546
+ const after = await this.getKnowledgeMemoryById(oldId);
1547
+ if (before && after) {
1548
+ await insertMemoryEventInternal({
1549
+ ...normalizeScope(after),
1550
+ entity_kind: 'knowledge_memory',
1551
+ entity_id: String(after.id),
1552
+ event_type: 'knowledge.superseded',
1553
+ payload: {
1554
+ before,
1555
+ after,
1556
+ refs: {
1557
+ new_id: newId,
1558
+ },
1559
+ },
1560
+ created_at: supersededAt,
1561
+ });
1562
+ }
784
1563
  },
785
1564
  async insertWorkItem(input) {
786
1565
  const n = normalizeScope(input);
1566
+ const createdAt = now();
787
1567
  const { rows } = await pool.query(`INSERT INTO work_items (tenant_id, system_id, workspace_id, collaboration_id, scope_id, session_id, title, kind, status, detail, created_at, updated_at)
788
1568
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $11)
789
1569
  RETURNING *`, [n.tenant_id, n.system_id, n.workspace_id, n.collaboration_id, n.scope_id, input.session_id,
790
- input.title, input.kind ?? 'objective', input.status ?? 'open', input.detail ?? null, now()]);
791
- return mapWorkItem(rows[0]);
1570
+ input.title, input.kind ?? 'objective', input.status ?? 'open', input.detail ?? null, createdAt]);
1571
+ const workItem = mapWorkItem(rows[0]);
1572
+ await insertMemoryEventInternal({
1573
+ ...n,
1574
+ session_id: workItem.session_id,
1575
+ entity_kind: 'work_item',
1576
+ entity_id: String(workItem.id),
1577
+ event_type: 'work_item.created',
1578
+ payload: {
1579
+ after: workItem,
1580
+ },
1581
+ created_at: createdAt,
1582
+ });
1583
+ return workItem;
792
1584
  },
793
1585
  async getActiveWorkItems(scope) {
794
1586
  const { rows } = await pool.query(`SELECT * FROM work_items WHERE ${scopeWhere()} AND status != 'done' ORDER BY id DESC`, scopeParams(scope));
795
1587
  return rows.map(mapWorkItem);
796
1588
  },
1589
+ async getWorkItemById(id) {
1590
+ const { rows } = await pool.query('SELECT * FROM work_items WHERE id = $1', [id]);
1591
+ return rows[0] ? mapWorkItem(rows[0]) : null;
1592
+ },
1593
+ async getExistingWorkItemIds(ids) {
1594
+ return getExistingIds('work_items', ids);
1595
+ },
1596
+ async getActiveWorkItemsCrossScope(scope, level) {
1597
+ const { rows } = await pool.query(`SELECT * FROM work_items WHERE ${wideScopeWhere(scope, level)} AND status != 'done' ORDER BY id DESC`, wideScopeParams(scope, level));
1598
+ return rows.map(mapWorkItem);
1599
+ },
797
1600
  async getWorkItemsByTimeRange(scope, range) {
798
1601
  const params = scopeParams(scope);
799
1602
  let query = `SELECT * FROM work_items WHERE ${scopeWhere()}`;
@@ -809,11 +1612,605 @@ export function createPostgresAdapter(pool, options) {
809
1612
  const { rows } = await pool.query(query, params);
810
1613
  return rows.map(mapWorkItem);
811
1614
  },
1615
+ async getWorkItemsByTimeRangeCrossScope(scope, level, range) {
1616
+ const params = wideScopeParams(scope, level);
1617
+ let query = `SELECT * FROM work_items WHERE ${wideScopeWhere(scope, level)}`;
1618
+ if (range.start_at != null) {
1619
+ params.push(range.start_at);
1620
+ query += ` AND created_at >= $${params.length}`;
1621
+ }
1622
+ if (range.end_at != null) {
1623
+ params.push(range.end_at);
1624
+ query += ` AND created_at <= $${params.length}`;
1625
+ }
1626
+ query += ' ORDER BY id DESC';
1627
+ const { rows } = await pool.query(query, params);
1628
+ return rows.map(mapWorkItem);
1629
+ },
812
1630
  async updateWorkItemStatus(id, status) {
813
- await pool.query(`UPDATE work_items SET status = $2, updated_at = $3 WHERE id = $1`, [id, status, now()]);
1631
+ const { rows: beforeRows } = await pool.query('SELECT * FROM work_items WHERE id = $1', [id]);
1632
+ const updatedAt = now();
1633
+ await pool.query(`UPDATE work_items SET status = $2, version = COALESCE(version, 1) + 1, updated_at = $3 WHERE id = $1`, [id, status, updatedAt]);
1634
+ const { rows: afterRows } = await pool.query('SELECT * FROM work_items WHERE id = $1', [id]);
1635
+ if (beforeRows[0] && afterRows[0]) {
1636
+ const before = mapWorkItem(beforeRows[0]);
1637
+ const after = mapWorkItem(afterRows[0]);
1638
+ await insertMemoryEventInternal({
1639
+ ...normalizeScope(after),
1640
+ session_id: after.session_id,
1641
+ entity_kind: 'work_item',
1642
+ entity_id: String(after.id),
1643
+ event_type: 'work_item.status_changed',
1644
+ payload: {
1645
+ before,
1646
+ after,
1647
+ patch: {
1648
+ status,
1649
+ updated_at: updatedAt,
1650
+ },
1651
+ },
1652
+ created_at: updatedAt,
1653
+ });
1654
+ }
1655
+ },
1656
+ async updateWorkItem(id, patch, options) {
1657
+ const { rows: beforeRows } = await pool.query('SELECT * FROM work_items WHERE id = $1', [id]);
1658
+ if (!beforeRows[0])
1659
+ return null;
1660
+ const before = mapWorkItem(beforeRows[0]);
1661
+ if (options?.expectedVersion != null && before.version !== options.expectedVersion) {
1662
+ throw new ConflictError(`Work item ${id} version mismatch`);
1663
+ }
1664
+ const updatedAt = now();
1665
+ const nextTitle = patch.title ?? before.title;
1666
+ const nextDetail = patch.detail !== undefined ? patch.detail : before.detail;
1667
+ const nextStatus = patch.status ?? before.status;
1668
+ const nextVisibility = patch.visibility_class ?? before.visibility_class;
1669
+ const { rows: afterRows } = await pool.query(`UPDATE work_items
1670
+ SET title = $2, detail = $3, status = $4, visibility_class = $5, version = COALESCE(version, 1) + 1, updated_at = $6
1671
+ WHERE id = $1
1672
+ RETURNING *`, [id, nextTitle, nextDetail, nextStatus, nextVisibility, updatedAt]);
1673
+ const after = mapWorkItem(afterRows[0]);
1674
+ await insertMemoryEventInternal({
1675
+ ...normalizeScope(after),
1676
+ session_id: after.session_id,
1677
+ entity_kind: 'work_item',
1678
+ entity_id: String(after.id),
1679
+ event_type: patch.visibility_class !== undefined &&
1680
+ patch.title === undefined &&
1681
+ patch.detail === undefined &&
1682
+ patch.status === undefined
1683
+ ? 'work_item.visibility_changed'
1684
+ : 'work_item.updated',
1685
+ payload: { before, after, patch },
1686
+ created_at: updatedAt,
1687
+ });
1688
+ return after;
814
1689
  },
815
1690
  async deleteWorkItem(id) {
1691
+ const { rows } = await pool.query('SELECT * FROM work_items WHERE id = $1', [id]);
816
1692
  await pool.query('DELETE FROM work_items WHERE id = $1', [id]);
1693
+ if (rows[0]) {
1694
+ const workItem = mapWorkItem(rows[0]);
1695
+ await insertMemoryEventInternal({
1696
+ ...normalizeScope(workItem),
1697
+ session_id: workItem.session_id,
1698
+ entity_kind: 'work_item',
1699
+ entity_id: String(workItem.id),
1700
+ event_type: 'work_item.deleted',
1701
+ payload: {
1702
+ before: workItem,
1703
+ },
1704
+ created_at: now(),
1705
+ });
1706
+ }
1707
+ },
1708
+ async claimWorkItem(input) {
1709
+ return this.transaction(async () => {
1710
+ const { rows: workItemRows } = await pool.query('SELECT * FROM work_items WHERE id = $1', [
1711
+ input.work_item_id,
1712
+ ]);
1713
+ if (!workItemRows[0]) {
1714
+ throw new ConflictError(`Work item ${input.work_item_id} does not exist`);
1715
+ }
1716
+ const workItem = mapWorkItem(workItemRows[0]);
1717
+ if (workItem.status === 'done') {
1718
+ throw new ConflictError(`Work item ${input.work_item_id} is done`);
1719
+ }
1720
+ const claimedAt = input.claimed_at ?? now();
1721
+ const existingRow = await getAnyClaimRowByWorkItem(input.work_item_id);
1722
+ if (existingRow) {
1723
+ const existing = mapWorkClaim(existingRow);
1724
+ if (existing.status === 'active' && existing.expires_at <= claimedAt) {
1725
+ await expireClaimRecord(existingRow, claimedAt);
1726
+ }
1727
+ else if (existing.status === 'active') {
1728
+ if (!sameActor(existing.actor, input.actor)) {
1729
+ throw new ConflictError(`Work item ${input.work_item_id} is already claimed`);
1730
+ }
1731
+ return (await this.renewWorkClaim(existing.id, input.actor, input.lease_seconds ?? 300));
1732
+ }
1733
+ }
1734
+ const normalized = normalizeScope(input);
1735
+ const actorParts = serializeActorMetadata(input.actor);
1736
+ const claimToken = `claim-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
1737
+ const expiresAt = claimedAt + (input.lease_seconds ?? 300);
1738
+ const { rows } = await pool.query(`INSERT INTO work_claims_current
1739
+ (tenant_id, system_id, workspace_id, collaboration_id, scope_id, work_item_id, session_id,
1740
+ actor_kind, actor_id, actor_system_id, actor_display_name, actor_metadata,
1741
+ claim_token, status, claimed_at, expires_at, released_at, release_reason, source_event_id, visibility_class, version)
1742
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12::jsonb, $13, 'active', $14, $15, NULL, NULL, NULL, $16, 1)
1743
+ ON CONFLICT (work_item_id) DO UPDATE SET
1744
+ tenant_id = EXCLUDED.tenant_id,
1745
+ system_id = EXCLUDED.system_id,
1746
+ workspace_id = EXCLUDED.workspace_id,
1747
+ collaboration_id = EXCLUDED.collaboration_id,
1748
+ scope_id = EXCLUDED.scope_id,
1749
+ session_id = EXCLUDED.session_id,
1750
+ actor_kind = EXCLUDED.actor_kind,
1751
+ actor_id = EXCLUDED.actor_id,
1752
+ actor_system_id = EXCLUDED.actor_system_id,
1753
+ actor_display_name = EXCLUDED.actor_display_name,
1754
+ actor_metadata = EXCLUDED.actor_metadata,
1755
+ claim_token = EXCLUDED.claim_token,
1756
+ status = 'active',
1757
+ claimed_at = EXCLUDED.claimed_at,
1758
+ expires_at = EXCLUDED.expires_at,
1759
+ released_at = NULL,
1760
+ release_reason = NULL,
1761
+ source_event_id = NULL,
1762
+ visibility_class = EXCLUDED.visibility_class,
1763
+ version = COALESCE(work_claims_current.version, 1) + 1
1764
+ RETURNING *`, [
1765
+ normalized.tenant_id,
1766
+ normalized.system_id,
1767
+ normalized.workspace_id,
1768
+ normalized.collaboration_id,
1769
+ normalized.scope_id,
1770
+ input.work_item_id,
1771
+ input.session_id ?? null,
1772
+ actorParts[0],
1773
+ actorParts[1],
1774
+ actorParts[2],
1775
+ actorParts[3],
1776
+ actorParts[4],
1777
+ claimToken,
1778
+ claimedAt,
1779
+ expiresAt,
1780
+ input.visibility_class,
1781
+ ]);
1782
+ const claim = mapWorkClaim(rows[0]);
1783
+ const event = await insertMemoryEventInternal({
1784
+ ...normalizeScope(claim),
1785
+ session_id: claim.session_id,
1786
+ actor_id: claim.actor.actor_id,
1787
+ actor_kind: claim.actor.actor_kind,
1788
+ actor_system_id: claim.actor.system_id,
1789
+ actor_display_name: claim.actor.display_name,
1790
+ actor_metadata: claim.actor.metadata,
1791
+ entity_kind: 'work_claim',
1792
+ entity_id: String(claim.id),
1793
+ event_type: 'work_claim.claimed',
1794
+ payload: { after: claim },
1795
+ created_at: claimedAt,
1796
+ });
1797
+ await pool.query('UPDATE work_claims_current SET source_event_id = $2 WHERE id = $1', [
1798
+ claim.id,
1799
+ event.event_id,
1800
+ ]);
1801
+ return { ...claim, source_event_id: event.event_id };
1802
+ });
1803
+ },
1804
+ async renewWorkClaim(claimId, actor, leaseSeconds = 300) {
1805
+ return this.transaction(async () => {
1806
+ const { rows: beforeRows } = await pool.query('SELECT * FROM work_claims_current WHERE id = $1', [claimId]);
1807
+ if (!beforeRows[0])
1808
+ return null;
1809
+ const claim = mapWorkClaim(beforeRows[0]);
1810
+ if (!sameActor(claim.actor, actor)) {
1811
+ throw new ConflictError(`Claim ${claimId} is owned by another actor`);
1812
+ }
1813
+ const currentNow = now();
1814
+ if (claim.status !== 'active') {
1815
+ throw new ConflictError(`Claim ${claimId} is no longer active`);
1816
+ }
1817
+ if (claim.expires_at <= currentNow) {
1818
+ await expireClaimRecord(beforeRows[0], currentNow);
1819
+ return null;
1820
+ }
1821
+ const { rows } = await pool.query(`UPDATE work_claims_current
1822
+ SET expires_at = $2, version = COALESCE(version, 1) + 1
1823
+ WHERE id = $1
1824
+ RETURNING *`, [claimId, Math.max(claim.expires_at, currentNow) + leaseSeconds]);
1825
+ const after = mapWorkClaim(rows[0]);
1826
+ const event = await insertMemoryEventInternal({
1827
+ ...normalizeScope(after),
1828
+ session_id: after.session_id,
1829
+ actor_id: after.actor.actor_id,
1830
+ actor_kind: after.actor.actor_kind,
1831
+ actor_system_id: after.actor.system_id,
1832
+ actor_display_name: after.actor.display_name,
1833
+ actor_metadata: after.actor.metadata,
1834
+ entity_kind: 'work_claim',
1835
+ entity_id: String(after.id),
1836
+ event_type: 'work_claim.renewed',
1837
+ payload: { before: claim, after },
1838
+ created_at: currentNow,
1839
+ });
1840
+ await pool.query('UPDATE work_claims_current SET source_event_id = $2 WHERE id = $1', [
1841
+ after.id,
1842
+ event.event_id,
1843
+ ]);
1844
+ return { ...after, source_event_id: event.event_id };
1845
+ });
1846
+ },
1847
+ async releaseWorkClaim(claimId, actor, reason) {
1848
+ return this.transaction(async () => {
1849
+ const { rows: beforeRows } = await pool.query('SELECT * FROM work_claims_current WHERE id = $1', [claimId]);
1850
+ if (!beforeRows[0])
1851
+ return null;
1852
+ const claim = mapWorkClaim(beforeRows[0]);
1853
+ if (!sameActor(claim.actor, actor)) {
1854
+ throw new ConflictError(`Claim ${claimId} is owned by another actor`);
1855
+ }
1856
+ if (claim.status !== 'active') {
1857
+ throw new ConflictError(`Claim ${claimId} is no longer active`);
1858
+ }
1859
+ const releasedAt = now();
1860
+ const { rows } = await pool.query(`UPDATE work_claims_current
1861
+ SET status = 'released', released_at = $2, release_reason = $3, version = COALESCE(version, 1) + 1
1862
+ WHERE id = $1
1863
+ RETURNING *`, [claimId, releasedAt, reason ?? null]);
1864
+ const after = mapWorkClaim(rows[0]);
1865
+ const event = await insertMemoryEventInternal({
1866
+ ...normalizeScope(after),
1867
+ session_id: after.session_id,
1868
+ actor_id: after.actor.actor_id,
1869
+ actor_kind: after.actor.actor_kind,
1870
+ actor_system_id: after.actor.system_id,
1871
+ actor_display_name: after.actor.display_name,
1872
+ actor_metadata: after.actor.metadata,
1873
+ entity_kind: 'work_claim',
1874
+ entity_id: String(after.id),
1875
+ event_type: 'work_claim.released',
1876
+ payload: { before: claim, after },
1877
+ created_at: releasedAt,
1878
+ });
1879
+ await pool.query('UPDATE work_claims_current SET source_event_id = $2 WHERE id = $1', [
1880
+ after.id,
1881
+ event.event_id,
1882
+ ]);
1883
+ return { ...after, source_event_id: event.event_id };
1884
+ });
1885
+ },
1886
+ async getActiveWorkClaim(workItemId) {
1887
+ const existingRow = await getAnyClaimRowByWorkItem(workItemId);
1888
+ if (!existingRow)
1889
+ return null;
1890
+ const claim = mapWorkClaim(existingRow);
1891
+ if (claim.status === 'active' && claim.expires_at <= now()) {
1892
+ await this.transaction(async () => {
1893
+ await expireClaimRecord(existingRow);
1894
+ });
1895
+ return null;
1896
+ }
1897
+ return claim.status === 'active' ? claim : null;
1898
+ },
1899
+ async listWorkClaims(scope, options) {
1900
+ const expiredAt = now();
1901
+ const { rows: expiredRows } = await pool.query(`SELECT * FROM work_claims_current
1902
+ WHERE ${scopeWhere()} AND status = 'active' AND expires_at <= $6`, [...scopeParams(scope), expiredAt]);
1903
+ if (expiredRows.length > 0) {
1904
+ await this.transaction(async () => {
1905
+ for (const row of expiredRows) {
1906
+ await expireClaimRecord(row, expiredAt);
1907
+ }
1908
+ });
1909
+ }
1910
+ const { rows } = await pool.query(`SELECT * FROM work_claims_current WHERE ${scopeWhere()} ORDER BY claimed_at DESC`, scopeParams(scope));
1911
+ const claims = rows.map(mapWorkClaim);
1912
+ return claims.filter((claim) => {
1913
+ if (!options?.includeExpired && claim.status === 'expired')
1914
+ return false;
1915
+ if (!options?.includeReleased && claim.status === 'released')
1916
+ return false;
1917
+ if (options?.sessionId && claim.session_id !== options.sessionId)
1918
+ return false;
1919
+ if (options?.visibilityClass && claim.visibility_class !== options.visibilityClass)
1920
+ return false;
1921
+ if (options?.actor && !sameActor(claim.actor, options.actor))
1922
+ return false;
1923
+ return true;
1924
+ });
1925
+ },
1926
+ async listWorkClaimsCrossScope(scope, level, options) {
1927
+ const expiredAt = now();
1928
+ const levelParams = wideScopeParams(scope, level);
1929
+ const { rows: expiredRows } = await pool.query(`SELECT * FROM work_claims_current
1930
+ WHERE ${wideScopeWhere(scope, level)} AND status = 'active' AND expires_at <= $${levelParams.length + 1}`, [...levelParams, expiredAt]);
1931
+ if (expiredRows.length > 0) {
1932
+ await this.transaction(async () => {
1933
+ for (const row of expiredRows) {
1934
+ await expireClaimRecord(row, expiredAt);
1935
+ }
1936
+ });
1937
+ }
1938
+ const { rows } = await pool.query(`SELECT * FROM work_claims_current WHERE ${wideScopeWhere(scope, level)} ORDER BY claimed_at DESC`, levelParams);
1939
+ const claims = rows.map(mapWorkClaim);
1940
+ return claims.filter((claim) => {
1941
+ if (!options?.includeExpired && claim.status === 'expired')
1942
+ return false;
1943
+ if (!options?.includeReleased && claim.status === 'released')
1944
+ return false;
1945
+ if (options?.sessionId && claim.session_id !== options.sessionId)
1946
+ return false;
1947
+ if (options?.visibilityClass && claim.visibility_class !== options.visibilityClass)
1948
+ return false;
1949
+ if (options?.actor && !sameActor(claim.actor, options.actor))
1950
+ return false;
1951
+ return true;
1952
+ });
1953
+ },
1954
+ async createHandoff(input) {
1955
+ return this.transaction(async () => {
1956
+ const normalized = normalizeScope(input);
1957
+ const createdAt = input.created_at ?? now();
1958
+ const fromParts = serializeActorMetadata(input.from_actor);
1959
+ const toParts = serializeActorMetadata(input.to_actor);
1960
+ const { rows } = await pool.query(`INSERT INTO handoff_records
1961
+ (tenant_id, system_id, workspace_id, collaboration_id, scope_id, work_item_id, session_id,
1962
+ from_actor_kind, from_actor_id, from_actor_system_id, from_actor_display_name, from_actor_metadata,
1963
+ to_actor_kind, to_actor_id, to_actor_system_id, to_actor_display_name, to_actor_metadata,
1964
+ summary, context_bundle_ref, status, created_at, expires_at, visibility_class, version)
1965
+ VALUES ($1, $2, $3, $4, $5, $6, $7,
1966
+ $8, $9, $10, $11, $12::jsonb,
1967
+ $13, $14, $15, $16, $17::jsonb,
1968
+ $18, $19, 'pending', $20, $21, $22, 1)
1969
+ RETURNING *`, [
1970
+ normalized.tenant_id,
1971
+ normalized.system_id,
1972
+ normalized.workspace_id,
1973
+ normalized.collaboration_id,
1974
+ normalized.scope_id,
1975
+ input.work_item_id,
1976
+ input.session_id ?? null,
1977
+ fromParts[0],
1978
+ fromParts[1],
1979
+ fromParts[2],
1980
+ fromParts[3],
1981
+ fromParts[4],
1982
+ toParts[0],
1983
+ toParts[1],
1984
+ toParts[2],
1985
+ toParts[3],
1986
+ toParts[4],
1987
+ input.summary,
1988
+ input.context_bundle_ref ?? null,
1989
+ createdAt,
1990
+ input.expires_at ?? null,
1991
+ input.visibility_class,
1992
+ ]);
1993
+ const handoff = mapHandoff(rows[0]);
1994
+ const event = await insertMemoryEventInternal({
1995
+ ...normalizeScope(handoff),
1996
+ session_id: handoff.session_id,
1997
+ actor_id: handoff.from_actor.actor_id,
1998
+ actor_kind: handoff.from_actor.actor_kind,
1999
+ actor_system_id: handoff.from_actor.system_id,
2000
+ actor_display_name: handoff.from_actor.display_name,
2001
+ actor_metadata: handoff.from_actor.metadata,
2002
+ entity_kind: 'handoff',
2003
+ entity_id: String(handoff.id),
2004
+ event_type: 'handoff.created',
2005
+ payload: { after: handoff },
2006
+ created_at: createdAt,
2007
+ });
2008
+ await pool.query('UPDATE handoff_records SET source_event_id = $2 WHERE id = $1', [
2009
+ handoff.id,
2010
+ event.event_id,
2011
+ ]);
2012
+ return { ...handoff, source_event_id: event.event_id };
2013
+ });
2014
+ },
2015
+ async acceptHandoff(handoffId, actor, reason) {
2016
+ return this.transaction(async () => {
2017
+ const { rows: handoffRows } = await pool.query('SELECT * FROM handoff_records WHERE id = $1', [handoffId]);
2018
+ if (!handoffRows[0])
2019
+ return null;
2020
+ const handoff = mapHandoff(handoffRows[0]);
2021
+ if (!sameActor(handoff.to_actor, actor)) {
2022
+ throw new ConflictError(`Handoff ${handoffId} is assigned to another actor`);
2023
+ }
2024
+ const acceptedAt = now();
2025
+ if (handoff.status !== 'pending') {
2026
+ throw new ConflictError(`Handoff ${handoffId} is no longer pending`);
2027
+ }
2028
+ if (handoff.expires_at != null && handoff.expires_at <= acceptedAt) {
2029
+ await expireHandoffRecord(handoffRows[0], acceptedAt);
2030
+ return null;
2031
+ }
2032
+ const activeClaim = await this.getActiveWorkClaim(handoff.work_item_id);
2033
+ if (activeClaim && !sameActor(activeClaim.actor, handoff.from_actor)) {
2034
+ throw new ConflictError(`Work item ${handoff.work_item_id} has another active owner`);
2035
+ }
2036
+ if (activeClaim) {
2037
+ await this.releaseWorkClaim(activeClaim.id, handoff.from_actor, 'handoff_accepted');
2038
+ }
2039
+ await this.claimWorkItem({
2040
+ ...normalizeScope(handoff),
2041
+ work_item_id: handoff.work_item_id,
2042
+ actor,
2043
+ session_id: handoff.session_id,
2044
+ visibility_class: handoff.visibility_class,
2045
+ });
2046
+ const { rows } = await pool.query(`UPDATE handoff_records
2047
+ SET status = 'accepted', accepted_at = $2, decision_reason = $3, version = COALESCE(version, 1) + 1
2048
+ WHERE id = $1
2049
+ RETURNING *`, [handoffId, acceptedAt, reason ?? null]);
2050
+ const after = mapHandoff(rows[0]);
2051
+ const event = await insertMemoryEventInternal({
2052
+ ...normalizeScope(after),
2053
+ session_id: after.session_id,
2054
+ actor_id: actor.actor_id,
2055
+ actor_kind: actor.actor_kind,
2056
+ actor_system_id: actor.system_id,
2057
+ actor_display_name: actor.display_name,
2058
+ actor_metadata: actor.metadata,
2059
+ entity_kind: 'handoff',
2060
+ entity_id: String(after.id),
2061
+ event_type: 'handoff.accepted',
2062
+ payload: { before: handoff, after },
2063
+ created_at: acceptedAt,
2064
+ });
2065
+ await pool.query('UPDATE handoff_records SET source_event_id = $2 WHERE id = $1', [
2066
+ after.id,
2067
+ event.event_id,
2068
+ ]);
2069
+ return { ...after, source_event_id: event.event_id };
2070
+ });
2071
+ },
2072
+ async rejectHandoff(handoffId, actor, reason) {
2073
+ return this.transaction(async () => {
2074
+ const { rows: handoffRows } = await pool.query('SELECT * FROM handoff_records WHERE id = $1', [handoffId]);
2075
+ if (!handoffRows[0])
2076
+ return null;
2077
+ const handoff = mapHandoff(handoffRows[0]);
2078
+ if (!sameActor(handoff.to_actor, actor)) {
2079
+ throw new ConflictError(`Handoff ${handoffId} is assigned to another actor`);
2080
+ }
2081
+ const rejectedAt = now();
2082
+ if (handoff.status !== 'pending') {
2083
+ throw new ConflictError(`Handoff ${handoffId} is no longer pending`);
2084
+ }
2085
+ if (handoff.expires_at != null && handoff.expires_at <= rejectedAt) {
2086
+ await expireHandoffRecord(handoffRows[0], rejectedAt);
2087
+ return null;
2088
+ }
2089
+ const { rows } = await pool.query(`UPDATE handoff_records
2090
+ SET status = 'rejected', rejected_at = $2, decision_reason = $3, version = COALESCE(version, 1) + 1
2091
+ WHERE id = $1
2092
+ RETURNING *`, [handoffId, rejectedAt, reason ?? null]);
2093
+ const after = mapHandoff(rows[0]);
2094
+ const event = await insertMemoryEventInternal({
2095
+ ...normalizeScope(after),
2096
+ session_id: after.session_id,
2097
+ actor_id: actor.actor_id,
2098
+ actor_kind: actor.actor_kind,
2099
+ actor_system_id: actor.system_id,
2100
+ actor_display_name: actor.display_name,
2101
+ actor_metadata: actor.metadata,
2102
+ entity_kind: 'handoff',
2103
+ entity_id: String(after.id),
2104
+ event_type: 'handoff.rejected',
2105
+ payload: { before: handoff, after },
2106
+ created_at: rejectedAt,
2107
+ });
2108
+ await pool.query('UPDATE handoff_records SET source_event_id = $2 WHERE id = $1', [
2109
+ after.id,
2110
+ event.event_id,
2111
+ ]);
2112
+ return { ...after, source_event_id: event.event_id };
2113
+ });
2114
+ },
2115
+ async cancelHandoff(handoffId, actor, reason) {
2116
+ return this.transaction(async () => {
2117
+ const { rows: handoffRows } = await pool.query('SELECT * FROM handoff_records WHERE id = $1', [handoffId]);
2118
+ if (!handoffRows[0])
2119
+ return null;
2120
+ const handoff = mapHandoff(handoffRows[0]);
2121
+ if (!sameActor(handoff.from_actor, actor)) {
2122
+ throw new ConflictError(`Handoff ${handoffId} was created by another actor`);
2123
+ }
2124
+ const canceledAt = now();
2125
+ if (handoff.status !== 'pending') {
2126
+ throw new ConflictError(`Handoff ${handoffId} is no longer pending`);
2127
+ }
2128
+ if (handoff.expires_at != null && handoff.expires_at <= canceledAt) {
2129
+ await expireHandoffRecord(handoffRows[0], canceledAt);
2130
+ return null;
2131
+ }
2132
+ const { rows } = await pool.query(`UPDATE handoff_records
2133
+ SET status = 'canceled', canceled_at = $2, decision_reason = $3, version = COALESCE(version, 1) + 1
2134
+ WHERE id = $1
2135
+ RETURNING *`, [handoffId, canceledAt, reason ?? null]);
2136
+ const after = mapHandoff(rows[0]);
2137
+ const event = await insertMemoryEventInternal({
2138
+ ...normalizeScope(after),
2139
+ session_id: after.session_id,
2140
+ actor_id: actor.actor_id,
2141
+ actor_kind: actor.actor_kind,
2142
+ actor_system_id: actor.system_id,
2143
+ actor_display_name: actor.display_name,
2144
+ actor_metadata: actor.metadata,
2145
+ entity_kind: 'handoff',
2146
+ entity_id: String(after.id),
2147
+ event_type: 'handoff.canceled',
2148
+ payload: { before: handoff, after },
2149
+ created_at: canceledAt,
2150
+ });
2151
+ await pool.query('UPDATE handoff_records SET source_event_id = $2 WHERE id = $1', [
2152
+ after.id,
2153
+ event.event_id,
2154
+ ]);
2155
+ return { ...after, source_event_id: event.event_id };
2156
+ });
2157
+ },
2158
+ async listHandoffs(scope, options) {
2159
+ const expiredAt = now();
2160
+ const { rows: expiredRows } = await pool.query(`SELECT * FROM handoff_records
2161
+ WHERE ${scopeWhere()} AND status = 'pending' AND expires_at IS NOT NULL AND expires_at <= $6`, [...scopeParams(scope), expiredAt]);
2162
+ if (expiredRows.length > 0) {
2163
+ await this.transaction(async () => {
2164
+ for (const row of expiredRows) {
2165
+ await expireHandoffRecord(row, expiredAt);
2166
+ }
2167
+ });
2168
+ }
2169
+ const { rows } = await pool.query(`SELECT * FROM handoff_records WHERE ${scopeWhere()} ORDER BY created_at DESC`, scopeParams(scope));
2170
+ const handoffs = rows.map(mapHandoff);
2171
+ return handoffs.filter((handoff) => {
2172
+ if (options?.sessionId && handoff.session_id !== options.sessionId)
2173
+ return false;
2174
+ if (options?.statuses && !options.statuses.includes(handoff.status))
2175
+ return false;
2176
+ if (options?.actor) {
2177
+ if (options.direction === 'inbound')
2178
+ return sameActor(handoff.to_actor, options.actor);
2179
+ if (options.direction === 'outbound')
2180
+ return sameActor(handoff.from_actor, options.actor);
2181
+ return sameActor(handoff.to_actor, options.actor) || sameActor(handoff.from_actor, options.actor);
2182
+ }
2183
+ return true;
2184
+ }).slice(0, options?.limit ?? handoffs.length);
2185
+ },
2186
+ async listHandoffsCrossScope(scope, level, options) {
2187
+ const expiredAt = now();
2188
+ const levelParams = wideScopeParams(scope, level);
2189
+ const { rows: expiredRows } = await pool.query(`SELECT * FROM handoff_records
2190
+ WHERE ${wideScopeWhere(scope, level)} AND status = 'pending' AND expires_at IS NOT NULL AND expires_at <= $${levelParams.length + 1}`, [...levelParams, expiredAt]);
2191
+ if (expiredRows.length > 0) {
2192
+ await this.transaction(async () => {
2193
+ for (const row of expiredRows) {
2194
+ await expireHandoffRecord(row, expiredAt);
2195
+ }
2196
+ });
2197
+ }
2198
+ const { rows } = await pool.query(`SELECT * FROM handoff_records WHERE ${wideScopeWhere(scope, level)} ORDER BY created_at DESC`, levelParams);
2199
+ const handoffs = rows.map(mapHandoff);
2200
+ return handoffs.filter((handoff) => {
2201
+ if (options?.sessionId && handoff.session_id !== options.sessionId)
2202
+ return false;
2203
+ if (options?.statuses && !options.statuses.includes(handoff.status))
2204
+ return false;
2205
+ if (options?.actor) {
2206
+ if (options.direction === 'inbound')
2207
+ return sameActor(handoff.to_actor, options.actor);
2208
+ if (options.direction === 'outbound')
2209
+ return sameActor(handoff.from_actor, options.actor);
2210
+ return sameActor(handoff.to_actor, options.actor) || sameActor(handoff.from_actor, options.actor);
2211
+ }
2212
+ return true;
2213
+ }).slice(0, options?.limit ?? handoffs.length);
817
2214
  },
818
2215
  async upsertContextMonitor(input) {
819
2216
  const n = normalizeScope(input);
@@ -850,6 +2247,324 @@ export function createPostgresAdapter(pool, options) {
850
2247
  const { rows } = await pool.query(`SELECT * FROM compaction_log WHERE ${scopeWhere()} ORDER BY id DESC LIMIT $6`, params);
851
2248
  return rows.map(mapCompactionLog);
852
2249
  },
2250
+ async insertPlaybook(input) {
2251
+ const n = normalizeScope(input);
2252
+ const createdAt = input.created_at ?? now();
2253
+ const { rows } = await pool.query(`INSERT INTO playbooks (tenant_id, system_id, workspace_id, collaboration_id, scope_id, title, description, instructions,
2254
+ references_json, templates, scripts, assets, tags, status, source_session_id, source_working_memory_id, created_at, updated_at)
2255
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $17)
2256
+ RETURNING *`, [n.tenant_id, n.system_id, n.workspace_id, n.collaboration_id, n.scope_id,
2257
+ input.title, input.description, input.instructions,
2258
+ JSON.stringify(input.references ?? []), JSON.stringify(input.templates ?? []),
2259
+ JSON.stringify(input.scripts ?? []), JSON.stringify(input.assets ?? []),
2260
+ JSON.stringify(input.tags ?? []), input.status ?? 'draft',
2261
+ input.source_session_id ?? null, input.source_working_memory_id ?? null, createdAt]);
2262
+ const playbook = mapPlaybook(rows[0]);
2263
+ await insertMemoryEventInternal({
2264
+ ...n,
2265
+ session_id: playbook.source_session_id,
2266
+ entity_kind: 'playbook',
2267
+ entity_id: String(playbook.id),
2268
+ event_type: 'playbook.created',
2269
+ payload: {
2270
+ after: playbook,
2271
+ },
2272
+ created_at: createdAt,
2273
+ });
2274
+ return playbook;
2275
+ },
2276
+ async getPlaybookById(id) {
2277
+ const { rows } = await pool.query('SELECT * FROM playbooks WHERE id = $1', [id]);
2278
+ return rows[0] ? mapPlaybook(rows[0]) : null;
2279
+ },
2280
+ async getExistingPlaybookIds(ids) {
2281
+ return getExistingIds('playbooks', ids);
2282
+ },
2283
+ async getActivePlaybooks(scope) {
2284
+ const { rows } = await pool.query(`SELECT * FROM playbooks WHERE ${scopeWhere()} AND status IN ('draft', 'active') ORDER BY id DESC`, scopeParams(scope));
2285
+ return rows.map(mapPlaybook);
2286
+ },
2287
+ async getActivePlaybooksCrossScope(scope, level) {
2288
+ const { rows } = await pool.query(`SELECT * FROM playbooks
2289
+ WHERE ${wideScopeWhere(scope, level)} AND status IN ('draft', 'active')
2290
+ ORDER BY id DESC`, wideScopeParams(scope, level));
2291
+ return rows.map(mapPlaybook);
2292
+ },
2293
+ async searchPlaybooks(scope, query, options) {
2294
+ const limit = options?.limit ?? 20;
2295
+ const activeOnly = options?.activeOnly ?? true;
2296
+ const statusFilter = activeOnly
2297
+ ? `AND status NOT IN ('archived', 'deprecated')`
2298
+ : '';
2299
+ const { rows } = await pool.query(`SELECT *, ts_rank(search_vector, plainto_tsquery('english', $6)) AS rank
2300
+ FROM playbooks WHERE ${scopeWhere()} ${statusFilter}
2301
+ AND search_vector @@ plainto_tsquery('english', $6)
2302
+ ORDER BY rank DESC LIMIT $7`, [...scopeParams(scope), query, limit]);
2303
+ return rows.map((row, index) => ({
2304
+ item: mapPlaybook(row),
2305
+ rank: Number(row.rank ?? index),
2306
+ }));
2307
+ },
2308
+ async searchPlaybooksCrossScope(scope, level, query, options) {
2309
+ const limit = options?.limit ?? 20;
2310
+ const activeOnly = options?.activeOnly ?? true;
2311
+ const scopeClause = wideScopeWhere(scope, level);
2312
+ const statusFilter = activeOnly
2313
+ ? `AND status NOT IN ('archived', 'deprecated')`
2314
+ : '';
2315
+ const baseParams = wideScopeParams(scope, level);
2316
+ const queryIndex = baseParams.length + 1;
2317
+ const limitIndex = baseParams.length + 2;
2318
+ const { rows } = await pool.query(`SELECT *, ts_rank(search_vector, plainto_tsquery('english', $${queryIndex})) AS rank
2319
+ FROM playbooks WHERE ${scopeClause} ${statusFilter}
2320
+ AND search_vector @@ plainto_tsquery('english', $${queryIndex})
2321
+ ORDER BY rank DESC LIMIT $${limitIndex}`, [...baseParams, query, limit]);
2322
+ return rows.map((row, index) => ({
2323
+ item: mapPlaybook(row),
2324
+ rank: Number(row.rank ?? index),
2325
+ }));
2326
+ },
2327
+ async updatePlaybook(id, patch) {
2328
+ const before = await this.getPlaybookById(id);
2329
+ const sets = [];
2330
+ const values = [];
2331
+ let idx = 1;
2332
+ if (patch.title != null) {
2333
+ sets.push(`title = $${idx++}`);
2334
+ values.push(patch.title);
2335
+ }
2336
+ if (patch.description != null) {
2337
+ sets.push(`description = $${idx++}`);
2338
+ values.push(patch.description);
2339
+ }
2340
+ if (patch.instructions != null) {
2341
+ sets.push(`instructions = $${idx++}`);
2342
+ values.push(patch.instructions);
2343
+ }
2344
+ if (patch.references != null) {
2345
+ sets.push(`references_json = $${idx++}`);
2346
+ values.push(JSON.stringify(patch.references));
2347
+ }
2348
+ if (patch.templates != null) {
2349
+ sets.push(`templates = $${idx++}`);
2350
+ values.push(JSON.stringify(patch.templates));
2351
+ }
2352
+ if (patch.scripts != null) {
2353
+ sets.push(`scripts = $${idx++}`);
2354
+ values.push(JSON.stringify(patch.scripts));
2355
+ }
2356
+ if (patch.assets != null) {
2357
+ sets.push(`assets = $${idx++}`);
2358
+ values.push(JSON.stringify(patch.assets));
2359
+ }
2360
+ if (patch.tags != null) {
2361
+ sets.push(`tags = $${idx++}`);
2362
+ values.push(JSON.stringify(patch.tags));
2363
+ }
2364
+ if (patch.status != null) {
2365
+ sets.push(`status = $${idx++}`);
2366
+ values.push(patch.status);
2367
+ }
2368
+ if (sets.length === 0)
2369
+ return this.getPlaybookById(id);
2370
+ sets.push(`updated_at = $${idx++}`);
2371
+ values.push(now());
2372
+ values.push(id);
2373
+ const { rows } = await pool.query(`UPDATE playbooks SET ${sets.join(', ')} WHERE id = $${idx} RETURNING *`, values);
2374
+ const after = rows[0] ? mapPlaybook(rows[0]) : null;
2375
+ if (before && after) {
2376
+ await insertMemoryEventInternal({
2377
+ ...normalizeScope(after),
2378
+ session_id: after.source_session_id,
2379
+ entity_kind: 'playbook',
2380
+ entity_id: String(after.id),
2381
+ event_type: 'playbook.updated',
2382
+ payload: {
2383
+ before,
2384
+ after,
2385
+ patch,
2386
+ },
2387
+ created_at: after.updated_at,
2388
+ });
2389
+ }
2390
+ return after;
2391
+ },
2392
+ async recordPlaybookUse(id) {
2393
+ const before = await this.getPlaybookById(id);
2394
+ const usedAt = now();
2395
+ await pool.query('UPDATE playbooks SET use_count = use_count + 1, last_used_at = $1 WHERE id = $2', [usedAt, id]);
2396
+ const after = await this.getPlaybookById(id);
2397
+ if (before && after) {
2398
+ await insertMemoryEventInternal({
2399
+ ...normalizeScope(after),
2400
+ session_id: after.source_session_id,
2401
+ entity_kind: 'playbook',
2402
+ entity_id: String(after.id),
2403
+ event_type: 'playbook.used',
2404
+ payload: {
2405
+ before,
2406
+ after,
2407
+ refs: {
2408
+ use_count: after.use_count,
2409
+ },
2410
+ },
2411
+ created_at: usedAt,
2412
+ });
2413
+ }
2414
+ },
2415
+ async insertPlaybookRevision(input) {
2416
+ const playbook = await this.getPlaybookById(input.playbook_id);
2417
+ if (!playbook) {
2418
+ throw new Error(`Playbook ${input.playbook_id} not found`);
2419
+ }
2420
+ const { rows } = await pool.query(`INSERT INTO playbook_revisions (tenant_id, system_id, workspace_id, collaboration_id, scope_id, playbook_id, instructions, revision_reason, source_session_id, created_at)
2421
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
2422
+ RETURNING *`, [playbook.tenant_id, playbook.system_id, playbook.workspace_id, playbook.collaboration_id, playbook.scope_id,
2423
+ input.playbook_id, input.instructions, input.revision_reason,
2424
+ input.source_session_id ?? null, input.created_at ?? now()]);
2425
+ await pool.query('UPDATE playbooks SET revision_count = revision_count + 1 WHERE id = $1', [input.playbook_id]);
2426
+ const revision = mapPlaybookRevision(rows[0]);
2427
+ await insertMemoryEventInternal({
2428
+ ...normalizeScope(revision),
2429
+ session_id: revision.source_session_id,
2430
+ entity_kind: 'playbook_revision',
2431
+ entity_id: String(revision.id),
2432
+ event_type: 'playbook.revised',
2433
+ payload: {
2434
+ after: revision,
2435
+ refs: {
2436
+ playbook_id: revision.playbook_id,
2437
+ },
2438
+ },
2439
+ created_at: revision.created_at,
2440
+ });
2441
+ return revision;
2442
+ },
2443
+ async getPlaybookRevisions(playbookId) {
2444
+ const { rows } = await pool.query('SELECT * FROM playbook_revisions WHERE playbook_id = $1 ORDER BY created_at DESC', [playbookId]);
2445
+ return rows.map(mapPlaybookRevision);
2446
+ },
2447
+ async insertAssociation(input) {
2448
+ const n = normalizeScope(input);
2449
+ try {
2450
+ const { rows } = await pool.query(`INSERT INTO associations
2451
+ (tenant_id, system_id, workspace_id, collaboration_id, scope_id,
2452
+ source_kind, source_id, target_kind, target_id, association_type, confidence, auto_generated, created_at)
2453
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
2454
+ RETURNING *`, [n.tenant_id, n.system_id, n.workspace_id, n.collaboration_id, n.scope_id,
2455
+ input.source_kind, input.source_id, input.target_kind, input.target_id,
2456
+ input.association_type, input.confidence ?? 0.5, input.auto_generated ?? false,
2457
+ input.created_at ?? now()]);
2458
+ const association = mapAssociation(rows[0]);
2459
+ await insertMemoryEventInternal({
2460
+ ...n,
2461
+ entity_kind: 'association',
2462
+ entity_id: String(association.id),
2463
+ event_type: 'association.created',
2464
+ payload: {
2465
+ after: association,
2466
+ },
2467
+ created_at: association.created_at,
2468
+ });
2469
+ return association;
2470
+ }
2471
+ catch (err) {
2472
+ // Postgres unique_violation is SQLSTATE 23505.
2473
+ if (err && typeof err === 'object' && err.code === '23505') {
2474
+ throw new UniqueConstraintError(`Association already exists: ${input.source_kind}:${input.source_id} -> ${input.target_kind}:${input.target_id} (${input.association_type})`, err);
2475
+ }
2476
+ throw err;
2477
+ }
2478
+ },
2479
+ async getAssociationById(id) {
2480
+ const { rows } = await pool.query('SELECT * FROM associations WHERE id = $1', [id]);
2481
+ if (rows.length === 0)
2482
+ return null;
2483
+ return mapAssociation(rows[0]);
2484
+ },
2485
+ async getAssociationsFrom(kind, id, scope) {
2486
+ const n = normalizeScope(scope);
2487
+ const { rows } = await pool.query(`SELECT * FROM associations WHERE source_kind = $1 AND source_id = $2
2488
+ AND tenant_id = $3 AND system_id = $4 AND workspace_id = $5 AND collaboration_id = $6 AND scope_id = $7
2489
+ ORDER BY id DESC`, [kind, id, n.tenant_id, n.system_id, n.workspace_id, n.collaboration_id, n.scope_id]);
2490
+ return rows.map(mapAssociation);
2491
+ },
2492
+ async getAssociationsTo(kind, id, scope) {
2493
+ const n = normalizeScope(scope);
2494
+ const { rows } = await pool.query(`SELECT * FROM associations WHERE target_kind = $1 AND target_id = $2
2495
+ AND tenant_id = $3 AND system_id = $4 AND workspace_id = $5 AND collaboration_id = $6 AND scope_id = $7
2496
+ ORDER BY id DESC`, [kind, id, n.tenant_id, n.system_id, n.workspace_id, n.collaboration_id, n.scope_id]);
2497
+ return rows.map(mapAssociation);
2498
+ },
2499
+ async listAssociations(scope) {
2500
+ const n = normalizeScope(scope);
2501
+ const { rows } = await pool.query(`SELECT * FROM associations
2502
+ WHERE tenant_id = $1 AND system_id = $2 AND workspace_id = $3 AND collaboration_id = $4 AND scope_id = $5
2503
+ ORDER BY id DESC`, [n.tenant_id, n.system_id, n.workspace_id, n.collaboration_id, n.scope_id]);
2504
+ return rows.map(mapAssociation);
2505
+ },
2506
+ async deleteAssociation(id) {
2507
+ const before = await this.getAssociationById(id);
2508
+ await pool.query('DELETE FROM associations WHERE id = $1', [id]);
2509
+ if (before) {
2510
+ await insertMemoryEventInternal({
2511
+ ...normalizeScope(before),
2512
+ entity_kind: 'association',
2513
+ entity_id: String(before.id),
2514
+ event_type: 'association.deleted',
2515
+ payload: {
2516
+ before,
2517
+ },
2518
+ created_at: now(),
2519
+ });
2520
+ }
2521
+ },
2522
+ async insertMemoryEvent(input) {
2523
+ return insertMemoryEventInternal(input);
2524
+ },
2525
+ async listMemoryEvents(scope, query) {
2526
+ return listScopedMemoryEvents(scope, query);
2527
+ },
2528
+ async listMemoryEventsCrossScope(scope, level, query) {
2529
+ return listScopedMemoryEventsCrossScope(scope, level, query);
2530
+ },
2531
+ async getMemoryEventsByEntity(scope, entityKind, entityId, query) {
2532
+ return listScopedMemoryEvents(scope, {
2533
+ ...query,
2534
+ entityKind,
2535
+ entityId,
2536
+ });
2537
+ },
2538
+ async getMemoryEventsBySession(scope, sessionId, query) {
2539
+ return listScopedMemoryEvents(scope, {
2540
+ ...query,
2541
+ sessionId,
2542
+ });
2543
+ },
2544
+ async getSessionState(scope, sessionId) {
2545
+ return readSessionStateProjection(scope, sessionId);
2546
+ },
2547
+ async upsertSessionState(input) {
2548
+ const projection = await writeSessionStateProjection(input);
2549
+ await insertMemoryEventInternal({
2550
+ ...normalizeScope(projection),
2551
+ session_id: projection.session_id,
2552
+ entity_kind: 'session_state',
2553
+ entity_id: projection.session_id,
2554
+ event_type: 'session_state.updated',
2555
+ payload: {
2556
+ after: projection,
2557
+ },
2558
+ created_at: projection.updatedAt,
2559
+ });
2560
+ return projection;
2561
+ },
2562
+ async getTemporalWatermark(projectionName) {
2563
+ return readTemporalWatermark(projectionName);
2564
+ },
2565
+ async upsertTemporalWatermark(input) {
2566
+ return writeTemporalWatermark(input);
2567
+ },
853
2568
  async transaction(fn) {
854
2569
  const existing = txStorage.getStore();
855
2570
  if (existing) {
@@ -861,8 +2576,12 @@ export function createPostgresAdapter(pool, options) {
861
2576
  return result;
862
2577
  }
863
2578
  catch (error) {
864
- await existing.client.query(`ROLLBACK TO SAVEPOINT ${savepoint}`);
865
- await existing.client.query(`RELEASE SAVEPOINT ${savepoint}`);
2579
+ try {
2580
+ await existing.client.query(`ROLLBACK TO SAVEPOINT ${savepoint}`);
2581
+ }
2582
+ finally {
2583
+ await existing.client.query(`RELEASE SAVEPOINT ${savepoint}`).catch(() => undefined);
2584
+ }
866
2585
  throw error;
867
2586
  }
868
2587
  }
@@ -875,7 +2594,7 @@ export function createPostgresAdapter(pool, options) {
875
2594
  return result;
876
2595
  }
877
2596
  catch (error) {
878
- await client.query('ROLLBACK');
2597
+ await client.query('ROLLBACK').catch(() => undefined);
879
2598
  throw error;
880
2599
  }
881
2600
  finally {
@@ -883,7 +2602,9 @@ export function createPostgresAdapter(pool, options) {
883
2602
  }
884
2603
  },
885
2604
  async close() {
886
- await pool.end();
2605
+ if (options?.ownsPool !== false) {
2606
+ await rootPool.end();
2607
+ }
887
2608
  },
888
2609
  };
889
2610
  }
@@ -972,10 +2693,17 @@ export function createPostgresEmbeddingAdapter(pool, options) {
972
2693
  similarity: Number(row.similarity),
973
2694
  }));
974
2695
  },
975
- async deleteEmbedding(knowledgeMemoryId) {
976
- await pool.query('DELETE FROM knowledge_embeddings WHERE knowledge_memory_id = $1', [
977
- knowledgeMemoryId,
978
- ]);
2696
+ async deleteEmbedding(knowledgeMemoryId, scope) {
2697
+ if (scope) {
2698
+ await pool.query(`DELETE FROM knowledge_embeddings ke
2699
+ USING knowledge_memory km
2700
+ WHERE ke.knowledge_memory_id = $1
2701
+ AND km.id = ke.knowledge_memory_id
2702
+ AND km.id = $1
2703
+ AND ${scopeWhere('km')}`, [knowledgeMemoryId, ...scopeParams(scope)]);
2704
+ return;
2705
+ }
2706
+ await pool.query('DELETE FROM knowledge_embeddings WHERE knowledge_memory_id = $1', [knowledgeMemoryId]);
979
2707
  },
980
2708
  };
981
2709
  }