@remnic/core 9.7.6 → 9.7.8
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/dist/access-boundary.d.ts +2 -0
- package/dist/access-boundary.js +1 -1
- package/dist/access-cli.js +7 -7
- package/dist/access-http.js +5 -5
- package/dist/access-mcp.d.ts +2 -0
- package/dist/access-mcp.js +4 -4
- package/dist/access-operations-batch.js +2 -2
- package/dist/access-operations.d.ts +3 -3
- package/dist/access-operations.js +3 -3
- package/dist/access-schema.d.ts +64 -64
- package/dist/{chunk-TBQ4CFIP.js → chunk-57INMZ6F.js} +1 -1
- package/dist/chunk-57INMZ6F.js.map +1 -0
- package/dist/{chunk-BFCQPJ5B.js → chunk-CHVU4RE5.js} +24 -6
- package/dist/chunk-CHVU4RE5.js.map +1 -0
- package/dist/{chunk-3CRHW42H.js → chunk-FYKIEOG6.js} +2 -2
- package/dist/{chunk-ESE55PZJ.js → chunk-NMMKRVUF.js} +10 -6
- package/dist/chunk-NMMKRVUF.js.map +1 -0
- package/dist/{chunk-3UPBVNBX.js → chunk-Q6JPMCPO.js} +3 -3
- package/dist/{chunk-P2UA6XQG.js → chunk-UHGHTOR5.js} +5 -5
- package/dist/chunk-UHGHTOR5.js.map +1 -0
- package/dist/{chunk-H67QFYUK.js → chunk-UJBESW6X.js} +918 -146
- package/dist/chunk-UJBESW6X.js.map +1 -0
- package/dist/cli.js +6 -6
- package/dist/connectors/index.d.ts +7 -0
- package/dist/connectors/index.js +1 -1
- package/dist/index.js +7 -7
- package/dist/orchestrator.js +7 -7
- package/dist/schemas.d.ts +74 -74
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/transfer/types.d.ts +66 -66
- package/package.json +2 -2
- package/src/access-boundary.ts +2 -0
- package/src/access-http.ts +15 -2
- package/src/access-mcp-cancellation.test.ts +405 -0
- package/src/access-mcp.ts +25 -1
- package/src/access-operations-batch.ts +3 -3
- package/src/connectors/hermes-shim.ts +523 -0
- package/src/connectors/index.ts +769 -15
- package/dist/chunk-BFCQPJ5B.js.map +0 -1
- package/dist/chunk-ESE55PZJ.js.map +0 -1
- package/dist/chunk-H67QFYUK.js.map +0 -1
- package/dist/chunk-P2UA6XQG.js.map +0 -1
- package/dist/chunk-TBQ4CFIP.js.map +0 -1
- /package/dist/{chunk-3CRHW42H.js.map → chunk-FYKIEOG6.js.map} +0 -0
- /package/dist/{chunk-3UPBVNBX.js.map → chunk-Q6JPMCPO.js.map} +0 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { request as httpRequest } from "node:http";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
|
|
5
|
+
import { abortError } from "./abort-error.js";
|
|
6
|
+
import { EngramAccessHttpServer } from "./access-http.js";
|
|
7
|
+
import { EngramMcpServer } from "./access-mcp.js";
|
|
8
|
+
import { EngramAccessService, type EngramAccessRecallRequest } from "./access-service.js";
|
|
9
|
+
|
|
10
|
+
function deferred<T = void>(): {
|
|
11
|
+
promise: Promise<T>;
|
|
12
|
+
resolve: (value: T | PromiseLike<T>) => void;
|
|
13
|
+
reject: (reason?: unknown) => void;
|
|
14
|
+
} {
|
|
15
|
+
let resolve!: (value: T | PromiseLike<T>) => void;
|
|
16
|
+
let reject!: (reason?: unknown) => void;
|
|
17
|
+
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
|
18
|
+
resolve = resolvePromise;
|
|
19
|
+
reject = rejectPromise;
|
|
20
|
+
});
|
|
21
|
+
return { promise, resolve, reject };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function waitFor<T>(promise: Promise<T>, timeoutMs = 1_000): Promise<T> {
|
|
25
|
+
return Promise.race([
|
|
26
|
+
promise,
|
|
27
|
+
new Promise<never>((_resolve, reject) => {
|
|
28
|
+
const timer = setTimeout(() => reject(new Error(`timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
29
|
+
timer.unref?.();
|
|
30
|
+
}),
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function mcpBody(
|
|
35
|
+
name: string,
|
|
36
|
+
args: Record<string, unknown> = { query: "slow recall" },
|
|
37
|
+
): string {
|
|
38
|
+
return JSON.stringify({
|
|
39
|
+
jsonrpc: "2.0",
|
|
40
|
+
id: 1,
|
|
41
|
+
method: "tools/call",
|
|
42
|
+
params: { name, arguments: args },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function sendMcp(port: number, name: string): {
|
|
47
|
+
client: ReturnType<typeof httpRequest>;
|
|
48
|
+
response: Promise<string>;
|
|
49
|
+
} {
|
|
50
|
+
const response = deferred<string>();
|
|
51
|
+
const client = httpRequest({
|
|
52
|
+
host: "127.0.0.1",
|
|
53
|
+
port,
|
|
54
|
+
path: "/mcp",
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: {
|
|
57
|
+
authorization: "Bearer test-token",
|
|
58
|
+
"content-type": "application/json",
|
|
59
|
+
},
|
|
60
|
+
}, (res) => {
|
|
61
|
+
const chunks: Buffer[] = [];
|
|
62
|
+
res.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
63
|
+
res.on("end", () => response.resolve(Buffer.concat(chunks).toString("utf8")));
|
|
64
|
+
res.on("error", response.reject);
|
|
65
|
+
});
|
|
66
|
+
client.on("error", (error) => response.reject(error));
|
|
67
|
+
client.end(mcpBody(name));
|
|
68
|
+
return { client, response: response.promise };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
test("MCP recall aborts in-flight work without writing a JSON-RPC error after disconnect", async () => {
|
|
72
|
+
const recallStarted = deferred<void>();
|
|
73
|
+
const recallSettled = deferred<void>();
|
|
74
|
+
const forceRelease = deferred<void>();
|
|
75
|
+
let observedSignal: AbortSignal | undefined;
|
|
76
|
+
let responseStarted = false;
|
|
77
|
+
const service = {
|
|
78
|
+
recall: async (input: EngramAccessRecallRequest) => {
|
|
79
|
+
observedSignal = input.abortSignal;
|
|
80
|
+
recallStarted.resolve();
|
|
81
|
+
try {
|
|
82
|
+
await Promise.race([
|
|
83
|
+
forceRelease.promise,
|
|
84
|
+
new Promise<void>((_resolve, reject) => {
|
|
85
|
+
input.abortSignal?.addEventListener("abort", () => reject(input.abortSignal?.reason), { once: true });
|
|
86
|
+
}),
|
|
87
|
+
]);
|
|
88
|
+
} finally {
|
|
89
|
+
recallSettled.resolve();
|
|
90
|
+
}
|
|
91
|
+
return {} as never;
|
|
92
|
+
},
|
|
93
|
+
} as unknown as EngramAccessService;
|
|
94
|
+
const server = new EngramAccessHttpServer({
|
|
95
|
+
service,
|
|
96
|
+
port: 0,
|
|
97
|
+
authToken: "test-token",
|
|
98
|
+
adminConsoleEnabled: false,
|
|
99
|
+
});
|
|
100
|
+
const status = await server.start();
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const client = httpRequest({
|
|
104
|
+
host: "127.0.0.1",
|
|
105
|
+
port: status.port,
|
|
106
|
+
path: "/mcp",
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: {
|
|
109
|
+
authorization: "Bearer test-token",
|
|
110
|
+
"content-type": "application/json",
|
|
111
|
+
},
|
|
112
|
+
}, (res) => {
|
|
113
|
+
responseStarted = true;
|
|
114
|
+
res.resume();
|
|
115
|
+
});
|
|
116
|
+
client.on("error", () => {});
|
|
117
|
+
client.end(mcpBody("remnic.recall"));
|
|
118
|
+
|
|
119
|
+
await waitFor(recallStarted.promise);
|
|
120
|
+
assert.ok(observedSignal, "the HTTP request signal must reach the MCP recall service");
|
|
121
|
+
client.destroy();
|
|
122
|
+
|
|
123
|
+
await waitFor(recallSettled.promise);
|
|
124
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
125
|
+
assert.equal(observedSignal.aborted, true);
|
|
126
|
+
assert.equal(observedSignal.reason?.name, "AbortError");
|
|
127
|
+
assert.equal(responseStarted, false, "a disconnected client must not receive a JSON-RPC error response");
|
|
128
|
+
} finally {
|
|
129
|
+
forceRelease.resolve();
|
|
130
|
+
await server.stop();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("MCP recall_xray forwards the signal and preserves the original AbortError", async () => {
|
|
135
|
+
const controller = new AbortController();
|
|
136
|
+
const reason = abortError("caller disconnected");
|
|
137
|
+
let observedSignal: AbortSignal | undefined;
|
|
138
|
+
const service = {
|
|
139
|
+
recallXray: async (input: { abortSignal?: AbortSignal }) => {
|
|
140
|
+
observedSignal = input.abortSignal;
|
|
141
|
+
controller.abort(reason);
|
|
142
|
+
return { snapshotFound: false };
|
|
143
|
+
},
|
|
144
|
+
} as unknown as EngramAccessService;
|
|
145
|
+
const mcp = new EngramMcpServer(service);
|
|
146
|
+
|
|
147
|
+
await assert.rejects(
|
|
148
|
+
mcp.handleRequest(
|
|
149
|
+
{
|
|
150
|
+
jsonrpc: "2.0",
|
|
151
|
+
id: 1,
|
|
152
|
+
method: "tools/call",
|
|
153
|
+
params: { name: "remnic.recall_xray", arguments: { query: "why" } },
|
|
154
|
+
},
|
|
155
|
+
{ abortSignal: controller.signal },
|
|
156
|
+
),
|
|
157
|
+
(error: unknown) => error === reason,
|
|
158
|
+
);
|
|
159
|
+
assert.equal(observedSignal, controller.signal);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("MCP memory inspector forwards cancellation to its full recall", async () => {
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
const reason = abortError("inspector caller disconnected");
|
|
165
|
+
let observedSignal: AbortSignal | undefined;
|
|
166
|
+
let actionConfidenceStarted = false;
|
|
167
|
+
const service = {
|
|
168
|
+
recallXray: async (input: { abortSignal?: AbortSignal }) => {
|
|
169
|
+
observedSignal = input.abortSignal;
|
|
170
|
+
controller.abort(reason);
|
|
171
|
+
throw input.abortSignal?.reason;
|
|
172
|
+
},
|
|
173
|
+
actionConfidence: async () => {
|
|
174
|
+
actionConfidenceStarted = true;
|
|
175
|
+
return {} as never;
|
|
176
|
+
},
|
|
177
|
+
} as unknown as EngramAccessService;
|
|
178
|
+
const mcp = new EngramMcpServer(service);
|
|
179
|
+
|
|
180
|
+
await assert.rejects(
|
|
181
|
+
mcp.handleRequest(
|
|
182
|
+
{
|
|
183
|
+
jsonrpc: "2.0",
|
|
184
|
+
id: 1,
|
|
185
|
+
method: "tools/call",
|
|
186
|
+
params: {
|
|
187
|
+
name: "remnic.chatgpt_memory_inspector",
|
|
188
|
+
arguments: { query: "inspect this" },
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{ abortSignal: controller.signal },
|
|
192
|
+
),
|
|
193
|
+
(error: unknown) => error === reason,
|
|
194
|
+
);
|
|
195
|
+
assert.equal(observedSignal, controller.signal);
|
|
196
|
+
assert.equal(actionConfidenceStarted, false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("standalone MCP calls leave recall cancellation undefined", async () => {
|
|
200
|
+
let observedSignal: AbortSignal | undefined;
|
|
201
|
+
const service = {
|
|
202
|
+
recall: async (input: EngramAccessRecallRequest) => {
|
|
203
|
+
observedSignal = input.abortSignal;
|
|
204
|
+
return {} as never;
|
|
205
|
+
},
|
|
206
|
+
} as unknown as EngramAccessService;
|
|
207
|
+
const mcp = new EngramMcpServer(service);
|
|
208
|
+
|
|
209
|
+
const response = await mcp.handleRequest({
|
|
210
|
+
jsonrpc: "2.0",
|
|
211
|
+
id: 1,
|
|
212
|
+
method: "tools/call",
|
|
213
|
+
params: { name: "remnic.recall", arguments: { query: "live" } },
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
assert.equal(observedSignal, undefined);
|
|
217
|
+
assert.equal((response?.result as { isError?: boolean } | undefined)?.isError, false);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("an already-aborted MCP write never starts and preserves the AbortError", async () => {
|
|
221
|
+
const controller = new AbortController();
|
|
222
|
+
const reason = abortError("caller disconnected before dispatch");
|
|
223
|
+
let writeStarted = false;
|
|
224
|
+
const service = {
|
|
225
|
+
memoryStore: async () => {
|
|
226
|
+
writeStarted = true;
|
|
227
|
+
return {} as never;
|
|
228
|
+
},
|
|
229
|
+
} as unknown as EngramAccessService;
|
|
230
|
+
const mcp = new EngramMcpServer(service);
|
|
231
|
+
controller.abort(reason);
|
|
232
|
+
|
|
233
|
+
await assert.rejects(
|
|
234
|
+
mcp.handleRequest(
|
|
235
|
+
{
|
|
236
|
+
jsonrpc: "2.0",
|
|
237
|
+
id: 1,
|
|
238
|
+
method: "tools/call",
|
|
239
|
+
params: {
|
|
240
|
+
name: "remnic.memory_store",
|
|
241
|
+
arguments: { content: "must not be stored", category: "fact" },
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
{ abortSignal: controller.signal },
|
|
245
|
+
),
|
|
246
|
+
(error: unknown) => error === reason,
|
|
247
|
+
);
|
|
248
|
+
assert.equal(writeStarted, false);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("MCP write quota is recorded after commit even when the client disconnects before completion", async () => {
|
|
252
|
+
const mutationCommitted = deferred<void>();
|
|
253
|
+
const releaseOperation = deferred<void>();
|
|
254
|
+
const operationSettled = deferred<void>();
|
|
255
|
+
const requestAborted = deferred<void>();
|
|
256
|
+
let responseStarted = false;
|
|
257
|
+
const service = {
|
|
258
|
+
memoryStore: async () => {
|
|
259
|
+
mutationCommitted.resolve();
|
|
260
|
+
try {
|
|
261
|
+
await releaseOperation.promise;
|
|
262
|
+
} finally {
|
|
263
|
+
operationSettled.resolve();
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
schemaVersion: 1,
|
|
267
|
+
operation: "memory_store",
|
|
268
|
+
namespace: "default",
|
|
269
|
+
dryRun: false,
|
|
270
|
+
accepted: true,
|
|
271
|
+
queued: false,
|
|
272
|
+
status: "stored",
|
|
273
|
+
memoryId: "committed-memory",
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
} as unknown as EngramAccessService;
|
|
277
|
+
const server = new EngramAccessHttpServer({
|
|
278
|
+
service,
|
|
279
|
+
port: 0,
|
|
280
|
+
authToken: "test-token",
|
|
281
|
+
adminConsoleEnabled: false,
|
|
282
|
+
});
|
|
283
|
+
const serverHost = server as unknown as {
|
|
284
|
+
mcpServer: EngramMcpServer;
|
|
285
|
+
writeRequestTimestamps: number[];
|
|
286
|
+
};
|
|
287
|
+
const originalHandleRequest = serverHost.mcpServer.handleRequest.bind(serverHost.mcpServer);
|
|
288
|
+
serverHost.mcpServer.handleRequest = async (request, options) => {
|
|
289
|
+
options?.abortSignal?.addEventListener("abort", () => requestAborted.resolve(), { once: true });
|
|
290
|
+
return originalHandleRequest(request, options);
|
|
291
|
+
};
|
|
292
|
+
const status = await server.start();
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const client = httpRequest({
|
|
296
|
+
host: "127.0.0.1",
|
|
297
|
+
port: status.port,
|
|
298
|
+
path: "/mcp",
|
|
299
|
+
method: "POST",
|
|
300
|
+
headers: {
|
|
301
|
+
authorization: "Bearer test-token",
|
|
302
|
+
"content-type": "application/json",
|
|
303
|
+
},
|
|
304
|
+
}, (res) => {
|
|
305
|
+
responseStarted = true;
|
|
306
|
+
res.resume();
|
|
307
|
+
});
|
|
308
|
+
client.on("error", () => {});
|
|
309
|
+
client.end(mcpBody("remnic.memory_store", {
|
|
310
|
+
content: "committed before disconnect",
|
|
311
|
+
category: "fact",
|
|
312
|
+
}));
|
|
313
|
+
|
|
314
|
+
await waitFor(mutationCommitted.promise);
|
|
315
|
+
client.destroy();
|
|
316
|
+
await waitFor(requestAborted.promise);
|
|
317
|
+
releaseOperation.resolve();
|
|
318
|
+
|
|
319
|
+
await waitFor(operationSettled.promise);
|
|
320
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
321
|
+
assert.equal(serverHost.writeRequestTimestamps.length, 1);
|
|
322
|
+
assert.equal(responseStarted, false, "the disconnected client must not receive the committed write response");
|
|
323
|
+
} finally {
|
|
324
|
+
releaseOperation.resolve();
|
|
325
|
+
await server.stop();
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("a disconnected MCP recall queued on the budget lock never starts and does not poison the lock", async () => {
|
|
330
|
+
const firstStarted = deferred<void>();
|
|
331
|
+
const releaseFirst = deferred<void>();
|
|
332
|
+
const secondQueued = deferred<void>();
|
|
333
|
+
const secondAborted = deferred<void>();
|
|
334
|
+
const secondSettled = deferred<void>();
|
|
335
|
+
let secondStarted = false;
|
|
336
|
+
let thirdStarted = false;
|
|
337
|
+
let callCount = 0;
|
|
338
|
+
|
|
339
|
+
const lockService = Object.create(EngramAccessService.prototype) as EngramAccessService;
|
|
340
|
+
const lockHost = lockService as unknown as {
|
|
341
|
+
budgetLocks: Map<string, Promise<void>>;
|
|
342
|
+
withBudgetLock<T>(
|
|
343
|
+
principal: string,
|
|
344
|
+
abortSignal: AbortSignal | undefined,
|
|
345
|
+
operation: () => Promise<T>,
|
|
346
|
+
): Promise<T>;
|
|
347
|
+
};
|
|
348
|
+
lockHost.budgetLocks = new Map();
|
|
349
|
+
|
|
350
|
+
const service = {
|
|
351
|
+
recall: async (input: EngramAccessRecallRequest) => {
|
|
352
|
+
callCount += 1;
|
|
353
|
+
const call = callCount;
|
|
354
|
+
if (call === 2) {
|
|
355
|
+
input.abortSignal?.addEventListener("abort", () => secondAborted.resolve(), { once: true });
|
|
356
|
+
secondQueued.resolve();
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
return await lockHost.withBudgetLock("principal", input.abortSignal, async () => {
|
|
360
|
+
if (call === 1) {
|
|
361
|
+
firstStarted.resolve();
|
|
362
|
+
await releaseFirst.promise;
|
|
363
|
+
} else if (call === 2) {
|
|
364
|
+
secondStarted = true;
|
|
365
|
+
} else {
|
|
366
|
+
thirdStarted = true;
|
|
367
|
+
}
|
|
368
|
+
return {} as never;
|
|
369
|
+
});
|
|
370
|
+
} finally {
|
|
371
|
+
if (call === 2) secondSettled.resolve();
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
} as unknown as EngramAccessService;
|
|
375
|
+
const server = new EngramAccessHttpServer({
|
|
376
|
+
service,
|
|
377
|
+
port: 0,
|
|
378
|
+
authToken: "test-token",
|
|
379
|
+
adminConsoleEnabled: false,
|
|
380
|
+
});
|
|
381
|
+
const status = await server.start();
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
const first = sendMcp(status.port, "remnic.recall");
|
|
385
|
+
await waitFor(firstStarted.promise);
|
|
386
|
+
|
|
387
|
+
const second = sendMcp(status.port, "remnic.recall");
|
|
388
|
+
second.response.catch(() => {});
|
|
389
|
+
await waitFor(secondQueued.promise);
|
|
390
|
+
second.client.destroy();
|
|
391
|
+
await waitFor(secondAborted.promise);
|
|
392
|
+
releaseFirst.resolve();
|
|
393
|
+
|
|
394
|
+
await waitFor(first.response);
|
|
395
|
+
await waitFor(secondSettled.promise);
|
|
396
|
+
assert.equal(secondStarted, false);
|
|
397
|
+
|
|
398
|
+
const third = sendMcp(status.port, "remnic.recall");
|
|
399
|
+
await waitFor(third.response);
|
|
400
|
+
assert.equal(thirdStarted, true);
|
|
401
|
+
} finally {
|
|
402
|
+
releaseFirst.resolve();
|
|
403
|
+
await server.stop();
|
|
404
|
+
}
|
|
405
|
+
});
|
package/src/access-mcp.ts
CHANGED
|
@@ -40,6 +40,7 @@ import { expandTildePath } from "./utils/path.js";
|
|
|
40
40
|
|
|
41
41
|
import { applyToolOutputSchemas } from "./access-mcp-output-schemas.js";
|
|
42
42
|
import { MCP_READ_ONLY_TOOL_SUFFIXES } from "./mcp-read-only-tools.js";
|
|
43
|
+
import { abortError, isAbortError } from "./abort-error.js";
|
|
43
44
|
type JsonRpcId = string | number | null;
|
|
44
45
|
|
|
45
46
|
type JsonRpcRequest = {
|
|
@@ -49,6 +50,12 @@ type JsonRpcRequest = {
|
|
|
49
50
|
params?: Record<string, unknown>;
|
|
50
51
|
};
|
|
51
52
|
|
|
53
|
+
function throwMcpAbort(signal: AbortSignal | undefined, message: string): void {
|
|
54
|
+
if (!signal?.aborted) return;
|
|
55
|
+
if (isAbortError(signal.reason)) throw signal.reason;
|
|
56
|
+
throw abortError(message);
|
|
57
|
+
}
|
|
58
|
+
|
|
52
59
|
type McpRequestOptions = {
|
|
53
60
|
principalOverride?: string;
|
|
54
61
|
namespaceOverride?: string;
|
|
@@ -67,6 +74,8 @@ type McpRequestOptions = {
|
|
|
67
74
|
* the operation context so write handlers stamp it onto frontmatter.
|
|
68
75
|
*/
|
|
69
76
|
sourceConnector?: string;
|
|
77
|
+
/** HTTP request lifetime; absent for the standalone stdio transport. */
|
|
78
|
+
abortSignal?: AbortSignal;
|
|
70
79
|
};
|
|
71
80
|
|
|
72
81
|
type McpTool = {
|
|
@@ -2645,6 +2654,10 @@ export class EngramMcpServer {
|
|
|
2645
2654
|
...(options?.namespaceOverride ? { namespace: options.namespaceOverride } : {}),
|
|
2646
2655
|
...(options?.sessionKeyOverride ? { sessionKey: options.sessionKeyOverride } : {}),
|
|
2647
2656
|
};
|
|
2657
|
+
// Abort before dispatch so a disconnected request never starts work.
|
|
2658
|
+
// Once a mutating tool has returned, cancellation is deferred to the
|
|
2659
|
+
// HTTP transport so it can account for the committed write first.
|
|
2660
|
+
throwMcpAbort(options?.abortSignal, "MCP request aborted before operation start");
|
|
2648
2661
|
const result = await this.callTool(
|
|
2649
2662
|
name,
|
|
2650
2663
|
argumentsObject,
|
|
@@ -2652,8 +2665,12 @@ export class EngramMcpServer {
|
|
|
2652
2665
|
options?.sessionId,
|
|
2653
2666
|
mcpScope,
|
|
2654
2667
|
options?.enforceWriteQuota,
|
|
2655
|
-
options?.sourceConnector
|
|
2668
|
+
options?.sourceConnector,
|
|
2669
|
+
options?.abortSignal,
|
|
2656
2670
|
);
|
|
2671
|
+
if (isReadOnlyToolName(name)) {
|
|
2672
|
+
throwMcpAbort(options?.abortSignal, "MCP request aborted before response");
|
|
2673
|
+
}
|
|
2657
2674
|
return {
|
|
2658
2675
|
jsonrpc: "2.0",
|
|
2659
2676
|
id,
|
|
@@ -2664,6 +2681,9 @@ export class EngramMcpServer {
|
|
|
2664
2681
|
},
|
|
2665
2682
|
};
|
|
2666
2683
|
} catch (err) {
|
|
2684
|
+
// Cancellation is transport control flow, not a JSON-RPC tool error.
|
|
2685
|
+
// Preserve the original AbortError so HTTP can silently end a dead socket.
|
|
2686
|
+
if (isAbortError(err)) throw err;
|
|
2667
2687
|
const message = err instanceof Error ? err.message : String(err);
|
|
2668
2688
|
return {
|
|
2669
2689
|
jsonrpc: "2.0",
|
|
@@ -2862,6 +2882,7 @@ export class EngramMcpServer {
|
|
|
2862
2882
|
scope?: { namespace?: string; sessionKey?: string },
|
|
2863
2883
|
enforceWriteQuota?: () => void | Promise<void>,
|
|
2864
2884
|
sourceConnector?: string,
|
|
2885
|
+
abortSignal?: AbortSignal,
|
|
2865
2886
|
): Promise<unknown> {
|
|
2866
2887
|
const migrated = MCP_MIGRATED_OPERATIONS[toLegacyToolName(name)];
|
|
2867
2888
|
if (!migrated) {
|
|
@@ -2941,7 +2962,9 @@ export class EngramMcpServer {
|
|
|
2941
2962
|
const result = (await op.run(envelope, {
|
|
2942
2963
|
service: this.service,
|
|
2943
2964
|
authenticatedPrincipal: effectivePrincipal,
|
|
2965
|
+
...(abortSignal ? { abortSignal } : {}),
|
|
2944
2966
|
})) as { result: unknown };
|
|
2967
|
+
throwMcpAbort(abortSignal, "MCP recall aborted before postprocessing");
|
|
2945
2968
|
const response = result.result as Record<string, unknown>;
|
|
2946
2969
|
if (this.shouldEmitCitations(mcpSessionId)) {
|
|
2947
2970
|
const citations = this.buildRecallCitations(response as unknown as EngramAccessRecallResponse);
|
|
@@ -2961,6 +2984,7 @@ export class EngramMcpServer {
|
|
|
2961
2984
|
authenticatedPrincipal: effectivePrincipal,
|
|
2962
2985
|
...(enforceWriteQuota ? { hooks: { enforceWriteQuota } } : {}),
|
|
2963
2986
|
...(sourceConnector ? { sourceConnector } : {}),
|
|
2987
|
+
...(abortSignal ? { abortSignal } : {}),
|
|
2964
2988
|
})) as { result: unknown };
|
|
2965
2989
|
return output.result;
|
|
2966
2990
|
}
|
|
@@ -83,7 +83,7 @@ defineOperation({ name: "recall", description: "Semantic recall.", schema: stric
|
|
|
83
83
|
if (input.tags !== undefined) { if (!Array.isArray(input.tags) || !input.tags.every((t) => typeof t === "string")) throw new EngramAccessInputError("tags must be an array of strings"); tags = input.tags; }
|
|
84
84
|
let tagMatch: "any" | "all" | undefined;
|
|
85
85
|
if (input.tagMatch !== undefined) { if (input.tagMatch !== "any" && input.tagMatch !== "all") throw new EngramAccessInputError("tagMatch must be one of: any, all"); tagMatch = input.tagMatch; }
|
|
86
|
-
const result = await ctx.service.recall({ query: typeof input.query === "string" ? input.query : "", sessionKey: optStr(input.sessionKey), authenticatedPrincipal: ctx.authenticatedPrincipal, namespace: optStr(input.namespace), topK: optNum(input.topK), mode: optStr(input.mode) as RecallPlanMode | "auto" | undefined, includeDebug: input.includeDebug === true, disclosure, cwd: optStr(input.cwd), projectTag: optStr(input.projectTag), asOf: optStr(input.asOf), ...(tags ? { tags } : {}), ...(tagMatch ? { tagMatch } : {}) });
|
|
86
|
+
const result = await ctx.service.recall({ query: typeof input.query === "string" ? input.query : "", sessionKey: optStr(input.sessionKey), authenticatedPrincipal: ctx.authenticatedPrincipal, namespace: optStr(input.namespace), topK: optNum(input.topK), mode: optStr(input.mode) as RecallPlanMode | "auto" | undefined, includeDebug: input.includeDebug === true, disclosure, cwd: optStr(input.cwd), projectTag: optStr(input.projectTag), asOf: optStr(input.asOf), ...(tags ? { tags } : {}), ...(tagMatch ? { tagMatch } : {}), ...(ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}) });
|
|
87
87
|
return { result };
|
|
88
88
|
},
|
|
89
89
|
});
|
|
@@ -110,7 +110,7 @@ defineOperation({ name: "recall_xray", description: "X-ray recall.", schema: str
|
|
|
110
110
|
let budget: number | undefined;
|
|
111
111
|
if (input.budget !== undefined) { const p = typeof input.budget === "number" ? input.budget : typeof input.budget === "string" ? Number(input.budget) : undefined; if (p === undefined || !Number.isFinite(p) || p <= 0 || !Number.isInteger(p)) throw new EngramAccessInputError("recall_xray: budget expects a positive integer"); budget = p; }
|
|
112
112
|
const dr = optStr(input.disclosure);
|
|
113
|
-
return { result: await ctx.service.recallXray({ query: defStr(input.query, ""), sessionKey: optStr(input.sessionKey), namespace: optStr(input.namespace), budget, authenticatedPrincipal: ctx.authenticatedPrincipal, ...(dr && dr !== "" ? { disclosure: dr as RecallDisclosure } : {}) }) };
|
|
113
|
+
return { result: await ctx.service.recallXray({ query: defStr(input.query, ""), sessionKey: optStr(input.sessionKey), namespace: optStr(input.namespace), budget, authenticatedPrincipal: ctx.authenticatedPrincipal, ...(dr && dr !== "" ? { disclosure: dr as RecallDisclosure } : {}), ...(ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}) }) };
|
|
114
114
|
},
|
|
115
115
|
});
|
|
116
116
|
|
|
@@ -133,7 +133,7 @@ defineOperation({ name: "chatgpt_memory_inspector", description: "Memory inspect
|
|
|
133
133
|
if (input.currentContextScopes !== undefined) ii.currentContextScopes = input.currentContextScopes as string[];
|
|
134
134
|
if (input.allowUnverifiedPreview !== undefined) ii.allowUnverifiedPreview = input.allowUnverifiedPreview as boolean;
|
|
135
135
|
const rsk = ii.sessionKey ?? (ctx.authenticatedPrincipal ? "remnic:chatgpt-memory-inspector:" + randomUUID() : undefined);
|
|
136
|
-
const xr = await ctx.service.recallXray({ query: ii.query, sessionKey: rsk, namespace: ii.namespace, currentContextScopes: ii.currentContextScopes, authenticatedPrincipal: ctx.authenticatedPrincipal, mode: "full", disclosure: "chunk", includeRecall: true });
|
|
136
|
+
const xr = await ctx.service.recallXray({ query: ii.query, sessionKey: rsk, namespace: ii.namespace, currentContextScopes: ii.currentContextScopes, authenticatedPrincipal: ctx.authenticatedPrincipal, mode: "full", disclosure: "chunk", includeRecall: true, ...(ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}) });
|
|
137
137
|
const x = xr.snapshotFound === true ? (xr.snapshot ?? null) : null;
|
|
138
138
|
const r = xr.recall ?? { query: ii.query, namespace: ii.namespace ?? x?.namespace ?? "global", context: "", count: 0, memoryIds: [], results: [], fallbackUsed: false, sourcesUsed: [], disclosure: "chunk" as const };
|
|
139
139
|
const ac = await ctx.service.actionConfidence(buildChatGptMemoryInspectorActionRequest(ii, r, x));
|