@remnic/core 9.6.29 → 9.6.31
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 -0
- package/dist/access-admin-ops-surface.js +2 -1
- package/dist/access-boundary.d.ts +1 -0
- package/dist/access-boundary.js +3 -2
- package/dist/access-cli.js +18 -17
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +1 -0
- package/dist/access-http.js +7 -6
- package/dist/access-identity-continuity-surface.d.ts +1 -0
- package/dist/access-identity-continuity-surface.js +2 -1
- package/dist/access-lcm-surface.d.ts +1 -0
- package/dist/access-lcm-surface.js +2 -1
- package/dist/access-mcp.d.ts +1 -0
- package/dist/access-mcp.js +6 -5
- package/dist/access-observe-write-surface.d.ts +1 -0
- package/dist/access-observe-write-surface.js +2 -1
- package/dist/access-operations-batch.js +4 -3
- package/dist/access-operations.d.ts +1 -0
- package/dist/access-operations.js +5 -4
- package/dist/access-recall-surface.d.ts +1 -0
- package/dist/access-recall-surface.js +2 -1
- package/dist/access-service.d.ts +1 -0
- package/dist/access-service.js +2 -1
- package/dist/access-surface-catalog.d.ts +1 -0
- package/dist/bootstrap.d.ts +1 -0
- package/dist/causal-behavior.d.ts +1 -0
- package/dist/{chunk-L6W77GWW.js → chunk-6TCXIRP5.js} +25 -13
- package/dist/chunk-6TCXIRP5.js.map +1 -0
- package/dist/chunk-C37ZRXX7.js +36 -0
- package/dist/chunk-C37ZRXX7.js.map +1 -0
- package/dist/{chunk-SGYTQKC2.js → chunk-C6TKSI35.js} +3 -3
- package/dist/{chunk-C2SEH754.js → chunk-DAAJUA6Q.js} +4 -4
- package/dist/{chunk-FV5FVO2J.js → chunk-DEOYTFA6.js} +9 -5
- package/dist/chunk-DEOYTFA6.js.map +1 -0
- package/dist/{chunk-PYPOFEMK.js → chunk-DGFU5MSX.js} +16 -1
- package/dist/chunk-DGFU5MSX.js.map +1 -0
- package/dist/{chunk-EAREADWV.js → chunk-GQURC4B7.js} +2 -2
- package/dist/{chunk-XA4PWKBW.js → chunk-MXAGJHTA.js} +3 -3
- package/dist/{chunk-LZTFCAKE.js → chunk-OH7QLI6W.js} +28 -15
- package/dist/chunk-OH7QLI6W.js.map +1 -0
- package/dist/{chunk-5BHIGBHQ.js → chunk-OXVKR5OU.js} +6 -3
- package/dist/{chunk-5BHIGBHQ.js.map → chunk-OXVKR5OU.js.map} +1 -1
- package/dist/{chunk-IF7THCGJ.js → chunk-PUQZIS6A.js} +3 -3
- package/dist/{chunk-4SBW47WK.js → chunk-T67LUYCR.js} +28 -15
- package/dist/chunk-T67LUYCR.js.map +1 -0
- package/dist/{chunk-4463LUIE.js → chunk-TVIVY7KU.js} +180 -70
- package/dist/chunk-TVIVY7KU.js.map +1 -0
- package/dist/{chunk-KCYE2MZM.js → chunk-UXPUUL3G.js} +29 -8
- package/dist/chunk-UXPUUL3G.js.map +1 -0
- package/dist/chunk-VDC6RZI4.js +1 -0
- package/dist/{chunk-3RROR5KV.js → chunk-VJRJTRYF.js} +4 -4
- package/dist/{chunk-5WJ4LBFR.js → chunk-WSOEYOHN.js} +8 -8
- package/dist/{chunk-5AJIDEFT.js → chunk-XFTPUXCE.js} +11 -7
- package/dist/chunk-XFTPUXCE.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +8 -7
- package/dist/compounding/preference-consolidator.d.ts +1 -0
- package/dist/event-order-recall.d.ts +1 -0
- package/dist/event-order-recall.js +3 -2
- package/dist/evidence-pack.d.ts +18 -1
- package/dist/evidence-pack.js +1 -1
- package/dist/explicit-capture.d.ts +1 -0
- package/dist/explicit-cue-recall.d.ts +19 -1
- package/dist/explicit-cue-recall.js +3 -2
- package/dist/focused-list-recall.d.ts +1 -0
- package/dist/focused-list-recall.js +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +18 -17
- package/dist/lcm/engine.d.ts +3 -0
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.d.ts +23 -1
- package/dist/lcm/index.js +14 -4
- package/dist/lcm/recall.d.ts +15 -1
- package/dist/lcm/recall.js +5 -3
- package/dist/lcm/tools.d.ts +1 -0
- package/dist/mcp-memory-inspector-app.d.ts +1 -0
- package/dist/orchestrator.d.ts +1 -0
- package/dist/orchestrator.js +18 -17
- package/dist/response-guidance-recall.d.ts +1 -0
- package/dist/response-guidance-recall.js +3 -2
- package/dist/targeted-fact-recall.d.ts +1 -0
- package/dist/targeted-fact-recall.js +3 -2
- package/package.json +2 -2
- package/src/access-lcm-surface.ts +20 -5
- package/src/access-recall-surface.ts +2 -1
- package/src/event-order-recall.ts +5 -3
- package/src/evidence-pack.test.ts +66 -1
- package/src/evidence-pack.ts +41 -0
- package/src/explicit-cue-recall.ts +291 -133
- package/src/focused-list-recall.ts +25 -16
- package/src/index.ts +3 -0
- package/src/lcm/engine.ts +17 -5
- package/src/lcm/evidence-identity.ts +59 -0
- package/src/lcm/index.ts +13 -1
- package/src/lcm/recall.ts +48 -5
- package/src/lcm-engine.test.ts +46 -0
- package/src/lcm-recall.test.ts +48 -1
- package/src/lcm-retrieval-lineage.test.ts +236 -0
- package/src/recall-trace-receipts.test.ts +380 -0
- package/src/response-guidance-recall.ts +22 -14
- package/src/targeted-fact-recall.ts +25 -16
- package/dist/chunk-4463LUIE.js.map +0 -1
- package/dist/chunk-4SBW47WK.js.map +0 -1
- package/dist/chunk-5AJIDEFT.js.map +0 -1
- package/dist/chunk-FV5FVO2J.js.map +0 -1
- package/dist/chunk-HHLLAQGZ.js +0 -1
- package/dist/chunk-KCYE2MZM.js.map +0 -1
- package/dist/chunk-L6W77GWW.js.map +0 -1
- package/dist/chunk-LZTFCAKE.js.map +0 -1
- package/dist/chunk-PYPOFEMK.js.map +0 -1
- /package/dist/{chunk-SGYTQKC2.js.map → chunk-C6TKSI35.js.map} +0 -0
- /package/dist/{chunk-C2SEH754.js.map → chunk-DAAJUA6Q.js.map} +0 -0
- /package/dist/{chunk-EAREADWV.js.map → chunk-GQURC4B7.js.map} +0 -0
- /package/dist/{chunk-XA4PWKBW.js.map → chunk-MXAGJHTA.js.map} +0 -0
- /package/dist/{chunk-IF7THCGJ.js.map → chunk-PUQZIS6A.js.map} +0 -0
- /package/dist/{chunk-HHLLAQGZ.js.map → chunk-VDC6RZI4.js.map} +0 -0
- /package/dist/{chunk-3RROR5KV.js.map → chunk-VJRJTRYF.js.map} +0 -0
- /package/dist/{chunk-5WJ4LBFR.js.map → chunk-WSOEYOHN.js.map} +0 -0
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
gatherAcrossReadSessions,
|
|
9
9
|
resolveLcmReadSessionIds,
|
|
10
10
|
} from "./lcm-fallback-read.js";
|
|
11
|
+
import { isSameLcmRow, lcmEvidenceIdentity } from "./lcm/evidence-identity.js";
|
|
11
12
|
|
|
12
13
|
export interface FocusedListRecallOptions {
|
|
13
14
|
engine: ExplicitCueRecallEngine | null | undefined;
|
|
@@ -134,27 +135,35 @@ async function collectFocusedListItems(
|
|
|
134
135
|
result.turn_index + searchWindowAfter,
|
|
135
136
|
searchWindowTokens,
|
|
136
137
|
);
|
|
138
|
+
const searchIdentity = lcmEvidenceIdentity(result, result.session_id);
|
|
137
139
|
const searchHit: EvidencePackItem = {
|
|
138
|
-
|
|
140
|
+
...searchIdentity,
|
|
139
141
|
sessionId: result.session_id,
|
|
140
142
|
turnIndex: result.turn_index,
|
|
141
143
|
role: result.role,
|
|
142
144
|
content: result.content,
|
|
143
145
|
...(typeof result.score === "number" ? { score: result.score } : {}),
|
|
144
146
|
};
|
|
145
|
-
const candidates: EvidencePackItem[] = expanded.map((message) =>
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
147
|
+
const candidates: EvidencePackItem[] = expanded.map((message) => {
|
|
148
|
+
const matchesSearchHit = isSameLcmRow(
|
|
149
|
+
message,
|
|
150
|
+
result.session_id,
|
|
151
|
+
result,
|
|
152
|
+
result.session_id,
|
|
153
|
+
);
|
|
154
|
+
return {
|
|
155
|
+
...lcmEvidenceIdentity(message, result.session_id),
|
|
156
|
+
sessionId: message.session_id ?? result.session_id,
|
|
157
|
+
turnIndex: message.turn_index,
|
|
158
|
+
role: message.role,
|
|
159
|
+
content: message.content,
|
|
160
|
+
...(matchesSearchHit && typeof result.score === "number"
|
|
161
|
+
? { score: result.score }
|
|
162
|
+
: {}),
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
const hitIndex = expanded.findIndex((message) =>
|
|
166
|
+
isSameLcmRow(message, result.session_id, result, result.session_id)
|
|
158
167
|
);
|
|
159
168
|
if (hitIndex >= 0) {
|
|
160
169
|
candidates[hitIndex] = searchHit;
|
|
@@ -221,8 +230,8 @@ async function collectFocusedListScanItems(
|
|
|
221
230
|
);
|
|
222
231
|
for (const message of messages) {
|
|
223
232
|
const candidate = {
|
|
224
|
-
|
|
225
|
-
sessionId: options.sessionId,
|
|
233
|
+
...lcmEvidenceIdentity(message, options.sessionId),
|
|
234
|
+
sessionId: message.session_id ?? options.sessionId,
|
|
226
235
|
turnIndex: message.turn_index,
|
|
227
236
|
role: message.role,
|
|
228
237
|
content: message.content,
|
package/src/index.ts
CHANGED
|
@@ -58,6 +58,8 @@ export {
|
|
|
58
58
|
buildEvidencePack,
|
|
59
59
|
type EvidencePackItem,
|
|
60
60
|
type EvidencePackOptions,
|
|
61
|
+
type EvidencePackSelectedItem,
|
|
62
|
+
type EvidencePackSelectionReceipt,
|
|
61
63
|
} from "./evidence-pack.js";
|
|
62
64
|
export {
|
|
63
65
|
buildExplicitCueRecallSection,
|
|
@@ -74,6 +76,7 @@ export {
|
|
|
74
76
|
type ExplicitCueRecallOptions,
|
|
75
77
|
type ExplicitTurnReference,
|
|
76
78
|
type TrajectoryAnalysisRecallOptions,
|
|
79
|
+
type TrajectoryAnalysisLineReceipt,
|
|
77
80
|
} from "./explicit-cue-recall.js";
|
|
78
81
|
export {
|
|
79
82
|
buildTargetedFactRecallSection,
|
package/src/lcm/engine.ts
CHANGED
|
@@ -3,7 +3,11 @@ import { openLcmDatabase, ensureLcmStateDir } from "./schema.js";
|
|
|
3
3
|
import { LcmArchive, type LcmStructuredRecallMatch } from "./archive.js";
|
|
4
4
|
import { LcmDag } from "./dag.js";
|
|
5
5
|
import { LcmSummarizer, type SummarizeFn } from "./summarizer.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
assembleCompressedHistoryWithTrace,
|
|
8
|
+
type LcmRecallConfig,
|
|
9
|
+
type LcmRecallWithTraceResult,
|
|
10
|
+
} from "./recall.js";
|
|
7
11
|
import { LcmWorkQueue, type LcmObserveMessage } from "./queue.js";
|
|
8
12
|
import type { PluginConfig } from "../types.js";
|
|
9
13
|
import { log } from "../logger.js";
|
|
@@ -336,17 +340,25 @@ export class LcmEngine {
|
|
|
336
340
|
sessionId: string,
|
|
337
341
|
budgetChars: number,
|
|
338
342
|
): Promise<string> {
|
|
339
|
-
|
|
343
|
+
return (await this.assembleRecallWithTrace(sessionId, budgetChars)).text;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Build compressed recall plus content-free selected-summary receipts. */
|
|
347
|
+
async assembleRecallWithTrace(
|
|
348
|
+
sessionId: string,
|
|
349
|
+
budgetChars: number,
|
|
350
|
+
): Promise<LcmRecallWithTraceResult> {
|
|
351
|
+
if (!this.config.enabled) return { text: "", selectedSummaries: [] };
|
|
340
352
|
const normalizedSessionId = normalizeLcmSessionId(sessionId);
|
|
341
|
-
if (!normalizedSessionId) return "";
|
|
353
|
+
if (!normalizedSessionId) return { text: "", selectedSummaries: [] };
|
|
342
354
|
await this.ensureInitialized();
|
|
343
355
|
|
|
344
356
|
const effectiveBudget = Math.ceil(
|
|
345
357
|
budgetChars * this.config.recallBudgetShare,
|
|
346
358
|
);
|
|
347
|
-
if (effectiveBudget <= 0) return "";
|
|
359
|
+
if (effectiveBudget <= 0) return { text: "", selectedSummaries: [] };
|
|
348
360
|
|
|
349
|
-
return
|
|
361
|
+
return assembleCompressedHistoryWithTrace(this.dag!, this.archive!, normalizedSessionId, {
|
|
350
362
|
freshTailTurns: this.config.freshTailTurns,
|
|
351
363
|
budgetChars: effectiveBudget,
|
|
352
364
|
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface LcmRowIdentity {
|
|
2
|
+
id?: number;
|
|
3
|
+
session_id?: string;
|
|
4
|
+
turn_index: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface LcmEvidenceIdentity {
|
|
8
|
+
id: string;
|
|
9
|
+
archiveRowId?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function lcmArchiveRowId(row: Pick<LcmRowIdentity, "id">): number | undefined {
|
|
13
|
+
const value = row.id;
|
|
14
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0
|
|
15
|
+
? value
|
|
16
|
+
: undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveSessionId(row: LcmRowIdentity, fallbackSessionId: string): string {
|
|
20
|
+
return row.session_id ?? fallbackSessionId;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build the non-rendered evidence identity for an LCM archive row.
|
|
25
|
+
*
|
|
26
|
+
* Older adapters and test doubles may omit the SQLite row id. Those callers
|
|
27
|
+
* retain the historical session+turn identity until they adopt row lineage.
|
|
28
|
+
*/
|
|
29
|
+
export function lcmEvidenceIdentity(
|
|
30
|
+
row: LcmRowIdentity,
|
|
31
|
+
fallbackSessionId: string,
|
|
32
|
+
): LcmEvidenceIdentity {
|
|
33
|
+
const sessionId = resolveSessionId(row, fallbackSessionId);
|
|
34
|
+
const archiveRowId = lcmArchiveRowId(row);
|
|
35
|
+
if (archiveRowId === undefined) {
|
|
36
|
+
return { id: `${sessionId}:${row.turn_index}` };
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
id: JSON.stringify(["lcm-row", sessionId, archiveRowId]),
|
|
40
|
+
archiveRowId,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Match a search hit to its expanded archive row without conflating siblings. */
|
|
45
|
+
export function isSameLcmRow(
|
|
46
|
+
left: LcmRowIdentity,
|
|
47
|
+
leftFallbackSessionId: string,
|
|
48
|
+
right: LcmRowIdentity,
|
|
49
|
+
rightFallbackSessionId: string,
|
|
50
|
+
): boolean {
|
|
51
|
+
const leftRowId = lcmArchiveRowId(left);
|
|
52
|
+
const rightRowId = lcmArchiveRowId(right);
|
|
53
|
+
const leftSessionId = resolveSessionId(left, leftFallbackSessionId);
|
|
54
|
+
const rightSessionId = resolveSessionId(right, rightFallbackSessionId);
|
|
55
|
+
if (leftRowId !== undefined && rightRowId !== undefined) {
|
|
56
|
+
return leftRowId === rightRowId && leftSessionId === rightSessionId;
|
|
57
|
+
}
|
|
58
|
+
return leftSessionId === rightSessionId && left.turn_index === right.turn_index;
|
|
59
|
+
}
|
package/src/lcm/index.ts
CHANGED
|
@@ -4,9 +4,21 @@ export {
|
|
|
4
4
|
type LcmEngineConfig,
|
|
5
5
|
type LcmExpandedMessage,
|
|
6
6
|
} from "./engine.js";
|
|
7
|
+
export {
|
|
8
|
+
isSameLcmRow,
|
|
9
|
+
lcmArchiveRowId,
|
|
10
|
+
lcmEvidenceIdentity,
|
|
11
|
+
type LcmEvidenceIdentity,
|
|
12
|
+
type LcmRowIdentity,
|
|
13
|
+
} from "./evidence-identity.js";
|
|
7
14
|
export { LcmArchive, estimateTokens } from "./archive.js";
|
|
8
15
|
export { LcmDag, type SummaryNode } from "./dag.js";
|
|
9
16
|
export { LcmSummarizer, type SummarizeFn } from "./summarizer.js";
|
|
10
|
-
export {
|
|
17
|
+
export {
|
|
18
|
+
assembleCompressedHistory,
|
|
19
|
+
assembleCompressedHistoryWithTrace,
|
|
20
|
+
type LcmRecallWithTraceResult,
|
|
21
|
+
type LcmSummarySelectionReceipt,
|
|
22
|
+
} from "./recall.js";
|
|
11
23
|
export { registerLcmTools } from "./tools.js";
|
|
12
24
|
export { openLcmDatabase, ensureLcmStateDir } from "./schema.js";
|
package/src/lcm/recall.ts
CHANGED
|
@@ -7,6 +7,21 @@ export interface LcmRecallConfig {
|
|
|
7
7
|
budgetChars: number;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export interface LcmSummarySelectionReceipt {
|
|
11
|
+
id: string;
|
|
12
|
+
depth: number;
|
|
13
|
+
msgStart: number;
|
|
14
|
+
msgEnd: number;
|
|
15
|
+
/** Half-open offsets within the returned compressed-history string. */
|
|
16
|
+
entryStart: number;
|
|
17
|
+
entryEnd: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface LcmRecallWithTraceResult {
|
|
21
|
+
text: string;
|
|
22
|
+
selectedSummaries: LcmSummarySelectionReceipt[];
|
|
23
|
+
}
|
|
24
|
+
|
|
10
25
|
/**
|
|
11
26
|
* Assemble a compressed session history section from the summary DAG.
|
|
12
27
|
*
|
|
@@ -21,14 +36,24 @@ export function assembleCompressedHistory(
|
|
|
21
36
|
sessionId: string,
|
|
22
37
|
config: LcmRecallConfig,
|
|
23
38
|
): string {
|
|
39
|
+
return assembleCompressedHistoryWithTrace(dag, archive, sessionId, config).text;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function assembleCompressedHistoryWithTrace(
|
|
43
|
+
dag: LcmDag,
|
|
44
|
+
archive: LcmArchive,
|
|
45
|
+
sessionId: string,
|
|
46
|
+
config: LcmRecallConfig,
|
|
47
|
+
): LcmRecallWithTraceResult {
|
|
24
48
|
const maxTurn = archive.getMaxTurnIndex(sessionId);
|
|
25
|
-
if (maxTurn < 0) return "";
|
|
49
|
+
if (maxTurn < 0) return { text: "", selectedSummaries: [] };
|
|
26
50
|
|
|
27
51
|
const allNodes = dag.getAllNodes(sessionId);
|
|
28
|
-
if (allNodes.length === 0) return "";
|
|
52
|
+
if (allNodes.length === 0) return { text: "", selectedSummaries: [] };
|
|
29
53
|
|
|
30
54
|
const freshTailStart = Math.max(0, maxTurn - config.freshTailTurns + 1);
|
|
31
55
|
const sections: string[] = [];
|
|
56
|
+
const selectedNodes: SummaryNode[] = [];
|
|
32
57
|
let usedChars = 0;
|
|
33
58
|
|
|
34
59
|
// Collect nodes covering the "old" portion (before fresh tail)
|
|
@@ -39,6 +64,7 @@ export function assembleCompressedHistory(
|
|
|
39
64
|
const entry = `**${label}** (depth ${node.depth}):\n${node.summary_text}`;
|
|
40
65
|
if (usedChars + entry.length > config.budgetChars) break;
|
|
41
66
|
sections.push(entry);
|
|
67
|
+
selectedNodes.push(node);
|
|
42
68
|
usedChars += entry.length;
|
|
43
69
|
}
|
|
44
70
|
}
|
|
@@ -58,12 +84,29 @@ export function assembleCompressedHistory(
|
|
|
58
84
|
const entry = `**${label}**:\n${node.summary_text}`;
|
|
59
85
|
if (usedChars + entry.length > config.budgetChars) break;
|
|
60
86
|
sections.push(entry);
|
|
87
|
+
selectedNodes.push(node);
|
|
61
88
|
usedChars += entry.length;
|
|
62
89
|
}
|
|
63
90
|
|
|
64
|
-
if (sections.length === 0) return "";
|
|
65
|
-
|
|
66
|
-
|
|
91
|
+
if (sections.length === 0) return { text: "", selectedSummaries: [] };
|
|
92
|
+
|
|
93
|
+
const prefix = "## Session History (Compressed)\n\n";
|
|
94
|
+
const text = `${prefix}${sections.join("\n\n")}`;
|
|
95
|
+
let entryStart = prefix.length;
|
|
96
|
+
const selectedSummaries = selectedNodes.map((node, index) => {
|
|
97
|
+
const entryEnd = entryStart + sections[index]!.length;
|
|
98
|
+
const receipt: LcmSummarySelectionReceipt = {
|
|
99
|
+
id: node.id,
|
|
100
|
+
depth: node.depth,
|
|
101
|
+
msgStart: node.msg_start,
|
|
102
|
+
msgEnd: node.msg_end,
|
|
103
|
+
entryStart,
|
|
104
|
+
entryEnd,
|
|
105
|
+
};
|
|
106
|
+
entryStart = entryEnd + 2;
|
|
107
|
+
return receipt;
|
|
108
|
+
});
|
|
109
|
+
return { text, selectedSummaries };
|
|
67
110
|
}
|
|
68
111
|
|
|
69
112
|
/**
|
package/src/lcm-engine.test.ts
CHANGED
|
@@ -676,6 +676,52 @@ test("expandContext preserves first, middle, and last row identity when truncate
|
|
|
676
676
|
}
|
|
677
677
|
});
|
|
678
678
|
|
|
679
|
+
test("assembleRecallWithTrace preserves text and reports selected summary metadata", async () => {
|
|
680
|
+
const memoryDir = await mkdtemp(
|
|
681
|
+
path.join(os.tmpdir(), "engram-lcm-summary-receipts-"),
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
try {
|
|
685
|
+
const engine = new LcmEngine(
|
|
686
|
+
createPluginConfig(memoryDir),
|
|
687
|
+
async () => "private summary body",
|
|
688
|
+
);
|
|
689
|
+
await engine.observeMessages(
|
|
690
|
+
"private-session",
|
|
691
|
+
Array.from({ length: 8 }, (_, index) => ({
|
|
692
|
+
role: index % 2 === 0 ? "user" : "assistant",
|
|
693
|
+
content: `private message ${index}`,
|
|
694
|
+
})),
|
|
695
|
+
);
|
|
696
|
+
await engine.waitForSessionObserveIdle("private-session");
|
|
697
|
+
|
|
698
|
+
const baseline = await engine.assembleRecall("private-session", 10_000);
|
|
699
|
+
const traced = await engine.assembleRecallWithTrace(
|
|
700
|
+
"private-session",
|
|
701
|
+
10_000,
|
|
702
|
+
);
|
|
703
|
+
|
|
704
|
+
assert.equal(traced.text, baseline);
|
|
705
|
+
assert.ok(traced.selectedSummaries.length > 0);
|
|
706
|
+
assert.ok(
|
|
707
|
+
traced.selectedSummaries.every((receipt) =>
|
|
708
|
+
receipt.entryStart < receipt.entryEnd &&
|
|
709
|
+
receipt.msgStart <= receipt.msgEnd
|
|
710
|
+
),
|
|
711
|
+
);
|
|
712
|
+
assert.equal(
|
|
713
|
+
JSON.stringify(traced.selectedSummaries).includes("private summary body"),
|
|
714
|
+
false,
|
|
715
|
+
);
|
|
716
|
+
assert.equal(
|
|
717
|
+
JSON.stringify(traced.selectedSummaries).includes("private-session"),
|
|
718
|
+
false,
|
|
719
|
+
);
|
|
720
|
+
} finally {
|
|
721
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
|
|
679
725
|
test("close prevents deferred observe work from reinitializing the engine", async () => {
|
|
680
726
|
const memoryDir = await mkdtemp(
|
|
681
727
|
path.join(os.tmpdir(), "engram-lcm-engine-close-"),
|
package/src/lcm-recall.test.ts
CHANGED
|
@@ -6,7 +6,10 @@ import test from "node:test";
|
|
|
6
6
|
|
|
7
7
|
import { LcmArchive } from "./lcm/archive.js";
|
|
8
8
|
import { LcmDag } from "./lcm/dag.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
assembleCompressedHistory,
|
|
11
|
+
assembleCompressedHistoryWithTrace,
|
|
12
|
+
} from "./lcm/recall.js";
|
|
10
13
|
import { ensureLcmStateDir, openLcmDatabase } from "./lcm/schema.js";
|
|
11
14
|
|
|
12
15
|
test("assembleCompressedHistory excludes old summaries that straddle the fresh tail", async () => {
|
|
@@ -58,12 +61,56 @@ test("assembleCompressedHistory excludes old summaries that straddle the fresh t
|
|
|
58
61
|
freshTailTurns: 16,
|
|
59
62
|
budgetChars: 10_000,
|
|
60
63
|
});
|
|
64
|
+
const traced = assembleCompressedHistoryWithTrace(
|
|
65
|
+
dag,
|
|
66
|
+
archive,
|
|
67
|
+
sessionId,
|
|
68
|
+
{ freshTailTurns: 16, budgetChars: 10_000 },
|
|
69
|
+
);
|
|
61
70
|
|
|
71
|
+
assert.equal(traced.text, section);
|
|
62
72
|
assert.doesNotMatch(section, /parent summary 0-31/);
|
|
63
73
|
assert.match(section, /leaf summary 0-7/);
|
|
64
74
|
assert.match(section, /leaf summary 8-15/);
|
|
65
75
|
assert.match(section, /leaf summary 16-23/);
|
|
66
76
|
assert.match(section, /leaf summary 24-31/);
|
|
77
|
+
assert.deepEqual(
|
|
78
|
+
traced.selectedSummaries.map(({ id, depth, msgStart, msgEnd }) => ({
|
|
79
|
+
id,
|
|
80
|
+
depth,
|
|
81
|
+
msgStart,
|
|
82
|
+
msgEnd,
|
|
83
|
+
})),
|
|
84
|
+
[
|
|
85
|
+
{ id: "leaf-0-7", depth: 0, msgStart: 0, msgEnd: 7 },
|
|
86
|
+
{ id: "leaf-8-15", depth: 0, msgStart: 8, msgEnd: 15 },
|
|
87
|
+
{ id: "leaf-16-23", depth: 0, msgStart: 16, msgEnd: 23 },
|
|
88
|
+
{ id: "leaf-24-31", depth: 0, msgStart: 24, msgEnd: 31 },
|
|
89
|
+
],
|
|
90
|
+
);
|
|
91
|
+
for (const receipt of traced.selectedSummaries) {
|
|
92
|
+
assert.ok(receipt.entryStart < receipt.entryEnd);
|
|
93
|
+
const expectedSummary = receipt.id.replace("leaf-", "leaf summary ");
|
|
94
|
+
assert.equal(
|
|
95
|
+
traced.text.slice(receipt.entryStart, receipt.entryEnd).includes(expectedSummary),
|
|
96
|
+
true,
|
|
97
|
+
);
|
|
98
|
+
assert.equal("summary" in receipt, false);
|
|
99
|
+
assert.equal("sessionId" in receipt, false);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const first = traced.selectedSummaries[0]!;
|
|
103
|
+
const firstEntryBudget = first.entryEnd - first.entryStart;
|
|
104
|
+
const limited = assembleCompressedHistoryWithTrace(
|
|
105
|
+
dag,
|
|
106
|
+
archive,
|
|
107
|
+
sessionId,
|
|
108
|
+
{ freshTailTurns: 16, budgetChars: firstEntryBudget },
|
|
109
|
+
);
|
|
110
|
+
assert.deepEqual(
|
|
111
|
+
limited.selectedSummaries.map((receipt) => receipt.id),
|
|
112
|
+
["leaf-0-7"],
|
|
113
|
+
);
|
|
67
114
|
} finally {
|
|
68
115
|
db.close();
|
|
69
116
|
await rm(memoryDir, { recursive: true, force: true });
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { AccessLcmSurface, type AccessLcmSurfaceDeps } from "./access-lcm-surface.js";
|
|
5
|
+
import {
|
|
6
|
+
AccessRecallSurface,
|
|
7
|
+
type AccessRecallSurfaceDeps,
|
|
8
|
+
} from "./access-recall-surface.js";
|
|
9
|
+
import { buildEventOrderRecallSection } from "./event-order-recall.js";
|
|
10
|
+
import { buildExplicitCueRecallSection, type ExplicitCueRecallEngine } from "./explicit-cue-recall.js";
|
|
11
|
+
import { buildFocusedListRecallSection } from "./focused-list-recall.js";
|
|
12
|
+
import {
|
|
13
|
+
isSameLcmRow,
|
|
14
|
+
lcmEvidenceIdentity,
|
|
15
|
+
} from "./lcm/evidence-identity.js";
|
|
16
|
+
import { buildResponseGuidanceRecallSection } from "./response-guidance-recall.js";
|
|
17
|
+
import { buildTargetedFactRecallSection } from "./targeted-fact-recall.js";
|
|
18
|
+
|
|
19
|
+
interface TestRow {
|
|
20
|
+
id?: number;
|
|
21
|
+
session_id: string;
|
|
22
|
+
turn_index: number;
|
|
23
|
+
role: string;
|
|
24
|
+
content: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class LineageEngine implements ExplicitCueRecallEngine {
|
|
28
|
+
constructor(
|
|
29
|
+
private readonly rows: TestRow[],
|
|
30
|
+
private readonly searchRowId?: number,
|
|
31
|
+
private readonly searchContent?: string,
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
async expandContext(
|
|
35
|
+
sessionId: string,
|
|
36
|
+
fromTurn: number,
|
|
37
|
+
toTurn: number,
|
|
38
|
+
_maxTokens: number,
|
|
39
|
+
): Promise<TestRow[]> {
|
|
40
|
+
return this.rows.filter((row) =>
|
|
41
|
+
row.session_id === sessionId &&
|
|
42
|
+
row.turn_index >= fromTurn &&
|
|
43
|
+
row.turn_index <= toTurn
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async searchContextFull(): Promise<
|
|
48
|
+
Array<TestRow & { score: number }>
|
|
49
|
+
> {
|
|
50
|
+
const row = this.rows.find((candidate) => candidate.id === this.searchRowId);
|
|
51
|
+
return row
|
|
52
|
+
? [{
|
|
53
|
+
...row,
|
|
54
|
+
content: this.searchContent ?? row.content,
|
|
55
|
+
score: 99,
|
|
56
|
+
}]
|
|
57
|
+
: [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getStats(sessionId?: string): Promise<{
|
|
61
|
+
totalMessages: number;
|
|
62
|
+
maxTurnIndex?: number;
|
|
63
|
+
}> {
|
|
64
|
+
const rows = sessionId
|
|
65
|
+
? this.rows.filter((row) => row.session_id === sessionId)
|
|
66
|
+
: this.rows;
|
|
67
|
+
return {
|
|
68
|
+
totalMessages: rows.length,
|
|
69
|
+
maxTurnIndex: rows.length > 0
|
|
70
|
+
? Math.max(...rows.map((row) => row.turn_index))
|
|
71
|
+
: undefined,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
test("LCM evidence identity distinguishes sibling rows and preserves the legacy fallback", () => {
|
|
77
|
+
const first = { id: 41, session_id: "session", turn_index: 7 };
|
|
78
|
+
const second = { id: 42, session_id: "session", turn_index: 7 };
|
|
79
|
+
assert.notEqual(
|
|
80
|
+
lcmEvidenceIdentity(first, "session").id,
|
|
81
|
+
lcmEvidenceIdentity(second, "session").id,
|
|
82
|
+
);
|
|
83
|
+
assert.equal(isSameLcmRow(first, "session", second, "session"), false);
|
|
84
|
+
assert.equal(
|
|
85
|
+
lcmEvidenceIdentity({ turn_index: 7 }, "session").id,
|
|
86
|
+
"session:7",
|
|
87
|
+
);
|
|
88
|
+
assert.equal(
|
|
89
|
+
isSameLcmRow(
|
|
90
|
+
{ turn_index: 7 },
|
|
91
|
+
"session",
|
|
92
|
+
{ turn_index: 7 },
|
|
93
|
+
"session",
|
|
94
|
+
),
|
|
95
|
+
true,
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("explicit-cue recall keeps distinct archive rows that share a turn", async () => {
|
|
100
|
+
const engine = new LineageEngine([
|
|
101
|
+
{ id: 101, session_id: "s", turn_index: 7, role: "user", content: "First row at the referenced turn." },
|
|
102
|
+
{ id: 102, session_id: "s", turn_index: 7, role: "assistant", content: "Second row at the referenced turn." },
|
|
103
|
+
]);
|
|
104
|
+
const recalled = await buildExplicitCueRecallSection({
|
|
105
|
+
engine,
|
|
106
|
+
sessionId: "s",
|
|
107
|
+
query: "Review turn 7",
|
|
108
|
+
maxChars: 4_000,
|
|
109
|
+
});
|
|
110
|
+
assert.match(recalled, /First row at the referenced turn/);
|
|
111
|
+
assert.match(recalled, /Second row at the referenced turn/);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("targeted recall replaces and scores only the matching same-turn row", async () => {
|
|
115
|
+
const engine = new LineageEngine([
|
|
116
|
+
{ id: 101, session_id: "s", turn_index: 7, role: "user", content: "I earn approximately $100,000 CAD annually as a senior engineer." },
|
|
117
|
+
{ id: 102, session_id: "s", turn_index: 7, role: "user", content: "My recent raise to $110,000 CAD as a senior engineer at Saint Pierre Manufacturing Ltd is now effective." },
|
|
118
|
+
], 102, "The matching row records my recent raise to $110,000 CAD as a senior engineer at Saint Pierre Manufacturing Ltd.");
|
|
119
|
+
const recalled = await buildTargetedFactRecallSection({
|
|
120
|
+
engine,
|
|
121
|
+
sessionId: "s",
|
|
122
|
+
query: "What is my annual salary as a senior engineer at Saint Pierre Manufacturing Ltd?",
|
|
123
|
+
maxChars: 6_000,
|
|
124
|
+
});
|
|
125
|
+
assert.match(recalled, /earn approximately \$100,000 CAD annually/);
|
|
126
|
+
assert.match(recalled, /matching row records my recent raise to \$110,000 CAD/);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("focused-list recall keeps distinct same-turn calculations while matching the search row", async () => {
|
|
130
|
+
const engine = new LineageEngine([
|
|
131
|
+
{ id: 101, session_id: "s", turn_index: 7, role: "user", content: "Can you help me calculate P(both heads) = 1/2 x 1/2 = 1/4 so I can make sure I get it right?" },
|
|
132
|
+
{ id: 102, session_id: "s", turn_index: 7, role: "user", content: "I am trying to verify if P(rolling a number greater than 4) = 2/6 = 1/3 is correct." },
|
|
133
|
+
], 102, "I confirmed P(rolling a number greater than 4) = 2/6 = 1/3.");
|
|
134
|
+
const recalled = await buildFocusedListRecallSection({
|
|
135
|
+
engine,
|
|
136
|
+
sessionId: "s",
|
|
137
|
+
query: "In my questions about tossing coins and rolling dice, how many different probability calculations did I try to confirm?",
|
|
138
|
+
maxChars: 6_000,
|
|
139
|
+
});
|
|
140
|
+
assert.match(recalled, /P\(both heads\)/);
|
|
141
|
+
assert.match(recalled, /P\(rolling a number greater than 4\)/);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("response guidance keeps distinct same-turn rows while preserving its existing filters", async () => {
|
|
145
|
+
const engine = new LineageEngine([
|
|
146
|
+
{ id: 101, session_id: "s", turn_index: 7, role: "user", content: "I prefer a structured daily routine with a written priority list each morning." },
|
|
147
|
+
{ id: 102, session_id: "s", turn_index: 7, role: "user", content: "My structured daily routine schedules focused work blocks and buffer time between meetings." },
|
|
148
|
+
], 102, "My structured daily routine schedules focused work blocks and buffer time between meetings.");
|
|
149
|
+
const recalled = await buildResponseGuidanceRecallSection({
|
|
150
|
+
engine,
|
|
151
|
+
sessionId: "s",
|
|
152
|
+
query: "How should I organize my day to stay on track?",
|
|
153
|
+
maxChars: 6_000,
|
|
154
|
+
});
|
|
155
|
+
assert.match(recalled, /written priority list each morning/);
|
|
156
|
+
assert.match(recalled, /focused work blocks/);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("event-order recall keeps distinct same-turn chronological rows", async () => {
|
|
160
|
+
const engine = new LineageEngine([
|
|
161
|
+
{ id: 101, session_id: "s", turn_index: 7, role: "user", content: "Patrick suggested a March workshop on workflow optimization." },
|
|
162
|
+
{ id: 102, session_id: "s", turn_index: 7, role: "user", content: "Patrick and I planned an interview tips meeting for tomorrow." },
|
|
163
|
+
]);
|
|
164
|
+
const recalled = await buildEventOrderRecallSection({
|
|
165
|
+
engine,
|
|
166
|
+
sessionId: "s",
|
|
167
|
+
query: "Walk me through my interactions with Patrick in order.",
|
|
168
|
+
maxChars: 6_000,
|
|
169
|
+
});
|
|
170
|
+
assert.match(recalled, /workshop suggestion/);
|
|
171
|
+
assert.match(recalled, /interview tips meeting/);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
function accessSearchDeps(rows: TestRow[]): AccessLcmSurfaceDeps {
|
|
175
|
+
const orchestrator = {
|
|
176
|
+
config: {
|
|
177
|
+
defaultNamespace: "default",
|
|
178
|
+
namespaces: false,
|
|
179
|
+
},
|
|
180
|
+
lcmEngine: {
|
|
181
|
+
enabled: true,
|
|
182
|
+
searchContextFull: async () => rows,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
return {
|
|
186
|
+
orchestrator,
|
|
187
|
+
lcmSessionIdsForNamespaces: () => ["primary", "fallback"],
|
|
188
|
+
resolveImplicitLcmReadFallbackNamespace: () => "default",
|
|
189
|
+
resolveLcmReadNamespace: () => "default",
|
|
190
|
+
resolveLcmReadSessionIds: () => ["primary", "fallback"],
|
|
191
|
+
resolveLcmReadSessionKey: () => "primary",
|
|
192
|
+
resolveReadableNamespace: () => "default",
|
|
193
|
+
resolveRequestPrincipal: () => "default",
|
|
194
|
+
resolveScopeProfileLcmReadNamespaces: () => null,
|
|
195
|
+
} as unknown as AccessLcmSurfaceDeps;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
test("access LCM search dedupes repeated rows but retains same-turn siblings", async () => {
|
|
199
|
+
const rows: TestRow[] = [
|
|
200
|
+
{ id: 101, session_id: "s", turn_index: 7, role: "user", content: "first sibling" },
|
|
201
|
+
{ id: 102, session_id: "s", turn_index: 7, role: "assistant", content: "second sibling" },
|
|
202
|
+
];
|
|
203
|
+
const response = await new AccessLcmSurface(accessSearchDeps(rows)).lcmSearch({
|
|
204
|
+
query: "sibling",
|
|
205
|
+
sessionKey: "s",
|
|
206
|
+
});
|
|
207
|
+
assert.deepEqual(response.results.map((row) => row.content), [
|
|
208
|
+
"first sibling",
|
|
209
|
+
"second sibling",
|
|
210
|
+
]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("raw excerpts dedupe repeated rows but retain same-turn siblings", async () => {
|
|
214
|
+
const rows: TestRow[] = [
|
|
215
|
+
{ id: 101, session_id: "s", turn_index: 7, role: "user", content: "first sibling" },
|
|
216
|
+
{ id: 102, session_id: "s", turn_index: 7, role: "assistant", content: "second sibling" },
|
|
217
|
+
];
|
|
218
|
+
const deps = {
|
|
219
|
+
orchestrator: {
|
|
220
|
+
config: { defaultNamespace: "default" },
|
|
221
|
+
lcmEngine: {
|
|
222
|
+
enabled: true,
|
|
223
|
+
searchContextFull: async () => rows,
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
} as unknown as AccessRecallSurfaceDeps;
|
|
227
|
+
const excerpts = await new AccessRecallSurface(deps).fetchRawExcerpts("raw", {
|
|
228
|
+
query: "sibling",
|
|
229
|
+
sessionKey: "s",
|
|
230
|
+
lcmSessionIds: ["primary", "fallback"],
|
|
231
|
+
});
|
|
232
|
+
assert.deepEqual(excerpts?.map((row) => row.content), [
|
|
233
|
+
"first sibling",
|
|
234
|
+
"second sibling",
|
|
235
|
+
]);
|
|
236
|
+
});
|