@remnic/core 9.6.28 → 9.6.30
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 +2 -2
- package/dist/access-admin-ops-surface.js +2 -1
- package/dist/access-boundary.d.ts +2 -2
- package/dist/access-boundary.js +3 -2
- package/dist/access-cli.js +16 -15
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +2 -2
- package/dist/access-http.js +7 -6
- package/dist/access-identity-continuity-surface.d.ts +1 -1
- package/dist/access-identity-continuity-surface.js +2 -1
- package/dist/access-lcm-surface.d.ts +2 -2
- package/dist/access-lcm-surface.js +2 -1
- package/dist/access-mcp.d.ts +2 -2
- package/dist/access-mcp.js +6 -5
- package/dist/access-observe-write-surface.d.ts +2 -2
- package/dist/access-observe-write-surface.js +2 -1
- package/dist/access-operations-batch.js +4 -3
- package/dist/access-operations.d.ts +2 -2
- package/dist/access-operations.js +5 -4
- package/dist/access-recall-surface.d.ts +4 -3
- package/dist/access-recall-surface.js +2 -1
- package/dist/{access-service-DK7s3wMj.d.ts → access-service-BofDzaPT.d.ts} +3 -2
- package/dist/access-service.d.ts +2 -2
- package/dist/access-service.js +2 -1
- package/dist/access-surface-catalog.d.ts +2 -2
- package/dist/bootstrap.d.ts +1 -1
- package/dist/{chunk-4273YO6I.js → chunk-3VSTTB74.js} +111 -11
- package/dist/chunk-3VSTTB74.js.map +1 -0
- package/dist/{chunk-4463LUIE.js → chunk-77UPM7P3.js} +22 -11
- package/dist/chunk-77UPM7P3.js.map +1 -0
- package/dist/{chunk-FV5FVO2J.js → chunk-7O2JL5RY.js} +9 -5
- package/dist/chunk-7O2JL5RY.js.map +1 -0
- package/dist/{chunk-LZTFCAKE.js → chunk-BFU7H32D.js} +28 -15
- package/dist/chunk-BFU7H32D.js.map +1 -0
- package/dist/chunk-C37ZRXX7.js +36 -0
- package/dist/chunk-C37ZRXX7.js.map +1 -0
- package/dist/{chunk-ARDRBJHD.js → chunk-C6TKSI35.js} +3 -3
- package/dist/{chunk-N6VIXDT7.js → chunk-DAAJUA6Q.js} +4 -4
- package/dist/{chunk-MRQN5Q3I.js → chunk-GQURC4B7.js} +2 -2
- package/dist/{chunk-4SBW47WK.js → chunk-IRMLYBMU.js} +28 -15
- package/dist/chunk-IRMLYBMU.js.map +1 -0
- package/dist/{chunk-QLYLTKUG.js → chunk-MXAGJHTA.js} +3 -3
- package/dist/{chunk-YDF4HL3W.js → chunk-OXVKR5OU.js} +35 -39
- package/dist/chunk-OXVKR5OU.js.map +1 -0
- package/dist/{chunk-GXLFDLX2.js → chunk-PUQZIS6A.js} +3 -3
- package/dist/{chunk-PYPOFEMK.js → chunk-R26WO46Z.js} +1 -1
- package/dist/chunk-R26WO46Z.js.map +1 -0
- package/dist/chunk-VDC6RZI4.js +1 -0
- package/dist/{chunk-43VNWTBB.js → chunk-VJRJTRYF.js} +4 -4
- package/dist/chunk-VJRJTRYF.js.map +1 -0
- package/dist/{chunk-L6W77GWW.js → chunk-XGSOXFD4.js} +25 -13
- package/dist/chunk-XGSOXFD4.js.map +1 -0
- package/dist/{cli-CLSdmOVU.d.ts → cli-3_x5sJTL.d.ts} +2 -2
- package/dist/cli.d.ts +3 -3
- package/dist/cli.js +8 -7
- package/dist/event-order-recall.js +3 -2
- package/dist/evidence-pack.d.ts +2 -0
- package/dist/evidence-pack.js +1 -1
- package/dist/explicit-capture.d.ts +1 -1
- package/dist/explicit-cue-recall.d.ts +3 -0
- package/dist/explicit-cue-recall.js +3 -2
- package/dist/focused-list-recall.js +3 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +16 -15
- package/dist/lcm/index.d.ts +22 -0
- package/dist/lcm/index.js +9 -1
- package/dist/mcp-memory-inspector-app.d.ts +2 -2
- package/dist/{orchestrator-BzXGNHco.d.ts → orchestrator-CAH5-PKN.d.ts} +20 -3
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +16 -15
- package/dist/response-guidance-recall.js +3 -2
- 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 +48 -50
- package/src/access-service.ts +2 -7
- package/src/cli.ts +4 -10
- package/src/event-order-recall.ts +5 -3
- package/src/evidence-pack.ts +2 -0
- package/src/explicit-cue-recall.ts +67 -66
- package/src/focused-list-recall.ts +25 -16
- package/src/lcm/evidence-identity.ts +59 -0
- package/src/lcm/index.ts +7 -0
- package/src/lcm-retrieval-lineage.test.ts +236 -0
- package/src/orchestration/xray-capture-queue.test.ts +42 -0
- package/src/orchestration/xray-capture-queue.ts +78 -0
- package/src/orchestrator-xray-capture.test.ts +319 -0
- package/src/orchestrator.ts +76 -8
- package/src/response-guidance-recall.ts +22 -14
- package/src/targeted-fact-recall.ts +25 -16
- package/dist/chunk-4273YO6I.js.map +0 -1
- package/dist/chunk-43VNWTBB.js.map +0 -1
- package/dist/chunk-4463LUIE.js.map +0 -1
- package/dist/chunk-4SBW47WK.js.map +0 -1
- package/dist/chunk-FV5FVO2J.js.map +0 -1
- package/dist/chunk-HHLLAQGZ.js +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-YDF4HL3W.js.map +0 -1
- /package/dist/{chunk-ARDRBJHD.js.map → chunk-C6TKSI35.js.map} +0 -0
- /package/dist/{chunk-N6VIXDT7.js.map → chunk-DAAJUA6Q.js.map} +0 -0
- /package/dist/{chunk-MRQN5Q3I.js.map → chunk-GQURC4B7.js.map} +0 -0
- /package/dist/{chunk-QLYLTKUG.js.map → chunk-MXAGJHTA.js.map} +0 -0
- /package/dist/{chunk-GXLFDLX2.js.map → chunk-PUQZIS6A.js.map} +0 -0
- /package/dist/{chunk-HHLLAQGZ.js.map → chunk-VDC6RZI4.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,
|
|
@@ -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,6 +4,13 @@ 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";
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { XrayCaptureQueue } from "./xray-capture-queue.js";
|
|
5
|
+
|
|
6
|
+
test("initial snapshot read failure releases the next queued capture", async () => {
|
|
7
|
+
const queue = new XrayCaptureQueue();
|
|
8
|
+
let failNextRead = true;
|
|
9
|
+
let snapshot: string | null = "prior";
|
|
10
|
+
const operations: string[] = [];
|
|
11
|
+
const state = {
|
|
12
|
+
read(): string | null {
|
|
13
|
+
if (failNextRead) {
|
|
14
|
+
failNextRead = false;
|
|
15
|
+
throw new Error("snapshot clone failed");
|
|
16
|
+
}
|
|
17
|
+
return snapshot;
|
|
18
|
+
},
|
|
19
|
+
clear(): void {
|
|
20
|
+
snapshot = null;
|
|
21
|
+
},
|
|
22
|
+
restore(value: string | null): void {
|
|
23
|
+
snapshot = value;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const failed = queue.run(async () => {
|
|
28
|
+
operations.push("failed");
|
|
29
|
+
return "unexpected";
|
|
30
|
+
}, state);
|
|
31
|
+
const next = queue.run(async () => {
|
|
32
|
+
operations.push("next");
|
|
33
|
+
snapshot = "fresh";
|
|
34
|
+
return "next-result";
|
|
35
|
+
}, state);
|
|
36
|
+
|
|
37
|
+
await assert.rejects(failed, /snapshot clone failed/);
|
|
38
|
+
const result = await next;
|
|
39
|
+
assert.deepEqual(operations, ["next"]);
|
|
40
|
+
assert.equal(result.result, "next-result");
|
|
41
|
+
assert.equal(result.snapshot, "fresh");
|
|
42
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { abortError, throwIfAborted } from "../abort-error.js";
|
|
2
|
+
|
|
3
|
+
export interface AtomicCaptureState<TSnapshot> {
|
|
4
|
+
read(): TSnapshot | null;
|
|
5
|
+
clear(): void;
|
|
6
|
+
restore(snapshot: TSnapshot | null): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AtomicCaptureResult<TResult, TSnapshot> {
|
|
10
|
+
result: TResult;
|
|
11
|
+
snapshot: TSnapshot | null;
|
|
12
|
+
recallStartedAt: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Per-owner abortable FIFO for operations that publish through one mutable slot. */
|
|
16
|
+
export class XrayCaptureQueue {
|
|
17
|
+
private tail: Promise<void> = Promise.resolve();
|
|
18
|
+
|
|
19
|
+
async run<TResult, TSnapshot>(
|
|
20
|
+
operation: () => Promise<TResult>,
|
|
21
|
+
state: AtomicCaptureState<TSnapshot>,
|
|
22
|
+
signal?: AbortSignal,
|
|
23
|
+
): Promise<AtomicCaptureResult<TResult, TSnapshot>> {
|
|
24
|
+
throwIfAborted(signal, "x-ray capture aborted before queueing");
|
|
25
|
+
const previous = this.tail;
|
|
26
|
+
let release: () => void = () => {};
|
|
27
|
+
this.tail = new Promise<void>((resolve) => {
|
|
28
|
+
release = resolve;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
await this.waitForTurn(previous, signal);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// Keep this abandoned node as a barrier until its predecessor settles,
|
|
35
|
+
// so later callers cannot overtake the still-active operation.
|
|
36
|
+
void previous.then(release, release);
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let previousSnapshot: TSnapshot | null = null;
|
|
41
|
+
let didReadPreviousSnapshot = false;
|
|
42
|
+
try {
|
|
43
|
+
previousSnapshot = state.read();
|
|
44
|
+
didReadPreviousSnapshot = true;
|
|
45
|
+
const recallStartedAt = Date.now();
|
|
46
|
+
state.clear();
|
|
47
|
+
const result = await operation();
|
|
48
|
+
const snapshot = state.read();
|
|
49
|
+
if (!snapshot) state.restore(previousSnapshot);
|
|
50
|
+
return { result, snapshot, recallStartedAt };
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (didReadPreviousSnapshot) state.restore(previousSnapshot);
|
|
53
|
+
throw error;
|
|
54
|
+
} finally {
|
|
55
|
+
release();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private async waitForTurn(previous: Promise<void>, signal?: AbortSignal): Promise<void> {
|
|
60
|
+
throwIfAborted(signal, "x-ray capture aborted while queued");
|
|
61
|
+
if (!signal) {
|
|
62
|
+
await previous;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let onAbort: (() => void) | undefined;
|
|
67
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
68
|
+
onAbort = () => reject(abortError("x-ray capture aborted while queued"));
|
|
69
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
70
|
+
});
|
|
71
|
+
try {
|
|
72
|
+
await Promise.race([previous, aborted]);
|
|
73
|
+
throwIfAborted(signal, "x-ray capture aborted while queued");
|
|
74
|
+
} finally {
|
|
75
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|