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