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