@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.
Files changed (118) hide show
  1. package/dist/access-admin-ops-surface.d.ts +1 -0
  2. package/dist/access-admin-ops-surface.js +2 -1
  3. package/dist/access-boundary.d.ts +1 -0
  4. package/dist/access-boundary.js +3 -2
  5. package/dist/access-cli.js +18 -17
  6. package/dist/access-cli.js.map +1 -1
  7. package/dist/access-http.d.ts +1 -0
  8. package/dist/access-http.js +7 -6
  9. package/dist/access-identity-continuity-surface.d.ts +1 -0
  10. package/dist/access-identity-continuity-surface.js +2 -1
  11. package/dist/access-lcm-surface.d.ts +1 -0
  12. package/dist/access-lcm-surface.js +2 -1
  13. package/dist/access-mcp.d.ts +1 -0
  14. package/dist/access-mcp.js +6 -5
  15. package/dist/access-observe-write-surface.d.ts +1 -0
  16. package/dist/access-observe-write-surface.js +2 -1
  17. package/dist/access-operations-batch.js +4 -3
  18. package/dist/access-operations.d.ts +1 -0
  19. package/dist/access-operations.js +5 -4
  20. package/dist/access-recall-surface.d.ts +1 -0
  21. package/dist/access-recall-surface.js +2 -1
  22. package/dist/access-service.d.ts +1 -0
  23. package/dist/access-service.js +2 -1
  24. package/dist/access-surface-catalog.d.ts +1 -0
  25. package/dist/bootstrap.d.ts +1 -0
  26. package/dist/causal-behavior.d.ts +1 -0
  27. package/dist/{chunk-L6W77GWW.js → chunk-6TCXIRP5.js} +25 -13
  28. package/dist/chunk-6TCXIRP5.js.map +1 -0
  29. package/dist/chunk-C37ZRXX7.js +36 -0
  30. package/dist/chunk-C37ZRXX7.js.map +1 -0
  31. package/dist/{chunk-SGYTQKC2.js → chunk-C6TKSI35.js} +3 -3
  32. package/dist/{chunk-C2SEH754.js → chunk-DAAJUA6Q.js} +4 -4
  33. package/dist/{chunk-FV5FVO2J.js → chunk-DEOYTFA6.js} +9 -5
  34. package/dist/chunk-DEOYTFA6.js.map +1 -0
  35. package/dist/{chunk-PYPOFEMK.js → chunk-DGFU5MSX.js} +16 -1
  36. package/dist/chunk-DGFU5MSX.js.map +1 -0
  37. package/dist/{chunk-EAREADWV.js → chunk-GQURC4B7.js} +2 -2
  38. package/dist/{chunk-XA4PWKBW.js → chunk-MXAGJHTA.js} +3 -3
  39. package/dist/{chunk-LZTFCAKE.js → chunk-OH7QLI6W.js} +28 -15
  40. package/dist/chunk-OH7QLI6W.js.map +1 -0
  41. package/dist/{chunk-5BHIGBHQ.js → chunk-OXVKR5OU.js} +6 -3
  42. package/dist/{chunk-5BHIGBHQ.js.map → chunk-OXVKR5OU.js.map} +1 -1
  43. package/dist/{chunk-IF7THCGJ.js → chunk-PUQZIS6A.js} +3 -3
  44. package/dist/{chunk-4SBW47WK.js → chunk-T67LUYCR.js} +28 -15
  45. package/dist/chunk-T67LUYCR.js.map +1 -0
  46. package/dist/{chunk-4463LUIE.js → chunk-TVIVY7KU.js} +180 -70
  47. package/dist/chunk-TVIVY7KU.js.map +1 -0
  48. package/dist/{chunk-KCYE2MZM.js → chunk-UXPUUL3G.js} +29 -8
  49. package/dist/chunk-UXPUUL3G.js.map +1 -0
  50. package/dist/chunk-VDC6RZI4.js +1 -0
  51. package/dist/{chunk-3RROR5KV.js → chunk-VJRJTRYF.js} +4 -4
  52. package/dist/{chunk-5WJ4LBFR.js → chunk-WSOEYOHN.js} +8 -8
  53. package/dist/{chunk-5AJIDEFT.js → chunk-XFTPUXCE.js} +11 -7
  54. package/dist/chunk-XFTPUXCE.js.map +1 -0
  55. package/dist/cli.d.ts +1 -0
  56. package/dist/cli.js +8 -7
  57. package/dist/compounding/preference-consolidator.d.ts +1 -0
  58. package/dist/event-order-recall.d.ts +1 -0
  59. package/dist/event-order-recall.js +3 -2
  60. package/dist/evidence-pack.d.ts +18 -1
  61. package/dist/evidence-pack.js +1 -1
  62. package/dist/explicit-capture.d.ts +1 -0
  63. package/dist/explicit-cue-recall.d.ts +19 -1
  64. package/dist/explicit-cue-recall.js +3 -2
  65. package/dist/focused-list-recall.d.ts +1 -0
  66. package/dist/focused-list-recall.js +3 -2
  67. package/dist/index.d.ts +3 -2
  68. package/dist/index.js +18 -17
  69. package/dist/lcm/engine.d.ts +3 -0
  70. package/dist/lcm/engine.js +2 -2
  71. package/dist/lcm/index.d.ts +23 -1
  72. package/dist/lcm/index.js +14 -4
  73. package/dist/lcm/recall.d.ts +15 -1
  74. package/dist/lcm/recall.js +5 -3
  75. package/dist/lcm/tools.d.ts +1 -0
  76. package/dist/mcp-memory-inspector-app.d.ts +1 -0
  77. package/dist/orchestrator.d.ts +1 -0
  78. package/dist/orchestrator.js +18 -17
  79. package/dist/response-guidance-recall.d.ts +1 -0
  80. package/dist/response-guidance-recall.js +3 -2
  81. package/dist/targeted-fact-recall.d.ts +1 -0
  82. package/dist/targeted-fact-recall.js +3 -2
  83. package/package.json +2 -2
  84. package/src/access-lcm-surface.ts +20 -5
  85. package/src/access-recall-surface.ts +2 -1
  86. package/src/event-order-recall.ts +5 -3
  87. package/src/evidence-pack.test.ts +66 -1
  88. package/src/evidence-pack.ts +41 -0
  89. package/src/explicit-cue-recall.ts +291 -133
  90. package/src/focused-list-recall.ts +25 -16
  91. package/src/index.ts +3 -0
  92. package/src/lcm/engine.ts +17 -5
  93. package/src/lcm/evidence-identity.ts +59 -0
  94. package/src/lcm/index.ts +13 -1
  95. package/src/lcm/recall.ts +48 -5
  96. package/src/lcm-engine.test.ts +46 -0
  97. package/src/lcm-recall.test.ts +48 -1
  98. package/src/lcm-retrieval-lineage.test.ts +236 -0
  99. package/src/recall-trace-receipts.test.ts +380 -0
  100. package/src/response-guidance-recall.ts +22 -14
  101. package/src/targeted-fact-recall.ts +25 -16
  102. package/dist/chunk-4463LUIE.js.map +0 -1
  103. package/dist/chunk-4SBW47WK.js.map +0 -1
  104. package/dist/chunk-5AJIDEFT.js.map +0 -1
  105. package/dist/chunk-FV5FVO2J.js.map +0 -1
  106. package/dist/chunk-HHLLAQGZ.js +0 -1
  107. package/dist/chunk-KCYE2MZM.js.map +0 -1
  108. package/dist/chunk-L6W77GWW.js.map +0 -1
  109. package/dist/chunk-LZTFCAKE.js.map +0 -1
  110. package/dist/chunk-PYPOFEMK.js.map +0 -1
  111. /package/dist/{chunk-SGYTQKC2.js.map → chunk-C6TKSI35.js.map} +0 -0
  112. /package/dist/{chunk-C2SEH754.js.map → chunk-DAAJUA6Q.js.map} +0 -0
  113. /package/dist/{chunk-EAREADWV.js.map → chunk-GQURC4B7.js.map} +0 -0
  114. /package/dist/{chunk-XA4PWKBW.js.map → chunk-MXAGJHTA.js.map} +0 -0
  115. /package/dist/{chunk-IF7THCGJ.js.map → chunk-PUQZIS6A.js.map} +0 -0
  116. /package/dist/{chunk-HHLLAQGZ.js.map → chunk-VDC6RZI4.js.map} +0 -0
  117. /package/dist/{chunk-3RROR5KV.js.map → chunk-VJRJTRYF.js.map} +0 -0
  118. /package/dist/{chunk-5WJ4LBFR.js.map → chunk-WSOEYOHN.js.map} +0 -0
