@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.
Files changed (45) hide show
  1. package/dist/access-boundary.d.ts +2 -0
  2. package/dist/access-boundary.js +1 -1
  3. package/dist/access-cli.js +7 -7
  4. package/dist/access-http.js +5 -5
  5. package/dist/access-mcp.d.ts +2 -0
  6. package/dist/access-mcp.js +4 -4
  7. package/dist/access-operations-batch.js +2 -2
  8. package/dist/access-operations.d.ts +3 -3
  9. package/dist/access-operations.js +3 -3
  10. package/dist/access-schema.d.ts +64 -64
  11. package/dist/{chunk-TBQ4CFIP.js → chunk-57INMZ6F.js} +1 -1
  12. package/dist/chunk-57INMZ6F.js.map +1 -0
  13. package/dist/{chunk-BFCQPJ5B.js → chunk-CHVU4RE5.js} +24 -6
  14. package/dist/chunk-CHVU4RE5.js.map +1 -0
  15. package/dist/{chunk-3CRHW42H.js → chunk-FYKIEOG6.js} +2 -2
  16. package/dist/{chunk-ESE55PZJ.js → chunk-NMMKRVUF.js} +10 -6
  17. package/dist/chunk-NMMKRVUF.js.map +1 -0
  18. package/dist/{chunk-3UPBVNBX.js → chunk-Q6JPMCPO.js} +3 -3
  19. package/dist/{chunk-P2UA6XQG.js → chunk-UHGHTOR5.js} +5 -5
  20. package/dist/chunk-UHGHTOR5.js.map +1 -0
  21. package/dist/{chunk-H67QFYUK.js → chunk-UJBESW6X.js} +918 -146
  22. package/dist/chunk-UJBESW6X.js.map +1 -0
  23. package/dist/cli.js +6 -6
  24. package/dist/connectors/index.d.ts +7 -0
  25. package/dist/connectors/index.js +1 -1
  26. package/dist/index.js +7 -7
  27. package/dist/orchestrator.js +7 -7
  28. package/dist/schemas.d.ts +74 -74
  29. package/dist/shared-context/manager.d.ts +8 -8
  30. package/dist/transfer/types.d.ts +66 -66
  31. package/package.json +2 -2
  32. package/src/access-boundary.ts +2 -0
  33. package/src/access-http.ts +15 -2
  34. package/src/access-mcp-cancellation.test.ts +405 -0
  35. package/src/access-mcp.ts +25 -1
  36. package/src/access-operations-batch.ts +3 -3
  37. package/src/connectors/hermes-shim.ts +523 -0
  38. package/src/connectors/index.ts +769 -15
  39. package/dist/chunk-BFCQPJ5B.js.map +0 -1
  40. package/dist/chunk-ESE55PZJ.js.map +0 -1
  41. package/dist/chunk-H67QFYUK.js.map +0 -1
  42. package/dist/chunk-P2UA6XQG.js.map +0 -1
  43. package/dist/chunk-TBQ4CFIP.js.map +0 -1
  44. /package/dist/{chunk-3CRHW42H.js.map → chunk-FYKIEOG6.js.map} +0 -0
  45. /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));