@remnic/core 9.7.6 → 9.7.7
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.js +3 -3
- 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/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
package/dist/cli.js
CHANGED
|
@@ -90,7 +90,7 @@ import {
|
|
|
90
90
|
runWorkProductStatusCliCommand,
|
|
91
91
|
runWorkProjectCliCommand,
|
|
92
92
|
runWorkTaskCliCommand
|
|
93
|
-
} from "./chunk-
|
|
93
|
+
} from "./chunk-Q6JPMCPO.js";
|
|
94
94
|
import "./chunk-MMACM5G2.js";
|
|
95
95
|
import "./chunk-54BGIBCR.js";
|
|
96
96
|
import "./chunk-H4CKS5B2.js";
|
|
@@ -169,7 +169,7 @@ import "./chunk-G6NKALRN.js";
|
|
|
169
169
|
import "./chunk-OO42R444.js";
|
|
170
170
|
import "./chunk-K6MV3TRQ.js";
|
|
171
171
|
import "./chunk-OADWQ5CR.js";
|
|
172
|
-
import "./chunk-
|
|
172
|
+
import "./chunk-NMMKRVUF.js";
|
|
173
173
|
import "./chunk-V7IZKJI3.js";
|
|
174
174
|
import "./chunk-SEDEKFYQ.js";
|
|
175
175
|
import "./chunk-RKNJBZ55.js";
|
|
@@ -178,13 +178,13 @@ import "./chunk-RSUYKGGZ.js";
|
|
|
178
178
|
import "./chunk-42NQ7AVG.js";
|
|
179
179
|
import "./chunk-TMSXWOBZ.js";
|
|
180
180
|
import "./chunk-7RXCMVFQ.js";
|
|
181
|
-
import "./chunk-
|
|
181
|
+
import "./chunk-CHVU4RE5.js";
|
|
182
182
|
import "./chunk-IC2TCZJ2.js";
|
|
183
183
|
import "./chunk-D24OXEPB.js";
|
|
184
184
|
import "./chunk-5RUE3GHQ.js";
|
|
185
|
-
import "./chunk-
|
|
185
|
+
import "./chunk-FYKIEOG6.js";
|
|
186
186
|
import "./chunk-2MR3MFQB.js";
|
|
187
|
-
import "./chunk-
|
|
187
|
+
import "./chunk-UHGHTOR5.js";
|
|
188
188
|
import "./chunk-HLTAPHZZ.js";
|
|
189
189
|
import "./chunk-YF6FMLBT.js";
|
|
190
190
|
import "./chunk-W4PGTAS2.js";
|
|
@@ -192,7 +192,7 @@ import "./chunk-KQDNAXOM.js";
|
|
|
192
192
|
import "./chunk-35NN3ZFA.js";
|
|
193
193
|
import "./chunk-FTYDWKO6.js";
|
|
194
194
|
import "./chunk-T4WDJPEZ.js";
|
|
195
|
-
import "./chunk-
|
|
195
|
+
import "./chunk-57INMZ6F.js";
|
|
196
196
|
import "./chunk-D7FOKIY5.js";
|
|
197
197
|
import "./chunk-CNWC274T.js";
|
|
198
198
|
import "./chunk-GDASG7NC.js";
|
|
@@ -319,9 +319,16 @@ declare function upsertHermesConfig(opts: {
|
|
|
319
319
|
/**
|
|
320
320
|
* Remove the `remnic:` block from a Hermes profile config.yaml.
|
|
321
321
|
* Idempotent — if the block is absent, returns skipped.
|
|
322
|
+
*
|
|
323
|
+
* `extraConfigPaths` carries the config.yaml location persisted in the
|
|
324
|
+
* connector JSON at install time, so a HERMES_HOME change between install and
|
|
325
|
+
* remove cannot leave the old home's remnic: block (with its daemon token)
|
|
326
|
+
* behind (Codex P2 on PR #1938, round 6). Extra candidates are shape-guarded
|
|
327
|
+
* and de-duplicated against the environment-resolved paths.
|
|
322
328
|
*/
|
|
323
329
|
declare function removeHermesConfig(opts: {
|
|
324
330
|
profile: string;
|
|
331
|
+
extraConfigPaths?: readonly string[];
|
|
325
332
|
}): HermesConfigResult;
|
|
326
333
|
declare function doctorConnector(connectorId: string): Promise<DoctorResult>;
|
|
327
334
|
interface CodexMemoryExtensionPaths {
|
package/dist/connectors/index.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -185,7 +185,7 @@ import {
|
|
|
185
185
|
saveRegistry,
|
|
186
186
|
validateMarketplaceManifest,
|
|
187
187
|
writeMarketplaceManifest
|
|
188
|
-
} from "./chunk-
|
|
188
|
+
} from "./chunk-UJBESW6X.js";
|
|
189
189
|
import "./chunk-V5OCT34X.js";
|
|
190
190
|
import "./chunk-TECVW3JP.js";
|
|
191
191
|
import "./chunk-LXOM6IQU.js";
|
|
@@ -348,7 +348,7 @@ import {
|
|
|
348
348
|
registerTrainingExportAdapter,
|
|
349
349
|
runBulkImportCliCommand,
|
|
350
350
|
runWearablesCliCommand
|
|
351
|
-
} from "./chunk-
|
|
351
|
+
} from "./chunk-Q6JPMCPO.js";
|
|
352
352
|
import "./chunk-MMACM5G2.js";
|
|
353
353
|
import "./chunk-54BGIBCR.js";
|
|
354
354
|
import "./chunk-H4CKS5B2.js";
|
|
@@ -551,7 +551,7 @@ import "./chunk-K6MV3TRQ.js";
|
|
|
551
551
|
import "./chunk-OADWQ5CR.js";
|
|
552
552
|
import {
|
|
553
553
|
EngramAccessHttpServer
|
|
554
|
-
} from "./chunk-
|
|
554
|
+
} from "./chunk-NMMKRVUF.js";
|
|
555
555
|
import {
|
|
556
556
|
WearablesInputError,
|
|
557
557
|
describeErrorForOperator
|
|
@@ -565,7 +565,7 @@ import "./chunk-TMSXWOBZ.js";
|
|
|
565
565
|
import "./chunk-7RXCMVFQ.js";
|
|
566
566
|
import {
|
|
567
567
|
EngramMcpServer
|
|
568
|
-
} from "./chunk-
|
|
568
|
+
} from "./chunk-CHVU4RE5.js";
|
|
569
569
|
import "./chunk-IC2TCZJ2.js";
|
|
570
570
|
import {
|
|
571
571
|
buildCitationGuidance,
|
|
@@ -574,7 +574,7 @@ import {
|
|
|
574
574
|
sanitizeNoteForCitation
|
|
575
575
|
} from "./chunk-D24OXEPB.js";
|
|
576
576
|
import "./chunk-5RUE3GHQ.js";
|
|
577
|
-
import "./chunk-
|
|
577
|
+
import "./chunk-FYKIEOG6.js";
|
|
578
578
|
import {
|
|
579
579
|
formatZodError,
|
|
580
580
|
memoryStoreRequestSchema,
|
|
@@ -583,7 +583,7 @@ import {
|
|
|
583
583
|
suggestionSubmitRequestSchema,
|
|
584
584
|
validateRequest
|
|
585
585
|
} from "./chunk-2MR3MFQB.js";
|
|
586
|
-
import "./chunk-
|
|
586
|
+
import "./chunk-UHGHTOR5.js";
|
|
587
587
|
import "./chunk-HLTAPHZZ.js";
|
|
588
588
|
import "./chunk-YF6FMLBT.js";
|
|
589
589
|
import {
|
|
@@ -616,7 +616,7 @@ import {
|
|
|
616
616
|
} from "./chunk-T4WDJPEZ.js";
|
|
617
617
|
import {
|
|
618
618
|
OPERATION_NAMES
|
|
619
|
-
} from "./chunk-
|
|
619
|
+
} from "./chunk-57INMZ6F.js";
|
|
620
620
|
import {
|
|
621
621
|
TOKEN_CAPABILITIES_VERSION,
|
|
622
622
|
capabilityAllowsNamespace,
|
package/dist/orchestrator.js
CHANGED
|
@@ -67,7 +67,7 @@ import "./chunk-O7BBFJLP.js";
|
|
|
67
67
|
import "./chunk-7N4KAIGN.js";
|
|
68
68
|
import "./chunk-AD2KK5MT.js";
|
|
69
69
|
import "./chunk-5CJVLG3H.js";
|
|
70
|
-
import "./chunk-
|
|
70
|
+
import "./chunk-UJBESW6X.js";
|
|
71
71
|
import "./chunk-V5OCT34X.js";
|
|
72
72
|
import "./chunk-TECVW3JP.js";
|
|
73
73
|
import "./chunk-LXOM6IQU.js";
|
|
@@ -126,7 +126,7 @@ import "./chunk-Q6IP2ARR.js";
|
|
|
126
126
|
import "./chunk-6HEM6HTQ.js";
|
|
127
127
|
import "./chunk-ZQ7UBUZ6.js";
|
|
128
128
|
import "./chunk-HXDKC3BI.js";
|
|
129
|
-
import "./chunk-
|
|
129
|
+
import "./chunk-Q6JPMCPO.js";
|
|
130
130
|
import "./chunk-MMACM5G2.js";
|
|
131
131
|
import "./chunk-54BGIBCR.js";
|
|
132
132
|
import "./chunk-H4CKS5B2.js";
|
|
@@ -218,7 +218,7 @@ import "./chunk-G6NKALRN.js";
|
|
|
218
218
|
import "./chunk-OO42R444.js";
|
|
219
219
|
import "./chunk-K6MV3TRQ.js";
|
|
220
220
|
import "./chunk-OADWQ5CR.js";
|
|
221
|
-
import "./chunk-
|
|
221
|
+
import "./chunk-NMMKRVUF.js";
|
|
222
222
|
import "./chunk-V7IZKJI3.js";
|
|
223
223
|
import "./chunk-SEDEKFYQ.js";
|
|
224
224
|
import "./chunk-RKNJBZ55.js";
|
|
@@ -227,13 +227,13 @@ import "./chunk-RSUYKGGZ.js";
|
|
|
227
227
|
import "./chunk-42NQ7AVG.js";
|
|
228
228
|
import "./chunk-TMSXWOBZ.js";
|
|
229
229
|
import "./chunk-7RXCMVFQ.js";
|
|
230
|
-
import "./chunk-
|
|
230
|
+
import "./chunk-CHVU4RE5.js";
|
|
231
231
|
import "./chunk-IC2TCZJ2.js";
|
|
232
232
|
import "./chunk-D24OXEPB.js";
|
|
233
233
|
import "./chunk-5RUE3GHQ.js";
|
|
234
|
-
import "./chunk-
|
|
234
|
+
import "./chunk-FYKIEOG6.js";
|
|
235
235
|
import "./chunk-2MR3MFQB.js";
|
|
236
|
-
import "./chunk-
|
|
236
|
+
import "./chunk-UHGHTOR5.js";
|
|
237
237
|
import "./chunk-HLTAPHZZ.js";
|
|
238
238
|
import "./chunk-YF6FMLBT.js";
|
|
239
239
|
import "./chunk-W4PGTAS2.js";
|
|
@@ -241,7 +241,7 @@ import "./chunk-KQDNAXOM.js";
|
|
|
241
241
|
import "./chunk-35NN3ZFA.js";
|
|
242
242
|
import "./chunk-FTYDWKO6.js";
|
|
243
243
|
import "./chunk-T4WDJPEZ.js";
|
|
244
|
-
import "./chunk-
|
|
244
|
+
import "./chunk-57INMZ6F.js";
|
|
245
245
|
import "./chunk-D7FOKIY5.js";
|
|
246
246
|
import "./chunk-CNWC274T.js";
|
|
247
247
|
import "./chunk-GDASG7NC.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/core",
|
|
3
|
-
"version": "9.7.
|
|
3
|
+
"version": "9.7.7",
|
|
4
4
|
"description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -3000,7 +3000,7 @@
|
|
|
3000
3000
|
"core"
|
|
3001
3001
|
],
|
|
3002
3002
|
"peerDependencies": {
|
|
3003
|
-
"@remnic/coding-graph": "^9.7.
|
|
3003
|
+
"@remnic/coding-graph": "^9.7.7"
|
|
3004
3004
|
},
|
|
3005
3005
|
"peerDependenciesMeta": {
|
|
3006
3006
|
"@remnic/coding-graph": {
|
package/src/access-boundary.ts
CHANGED
|
@@ -193,6 +193,8 @@ export interface OperationContext {
|
|
|
193
193
|
readonly service: EngramAccessService;
|
|
194
194
|
readonly authenticatedPrincipal?: string;
|
|
195
195
|
readonly operatorPrincipal?: string;
|
|
196
|
+
/** Per-request cancellation supplied by transports that own a request lifecycle. */
|
|
197
|
+
readonly abortSignal?: AbortSignal;
|
|
196
198
|
/** Server-resolved connector identity (Phase 1 provenance). Set by the HTTP auth boundary from the matched token's connector; flows to write handlers so frontmatter records which connector submitted the memory. Client-supplied values are always overridden. */
|
|
197
199
|
readonly sourceConnector?: string;
|
|
198
200
|
readonly hooks?: OperationHooks;
|
package/src/access-http.ts
CHANGED
|
@@ -854,7 +854,7 @@ export class EngramAccessHttpServer {
|
|
|
854
854
|
}
|
|
855
855
|
|
|
856
856
|
if (req.method === "POST" && pathname === "/mcp") {
|
|
857
|
-
await this.handleMcpRequest(req, res);
|
|
857
|
+
await this.handleMcpRequest(req, res, abortSignal);
|
|
858
858
|
return;
|
|
859
859
|
}
|
|
860
860
|
|
|
@@ -2982,7 +2982,11 @@ export class EngramAccessHttpServer {
|
|
|
2982
2982
|
req.once("error", cleanup);
|
|
2983
2983
|
}
|
|
2984
2984
|
|
|
2985
|
-
private async handleMcpRequest(
|
|
2985
|
+
private async handleMcpRequest(
|
|
2986
|
+
req: IncomingMessage,
|
|
2987
|
+
res: ServerResponse,
|
|
2988
|
+
abortSignal: AbortSignal,
|
|
2989
|
+
): Promise<void> {
|
|
2986
2990
|
// Reject requests that advertise an unknown MCP protocol version in
|
|
2987
2991
|
// the streamable-HTTP `MCP-Protocol-Version` header. Absent or
|
|
2988
2992
|
// valid → proceed. Unknown → 400 with a JSON-RPC-shaped error so
|
|
@@ -3143,6 +3147,7 @@ export class EngramAccessHttpServer {
|
|
|
3143
3147
|
? () => this.ensureWriteRateLimitAvailable()
|
|
3144
3148
|
: undefined,
|
|
3145
3149
|
sourceConnector: this.resolveConnector(req),
|
|
3150
|
+
abortSignal,
|
|
3146
3151
|
});
|
|
3147
3152
|
|
|
3148
3153
|
if (isMcpWrite && response !== null) {
|
|
@@ -3166,6 +3171,14 @@ export class EngramAccessHttpServer {
|
|
|
3166
3171
|
this.recordWriteRateLimitHit();
|
|
3167
3172
|
}
|
|
3168
3173
|
}
|
|
3174
|
+
// A mutating tool may have committed just before the client disconnected.
|
|
3175
|
+
// Record that side effect above, then honor cancellation before emitting
|
|
3176
|
+
// any response. Read-only calls reach this guard without accounting.
|
|
3177
|
+
if (abortSignal.aborted) {
|
|
3178
|
+
throw isAbortError(abortSignal.reason)
|
|
3179
|
+
? abortSignal.reason
|
|
3180
|
+
: abortError("HTTP client disconnected");
|
|
3181
|
+
}
|
|
3169
3182
|
if (response === null) {
|
|
3170
3183
|
res.statusCode = 202;
|
|
3171
3184
|
res.end();
|
|
@@ -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
|
+
});
|