@@ -0,0 +1,380 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import {
5
+ buildExplicitCueRecallSection,
6
+ buildTrajectoryAnalysisRecallSection,
7
+ type ExplicitCueRecallEngine,
8
+ type TrajectoryAnalysisLineReceipt,
9
+ } from "./explicit-cue-recall.js";
10
+ import type { EvidencePackSelectionReceipt } from "./evidence-pack.js";
11
+
12
+ interface ReceiptRow {
13
+ id?: number;
14
+ session_id: string;
15
+ turn_index: number;
16
+ role: string;
17
+ content: string;
18
+ }
19
+
20
+ class ReceiptEngine implements ExplicitCueRecallEngine {
21
+ constructor(private readonly rows: ReceiptRow[]) {}
22
+
23
+ async expandContext(
24
+ sessionId: string,
25
+ fromTurn: number,
26
+ toTurn: number,
27
+ ): Promise<ReceiptRow[]> {
28
+ return this.rows.filter((row) =>
29
+ row.session_id === sessionId &&
30
+ row.turn_index >= fromTurn &&
31
+ row.turn_index <= toTurn
32
+ );
33
+ }
34
+
35
+ async searchContextFull(): Promise<Array<ReceiptRow & { score: number }>> {
36
+ return [];
37
+ }
38
+
39
+ async getStats(): Promise<{ totalMessages: number; maxTurnIndex?: number }> {
40
+ return {
41
+ totalMessages: this.rows.length,
42
+ maxTurnIndex: this.rows.length > 0
43
+ ? Math.max(...this.rows.map((row) => row.turn_index))
44
+ : undefined,
45
+ };
46
+ }
47
+ }
48
+
49
+ test("explicit-cue receipts identify only blocks that survive selection", async () => {
50
+ const engine = new ReceiptEngine([
51
+ {
52
+ id: 101,
53
+ session_id: "s",
54
+ turn_index: 7,
55
+ role: "user",
56
+ content: "first selected row",
57
+ },
58
+ {
59
+ id: 102,
60
+ session_id: "s",
61
+ turn_index: 7,
62
+ role: "assistant",
63
+ content: "second row that the deliberately narrow section budget rejects",
64
+ },
65
+ ]);
66
+ const receipts: EvidencePackSelectionReceipt[] = [];
67
+ const options = {
68
+ engine,
69
+ sessionId: "s",
70
+ query: "Review turn 7",
71
+ maxChars:
72
+ "## Explicit Cue Evidence".length +
73
+ 2 +
74
+ "[s, turn 7, user]: first selected row".length,
75
+ maxItemChars: 200,
76
+ };
77
+
78
+ const baseline = await buildExplicitCueRecallSection(options);
79
+ const traced = await buildExplicitCueRecallSection({
80
+ ...options,
81
+ onEvidenceSelected: (receipt) => receipts.push(receipt),
82
+ });
83
+
84
+ assert.equal(traced, baseline);
85
+ assert.deepEqual(receipts.map((receipt) => receipt.item.archiveRowId), [101]);
86
+ assert.equal(
87
+ traced.slice(receipts[0]!.blockStart, receipts[0]!.blockEnd).includes(
88
+ "first selected row",
89
+ ),
90
+ true,
91
+ );
92
+ assert.equal(JSON.stringify(receipts).includes("first selected row"), false);
93
+ assert.equal(JSON.stringify(receipts).includes("second row"), false);
94
+ });
95
+
96
+ test("trajectory receipts retain row lineage only for emitted clipped lines", async () => {
97
+ const engine = new ReceiptEngine([
98
+ {
99
+ id: 201,
100
+ session_id: "ama",
101
+ turn_index: 1,
102
+ role: "user",
103
+ content: "[Action 1]: move right",
104
+ },
105
+ {
106
+ id: 202,
107
+ session_id: "ama",
108
+ turn_index: 1,
109
+ role: "assistant",
110
+ content: "[Observation 1]: reached the wall",
111
+ },
112
+ {
113
+ id: 203,
114
+ session_id: "ama",
115
+ turn_index: 2,
116
+ role: "user",
117
+ content: "[Action 2]: move left",
118
+ },
119
+ {
120
+ id: 204,
121
+ session_id: "ama",
122
+ turn_index: 2,
123
+ role: "assistant",
124
+ content: "[Observation 2]: reached the door",
125
+ },
126
+ ]);
127
+ const receipts: TrajectoryAnalysisLineReceipt[] = [];
128
+ const options = {
129
+ engine,
130
+ sessionId: "ama",
131
+ query:
132
+ "What will be the resulting state at action 1? Provide the full observation.",
133
+ maxChars: 4_000,
134
+ };
135
+
136
+ const baseline = await buildTrajectoryAnalysisRecallSection(options);
137
+ const traced = await buildTrajectoryAnalysisRecallSection({
138
+ ...options,
139
+ onLineSelected: (receipt) => receipts.push(receipt),
140
+ });
141
+
142
+ assert.equal(traced, baseline);
143
+ const actionOne = receipts.find((receipt) =>
144
+ traced.slice(receipt.lineStart, receipt.lineEnd).startsWith("[Action 1]")
145
+ );
146
+ const observationOne = receipts.find((receipt) =>
147
+ traced.slice(receipt.lineStart, receipt.lineEnd).startsWith("[Observation 1]")
148
+ );
149
+ assert.ok(actionOne);
150
+ assert.ok(observationOne);
151
+ assert.equal(actionOne.lineageStatus, "exact");
152
+ assert.equal(observationOne.lineageStatus, "exact");
153
+ assert.deepEqual(actionOne.actionArchiveRowIds, [201]);
154
+ assert.deepEqual(actionOne.observationArchiveRowIds, []);
155
+ assert.deepEqual(observationOne.actionArchiveRowIds, []);
156
+ assert.deepEqual(observationOne.observationArchiveRowIds, [202]);
157
+ assert.equal(actionOne.actionArchiveRowIds.includes(203), false);
158
+ assert.equal(observationOne.observationArchiveRowIds.includes(204), false);
159
+
160
+ const clippedReceipts: TrajectoryAnalysisLineReceipt[] = [];
161
+ const clipped = await buildTrajectoryAnalysisRecallSection({
162
+ ...options,
163
+ maxChars: actionOne.lineStart + 8,
164
+ onLineSelected: (receipt) => clippedReceipts.push(receipt),
165
+ });
166
+ const partialAction = clippedReceipts.at(-1)!;
167
+ assert.equal(partialAction.lineStart, actionOne.lineStart);
168
+ assert.equal(partialAction.lineEnd, clipped.length);
169
+ assert.equal(partialAction.lineageStatus, "exact");
170
+ assert.deepEqual(partialAction.actionArchiveRowIds, [201]);
171
+ assert.equal(partialAction.actionArchiveRowIds.includes(203), false);
172
+ assert.equal(JSON.stringify(receipts).includes("move right"), false);
173
+ assert.equal(JSON.stringify(receipts).includes("reached the wall"), false);
174
+ });
175
+
176
+ test("spatial derived-line receipts mark lineage unavailable without guessing siblings", async () => {
177
+ const engine = new ReceiptEngine([
178
+ {
179
+ id: 301,
180
+ session_id: "ama",
181
+ turn_index: 20,
182
+ role: "user",
183
+ content: "[Action 20]: right",
184
+ },
185
+ {
186
+ id: 302,
187
+ session_id: "ama",
188
+ turn_index: 20,
189
+ role: "assistant",
190
+ content: [
191
+ "[Observation 20]: Active rules:",
192
+ "baba is you",
193
+ "",
194
+ "Objects on the map:",
195
+ "rule `win` 3 step to the left",
196
+ ].join("\n"),
197
+ },
198
+ {
199
+ id: 303,
200
+ session_id: "ama",
201
+ turn_index: 21,
202
+ role: "user",
203
+ content: "[Action 21]: left",
204
+ },
205
+ {
206
+ id: 304,
207
+ session_id: "ama",
208
+ turn_index: 21,
209
+ role: "assistant",
210
+ content: [
211
+ "[Observation 21]: Active rules:",
212
+ "baba is you",
213
+ "",
214
+ "Objects on the map:",
215
+ "rule `win` 2 step to the left",
216
+ ].join("\n"),
217
+ },
218
+ ]);
219
+ const receipts: TrajectoryAnalysisLineReceipt[] = [];
220
+ const options = {
221
+ engine,
222
+ sessionId: "ama",
223
+ query:
224
+ "Between steps 20 and 21, the rule 'win' block's relative position changed from 3 step to the left to 2 step to the left. What was the actual movement?",
225
+ maxChars: 4_000,
226
+ };
227
+
228
+ const baseline = await buildTrajectoryAnalysisRecallSection(options);
229
+ const traced = await buildTrajectoryAnalysisRecallSection({
230
+ ...options,
231
+ onLineSelected: (receipt) => receipts.push(receipt),
232
+ });
233
+
234
+ assert.equal(traced, baseline);
235
+ const heading = receipts.find((receipt) =>
236
+ traced.slice(receipt.lineStart, receipt.lineEnd) === "Relative-position movement cues:"
237
+ );
238
+ const movement = receipts.find((receipt) =>
239
+ traced.slice(receipt.lineStart, receipt.lineEnd).startsWith("Observation 20->21:")
240
+ );
241
+ assert.ok(heading);
242
+ assert.ok(movement);
243
+ assert.equal(heading.lineageStatus, "exact");
244
+ assert.deepEqual(heading.actionArchiveRowIds, []);
245
+ assert.deepEqual(heading.observationArchiveRowIds, []);
246
+ assert.equal(movement.lineageStatus, "unavailable");
247
+ assert.deepEqual(movement.actionArchiveRowIds, []);
248
+ assert.deepEqual(movement.observationArchiveRowIds, []);
249
+ assert.equal(JSON.stringify(movement).includes("rule win"), false);
250
+ });
251
+
252
+ test("trajectory receipts mark id-less and partially id-less direct sources unavailable", async () => {
253
+ const engine = new ReceiptEngine([
254
+ {
255
+ session_id: "ama",
256
+ turn_index: 1,
257
+ role: "user",
258
+ content: "[Action 1]: move right",
259
+ },
260
+ {
261
+ id: 402,
262
+ session_id: "ama",
263
+ turn_index: 1,
264
+ role: "assistant",
265
+ content: "[Observation 1]: reached the wall",
266
+ },
267
+ {
268
+ session_id: "ama",
269
+ turn_index: 2,
270
+ role: "user",
271
+ content: "[Action 2]: move left",
272
+ },
273
+ {
274
+ session_id: "ama",
275
+ turn_index: 2,
276
+ role: "assistant",
277
+ content: "[Observation 2]: reached the door",
278
+ },
279
+ ]);
280
+ const receipts: TrajectoryAnalysisLineReceipt[] = [];
281
+ const text = await buildTrajectoryAnalysisRecallSection({
282
+ engine,
283
+ sessionId: "ama",
284
+ query: "Compare steps 1 and 2 and provide the full observations.",
285
+ maxChars: 4_000,
286
+ onLineSelected: (receipt) => receipts.push(receipt),
287
+ });
288
+ const findLine = (prefix: string) => receipts.find((receipt) =>
289
+ text.slice(receipt.lineStart, receipt.lineEnd).startsWith(prefix)
290
+ );
291
+
292
+ const idLessAction = findLine("[Action 1]");
293
+ const identifiedObservation = findLine("[Observation 1]");
294
+ const idLessObservation = findLine("[Observation 2]");
295
+ assert.ok(idLessAction);
296
+ assert.ok(identifiedObservation);
297
+ assert.ok(idLessObservation);
298
+ assert.equal(idLessAction.lineageStatus, "unavailable");
299
+ assert.deepEqual(idLessAction.actionArchiveRowIds, []);
300
+ assert.equal(identifiedObservation.lineageStatus, "exact");
301
+ assert.deepEqual(identifiedObservation.observationArchiveRowIds, [402]);
302
+ assert.equal(idLessObservation.lineageStatus, "unavailable");
303
+ assert.deepEqual(idLessObservation.observationArchiveRowIds, []);
304
+ });
305
+
306
+ test("inferred-location receipts identify only the action that establishes the final location", async () => {
307
+ const engine = new ReceiptEngine([
308
+ {
309
+ id: 501,
310
+ session_id: "ama",
311
+ turn_index: 1,
312
+ role: "user",
313
+ content: "[Action 1]: open cabinet 1",
314
+ },
315
+ {
316
+ id: 502,
317
+ session_id: "ama",
318
+ turn_index: 2,
319
+ role: "user",
320
+ content: "[Action 2]: move cabinet 1 to kitchen",
321
+ },
322
+ ]);
323
+ const receipts: TrajectoryAnalysisLineReceipt[] = [];
324
+ const text = await buildTrajectoryAnalysisRecallSection({
325
+ engine,
326
+ sessionId: "ama",
327
+ query:
328
+ "What is the state of cabinet 1 at step 2 and what was its prior whole changes history?",
329
+ maxChars: 4_000,
330
+ onLineSelected: (receipt) => receipts.push(receipt),
331
+ });
332
+ const location = receipts.find((receipt) =>
333
+ text.slice(receipt.lineStart, receipt.lineEnd).startsWith(
334
+ "Inferred cabinet 1 location at step 2: kitchen",
335
+ )
336
+ );
337
+
338
+ assert.ok(location);
339
+ assert.equal(location.lineageStatus, "exact");
340
+ assert.deepEqual(location.actionArchiveRowIds, [502]);
341
+ assert.equal(location.actionArchiveRowIds.includes(501), false);
342
+ assert.deepEqual(location.observationArchiveRowIds, []);
343
+ });
344
+
345
+ test("stateful inventory-change lines do not publish partial current-row lineage", async () => {
346
+ const engine = new ReceiptEngine([
347
+ {
348
+ id: 601,
349
+ session_id: "ama",
350
+ turn_index: 1,
351
+ role: "user",
352
+ content: "[Action 1]: take apple 1 from shelf 1",
353
+ },
354
+ {
355
+ id: 602,
356
+ session_id: "ama",
357
+ turn_index: 2,
358
+ role: "user",
359
+ content: "[Action 2]: place apple 1 into bowl 1",
360
+ },
361
+ ]);
362
+ const receipts: TrajectoryAnalysisLineReceipt[] = [];
363
+ const text = await buildTrajectoryAnalysisRecallSection({
364
+ engine,
365
+ sessionId: "ama",
366
+ query: "What changes occurred to the inventory throughout the trajectory?",
367
+ maxChars: 4_000,
368
+ onLineSelected: (receipt) => receipts.push(receipt),
369
+ });
370
+ const placed = receipts.find((receipt) =>
371
+ text.slice(receipt.lineStart, receipt.lineEnd).startsWith(
372
+ "[Action 2]: place apple 1 into bowl 1 => inventory removed apple 1",
373
+ )
374
+ );
375
+
376
+ assert.ok(placed);
377
+ assert.equal(placed.lineageStatus, "unavailable");
378
+ assert.deepEqual(placed.actionArchiveRowIds, []);
379
+ assert.deepEqual(placed.observationArchiveRowIds, []);
380
+ });
@@ -1,5 +1,6 @@
1
1
  import { buildEvidencePack, type EvidencePackItem } from "./evidence-pack.js";
