@remnic/core 9.69.78 → 9.69.80
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.js +2 -2
- package/dist/access-authorization-probe.js +3 -3
- package/dist/access-boundary.js +2 -2
- package/dist/access-cli.js +21 -21
- package/dist/access-extraction-force-flush.js +2 -2
- package/dist/access-http-briefing.js +3 -3
- package/dist/access-http-lcm-compaction.d.ts +2 -1
- package/dist/access-http-lcm-compaction.d.ts.map +1 -1
- package/dist/access-http-lcm-compaction.js +1 -1
- package/dist/access-http-lifecycle-flush.d.ts.map +1 -1
- package/dist/access-http-lifecycle-flush.js +2 -2
- package/dist/access-http-query.js +2 -2
- package/dist/access-http.js +16 -16
- package/dist/access-identity-continuity-surface.js +2 -2
- package/dist/access-lcm-surface.js +2 -2
- package/dist/access-mcp.js +9 -9
- package/dist/access-namespace-preflight.js +2 -2
- package/dist/access-observe-write-surface.js +2 -2
- package/dist/access-operations-batch.js +3 -3
- package/dist/access-operations.js +8 -8
- package/dist/access-recall-concurrency.js +2 -2
- package/dist/access-recall-response.js +2 -2
- package/dist/access-recall-surface.js +2 -2
- package/dist/access-service-helpers.d.ts +1 -0
- package/dist/access-service-helpers.d.ts.map +1 -1
- package/dist/access-service-helpers.js +1 -1
- package/dist/access-service.js +2 -2
- package/dist/access-surface-catalog.js +4 -4
- package/dist/{chunk-TIDEL77O.js → chunk-2BPPGNAQ.js} +4 -3
- package/dist/chunk-2BPPGNAQ.js.map +1 -0
- package/dist/{chunk-JA64OVTE.js → chunk-3GN2PEOT.js} +2 -2
- package/dist/{chunk-TIVPGSGO.js → chunk-4V44AA7O.js} +2 -2
- package/dist/{chunk-2L5FSNPC.js → chunk-5FNZ3G6X.js} +40 -11
- package/dist/chunk-5FNZ3G6X.js.map +1 -0
- package/dist/{chunk-TS6WB2CL.js → chunk-7QK5TUTW.js} +2 -2
- package/dist/{chunk-WGXUEXSV.js → chunk-B47OHVFT.js} +3 -3
- package/dist/chunk-B47OHVFT.js.map +1 -0
- package/dist/{chunk-3QL24R5Y.js → chunk-C2WWQ4OR.js} +1 -1
- package/dist/chunk-C2WWQ4OR.js.map +1 -0
- package/dist/{chunk-DZQ734SM.js → chunk-C7JAUNFC.js} +2 -2
- package/dist/{chunk-GZ3R6XOE.js → chunk-DJSJ4ULL.js} +22 -12
- package/dist/chunk-DJSJ4ULL.js.map +1 -0
- package/dist/{chunk-7R4DKXKP.js → chunk-GEX5NI36.js} +2 -2
- package/dist/{chunk-MJQ3QF7Y.js → chunk-HKFJ3PF3.js} +2 -2
- package/dist/{chunk-P6GTQGGB.js → chunk-JXMECXMF.js} +2 -2
- package/dist/{chunk-2IAAJS3R.js → chunk-KERNN6BC.js} +2 -2
- package/dist/{chunk-TPDBFYEG.js → chunk-MMCO4SRG.js} +17 -6
- package/dist/chunk-MMCO4SRG.js.map +1 -0
- package/dist/{chunk-BIFFCC62.js → chunk-NMBX4D7P.js} +2 -2
- package/dist/{chunk-AFZWKVEX.js → chunk-Q5TRJ4BX.js} +5 -5
- package/dist/{chunk-H4USONK4.js → chunk-QB75LKQQ.js} +2 -2
- package/dist/{chunk-OXE7KNFJ.js → chunk-RDEUG2M2.js} +3 -3
- package/dist/{chunk-3NYCO5Q5.js → chunk-SVZWVZ7E.js} +4 -4
- package/dist/chunk-SVZWVZ7E.js.map +1 -0
- package/dist/{chunk-B5TUL5KO.js → chunk-TEV2JGU5.js} +10 -10
- package/dist/{chunk-54CG7SO6.js → chunk-X22B4KTI.js} +4 -4
- package/dist/cli.js +17 -17
- package/dist/external-wiki-access.js +3 -3
- package/dist/index.js +21 -21
- package/dist/lcm/engine.d.ts +2 -2
- package/dist/lcm/engine.d.ts.map +1 -1
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +2 -2
- package/dist/lcm/queue.d.ts +1 -1
- package/dist/lcm/queue.d.ts.map +1 -1
- package/dist/lcm/queue.js +1 -1
- package/dist/memory-browse-operations.js +3 -3
- package/dist/orchestrator.js +21 -21
- package/dist/recall-navigation-operations.js +3 -3
- package/dist/recall-navigation.js +2 -2
- package/dist/support-passport/index.js +5 -5
- package/package.json +2 -2
- package/src/access-http-extraction-force-flush.test.ts +6 -2
- package/src/access-http-lcm-compaction.abort.test.ts +60 -0
- package/src/access-http-lcm-compaction.ts +20 -9
- package/src/access-http-lifecycle-flush.ts +1 -0
- package/src/access-http.test.ts +4 -2
- package/src/access-operations-batch.ts +1 -1
- package/src/access-service-helpers.ts +1 -0
- package/src/access-service.ts +2 -2
- package/src/lcm/engine.ts +39 -9
- package/src/lcm/queue.test.ts +19 -0
- package/src/lcm/queue.ts +16 -5
- package/src/lcm-engine.test.ts +72 -0
- package/dist/chunk-2L5FSNPC.js.map +0 -1
- package/dist/chunk-3NYCO5Q5.js.map +0 -1
- package/dist/chunk-3QL24R5Y.js.map +0 -1
- package/dist/chunk-GZ3R6XOE.js.map +0 -1
- package/dist/chunk-TIDEL77O.js.map +0 -1
- package/dist/chunk-TPDBFYEG.js.map +0 -1
- package/dist/chunk-WGXUEXSV.js.map +0 -1
- /package/dist/{chunk-JA64OVTE.js.map → chunk-3GN2PEOT.js.map} +0 -0
- /package/dist/{chunk-TIVPGSGO.js.map → chunk-4V44AA7O.js.map} +0 -0
- /package/dist/{chunk-TS6WB2CL.js.map → chunk-7QK5TUTW.js.map} +0 -0
- /package/dist/{chunk-DZQ734SM.js.map → chunk-C7JAUNFC.js.map} +0 -0
- /package/dist/{chunk-7R4DKXKP.js.map → chunk-GEX5NI36.js.map} +0 -0
- /package/dist/{chunk-MJQ3QF7Y.js.map → chunk-HKFJ3PF3.js.map} +0 -0
- /package/dist/{chunk-P6GTQGGB.js.map → chunk-JXMECXMF.js.map} +0 -0
- /package/dist/{chunk-2IAAJS3R.js.map → chunk-KERNN6BC.js.map} +0 -0
- /package/dist/{chunk-BIFFCC62.js.map → chunk-NMBX4D7P.js.map} +0 -0
- /package/dist/{chunk-AFZWKVEX.js.map → chunk-Q5TRJ4BX.js.map} +0 -0
- /package/dist/{chunk-H4USONK4.js.map → chunk-QB75LKQQ.js.map} +0 -0
- /package/dist/{chunk-OXE7KNFJ.js.map → chunk-RDEUG2M2.js.map} +0 -0
- /package/dist/{chunk-B5TUL5KO.js.map → chunk-TEV2JGU5.js.map} +0 -0
- /package/dist/{chunk-54CG7SO6.js.map → chunk-X22B4KTI.js.map} +0 -0
|
@@ -119,13 +119,17 @@ test("HTTP lifecycle flush preserves the scoped LCM compaction route", async ()
|
|
|
119
119
|
sessionKey: "pi:session-1",
|
|
120
120
|
namespace: "team-project",
|
|
121
121
|
});
|
|
122
|
-
|
|
122
|
+
const [request] = requests;
|
|
123
|
+
assert.ok(request);
|
|
124
|
+
assert.equal(request.abortSignal instanceof AbortSignal, true);
|
|
125
|
+
const { abortSignal: _abortSignal, ...rest } = request;
|
|
126
|
+
assert.deepEqual(rest, {
|
|
123
127
|
sessionKey: "pi:session-1",
|
|
124
128
|
namespace: "team-project",
|
|
125
129
|
cwd: "/workspace/project",
|
|
126
130
|
projectTag: "Acme/Webshop",
|
|
127
131
|
authenticatedPrincipal: undefined,
|
|
128
|
-
}
|
|
132
|
+
});
|
|
129
133
|
} finally {
|
|
130
134
|
await server.stop();
|
|
131
135
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { runLcmCompactionFlushHttp } from "./access-http-lcm-compaction.js";
|
|
5
|
+
|
|
6
|
+
test("#3077 aborted LCM flush does not start compaction", async () => {
|
|
7
|
+
let calls = 0;
|
|
8
|
+
const abortSignal = AbortSignal.abort();
|
|
9
|
+
await assert.rejects(
|
|
10
|
+
() =>
|
|
11
|
+
runLcmCompactionFlushHttp({
|
|
12
|
+
body: { sessionKey: "sess-1" },
|
|
13
|
+
service: {
|
|
14
|
+
async lcmCompactionFlush() {
|
|
15
|
+
calls += 1;
|
|
16
|
+
return { enabled: true, flushed: true, sessionKey: "sess-1", namespace: "default" };
|
|
17
|
+
},
|
|
18
|
+
async lcmCompactionRecord() {
|
|
19
|
+
throw new Error("unused");
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
ensureWriteRateLimitAvailable() {},
|
|
23
|
+
recordWriteRateLimitHit() {},
|
|
24
|
+
resolveNamespace: (namespace) => namespace,
|
|
25
|
+
resolveRequestPrincipal: () => undefined,
|
|
26
|
+
abortSignal,
|
|
27
|
+
}),
|
|
28
|
+
(err: unknown) => err instanceof Error && err.name === "AbortError",
|
|
29
|
+
);
|
|
30
|
+
assert.equal(calls, 0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("#3077 aborted batch LCM flush rethrows instead of settling", async () => {
|
|
34
|
+
const abort = new AbortController();
|
|
35
|
+
let hits = 0;
|
|
36
|
+
await assert.rejects(
|
|
37
|
+
() =>
|
|
38
|
+
runLcmCompactionFlushHttp({
|
|
39
|
+
body: { sessionKey: "sess-1", namespaces: ["ns-a", "ns-b"] },
|
|
40
|
+
service: {
|
|
41
|
+
async lcmCompactionFlush() {
|
|
42
|
+
abort.abort();
|
|
43
|
+
return { enabled: true, flushed: true, sessionKey: "sess-1", namespace: "ns-a" };
|
|
44
|
+
},
|
|
45
|
+
async lcmCompactionRecord() {
|
|
46
|
+
throw new Error("unused");
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
ensureWriteRateLimitAvailable() {},
|
|
50
|
+
recordWriteRateLimitHit() {
|
|
51
|
+
hits += 1;
|
|
52
|
+
},
|
|
53
|
+
resolveNamespace: (namespace) => namespace,
|
|
54
|
+
resolveRequestPrincipal: () => undefined,
|
|
55
|
+
abortSignal: abort.signal,
|
|
56
|
+
}),
|
|
57
|
+
(err: unknown) => err instanceof Error && err.name === "AbortError",
|
|
58
|
+
);
|
|
59
|
+
assert.equal(hits, 1);
|
|
60
|
+
});
|
|
@@ -42,6 +42,7 @@ export interface LcmCompactionFlushRunnerOptions {
|
|
|
42
42
|
resolveNamespace: (namespace?: string) => string | undefined;
|
|
43
43
|
defaultNamespace?: string;
|
|
44
44
|
resolveRequestPrincipal: () => string | undefined;
|
|
45
|
+
abortSignal?: AbortSignal;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
type LcmCompactionFlushBatchResult =
|
|
@@ -70,19 +71,26 @@ export async function runLcmCompactionFlushHttp({
|
|
|
70
71
|
resolveNamespace,
|
|
71
72
|
defaultNamespace,
|
|
72
73
|
resolveRequestPrincipal,
|
|
74
|
+
abortSignal,
|
|
73
75
|
}: LcmCompactionFlushRunnerOptions): Promise<LcmCompactionFlushHttpResult> {
|
|
76
|
+
abortSignal?.throwIfAborted();
|
|
74
77
|
ensureWriteRateLimitAvailable();
|
|
75
78
|
const requestedNamespaces = body.namespaces;
|
|
76
79
|
if (requestedNamespaces === undefined) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
try {
|
|
81
|
+
const result = await service.lcmCompactionFlush({
|
|
82
|
+
sessionKey: body.sessionKey,
|
|
83
|
+
namespace: resolveNamespace(body.namespace),
|
|
84
|
+
...(body.cwd !== undefined ? { cwd: body.cwd } : {}),
|
|
85
|
+
...(body.projectTag !== undefined ? { projectTag: body.projectTag } : {}),
|
|
86
|
+
authenticatedPrincipal: resolveRequestPrincipal(),
|
|
87
|
+
...(abortSignal !== undefined ? { abortSignal } : {}),
|
|
88
|
+
});
|
|
89
|
+
abortSignal?.throwIfAborted();
|
|
90
|
+
return result;
|
|
91
|
+
} finally {
|
|
92
|
+
recordWriteRateLimitHit();
|
|
93
|
+
}
|
|
86
94
|
}
|
|
87
95
|
const resolutionOutcomes = await Promise.allSettled(
|
|
88
96
|
requestedNamespaces.map(async (requestedNamespace) => {
|
|
@@ -106,6 +114,7 @@ export async function runLcmCompactionFlushHttp({
|
|
|
106
114
|
uniqueResolvedNamespaces.push(outcome.value);
|
|
107
115
|
}
|
|
108
116
|
}
|
|
117
|
+
abortSignal?.throwIfAborted();
|
|
109
118
|
const serviceOutcomes = await Promise.allSettled(
|
|
110
119
|
uniqueResolvedNamespaces.map(({ namespace }) =>
|
|
111
120
|
service.lcmCompactionFlush({
|
|
@@ -114,6 +123,7 @@ export async function runLcmCompactionFlushHttp({
|
|
|
114
123
|
...(body.cwd !== undefined ? { cwd: body.cwd } : {}),
|
|
115
124
|
...(body.projectTag !== undefined ? { projectTag: body.projectTag } : {}),
|
|
116
125
|
authenticatedPrincipal: resolveRequestPrincipal(),
|
|
126
|
+
...(abortSignal !== undefined ? { abortSignal } : {}),
|
|
117
127
|
})
|
|
118
128
|
)
|
|
119
129
|
);
|
|
@@ -138,6 +148,7 @@ export async function runLcmCompactionFlushHttp({
|
|
|
138
148
|
: { status: "rejected", namespace: requestedNamespace };
|
|
139
149
|
});
|
|
140
150
|
recordWriteRateLimitHit();
|
|
151
|
+
abortSignal?.throwIfAborted();
|
|
141
152
|
return {
|
|
142
153
|
enabled: results.every((result) => result.status === "fulfilled" && result.result.enabled !== false),
|
|
143
154
|
flushed: results.every((result) => result.status === "fulfilled" && result.result.flushed !== false),
|
|
@@ -35,6 +35,7 @@ export async function maybeHandleLifecycleFlush(
|
|
|
35
35
|
resolveNamespace: deps.resolveNamespace,
|
|
36
36
|
defaultNamespace: deps.defaultNamespace,
|
|
37
37
|
resolveRequestPrincipal: deps.resolveRequestPrincipal,
|
|
38
|
+
abortSignal,
|
|
38
39
|
});
|
|
39
40
|
deps.respondJson(response);
|
|
40
41
|
return true;
|
package/src/access-http.test.ts
CHANGED
|
@@ -216,7 +216,8 @@ test("HTTP batch LCM flush isolates namespace failures and charges one write quo
|
|
|
216
216
|
sessionKey: string;
|
|
217
217
|
authenticatedPrincipal?: string;
|
|
218
218
|
}) => {
|
|
219
|
-
|
|
219
|
+
const { sessionKey, namespace, authenticatedPrincipal } = request;
|
|
220
|
+
calls.push({ sessionKey, namespace, authenticatedPrincipal });
|
|
220
221
|
if (request.namespace === "team-b")
|
|
221
222
|
throw new Error("team-b flush failed");
|
|
222
223
|
return { enabled: true, flushed: true };
|
|
@@ -405,7 +406,8 @@ test("HTTP batch LCM flush deduplicates aliases after effective namespace resolu
|
|
|
405
406
|
sessionKey: string;
|
|
406
407
|
authenticatedPrincipal?: string;
|
|
407
408
|
}) => {
|
|
408
|
-
|
|
409
|
+
const { sessionKey, namespace, authenticatedPrincipal } = request;
|
|
410
|
+
calls.push({ sessionKey, namespace, authenticatedPrincipal });
|
|
409
411
|
return { enabled: true, flushed: true };
|
|
410
412
|
},
|
|
411
413
|
} as unknown as EngramAccessService;
|
|
@@ -312,7 +312,7 @@ defineOperation({
|
|
|
312
312
|
}),
|
|
313
313
|
});
|
|
314
314
|
defineOperation({ name: "lcm_search", description: "Search LCM.", schema: strictSchema({ query: S.str, sessionKey: S.str, sessionPrefix: S.str, namespace: S.str, limit: S.num }), handler: async (input, ctx) => ({ result: await ctx.service.lcmSearch({ query: defStr(input.query, ""), sessionKey: optStr(input.sessionKey), sessionPrefix: optStr(input.sessionPrefix), namespace: optStr(input.namespace), limit: optNum(input.limit), authenticatedPrincipal: ctx.authenticatedPrincipal }) }) });
|
|
315
|
-
defineOperation({ name: "lcm_compaction_flush", description: "Flush LCM compaction.", schema: strictSchema({ sessionKey: S.str, namespace: S.str, cwd: S.str, projectTag: S.str }), handler: async (input, ctx) => ({ result: await ctx.service.lcmCompactionFlush({ sessionKey: optStr(input.sessionKey) ?? "", namespace: optStr(input.namespace), cwd: optStr(input.cwd), projectTag: optStr(input.projectTag), authenticatedPrincipal: ctx.authenticatedPrincipal }) }) });
|
|
315
|
+
defineOperation({ name: "lcm_compaction_flush", description: "Flush LCM compaction.", schema: strictSchema({ sessionKey: S.str, namespace: S.str, cwd: S.str, projectTag: S.str }), handler: async (input, ctx) => ({ result: await ctx.service.lcmCompactionFlush({ sessionKey: optStr(input.sessionKey) ?? "", namespace: optStr(input.namespace), cwd: optStr(input.cwd), projectTag: optStr(input.projectTag), authenticatedPrincipal: ctx.authenticatedPrincipal, ...(ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}) }) }) });
|
|
316
316
|
defineOperation({ name: "extraction_force_flush", description: "Force-drain a session extraction buffer.", schema: strictSchema({ sessionKey: S.str, namespace: S.str, cwd: S.str, projectTag: S.str, deadlineMs: S.num }), handler: async (input, ctx) => ({ result: await ctx.service.extractionForceFlush({ sessionKey: optStr(input.sessionKey) ?? "", namespace: optStr(input.namespace), cwd: optStr(input.cwd), projectTag: optStr(input.projectTag), deadlineMs: optNum(input.deadlineMs), authenticatedPrincipal: ctx.authenticatedPrincipal, onCommitted: ctx.hooks?.recordWriteCommit, ...(ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}) }) }) });
|
|
317
317
|
defineOperation({ name: "lcm_compaction_record", description: "Record LCM compaction.", schema: strictSchema({ sessionKey: S.str, namespace: S.str, tokensBefore: S.num, tokensAfter: S.num }), handler: async (input, ctx) => ({ result: await ctx.service.lcmCompactionRecord({ sessionKey: optStr(input.sessionKey) ?? "", namespace: optStr(input.namespace), tokensBefore: optNum(input.tokensBefore) ?? 0, tokensAfter: optNum(input.tokensAfter) ?? 0, authenticatedPrincipal: ctx.authenticatedPrincipal }) }) });
|
|
318
318
|
|
package/src/access-service.ts
CHANGED
|
@@ -4379,8 +4379,8 @@ export class EngramAccessService extends SupportPassportAccessServiceBase {
|
|
|
4379
4379
|
const lcmSessionKey =
|
|
4380
4380
|
lcmSessionKeyForNamespace(scope.writeNamespace, request.sessionKey, this.orchestrator.config.defaultNamespace) ??
|
|
4381
4381
|
request.sessionKey;
|
|
4382
|
-
await this.orchestrator.lcmEngine.waitForSessionObserveIdle(lcmSessionKey);
|
|
4383
|
-
await this.orchestrator.lcmEngine.preCompactionFlush(lcmSessionKey);
|
|
4382
|
+
await this.orchestrator.lcmEngine.waitForSessionObserveIdle(lcmSessionKey, request.abortSignal);
|
|
4383
|
+
await this.orchestrator.lcmEngine.preCompactionFlush(lcmSessionKey, request.abortSignal);
|
|
4384
4384
|
return {
|
|
4385
4385
|
enabled: true,
|
|
4386
4386
|
flushed: true,
|
package/src/lcm/engine.ts
CHANGED
|
@@ -42,6 +42,18 @@ function positiveInteger(value: unknown, fallback: number, min = 1): number {
|
|
|
42
42
|
return Math.max(min, Math.floor(value));
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function abortOnSignal<T>(abortSignal: AbortSignal | undefined, work: Promise<T>): Promise<T> {
|
|
46
|
+
if (abortSignal === undefined) return work;
|
|
47
|
+
abortSignal.throwIfAborted();
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const onAbort = (): void => {
|
|
50
|
+
reject(abortSignal.reason instanceof Error ? abortSignal.reason : new DOMException("Aborted", "AbortError"));
|
|
51
|
+
};
|
|
52
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
53
|
+
work.then(resolve, reject).finally(() => abortSignal.removeEventListener("abort", onAbort));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
45
57
|
export function extractLcmConfig(cfg: PluginConfig): LcmEngineConfig {
|
|
46
58
|
return {
|
|
47
59
|
enabled: (cfg as any).lcmEnabled === true,
|
|
@@ -284,14 +296,16 @@ export class LcmEngine {
|
|
|
284
296
|
await this.observeQueue?.whenIdle();
|
|
285
297
|
}
|
|
286
298
|
|
|
287
|
-
async waitForSessionObserveIdle(sessionId: string): Promise<void> {
|
|
299
|
+
async waitForSessionObserveIdle(sessionId: string, abortSignal?: AbortSignal): Promise<void> {
|
|
300
|
+
abortSignal?.throwIfAborted();
|
|
288
301
|
if (!this.config.enabled || this.closed) return;
|
|
289
302
|
const normalizedSessionId = normalizeLcmSessionId(sessionId);
|
|
290
303
|
if (!normalizedSessionId) return;
|
|
291
304
|
await this.ensureInitialized();
|
|
305
|
+
abortSignal?.throwIfAborted();
|
|
292
306
|
if (this.closed) return;
|
|
293
|
-
await this.waitForPendingObserveInitIdle(normalizedSessionId);
|
|
294
|
-
await this.observeQueue?.whenSessionIdle(normalizedSessionId);
|
|
307
|
+
await this.waitForPendingObserveInitIdle(normalizedSessionId, abortSignal);
|
|
308
|
+
await this.observeQueue?.whenSessionIdle(normalizedSessionId, abortSignal) ?? Promise.resolve();
|
|
295
309
|
}
|
|
296
310
|
|
|
297
311
|
private reservePendingObserveInit(sessionId: string): void {
|
|
@@ -321,13 +335,25 @@ export class LcmEngine {
|
|
|
321
335
|
this.pendingObserveInitCounts.set(sessionId, count - 1);
|
|
322
336
|
}
|
|
323
337
|
|
|
324
|
-
private async waitForPendingObserveInitIdle(sessionId?: string): Promise<void> {
|
|
338
|
+
private async waitForPendingObserveInitIdle(sessionId?: string, abortSignal?: AbortSignal): Promise<void> {
|
|
339
|
+
abortSignal?.throwIfAborted();
|
|
325
340
|
if (sessionId) {
|
|
326
341
|
if (!this.pendingObserveInitCounts.has(sessionId)) return;
|
|
327
|
-
await new Promise<void>((resolve) => {
|
|
342
|
+
await new Promise<void>((resolve, reject) => {
|
|
343
|
+
const finish = (): void => {
|
|
344
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
345
|
+
resolve();
|
|
346
|
+
};
|
|
347
|
+
const onAbort = (): void => {
|
|
348
|
+
const remaining = (this.pendingObserveInitWaiters.get(sessionId) ?? []).filter((waiter) => waiter !== finish);
|
|
349
|
+
if (remaining.length === 0) this.pendingObserveInitWaiters.delete(sessionId);
|
|
350
|
+
else this.pendingObserveInitWaiters.set(sessionId, remaining);
|
|
351
|
+
reject(abortSignal?.reason instanceof Error ? abortSignal.reason : new DOMException("Aborted", "AbortError"));
|
|
352
|
+
};
|
|
328
353
|
const waiters = this.pendingObserveInitWaiters.get(sessionId) ?? [];
|
|
329
|
-
waiters.push(
|
|
354
|
+
waiters.push(finish);
|
|
330
355
|
this.pendingObserveInitWaiters.set(sessionId, waiters);
|
|
356
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
331
357
|
});
|
|
332
358
|
return;
|
|
333
359
|
}
|
|
@@ -404,16 +430,20 @@ export class LcmEngine {
|
|
|
404
430
|
}
|
|
405
431
|
|
|
406
432
|
/** Flush pending summaries before compaction (called from before_compaction hook). */
|
|
407
|
-
async preCompactionFlush(sessionId: string): Promise<void> {
|
|
433
|
+
async preCompactionFlush(sessionId: string, abortSignal?: AbortSignal): Promise<void> {
|
|
434
|
+
abortSignal?.throwIfAborted();
|
|
408
435
|
if (!this.config.enabled) return;
|
|
409
436
|
const normalizedSessionId = normalizeLcmSessionId(sessionId);
|
|
410
437
|
if (!normalizedSessionId) return;
|
|
411
438
|
await this.ensureInitialized();
|
|
412
|
-
|
|
439
|
+
abortSignal?.throwIfAborted();
|
|
440
|
+
await this.waitForSessionObserveIdle(normalizedSessionId, abortSignal);
|
|
441
|
+
abortSignal?.throwIfAborted();
|
|
413
442
|
|
|
414
443
|
try {
|
|
415
|
-
await this.summarizer!.summarizeIncremental(normalizedSessionId);
|
|
444
|
+
await abortOnSignal(abortSignal, this.summarizer!.summarizeIncremental(normalizedSessionId));
|
|
416
445
|
} catch (err) {
|
|
446
|
+
if (abortSignal?.aborted) throw err;
|
|
417
447
|
log.debug(`LCM pre-compaction flush error: ${err}`);
|
|
418
448
|
}
|
|
419
449
|
}
|
package/src/lcm/queue.test.ts
CHANGED
|
@@ -176,3 +176,22 @@ test("LcmWorkQueue waits for a specific session without waiting for unrelated id
|
|
|
176
176
|
assert.equal(sessionIdleResolved, true);
|
|
177
177
|
await queue.whenIdle();
|
|
178
178
|
});
|
|
179
|
+
|
|
180
|
+
test("#3077 whenSessionIdle abort unregisters the waiter", async () => {
|
|
181
|
+
const blocker = deferred();
|
|
182
|
+
const queue = new LcmWorkQueue({
|
|
183
|
+
concurrency: 1,
|
|
184
|
+
worker: async () => {
|
|
185
|
+
await blocker.promise;
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
queue.enqueue("blocked", [{ role: "user", content: "hold" }]);
|
|
189
|
+
await Promise.resolve();
|
|
190
|
+
const abort = new AbortController();
|
|
191
|
+
const waiting = queue.whenSessionIdle("blocked", abort.signal);
|
|
192
|
+
abort.abort();
|
|
193
|
+
await assert.rejects(waiting, (err: unknown) => err instanceof Error && err.name === "AbortError");
|
|
194
|
+
blocker.resolve();
|
|
195
|
+
await queue.whenIdle();
|
|
196
|
+
await queue.whenSessionIdle("blocked");
|
|
197
|
+
});
|
package/src/lcm/queue.ts
CHANGED
|
@@ -96,13 +96,24 @@ export class LcmWorkQueue {
|
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
async whenSessionIdle(sessionId: string): Promise<void> {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
await new Promise<void>((resolve) => {
|
|
99
|
+
async whenSessionIdle(sessionId: string, abortSignal?: AbortSignal): Promise<void> {
|
|
100
|
+
abortSignal?.throwIfAborted();
|
|
101
|
+
if (!this.pending.has(sessionId) && !this.inFlightSessions.has(sessionId)) return;
|
|
102
|
+
await new Promise<void>((resolve, reject) => {
|
|
103
|
+
const finish = (): void => {
|
|
104
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
105
|
+
resolve();
|
|
106
|
+
};
|
|
107
|
+
const onAbort = (): void => {
|
|
108
|
+
const remaining = (this.sessionIdleWaiters.get(sessionId) ?? []).filter((waiter) => waiter !== finish);
|
|
109
|
+
if (remaining.length === 0) this.sessionIdleWaiters.delete(sessionId);
|
|
110
|
+
else this.sessionIdleWaiters.set(sessionId, remaining);
|
|
111
|
+
reject(abortSignal?.reason instanceof Error ? abortSignal.reason : new DOMException("Aborted", "AbortError"));
|
|
112
|
+
};
|
|
103
113
|
const waiters = this.sessionIdleWaiters.get(sessionId) ?? [];
|
|
104
|
-
waiters.push(
|
|
114
|
+
waiters.push(finish);
|
|
105
115
|
this.sessionIdleWaiters.set(sessionId, waiters);
|
|
116
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
106
117
|
});
|
|
107
118
|
}
|
|
108
119
|
|
package/src/lcm-engine.test.ts
CHANGED
|
@@ -381,6 +381,78 @@ test("waitForSessionObserveIdle waits for deferred enqueue registration", async
|
|
|
381
381
|
}
|
|
382
382
|
});
|
|
383
383
|
|
|
384
|
+
test("#3077 waitForSessionObserveIdle rejects when aborted during wait", async () => {
|
|
385
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "engram-lcm-engine-abort-"));
|
|
386
|
+
const summarizeStarted = deferred<void>();
|
|
387
|
+
const releaseSummarize = deferred<void>();
|
|
388
|
+
try {
|
|
389
|
+
const engine = new LcmEngine(createPluginConfig(memoryDir), async () => {
|
|
390
|
+
summarizeStarted.resolve();
|
|
391
|
+
await releaseSummarize.promise;
|
|
392
|
+
return "summary";
|
|
393
|
+
});
|
|
394
|
+
await engine.ensureInitialized();
|
|
395
|
+
engine.enqueueObserveMessages("session-1", [{ role: "user", content: "queued after init" }]);
|
|
396
|
+
const abort = new AbortController();
|
|
397
|
+
const waiting = engine.waitForSessionObserveIdle("session-1", abort.signal);
|
|
398
|
+
await summarizeStarted.promise;
|
|
399
|
+
abort.abort();
|
|
400
|
+
await assert.rejects(waiting, (err: unknown) => err instanceof Error && err.name === "AbortError");
|
|
401
|
+
} finally {
|
|
402
|
+
releaseSummarize.resolve();
|
|
403
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
test("#3077 preCompactionFlush rejects when aborted during summarization", async () => {
|
|
409
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "engram-lcm-engine-flush-abort-"));
|
|
410
|
+
const summarizeStarted = deferred<void>();
|
|
411
|
+
const releaseSummarize = deferred<void>();
|
|
412
|
+
try {
|
|
413
|
+
const engine = new LcmEngine(createPluginConfig(memoryDir), async () => {
|
|
414
|
+
summarizeStarted.resolve();
|
|
415
|
+
await releaseSummarize.promise;
|
|
416
|
+
return "summary";
|
|
417
|
+
});
|
|
418
|
+
await engine.ensureInitialized();
|
|
419
|
+
engine.enqueueObserveMessages("session-1", [{ role: "user", content: "queued after init" }]);
|
|
420
|
+
const abort = new AbortController();
|
|
421
|
+
const flushing = engine.preCompactionFlush("session-1", abort.signal);
|
|
422
|
+
await summarizeStarted.promise;
|
|
423
|
+
abort.abort();
|
|
424
|
+
await assert.rejects(flushing, (err: unknown) => err instanceof Error && err.name === "AbortError");
|
|
425
|
+
} finally {
|
|
426
|
+
releaseSummarize.resolve();
|
|
427
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("#3077 preCompactionFlush rethrows a non-AbortError abort reason", async () => {
|
|
432
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "engram-lcm-engine-flush-timeout-"));
|
|
433
|
+
const summarizeStarted = deferred<void>();
|
|
434
|
+
const releaseSummarize = deferred<void>();
|
|
435
|
+
try {
|
|
436
|
+
const engine = new LcmEngine(createPluginConfig(memoryDir), async () => {
|
|
437
|
+
summarizeStarted.resolve();
|
|
438
|
+
await releaseSummarize.promise;
|
|
439
|
+
return "summary";
|
|
440
|
+
});
|
|
441
|
+
await engine.ensureInitialized();
|
|
442
|
+
engine.enqueueObserveMessages("session-1", [{ role: "user", content: "queued after init" }]);
|
|
443
|
+
const abort = new AbortController();
|
|
444
|
+
const flushing = engine.preCompactionFlush("session-1", abort.signal);
|
|
445
|
+
await summarizeStarted.promise;
|
|
446
|
+
abort.abort(new Error("custom-cancel"));
|
|
447
|
+
await assert.rejects(flushing, (err: unknown) => err instanceof Error && err.message === "custom-cancel");
|
|
448
|
+
} finally {
|
|
449
|
+
releaseSummarize.resolve();
|
|
450
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
|
|
384
456
|
test("waitForSessionObserveIdle resolves once the target session drains even if other work remains", async () => {
|
|
385
457
|
const memoryDir = await mkdtemp(
|
|
386
458
|
path.join(os.tmpdir(), "engram-lcm-engine-session-idle-"),
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lcm/engine.ts"],"sourcesContent":["import type Database from \"better-sqlite3\";\nimport { openLcmDatabase, ensureLcmStateDir } from \"./schema.js\";\nimport { LcmArchive, type LcmMessage, type LcmStructuredRecallMatch } from \"./archive.js\";\nimport { LcmDag } from \"./dag.js\";\nimport { LcmSummarizer, type SummarizeFn } from \"./summarizer.js\";\nimport {\n assembleCompressedHistoryWithTrace,\n type LcmRecallConfig,\n type LcmRecallWithTraceResult,\n} from \"./recall.js\";\nimport { LcmWorkQueue, type LcmObserveMessage } from \"./queue.js\";\nimport type { PluginConfig } from \"../types.js\";\nimport { estimateTokenCount } from \"../token-estimate.js\";\nimport { log } from \"../logger.js\";\nimport { nativeBindingRecoveryHint } from \"../native-binding-hint.js\";\nexport interface LcmEngineConfig {\n enabled: boolean;\n leafBatchSize: number;\n rollupFanIn: number;\n freshTailTurns: number;\n maxDepth: number;\n deterministicMaxTokens: number;\n archiveRetentionDays: number;\n recallBudgetShare: number;\n observeConcurrency: number;\n telemetryPrefilterEnabled: boolean;\n messagePartsEnabled: boolean;\n messagePartsRecallMaxResults: number;\n}\n\n/** An expanded archive row with stable lineage identity; content may be budget-truncated. */\nexport interface LcmExpandedMessage {\n id: number;\n session_id: string;\n turn_index: number;\n role: string;\n content: string;\n}\n\nfunction positiveInteger(value: unknown, fallback: number, min = 1): number {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return fallback;\n return Math.max(min, Math.floor(value));\n}\n\nexport function extractLcmConfig(cfg: PluginConfig): LcmEngineConfig {\n return {\n enabled: (cfg as any).lcmEnabled === true,\n leafBatchSize: positiveInteger((cfg as any).lcmLeafBatchSize, 8),\n rollupFanIn: positiveInteger((cfg as any).lcmRollupFanIn, 4, 2),\n freshTailTurns: positiveInteger((cfg as any).lcmFreshTailTurns, 16),\n maxDepth: positiveInteger((cfg as any).lcmMaxDepth, 5),\n deterministicMaxTokens: positiveInteger((cfg as any).lcmDeterministicMaxTokens, 512),\n archiveRetentionDays: positiveInteger((cfg as any).lcmArchiveRetentionDays, 90, 0),\n recallBudgetShare: (cfg as any).lcmRecallBudgetShare ?? 0.15,\n observeConcurrency:\n typeof (cfg as any).lcmObserveConcurrency === \"number\" &&\n Number.isFinite((cfg as any).lcmObserveConcurrency)\n ? Math.max(1, Math.floor((cfg as any).lcmObserveConcurrency))\n : 1,\n telemetryPrefilterEnabled: (cfg as any).lcmTelemetryPrefilterEnabled !== false,\n messagePartsEnabled: (cfg as any).messagePartsEnabled === true,\n messagePartsRecallMaxResults:\n typeof (cfg as any).messagePartsRecallMaxResults === \"number\"\n ? Math.max(0, Math.floor((cfg as any).messagePartsRecallMaxResults))\n : 6,\n };\n}\n\nfunction normalizeLcmSessionId(sessionId: string): string {\n return sessionId.trim();\n}\n\nfunction normalizeOptionalLcmSessionId(sessionId?: string): string | undefined {\n if (sessionId === undefined) {\n return undefined;\n }\n const normalized = normalizeLcmSessionId(sessionId);\n return normalized.length > 0 ? normalized : undefined;\n}\n\nconst EMPTY_LCM_STATS = {\n totalMessages: 0,\n totalSummaryNodes: 0,\n maxDepth: -1,\n} as const;\n\nexport class LcmEngine {\n private db: Database.Database | null = null;\n private archive: LcmArchive | null = null;\n private dag: LcmDag | null = null;\n private summarizer: LcmSummarizer | null = null;\n private observeQueue: LcmWorkQueue | null = null;\n private closed = false;\n private readonly config: LcmEngineConfig;\n private readonly memoryDir: string;\n private initPromise: Promise<void> | null = null;\n private readonly pendingObserveInitCounts = new Map<string, number>();\n private readonly pendingObserveInitWaiters = new Map<string, Array<() => void>>();\n private readonly pendingObserveInitIdleWaiters: Array<() => void> = [];\n\n constructor(\n pluginConfig: PluginConfig,\n private readonly summarizeFn: SummarizeFn,\n ) {\n this.config = extractLcmConfig(pluginConfig);\n this.memoryDir = pluginConfig.memoryDir;\n }\n\n get enabled(): boolean {\n return this.config.enabled;\n }\n\n /** Lazy init — open database on first use. */\n async ensureInitialized(): Promise<void> {\n if (this.closed) return;\n if (this.db) return;\n if (this.initPromise) {\n await this.initPromise;\n return;\n }\n this.initPromise = this.doInit().catch((err) => {\n // Reset so next call retries instead of caching the failure\n this.initPromise = null;\n throw err;\n });\n await this.initPromise;\n }\n\n private async doInit(): Promise<void> {\n await ensureLcmStateDir(this.memoryDir);\n const db = openLcmDatabase(this.memoryDir);\n const archive = new LcmArchive(db);\n const dag = new LcmDag(db);\n const { telemetryPrefilterEnabled } = this.config;\n const summarizer = new LcmSummarizer(\n archive,\n dag,\n this.summarizeFn,\n {\n leafBatchSize: this.config.leafBatchSize,\n rollupFanIn: this.config.rollupFanIn,\n maxDepth: this.config.maxDepth,\n deterministicMaxTokens: this.config.deterministicMaxTokens,\n telemetryPrefilterEnabled,\n },\n );\n const observeQueue = new LcmWorkQueue({\n concurrency: this.config.observeConcurrency,\n worker: async (sessionId, messages) => {\n await this.processObserveMessages(sessionId, messages);\n },\n hooks: {\n onJobStart: ({ sessionId, depth, inFlight, waitMs }) => {\n log.debug(\n `LCM observe queue start: session=${sessionId}, depth=${depth}, inFlight=${inFlight}, wait=${waitMs}ms`,\n );\n },\n onJobFinish: ({\n sessionId,\n depth,\n inFlight,\n waitMs,\n runMs,\n totalMs,\n error,\n }) => {\n if (error) {\n log.error(\n `LCM observe queue failure: session=${sessionId}, depth=${depth}, inFlight=${inFlight}, wait=${waitMs}ms, run=${runMs}ms, total=${totalMs}ms, error=${error}`,\n );\n return;\n }\n\n log.debug(\n `LCM observe queue finish: session=${sessionId}, depth=${depth}, inFlight=${inFlight}, wait=${waitMs}ms, run=${runMs}ms, total=${totalMs}ms`,\n );\n },\n },\n });\n\n if (this.closed) {\n db.close();\n return;\n }\n\n this.db = db;\n this.archive = archive;\n this.dag = dag;\n this.summarizer = summarizer;\n this.observeQueue = observeQueue;\n log.info(\"LCM engine initialized\");\n }\n\n /**\n * Enqueue messages from agent_end hook.\n * The queue worker performs the archive append and incremental summarization.\n */\n async observeMessages(\n sessionId: string,\n messages: LcmObserveMessage[],\n ): Promise<void> {\n const normalizedSessionId = normalizeLcmSessionId(sessionId);\n if (!normalizedSessionId) return;\n this.enqueueObserveMessages(normalizedSessionId, messages);\n }\n\n /** Enqueue an observe job without waiting for worker completion. */\n enqueueObserveMessages(\n sessionId: string,\n messages: LcmObserveMessage[],\n ): void {\n if (!this.config.enabled || this.closed) return;\n const normalizedSessionId = normalizeLcmSessionId(sessionId);\n if (!normalizedSessionId) return;\n if (messages.length === 0) return;\n\n this.reservePendingObserveInit(normalizedSessionId);\n\n void this.ensureInitialized()\n .then(() => {\n if (this.closed || !this.observeQueue) return;\n this.observeQueue.enqueue(normalizedSessionId, messages);\n log.debug(\n `LCM observe enqueued: session=${normalizedSessionId}, depth=${this.observeQueue.depth}, inFlight=${this.observeQueue.inFlightCount}`,\n );\n })\n .catch((err) => {\n if (this.closed) return;\n log.error(\n `LCM observe enqueue initialization error: ${err}${nativeBindingRecoveryHint(err)}`,\n );\n })\n .finally(() => {\n this.releasePendingObserveInit(normalizedSessionId);\n });\n }\n\n private async processObserveMessages(\n sessionId: string,\n messages: LcmObserveMessage[],\n ): Promise<void> {\n if (this.closed) return;\n await this.ensureInitialized();\n if (this.closed || !this.archive || !this.summarizer) return;\n\n if (messages.length === 0) return;\n\n const currentMax = this.archive.getMaxTurnIndex(sessionId);\n const { messagePartsEnabled } = this.config;\n const newMessages = messages.map((m, i) => ({\n turnIndex: currentMax + 1 + i,\n role: m.role,\n content: m.content,\n parts: messagePartsEnabled ? m.parts : undefined,\n rawContent: messagePartsEnabled ? m.rawContent : undefined,\n sourceFormat: messagePartsEnabled ? m.sourceFormat : undefined,\n }));\n\n this.archive.appendMessages(sessionId, newMessages, {\n messagePartsEnabled,\n });\n\n // Trigger incremental summarization inside the worker, after append.\n try {\n await this.summarizer.summarizeIncremental(sessionId);\n } catch (err) {\n log.debug(`LCM incremental summarization error: ${err}`);\n }\n }\n\n get observeQueueDepth(): number {\n return this.observeQueue?.depth ?? 0;\n }\n\n get observeQueueInFlightCount(): number {\n return this.observeQueue?.inFlightCount ?? 0;\n }\n\n async waitForObserveQueueIdle(): Promise<void> {\n if (!this.config.enabled || this.closed) return;\n await this.ensureInitialized();\n if (this.closed) return;\n await this.waitForPendingObserveInitIdle();\n await this.observeQueue?.whenIdle();\n }\n\n async waitForSessionObserveIdle(sessionId: string): Promise<void> {\n if (!this.config.enabled || this.closed) return;\n const normalizedSessionId = normalizeLcmSessionId(sessionId);\n if (!normalizedSessionId) return;\n await this.ensureInitialized();\n if (this.closed) return;\n await this.waitForPendingObserveInitIdle(normalizedSessionId);\n await this.observeQueue?.whenSessionIdle(normalizedSessionId);\n }\n\n private reservePendingObserveInit(sessionId: string): void {\n const count = this.pendingObserveInitCounts.get(sessionId) ?? 0;\n this.pendingObserveInitCounts.set(sessionId, count + 1);\n }\n\n private releasePendingObserveInit(sessionId: string): void {\n const count = this.pendingObserveInitCounts.get(sessionId);\n if (!count) return;\n\n if (count === 1) {\n this.pendingObserveInitCounts.delete(sessionId);\n const waiters = this.pendingObserveInitWaiters.get(sessionId) ?? [];\n this.pendingObserveInitWaiters.delete(sessionId);\n for (const resolve of waiters) resolve();\n if (this.pendingObserveInitCounts.size === 0) {\n const idleWaiters = this.pendingObserveInitIdleWaiters.splice(\n 0,\n this.pendingObserveInitIdleWaiters.length,\n );\n for (const resolve of idleWaiters) resolve();\n }\n return;\n }\n\n this.pendingObserveInitCounts.set(sessionId, count - 1);\n }\n\n private async waitForPendingObserveInitIdle(sessionId?: string): Promise<void> {\n if (sessionId) {\n if (!this.pendingObserveInitCounts.has(sessionId)) return;\n await new Promise<void>((resolve) => {\n const waiters = this.pendingObserveInitWaiters.get(sessionId) ?? [];\n waiters.push(resolve);\n this.pendingObserveInitWaiters.set(sessionId, waiters);\n });\n return;\n }\n\n if (this.pendingObserveInitCounts.size === 0) return;\n await new Promise<void>((resolve) => {\n this.pendingObserveInitIdleWaiters.push(resolve);\n });\n }\n\n /** Build the compressed history recall section for a session. */\n async assembleRecall(\n sessionId: string,\n budgetChars: number,\n ): Promise<string> {\n return (await this.assembleRecallWithTrace(sessionId, budgetChars)).text;\n }\n\n /** Build compressed recall plus content-free selected-summary receipts. */\n async assembleRecallWithTrace(\n sessionId: string,\n budgetChars: number,\n ): Promise<LcmRecallWithTraceResult> {\n if (!this.config.enabled) return { text: \"\", selectedSummaries: [] };\n const normalizedSessionId = normalizeLcmSessionId(sessionId);\n if (!normalizedSessionId) return { text: \"\", selectedSummaries: [] };\n await this.ensureInitialized();\n\n const effectiveBudget = Math.ceil(\n budgetChars * this.config.recallBudgetShare,\n );\n if (effectiveBudget <= 0) return { text: \"\", selectedSummaries: [] };\n\n return assembleCompressedHistoryWithTrace(this.dag!, this.archive!, normalizedSessionId, {\n freshTailTurns: this.config.freshTailTurns,\n budgetChars: effectiveBudget,\n });\n }\n\n async searchStructuredParts(\n sessionId: string,\n query: string,\n limit = this.config.messagePartsRecallMaxResults,\n ): Promise<LcmStructuredRecallMatch[]> {\n const { messagePartsEnabled } = this.config;\n if (!this.config.enabled || !messagePartsEnabled) return [];\n const normalizedSessionId = normalizeLcmSessionId(sessionId);\n if (!normalizedSessionId) return [];\n await this.ensureInitialized();\n if (!this.archive) return [];\n return this.archive.searchStructuredParts(query, limit, normalizedSessionId);\n }\n\n formatStructuredRecall(\n matches: LcmStructuredRecallMatch[],\n budgetChars: number,\n ): string {\n if (matches.length === 0 || budgetChars <= 0) return \"\";\n const lines: string[] = [];\n let used = \"## Structured Session Matches\\n\\n\".length;\n for (const match of matches) {\n const label = match.file_path\n ? `${match.kind} ${match.file_path}`\n : match.tool_name\n ? `${match.kind} ${match.tool_name}`\n : match.kind;\n const excerpt = match.content.replace(/\\s+/g, \" \").slice(0, 220);\n const line = `- turn ${match.turn_index} (${match.role}): ${label} — ${excerpt}`;\n if (used + line.length + 1 > budgetChars) break;\n lines.push(line);\n used += line.length + 1;\n }\n return lines.length > 0 ? `## Structured Session Matches\\n\\n${lines.join(\"\\n\")}` : \"\";\n }\n\n /** Flush pending summaries before compaction (called from before_compaction hook). */\n async preCompactionFlush(sessionId: string): Promise<void> {\n if (!this.config.enabled) return;\n const normalizedSessionId = normalizeLcmSessionId(sessionId);\n if (!normalizedSessionId) return;\n await this.ensureInitialized();\n await this.waitForSessionObserveIdle(normalizedSessionId);\n\n try {\n await this.summarizer!.summarizeIncremental(normalizedSessionId);\n } catch (err) {\n log.debug(`LCM pre-compaction flush error: ${err}`);\n }\n }\n\n /** Record a compaction event with real token counts (called from after_compaction hook). */\n async recordCompaction(\n sessionId: string,\n tokensBefore: number,\n tokensAfter: number,\n ): Promise<void> {\n if (!this.config.enabled) return;\n const normalizedSessionId = normalizeLcmSessionId(sessionId);\n if (!normalizedSessionId) return;\n await this.ensureInitialized();\n await this.waitForSessionObserveIdle(normalizedSessionId);\n\n const maxTurn = this.archive!.getMaxTurnIndex(normalizedSessionId);\n\n this.dag!.recordCompaction(normalizedSessionId, maxTurn, tokensBefore, tokensAfter);\n log.info(\n `LCM compaction recorded: session=${normalizedSessionId}, turn=${maxTurn}, tokens ${tokensBefore}→${tokensAfter}`,\n );\n }\n\n /** Verify archive coverage after compaction. */\n async verifyPostCompaction(sessionId: string): Promise<void> {\n if (!this.config.enabled) return;\n const normalizedSessionId = normalizeLcmSessionId(sessionId);\n if (!normalizedSessionId) return;\n await this.ensureInitialized();\n\n const msgCount = this.archive!.getMessageCount(normalizedSessionId);\n const nodeCount = this.dag!.getNodeCount(normalizedSessionId);\n log.debug(\n `LCM post-compaction verify: session=${normalizedSessionId}, messages=${msgCount}, summaryNodes=${nodeCount}`,\n );\n }\n\n // ── MCP Tool implementations ──\n\n /** Search across all conversation history via FTS (snippet mode). */\n async searchContext(\n query: string,\n limit: number,\n sessionId?: string,\n sessionPrefix?: string,\n ): Promise<\n Array<{\n turn_index: number;\n role: string;\n snippet: string;\n session_id: string;\n }>\n > {\n if (!this.config.enabled) return [];\n const normalizedSessionId = normalizeOptionalLcmSessionId(sessionId);\n if (sessionId !== undefined && !normalizedSessionId) return [];\n const normalizedSessionPrefix = normalizeOptionalLcmSessionId(sessionPrefix);\n if (sessionPrefix !== undefined && !normalizedSessionPrefix) return [];\n await this.ensureInitialized();\n return this.archive!.search(query, limit, normalizedSessionId, normalizedSessionPrefix);\n }\n\n /** Search via FTS returning full message content (not snippets). */\n async searchContextFull(\n query: string,\n limit: number,\n sessionId?: string,\n sessionPrefix?: string,\n ): Promise<\n Array<{\n id: number;\n turn_index: number;\n role: string;\n content: string;\n session_id: string;\n score: number;\n }>\n > {\n if (!this.config.enabled) return [];\n const normalizedSessionId = normalizeOptionalLcmSessionId(sessionId);\n if (sessionId !== undefined && !normalizedSessionId) return [];\n const normalizedSessionPrefix = normalizeOptionalLcmSessionId(sessionPrefix);\n if (sessionPrefix !== undefined && !normalizedSessionPrefix) return [];\n await this.ensureInitialized();\n return this.archive!.searchWithContent(\n query,\n limit,\n normalizedSessionId,\n 1000,\n normalizedSessionPrefix,\n );\n }\n\n /** Get a compressed summary of a turn range. */\n async describeContext(\n sessionId: string,\n fromTurn: number,\n toTurn: number,\n ): Promise<{ summary: string; turn_count: number; depth: number } | null> {\n if (!this.config.enabled) return null;\n const normalizedSessionId = normalizeLcmSessionId(sessionId);\n if (!normalizedSessionId) return null;\n await this.ensureInitialized();\n\n const nodes = this.dag!.getCoveringNodes(normalizedSessionId, fromTurn, toTurn);\n if (nodes.length === 0) {\n // No summary exists — build a description from raw messages\n const messages = this.archive!.getMessages(normalizedSessionId, fromTurn, toTurn);\n if (messages.length === 0) return null;\n const preview = messages\n .slice(0, 5)\n .map((m) => `[${m.role}] ${m.content.slice(0, 100)}`)\n .join(\"\\n\");\n return {\n summary: `No summary available for this range. Preview of ${messages.length} messages:\\n${preview}`,\n turn_count: messages.length,\n depth: -1,\n };\n }\n\n // Use the deepest covering node\n const best = nodes[0]; // Already sorted by depth DESC\n return {\n summary: best.summary_text,\n turn_count: best.msg_end - best.msg_start + 1,\n depth: best.depth,\n };\n }\n\n /**\n * Retrieve raw archived messages for a turn range (inclusive bounds),\n * read-only and unshaped — no token-budget trimming (issue #2331\n * episodic-context). Callers own truncation and cleaning.\n */\n async getTurnRange(\n sessionId: string,\n fromTurn: number,\n toTurn: number,\n ): Promise<LcmMessage[]> {\n if (!this.config.enabled) return [];\n const normalizedSessionId = normalizeLcmSessionId(sessionId);\n if (!normalizedSessionId) return [];\n await this.ensureInitialized();\n return this.archive!.getMessages(normalizedSessionId, fromTurn, toTurn);\n }\n\n /** Retrieve raw messages for a turn range (lossless expansion). */\n async expandContext(\n sessionId: string,\n fromTurn: number,\n toTurn: number,\n maxTokens: number,\n ): Promise<LcmExpandedMessage[]> {\n if (!this.config.enabled) return [];\n const normalizedSessionId = normalizeLcmSessionId(sessionId);\n if (!normalizedSessionId) return [];\n await this.ensureInitialized();\n\n const messages = this.archive!.getMessages(normalizedSessionId, fromTurn, toTurn);\n if (messages.length === 0) return [];\n\n // Enforce the token budget while keeping the existing first/last shape.\n const tokenBudget = Math.max(0, Math.floor(maxTokens));\n if (tokenBudget === 0) return [];\n const totalTokens = messages.reduce(\n (sum, message) => sum + estimateTokenCount(message.content),\n 0,\n );\n if (totalTokens <= tokenBudget) {\n return messages.map((m) => ({\n id: m.id,\n session_id: m.session_id,\n turn_index: m.turn_index,\n role: m.role,\n content: m.content,\n }));\n }\n\n const result: LcmExpandedMessage[] = [];\n const lastMsg = messages[messages.length - 1];\n const reservedLastTokens = Math.min(\n estimateTokenCount(lastMsg.content),\n Math.max(1, Math.floor(tokenBudget * 0.3)),\n );\n let remainingTokens = tokenBudget - reservedLastTokens;\n\n for (let i = 0; i < messages.length - 1; i++) {\n if (remainingTokens <= 0) break;\n const content = truncateToTokenBudget(\n messages[i].content,\n remainingTokens,\n );\n if (!content) continue;\n result.push({\n id: messages[i].id,\n session_id: messages[i].session_id,\n turn_index: messages[i].turn_index,\n role: messages[i].role,\n content,\n });\n remainingTokens -= estimateTokenCount(content);\n }\n\n const lastContent = truncateToTokenBudget(\n lastMsg.content,\n reservedLastTokens + remainingTokens,\n );\n if (lastContent) {\n result.push({\n id: lastMsg.id,\n session_id: lastMsg.session_id,\n turn_index: lastMsg.turn_index,\n role: lastMsg.role,\n content: lastContent,\n });\n }\n return result;\n }\n\n\n /** Get statistics about the LCM archive. */\n async getStats(sessionId?: string): Promise<{\n totalMessages: number;\n totalSummaryNodes: number;\n maxDepth: number;\n maxTurnIndex?: number;\n }> {\n if (!this.config.enabled)\n return { totalMessages: 0, totalSummaryNodes: 0, maxDepth: -1 };\n const normalizedSessionId = normalizeOptionalLcmSessionId(sessionId);\n if (sessionId !== undefined && !normalizedSessionId) {\n return { ...EMPTY_LCM_STATS };\n }\n await this.ensureInitialized();\n\n if (normalizedSessionId) {\n return {\n totalMessages: this.archive!.getMessageCount(normalizedSessionId),\n totalSummaryNodes: this.dag!.getNodeCount(normalizedSessionId),\n maxDepth: this.dag!.getMaxDepth(normalizedSessionId),\n maxTurnIndex: this.archive!.getMaxTurnIndex(normalizedSessionId),\n };\n }\n\n return {\n totalMessages: this.archive!.getTotalMessageCount(),\n totalSummaryNodes: 0, // Would need a global count query\n maxDepth: -1,\n };\n }\n\n /** Clear all LCM archive and summary state for one session. */\n async clearSession(sessionId: string): Promise<void> {\n if (!this.config.enabled || this.closed) return;\n const normalizedSessionId = normalizeLcmSessionId(sessionId);\n if (!normalizedSessionId) return;\n await this.ensureInitialized();\n if (this.closed || !this.archive || !this.dag) return;\n await this.waitForSessionObserveIdle(normalizedSessionId);\n this.dag.deleteSession(normalizedSessionId);\n this.archive.deleteSession(normalizedSessionId);\n }\n\n /** Clear all LCM archive and summary state without closing the engine. */\n async clearAll(): Promise<void> {\n if (!this.config.enabled || this.closed) return;\n await this.ensureInitialized();\n if (this.closed || !this.archive || !this.dag) return;\n await this.waitForObserveQueueIdle();\n this.dag.deleteAll();\n this.archive.deleteAll();\n }\n\n /** Prune old data beyond retention period. */\n async prune(): Promise<{ messagesPruned: number; nodesPruned: number }> {\n if (!this.config.enabled) return { messagesPruned: 0, nodesPruned: 0 };\n await this.ensureInitialized();\n\n const messagesPruned = this.archive!.pruneOldMessages(\n this.config.archiveRetentionDays,\n );\n const nodesPruned = this.dag!.pruneOldNodes(\n this.config.archiveRetentionDays,\n );\n return { messagesPruned, nodesPruned };\n }\n\n /** Close the database connection. */\n close(): void {\n this.closed = true;\n if (this.db) {\n this.db.close();\n }\n this.db = null;\n this.archive = null;\n this.dag = null;\n this.summarizer = null;\n this.observeQueue = null;\n this.initPromise = null;\n }\n}\nfunction truncateToTokenBudget(text: string, maxTokens: number): string {\n if (maxTokens <= 0) return \"\";\n if (estimateTokenCount(text) <= maxTokens) return text;\n const codePoints = [...text];\n let low = 0;\n let high = codePoints.length;\n while (low < high) {\n const mid = Math.ceil((low + high) / 2);\n if (estimateTokenCount(codePoints.slice(0, mid).join(\"\")) <= maxTokens) {\n low = mid;\n } else {\n high = mid - 1;\n }\n }\n return codePoints.slice(0, low).join(\"\");\n}\n"],"mappings":"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","names":[]}
|