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.
- package/CHANGELOG.md +19 -12
- package/README.md +435 -320
- package/bin/memory-server.mjs +0 -0
- package/dist/adapters/memory/embeddings.d.ts.map +1 -1
- package/dist/adapters/memory/embeddings.js +12 -1
- package/dist/adapters/memory/embeddings.js.map +1 -1
- package/dist/adapters/memory/index.d.ts.map +1 -1
- package/dist/adapters/memory/index.js +1281 -48
- package/dist/adapters/memory/index.js.map +1 -1
- package/dist/adapters/postgres/index.d.ts +1 -0
- package/dist/adapters/postgres/index.d.ts.map +1 -1
- package/dist/adapters/postgres/index.js +1770 -42
- package/dist/adapters/postgres/index.js.map +1 -1
- package/dist/adapters/sqlite/embeddings.d.ts.map +1 -1
- package/dist/adapters/sqlite/embeddings.js +49 -12
- package/dist/adapters/sqlite/embeddings.js.map +1 -1
- package/dist/adapters/sqlite/index.d.ts.map +1 -1
- package/dist/adapters/sqlite/index.js +1720 -38
- package/dist/adapters/sqlite/index.js.map +1 -1
- package/dist/adapters/sqlite/mappers.d.ts +39 -4
- package/dist/adapters/sqlite/mappers.d.ts.map +1 -1
- package/dist/adapters/sqlite/mappers.js +87 -0
- package/dist/adapters/sqlite/mappers.js.map +1 -1
- package/dist/adapters/sqlite/schema.d.ts +1 -1
- package/dist/adapters/sqlite/schema.d.ts.map +1 -1
- package/dist/adapters/sqlite/schema.js +297 -1
- package/dist/adapters/sqlite/schema.js.map +1 -1
- package/dist/adapters/sync-to-async.d.ts.map +1 -1
- package/dist/adapters/sync-to-async.js +54 -0
- package/dist/adapters/sync-to-async.js.map +1 -1
- package/dist/contracts/async-storage.d.ts +61 -1
- package/dist/contracts/async-storage.d.ts.map +1 -1
- package/dist/contracts/cognitive.d.ts +37 -0
- package/dist/contracts/cognitive.d.ts.map +1 -0
- package/dist/contracts/cognitive.js +24 -0
- package/dist/contracts/cognitive.js.map +1 -0
- package/dist/contracts/coordination.d.ts +101 -0
- package/dist/contracts/coordination.d.ts.map +1 -0
- package/dist/contracts/coordination.js +26 -0
- package/dist/contracts/coordination.js.map +1 -0
- package/dist/contracts/embedding.d.ts +1 -1
- package/dist/contracts/embedding.d.ts.map +1 -1
- package/dist/contracts/errors.d.ts +28 -0
- package/dist/contracts/errors.d.ts.map +1 -0
- package/dist/contracts/errors.js +41 -0
- package/dist/contracts/errors.js.map +1 -0
- package/dist/contracts/identity.d.ts +2 -0
- package/dist/contracts/identity.d.ts.map +1 -1
- package/dist/contracts/identity.js +26 -1
- package/dist/contracts/identity.js.map +1 -1
- package/dist/contracts/observability.d.ts +2 -1
- package/dist/contracts/observability.d.ts.map +1 -1
- package/dist/contracts/observability.js +11 -0
- package/dist/contracts/observability.js.map +1 -1
- package/dist/contracts/profile.d.ts +29 -0
- package/dist/contracts/profile.d.ts.map +1 -0
- package/dist/contracts/profile.js +2 -0
- package/dist/contracts/profile.js.map +1 -0
- package/dist/contracts/session-state.d.ts +10 -0
- package/dist/contracts/session-state.d.ts.map +1 -0
- package/dist/contracts/session-state.js +2 -0
- package/dist/contracts/session-state.js.map +1 -0
- package/dist/contracts/storage.d.ts +73 -1
- package/dist/contracts/storage.d.ts.map +1 -1
- package/dist/contracts/storage.js +16 -1
- package/dist/contracts/storage.js.map +1 -1
- package/dist/contracts/temporal.d.ts +112 -0
- package/dist/contracts/temporal.d.ts.map +1 -0
- package/dist/contracts/temporal.js +31 -0
- package/dist/contracts/temporal.js.map +1 -0
- package/dist/contracts/types.d.ts +135 -0
- package/dist/contracts/types.d.ts.map +1 -1
- package/dist/contracts/types.js +27 -0
- package/dist/contracts/types.js.map +1 -1
- package/dist/core/associations.d.ts +18 -0
- package/dist/core/associations.d.ts.map +1 -0
- package/dist/core/associations.js +185 -0
- package/dist/core/associations.js.map +1 -0
- package/dist/core/circuit-breaker.d.ts +9 -0
- package/dist/core/circuit-breaker.d.ts.map +1 -1
- package/dist/core/circuit-breaker.js +13 -1
- package/dist/core/circuit-breaker.js.map +1 -1
- package/dist/core/cognitive.d.ts +5 -0
- package/dist/core/cognitive.d.ts.map +1 -0
- package/dist/core/cognitive.js +120 -0
- package/dist/core/cognitive.js.map +1 -0
- package/dist/core/context.d.ts +72 -1
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +471 -45
- package/dist/core/context.js.map +1 -1
- package/dist/core/episodic.d.ts +28 -0
- package/dist/core/episodic.d.ts.map +1 -0
- package/dist/core/episodic.js +371 -0
- package/dist/core/episodic.js.map +1 -0
- package/dist/core/formatter.d.ts +4 -0
- package/dist/core/formatter.d.ts.map +1 -1
- package/dist/core/formatter.js +103 -0
- package/dist/core/formatter.js.map +1 -1
- package/dist/core/maintenance.d.ts +1 -0
- package/dist/core/maintenance.d.ts.map +1 -1
- package/dist/core/maintenance.js +75 -0
- package/dist/core/maintenance.js.map +1 -1
- package/dist/core/manager.d.ts +159 -7
- package/dist/core/manager.d.ts.map +1 -1
- package/dist/core/manager.js +740 -31
- package/dist/core/manager.js.map +1 -1
- package/dist/core/orchestrator.d.ts.map +1 -1
- package/dist/core/orchestrator.js +210 -178
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/playbook.d.ts +35 -0
- package/dist/core/playbook.d.ts.map +1 -0
- package/dist/core/playbook.js +184 -0
- package/dist/core/playbook.js.map +1 -0
- package/dist/core/profile.d.ts +8 -0
- package/dist/core/profile.d.ts.map +1 -0
- package/dist/core/profile.js +103 -0
- package/dist/core/profile.js.map +1 -0
- package/dist/core/quick.d.ts +5 -0
- package/dist/core/quick.d.ts.map +1 -1
- package/dist/core/quick.js +10 -1
- package/dist/core/quick.js.map +1 -1
- package/dist/core/runtime.d.ts +17 -1
- package/dist/core/runtime.d.ts.map +1 -1
- package/dist/core/runtime.js +88 -5
- package/dist/core/runtime.js.map +1 -1
- package/dist/core/streaming.d.ts +1 -1
- package/dist/core/streaming.d.ts.map +1 -1
- package/dist/core/temporal.d.ts +29 -0
- package/dist/core/temporal.d.ts.map +1 -0
- package/dist/core/temporal.js +447 -0
- package/dist/core/temporal.js.map +1 -0
- package/dist/core/validation.d.ts +3 -0
- package/dist/core/validation.d.ts.map +1 -1
- package/dist/core/validation.js +25 -10
- package/dist/core/validation.js.map +1 -1
- package/dist/core/workspace-detect.d.ts +17 -0
- package/dist/core/workspace-detect.d.ts.map +1 -0
- package/dist/core/workspace-detect.js +55 -0
- package/dist/core/workspace-detect.js.map +1 -0
- package/dist/embeddings/resilience.d.ts.map +1 -1
- package/dist/embeddings/resilience.js +19 -8
- package/dist/embeddings/resilience.js.map +1 -1
- package/dist/index.d.ts +21 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/integrations/claude-agent.d.ts +6 -0
- package/dist/integrations/claude-agent.d.ts.map +1 -1
- package/dist/integrations/claude-agent.js +5 -1
- package/dist/integrations/claude-agent.js.map +1 -1
- package/dist/integrations/claude-tools.d.ts +5 -4
- package/dist/integrations/claude-tools.d.ts.map +1 -1
- package/dist/integrations/claude-tools.js +155 -2
- package/dist/integrations/claude-tools.js.map +1 -1
- package/dist/integrations/middleware.d.ts +6 -0
- package/dist/integrations/middleware.d.ts.map +1 -1
- package/dist/integrations/middleware.js +11 -1
- package/dist/integrations/middleware.js.map +1 -1
- package/dist/integrations/openai-tools.d.ts +5 -4
- package/dist/integrations/openai-tools.d.ts.map +1 -1
- package/dist/integrations/openai-tools.js +170 -2
- package/dist/integrations/openai-tools.js.map +1 -1
- package/dist/integrations/vercel-ai.d.ts +6 -0
- package/dist/integrations/vercel-ai.d.ts.map +1 -1
- package/dist/integrations/vercel-ai.js +4 -0
- package/dist/integrations/vercel-ai.js.map +1 -1
- package/dist/server/http-server.d.ts +8 -0
- package/dist/server/http-server.d.ts.map +1 -1
- package/dist/server/http-server.js +976 -58
- package/dist/server/http-server.js.map +1 -1
- package/dist/server/mcp-server.d.ts +8 -0
- package/dist/server/mcp-server.d.ts.map +1 -1
- package/dist/server/mcp-server.js +1157 -37
- package/dist/server/mcp-server.js.map +1 -1
- package/dist/server/parsing.d.ts +12 -0
- package/dist/server/parsing.d.ts.map +1 -0
- package/dist/server/parsing.js +42 -0
- package/dist/server/parsing.js.map +1 -0
- package/dist/summarizers/prompts.d.ts +4 -0
- package/dist/summarizers/prompts.d.ts.map +1 -1
- package/dist/summarizers/prompts.js +42 -0
- package/dist/summarizers/prompts.js.map +1 -1
- package/docs/ULTIMATE_MEMORY_LAYER_ROADMAP.md +291 -0
- package/docs/prd.json +1498 -0
- package/openapi.yaml +1945 -112
- 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
|
|
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.
|
|
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
|
|
327
|
-
|
|
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,
|
|
338
|
-
|
|
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', $
|
|
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', $
|
|
976
|
+
AND search_vector @@ plainto_tsquery('english', $6)
|
|
405
977
|
ORDER BY rank DESC
|
|
406
|
-
LIMIT $
|
|
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
|
|
436
|
-
|
|
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,
|
|
440
|
-
|
|
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
|
-
|
|
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
|
|
485
|
-
|
|
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.
|
|
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,
|
|
501
|
-
|
|
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', $
|
|
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', $
|
|
1339
|
+
AND search_vector @@ plainto_tsquery('english', $6)
|
|
673
1340
|
ORDER BY rank DESC
|
|
674
|
-
LIMIT $
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
791
|
-
|
|
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
|
-
|
|
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
|
-
|
|
865
|
-
|
|
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
|
-
|
|
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
|
-
|
|
977
|
-
|
|
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
|
}
|