2
2
  import type { ExplicitCueRecallEngine } from "./explicit-cue-recall.js";
3
+ import { isSameLcmRow, lcmEvidenceIdentity } from "./lcm/evidence-identity.js";
3
4
  import {
4
5
  gatherAcrossReadSessions,
5
6
  resolveLcmReadSessionIds,
@@ -252,19 +253,26 @@ async function collectGuidanceItems(
252
253
  searchWindowTokens,
253
254
  );
254
255
  const candidates = expanded.length > 0
255
- ? expanded.map((message) => ({
256
- id: `${result.session_id}:${message.turn_index}`,
257
- sessionId: result.session_id,
258
- turnIndex: message.turn_index,
259
- role: message.role,
260
- content: message.content,
261
- ...(message.turn_index === result.turn_index &&
262
- typeof result.score === "number"
263
- ? { score: result.score }
264
- : {}),
265
- }))
256
+ ? expanded.map((message) => {
257
+ const matchesSearchHit = isSameLcmRow(
258
+ message,
259
+ result.session_id,
260
+ result,
261
+ result.session_id,
262
+ );
263
+ return {
264
+ ...lcmEvidenceIdentity(message, result.session_id),
265
+ sessionId: message.session_id ?? result.session_id,
266
+ turnIndex: message.turn_index,
267
+ role: message.role,
268
+ content: message.content,
269
+ ...(matchesSearchHit && typeof result.score === "number"
270
+ ? { score: result.score }
271
+ : {}),
272
+ };
273
+ })
266
274
  : [{
267
- id: `${result.session_id}:${result.turn_index}`,
275
+ ...lcmEvidenceIdentity(result, result.session_id),
268
276
  sessionId: result.session_id,
269
277
  turnIndex: result.turn_index,
270
278
  role: result.role,
@@ -331,8 +339,8 @@ async function collectGuidanceScanItems(
331
339
  for (const message of messages) {
332
340
  if (!isGuidanceEvidence(message.content, options.query, intents, options.forceGeneric === true)) continue;
333
341
  items.push({
334
- id: `${options.sessionId}:${message.turn_index}`,
335
- sessionId: options.sessionId,
342
+ ...lcmEvidenceIdentity(message, options.sessionId),
343
+ sessionId: message.session_id ?? options.sessionId,
336
344
  turnIndex: message.turn_index,
337
345
  role: message.role,
338
346
  content: message.content,
@@ -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
  import {
13
14
  unifiedDedupeAndRank,
@@ -124,27 +125,35 @@ async function collectTargetedFactSearchItems(
124
125
  result.turn_index + searchWindowAfter,
125
126
  searchWindowTokens,
126
127
  );
128
+ const searchIdentity = lcmEvidenceIdentity(result, result.session_id);
127
129
  const searchHit: EvidencePackItem = {
128
- id: `${result.session_id}:${result.turn_index}`,
130
+ ...searchIdentity,
129
131
  sessionId: result.session_id,
130
132
  turnIndex: result.turn_index,
131
133
  role: result.role,
132
134
  content: result.content,
133
135
  ...(typeof result.score === "number" ? { score: result.score } : {}),
134
136
  };
135
- const candidates: EvidencePackItem[] = expanded.map((message) => ({
136
- id: `${result.session_id}:${message.turn_index}`,
137
- sessionId: result.session_id,
138
- turnIndex: message.turn_index,
139
- role: message.role,
140
- content: message.content,
141
- ...(message.turn_index === result.turn_index &&
142
- typeof result.score === "number"
143
- ? { score: result.score }
144
- : {}),
145
- }));
146
- const hitIndex = candidates.findIndex((candidate) =>
147
- candidate.turnIndex === result.turn_index
137
+ const candidates: EvidencePackItem[] = expanded.map((message) => {
138
+ const matchesSearchHit = isSameLcmRow(
139
+ message,
140
+ result.session_id,
141
+ result,
142
+ result.session_id,
143
+ );
144
+ return {
145
+ ...lcmEvidenceIdentity(message, result.session_id),
146
+ sessionId: message.session_id ?? result.session_id,
147
+ turnIndex: message.turn_index,
148
+ role: message.role,
149
+ content: message.content,
150
+ ...(matchesSearchHit && typeof result.score === "number"
151
+ ? { score: result.score }
152
+ : {}),
153
+ };
154
+ });
155
+ const hitIndex = expanded.findIndex((message) =>
156
+ isSameLcmRow(message, result.session_id, result, result.session_id)
148
157
  );
149
158
  if (hitIndex >= 0) {
150
159
  candidates[hitIndex] = searchHit;
@@ -200,8 +209,8 @@ async function collectTargetedFactScanItems(
200
209
  for (const message of messages) {
201
210
  if (!isTargetedFactEvidence(message.content, options.query)) continue;
202
211
  items.push({
203
- id: `${options.sessionId}:${message.turn_index}`,
204
- sessionId: options.sessionId,
212
+ ...lcmEvidenceIdentity(message, options.sessionId),
213
+ sessionId: message.session_id ?? options.sessionId,
205
214
  turnIndex: message.turn_index,
206
215
  role: message.role,
207
216
  content: message.content,