ai-memory-layer 2.0.1 → 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 +4 -2
package/dist/core/manager.js
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
|
+
import { matchesScope, normalizeScope } from '../contracts/identity.js';
|
|
2
|
+
import { ProviderUnavailableError, ResourceNotFoundError, ScopeMismatchError, ValidationError, } from '../contracts/errors.js';
|
|
1
3
|
import { DEFAULT_CONTEXT_POLICY, DEFAULT_MONITOR_POLICY, } from '../contracts/policy.js';
|
|
2
|
-
import { buildMemoryContext } from './context.js';
|
|
4
|
+
import { buildDerivedShortTermState, buildMemoryContext, getContextWorkItems, resolveContextScopeLevel, resolveVisibleHandoffs, resolveVisibleKnowledge, resolveVisiblePlaybooks, resolveVisibleWorkClaims, resolveVisibleWorkItems, } from './context.js';
|
|
3
5
|
import { compactTurns, extractKnowledge, } from './orchestrator.js';
|
|
4
6
|
import { assessContext } from './monitor.js';
|
|
5
7
|
import { runMaintenance } from './maintenance.js';
|
|
6
8
|
import { emitMemoryEvent } from './telemetry.js';
|
|
7
9
|
import { wrapSyncAdapter } from '../adapters/sync-to-async.js';
|
|
8
10
|
import { estimateTokens } from './tokens.js';
|
|
9
|
-
import { createCircuitBreaker } from './circuit-breaker.js';
|
|
11
|
+
import { createCircuitBreaker, } from './circuit-breaker.js';
|
|
10
12
|
import { DEFAULT_EXTRACTION_POLICY } from '../contracts/policy.js';
|
|
11
13
|
import { assessKnowledgeReverification } from './trust.js';
|
|
12
14
|
import { matchesKnowledgeSearchOptions, rankKnowledge } from './retrieval.js';
|
|
13
15
|
import { computeNextReverificationAt, getDueReverificationKnowledge, resolveMaintenancePolicy, } from './knowledge-lifecycle.js';
|
|
14
|
-
import {
|
|
16
|
+
import { compareTemporalIds, normalizeTemporalId } from '../contracts/temporal.js';
|
|
17
|
+
import { searchEpisodes, summarizeEpisode, reflect } from './episodic.js';
|
|
18
|
+
import { searchCognitive } from './cognitive.js';
|
|
19
|
+
import { traverseAssociations } from './associations.js';
|
|
20
|
+
import { buildProfileFromKnowledge, getProfile } from './profile.js';
|
|
21
|
+
import { createPlaybookFromTask, revisePlaybook, findRelevantPlaybooks, } from './playbook.js';
|
|
22
|
+
import { createTemporalReplayAdapter, foldTemporalState, listAllMemoryEvents, listAllMemoryEventsBounded, listAllMemoryEventsCrossScope, normalizeReplayedTemporalState, normalizeHandoffAt, normalizeWorkClaimAt, } from './temporal.js';
|
|
15
23
|
function resolveAdapter(config) {
|
|
16
24
|
if (config.asyncAdapter) {
|
|
17
25
|
return config.asyncAdapter;
|
|
@@ -19,7 +27,7 @@ function resolveAdapter(config) {
|
|
|
19
27
|
if (config.adapter) {
|
|
20
28
|
return wrapSyncAdapter(config.adapter);
|
|
21
29
|
}
|
|
22
|
-
throw new
|
|
30
|
+
throw new ValidationError("MemoryManagerConfig requires either 'adapter' or 'asyncAdapter'");
|
|
23
31
|
}
|
|
24
32
|
function manualKnowledgeClassForFactType(factType) {
|
|
25
33
|
switch (factType) {
|
|
@@ -35,6 +43,79 @@ function manualKnowledgeClassForFactType(factType) {
|
|
|
35
43
|
return 'project_fact';
|
|
36
44
|
}
|
|
37
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve an association endpoint (source or target) and verify it exists
|
|
48
|
+
* and belongs to the caller's normalized scope. Throws a descriptive error
|
|
49
|
+
* if the node is missing or cross-scope. This is the sole authority on
|
|
50
|
+
* association ID validity; HTTP/MCP layers should NOT rely on their own
|
|
51
|
+
* type checks for scope safety.
|
|
52
|
+
*/
|
|
53
|
+
async function assertAssociationEndpointInScope(adapter, norm, kind, id, role) {
|
|
54
|
+
const scopedMatch = (record) => record.tenant_id === norm.tenant_id &&
|
|
55
|
+
record.system_id === norm.system_id &&
|
|
56
|
+
record.workspace_id === norm.workspace_id &&
|
|
57
|
+
record.collaboration_id === norm.collaboration_id &&
|
|
58
|
+
record.scope_id === norm.scope_id;
|
|
59
|
+
if (kind === 'knowledge') {
|
|
60
|
+
const km = await adapter.getKnowledgeMemoryById(id);
|
|
61
|
+
if (!km) {
|
|
62
|
+
throw new ResourceNotFoundError(`addAssociation: ${role} knowledge ${id} does not exist`);
|
|
63
|
+
}
|
|
64
|
+
if (!scopedMatch(km)) {
|
|
65
|
+
throw new ScopeMismatchError(`addAssociation: ${role} knowledge ${id} is not in the current scope`);
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (kind === 'playbook') {
|
|
70
|
+
const pb = await adapter.getPlaybookById(id);
|
|
71
|
+
if (!pb) {
|
|
72
|
+
throw new ResourceNotFoundError(`addAssociation: ${role} playbook ${id} does not exist`);
|
|
73
|
+
}
|
|
74
|
+
if (!scopedMatch(pb)) {
|
|
75
|
+
throw new ScopeMismatchError(`addAssociation: ${role} playbook ${id} is not in the current scope`);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (kind === 'working_memory') {
|
|
80
|
+
const wm = await adapter.getWorkingMemoryById(id);
|
|
81
|
+
if (!wm) {
|
|
82
|
+
throw new ResourceNotFoundError(`addAssociation: ${role} working_memory ${id} does not exist`);
|
|
83
|
+
}
|
|
84
|
+
if (!scopedMatch(wm)) {
|
|
85
|
+
throw new ScopeMismatchError(`addAssociation: ${role} working_memory ${id} is not in the current scope`);
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (kind === 'work_item') {
|
|
90
|
+
const match = await adapter.getWorkItemById(id);
|
|
91
|
+
if (!match) {
|
|
92
|
+
throw new ResourceNotFoundError(`addAssociation: ${role} work_item ${id} does not exist in the current scope`);
|
|
93
|
+
}
|
|
94
|
+
if (match.tenant_id !== norm.tenant_id ||
|
|
95
|
+
match.system_id !== norm.system_id ||
|
|
96
|
+
match.workspace_id !== norm.workspace_id ||
|
|
97
|
+
match.collaboration_id !== norm.collaboration_id ||
|
|
98
|
+
match.scope_id !== norm.scope_id) {
|
|
99
|
+
throw new ScopeMismatchError(`addAssociation: ${role} work_item ${id} does not exist in the current scope`);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Exhaustiveness: AssociationTargetKind has no other members.
|
|
104
|
+
throw new ValidationError(`addAssociation: unknown ${role} kind '${kind}'`);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Merge archived and active turns by id, preserving order by turn id.
|
|
108
|
+
* Partially compacted sessions have both sets; summarizing from only one
|
|
109
|
+
* drops context, so callers should always pass the union through this.
|
|
110
|
+
*/
|
|
111
|
+
function mergeTurnsById(archived, active) {
|
|
112
|
+
const byId = new Map();
|
|
113
|
+
for (const t of archived)
|
|
114
|
+
byId.set(t.id, t);
|
|
115
|
+
for (const t of active)
|
|
116
|
+
byId.set(t.id, t);
|
|
117
|
+
return Array.from(byId.values()).sort((a, b) => a.id - b.id);
|
|
118
|
+
}
|
|
38
119
|
function knowledgeMatchesScope(knowledge, scope) {
|
|
39
120
|
const normalized = normalizeScope(scope);
|
|
40
121
|
return (knowledge.tenant_id === normalized.tenant_id &&
|
|
@@ -43,6 +124,9 @@ function knowledgeMatchesScope(knowledge, scope) {
|
|
|
43
124
|
knowledge.collaboration_id === normalized.collaboration_id &&
|
|
44
125
|
knowledge.scope_id === normalized.scope_id);
|
|
45
126
|
}
|
|
127
|
+
function delay(ms) {
|
|
128
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
129
|
+
}
|
|
46
130
|
export function createMemoryManager(config) {
|
|
47
131
|
const asyncAdapter = resolveAdapter(config);
|
|
48
132
|
const autoCompact = config.autoCompact ?? true;
|
|
@@ -87,6 +171,14 @@ export function createMemoryManager(config) {
|
|
|
87
171
|
...detail,
|
|
88
172
|
});
|
|
89
173
|
}
|
|
174
|
+
function emitRetrievalFallback(reason, detail = {}) {
|
|
175
|
+
emitMemoryEvent('manager', config.scope, { logger: config.logger, onEvent }, 0, {
|
|
176
|
+
action: 'retrieval_fallback',
|
|
177
|
+
reason,
|
|
178
|
+
strategy: 'lexical_only',
|
|
179
|
+
...detail,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
90
182
|
async function withFailurePolicy(kind, run, fallback) {
|
|
91
183
|
const strategy = config.failurePolicy?.[kind] ??
|
|
92
184
|
(kind === 'extractor' ? 'disable_auto_extract' : 'throw');
|
|
@@ -138,7 +230,13 @@ export function createMemoryManager(config) {
|
|
|
138
230
|
});
|
|
139
231
|
}
|
|
140
232
|
async function buildQueryVector(input) {
|
|
141
|
-
if (
|
|
233
|
+
if (input.trim().length === 0) {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
if (!config.embeddingGenerator) {
|
|
237
|
+
emitRetrievalFallback('embedding_generator_unavailable', {
|
|
238
|
+
stage: 'query_vector',
|
|
239
|
+
});
|
|
142
240
|
return undefined;
|
|
143
241
|
}
|
|
144
242
|
try {
|
|
@@ -153,6 +251,10 @@ export function createMemoryManager(config) {
|
|
|
153
251
|
stage: 'query_vector',
|
|
154
252
|
error: String(error),
|
|
155
253
|
});
|
|
254
|
+
emitRetrievalFallback('query_vector_unavailable', {
|
|
255
|
+
stage: 'query_vector',
|
|
256
|
+
error: String(error),
|
|
257
|
+
});
|
|
156
258
|
return undefined;
|
|
157
259
|
}
|
|
158
260
|
}
|
|
@@ -198,10 +300,18 @@ export function createMemoryManager(config) {
|
|
|
198
300
|
: await asyncAdapter.searchKnowledgeCrossScope(config.scope, level, query, options);
|
|
199
301
|
const filteredLexical = lexical.filter((result) => matchesKnowledgeSearchOptions(result.item, options));
|
|
200
302
|
if (!config.embeddingAdapter) {
|
|
303
|
+
emitRetrievalFallback('embedding_adapter_unavailable', {
|
|
304
|
+
stage: 'semantic_search',
|
|
305
|
+
scopeLevel: level,
|
|
306
|
+
});
|
|
201
307
|
return filteredLexical;
|
|
202
308
|
}
|
|
203
309
|
const queryVector = await buildQueryVector(query);
|
|
204
310
|
if (!queryVector) {
|
|
311
|
+
emitRetrievalFallback('query_vector_unavailable', {
|
|
312
|
+
stage: 'semantic_search',
|
|
313
|
+
scopeLevel: level,
|
|
314
|
+
});
|
|
205
315
|
return filteredLexical;
|
|
206
316
|
}
|
|
207
317
|
let semantic;
|
|
@@ -227,6 +337,11 @@ export function createMemoryManager(config) {
|
|
|
227
337
|
error: String(error),
|
|
228
338
|
scopeLevel: level,
|
|
229
339
|
});
|
|
340
|
+
emitRetrievalFallback('semantic_search_failed', {
|
|
341
|
+
stage: 'semantic_search',
|
|
342
|
+
error: String(error),
|
|
343
|
+
scopeLevel: level,
|
|
344
|
+
});
|
|
230
345
|
return filteredLexical;
|
|
231
346
|
}
|
|
232
347
|
const lexicalRanks = new Map();
|
|
@@ -267,16 +382,17 @@ export function createMemoryManager(config) {
|
|
|
267
382
|
.sort((a, b) => b.rank - a.rank || b.item.last_accessed_at - a.item.last_accessed_at)
|
|
268
383
|
.slice(0, options?.limit ?? 10);
|
|
269
384
|
if (config.contextPolicy?.touchSelectedKnowledge ?? true) {
|
|
270
|
-
|
|
271
|
-
await asyncAdapter.touchKnowledgeMemory(result.item.id);
|
|
272
|
-
}
|
|
385
|
+
await asyncAdapter.touchKnowledgeMemories(results.map((result) => result.item.id));
|
|
273
386
|
}
|
|
274
387
|
return results;
|
|
275
388
|
}
|
|
276
|
-
async function getContextInternal(relevanceQuery, asOf) {
|
|
389
|
+
async function getContextInternal(relevanceQuery, asOf, options) {
|
|
277
390
|
const activeTurns = await asyncAdapter.getActiveTurns(config.scope, config.sessionId);
|
|
391
|
+
const relevantTurns = asOf == null
|
|
392
|
+
? activeTurns
|
|
393
|
+
: activeTurns.filter((turn) => turn.created_at <= asOf);
|
|
278
394
|
const queryVector = await buildQueryVector(relevanceQuery ??
|
|
279
|
-
|
|
395
|
+
relevantTurns
|
|
280
396
|
.slice(-4)
|
|
281
397
|
.map((turn) => turn.content)
|
|
282
398
|
.join('\n'));
|
|
@@ -289,10 +405,192 @@ export function createMemoryManager(config) {
|
|
|
289
405
|
policy: config.contextPolicy,
|
|
290
406
|
tokenEstimator,
|
|
291
407
|
asOf,
|
|
408
|
+
view: options?.view,
|
|
409
|
+
viewer: options?.viewer,
|
|
410
|
+
includeCoordinationState: options?.includeCoordinationState,
|
|
292
411
|
logger: config.logger,
|
|
293
412
|
onEvent,
|
|
294
413
|
});
|
|
295
414
|
}
|
|
415
|
+
async function refreshSessionStateProjection() {
|
|
416
|
+
try {
|
|
417
|
+
const [activeTurns, workingMemoryCandidates, contextWorkItems, watermark] = await Promise.all([
|
|
418
|
+
asyncAdapter.getActiveTurns(config.scope, config.sessionId),
|
|
419
|
+
asyncAdapter.getActiveWorkingMemory(config.scope, config.sessionId),
|
|
420
|
+
// Session-state projection stays scope-local even when wider
|
|
421
|
+
// retrieval is configured; it is a fast resume artifact, not a full
|
|
422
|
+
// cross-scope context assembly.
|
|
423
|
+
getContextWorkItems(asyncAdapter, config.scope, undefined, 'scope'),
|
|
424
|
+
asyncAdapter.getTemporalWatermark('temporal'),
|
|
425
|
+
]);
|
|
426
|
+
const shortTermState = buildDerivedShortTermState({
|
|
427
|
+
activeTurns,
|
|
428
|
+
workingMemoryCandidates,
|
|
429
|
+
contextWorkItems,
|
|
430
|
+
maxRecentSummaries: config.contextPolicy?.maxRecentSummaries ??
|
|
431
|
+
DEFAULT_CONTEXT_POLICY.maxRecentSummaries,
|
|
432
|
+
});
|
|
433
|
+
await asyncAdapter.upsertSessionState({
|
|
434
|
+
...normalizeScope(config.scope),
|
|
435
|
+
session_id: config.sessionId,
|
|
436
|
+
...shortTermState.sessionState,
|
|
437
|
+
source_event_id: watermark?.last_event_id ?? null,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
config.logger?.warn?.('memory.session_state_projection_refresh_failed', {
|
|
442
|
+
error: String(error),
|
|
443
|
+
sessionId: config.sessionId,
|
|
444
|
+
});
|
|
445
|
+
emitMemoryEvent('manager', config.scope, { logger: config.logger, onEvent }, 0, {
|
|
446
|
+
action: 'session_state_projection_refresh_failed',
|
|
447
|
+
sessionId: config.sessionId,
|
|
448
|
+
error: String(error),
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async function collectBestEffortTemporalState(asOf, options) {
|
|
453
|
+
const view = options?.view;
|
|
454
|
+
const effectiveScopeLevel = resolveContextScopeLevel(config.crossScopeLevel, view);
|
|
455
|
+
const [turns, workingMemory, knowledge, contextWorkItems, playbooks, associations, rawWorkClaims, rawHandoffs,] = await Promise.all([
|
|
456
|
+
asyncAdapter.getTurnsByTimeRange(config.scope, { end_at: asOf }),
|
|
457
|
+
asyncAdapter.getWorkingMemoryByTimeRange(config.scope, { end_at: asOf }),
|
|
458
|
+
effectiveScopeLevel && effectiveScopeLevel !== 'scope'
|
|
459
|
+
? asyncAdapter.getActiveKnowledgeCrossScope(config.scope, effectiveScopeLevel)
|
|
460
|
+
: asyncAdapter.getActiveKnowledgeMemory(config.scope),
|
|
461
|
+
getContextWorkItems(asyncAdapter, config.scope, asOf, effectiveScopeLevel),
|
|
462
|
+
effectiveScopeLevel && effectiveScopeLevel !== 'scope'
|
|
463
|
+
? asyncAdapter.getActivePlaybooksCrossScope(config.scope, effectiveScopeLevel)
|
|
464
|
+
: asyncAdapter.getActivePlaybooks(config.scope),
|
|
465
|
+
asyncAdapter.listAssociations(config.scope),
|
|
466
|
+
effectiveScopeLevel && effectiveScopeLevel !== 'scope'
|
|
467
|
+
? asyncAdapter.listWorkClaimsCrossScope(config.scope, effectiveScopeLevel, {
|
|
468
|
+
includeExpired: true,
|
|
469
|
+
includeReleased: true,
|
|
470
|
+
})
|
|
471
|
+
: asyncAdapter.listWorkClaims(config.scope, {
|
|
472
|
+
includeExpired: true,
|
|
473
|
+
includeReleased: true,
|
|
474
|
+
}),
|
|
475
|
+
effectiveScopeLevel && effectiveScopeLevel !== 'scope'
|
|
476
|
+
? asyncAdapter.listHandoffsCrossScope(config.scope, effectiveScopeLevel)
|
|
477
|
+
: asyncAdapter.listHandoffs(config.scope),
|
|
478
|
+
]);
|
|
479
|
+
const visibleKnowledge = (view
|
|
480
|
+
? resolveVisibleKnowledge(knowledge.filter((item) => item.created_at <= asOf), config.scope, view)
|
|
481
|
+
: knowledge.filter((item) => item.created_at <= asOf)).sort((a, b) => a.created_at - b.created_at || a.id - b.id);
|
|
482
|
+
const visibleWorkItems = (view ? resolveVisibleWorkItems(contextWorkItems, config.scope, view) : contextWorkItems).sort((a, b) => a.updated_at - b.updated_at || a.created_at - b.created_at || a.id - b.id);
|
|
483
|
+
const visiblePlaybooks = (view
|
|
484
|
+
? resolveVisiblePlaybooks(playbooks.filter((item) => item.created_at <= asOf), config.scope, view)
|
|
485
|
+
: playbooks.filter((item) => item.created_at <= asOf)).sort((a, b) => a.updated_at - b.updated_at || a.created_at - b.created_at || a.id - b.id);
|
|
486
|
+
const visibleWorkClaims = (view
|
|
487
|
+
? resolveVisibleWorkClaims(rawWorkClaims.map((claim) => normalizeWorkClaimAt(claim, asOf)), config.scope, view)
|
|
488
|
+
: rawWorkClaims.map((claim) => normalizeWorkClaimAt(claim, asOf))).sort((a, b) => a.claimed_at - b.claimed_at || a.id - b.id);
|
|
489
|
+
const visibleHandoffs = (view
|
|
490
|
+
? resolveVisibleHandoffs(rawHandoffs.map((handoff) => normalizeHandoffAt(handoff, asOf)), config.scope, view)
|
|
491
|
+
: rawHandoffs.map((handoff) => normalizeHandoffAt(handoff, asOf))).sort((a, b) => a.created_at - b.created_at || a.id - b.id);
|
|
492
|
+
return {
|
|
493
|
+
turns: turns
|
|
494
|
+
.filter((turn) => turn.session_id === config.sessionId)
|
|
495
|
+
.sort((a, b) => a.created_at - b.created_at || a.id - b.id),
|
|
496
|
+
workingMemory: workingMemory
|
|
497
|
+
.filter((item) => item.session_id === config.sessionId)
|
|
498
|
+
.sort((a, b) => a.created_at - b.created_at || a.id - b.id),
|
|
499
|
+
knowledge: visibleKnowledge,
|
|
500
|
+
workItems: visibleWorkItems,
|
|
501
|
+
workClaims: visibleWorkClaims,
|
|
502
|
+
handoffs: visibleHandoffs,
|
|
503
|
+
associations: associations
|
|
504
|
+
.filter((association) => association.created_at <= asOf)
|
|
505
|
+
.sort((a, b) => a.created_at - b.created_at || a.id - b.id),
|
|
506
|
+
playbooks: visiblePlaybooks,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
async function getTemporalCutoverAt() {
|
|
510
|
+
const watermark = await asyncAdapter.getTemporalWatermark('temporal');
|
|
511
|
+
return watermark?.cutover_at ?? null;
|
|
512
|
+
}
|
|
513
|
+
async function resolveChangeStreamCursorInternal(cursor) {
|
|
514
|
+
if (cursor != null)
|
|
515
|
+
return normalizeTemporalId(cursor);
|
|
516
|
+
return (await asyncAdapter.getTemporalWatermark('temporal'))?.last_event_id ?? '0';
|
|
517
|
+
}
|
|
518
|
+
async function buildReplayedContext(asOf, relevanceQuery, options, replayCutoff) {
|
|
519
|
+
const cutoverAt = await getTemporalCutoverAt();
|
|
520
|
+
if (cutoverAt == null || asOf < cutoverAt) {
|
|
521
|
+
return {
|
|
522
|
+
// Pre-cutover replay is best-effort only. The historical filters still
|
|
523
|
+
// apply, but semantic retrieval may consult the live embedding index
|
|
524
|
+
// because that index has no temporal dimension before the cutover.
|
|
525
|
+
context: await getContextInternal(relevanceQuery, asOf, options),
|
|
526
|
+
events: [],
|
|
527
|
+
state: null,
|
|
528
|
+
watermarkEventId: null,
|
|
529
|
+
exact: false,
|
|
530
|
+
cutoverAt,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
const replayScopeLevel = resolveContextScopeLevel(config.crossScopeLevel, options?.view);
|
|
534
|
+
const events = replayScopeLevel != null && replayScopeLevel !== 'scope'
|
|
535
|
+
? await listAllMemoryEventsCrossScope(asyncAdapter, config.scope, replayScopeLevel, {
|
|
536
|
+
endAt: asOf,
|
|
537
|
+
limit: 500,
|
|
538
|
+
})
|
|
539
|
+
: await listAllMemoryEvents(asyncAdapter, config.scope, {
|
|
540
|
+
endAt: asOf,
|
|
541
|
+
limit: 500,
|
|
542
|
+
});
|
|
543
|
+
const filteredEvents = replayCutoff?.throughEventId != null
|
|
544
|
+
? events.filter((event) => compareTemporalIds(event.event_id, replayCutoff.throughEventId) <= 0)
|
|
545
|
+
: events;
|
|
546
|
+
const replayed = normalizeReplayedTemporalState(foldTemporalState(filteredEvents, { sessionId: config.sessionId }), asOf);
|
|
547
|
+
const replayAdapter = createTemporalReplayAdapter(replayed, asOf);
|
|
548
|
+
const inferredRelevanceQuery = replayed.turns
|
|
549
|
+
.slice(-4)
|
|
550
|
+
.map((turn) => turn.content)
|
|
551
|
+
.join('\n')
|
|
552
|
+
.trim();
|
|
553
|
+
const resolvedRelevanceQuery = relevanceQuery ?? (inferredRelevanceQuery || undefined);
|
|
554
|
+
// Exact replay stays on the replay adapter only, so no live semantic data
|
|
555
|
+
// can leak into the assembled historical context after temporal cutover.
|
|
556
|
+
const context = await buildMemoryContext(replayAdapter, config.scope, {
|
|
557
|
+
sessionId: config.sessionId,
|
|
558
|
+
relevanceQuery: resolvedRelevanceQuery,
|
|
559
|
+
crossScopeLevel: config.crossScopeLevel,
|
|
560
|
+
policy: config.contextPolicy,
|
|
561
|
+
tokenEstimator,
|
|
562
|
+
view: options?.view,
|
|
563
|
+
viewer: options?.viewer,
|
|
564
|
+
includeCoordinationState: options?.includeCoordinationState,
|
|
565
|
+
logger: config.logger,
|
|
566
|
+
onEvent,
|
|
567
|
+
});
|
|
568
|
+
return {
|
|
569
|
+
context,
|
|
570
|
+
events: filteredEvents,
|
|
571
|
+
state: replayed,
|
|
572
|
+
watermarkEventId: replayCutoff?.throughEventId ?? replayed.watermarkEventId,
|
|
573
|
+
exact: true,
|
|
574
|
+
cutoverAt,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
function buildSessionBootstrapPayload(context, profile) {
|
|
578
|
+
return {
|
|
579
|
+
currentObjective: context.currentObjective,
|
|
580
|
+
sessionState: context.sessionState,
|
|
581
|
+
workingMemory: context.workingMemory,
|
|
582
|
+
relevantKnowledge: context.relevantKnowledge,
|
|
583
|
+
recentSummaries: context.recentSummaries,
|
|
584
|
+
activeObjectives: context.activeObjectives,
|
|
585
|
+
unresolvedWork: context.unresolvedWork,
|
|
586
|
+
coordinationState: context.coordinationState,
|
|
587
|
+
profile,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
async function getHistoricalProfileAt(asOf) {
|
|
591
|
+
const bestEffort = await collectBestEffortTemporalState(asOf);
|
|
592
|
+
return buildProfileFromKnowledge(bestEffort.knowledge.filter((item) => matchesScope(item, config.scope)));
|
|
593
|
+
}
|
|
296
594
|
async function executeCompaction(turns, trigger, retainedTurnCount, score) {
|
|
297
595
|
await persistMonitorState('compacting', score, turns);
|
|
298
596
|
const result = await withFailurePolicy('summarizer', () => compactTurns(asyncAdapter, config.scope, config.sessionId, turns, config.summarizer, trigger, retainedTurnCount, { logger: config.logger, onEvent }), () => null);
|
|
@@ -316,6 +614,7 @@ export function createMemoryManager(config) {
|
|
|
316
614
|
await maybeEmbedKnowledge(extracted);
|
|
317
615
|
extracted.forEach((knowledge) => emitKnowledgeChange('promoted', knowledge));
|
|
318
616
|
}
|
|
617
|
+
await refreshSessionStateProjection();
|
|
319
618
|
return result;
|
|
320
619
|
}
|
|
321
620
|
async function runCompaction(turns) {
|
|
@@ -342,9 +641,9 @@ export function createMemoryManager(config) {
|
|
|
342
641
|
}
|
|
343
642
|
return executeCompaction(turns, report.recommendation.action, Math.max(0, Math.min(report.recommendation.post_compaction_target_turns, turns.length - 1)), report.score_breakdown.total);
|
|
344
643
|
}
|
|
345
|
-
async function
|
|
644
|
+
async function insertManagedTurnRecord(role, content, actor) {
|
|
346
645
|
const redactedContent = config.redactText ? config.redactText({ kind: 'turn', text: content }) : content;
|
|
347
|
-
|
|
646
|
+
return asyncAdapter.insertTurn({
|
|
348
647
|
...config.scope,
|
|
349
648
|
session_id: config.sessionId,
|
|
350
649
|
actor,
|
|
@@ -352,6 +651,9 @@ export function createMemoryManager(config) {
|
|
|
352
651
|
content: redactedContent,
|
|
353
652
|
token_estimate: tokenEstimator(redactedContent),
|
|
354
653
|
});
|
|
654
|
+
}
|
|
655
|
+
async function insertManagedTurn(role, content, actor) {
|
|
656
|
+
const turn = await insertManagedTurnRecord(role, content, actor);
|
|
355
657
|
emitMemoryEvent('manager', config.scope, { logger: config.logger, onEvent }, 0, {
|
|
356
658
|
action: 'process_turn',
|
|
357
659
|
role,
|
|
@@ -366,35 +668,175 @@ export function createMemoryManager(config) {
|
|
|
366
668
|
const activeTurns = await asyncAdapter.getActiveTurns(config.scope, config.sessionId);
|
|
367
669
|
await runCompaction(activeTurns);
|
|
368
670
|
}
|
|
671
|
+
await refreshSessionStateProjection();
|
|
369
672
|
return turn;
|
|
370
673
|
},
|
|
371
674
|
async processExchange(userContent, assistantContent, actors) {
|
|
372
|
-
const userTurn = await
|
|
373
|
-
|
|
675
|
+
const [userTurn, assistantTurn] = await asyncAdapter.transaction(async () => {
|
|
676
|
+
const createdUserTurn = await insertManagedTurnRecord('user', userContent, actors?.user ?? 'user');
|
|
677
|
+
const createdAssistantTurn = await insertManagedTurnRecord('assistant', assistantContent, actors?.assistant ?? 'assistant');
|
|
678
|
+
return [createdUserTurn, createdAssistantTurn];
|
|
679
|
+
});
|
|
680
|
+
emitMemoryEvent('manager', config.scope, { logger: config.logger, onEvent }, 0, {
|
|
681
|
+
action: 'process_turn',
|
|
682
|
+
role: 'user',
|
|
683
|
+
turnId: userTurn.id,
|
|
684
|
+
});
|
|
685
|
+
emitMemoryEvent('manager', config.scope, { logger: config.logger, onEvent }, 0, {
|
|
686
|
+
action: 'process_turn',
|
|
687
|
+
role: 'assistant',
|
|
688
|
+
turnId: assistantTurn.id,
|
|
689
|
+
});
|
|
374
690
|
const compactionResult = autoCompact
|
|
375
691
|
? await runCompaction(await asyncAdapter.getActiveTurns(config.scope, config.sessionId))
|
|
376
692
|
: null;
|
|
693
|
+
await refreshSessionStateProjection();
|
|
377
694
|
return {
|
|
378
695
|
userTurn,
|
|
379
696
|
assistantTurn,
|
|
380
697
|
compactionResult,
|
|
381
698
|
};
|
|
382
699
|
},
|
|
383
|
-
async getContext(relevanceQuery) {
|
|
384
|
-
return getContextInternal(relevanceQuery);
|
|
700
|
+
async getContext(relevanceQuery, options) {
|
|
701
|
+
return getContextInternal(relevanceQuery, undefined, options);
|
|
702
|
+
},
|
|
703
|
+
async getContextAt(asOf, relevanceQuery, options) {
|
|
704
|
+
return (await buildReplayedContext(asOf, relevanceQuery, options)).context;
|
|
705
|
+
},
|
|
706
|
+
async getStateAt(asOf, options) {
|
|
707
|
+
const replay = await buildReplayedContext(asOf, options?.relevanceQuery, options);
|
|
708
|
+
const replayed = replay.exact
|
|
709
|
+
? replay.state
|
|
710
|
+
: await collectBestEffortTemporalState(asOf, options);
|
|
711
|
+
return {
|
|
712
|
+
asOf,
|
|
713
|
+
exact: replay.exact,
|
|
714
|
+
cutoverAt: replay.cutoverAt,
|
|
715
|
+
watermarkEventId: replay.watermarkEventId,
|
|
716
|
+
context: replay.context,
|
|
717
|
+
sessionState: replay.context.sessionState,
|
|
718
|
+
turns: replayed.turns,
|
|
719
|
+
workingMemory: replayed.workingMemory,
|
|
720
|
+
knowledge: replayed.knowledge,
|
|
721
|
+
workItems: replayed.workItems,
|
|
722
|
+
workClaims: replayed.workClaims,
|
|
723
|
+
handoffs: replayed.handoffs,
|
|
724
|
+
coordinationState: replay.context.coordinationState,
|
|
725
|
+
associations: replayed.associations,
|
|
726
|
+
playbooks: replayed.playbooks,
|
|
727
|
+
};
|
|
728
|
+
},
|
|
729
|
+
async getTimeline(options) {
|
|
730
|
+
return asyncAdapter.listMemoryEvents(config.scope, {
|
|
731
|
+
sessionId: options?.sessionId,
|
|
732
|
+
entityKind: options?.entityKind,
|
|
733
|
+
entityId: options?.entityId,
|
|
734
|
+
startAt: options?.startAt,
|
|
735
|
+
endAt: options?.endAt,
|
|
736
|
+
limit: options?.limit,
|
|
737
|
+
cursor: options?.cursor,
|
|
738
|
+
});
|
|
385
739
|
},
|
|
386
|
-
async
|
|
387
|
-
|
|
740
|
+
async diffState(from, to, options) {
|
|
741
|
+
const cutoverAt = await getTemporalCutoverAt();
|
|
742
|
+
const eventQuery = {
|
|
743
|
+
sessionId: options?.sessionId,
|
|
744
|
+
entityKind: options?.entityKind,
|
|
745
|
+
entityId: options?.entityId,
|
|
746
|
+
startAt: from + 1,
|
|
747
|
+
endAt: to,
|
|
748
|
+
limit: 500,
|
|
749
|
+
};
|
|
750
|
+
const events = options?.maxEvents != null
|
|
751
|
+
? await listAllMemoryEventsBounded(asyncAdapter, config.scope, options.maxEvents, eventQuery)
|
|
752
|
+
: await listAllMemoryEvents(asyncAdapter, config.scope, eventQuery);
|
|
753
|
+
const byEntityKind = {};
|
|
754
|
+
const byEventType = {};
|
|
755
|
+
for (const event of events) {
|
|
756
|
+
byEntityKind[event.entity_kind] = (byEntityKind[event.entity_kind] ?? 0) + 1;
|
|
757
|
+
byEventType[event.event_type] = (byEventType[event.event_type] ?? 0) + 1;
|
|
758
|
+
}
|
|
759
|
+
return {
|
|
760
|
+
from,
|
|
761
|
+
to,
|
|
762
|
+
exact: cutoverAt != null && from >= cutoverAt && to >= cutoverAt,
|
|
763
|
+
cutoverAt,
|
|
764
|
+
watermarkRange: {
|
|
765
|
+
fromEventId: events[0]?.event_id ?? null,
|
|
766
|
+
toEventId: events[events.length - 1]?.event_id ?? null,
|
|
767
|
+
},
|
|
768
|
+
events,
|
|
769
|
+
summary: {
|
|
770
|
+
totalEvents: events.length,
|
|
771
|
+
byEntityKind,
|
|
772
|
+
byEventType,
|
|
773
|
+
},
|
|
774
|
+
};
|
|
388
775
|
},
|
|
389
|
-
async
|
|
390
|
-
const
|
|
776
|
+
async listMemoryEvents(options) {
|
|
777
|
+
const timeline = await asyncAdapter.listMemoryEvents(config.scope, {
|
|
778
|
+
sessionId: options?.sessionId,
|
|
779
|
+
entityKind: options?.entityKind,
|
|
780
|
+
entityId: options?.entityId,
|
|
781
|
+
startAt: options?.startAt,
|
|
782
|
+
endAt: options?.endAt,
|
|
783
|
+
limit: options?.limit,
|
|
784
|
+
cursor: options?.cursor,
|
|
785
|
+
});
|
|
391
786
|
return {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
787
|
+
events: [...timeline.events].reverse(),
|
|
788
|
+
nextCursor: timeline.nextCursor,
|
|
789
|
+
};
|
|
790
|
+
},
|
|
791
|
+
async getSessionBootstrap(relevanceQuery, options) {
|
|
792
|
+
const context = await getContextInternal(relevanceQuery, undefined, options);
|
|
793
|
+
const profile = await getProfile(asyncAdapter, config.scope);
|
|
794
|
+
return buildSessionBootstrapPayload(context, profile);
|
|
795
|
+
},
|
|
796
|
+
async getSessionBootstrapAt(asOf, relevanceQuery, options) {
|
|
797
|
+
const replay = await buildReplayedContext(asOf, relevanceQuery, options);
|
|
798
|
+
const profile = replay.exact && replay.state
|
|
799
|
+
? buildProfileFromKnowledge(replay.state.knowledge.filter((item) => matchesScope(item, config.scope)))
|
|
800
|
+
: await getHistoricalProfileAt(asOf);
|
|
801
|
+
return buildSessionBootstrapPayload(replay.context, profile);
|
|
802
|
+
},
|
|
803
|
+
async captureSnapshot(relevanceQuery, options) {
|
|
804
|
+
const frozenAt = Math.floor(Date.now() / 1000);
|
|
805
|
+
const watermark = await asyncAdapter.getTemporalWatermark('temporal');
|
|
806
|
+
if (!watermark || watermark.last_event_id === '0') {
|
|
807
|
+
const [context, profile] = await Promise.all([
|
|
808
|
+
getContextInternal(relevanceQuery, undefined, options),
|
|
809
|
+
getProfile(asyncAdapter, config.scope),
|
|
810
|
+
]);
|
|
811
|
+
return {
|
|
812
|
+
bootstrap: buildSessionBootstrapPayload(context, profile),
|
|
813
|
+
context,
|
|
814
|
+
frozenAt,
|
|
815
|
+
watermarkEventId: null,
|
|
816
|
+
profile,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
const replay = await buildReplayedContext(watermark.updated_at, relevanceQuery, options, {
|
|
820
|
+
throughEventId: watermark.last_event_id,
|
|
821
|
+
});
|
|
822
|
+
const profile = replay.exact && replay.state
|
|
823
|
+
? buildProfileFromKnowledge(replay.state.knowledge.filter((item) => matchesScope(item, config.scope)))
|
|
824
|
+
: await getHistoricalProfileAt(watermark.updated_at);
|
|
825
|
+
return {
|
|
826
|
+
bootstrap: buildSessionBootstrapPayload(replay.context, profile),
|
|
827
|
+
context: replay.context,
|
|
828
|
+
frozenAt,
|
|
829
|
+
watermarkEventId: replay.exact ? watermark.last_event_id : null,
|
|
830
|
+
profile,
|
|
831
|
+
};
|
|
832
|
+
},
|
|
833
|
+
async getRuntimeDiagnostics() {
|
|
834
|
+
return {
|
|
835
|
+
circuitBreakers: {
|
|
836
|
+
summarizer: circuitBreakers.summarizer.getSnapshot(),
|
|
837
|
+
extractor: circuitBreakers.extractor.getSnapshot(),
|
|
838
|
+
embeddings: circuitBreakers.embeddings.getSnapshot(),
|
|
839
|
+
},
|
|
398
840
|
};
|
|
399
841
|
},
|
|
400
842
|
async recall(timeRange) {
|
|
@@ -461,10 +903,11 @@ export function createMemoryManager(config) {
|
|
|
461
903
|
emitKnowledgeChange('learned', knowledge);
|
|
462
904
|
return knowledge;
|
|
463
905
|
},
|
|
464
|
-
async trackWorkItem(title, kind = 'objective', status = 'open', detail) {
|
|
465
|
-
|
|
906
|
+
async trackWorkItem(title, kind = 'objective', status = 'open', detail, options) {
|
|
907
|
+
const workItem = await asyncAdapter.insertWorkItem({
|
|
466
908
|
...config.scope,
|
|
467
909
|
session_id: config.sessionId,
|
|
910
|
+
visibility_class: options?.visibilityClass ?? 'private',
|
|
468
911
|
title: config.redactText ? config.redactText({ kind: 'work_item', text: title }) : title,
|
|
469
912
|
kind,
|
|
470
913
|
status,
|
|
@@ -472,6 +915,88 @@ export function createMemoryManager(config) {
|
|
|
472
915
|
? config.redactText({ kind: 'work_item', text: detail })
|
|
473
916
|
: detail,
|
|
474
917
|
});
|
|
918
|
+
await refreshSessionStateProjection();
|
|
919
|
+
return workItem;
|
|
920
|
+
},
|
|
921
|
+
async updateWorkItem(id, patch, options) {
|
|
922
|
+
const workItem = await asyncAdapter.updateWorkItem(id, patch, options);
|
|
923
|
+
await refreshSessionStateProjection();
|
|
924
|
+
return workItem;
|
|
925
|
+
},
|
|
926
|
+
async claimWorkItem(input) {
|
|
927
|
+
const workItem = await asyncAdapter.getWorkItemById(input.workItemId);
|
|
928
|
+
return asyncAdapter.claimWorkItem({
|
|
929
|
+
...normalizeScope(config.scope),
|
|
930
|
+
work_item_id: input.workItemId,
|
|
931
|
+
actor: input.actor,
|
|
932
|
+
session_id: config.sessionId,
|
|
933
|
+
lease_seconds: input.leaseSeconds,
|
|
934
|
+
visibility_class: workItem?.visibility_class ?? 'private',
|
|
935
|
+
});
|
|
936
|
+
},
|
|
937
|
+
async renewWorkClaim(claimId, actor, leaseSeconds) {
|
|
938
|
+
return asyncAdapter.renewWorkClaim(claimId, actor, leaseSeconds);
|
|
939
|
+
},
|
|
940
|
+
async releaseWorkClaim(claimId, actor, reason) {
|
|
941
|
+
return asyncAdapter.releaseWorkClaim(claimId, actor, reason);
|
|
942
|
+
},
|
|
943
|
+
async listWorkClaims(options) {
|
|
944
|
+
return asyncAdapter.listWorkClaims(config.scope, {
|
|
945
|
+
actor: options?.actor,
|
|
946
|
+
sessionId: options?.sessionId,
|
|
947
|
+
});
|
|
948
|
+
},
|
|
949
|
+
async handoffWorkItem(input) {
|
|
950
|
+
const workItem = await asyncAdapter.getWorkItemById(input.workItemId);
|
|
951
|
+
return asyncAdapter.createHandoff({
|
|
952
|
+
...normalizeScope(config.scope),
|
|
953
|
+
work_item_id: input.workItemId,
|
|
954
|
+
from_actor: input.fromActor,
|
|
955
|
+
to_actor: input.toActor,
|
|
956
|
+
session_id: config.sessionId,
|
|
957
|
+
summary: input.summary,
|
|
958
|
+
context_bundle_ref: input.contextBundleRef ?? null,
|
|
959
|
+
expires_at: input.expiresAt ?? null,
|
|
960
|
+
visibility_class: workItem?.visibility_class ?? 'private',
|
|
961
|
+
});
|
|
962
|
+
},
|
|
963
|
+
async acceptHandoff(handoffId, actor, reason) {
|
|
964
|
+
return asyncAdapter.acceptHandoff(handoffId, actor, reason);
|
|
965
|
+
},
|
|
966
|
+
async rejectHandoff(handoffId, actor, reason) {
|
|
967
|
+
return asyncAdapter.rejectHandoff(handoffId, actor, reason);
|
|
968
|
+
},
|
|
969
|
+
async cancelHandoff(handoffId, actor, reason) {
|
|
970
|
+
return asyncAdapter.cancelHandoff(handoffId, actor, reason);
|
|
971
|
+
},
|
|
972
|
+
async listPendingHandoffs(options) {
|
|
973
|
+
return asyncAdapter.listHandoffs(config.scope, {
|
|
974
|
+
actor: options?.actor,
|
|
975
|
+
direction: options?.direction,
|
|
976
|
+
statuses: ['pending'],
|
|
977
|
+
});
|
|
978
|
+
},
|
|
979
|
+
async resolveChangeStreamCursor(cursor) {
|
|
980
|
+
return resolveChangeStreamCursorInternal(cursor);
|
|
981
|
+
},
|
|
982
|
+
async *streamChanges(options) {
|
|
983
|
+
let cursor = await resolveChangeStreamCursorInternal(options?.cursor);
|
|
984
|
+
while (!options?.signal?.aborted) {
|
|
985
|
+
const page = await asyncAdapter.listMemoryEvents(config.scope, {
|
|
986
|
+
cursor,
|
|
987
|
+
sessionId: options?.sessionId,
|
|
988
|
+
entityKind: options?.entityKind,
|
|
989
|
+
entityId: options?.entityId,
|
|
990
|
+
limit: 100,
|
|
991
|
+
});
|
|
992
|
+
for (const event of page.events) {
|
|
993
|
+
cursor = event.event_id;
|
|
994
|
+
yield event;
|
|
995
|
+
}
|
|
996
|
+
if (options?.signal?.aborted)
|
|
997
|
+
break;
|
|
998
|
+
await delay(options?.pollIntervalMs ?? 250);
|
|
999
|
+
}
|
|
475
1000
|
},
|
|
476
1001
|
async inspectKnowledge(id) {
|
|
477
1002
|
const knowledge = await asyncAdapter.getKnowledgeMemoryById(id);
|
|
@@ -506,10 +1031,10 @@ export function createMemoryManager(config) {
|
|
|
506
1031
|
async reverifyKnowledge(id) {
|
|
507
1032
|
const knowledge = await asyncAdapter.getKnowledgeMemoryById(id);
|
|
508
1033
|
if (!knowledge) {
|
|
509
|
-
throw new
|
|
1034
|
+
throw new ResourceNotFoundError(`Memory validation: knowledge memory ${id} was not found`);
|
|
510
1035
|
}
|
|
511
1036
|
if (!knowledgeMatchesScope(knowledge, config.scope)) {
|
|
512
|
-
throw new
|
|
1037
|
+
throw new ScopeMismatchError(`Memory validation: knowledge memory ${id} does not belong to the requested scope`);
|
|
513
1038
|
}
|
|
514
1039
|
const evidence = await asyncAdapter.listKnowledgeEvidenceForKnowledge(id);
|
|
515
1040
|
const policy = {
|
|
@@ -627,13 +1152,197 @@ export function createMemoryManager(config) {
|
|
|
627
1152
|
expiredWorkingMemoryCount: report.expiredWorkingMemoryIds.length,
|
|
628
1153
|
retiredKnowledgeCount: report.retiredKnowledgeIds.length,
|
|
629
1154
|
deletedWorkItemCount: report.deletedWorkItemIds.length,
|
|
1155
|
+
deletedAssociationCount: report.deletedAssociationIds.length,
|
|
630
1156
|
reverifiedKnowledgeCount: report.reverifiedKnowledgeIds.length,
|
|
631
1157
|
demotedKnowledgeCount: report.demotedKnowledgeIds.length,
|
|
632
1158
|
});
|
|
1159
|
+
await refreshSessionStateProjection();
|
|
633
1160
|
return report;
|
|
634
1161
|
},
|
|
1162
|
+
async searchEpisodes(options) {
|
|
1163
|
+
if (!config.structuredClient) {
|
|
1164
|
+
throw new ProviderUnavailableError('searchEpisodes requires a structuredClient in MemoryManagerConfig');
|
|
1165
|
+
}
|
|
1166
|
+
return searchEpisodes({
|
|
1167
|
+
adapter: asyncAdapter,
|
|
1168
|
+
scope: config.scope,
|
|
1169
|
+
client: config.structuredClient,
|
|
1170
|
+
telemetry: { logger: config.logger, onEvent },
|
|
1171
|
+
}, options);
|
|
1172
|
+
},
|
|
1173
|
+
async summarizeEpisode(sessionId, options) {
|
|
1174
|
+
if (!config.structuredClient) {
|
|
1175
|
+
throw new ProviderUnavailableError('summarizeEpisode requires a structuredClient in MemoryManagerConfig');
|
|
1176
|
+
}
|
|
1177
|
+
const detailLevel = options?.detailLevel ?? 'overview';
|
|
1178
|
+
// Fetch both active and all session working memories. Partially
|
|
1179
|
+
// compacted sessions have BOTH archived history (covered by working
|
|
1180
|
+
// memory turn ranges) and active turns; a recap built from only the
|
|
1181
|
+
// active fragment silently drops earlier context, so we always merge
|
|
1182
|
+
// archived + active and dedupe by turn id.
|
|
1183
|
+
const activeTurns = await asyncAdapter.getActiveTurns(config.scope, sessionId);
|
|
1184
|
+
const allSessionWm = await asyncAdapter.getWorkingMemoryBySession(sessionId, config.scope);
|
|
1185
|
+
let archivedTurns = [];
|
|
1186
|
+
if (allSessionWm.length > 0) {
|
|
1187
|
+
const minStart = Math.min(...allSessionWm.map((wm) => wm.turn_id_start));
|
|
1188
|
+
const maxEnd = Math.max(...allSessionWm.map((wm) => wm.turn_id_end));
|
|
1189
|
+
archivedTurns = await asyncAdapter.getArchivedTurnRange(sessionId, minStart, maxEnd, config.scope);
|
|
1190
|
+
}
|
|
1191
|
+
const turns = mergeTurnsById(archivedTurns, activeTurns);
|
|
1192
|
+
return summarizeEpisode({
|
|
1193
|
+
adapter: asyncAdapter,
|
|
1194
|
+
scope: config.scope,
|
|
1195
|
+
client: config.structuredClient,
|
|
1196
|
+
telemetry: { logger: config.logger, onEvent },
|
|
1197
|
+
}, { turns, workingMemories: allSessionWm, sessionId, detailLevel, client: config.structuredClient });
|
|
1198
|
+
},
|
|
1199
|
+
async reflect(options) {
|
|
1200
|
+
if (!config.structuredClient) {
|
|
1201
|
+
throw new ProviderUnavailableError('reflect requires a structuredClient in MemoryManagerConfig');
|
|
1202
|
+
}
|
|
1203
|
+
return reflect({
|
|
1204
|
+
adapter: asyncAdapter,
|
|
1205
|
+
scope: config.scope,
|
|
1206
|
+
client: config.structuredClient,
|
|
1207
|
+
telemetry: { logger: config.logger, onEvent },
|
|
1208
|
+
}, options);
|
|
1209
|
+
},
|
|
1210
|
+
async searchCognitive(options) {
|
|
1211
|
+
return searchCognitive(asyncAdapter, config.scope, options);
|
|
1212
|
+
},
|
|
1213
|
+
async getProfile(options) {
|
|
1214
|
+
return getProfile(asyncAdapter, config.scope, options);
|
|
1215
|
+
},
|
|
1216
|
+
async createPlaybook(input) {
|
|
1217
|
+
return asyncAdapter.insertPlaybook({ ...input, ...config.scope });
|
|
1218
|
+
},
|
|
1219
|
+
async createPlaybookFromTask(input) {
|
|
1220
|
+
if (!config.structuredClient) {
|
|
1221
|
+
throw new ProviderUnavailableError('createPlaybookFromTask requires a structuredClient in MemoryManagerConfig');
|
|
1222
|
+
}
|
|
1223
|
+
return createPlaybookFromTask({ adapter: asyncAdapter, scope: config.scope, client: config.structuredClient }, input);
|
|
1224
|
+
},
|
|
1225
|
+
async revisePlaybook(playbookId, newInstructions, revisionReason, sourceSessionId) {
|
|
1226
|
+
return revisePlaybook(asyncAdapter, config.scope, playbookId, newInstructions, revisionReason, sourceSessionId);
|
|
1227
|
+
},
|
|
1228
|
+
async getPlaybook(id) {
|
|
1229
|
+
const playbook = await asyncAdapter.getPlaybookById(id);
|
|
1230
|
+
if (!playbook)
|
|
1231
|
+
return null;
|
|
1232
|
+
const norm = normalizeScope(config.scope);
|
|
1233
|
+
if (playbook.tenant_id !== norm.tenant_id ||
|
|
1234
|
+
playbook.system_id !== norm.system_id ||
|
|
1235
|
+
playbook.workspace_id !== norm.workspace_id ||
|
|
1236
|
+
playbook.collaboration_id !== norm.collaboration_id ||
|
|
1237
|
+
playbook.scope_id !== norm.scope_id) {
|
|
1238
|
+
return null;
|
|
1239
|
+
}
|
|
1240
|
+
return playbook;
|
|
1241
|
+
},
|
|
1242
|
+
async listPlaybooks() {
|
|
1243
|
+
return asyncAdapter.getActivePlaybooks(config.scope);
|
|
1244
|
+
},
|
|
1245
|
+
async searchPlaybooks(query, options) {
|
|
1246
|
+
return findRelevantPlaybooks(asyncAdapter, config.scope, query, options);
|
|
1247
|
+
},
|
|
1248
|
+
async updatePlaybook(id, patch) {
|
|
1249
|
+
const playbook = await asyncAdapter.getPlaybookById(id);
|
|
1250
|
+
if (!playbook)
|
|
1251
|
+
return null;
|
|
1252
|
+
const norm = normalizeScope(config.scope);
|
|
1253
|
+
if (playbook.tenant_id !== norm.tenant_id ||
|
|
1254
|
+
playbook.system_id !== norm.system_id ||
|
|
1255
|
+
playbook.workspace_id !== norm.workspace_id ||
|
|
1256
|
+
playbook.collaboration_id !== norm.collaboration_id ||
|
|
1257
|
+
playbook.scope_id !== norm.scope_id) {
|
|
1258
|
+
return null;
|
|
1259
|
+
}
|
|
1260
|
+
return asyncAdapter.updatePlaybook(id, patch);
|
|
1261
|
+
},
|
|
1262
|
+
async recordPlaybookUse(id) {
|
|
1263
|
+
const playbook = await asyncAdapter.getPlaybookById(id);
|
|
1264
|
+
if (!playbook) {
|
|
1265
|
+
throw new ResourceNotFoundError(`Playbook ${id} not found`);
|
|
1266
|
+
}
|
|
1267
|
+
const norm = normalizeScope(config.scope);
|
|
1268
|
+
if (playbook.tenant_id !== norm.tenant_id ||
|
|
1269
|
+
playbook.system_id !== norm.system_id ||
|
|
1270
|
+
playbook.workspace_id !== norm.workspace_id ||
|
|
1271
|
+
playbook.collaboration_id !== norm.collaboration_id ||
|
|
1272
|
+
playbook.scope_id !== norm.scope_id) {
|
|
1273
|
+
throw new ScopeMismatchError(`Playbook ${id} does not belong to the requested scope`);
|
|
1274
|
+
}
|
|
1275
|
+
return asyncAdapter.recordPlaybookUse(id);
|
|
1276
|
+
},
|
|
1277
|
+
async addAssociation(input) {
|
|
1278
|
+
// Validate source/target IDs are positive integers. Callers (HTTP/MCP)
|
|
1279
|
+
// only check typeof number, so this is the authoritative guard.
|
|
1280
|
+
if (!Number.isInteger(input.source_id) || input.source_id <= 0) {
|
|
1281
|
+
throw new ValidationError(`addAssociation: source_id must be a positive integer, got ${input.source_id}`);
|
|
1282
|
+
}
|
|
1283
|
+
if (!Number.isInteger(input.target_id) || input.target_id <= 0) {
|
|
1284
|
+
throw new ValidationError(`addAssociation: target_id must be a positive integer, got ${input.target_id}`);
|
|
1285
|
+
}
|
|
1286
|
+
if (input.source_kind === input.target_kind && input.source_id === input.target_id) {
|
|
1287
|
+
throw new ValidationError('addAssociation: self-referential associations are not allowed');
|
|
1288
|
+
}
|
|
1289
|
+
// Validate confidence is in [0, 1] when provided.
|
|
1290
|
+
if (input.confidence !== undefined) {
|
|
1291
|
+
if (typeof input.confidence !== 'number' ||
|
|
1292
|
+
Number.isNaN(input.confidence) ||
|
|
1293
|
+
input.confidence < 0 ||
|
|
1294
|
+
input.confidence > 1) {
|
|
1295
|
+
throw new ValidationError(`addAssociation: confidence must be a number in [0, 1], got ${input.confidence}`);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
// Resolve source and target: both must exist and belong to the caller's
|
|
1299
|
+
// scope. Without this, callers can create orphaned or cross-scope edges,
|
|
1300
|
+
// polluting the graph and weakening isolation guarantees.
|
|
1301
|
+
const norm = normalizeScope(config.scope);
|
|
1302
|
+
await assertAssociationEndpointInScope(asyncAdapter, norm, input.source_kind, input.source_id, 'source');
|
|
1303
|
+
await assertAssociationEndpointInScope(asyncAdapter, norm, input.target_kind, input.target_id, 'target');
|
|
1304
|
+
return asyncAdapter.insertAssociation({
|
|
1305
|
+
...input,
|
|
1306
|
+
...norm,
|
|
1307
|
+
});
|
|
1308
|
+
},
|
|
1309
|
+
async getAssociations(kind, id) {
|
|
1310
|
+
const [from, to] = await Promise.all([
|
|
1311
|
+
asyncAdapter.getAssociationsFrom(kind, id, config.scope),
|
|
1312
|
+
asyncAdapter.getAssociationsTo(kind, id, config.scope),
|
|
1313
|
+
]);
|
|
1314
|
+
return { from, to };
|
|
1315
|
+
},
|
|
1316
|
+
async traverseAssociations(kind, id, options) {
|
|
1317
|
+
return traverseAssociations(asyncAdapter, config.scope, kind, id, options);
|
|
1318
|
+
},
|
|
1319
|
+
async removeAssociation(id) {
|
|
1320
|
+
// Scope safety: verify the association belongs to the current scope by
|
|
1321
|
+
// checking the association row's own scope columns. Scanning through
|
|
1322
|
+
// active knowledge/playbooks/WM/work items would incorrectly reject
|
|
1323
|
+
// associations attached to archived/expired/orphaned nodes, leaving
|
|
1324
|
+
// stale edges permanently in the graph.
|
|
1325
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
1326
|
+
throw new ValidationError(`removeAssociation: id must be a positive integer, got ${id}`);
|
|
1327
|
+
}
|
|
1328
|
+
const association = await asyncAdapter.getAssociationById(id);
|
|
1329
|
+
if (!association) {
|
|
1330
|
+
throw new ResourceNotFoundError(`Association ${id} not found`);
|
|
1331
|
+
}
|
|
1332
|
+
const norm = normalizeScope(config.scope);
|
|
1333
|
+
if (association.tenant_id !== norm.tenant_id ||
|
|
1334
|
+
association.system_id !== norm.system_id ||
|
|
1335
|
+
association.workspace_id !== norm.workspace_id ||
|
|
1336
|
+
association.collaboration_id !== norm.collaboration_id ||
|
|
1337
|
+
association.scope_id !== norm.scope_id) {
|
|
1338
|
+
throw new ScopeMismatchError(`Association ${id} not found in the current scope`);
|
|
1339
|
+
}
|
|
1340
|
+
await asyncAdapter.deleteAssociation(id);
|
|
1341
|
+
},
|
|
635
1342
|
async close() {
|
|
636
|
-
|
|
1343
|
+
if (config.closeAdapter !== false) {
|
|
1344
|
+
await asyncAdapter.close();
|
|
1345
|
+
}
|
|
637
1346
|
},
|
|
638
1347
|
};
|
|
639
1348
|
}
|