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
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from 'crypto';
|
|
1
2
|
import { createServer } from 'http';
|
|
2
3
|
import { createMemoryWithAsyncAdapter, } from '../core/quick.js';
|
|
3
4
|
import { normalizeScope } from '../contracts/identity.js';
|
|
5
|
+
import { isMemoryDomainError } from '../contracts/errors.js';
|
|
6
|
+
import { MEMORY_EVENT_TYPES, } from '../contracts/observability.js';
|
|
7
|
+
import { EPISODE_DETAIL_LEVELS, PLAYBOOK_STATUSES, ASSOCIATION_TYPES, ASSOCIATION_TARGET_KINDS } from '../contracts/types.js';
|
|
8
|
+
import { ACTOR_KINDS, CONTEXT_VIEW_POLICIES, MEMORY_VISIBILITY_CLASSES } from '../contracts/coordination.js';
|
|
4
9
|
import { createSQLiteAdapterWithEmbeddings } from '../adapters/sqlite/index.js';
|
|
5
10
|
import { wrapSyncAdapter } from '../adapters/sync-to-async.js';
|
|
11
|
+
import { parseOptionalFiniteInteger, parseOptionalFiniteNumber, parseOptionalTemporalIdValue, } from './parsing.js';
|
|
6
12
|
class HttpRequestError extends Error {
|
|
7
13
|
status;
|
|
8
14
|
constructor(status, message) {
|
|
@@ -10,6 +16,40 @@ class HttpRequestError extends Error {
|
|
|
10
16
|
this.status = status;
|
|
11
17
|
}
|
|
12
18
|
}
|
|
19
|
+
const SESSION_SNAPSHOT_LIMIT = 1000;
|
|
20
|
+
const PER_SCOPE_SESSION_SNAPSHOT_LIMIT = 10;
|
|
21
|
+
const MANAGER_CACHE_LIMIT = 256;
|
|
22
|
+
const SESSION_MANAGER_CACHE_LIMIT = 256;
|
|
23
|
+
const MAX_LIST_LIMIT = 100;
|
|
24
|
+
const DEFAULT_DIFF_MAX_EVENTS = 5000;
|
|
25
|
+
const MAX_DIFF_MAX_EVENTS = 20000;
|
|
26
|
+
function resolveDiffEventCaps(defaultMaxEvents, maxMaxEvents) {
|
|
27
|
+
const resolvedMax = maxMaxEvents ?? MAX_DIFF_MAX_EVENTS;
|
|
28
|
+
const resolvedDefault = defaultMaxEvents ?? DEFAULT_DIFF_MAX_EVENTS;
|
|
29
|
+
if (!Number.isInteger(resolvedMax) || resolvedMax < 1) {
|
|
30
|
+
throw new Error('memory-layer: maxDiffMaxEvents must be a positive integer');
|
|
31
|
+
}
|
|
32
|
+
if (!Number.isInteger(resolvedDefault) || resolvedDefault < 1) {
|
|
33
|
+
throw new Error('memory-layer: defaultDiffMaxEvents must be a positive integer');
|
|
34
|
+
}
|
|
35
|
+
if (resolvedDefault > resolvedMax) {
|
|
36
|
+
throw new Error('memory-layer: defaultDiffMaxEvents must not exceed maxDiffMaxEvents');
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
defaultDiffMaxEvents: resolvedDefault,
|
|
40
|
+
maxDiffMaxEvents: resolvedMax,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function failHttpValidation(message) {
|
|
44
|
+
throw new HttpRequestError(400, message);
|
|
45
|
+
}
|
|
46
|
+
function safeSecretEquals(provided, expected) {
|
|
47
|
+
if (typeof provided !== 'string')
|
|
48
|
+
return false;
|
|
49
|
+
const providedBuffer = createHash('sha256').update(provided).digest();
|
|
50
|
+
const expectedBuffer = createHash('sha256').update(expected).digest();
|
|
51
|
+
return timingSafeEqual(providedBuffer, expectedBuffer) && provided.length === expected.length;
|
|
52
|
+
}
|
|
13
53
|
function writeJson(res, status, data) {
|
|
14
54
|
const body = JSON.stringify(data);
|
|
15
55
|
res.writeHead(status, {
|
|
@@ -21,6 +61,143 @@ function writeJson(res, status, data) {
|
|
|
21
61
|
function writeError(res, status, message) {
|
|
22
62
|
writeJson(res, status, { error: message });
|
|
23
63
|
}
|
|
64
|
+
function serializeContextResponse(context, options = {}) {
|
|
65
|
+
return {
|
|
66
|
+
currentObjective: context.currentObjective,
|
|
67
|
+
sessionState: context.sessionState,
|
|
68
|
+
activeTurnCount: context.activeTurns.length,
|
|
69
|
+
workingMemory: context.workingMemory
|
|
70
|
+
? {
|
|
71
|
+
summary: context.workingMemory.summary,
|
|
72
|
+
key_entities: context.workingMemory.key_entities,
|
|
73
|
+
topic_tags: context.workingMemory.topic_tags,
|
|
74
|
+
}
|
|
75
|
+
: null,
|
|
76
|
+
relevantKnowledge: context.relevantKnowledge.map((knowledge) => ({
|
|
77
|
+
id: knowledge.id,
|
|
78
|
+
fact: knowledge.fact,
|
|
79
|
+
fact_type: knowledge.fact_type,
|
|
80
|
+
confidence: knowledge.confidence,
|
|
81
|
+
})),
|
|
82
|
+
activeObjectives: context.activeObjectives.map((objective) => ({
|
|
83
|
+
id: objective.id,
|
|
84
|
+
title: objective.title,
|
|
85
|
+
status: objective.status,
|
|
86
|
+
visibility_class: objective.visibility_class,
|
|
87
|
+
})),
|
|
88
|
+
associatedKnowledge: options.includeAssociatedKnowledge === false
|
|
89
|
+
? undefined
|
|
90
|
+
: context.associatedKnowledge.map((knowledge) => ({
|
|
91
|
+
id: knowledge.id,
|
|
92
|
+
fact: knowledge.fact,
|
|
93
|
+
fact_type: knowledge.fact_type,
|
|
94
|
+
knowledge_class: knowledge.knowledge_class,
|
|
95
|
+
trust_score: knowledge.trust_score,
|
|
96
|
+
})),
|
|
97
|
+
unresolvedWork: context.unresolvedWork,
|
|
98
|
+
coordinationState: context.coordinationState
|
|
99
|
+
? {
|
|
100
|
+
ownedClaims: context.coordinationState.ownedClaims.map(serializeWorkClaim),
|
|
101
|
+
pendingInboundHandoffs: context.coordinationState.pendingInboundHandoffs.map(serializeHandoffRecord),
|
|
102
|
+
pendingOutboundHandoffs: context.coordinationState.pendingOutboundHandoffs.map(serializeHandoffRecord),
|
|
103
|
+
sharedWorkItems: context.coordinationState.sharedWorkItems.map((item) => ({
|
|
104
|
+
id: item.id,
|
|
105
|
+
title: item.title,
|
|
106
|
+
status: item.status,
|
|
107
|
+
visibility_class: item.visibility_class,
|
|
108
|
+
})),
|
|
109
|
+
}
|
|
110
|
+
: null,
|
|
111
|
+
tokenEstimate: context.tokenEstimate,
|
|
112
|
+
...(options.includeDebug
|
|
113
|
+
? {
|
|
114
|
+
debugTrace: context.debugTrace,
|
|
115
|
+
knowledgeSelectionReasons: context.knowledgeSelectionReasons,
|
|
116
|
+
}
|
|
117
|
+
: {}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function serializeActorRef(actor) {
|
|
121
|
+
return {
|
|
122
|
+
actor_kind: actor.actor_kind,
|
|
123
|
+
actor_id: actor.actor_id,
|
|
124
|
+
system_id: actor.system_id,
|
|
125
|
+
display_name: actor.display_name,
|
|
126
|
+
metadata: actor.metadata,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function serializeWorkClaim(claim) {
|
|
130
|
+
return {
|
|
131
|
+
id: claim.id,
|
|
132
|
+
work_item_id: claim.work_item_id,
|
|
133
|
+
actor: serializeActorRef(claim.actor),
|
|
134
|
+
session_id: claim.session_id,
|
|
135
|
+
claim_token: claim.claim_token,
|
|
136
|
+
status: claim.status,
|
|
137
|
+
claimed_at: claim.claimed_at,
|
|
138
|
+
expires_at: claim.expires_at,
|
|
139
|
+
released_at: claim.released_at,
|
|
140
|
+
release_reason: claim.release_reason,
|
|
141
|
+
source_event_id: claim.source_event_id,
|
|
142
|
+
visibility_class: claim.visibility_class,
|
|
143
|
+
version: claim.version,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function serializeHandoffRecord(handoff) {
|
|
147
|
+
return {
|
|
148
|
+
id: handoff.id,
|
|
149
|
+
work_item_id: handoff.work_item_id,
|
|
150
|
+
from_actor: serializeActorRef(handoff.from_actor),
|
|
151
|
+
to_actor: serializeActorRef(handoff.to_actor),
|
|
152
|
+
session_id: handoff.session_id,
|
|
153
|
+
summary: handoff.summary,
|
|
154
|
+
context_bundle_ref: handoff.context_bundle_ref,
|
|
155
|
+
status: handoff.status,
|
|
156
|
+
created_at: handoff.created_at,
|
|
157
|
+
accepted_at: handoff.accepted_at,
|
|
158
|
+
rejected_at: handoff.rejected_at,
|
|
159
|
+
canceled_at: handoff.canceled_at,
|
|
160
|
+
expires_at: handoff.expires_at,
|
|
161
|
+
decision_reason: handoff.decision_reason,
|
|
162
|
+
source_event_id: handoff.source_event_id,
|
|
163
|
+
visibility_class: handoff.visibility_class,
|
|
164
|
+
version: handoff.version,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function serializeTimelineResult(result) {
|
|
168
|
+
return {
|
|
169
|
+
events: result.events,
|
|
170
|
+
nextCursor: result.nextCursor,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function serializeTemporalState(state, options = {}) {
|
|
174
|
+
return {
|
|
175
|
+
asOf: state.asOf,
|
|
176
|
+
exact: state.exact,
|
|
177
|
+
cutoverAt: state.cutoverAt,
|
|
178
|
+
watermarkEventId: state.watermarkEventId,
|
|
179
|
+
context: serializeContextResponse(state.context, {
|
|
180
|
+
includeDebug: options.includeDebug,
|
|
181
|
+
}),
|
|
182
|
+
sessionState: state.sessionState,
|
|
183
|
+
turns: state.turns,
|
|
184
|
+
workingMemory: state.workingMemory,
|
|
185
|
+
knowledge: state.knowledge,
|
|
186
|
+
workItems: state.workItems,
|
|
187
|
+
workClaims: state.workClaims.map(serializeWorkClaim),
|
|
188
|
+
handoffs: state.handoffs.map(serializeHandoffRecord),
|
|
189
|
+
coordinationState: state.coordinationState
|
|
190
|
+
? {
|
|
191
|
+
ownedClaims: state.coordinationState.ownedClaims.map(serializeWorkClaim),
|
|
192
|
+
pendingInboundHandoffs: state.coordinationState.pendingInboundHandoffs.map(serializeHandoffRecord),
|
|
193
|
+
pendingOutboundHandoffs: state.coordinationState.pendingOutboundHandoffs.map(serializeHandoffRecord),
|
|
194
|
+
sharedWorkItems: state.coordinationState.sharedWorkItems,
|
|
195
|
+
}
|
|
196
|
+
: null,
|
|
197
|
+
associations: state.associations,
|
|
198
|
+
playbooks: state.playbooks,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
24
201
|
async function readBody(req, limitBytes = 1_048_576) {
|
|
25
202
|
return new Promise((resolve, reject) => {
|
|
26
203
|
const chunks = [];
|
|
@@ -80,6 +257,9 @@ function parseOptionalInteger(value) {
|
|
|
80
257
|
const parsed = Number(value);
|
|
81
258
|
return Number.isInteger(parsed) ? parsed : undefined;
|
|
82
259
|
}
|
|
260
|
+
function parseOptionalTemporalId(value) {
|
|
261
|
+
return parseOptionalTemporalIdValue(value, 'cursor', failHttpValidation);
|
|
262
|
+
}
|
|
83
263
|
function normalizePath(path) {
|
|
84
264
|
if (path.length > 1 && path.endsWith('/')) {
|
|
85
265
|
return path.slice(0, -1);
|
|
@@ -109,13 +289,51 @@ function requireEnum(value, allowed, name) {
|
|
|
109
289
|
}
|
|
110
290
|
return value;
|
|
111
291
|
}
|
|
292
|
+
function parseContextViewPolicy(value, name = 'view') {
|
|
293
|
+
return value ? requireEnum(value, CONTEXT_VIEW_POLICIES, name) : undefined;
|
|
294
|
+
}
|
|
295
|
+
function parseViewerFromQuery(query) {
|
|
296
|
+
if (query.viewer_actor_id == null && query.viewer_actor_kind == null)
|
|
297
|
+
return undefined;
|
|
298
|
+
return parseActorRef({
|
|
299
|
+
actor_kind: query.viewer_actor_kind,
|
|
300
|
+
actor_id: query.viewer_actor_id,
|
|
301
|
+
system_id: query.viewer_system_id,
|
|
302
|
+
display_name: query.viewer_display_name,
|
|
303
|
+
}, 'viewer');
|
|
304
|
+
}
|
|
305
|
+
function parseActorRef(value, name = 'actor') {
|
|
306
|
+
if (value == null)
|
|
307
|
+
return undefined;
|
|
308
|
+
if (!isRecord(value)) {
|
|
309
|
+
throw new HttpRequestError(400, `Invalid field: ${name}`);
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
actor_kind: requireEnum(value.actor_kind, ACTOR_KINDS, `${name}.actor_kind`),
|
|
313
|
+
actor_id: requireString(value.actor_id, `${name}.actor_id`),
|
|
314
|
+
system_id: value.system_id == null ? null : requireString(value.system_id, `${name}.system_id`),
|
|
315
|
+
display_name: value.display_name == null ? null : requireString(value.display_name, `${name}.display_name`),
|
|
316
|
+
metadata: isRecord(value.metadata) ? value.metadata : null,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
112
319
|
function parseLimit(value) {
|
|
113
320
|
const parsed = parseOptionalInteger(value);
|
|
114
321
|
if (value != null && parsed == null) {
|
|
115
322
|
throw new HttpRequestError(400, 'Invalid limit parameter');
|
|
116
323
|
}
|
|
324
|
+
if (parsed != null && parsed > MAX_LIST_LIMIT) {
|
|
325
|
+
throw new HttpRequestError(400, `Limit parameter exceeds maximum of ${MAX_LIST_LIMIT}`);
|
|
326
|
+
}
|
|
117
327
|
return parsed;
|
|
118
328
|
}
|
|
329
|
+
function parseOptionalNonNegativeInteger(value, name) {
|
|
330
|
+
if (value === undefined || value === null)
|
|
331
|
+
return undefined;
|
|
332
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
|
|
333
|
+
throw new HttpRequestError(400, `Invalid field: ${name} (must be a non-negative integer)`);
|
|
334
|
+
}
|
|
335
|
+
return value;
|
|
336
|
+
}
|
|
119
337
|
function parseScopeLevel(value, name, allowed = ['scope', 'workspace', 'system', 'tenant']) {
|
|
120
338
|
if (value == null || value === '')
|
|
121
339
|
return undefined;
|
|
@@ -141,21 +359,11 @@ function resolvePartialScope(source, labels) {
|
|
|
141
359
|
function parseEventTypes(value) {
|
|
142
360
|
if (!value)
|
|
143
361
|
return undefined;
|
|
144
|
-
const allowed = [
|
|
145
|
-
'manager',
|
|
146
|
-
'search',
|
|
147
|
-
'compaction',
|
|
148
|
-
'extraction',
|
|
149
|
-
'promotion',
|
|
150
|
-
'knowledge_change',
|
|
151
|
-
'context_assembly',
|
|
152
|
-
'semantic_search',
|
|
153
|
-
];
|
|
154
362
|
return new Set(value
|
|
155
363
|
.split(',')
|
|
156
364
|
.map((entry) => entry.trim())
|
|
157
365
|
.filter(Boolean)
|
|
158
|
-
.map((entry) => requireEnum(entry,
|
|
366
|
+
.map((entry) => requireEnum(entry, MEMORY_EVENT_TYPES, 'event_types')));
|
|
159
367
|
}
|
|
160
368
|
function resolveRequestScope(fallbackScope, req, query, body) {
|
|
161
369
|
const bodyScope = body?.scope;
|
|
@@ -190,6 +398,18 @@ function resolveRequestScope(fallbackScope, req, query, body) {
|
|
|
190
398
|
}
|
|
191
399
|
return fallbackScope ?? 'default';
|
|
192
400
|
}
|
|
401
|
+
function materializeScope(scopeInput) {
|
|
402
|
+
return typeof scopeInput === 'string'
|
|
403
|
+
? {
|
|
404
|
+
tenant_id: 'default',
|
|
405
|
+
system_id: 'default',
|
|
406
|
+
scope_id: scopeInput,
|
|
407
|
+
}
|
|
408
|
+
: scopeInput;
|
|
409
|
+
}
|
|
410
|
+
function cloneSnapshotValue(value) {
|
|
411
|
+
return structuredClone(value);
|
|
412
|
+
}
|
|
193
413
|
function matchesEventScope(event, scope, level) {
|
|
194
414
|
const left = normalizeScope(event.scope);
|
|
195
415
|
const right = normalizeScope(scope);
|
|
@@ -197,6 +417,9 @@ function matchesEventScope(event, scope, level) {
|
|
|
197
417
|
return false;
|
|
198
418
|
if (level === 'tenant')
|
|
199
419
|
return true;
|
|
420
|
+
// Collaboration scope is the explicit shared-memory boundary across systems,
|
|
421
|
+
// so workspace-level collaboration listeners intentionally fan out across
|
|
422
|
+
// system_id values when both sides are bound to the same collaboration.
|
|
200
423
|
if (level === 'workspace' && left.collaboration_id && right.collaboration_id) {
|
|
201
424
|
return left.collaboration_id === right.collaboration_id;
|
|
202
425
|
}
|
|
@@ -233,7 +456,49 @@ export async function startHttpServer(config = {}) {
|
|
|
233
456
|
const adminApiKey = config.adminApiKey ?? process.env.MEMORY_ADMIN_API_KEY;
|
|
234
457
|
const enableCors = config.cors ?? true;
|
|
235
458
|
const bodyLimitBytes = config.bodyLimitBytes ?? 1_048_576;
|
|
459
|
+
const { defaultDiffMaxEvents, maxDiffMaxEvents } = resolveDiffEventCaps(config.defaultDiffMaxEvents, config.maxDiffMaxEvents);
|
|
236
460
|
const managers = new Map();
|
|
461
|
+
// Managers keyed by (scope, sessionId) for snapshot endpoints that must
|
|
462
|
+
// honor the URL-path session instead of the scope's bound default session.
|
|
463
|
+
const sessionManagers = new Map();
|
|
464
|
+
const sessionSnapshots = new Map();
|
|
465
|
+
function touchManagerCache(cache, key, manager, limit) {
|
|
466
|
+
cache.delete(key);
|
|
467
|
+
cache.set(key, manager);
|
|
468
|
+
while (cache.size > limit) {
|
|
469
|
+
const oldestKey = cache.keys().next().value;
|
|
470
|
+
if (!oldestKey)
|
|
471
|
+
break;
|
|
472
|
+
cache.delete(oldestKey);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function touchSnapshot(key, snapshot) {
|
|
476
|
+
const cachedSnapshot = cloneSnapshotValue(snapshot);
|
|
477
|
+
sessionSnapshots.delete(key);
|
|
478
|
+
sessionSnapshots.set(key, cachedSnapshot);
|
|
479
|
+
const scopeEntries = [...sessionSnapshots.entries()].filter(([, value]) => value.scopeKey === cachedSnapshot.scopeKey);
|
|
480
|
+
while (scopeEntries.length > PER_SCOPE_SESSION_SNAPSHOT_LIMIT) {
|
|
481
|
+
const [oldestKey] = scopeEntries.shift() ?? [];
|
|
482
|
+
if (!oldestKey)
|
|
483
|
+
break;
|
|
484
|
+
sessionSnapshots.delete(oldestKey);
|
|
485
|
+
}
|
|
486
|
+
while (sessionSnapshots.size > SESSION_SNAPSHOT_LIMIT) {
|
|
487
|
+
const oldest = sessionSnapshots.keys().next().value;
|
|
488
|
+
if (oldest === undefined)
|
|
489
|
+
break;
|
|
490
|
+
sessionSnapshots.delete(oldest);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function readSnapshot(key) {
|
|
494
|
+
const snapshot = sessionSnapshots.get(key);
|
|
495
|
+
if (!snapshot)
|
|
496
|
+
return undefined;
|
|
497
|
+
// Refresh LRU recency
|
|
498
|
+
sessionSnapshots.delete(key);
|
|
499
|
+
sessionSnapshots.set(key, snapshot);
|
|
500
|
+
return cloneSnapshotValue(snapshot);
|
|
501
|
+
}
|
|
237
502
|
const databaseUrl = config.databaseUrl ?? process.env.MEMORY_DATABASE_URL;
|
|
238
503
|
const adapterResources = databaseUrl
|
|
239
504
|
? await (async () => {
|
|
@@ -244,7 +509,7 @@ export async function startHttpServer(config = {}) {
|
|
|
244
509
|
const { createPostgresAdapter, createPostgresEmbeddingAdapter } = await import('../adapters/postgres/index.js');
|
|
245
510
|
const Pool = pgModule.Pool ?? pgModule.default?.Pool;
|
|
246
511
|
const pool = new Pool({ connectionString: databaseUrl });
|
|
247
|
-
const asyncAdapter = createPostgresAdapter(pool);
|
|
512
|
+
const asyncAdapter = createPostgresAdapter(pool, { ownsPool: false });
|
|
248
513
|
return {
|
|
249
514
|
asyncAdapter,
|
|
250
515
|
embeddingAdapter: createPostgresEmbeddingAdapter(pool),
|
|
@@ -264,11 +529,10 @@ export async function startHttpServer(config = {}) {
|
|
|
264
529
|
};
|
|
265
530
|
})();
|
|
266
531
|
const sseClients = new Set();
|
|
267
|
-
function
|
|
268
|
-
|
|
269
|
-
adapter: 'sqlite',
|
|
270
|
-
path: config.dbPath ?? ':memory:',
|
|
532
|
+
function buildHostedManagerOptions(scopeInput, sessionId) {
|
|
533
|
+
return {
|
|
271
534
|
scope: scopeInput,
|
|
535
|
+
...(sessionId ? { sessionId } : {}),
|
|
272
536
|
summarizer: config.summarizer ?? 'extractive',
|
|
273
537
|
extractor: config.extractor ?? 'regex',
|
|
274
538
|
preset: config.preset,
|
|
@@ -276,6 +540,13 @@ export async function startHttpServer(config = {}) {
|
|
|
276
540
|
qualityMode: config.qualityMode,
|
|
277
541
|
qualityTier: config.qualityTier,
|
|
278
542
|
crossScopeLevel: config.crossScopeLevel,
|
|
543
|
+
autoDetectWorkspace: config.autoDetectWorkspace,
|
|
544
|
+
structuredClient: config.structuredClient,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function createHostedManager(scopeInput) {
|
|
548
|
+
const baseOptions = {
|
|
549
|
+
...buildHostedManagerOptions(scopeInput),
|
|
279
550
|
onEvent: (event) => {
|
|
280
551
|
if (sseClients.size === 0)
|
|
281
552
|
return;
|
|
@@ -295,6 +566,7 @@ export async function startHttpServer(config = {}) {
|
|
|
295
566
|
...baseOptions,
|
|
296
567
|
asyncAdapter: adapterResources.asyncAdapter,
|
|
297
568
|
embeddingAdapter: adapterResources.embeddingAdapter,
|
|
569
|
+
closeAdapter: false,
|
|
298
570
|
});
|
|
299
571
|
}
|
|
300
572
|
function getManager(scopeInput) {
|
|
@@ -303,10 +575,38 @@ export async function startHttpServer(config = {}) {
|
|
|
303
575
|
: JSON.stringify(normalizeScope(scopeInput));
|
|
304
576
|
const existing = managers.get(key);
|
|
305
577
|
if (existing) {
|
|
578
|
+
touchManagerCache(managers, key, existing, MANAGER_CACHE_LIMIT);
|
|
306
579
|
return existing;
|
|
307
580
|
}
|
|
308
581
|
const manager = createHostedManager(scopeInput);
|
|
309
|
-
managers
|
|
582
|
+
touchManagerCache(managers, key, manager, MANAGER_CACHE_LIMIT);
|
|
583
|
+
return manager;
|
|
584
|
+
}
|
|
585
|
+
function scopeKeyFor(scopeInput) {
|
|
586
|
+
return typeof scopeInput === 'string'
|
|
587
|
+
? `scope:${scopeInput}`
|
|
588
|
+
: JSON.stringify(normalizeScope(scopeInput));
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Get a manager bound to a specific sessionId under the given scope.
|
|
592
|
+
* Snapshot endpoints use this so POST/GET/REFRESH against different URL
|
|
593
|
+
* :sessionId values read from the correct session, not the scope's default.
|
|
594
|
+
*/
|
|
595
|
+
function getSessionManager(scopeInput, sessionId) {
|
|
596
|
+
const key = `${scopeKeyFor(scopeInput)}|session:${sessionId}`;
|
|
597
|
+
const existing = sessionManagers.get(key);
|
|
598
|
+
if (existing) {
|
|
599
|
+
touchManagerCache(sessionManagers, key, existing, SESSION_MANAGER_CACHE_LIMIT);
|
|
600
|
+
return existing;
|
|
601
|
+
}
|
|
602
|
+
const baseOptions = buildHostedManagerOptions(scopeInput, sessionId);
|
|
603
|
+
const manager = createMemoryWithAsyncAdapter({
|
|
604
|
+
...baseOptions,
|
|
605
|
+
asyncAdapter: adapterResources.asyncAdapter,
|
|
606
|
+
embeddingAdapter: adapterResources.embeddingAdapter,
|
|
607
|
+
closeAdapter: false,
|
|
608
|
+
});
|
|
609
|
+
touchManagerCache(sessionManagers, key, manager, SESSION_MANAGER_CACHE_LIMIT);
|
|
310
610
|
return manager;
|
|
311
611
|
}
|
|
312
612
|
const manager = getManager(config.scope ?? 'default');
|
|
@@ -325,7 +625,7 @@ export async function startHttpServer(config = {}) {
|
|
|
325
625
|
// Auth
|
|
326
626
|
if (apiKey) {
|
|
327
627
|
const auth = req.headers.authorization;
|
|
328
|
-
if (!auth
|
|
628
|
+
if (!safeSecretEquals(auth, `Bearer ${apiKey}`)) {
|
|
329
629
|
writeError(res, 401, 'Unauthorized');
|
|
330
630
|
return;
|
|
331
631
|
}
|
|
@@ -357,30 +657,132 @@ export async function startHttpServer(config = {}) {
|
|
|
357
657
|
// GET /v1/context
|
|
358
658
|
if (path === '/v1/context' && req.method === 'GET') {
|
|
359
659
|
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
360
|
-
const context = await requestManager.getContext(query.query || undefined
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
660
|
+
const context = await requestManager.getContext(query.query || undefined, {
|
|
661
|
+
view: parseContextViewPolicy(query.view),
|
|
662
|
+
viewer: parseViewerFromQuery(query),
|
|
663
|
+
includeCoordinationState: query.include_coordination === 'true',
|
|
664
|
+
});
|
|
665
|
+
writeJson(res, 200, serializeContextResponse(context, {
|
|
666
|
+
includeDebug: query.debug === 'true',
|
|
667
|
+
}));
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
// GET /v1/state
|
|
671
|
+
if (path === '/v1/state' && req.method === 'GET') {
|
|
672
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
673
|
+
const asOf = parseOptionalFiniteNumber(query.as_of, { name: 'as_of' }, failHttpValidation);
|
|
674
|
+
if (asOf == null) {
|
|
675
|
+
writeError(res, 400, 'Missing or invalid as_of parameter');
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const state = await requestManager.getStateAt(asOf, {
|
|
679
|
+
relevanceQuery: query.query || undefined,
|
|
680
|
+
view: parseContextViewPolicy(query.view),
|
|
681
|
+
viewer: parseViewerFromQuery(query),
|
|
682
|
+
includeCoordinationState: query.include_coordination === 'true',
|
|
683
|
+
});
|
|
684
|
+
writeJson(res, 200, serializeTemporalState(state, {
|
|
685
|
+
includeDebug: query.include_debug === 'true',
|
|
686
|
+
}));
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
// GET /v1/timeline
|
|
690
|
+
if (path === '/v1/timeline' && req.method === 'GET') {
|
|
691
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
692
|
+
const startAt = parseOptionalFiniteNumber(query.start_at, { name: 'start_at' }, failHttpValidation);
|
|
693
|
+
const endAt = parseOptionalFiniteNumber(query.end_at, { name: 'end_at' }, failHttpValidation);
|
|
694
|
+
const cursor = parseOptionalTemporalId(query.cursor);
|
|
695
|
+
const limit = parseLimit(query.limit);
|
|
696
|
+
const timeline = await requestManager.getTimeline({
|
|
697
|
+
sessionId: query.session_id || undefined,
|
|
698
|
+
entityKind: query.entity_kind,
|
|
699
|
+
entityId: query.entity_id || undefined,
|
|
700
|
+
startAt,
|
|
701
|
+
endAt,
|
|
702
|
+
limit,
|
|
703
|
+
cursor,
|
|
704
|
+
});
|
|
705
|
+
writeJson(res, 200, serializeTimelineResult(timeline));
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
// GET /v1/state/diff
|
|
709
|
+
if (path === '/v1/state/diff' && req.method === 'GET') {
|
|
710
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
711
|
+
const from = parseOptionalFiniteNumber(query.from, { name: 'from' }, failHttpValidation);
|
|
712
|
+
const to = parseOptionalFiniteNumber(query.to, { name: 'to' }, failHttpValidation);
|
|
713
|
+
const maxEvents = parseOptionalFiniteInteger(query.max_events, { name: 'max_events', min: 1, max: maxDiffMaxEvents }, failHttpValidation);
|
|
714
|
+
if (from == null || to == null) {
|
|
715
|
+
writeError(res, 400, 'Missing or invalid from/to parameters');
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const diff = await requestManager.diffState(from, to, {
|
|
719
|
+
sessionId: query.session_id || undefined,
|
|
720
|
+
entityKind: query.entity_kind,
|
|
721
|
+
entityId: query.entity_id || undefined,
|
|
722
|
+
maxEvents: maxEvents ?? defaultDiffMaxEvents,
|
|
723
|
+
});
|
|
724
|
+
writeJson(res, 200, diff);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
// GET /v1/events/log
|
|
728
|
+
if (path === '/v1/events/log' && req.method === 'GET') {
|
|
729
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
730
|
+
const startAt = parseOptionalFiniteNumber(query.start_at, { name: 'start_at' }, failHttpValidation);
|
|
731
|
+
const endAt = parseOptionalFiniteNumber(query.end_at, { name: 'end_at' }, failHttpValidation);
|
|
732
|
+
const cursor = parseOptionalTemporalId(query.cursor);
|
|
733
|
+
const limit = parseLimit(query.limit);
|
|
734
|
+
const events = await requestManager.listMemoryEvents({
|
|
735
|
+
sessionId: query.session_id || undefined,
|
|
736
|
+
entityKind: query.entity_kind,
|
|
737
|
+
entityId: query.entity_id || undefined,
|
|
738
|
+
startAt,
|
|
739
|
+
endAt,
|
|
740
|
+
limit,
|
|
741
|
+
cursor,
|
|
383
742
|
});
|
|
743
|
+
writeJson(res, 200, serializeTimelineResult(events));
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
// GET /v1/changes/stream
|
|
747
|
+
if (path === '/v1/changes/stream' && req.method === 'GET') {
|
|
748
|
+
res.writeHead(200, {
|
|
749
|
+
'Content-Type': 'text/event-stream',
|
|
750
|
+
'Cache-Control': 'no-cache',
|
|
751
|
+
Connection: 'keep-alive',
|
|
752
|
+
});
|
|
753
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
754
|
+
let closed = false;
|
|
755
|
+
const abortController = new AbortController();
|
|
756
|
+
req.on('close', () => {
|
|
757
|
+
closed = true;
|
|
758
|
+
abortController.abort();
|
|
759
|
+
});
|
|
760
|
+
const cursor = parseOptionalTemporalId(query.cursor);
|
|
761
|
+
const initialCursor = await requestManager.resolveChangeStreamCursor(cursor);
|
|
762
|
+
const iterator = requestManager.streamChanges({
|
|
763
|
+
cursor,
|
|
764
|
+
sessionId: query.session_id || undefined,
|
|
765
|
+
entityKind: query.entity_kind,
|
|
766
|
+
entityId: query.entity_id || undefined,
|
|
767
|
+
pollIntervalMs: 250,
|
|
768
|
+
signal: abortController.signal,
|
|
769
|
+
});
|
|
770
|
+
void (async () => {
|
|
771
|
+
res.write(`data: ${JSON.stringify({ type: 'connected', cursor: initialCursor })}\n\n`);
|
|
772
|
+
try {
|
|
773
|
+
for await (const event of iterator) {
|
|
774
|
+
if (closed)
|
|
775
|
+
break;
|
|
776
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
catch (error) {
|
|
780
|
+
if (!closed) {
|
|
781
|
+
res.write(`data: ${JSON.stringify({ type: 'error', error: String(error) })}\n\n`);
|
|
782
|
+
res.end();
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
})();
|
|
384
786
|
return;
|
|
385
787
|
}
|
|
386
788
|
// GET /v1/search
|
|
@@ -435,7 +837,7 @@ export async function startHttpServer(config = {}) {
|
|
|
435
837
|
// GET /v1/inspect/knowledge
|
|
436
838
|
if (path === '/v1/inspect/knowledge' && req.method === 'GET') {
|
|
437
839
|
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
438
|
-
const limit =
|
|
840
|
+
const limit = parseLimit(query.limit);
|
|
439
841
|
const cursor = parseOptionalInteger(query.cursor);
|
|
440
842
|
if ((query.limit && limit == null) || (query.cursor && cursor == null)) {
|
|
441
843
|
writeError(res, 400, 'Invalid pagination parameters');
|
|
@@ -463,7 +865,7 @@ export async function startHttpServer(config = {}) {
|
|
|
463
865
|
if (path === '/v1/inspect/audits' && req.method === 'GET') {
|
|
464
866
|
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
465
867
|
const knowledgeId = parseOptionalInteger(query.knowledge_id);
|
|
466
|
-
const limit =
|
|
868
|
+
const limit = parseLimit(query.limit);
|
|
467
869
|
if ((query.knowledge_id && knowledgeId == null) || (query.limit && limit == null)) {
|
|
468
870
|
writeError(res, 400, 'Invalid audit inspection parameters');
|
|
469
871
|
return;
|
|
@@ -478,14 +880,17 @@ export async function startHttpServer(config = {}) {
|
|
|
478
880
|
// GET /v1/inspect/monitor
|
|
479
881
|
if (path === '/v1/inspect/monitor' && req.method === 'GET') {
|
|
480
882
|
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
481
|
-
const monitor = await
|
|
482
|
-
|
|
883
|
+
const [monitor, diagnostics] = await Promise.all([
|
|
884
|
+
requestManager.getContextMonitor(),
|
|
885
|
+
requestManager.getRuntimeDiagnostics(),
|
|
886
|
+
]);
|
|
887
|
+
writeJson(res, 200, { monitor, diagnostics });
|
|
483
888
|
return;
|
|
484
889
|
}
|
|
485
890
|
// GET /v1/inspect/compactions
|
|
486
891
|
if (path === '/v1/inspect/compactions' && req.method === 'GET') {
|
|
487
892
|
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
488
|
-
const limit =
|
|
893
|
+
const limit = parseLimit(query.limit);
|
|
489
894
|
if (query.limit && limit == null) {
|
|
490
895
|
writeError(res, 400, 'Invalid compaction inspection parameters');
|
|
491
896
|
return;
|
|
@@ -494,6 +899,40 @@ export async function startHttpServer(config = {}) {
|
|
|
494
899
|
writeJson(res, 200, { logs });
|
|
495
900
|
return;
|
|
496
901
|
}
|
|
902
|
+
// GET /v1/inspect/context
|
|
903
|
+
if (path === '/v1/inspect/context' && req.method === 'GET') {
|
|
904
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
905
|
+
const asOf = parseOptionalFiniteNumber(query.as_of, { name: 'as_of' }, failHttpValidation);
|
|
906
|
+
const context = asOf != null
|
|
907
|
+
? await requestManager.getContextAt(asOf, query.query || undefined)
|
|
908
|
+
: await requestManager.getContext(query.query || undefined);
|
|
909
|
+
writeJson(res, 200, serializeContextResponse(context, { includeDebug: true }));
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
// GET /v1/inspect/session-state
|
|
913
|
+
if (path === '/v1/inspect/session-state' && req.method === 'GET') {
|
|
914
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
915
|
+
const asOf = parseOptionalFiniteNumber(query.as_of, { name: 'as_of' }, failHttpValidation);
|
|
916
|
+
const context = asOf != null
|
|
917
|
+
? await requestManager.getContextAt(asOf, query.query || undefined)
|
|
918
|
+
: await requestManager.getContext(query.query || undefined);
|
|
919
|
+
writeJson(res, 200, { sessionState: context.sessionState });
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
// GET /v1/inspect/retrieval
|
|
923
|
+
if (path === '/v1/inspect/retrieval' && req.method === 'GET') {
|
|
924
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
925
|
+
const asOf = parseOptionalFiniteNumber(query.as_of, { name: 'as_of' }, failHttpValidation);
|
|
926
|
+
const context = asOf != null
|
|
927
|
+
? await requestManager.getContextAt(asOf, query.query || undefined)
|
|
928
|
+
: await requestManager.getContext(query.query || undefined);
|
|
929
|
+
writeJson(res, 200, {
|
|
930
|
+
sessionState: context.sessionState,
|
|
931
|
+
knowledgeSelectionReasons: context.knowledgeSelectionReasons,
|
|
932
|
+
debugTrace: context.debugTrace,
|
|
933
|
+
});
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
497
936
|
// GET /v1/inspect/reverification
|
|
498
937
|
if (path === '/v1/inspect/reverification' && req.method === 'GET') {
|
|
499
938
|
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
@@ -520,13 +959,134 @@ export async function startHttpServer(config = {}) {
|
|
|
520
959
|
if (path === '/v1/work' && req.method === 'POST') {
|
|
521
960
|
const body = await readBody(req, bodyLimitBytes);
|
|
522
961
|
const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
|
|
523
|
-
const item = await requestManager.trackWorkItem(requireString(body.title, 'title'), requireEnum(body.kind ?? 'objective', ['objective', 'unresolved_work', 'constraint'], 'kind'), requireEnum(body.status ?? 'open', ['open', 'in_progress', 'blocked', 'done'], 'status'), optionalString(body.detail, 'detail')
|
|
962
|
+
const item = await requestManager.trackWorkItem(requireString(body.title, 'title'), requireEnum(body.kind ?? 'objective', ['objective', 'unresolved_work', 'constraint'], 'kind'), requireEnum(body.status ?? 'open', ['open', 'in_progress', 'blocked', 'done'], 'status'), optionalString(body.detail, 'detail'), {
|
|
963
|
+
visibilityClass: body.visibility_class == null
|
|
964
|
+
? undefined
|
|
965
|
+
: requireEnum(body.visibility_class, MEMORY_VISIBILITY_CLASSES, 'visibility_class'),
|
|
966
|
+
});
|
|
524
967
|
writeJson(res, 201, { workItemId: item.id });
|
|
525
968
|
return;
|
|
526
969
|
}
|
|
970
|
+
const workItemMatch = path.match(/^\/v1\/work-items\/(\d+)$/);
|
|
971
|
+
if (workItemMatch && req.method === 'POST') {
|
|
972
|
+
const body = await readBody(req, bodyLimitBytes);
|
|
973
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
|
|
974
|
+
const item = await requestManager.updateWorkItem(Number(workItemMatch[1]), {
|
|
975
|
+
title: body.title != null ? requireString(body.title, 'title') : undefined,
|
|
976
|
+
detail: body.detail != null ? optionalString(body.detail, 'detail') ?? null : undefined,
|
|
977
|
+
status: body.status != null
|
|
978
|
+
? requireEnum(body.status, ['open', 'in_progress', 'blocked', 'done'], 'status')
|
|
979
|
+
: undefined,
|
|
980
|
+
visibility_class: body.visibility_class != null
|
|
981
|
+
? requireEnum(body.visibility_class, MEMORY_VISIBILITY_CLASSES, 'visibility_class')
|
|
982
|
+
: undefined,
|
|
983
|
+
}, {
|
|
984
|
+
expectedVersion: parseOptionalFiniteInteger(body.expectedVersion, { name: 'expectedVersion', min: 0 }, failHttpValidation),
|
|
985
|
+
});
|
|
986
|
+
writeJson(res, 200, { workItem: item });
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const claimMatch = path.match(/^\/v1\/work-items\/(\d+)\/claim$/);
|
|
990
|
+
if (claimMatch && req.method === 'POST') {
|
|
991
|
+
const body = await readBody(req, bodyLimitBytes);
|
|
992
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
|
|
993
|
+
const actor = parseActorRef(body.actor, 'actor');
|
|
994
|
+
if (!actor) {
|
|
995
|
+
writeError(res, 400, 'Missing required field: actor');
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
const claim = await requestManager.claimWorkItem({
|
|
999
|
+
workItemId: Number(claimMatch[1]),
|
|
1000
|
+
actor,
|
|
1001
|
+
leaseSeconds: parseOptionalFiniteInteger(body.leaseSeconds, { name: 'leaseSeconds', min: 1 }, failHttpValidation),
|
|
1002
|
+
});
|
|
1003
|
+
writeJson(res, 200, { claim: serializeWorkClaim(claim) });
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
const renewMatch = path.match(/^\/v1\/work-claims\/(\d+)\/renew$/);
|
|
1007
|
+
if (renewMatch && req.method === 'POST') {
|
|
1008
|
+
const body = await readBody(req, bodyLimitBytes);
|
|
1009
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
|
|
1010
|
+
const actor = parseActorRef(body.actor, 'actor');
|
|
1011
|
+
if (!actor) {
|
|
1012
|
+
writeError(res, 400, 'Missing required field: actor');
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
const claim = await requestManager.renewWorkClaim(Number(renewMatch[1]), actor, parseOptionalFiniteInteger(body.leaseSeconds, { name: 'leaseSeconds', min: 1 }, failHttpValidation));
|
|
1016
|
+
writeJson(res, 200, { claim: claim ? serializeWorkClaim(claim) : null });
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
const releaseMatch = path.match(/^\/v1\/work-claims\/(\d+)\/release$/);
|
|
1020
|
+
if (releaseMatch && req.method === 'POST') {
|
|
1021
|
+
const body = await readBody(req, bodyLimitBytes);
|
|
1022
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
|
|
1023
|
+
const actor = parseActorRef(body.actor, 'actor');
|
|
1024
|
+
if (!actor) {
|
|
1025
|
+
writeError(res, 400, 'Missing required field: actor');
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
const claim = await requestManager.releaseWorkClaim(Number(releaseMatch[1]), actor, optionalString(body.reason, 'reason'));
|
|
1029
|
+
writeJson(res, 200, { claim: claim ? serializeWorkClaim(claim) : null });
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
if (path === '/v1/work-claims' && req.method === 'GET') {
|
|
1033
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
1034
|
+
const claims = await requestManager.listWorkClaims();
|
|
1035
|
+
writeJson(res, 200, { claims: claims.map(serializeWorkClaim) });
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
const handoffCreateMatch = path.match(/^\/v1\/work-items\/(\d+)\/handoffs$/);
|
|
1039
|
+
if (handoffCreateMatch && req.method === 'POST') {
|
|
1040
|
+
const body = await readBody(req, bodyLimitBytes);
|
|
1041
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
|
|
1042
|
+
const fromActor = parseActorRef(body.from_actor, 'from_actor');
|
|
1043
|
+
const toActor = parseActorRef(body.to_actor, 'to_actor');
|
|
1044
|
+
if (!fromActor || !toActor) {
|
|
1045
|
+
writeError(res, 400, 'Missing required field: from_actor/to_actor');
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
const handoff = await requestManager.handoffWorkItem({
|
|
1049
|
+
workItemId: Number(handoffCreateMatch[1]),
|
|
1050
|
+
fromActor,
|
|
1051
|
+
toActor,
|
|
1052
|
+
summary: requireString(body.summary, 'summary'),
|
|
1053
|
+
contextBundleRef: optionalString(body.context_bundle_ref, 'context_bundle_ref') ?? null,
|
|
1054
|
+
expiresAt: parseOptionalFiniteInteger(body.expires_at, { name: 'expires_at', min: 0 }, failHttpValidation) ?? null,
|
|
1055
|
+
});
|
|
1056
|
+
writeJson(res, 201, { handoff: serializeHandoffRecord(handoff) });
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
const handoffActionMatch = path.match(/^\/v1\/handoffs\/(\d+)\/(accept|reject|cancel)$/);
|
|
1060
|
+
if (handoffActionMatch && req.method === 'POST') {
|
|
1061
|
+
const body = await readBody(req, bodyLimitBytes);
|
|
1062
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
|
|
1063
|
+
const actor = parseActorRef(body.actor, 'actor');
|
|
1064
|
+
if (!actor) {
|
|
1065
|
+
writeError(res, 400, 'Missing required field: actor');
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
const id = Number(handoffActionMatch[1]);
|
|
1069
|
+
const action = handoffActionMatch[2];
|
|
1070
|
+
const reason = optionalString(body.reason, 'reason');
|
|
1071
|
+
const handoff = action === 'accept'
|
|
1072
|
+
? await requestManager.acceptHandoff(id, actor, reason)
|
|
1073
|
+
: action === 'reject'
|
|
1074
|
+
? await requestManager.rejectHandoff(id, actor, reason)
|
|
1075
|
+
: await requestManager.cancelHandoff(id, actor, reason);
|
|
1076
|
+
writeJson(res, 200, { handoff: handoff ? serializeHandoffRecord(handoff) : null });
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
if (path === '/v1/handoffs' && req.method === 'GET') {
|
|
1080
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
1081
|
+
const handoffs = await requestManager.listPendingHandoffs({
|
|
1082
|
+
direction: query.direction ?? 'all',
|
|
1083
|
+
});
|
|
1084
|
+
writeJson(res, 200, { handoffs: handoffs.map(serializeHandoffRecord) });
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
527
1087
|
// POST /v1/compact
|
|
528
1088
|
if (path === '/v1/compact' && req.method === 'POST') {
|
|
529
|
-
if (adminApiKey && req.headers['x-admin-key']
|
|
1089
|
+
if (adminApiKey && !safeSecretEquals(req.headers['x-admin-key'], adminApiKey)) {
|
|
530
1090
|
writeError(res, 403, 'Admin key required');
|
|
531
1091
|
return;
|
|
532
1092
|
}
|
|
@@ -542,13 +1102,18 @@ export async function startHttpServer(config = {}) {
|
|
|
542
1102
|
// GET /v1/health
|
|
543
1103
|
if (path === '/v1/health' && req.method === 'GET') {
|
|
544
1104
|
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
545
|
-
const context = await
|
|
1105
|
+
const [context, diagnostics] = await Promise.all([
|
|
1106
|
+
requestManager.getContext(),
|
|
1107
|
+
requestManager.getRuntimeDiagnostics(),
|
|
1108
|
+
]);
|
|
546
1109
|
writeJson(res, 200, {
|
|
547
1110
|
activeTurnCount: context.activeTurns.length,
|
|
548
1111
|
tokenEstimate: context.tokenEstimate,
|
|
549
1112
|
knowledgeCount: context.relevantKnowledge.length,
|
|
550
1113
|
objectiveCount: context.activeObjectives.length,
|
|
551
1114
|
unresolvedWorkCount: context.unresolvedWork.length,
|
|
1115
|
+
sessionStateUpdatedAt: context.sessionState.updatedAt,
|
|
1116
|
+
circuitBreakers: diagnostics.circuitBreakers,
|
|
552
1117
|
});
|
|
553
1118
|
return;
|
|
554
1119
|
}
|
|
@@ -558,7 +1123,7 @@ export async function startHttpServer(config = {}) {
|
|
|
558
1123
|
}
|
|
559
1124
|
// POST /v1/maintenance
|
|
560
1125
|
if (path === '/v1/maintenance' && req.method === 'POST') {
|
|
561
|
-
if (adminApiKey && req.headers['x-admin-key']
|
|
1126
|
+
if (adminApiKey && !safeSecretEquals(req.headers['x-admin-key'], adminApiKey)) {
|
|
562
1127
|
writeError(res, 403, 'Admin key required');
|
|
563
1128
|
return;
|
|
564
1129
|
}
|
|
@@ -569,12 +1134,13 @@ export async function startHttpServer(config = {}) {
|
|
|
569
1134
|
expiredWorkingMemory: report.expiredWorkingMemoryIds.length,
|
|
570
1135
|
retiredKnowledge: report.retiredKnowledgeIds.length,
|
|
571
1136
|
deletedWorkItems: report.deletedWorkItemIds.length,
|
|
1137
|
+
deletedAssociationIds: report.deletedAssociationIds,
|
|
572
1138
|
});
|
|
573
1139
|
return;
|
|
574
1140
|
}
|
|
575
1141
|
const reverificationMatch = path.match(/^\/v1\/reverification\/(\d+)$/);
|
|
576
1142
|
if (reverificationMatch && req.method === 'POST') {
|
|
577
|
-
if (adminApiKey && req.headers['x-admin-key']
|
|
1143
|
+
if (adminApiKey && !safeSecretEquals(req.headers['x-admin-key'], adminApiKey)) {
|
|
578
1144
|
writeError(res, 403, 'Admin key required');
|
|
579
1145
|
return;
|
|
580
1146
|
}
|
|
@@ -585,7 +1151,7 @@ export async function startHttpServer(config = {}) {
|
|
|
585
1151
|
}
|
|
586
1152
|
// POST /v1/reverification/run
|
|
587
1153
|
if (path === '/v1/reverification/run' && req.method === 'POST') {
|
|
588
|
-
if (adminApiKey && req.headers['x-admin-key']
|
|
1154
|
+
if (adminApiKey && !safeSecretEquals(req.headers['x-admin-key'], adminApiKey)) {
|
|
589
1155
|
writeError(res, 403, 'Admin key required');
|
|
590
1156
|
return;
|
|
591
1157
|
}
|
|
@@ -637,7 +1203,7 @@ export async function startHttpServer(config = {}) {
|
|
|
637
1203
|
const scope = resolveRequestScope(config.scope, req, query);
|
|
638
1204
|
sseClients.add({
|
|
639
1205
|
response: res,
|
|
640
|
-
scope:
|
|
1206
|
+
scope: materializeScope(scope),
|
|
641
1207
|
scopeLevel: parseScopeLevel(query.scope_level, 'scope_level') ?? 'scope',
|
|
642
1208
|
eventTypes: parseEventTypes(query.event_types),
|
|
643
1209
|
});
|
|
@@ -650,6 +1216,352 @@ export async function startHttpServer(config = {}) {
|
|
|
650
1216
|
});
|
|
651
1217
|
return;
|
|
652
1218
|
}
|
|
1219
|
+
// GET /v1/episodes
|
|
1220
|
+
if (path === '/v1/episodes' && req.method === 'GET') {
|
|
1221
|
+
if (!query.q) {
|
|
1222
|
+
writeError(res, 400, 'Missing required query parameter: q');
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
1226
|
+
const detailLevel = query.detail
|
|
1227
|
+
? requireEnum(query.detail, EPISODE_DETAIL_LEVELS, 'detail')
|
|
1228
|
+
: undefined;
|
|
1229
|
+
const episodeStartAt = parseOptionalFiniteNumber(query.start_at, { name: 'start_at' }, failHttpValidation);
|
|
1230
|
+
const episodeEndAt = parseOptionalFiniteNumber(query.end_at, { name: 'end_at' }, failHttpValidation);
|
|
1231
|
+
const episodes = await requestManager.searchEpisodes({
|
|
1232
|
+
query: query.q,
|
|
1233
|
+
detailLevel,
|
|
1234
|
+
limit: parseLimit(query.limit),
|
|
1235
|
+
timeRange: episodeStartAt != null || episodeEndAt != null
|
|
1236
|
+
? {
|
|
1237
|
+
start_at: episodeStartAt,
|
|
1238
|
+
end_at: episodeEndAt,
|
|
1239
|
+
}
|
|
1240
|
+
: undefined,
|
|
1241
|
+
});
|
|
1242
|
+
writeJson(res, 200, { episodes });
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
// POST /v1/episodes/summarize
|
|
1246
|
+
if (path === '/v1/episodes/summarize' && req.method === 'POST') {
|
|
1247
|
+
const body = await readBody(req, bodyLimitBytes);
|
|
1248
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
|
|
1249
|
+
const sessionId = requireString(body.session_id, 'session_id');
|
|
1250
|
+
const detailLevel = body.detailLevel
|
|
1251
|
+
? requireEnum(body.detailLevel, EPISODE_DETAIL_LEVELS, 'detailLevel')
|
|
1252
|
+
: undefined;
|
|
1253
|
+
const summary = await requestManager.summarizeEpisode(sessionId, { detailLevel });
|
|
1254
|
+
writeJson(res, 200, { episode: summary });
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
// POST /v1/reflect
|
|
1258
|
+
if (path === '/v1/reflect' && req.method === 'POST') {
|
|
1259
|
+
const body = await readBody(req, bodyLimitBytes);
|
|
1260
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
|
|
1261
|
+
const reflectQuery = requireString(body.query, 'query');
|
|
1262
|
+
const detailLevel = body.detailLevel
|
|
1263
|
+
? requireEnum(body.detailLevel, EPISODE_DETAIL_LEVELS, 'detailLevel')
|
|
1264
|
+
: undefined;
|
|
1265
|
+
const includeDeclarative = body.includeDeclarative != null ? Boolean(body.includeDeclarative) : undefined;
|
|
1266
|
+
const includeEpisodic = body.includeEpisodic != null ? Boolean(body.includeEpisodic) : undefined;
|
|
1267
|
+
const reflectLimit = parseOptionalNonNegativeInteger(body.limit, 'limit');
|
|
1268
|
+
const timeRange = isRecord(body.timeRange)
|
|
1269
|
+
? {
|
|
1270
|
+
start_at: parseOptionalFiniteNumber(body.timeRange.start_at, { name: 'timeRange.start_at' }, failHttpValidation),
|
|
1271
|
+
end_at: parseOptionalFiniteNumber(body.timeRange.end_at, { name: 'timeRange.end_at' }, failHttpValidation),
|
|
1272
|
+
}
|
|
1273
|
+
: undefined;
|
|
1274
|
+
const result = await requestManager.reflect({
|
|
1275
|
+
query: reflectQuery,
|
|
1276
|
+
detailLevel,
|
|
1277
|
+
includeDeclarative,
|
|
1278
|
+
includeEpisodic,
|
|
1279
|
+
limit: reflectLimit,
|
|
1280
|
+
timeRange,
|
|
1281
|
+
});
|
|
1282
|
+
writeJson(res, 200, result);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
// GET /v1/memory
|
|
1286
|
+
if (path === '/v1/memory' && req.method === 'GET') {
|
|
1287
|
+
if (!query.q) {
|
|
1288
|
+
writeError(res, 400, 'Missing required query parameter: q');
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
1292
|
+
const types = query.types
|
|
1293
|
+
? query.types.split(',').map((t) => t.trim()).filter(Boolean)
|
|
1294
|
+
: undefined;
|
|
1295
|
+
const cogMinTrust = parseOptionalFiniteNumber(query.minimumTrustScore, { name: 'minimumTrustScore', min: 0, max: 1 }, failHttpValidation);
|
|
1296
|
+
const cogActiveOnly = query.activeOnly != null
|
|
1297
|
+
? query.activeOnly === 'true'
|
|
1298
|
+
: undefined;
|
|
1299
|
+
const result = await requestManager.searchCognitive({
|
|
1300
|
+
query: query.q,
|
|
1301
|
+
types,
|
|
1302
|
+
limit: parseLimit(query.limit),
|
|
1303
|
+
minimumTrustScore: cogMinTrust,
|
|
1304
|
+
activeOnly: cogActiveOnly,
|
|
1305
|
+
});
|
|
1306
|
+
writeJson(res, 200, result);
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
// GET /v1/profile
|
|
1310
|
+
if (path === '/v1/profile' && req.method === 'GET') {
|
|
1311
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
1312
|
+
const validViews = ['user', 'operator', 'workspace'];
|
|
1313
|
+
const view = query.view
|
|
1314
|
+
? requireEnum(query.view, validViews, 'view')
|
|
1315
|
+
: undefined;
|
|
1316
|
+
const validSections = ['identity', 'preferences', 'communication', 'constraints', 'workflows'];
|
|
1317
|
+
const sections = query.sections
|
|
1318
|
+
? query.sections.split(',').map((s) => requireEnum(s.trim(), validSections, 'sections'))
|
|
1319
|
+
: undefined;
|
|
1320
|
+
const minTrust = parseOptionalFiniteNumber(query.min_trust, { name: 'min_trust', min: 0, max: 1 }, failHttpValidation);
|
|
1321
|
+
const includeProvisional = query.includeProvisional === 'true' ? true : undefined;
|
|
1322
|
+
const includeDisputed = query.includeDisputed === 'true' ? true : undefined;
|
|
1323
|
+
const profile = await requestManager.getProfile({
|
|
1324
|
+
view,
|
|
1325
|
+
sections,
|
|
1326
|
+
minimumTrustScore: minTrust,
|
|
1327
|
+
includeProvisional,
|
|
1328
|
+
includeDisputed,
|
|
1329
|
+
});
|
|
1330
|
+
writeJson(res, 200, { profile });
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
// POST /v1/playbooks
|
|
1334
|
+
if (path === '/v1/playbooks' && req.method === 'POST') {
|
|
1335
|
+
const body = await readBody(req, bodyLimitBytes);
|
|
1336
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
|
|
1337
|
+
const playbook = await requestManager.createPlaybook({
|
|
1338
|
+
title: requireString(body.title, 'title'),
|
|
1339
|
+
description: requireString(body.description, 'description'),
|
|
1340
|
+
instructions: requireString(body.instructions, 'instructions'),
|
|
1341
|
+
references: Array.isArray(body.references) ? body.references.map(String) : undefined,
|
|
1342
|
+
templates: Array.isArray(body.templates) ? body.templates.map(String) : undefined,
|
|
1343
|
+
scripts: Array.isArray(body.scripts) ? body.scripts.map(String) : undefined,
|
|
1344
|
+
assets: Array.isArray(body.assets) ? body.assets.map(String) : undefined,
|
|
1345
|
+
tags: Array.isArray(body.tags) ? body.tags.map(String) : undefined,
|
|
1346
|
+
status: body.status ? requireEnum(body.status, PLAYBOOK_STATUSES, 'status') : undefined,
|
|
1347
|
+
});
|
|
1348
|
+
writeJson(res, 201, { playbook });
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
// GET /v1/playbooks
|
|
1352
|
+
if (path === '/v1/playbooks' && req.method === 'GET') {
|
|
1353
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
1354
|
+
if (query.q) {
|
|
1355
|
+
const results = await requestManager.searchPlaybooks(query.q, query.limit ? { limit: parseLimit(query.limit) } : undefined);
|
|
1356
|
+
writeJson(res, 200, {
|
|
1357
|
+
playbooks: results.map((r) => ({ ...r.item, rank: r.rank })),
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
else {
|
|
1361
|
+
const playbooks = await requestManager.listPlaybooks();
|
|
1362
|
+
writeJson(res, 200, { playbooks });
|
|
1363
|
+
}
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
// POST /v1/playbooks/from-task
|
|
1367
|
+
if (path === '/v1/playbooks/from-task' && req.method === 'POST') {
|
|
1368
|
+
const body = await readBody(req, bodyLimitBytes);
|
|
1369
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
|
|
1370
|
+
const playbook = await requestManager.createPlaybookFromTask({
|
|
1371
|
+
title: requireString(body.title, 'title'),
|
|
1372
|
+
description: requireString(body.description, 'description'),
|
|
1373
|
+
sessionId: requireString(body.sessionId, 'sessionId'),
|
|
1374
|
+
tags: Array.isArray(body.tags) ? body.tags.map(String) : undefined,
|
|
1375
|
+
sourceWorkingMemoryId: parseOptionalFiniteInteger(body.sourceWorkingMemoryId, { name: 'sourceWorkingMemoryId', min: 1 }, failHttpValidation),
|
|
1376
|
+
});
|
|
1377
|
+
writeJson(res, 201, { playbook });
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
// GET /v1/playbooks/:id
|
|
1381
|
+
const playbookGetMatch = path.match(/^\/v1\/playbooks\/(\d+)$/);
|
|
1382
|
+
if (playbookGetMatch && req.method === 'GET') {
|
|
1383
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
1384
|
+
const playbook = await requestManager.getPlaybook(Number(playbookGetMatch[1]));
|
|
1385
|
+
if (!playbook) {
|
|
1386
|
+
writeError(res, 404, 'Playbook not found');
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
writeJson(res, 200, { playbook });
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
// PUT /v1/playbooks/:id
|
|
1393
|
+
if (playbookGetMatch && req.method === 'PUT') {
|
|
1394
|
+
const body = await readBody(req, bodyLimitBytes);
|
|
1395
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
|
|
1396
|
+
const patch = {};
|
|
1397
|
+
if (body.title != null)
|
|
1398
|
+
patch.title = requireString(body.title, 'title');
|
|
1399
|
+
if (body.description != null)
|
|
1400
|
+
patch.description = requireString(body.description, 'description');
|
|
1401
|
+
if (body.instructions != null)
|
|
1402
|
+
patch.instructions = requireString(body.instructions, 'instructions');
|
|
1403
|
+
if (Array.isArray(body.references))
|
|
1404
|
+
patch.references = body.references.map(String);
|
|
1405
|
+
if (Array.isArray(body.templates))
|
|
1406
|
+
patch.templates = body.templates.map(String);
|
|
1407
|
+
if (Array.isArray(body.scripts))
|
|
1408
|
+
patch.scripts = body.scripts.map(String);
|
|
1409
|
+
if (Array.isArray(body.assets))
|
|
1410
|
+
patch.assets = body.assets.map(String);
|
|
1411
|
+
if (Array.isArray(body.tags))
|
|
1412
|
+
patch.tags = body.tags.map(String);
|
|
1413
|
+
if (body.status != null)
|
|
1414
|
+
patch.status = requireEnum(body.status, PLAYBOOK_STATUSES, 'status');
|
|
1415
|
+
const updated = await requestManager.updatePlaybook(Number(playbookGetMatch[1]), patch);
|
|
1416
|
+
if (!updated) {
|
|
1417
|
+
writeError(res, 404, 'Playbook not found');
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
writeJson(res, 200, { playbook: updated });
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
// POST /v1/playbooks/:id/revise
|
|
1424
|
+
const playbookReviseMatch = path.match(/^\/v1\/playbooks\/(\d+)\/revise$/);
|
|
1425
|
+
if (playbookReviseMatch && req.method === 'POST') {
|
|
1426
|
+
const body = await readBody(req, bodyLimitBytes);
|
|
1427
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
|
|
1428
|
+
const result = await requestManager.revisePlaybook(Number(playbookReviseMatch[1]), requireString(body.instructions, 'instructions'), requireString(body.revisionReason, 'revisionReason'), optionalString(body.sourceSessionId, 'sourceSessionId'));
|
|
1429
|
+
writeJson(res, 200, result);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
// POST /v1/playbooks/:id/use
|
|
1433
|
+
const playbookUseMatch = path.match(/^\/v1\/playbooks\/(\d+)\/use$/);
|
|
1434
|
+
if (playbookUseMatch && req.method === 'POST') {
|
|
1435
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
1436
|
+
const playbookId = Number(playbookUseMatch[1]);
|
|
1437
|
+
await requestManager.recordPlaybookUse(playbookId);
|
|
1438
|
+
const playbook = await requestManager.getPlaybook(playbookId);
|
|
1439
|
+
writeJson(res, 200, { recorded: true, playbook });
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
// POST /v1/associations
|
|
1443
|
+
if (path === '/v1/associations' && req.method === 'POST') {
|
|
1444
|
+
const body = await readBody(req, bodyLimitBytes);
|
|
1445
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
|
|
1446
|
+
const sourceId = Number.isInteger(body.source_id) && body.source_id > 0
|
|
1447
|
+
? body.source_id
|
|
1448
|
+
: (() => { throw new HttpRequestError(400, 'Missing or invalid field: source_id (must be positive integer)'); })();
|
|
1449
|
+
const targetId = Number.isInteger(body.target_id) && body.target_id > 0
|
|
1450
|
+
? body.target_id
|
|
1451
|
+
: (() => { throw new HttpRequestError(400, 'Missing or invalid field: target_id (must be positive integer)'); })();
|
|
1452
|
+
let confidence;
|
|
1453
|
+
if (body.confidence !== undefined && body.confidence !== null) {
|
|
1454
|
+
if (typeof body.confidence !== 'number' || Number.isNaN(body.confidence) || body.confidence < 0 || body.confidence > 1) {
|
|
1455
|
+
throw new HttpRequestError(400, 'Invalid field: confidence (must be a number in [0, 1])');
|
|
1456
|
+
}
|
|
1457
|
+
confidence = body.confidence;
|
|
1458
|
+
}
|
|
1459
|
+
const association = await requestManager.addAssociation({
|
|
1460
|
+
source_kind: requireEnum(body.source_kind, ASSOCIATION_TARGET_KINDS, 'source_kind'),
|
|
1461
|
+
source_id: sourceId,
|
|
1462
|
+
target_kind: requireEnum(body.target_kind, ASSOCIATION_TARGET_KINDS, 'target_kind'),
|
|
1463
|
+
target_id: targetId,
|
|
1464
|
+
association_type: requireEnum(body.association_type, ASSOCIATION_TYPES, 'association_type'),
|
|
1465
|
+
confidence,
|
|
1466
|
+
auto_generated: typeof body.auto_generated === 'boolean' ? body.auto_generated : undefined,
|
|
1467
|
+
});
|
|
1468
|
+
writeJson(res, 201, { association });
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
// GET /v1/associations/:kind/:id
|
|
1472
|
+
const assocGetMatch = path.match(/^\/v1\/associations\/([a-z_]+)\/(\d+)$/);
|
|
1473
|
+
if (assocGetMatch && req.method === 'GET') {
|
|
1474
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
1475
|
+
const kind = requireEnum(assocGetMatch[1], ASSOCIATION_TARGET_KINDS, 'kind');
|
|
1476
|
+
const targetId = Number(assocGetMatch[2]);
|
|
1477
|
+
const result = await requestManager.getAssociations(kind, targetId);
|
|
1478
|
+
writeJson(res, 200, result);
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
// POST /v1/associations/traverse
|
|
1482
|
+
if (path === '/v1/associations/traverse' && req.method === 'POST') {
|
|
1483
|
+
const body = await readBody(req, bodyLimitBytes);
|
|
1484
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query, body));
|
|
1485
|
+
const kind = requireEnum(body.kind, ASSOCIATION_TARGET_KINDS, 'kind');
|
|
1486
|
+
const id = Number.isInteger(body.id) && body.id > 0
|
|
1487
|
+
? body.id
|
|
1488
|
+
: (() => { throw new HttpRequestError(400, 'Missing or invalid field: id (must be positive integer)'); })();
|
|
1489
|
+
const maxDepth = parseOptionalNonNegativeInteger(body.maxDepth, 'maxDepth');
|
|
1490
|
+
const maxNodes = parseOptionalNonNegativeInteger(body.maxNodes, 'maxNodes');
|
|
1491
|
+
const graph = await requestManager.traverseAssociations(kind, id, { maxDepth, maxNodes });
|
|
1492
|
+
writeJson(res, 200, graph);
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
// DELETE /v1/associations/:id
|
|
1496
|
+
const assocDeleteMatch = path.match(/^\/v1\/associations\/(\d+)$/);
|
|
1497
|
+
if (assocDeleteMatch && req.method === 'DELETE') {
|
|
1498
|
+
const requestManager = getManager(resolveRequestScope(config.scope, req, query));
|
|
1499
|
+
await requestManager.removeAssociation(Number(assocDeleteMatch[1]));
|
|
1500
|
+
writeJson(res, 200, { deleted: true });
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
// POST /v1/sessions/:sessionId/snapshot — capture a frozen snapshot
|
|
1504
|
+
const snapshotCaptureMatch = path.match(/^\/v1\/sessions\/([^/]+)\/snapshot$/);
|
|
1505
|
+
if (snapshotCaptureMatch && req.method === 'POST') {
|
|
1506
|
+
const body = await readBody(req, bodyLimitBytes);
|
|
1507
|
+
const sessionId = decodeURIComponent(snapshotCaptureMatch[1]);
|
|
1508
|
+
const scopeInput = resolveRequestScope(config.scope, req, query, body);
|
|
1509
|
+
// Use session-aware manager so getContext/getSessionBootstrap read
|
|
1510
|
+
// the session named in the URL, not the scope's bound default.
|
|
1511
|
+
const requestManager = getSessionManager(scopeInput, sessionId);
|
|
1512
|
+
const scopeKey = scopeKeyFor(scopeInput);
|
|
1513
|
+
const relevanceQuery = typeof body.relevanceQuery === 'string' ? body.relevanceQuery : undefined;
|
|
1514
|
+
const snapshotData = await requestManager.captureSnapshot(relevanceQuery);
|
|
1515
|
+
const snapshot = {
|
|
1516
|
+
scopeKey,
|
|
1517
|
+
snapshotId: `snap-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
|
|
1518
|
+
bootstrap: snapshotData.bootstrap,
|
|
1519
|
+
context: snapshotData.context,
|
|
1520
|
+
frozenAt: snapshotData.frozenAt,
|
|
1521
|
+
watermarkEventId: snapshotData.watermarkEventId,
|
|
1522
|
+
};
|
|
1523
|
+
touchSnapshot(`${scopeKey}:${sessionId}`, snapshot);
|
|
1524
|
+
const { scopeKey: _scopeKey, ...publicSnapshot } = snapshot;
|
|
1525
|
+
writeJson(res, 201, { snapshot: { ...publicSnapshot, sessionId } });
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
// GET /v1/sessions/:sessionId/snapshot — fetch cached snapshot
|
|
1529
|
+
if (snapshotCaptureMatch && req.method === 'GET') {
|
|
1530
|
+
const sessionId = decodeURIComponent(snapshotCaptureMatch[1]);
|
|
1531
|
+
const scopeInput = resolveRequestScope(config.scope, req, query);
|
|
1532
|
+
const scopeKey = scopeKeyFor(scopeInput);
|
|
1533
|
+
const snapshot = readSnapshot(`${scopeKey}:${sessionId}`);
|
|
1534
|
+
if (!snapshot) {
|
|
1535
|
+
writeError(res, 404, 'Snapshot not found');
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
const { scopeKey: _scopeKey, ...publicSnapshot } = snapshot;
|
|
1539
|
+
writeJson(res, 200, { snapshot: { ...publicSnapshot, sessionId } });
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
// POST /v1/sessions/:sessionId/refresh — re-capture and replace
|
|
1543
|
+
const snapshotRefreshMatch = path.match(/^\/v1\/sessions\/([^/]+)\/refresh$/);
|
|
1544
|
+
if (snapshotRefreshMatch && req.method === 'POST') {
|
|
1545
|
+
const body = await readBody(req, bodyLimitBytes);
|
|
1546
|
+
const sessionId = decodeURIComponent(snapshotRefreshMatch[1]);
|
|
1547
|
+
const scopeInput = resolveRequestScope(config.scope, req, query, body);
|
|
1548
|
+
const requestManager = getSessionManager(scopeInput, sessionId);
|
|
1549
|
+
const scopeKey = scopeKeyFor(scopeInput);
|
|
1550
|
+
const relevanceQuery = typeof body.relevanceQuery === 'string' ? body.relevanceQuery : undefined;
|
|
1551
|
+
const snapshotData = await requestManager.captureSnapshot(relevanceQuery);
|
|
1552
|
+
const snapshot = {
|
|
1553
|
+
scopeKey,
|
|
1554
|
+
snapshotId: `snap-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
|
|
1555
|
+
bootstrap: snapshotData.bootstrap,
|
|
1556
|
+
context: snapshotData.context,
|
|
1557
|
+
frozenAt: snapshotData.frozenAt,
|
|
1558
|
+
watermarkEventId: snapshotData.watermarkEventId,
|
|
1559
|
+
};
|
|
1560
|
+
touchSnapshot(`${scopeKey}:${sessionId}`, snapshot);
|
|
1561
|
+
const { scopeKey: _scopeKey, ...publicSnapshot } = snapshot;
|
|
1562
|
+
writeJson(res, 200, { snapshot: { ...publicSnapshot, sessionId } });
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
653
1565
|
writeError(res, 404, `Not found: ${req.method} ${path}`);
|
|
654
1566
|
}
|
|
655
1567
|
catch (error) {
|
|
@@ -657,7 +1569,12 @@ export async function startHttpServer(config = {}) {
|
|
|
657
1569
|
writeError(res, error.status, error.message);
|
|
658
1570
|
return;
|
|
659
1571
|
}
|
|
660
|
-
|
|
1572
|
+
if (isMemoryDomainError(error)) {
|
|
1573
|
+
writeError(res, error.status, error.message);
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1577
|
+
writeError(res, 500, message);
|
|
661
1578
|
}
|
|
662
1579
|
});
|
|
663
1580
|
return new Promise((resolve) => {
|
|
@@ -670,11 +1587,12 @@ export async function startHttpServer(config = {}) {
|
|
|
670
1587
|
client.response.end();
|
|
671
1588
|
}
|
|
672
1589
|
sseClients.clear();
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
}
|
|
1590
|
+
await new Promise((resolveClose) => {
|
|
1591
|
+
server.close(() => resolveClose());
|
|
1592
|
+
});
|
|
677
1593
|
managers.clear();
|
|
1594
|
+
sessionManagers.clear();
|
|
1595
|
+
sessionSnapshots.clear();
|
|
678
1596
|
await adapterResources.close();
|
|
679
1597
|
},
|
|
680
1598
|
});
|