@remnic/core 9.3.764 → 9.3.766
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-admin-ops-surface.d.ts +1 -1
- package/dist/access-admin-ops-surface.js +9 -9
- package/dist/access-boundary.d.ts +1 -1
- package/dist/access-boundary.js +10 -10
- package/dist/access-cli.js +20 -20
- package/dist/access-http.d.ts +13 -2
- package/dist/access-http.js +14 -14
- package/dist/access-identity-continuity-surface.js +9 -9
- package/dist/access-lcm-surface.d.ts +1 -1
- package/dist/access-lcm-surface.js +9 -9
- package/dist/access-mcp.d.ts +1 -1
- package/dist/access-mcp.js +13 -13
- package/dist/access-observe-write-surface.d.ts +1 -1
- package/dist/access-observe-write-surface.js +9 -9
- package/dist/access-operations-batch.js +11 -11
- package/dist/access-operations.d.ts +1 -1
- package/dist/access-operations.js +12 -12
- package/dist/access-recall-surface.d.ts +1 -1
- package/dist/access-recall-surface.js +9 -9
- package/dist/{access-service-CAoJ4qN8.d.ts → access-service-CdW6B2qQ.d.ts} +3 -5
- package/dist/access-service.d.ts +1 -1
- package/dist/access-service.js +9 -9
- package/dist/access-surface-catalog.d.ts +1 -1
- package/dist/active-recall.js +1 -1
- package/dist/{chunk-NKP6II6G.js → chunk-4HYFKIGH.js} +6 -6
- package/dist/{chunk-CGLRKKMF.js → chunk-4JG55YV3.js} +3 -3
- package/dist/{chunk-BRYPUVJU.js → chunk-CKNAVW34.js} +2 -2
- package/dist/{chunk-WRFKZEO6.js → chunk-DMKMQTBH.js} +10 -2
- package/dist/chunk-DMKMQTBH.js.map +1 -0
- package/dist/{chunk-RJCQPRLB.js → chunk-GLERZDM7.js} +3 -3
- package/dist/{chunk-2V6CTSSA.js → chunk-HBOPSFQQ.js} +8 -8
- package/dist/{chunk-N3QDLE2N.js → chunk-IGVKWKR2.js} +3 -3
- package/dist/{chunk-422OELQ3.js → chunk-J3UJJZKI.js} +44 -7
- package/dist/chunk-J3UJJZKI.js.map +1 -0
- package/dist/{chunk-5CEJH5ZN.js → chunk-K2I3AWBR.js} +10 -2
- package/dist/chunk-K2I3AWBR.js.map +1 -0
- package/dist/{chunk-5JF4DIKZ.js → chunk-MSGPLITO.js} +2 -2
- package/dist/{chunk-K43PI6DQ.js → chunk-MYNRJGOY.js} +2 -2
- package/dist/{chunk-UQEE4H6Z.js → chunk-OAREUC7N.js} +13 -7
- package/dist/chunk-OAREUC7N.js.map +1 -0
- package/dist/{chunk-RIOJRLJ6.js → chunk-PA5ALD3S.js} +6 -6
- package/dist/{chunk-2HAG7JOI.js → chunk-T4AWNIXJ.js} +48 -58
- package/dist/chunk-T4AWNIXJ.js.map +1 -0
- package/dist/{chunk-P6ZZ4GX4.js → chunk-TVLN5EZZ.js} +3 -3
- package/dist/{chunk-KXG7HHIQ.js → chunk-U5GFYMV7.js} +2 -2
- package/dist/{chunk-4XL3CVII.js → chunk-WRBDUNSW.js} +2 -2
- package/dist/{chunk-YNQ6DFSV.js → chunk-WU46YPY6.js} +8 -1
- package/dist/chunk-WU46YPY6.js.map +1 -0
- package/dist/{chunk-LQ6JI4VH.js → chunk-XG4NBBRW.js} +10 -2
- package/dist/chunk-XG4NBBRW.js.map +1 -0
- package/dist/{chunk-SANZHXY2.js → chunk-ZZMRXJFY.js} +29 -3
- package/dist/chunk-ZZMRXJFY.js.map +1 -0
- package/dist/{cli-WRmxvfc_.d.ts → cli-Dfnc66gs.d.ts} +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +19 -19
- package/dist/config.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +20 -20
- package/dist/mcp-memory-inspector-app.d.ts +1 -1
- package/dist/namespaces/migrate.js +9 -9
- package/dist/namespaces/search.js +8 -8
- package/dist/operator-toolkit.js +11 -11
- package/dist/orchestrator.js +20 -20
- package/dist/qmd.js +2 -2
- package/dist/resume-bundles.js +2 -2
- package/dist/search/factory.js +7 -7
- package/dist/search/index.js +7 -7
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/orama-backend.js +2 -2
- package/dist/search/port.d.ts +4 -3
- package/dist/search/port.js +3 -1
- package/dist/search/remote-backend.js +2 -1
- package/package.json +2 -2
- package/src/access-http-cancellation.test.ts +240 -0
- package/src/access-http.ts +54 -2
- package/src/access-recall-surface.ts +1 -0
- package/src/access-service.ts +10 -10
- package/src/config.ts +5 -261
- package/src/namespaces/scope-profile-config.test.ts +60 -0
- package/src/namespaces/scope-profile-config.ts +243 -0
- package/src/search/lancedb-backend.ts +8 -0
- package/src/search/meilisearch-backend.ts +8 -0
- package/src/search/orama-backend.ts +8 -0
- package/src/search/port.ts +15 -2
- package/src/search/remote-backend.ts +32 -3
- package/dist/chunk-2HAG7JOI.js.map +0 -1
- package/dist/chunk-422OELQ3.js.map +0 -1
- package/dist/chunk-5CEJH5ZN.js.map +0 -1
- package/dist/chunk-LQ6JI4VH.js.map +0 -1
- package/dist/chunk-SANZHXY2.js.map +0 -1
- package/dist/chunk-UQEE4H6Z.js.map +0 -1
- package/dist/chunk-WRFKZEO6.js.map +0 -1
- package/dist/chunk-YNQ6DFSV.js.map +0 -1
- /package/dist/{chunk-NKP6II6G.js.map → chunk-4HYFKIGH.js.map} +0 -0
- /package/dist/{chunk-CGLRKKMF.js.map → chunk-4JG55YV3.js.map} +0 -0
- /package/dist/{chunk-BRYPUVJU.js.map → chunk-CKNAVW34.js.map} +0 -0
- /package/dist/{chunk-RJCQPRLB.js.map → chunk-GLERZDM7.js.map} +0 -0
- /package/dist/{chunk-2V6CTSSA.js.map → chunk-HBOPSFQQ.js.map} +0 -0
- /package/dist/{chunk-N3QDLE2N.js.map → chunk-IGVKWKR2.js.map} +0 -0
- /package/dist/{chunk-5JF4DIKZ.js.map → chunk-MSGPLITO.js.map} +0 -0
- /package/dist/{chunk-K43PI6DQ.js.map → chunk-MYNRJGOY.js.map} +0 -0
- /package/dist/{chunk-RIOJRLJ6.js.map → chunk-PA5ALD3S.js.map} +0 -0
- /package/dist/{chunk-P6ZZ4GX4.js.map → chunk-TVLN5EZZ.js.map} +0 -0
- /package/dist/{chunk-KXG7HHIQ.js.map → chunk-U5GFYMV7.js.map} +0 -0
- /package/dist/{chunk-4XL3CVII.js.map → chunk-WRBDUNSW.js.map} +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { request as httpRequest } from "node:http";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
|
|
5
|
+
import { EngramAccessHttpServer } from "./access-http.js";
|
|
6
|
+
import { EngramAccessService, type EngramAccessRecallRequest } from "./access-service.js";
|
|
7
|
+
|
|
8
|
+
function deferred<T = void>(): {
|
|
9
|
+
promise: Promise<T>;
|
|
10
|
+
resolve: (value: T | PromiseLike<T>) => void;
|
|
11
|
+
reject: (reason?: unknown) => void;
|
|
12
|
+
} {
|
|
13
|
+
let resolve!: (value: T | PromiseLike<T>) => void;
|
|
14
|
+
let reject!: (reason?: unknown) => void;
|
|
15
|
+
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
|
16
|
+
resolve = resolvePromise;
|
|
17
|
+
reject = rejectPromise;
|
|
18
|
+
});
|
|
19
|
+
return { promise, resolve, reject };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function waitFor<T>(promise: Promise<T>, timeoutMs = 1_000): Promise<T> {
|
|
23
|
+
return Promise.race([
|
|
24
|
+
promise,
|
|
25
|
+
new Promise<never>((_resolve, reject) => {
|
|
26
|
+
const timer = setTimeout(() => reject(new Error(`timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
27
|
+
timer.unref?.();
|
|
28
|
+
}),
|
|
29
|
+
]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test("HTTP recall aborts in-flight work when the client disconnects", async () => {
|
|
33
|
+
const recallStarted = deferred<void>();
|
|
34
|
+
const recallSettled = deferred<void>();
|
|
35
|
+
const forceRelease = deferred<void>();
|
|
36
|
+
let observedSignal: AbortSignal | undefined;
|
|
37
|
+
const service = {
|
|
38
|
+
recall: async (input: EngramAccessRecallRequest) => {
|
|
39
|
+
observedSignal = input.abortSignal;
|
|
40
|
+
recallStarted.resolve();
|
|
41
|
+
try {
|
|
42
|
+
await Promise.race([
|
|
43
|
+
forceRelease.promise,
|
|
44
|
+
new Promise<void>((_resolve, reject) => {
|
|
45
|
+
input.abortSignal?.addEventListener(
|
|
46
|
+
"abort",
|
|
47
|
+
() => reject(input.abortSignal?.reason),
|
|
48
|
+
{ once: true },
|
|
49
|
+
);
|
|
50
|
+
}),
|
|
51
|
+
]);
|
|
52
|
+
} finally {
|
|
53
|
+
recallSettled.resolve();
|
|
54
|
+
}
|
|
55
|
+
if (input.abortSignal?.aborted) {
|
|
56
|
+
throw input.abortSignal.reason;
|
|
57
|
+
}
|
|
58
|
+
return {} as never;
|
|
59
|
+
},
|
|
60
|
+
} as unknown as EngramAccessService;
|
|
61
|
+
const server = new EngramAccessHttpServer({
|
|
62
|
+
service,
|
|
63
|
+
port: 0,
|
|
64
|
+
authToken: "test-token",
|
|
65
|
+
adminConsoleEnabled: false,
|
|
66
|
+
});
|
|
67
|
+
const status = await server.start();
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const client = httpRequest({
|
|
71
|
+
host: "127.0.0.1",
|
|
72
|
+
port: status.port,
|
|
73
|
+
path: "/engram/v1/recall",
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: {
|
|
76
|
+
authorization: "Bearer test-token",
|
|
77
|
+
"content-type": "application/json",
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
client.on("error", () => {});
|
|
81
|
+
client.end(JSON.stringify({ query: "slow recall" }));
|
|
82
|
+
|
|
83
|
+
await waitFor(recallStarted.promise);
|
|
84
|
+
assert.ok(observedSignal, "the HTTP request signal must reach the recall service");
|
|
85
|
+
client.destroy();
|
|
86
|
+
|
|
87
|
+
await waitFor(recallSettled.promise);
|
|
88
|
+
assert.equal(observedSignal.aborted, true);
|
|
89
|
+
} finally {
|
|
90
|
+
forceRelease.resolve();
|
|
91
|
+
await server.stop();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("a disconnected recall queued on the principal budget lock never starts", async () => {
|
|
96
|
+
const firstStarted = deferred<void>();
|
|
97
|
+
const releaseFirst = deferred<void>();
|
|
98
|
+
let secondStarted = false;
|
|
99
|
+
const service = Object.create(EngramAccessService.prototype) as EngramAccessService;
|
|
100
|
+
const lockHost = service as unknown as {
|
|
101
|
+
budgetLocks: Map<string, Promise<void>>;
|
|
102
|
+
withBudgetLock<T>(
|
|
103
|
+
principal: string,
|
|
104
|
+
abortSignal: AbortSignal | undefined,
|
|
105
|
+
operation: () => Promise<T>,
|
|
106
|
+
): Promise<T>;
|
|
107
|
+
};
|
|
108
|
+
lockHost.budgetLocks = new Map();
|
|
109
|
+
|
|
110
|
+
const first = lockHost.withBudgetLock("principal", undefined, async () => {
|
|
111
|
+
firstStarted.resolve();
|
|
112
|
+
await releaseFirst.promise;
|
|
113
|
+
});
|
|
114
|
+
await firstStarted.promise;
|
|
115
|
+
|
|
116
|
+
const controller = new AbortController();
|
|
117
|
+
const second = lockHost.withBudgetLock("principal", controller.signal, async () => {
|
|
118
|
+
secondStarted = true;
|
|
119
|
+
});
|
|
120
|
+
controller.abort();
|
|
121
|
+
releaseFirst.resolve();
|
|
122
|
+
|
|
123
|
+
await first;
|
|
124
|
+
await assert.rejects(waitFor(second), (error: Error) => error.name === "AbortError");
|
|
125
|
+
assert.equal(secondStarted, false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("an aborted queued recall does not poison the principal budget lock", async () => {
|
|
129
|
+
const firstStarted = deferred<void>();
|
|
130
|
+
const releaseFirst = deferred<void>();
|
|
131
|
+
let secondStarted = false;
|
|
132
|
+
let thirdStarted = false;
|
|
133
|
+
const service = Object.create(EngramAccessService.prototype) as EngramAccessService;
|
|
134
|
+
const lockHost = service as unknown as {
|
|
135
|
+
budgetLocks: Map<string, Promise<void>>;
|
|
136
|
+
withBudgetLock<T>(
|
|
137
|
+
principal: string,
|
|
138
|
+
abortSignal: AbortSignal | undefined,
|
|
139
|
+
operation: () => Promise<T>,
|
|
140
|
+
): Promise<T>;
|
|
141
|
+
};
|
|
142
|
+
lockHost.budgetLocks = new Map();
|
|
143
|
+
|
|
144
|
+
const first = lockHost.withBudgetLock("principal", undefined, async () => {
|
|
145
|
+
firstStarted.resolve();
|
|
146
|
+
await releaseFirst.promise;
|
|
147
|
+
});
|
|
148
|
+
await firstStarted.promise;
|
|
149
|
+
|
|
150
|
+
const controller = new AbortController();
|
|
151
|
+
const second = lockHost.withBudgetLock("principal", controller.signal, async () => {
|
|
152
|
+
secondStarted = true;
|
|
153
|
+
});
|
|
154
|
+
const third = lockHost.withBudgetLock("principal", undefined, async () => {
|
|
155
|
+
thirdStarted = true;
|
|
156
|
+
});
|
|
157
|
+
controller.abort();
|
|
158
|
+
releaseFirst.resolve();
|
|
159
|
+
|
|
160
|
+
await first;
|
|
161
|
+
await assert.rejects(waitFor(second), (error: Error) => error.name === "AbortError");
|
|
162
|
+
await waitFor(third);
|
|
163
|
+
assert.equal(secondStarted, false);
|
|
164
|
+
assert.equal(thirdStarted, true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
function makeRecallServiceProbe(): {
|
|
168
|
+
service: EngramAccessService;
|
|
169
|
+
setExecuteRecall: (execute: () => Promise<void>) => void;
|
|
170
|
+
requestFingerprint: () => unknown;
|
|
171
|
+
stored: () => boolean;
|
|
172
|
+
} {
|
|
173
|
+
const service = Object.create(EngramAccessService.prototype) as EngramAccessService;
|
|
174
|
+
let executeRecall = async () => {};
|
|
175
|
+
let capturedFingerprint: unknown;
|
|
176
|
+
let didStore = false;
|
|
177
|
+
const host = service as unknown as {
|
|
178
|
+
budgetLocks: Map<string, Promise<void>>;
|
|
179
|
+
orchestrator: { config: Record<string, unknown> };
|
|
180
|
+
resolveRequestPrincipal: () => string;
|
|
181
|
+
executeRecall: () => Promise<{ response: Record<string, never>; budgetRecordPrincipal: null }>;
|
|
182
|
+
handleIdempotentRead: (options: {
|
|
183
|
+
requestFingerprint: unknown;
|
|
184
|
+
execute: () => Promise<Record<string, never>>;
|
|
185
|
+
}) => Promise<Record<string, never>>;
|
|
186
|
+
};
|
|
187
|
+
host.budgetLocks = new Map();
|
|
188
|
+
host.orchestrator = { config: {} };
|
|
189
|
+
host.resolveRequestPrincipal = () => "principal";
|
|
190
|
+
host.executeRecall = async () => {
|
|
191
|
+
await executeRecall();
|
|
192
|
+
return { response: {}, budgetRecordPrincipal: null };
|
|
193
|
+
};
|
|
194
|
+
host.handleIdempotentRead = async (options) => {
|
|
195
|
+
capturedFingerprint = options.requestFingerprint;
|
|
196
|
+
const response = await options.execute();
|
|
197
|
+
didStore = true;
|
|
198
|
+
return response;
|
|
199
|
+
};
|
|
200
|
+
return {
|
|
201
|
+
service,
|
|
202
|
+
setExecuteRecall: (execute) => {
|
|
203
|
+
executeRecall = execute;
|
|
204
|
+
},
|
|
205
|
+
requestFingerprint: () => capturedFingerprint,
|
|
206
|
+
stored: () => didStore,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
test("recall excludes its abort signal from the idempotency fingerprint", async () => {
|
|
211
|
+
const probe = makeRecallServiceProbe();
|
|
212
|
+
await probe.service.recall({
|
|
213
|
+
query: "same payload",
|
|
214
|
+
idempotencyKey: "same-key",
|
|
215
|
+
abortSignal: new AbortController().signal,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
assert.equal(
|
|
219
|
+
Object.hasOwn(probe.requestFingerprint() as object, "abortSignal"),
|
|
220
|
+
false,
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("recall does not store a result when the pipeline consumes an abort", async () => {
|
|
225
|
+
const probe = makeRecallServiceProbe();
|
|
226
|
+
const controller = new AbortController();
|
|
227
|
+
probe.setExecuteRecall(async () => {
|
|
228
|
+
controller.abort();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
await assert.rejects(
|
|
232
|
+
probe.service.recall({
|
|
233
|
+
query: "cancelled",
|
|
234
|
+
idempotencyKey: "cancelled-key",
|
|
235
|
+
abortSignal: controller.signal,
|
|
236
|
+
}),
|
|
237
|
+
(error: Error) => error.name === "AbortError",
|
|
238
|
+
);
|
|
239
|
+
assert.equal(probe.stored(), false);
|
|
240
|
+
});
|
package/src/access-http.ts
CHANGED
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { fileURLToPath, URL } from "node:url";
|
|
8
8
|
import { gunzipSync } from "node:zlib";
|
|
9
9
|
import { log } from "./logger.js";
|
|
10
|
+
import { abortError, isAbortError } from "./abort-error.js";
|
|
10
11
|
import { EngramAccessInputError, type EngramAccessService, type EngramAccessMemoryResponse, type EngramAccessWriteResponse } from "./access-service.js";
|
|
11
12
|
import { CorrectionContractError } from "./correction/correction-contract.js";
|
|
12
13
|
import { WearablesInputError } from "./wearables/errors.js";
|
|
@@ -38,6 +39,12 @@ import { handleChatMessage, handleChatEventsSSE } from "./chat/chat-http.js";
|
|
|
38
39
|
// server lifecycle (issue #1685 item 1 / #1687 Thread 21).
|
|
39
40
|
import { cleanupExpiredChatSessions } from "./chat/chat-session.js";
|
|
40
41
|
|
|
42
|
+
export interface AccessHttpReadinessState {
|
|
43
|
+
ready: boolean;
|
|
44
|
+
warmupAttempts: number;
|
|
45
|
+
lastError?: string | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
41
48
|
export interface EngramAccessHttpServerOptions {
|
|
42
49
|
service: EngramAccessService;
|
|
43
50
|
host?: string;
|
|
@@ -66,6 +73,11 @@ export interface EngramAccessHttpServerOptions {
|
|
|
66
73
|
emitLegacyTools?: boolean;
|
|
67
74
|
/** Optional authenticated admin dashboard/config controls supplied by the host server. */
|
|
68
75
|
adminControls?: RemnicAdminControls;
|
|
76
|
+
/**
|
|
77
|
+
* Standalone readiness state. Defaults to ready so embedded hosts keep their
|
|
78
|
+
* existing health behavior.
|
|
79
|
+
*/
|
|
80
|
+
readiness?: () => AccessHttpReadinessState;
|
|
69
81
|
}
|
|
70
82
|
|
|
71
83
|
export interface EngramAccessHttpServerStatus {
|
|
@@ -279,6 +291,7 @@ export class EngramAccessHttpServer {
|
|
|
279
291
|
private readonly adminControls?: RemnicAdminControls;
|
|
280
292
|
private readonly trustPrincipalHeader: boolean;
|
|
281
293
|
private readonly adapterRegistry: AdapterRegistry | null;
|
|
294
|
+
private readonly readiness: () => AccessHttpReadinessState;
|
|
282
295
|
private readonly writeRequestTimestamps: number[] = [];
|
|
283
296
|
private readonly mcpServer: EngramMcpServer;
|
|
284
297
|
private server: Server | null = null;
|
|
@@ -319,6 +332,7 @@ export class EngramAccessHttpServer {
|
|
|
319
332
|
this.adminConsolePrefillToken = options.adminConsolePrefillToken === true ? this.authToken : undefined;
|
|
320
333
|
this.adminControls = options.adminControls;
|
|
321
334
|
this.trustPrincipalHeader = options.trustPrincipalHeader === true;
|
|
335
|
+
this.readiness = options.readiness ?? (() => ({ ready: true, warmupAttempts: 0 }));
|
|
322
336
|
this.adapterRegistry = options.enableAdapters !== false
|
|
323
337
|
? (options.adapterRegistry ?? new AdapterRegistry())
|
|
324
338
|
: null;
|
|
@@ -344,8 +358,22 @@ export class EngramAccessHttpServer {
|
|
|
344
358
|
|
|
345
359
|
const server = createServer((req, res) => {
|
|
346
360
|
const correlationId = randomUUID();
|
|
361
|
+
const abortController = new AbortController();
|
|
362
|
+
const abortDisconnectedRequest = () => {
|
|
363
|
+
if (!res.writableFinished && !abortController.signal.aborted) {
|
|
364
|
+
abortController.abort(abortError("HTTP client disconnected"));
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
req.once("aborted", abortDisconnectedRequest);
|
|
368
|
+
res.once("close", abortDisconnectedRequest);
|
|
347
369
|
correlationIdStore.run(correlationId, () => {
|
|
348
|
-
void this.handle(req, res, correlationId).catch((err) => {
|
|
370
|
+
void this.handle(req, res, correlationId, abortController.signal).catch((err) => {
|
|
371
|
+
if (isAbortError(err)) {
|
|
372
|
+
if (!res.destroyed && !res.writableEnded) {
|
|
373
|
+
res.end();
|
|
374
|
+
}
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
349
377
|
log.debug(`engram access HTTP request failed [${correlationId}]: ${err}`);
|
|
350
378
|
if (err instanceof HttpError) {
|
|
351
379
|
const payload: Record<string, unknown> = { error: err.message, code: err.code };
|
|
@@ -370,6 +398,9 @@ export class EngramAccessHttpServer {
|
|
|
370
398
|
err,
|
|
371
399
|
);
|
|
372
400
|
this.respondJson(res, 500, { error: "internal_error", code: "internal_error" });
|
|
401
|
+
}).finally(() => {
|
|
402
|
+
req.off("aborted", abortDisconnectedRequest);
|
|
403
|
+
res.off("close", abortDisconnectedRequest);
|
|
373
404
|
});
|
|
374
405
|
});
|
|
375
406
|
});
|
|
@@ -583,7 +614,12 @@ export class EngramAccessHttpServer {
|
|
|
583
614
|
return queryDisclosure;
|
|
584
615
|
}
|
|
585
616
|
|
|
586
|
-
private async handle(
|
|
617
|
+
private async handle(
|
|
618
|
+
req: IncomingMessage,
|
|
619
|
+
res: ServerResponse,
|
|
620
|
+
correlationId: string,
|
|
621
|
+
abortSignal: AbortSignal,
|
|
622
|
+
): Promise<void> {
|
|
587
623
|
const parsed = new URL(req.url ?? "/", `http://${hostToUrlAuthority(this.host)}`);
|
|
588
624
|
const pathname = parsed.pathname;
|
|
589
625
|
|
|
@@ -591,6 +627,21 @@ export class EngramAccessHttpServer {
|
|
|
591
627
|
return;
|
|
592
628
|
}
|
|
593
629
|
|
|
630
|
+
if (req.method === "GET" && pathname === "/engram/v1/health") {
|
|
631
|
+
const readiness = this.readiness();
|
|
632
|
+
if (!readiness.ready) {
|
|
633
|
+
this.respondJson(res, 503, {
|
|
634
|
+
ok: false,
|
|
635
|
+
ready: false,
|
|
636
|
+
warmupAttempts: readiness.warmupAttempts,
|
|
637
|
+
lastError: readiness.lastError ?? null,
|
|
638
|
+
code: "not_ready",
|
|
639
|
+
});
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
}
|
|
644
|
+
|
|
594
645
|
if (!this.isAuthorized(req, pathname)) {
|
|
595
646
|
const body = JSON.stringify({ error: "unauthorized", code: "unauthorized" });
|
|
596
647
|
res.writeHead(401, {
|
|
@@ -767,6 +818,7 @@ export class EngramAccessHttpServer {
|
|
|
767
818
|
...(tags !== undefined ? { tags } : {}),
|
|
768
819
|
...(tagMatch !== undefined ? { tagMatch } : {}),
|
|
769
820
|
...(includeLowConfidence ? { includeLowConfidence: true } : {}),
|
|
821
|
+
abortSignal,
|
|
770
822
|
});
|
|
771
823
|
this.respondJson(res, 200, response);
|
|
772
824
|
return;
|
|
@@ -823,6 +823,7 @@ export class AccessRecallSurface {
|
|
|
823
823
|
...(authenticatedPrincipal ? { principalOverride: authenticatedPrincipal } : {}),
|
|
824
824
|
...(asOf !== undefined ? { asOf } : {}),
|
|
825
825
|
...(request.includeLowConfidence === true ? { includeLowConfidence: true } : {}),
|
|
826
|
+
...(request.abortSignal ? { abortSignal: request.abortSignal } : {}),
|
|
826
827
|
};
|
|
827
828
|
const startedAt = Date.now();
|
|
828
829
|
const context = await this.deps.orchestrator.recall(query, request.sessionKey, recallOptions);
|
package/src/access-service.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { readdirSync, unlinkSync } from "node:fs";
|
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { ZodError } from "zod";
|
|
7
7
|
import { AccessIdempotencyStore, hashAccessIdempotencyPayload } from "./access-idempotency.js";
|
|
8
|
+
import { throwIfAborted } from "./abort-error.js";
|
|
8
9
|
import { resolveNamespaceCapabilities,
|
|
9
10
|
resolveMemoryLifecycleCapabilities,
|
|
10
11
|
resolveQmdCapabilities,
|
|
@@ -401,12 +402,10 @@ export interface EngramAccessRecallRequest {
|
|
|
401
402
|
topK?: number;
|
|
402
403
|
mode?: RecallPlanMode | "auto";
|
|
403
404
|
includeDebug?: boolean;
|
|
405
|
+
abortSignal?: AbortSignal;
|
|
404
406
|
/**
|
|
405
|
-
* Recall disclosure depth
|
|
406
|
-
*
|
|
407
|
-
* this field is equivalent to passing `"chunk"` and preserves pre-#677
|
|
408
|
-
* behavior. Surfaces (CLI / HTTP / MCP) and per-level token telemetry
|
|
409
|
-
* are wired in subsequent PRs of #677.
|
|
407
|
+
* Recall disclosure depth. Omitting it preserves the `"chunk"` default.
|
|
408
|
+
* Other accepted values are `"section"` and `"raw"`.
|
|
410
409
|
*/
|
|
411
410
|
disclosure?: RecallDisclosure;
|
|
412
411
|
/**
|
|
@@ -2421,7 +2420,7 @@ export class EngramAccessService {
|
|
|
2421
2420
|
}
|
|
2422
2421
|
}
|
|
2423
2422
|
|
|
2424
|
-
private async withBudgetLock<T>(principal: string, fn: () => Promise<T>): Promise<T> {
|
|
2423
|
+
private async withBudgetLock<T>(principal: string, abortSignal: AbortSignal | undefined, fn: () => Promise<T>): Promise<T> {
|
|
2425
2424
|
const key = principal || "__anonymous__";
|
|
2426
2425
|
const previous = this.budgetLocks.get(key) ?? Promise.resolve();
|
|
2427
2426
|
let release!: () => void;
|
|
@@ -2430,9 +2429,9 @@ export class EngramAccessService {
|
|
|
2430
2429
|
});
|
|
2431
2430
|
const queued = previous.then(() => current, () => current);
|
|
2432
2431
|
this.budgetLocks.set(key, queued);
|
|
2433
|
-
|
|
2434
2432
|
await previous.catch(() => {});
|
|
2435
2433
|
try {
|
|
2434
|
+
throwIfAborted(abortSignal);
|
|
2436
2435
|
return await fn();
|
|
2437
2436
|
} finally {
|
|
2438
2437
|
release();
|
|
@@ -2736,6 +2735,7 @@ export class EngramAccessService {
|
|
|
2736
2735
|
throw new EngramAccessInputError("query is required");
|
|
2737
2736
|
}
|
|
2738
2737
|
const normalizedRequest = { ...request, query };
|
|
2738
|
+
const { abortSignal: _abortSignal, ...requestFingerprint } = normalizedRequest;
|
|
2739
2739
|
const authenticatedPrincipal = request.authenticatedPrincipal?.trim();
|
|
2740
2740
|
const principal = this.resolveRequestPrincipal(request.sessionKey, authenticatedPrincipal);
|
|
2741
2741
|
if (resolveNamespaceCapabilities(this.orchestrator.config).namespaces && !principal) {
|
|
@@ -2743,15 +2743,15 @@ export class EngramAccessService {
|
|
|
2743
2743
|
"authentication required: namespaces are enabled and no principal was supplied",
|
|
2744
2744
|
);
|
|
2745
2745
|
}
|
|
2746
|
-
|
|
2747
|
-
return this.withBudgetLock(budgetLockPrincipal, async () => {
|
|
2746
|
+
return this.withBudgetLock(principal ?? "default", request.abortSignal, async () => {
|
|
2748
2747
|
let budgetRecordPrincipal: string | null = null;
|
|
2749
2748
|
const response = await this.handleIdempotentRead({
|
|
2750
2749
|
operation: "recall",
|
|
2751
2750
|
idempotencyKey: request.idempotencyKey,
|
|
2752
|
-
requestFingerprint
|
|
2751
|
+
requestFingerprint,
|
|
2753
2752
|
execute: async () => {
|
|
2754
2753
|
const result = await this.executeRecall(normalizedRequest);
|
|
2754
|
+
throwIfAborted(request.abortSignal);
|
|
2755
2755
|
budgetRecordPrincipal = result.budgetRecordPrincipal;
|
|
2756
2756
|
return result.response;
|
|
2757
2757
|
},
|