@remnic/core 9.6.29 → 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.
Files changed (77) hide show
  1. package/dist/access-admin-ops-surface.js +2 -1
  2. package/dist/access-boundary.js +3 -2
  3. package/dist/access-cli.js +16 -15
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.js +7 -6
  6. package/dist/access-identity-continuity-surface.js +2 -1
  7. package/dist/access-lcm-surface.js +2 -1
  8. package/dist/access-mcp.js +6 -5
  9. package/dist/access-observe-write-surface.js +2 -1
  10. package/dist/access-operations-batch.js +4 -3
  11. package/dist/access-operations.js +5 -4
  12. package/dist/access-recall-surface.js +2 -1
  13. package/dist/access-service.js +2 -1
  14. package/dist/{chunk-5WJ4LBFR.js → chunk-3VSTTB74.js} +7 -7
  15. package/dist/{chunk-4463LUIE.js → chunk-77UPM7P3.js} +22 -11
  16. package/dist/chunk-77UPM7P3.js.map +1 -0
  17. package/dist/{chunk-FV5FVO2J.js → chunk-7O2JL5RY.js} +9 -5
  18. package/dist/chunk-7O2JL5RY.js.map +1 -0
  19. package/dist/{chunk-LZTFCAKE.js → chunk-BFU7H32D.js} +28 -15
  20. package/dist/chunk-BFU7H32D.js.map +1 -0
  21. package/dist/chunk-C37ZRXX7.js +36 -0
  22. package/dist/chunk-C37ZRXX7.js.map +1 -0
  23. package/dist/{chunk-SGYTQKC2.js → chunk-C6TKSI35.js} +3 -3
  24. package/dist/{chunk-C2SEH754.js → chunk-DAAJUA6Q.js} +4 -4
  25. package/dist/{chunk-EAREADWV.js → chunk-GQURC4B7.js} +2 -2
  26. package/dist/{chunk-4SBW47WK.js → chunk-IRMLYBMU.js} +28 -15
  27. package/dist/chunk-IRMLYBMU.js.map +1 -0
  28. package/dist/{chunk-XA4PWKBW.js → chunk-MXAGJHTA.js} +3 -3
  29. package/dist/{chunk-5BHIGBHQ.js → chunk-OXVKR5OU.js} +6 -3
  30. package/dist/{chunk-5BHIGBHQ.js.map → chunk-OXVKR5OU.js.map} +1 -1
  31. package/dist/{chunk-IF7THCGJ.js → chunk-PUQZIS6A.js} +3 -3
  32. package/dist/{chunk-PYPOFEMK.js → chunk-R26WO46Z.js} +1 -1
  33. package/dist/chunk-R26WO46Z.js.map +1 -0
  34. package/dist/chunk-VDC6RZI4.js +1 -0
  35. package/dist/{chunk-3RROR5KV.js → chunk-VJRJTRYF.js} +4 -4
  36. package/dist/{chunk-L6W77GWW.js → chunk-XGSOXFD4.js} +25 -13
  37. package/dist/chunk-XGSOXFD4.js.map +1 -0
  38. package/dist/cli.js +8 -7
  39. package/dist/event-order-recall.js +3 -2
  40. package/dist/evidence-pack.d.ts +2 -0
  41. package/dist/evidence-pack.js +1 -1
  42. package/dist/explicit-cue-recall.d.ts +3 -0
  43. package/dist/explicit-cue-recall.js +3 -2
  44. package/dist/focused-list-recall.js +3 -2
  45. package/dist/index.js +16 -15
  46. package/dist/lcm/index.d.ts +22 -0
  47. package/dist/lcm/index.js +9 -1
  48. package/dist/orchestrator.js +16 -15
  49. package/dist/response-guidance-recall.js +3 -2
  50. package/dist/targeted-fact-recall.js +3 -2
  51. package/package.json +2 -2
  52. package/src/access-lcm-surface.ts +20 -5
  53. package/src/access-recall-surface.ts +2 -1
  54. package/src/event-order-recall.ts +5 -3
  55. package/src/evidence-pack.ts +2 -0
  56. package/src/explicit-cue-recall.ts +67 -66
  57. package/src/focused-list-recall.ts +25 -16
  58. package/src/lcm/evidence-identity.ts +59 -0
  59. package/src/lcm/index.ts +7 -0
  60. package/src/lcm-retrieval-lineage.test.ts +236 -0
  61. package/src/response-guidance-recall.ts +22 -14
  62. package/src/targeted-fact-recall.ts +25 -16
  63. package/dist/chunk-4463LUIE.js.map +0 -1
  64. package/dist/chunk-4SBW47WK.js.map +0 -1
  65. package/dist/chunk-FV5FVO2J.js.map +0 -1
  66. package/dist/chunk-HHLLAQGZ.js +0 -1
  67. package/dist/chunk-L6W77GWW.js.map +0 -1
  68. package/dist/chunk-LZTFCAKE.js.map +0 -1
  69. package/dist/chunk-PYPOFEMK.js.map +0 -1
  70. /package/dist/{chunk-5WJ4LBFR.js.map → chunk-3VSTTB74.js.map} +0 -0
  71. /package/dist/{chunk-SGYTQKC2.js.map → chunk-C6TKSI35.js.map} +0 -0
  72. /package/dist/{chunk-C2SEH754.js.map → chunk-DAAJUA6Q.js.map} +0 -0
  73. /package/dist/{chunk-EAREADWV.js.map → chunk-GQURC4B7.js.map} +0 -0
  74. /package/dist/{chunk-XA4PWKBW.js.map → chunk-MXAGJHTA.js.map} +0 -0
  75. /package/dist/{chunk-IF7THCGJ.js.map → chunk-PUQZIS6A.js.map} +0 -0
  76. /package/dist/{chunk-HHLLAQGZ.js.map → chunk-VDC6RZI4.js.map} +0 -0
  77. /package/dist/{chunk-3RROR5KV.js.map → chunk-VJRJTRYF.js.map} +0 -0
@@ -4,6 +4,10 @@ import {
4
4
  resolveLcmReadSessionIds,
5
5
  } from "./lcm-fallback-read.js";
6
6
  import { unifiedDedupeAndRank } from "./recall-pipeline-stages.js";
7
+ import {
8
+ lcmArchiveRowId,
9
+ lcmEvidenceIdentity,
10
+ } from "./lcm/evidence-identity.js";
7
11
 
8
12
  export interface ExplicitCueRecallEngine {
9
13
  expandContext(
@@ -11,7 +15,15 @@ export interface ExplicitCueRecallEngine {
11
15
  fromTurn: number,
12
16
  toTurn: number,
13
17
  maxTokens: number,
14
- ): Promise<Array<{ turn_index: number; role: string; content: string }>>;
18
+ ): Promise<
19
+ Array<{
20
+ id?: number;
21
+ session_id?: string;
22
+ turn_index: number;
23
+ role: string;
24
+ content: string;
25
+ }>
26
+ >;
15
27
  getStats?(sessionId?: string): Promise<{
16
28
  totalMessages: number;
17
29
  maxTurnIndex?: number;
@@ -22,6 +34,7 @@ export interface ExplicitCueRecallEngine {
22
34
  sessionId?: string,
23
35
  ): Promise<
24
36
  Array<{
37
+ id?: number;
25
38
  turn_index: number;
26
39
  role: string;
27
40
  content: string;
@@ -62,6 +75,16 @@ export type ExplicitTurnReference = {
62
75
  includeDirectTurn: boolean;
63
76
  };
64
77
 
78
+ interface ExplicitCueEvidenceItem {
79
+ id: string;
80
+ archiveRowId?: number;
81
+ sessionId: string;
82
+ turnIndex: number;
83
+ role: string;
84
+ content: string;
85
+ score?: number;
86
+ }
87
+
65
88
  const DEFAULT_MAX_CHARS = 2_400;
66
89
  const DEFAULT_MAX_ITEM_CHARS = 1_200;
67
90
  const DEFAULT_MAX_REFERENCES = 24;
@@ -326,23 +349,17 @@ export async function buildExplicitCueRecallSection(
326
349
  return "";
327
350
  }
328
351
 
329
- const evidenceItems: Array<{
330
- id: string;
331
- sessionId: string;
332
- turnIndex: number;
333
- role: string;
334
- content: string;
335
- score?: number;
336
- }> = [];
352
+ const evidenceItems: ExplicitCueEvidenceItem[] = [];
337
353
  const seenTurns = new Set<string>();
338
354
 
339
355
  // #1505 codex P2: gather cue evidence across the ordered LCM read key set
340
356
  // (primary overlay → project/root fallbacks) into ONE shared accumulator
341
357
  // (`evidenceItems` / `seenTurns`), so a branch-scoped session's project/root
342
358
  // fallback cues are RECOVERED instead of being skipped by the old
343
- // first-non-empty short-circuit. `seenTurns` dedupes across keys by
344
- // `session_id`+`turn_index`; the budget is applied exactly once in the
345
- // `buildEvidencePack` call below. `gatherAcrossReadSessions` isolates a
359
+ // first-non-empty short-circuit. `seenTurns` is the historical local name;
360
+ // its keys use archive-row identity when available and fall back to
361
+ // `session_id`+`turn_index` for legacy engines. The budget is applied exactly
362
+ // once in the `buildEvidencePack` call below. `gatherAcrossReadSessions` isolates a
346
363
  // per-key read failure (a corrupt/locked fallback index must not discard the
347
364
  // other keys' cues); single-key recall runs each collector directly, so a
348
365
  // failure propagates exactly as before.
@@ -510,13 +527,7 @@ async function collectTurnReferenceEvidence(options: {
510
527
  sessionId?: string;
511
528
  query: string;
512
529
  maxReferences: number;
513
- evidenceItems: Array<{
514
- id: string;
515
- sessionId: string;
516
- turnIndex: number;
517
- role: string;
518
- content: string;
519
- }>;
530
+ evidenceItems: ExplicitCueEvidenceItem[];
520
531
  seenTurns: Set<string>;
521
532
  }): Promise<void> {
522
533
  if (!options.sessionId) {
@@ -575,13 +586,7 @@ async function collectFocusedTranscriptCueEvidence(options: {
575
586
  engine: ExplicitCueRecallEngine;
576
587
  sessionId?: string;
577
588
  query: string;
578
- evidenceItems: Array<{
579
- id: string;
580
- sessionId: string;
581
- turnIndex: number;
582
- role: string;
583
- content: string;
584
- }>;
589
+ evidenceItems: ExplicitCueEvidenceItem[];
585
590
  seenTurns: Set<string>;
586
591
  }): Promise<void> {
587
592
  if (!options.sessionId || !options.engine.getStats) {
@@ -611,8 +616,8 @@ async function collectFocusedTranscriptCueEvidence(options: {
611
616
  continue;
612
617
  }
613
618
  appendEvidenceItem(options.evidenceItems, options.seenTurns, {
614
- id: `${options.sessionId}:${message.turn_index}`,
615
- sessionId: options.sessionId,
619
+ ...lcmEvidenceIdentity(message, options.sessionId),
620
+ sessionId: message.session_id ?? options.sessionId,
616
621
  turnIndex: message.turn_index,
617
622
  role: message.role,
618
623
  content: message.content,
@@ -772,13 +777,7 @@ async function collectContentLabelReferenceEvidence(options: {
772
777
  sessionId?: string;
773
778
  query: string;
774
779
  references: ExplicitTurnReference[];
775
- evidenceItems: Array<{
776
- id: string;
777
- sessionId: string;
778
- turnIndex: number;
779
- role: string;
780
- content: string;
781
- }>;
780
+ evidenceItems: ExplicitCueEvidenceItem[];
782
781
  seenTurns: Set<string>;
783
782
  }): Promise<Set<number>> {
784
783
  const resolved = new Set<number>();
@@ -818,7 +817,7 @@ async function collectContentLabelReferenceEvidence(options: {
818
817
  }
819
818
  if (expanded.length === 0) {
820
819
  appendEvidenceItem(options.evidenceItems, options.seenTurns, {
821
- id: `${hit.session_id}:${hit.turn_index}`,
820
+ ...lcmEvidenceIdentity(hit, hit.session_id),
822
821
  sessionId: hit.session_id,
823
822
  turnIndex: hit.turn_index,
824
823
  role: hit.role,
@@ -845,6 +844,7 @@ async function searchReferenceContentLabels(
845
844
  sessionId?: string,
846
845
  ): Promise<
847
846
  Array<{
847
+ id?: number;
848
848
  turn_index: number;
849
849
  role: string;
850
850
  content: string;
@@ -855,6 +855,7 @@ async function searchReferenceContentLabels(
855
855
  const hits = new Map<
856
856
  string,
857
857
  {
858
+ id?: number;
858
859
  turn_index: number;
859
860
  role: string;
860
861
  content: string;
@@ -878,7 +879,8 @@ async function searchReferenceContentLabels(
878
879
  ) {
879
880
  continue;
880
881
  }
881
- hits.set(`${result.session_id}:${result.turn_index}:${labelKind}`, {
882
+ hits.set(`${lcmEvidenceIdentity(result, result.session_id).id}:${labelKind}`, {
883
+ ...(typeof result.id === "number" ? { id: result.id } : {}),
882
884
  turn_index: result.turn_index,
883
885
  role: result.role,
884
886
  content: result.content,
@@ -1054,14 +1056,7 @@ async function collectLexicalCueEvidence(options: {
1054
1056
  includeBenchmarkAnchorCues?: boolean;
1055
1057
  includeContentLexicalCues?: boolean;
1056
1058
  includeStructuredPlanCues?: boolean;
1057
- evidenceItems: Array<{
1058
- id: string;
1059
- sessionId: string;
1060
- turnIndex: number;
1061
- role: string;
1062
- content: string;
1063
- score?: number;
1064
- }>;
1059
+ evidenceItems: ExplicitCueEvidenceItem[];
1065
1060
  seenTurns: Set<string>;
1066
1061
  }): Promise<void> {
1067
1062
  const cues = prioritizeLexicalCueSearchCues(
@@ -1096,7 +1091,7 @@ async function collectLexicalCueEvidence(options: {
1096
1091
  );
1097
1092
  if (expanded.length === 0) {
1098
1093
  appendEvidenceItem(options.evidenceItems, options.seenTurns, {
1099
- id: `${result.session_id}:${result.turn_index}`,
1094
+ ...lcmEvidenceIdentity(result, result.session_id),
1100
1095
  sessionId: result.session_id,
1101
1096
  turnIndex: result.turn_index,
1102
1097
  role: result.role,
@@ -1107,7 +1102,7 @@ async function collectLexicalCueEvidence(options: {
1107
1102
  }
1108
1103
  if (exactHitFirst) {
1109
1104
  appendEvidenceItem(options.evidenceItems, options.seenTurns, {
1110
- id: `${result.session_id}:${result.turn_index}`,
1105
+ ...lcmEvidenceIdentity(result, result.session_id),
1111
1106
  sessionId: result.session_id,
1112
1107
  turnIndex: result.turn_index,
1113
1108
  role: result.role,
@@ -1131,6 +1126,8 @@ interface LabeledTrajectoryStep {
1131
1126
  observation?: string;
1132
1127
  actionTurnIndex?: number;
1133
1128
  observationTurnIndex?: number;
1129
+ actionArchiveRowId?: number;
1130
+ observationArchiveRowId?: number;
1134
1131
  }
1135
1132
 
1136
1133
  interface TrajectoryBounds {
@@ -1187,7 +1184,12 @@ function hasTrajectoryAnalysisIntent(query: string): boolean {
1187
1184
  }
1188
1185
 
1189
1186
  function parseLabeledTrajectory(
1190
- messages: Array<{ turn_index: number; role: string; content: string }>,
1187
+ messages: Array<{
1188
+ id?: number;
1189
+ turn_index: number;
1190
+ role: string;
1191
+ content: string;
1192
+ }>,
1191
1193
  ): LabeledTrajectoryStep[] {
1192
1194
  const byStep = new Map<number, LabeledTrajectoryStep>();
1193
1195
 
@@ -1197,6 +1199,8 @@ function parseLabeledTrajectory(
1197
1199
  const step = getOrCreateTrajectoryStep(byStep, action.step);
1198
1200
  step.action = action.value;
1199
1201
  step.actionTurnIndex = message.turn_index;
1202
+ const archiveRowId = lcmArchiveRowId(message);
1203
+ if (archiveRowId !== undefined) step.actionArchiveRowId = archiveRowId;
1200
1204
  continue;
1201
1205
  }
1202
1206
 
@@ -1205,6 +1209,10 @@ function parseLabeledTrajectory(
1205
1209
  const step = getOrCreateTrajectoryStep(byStep, observation.step);
1206
1210
  step.observation = observation.value;
1207
1211
  step.observationTurnIndex = message.turn_index;
1212
+ const archiveRowId = lcmArchiveRowId(message);
1213
+ if (archiveRowId !== undefined) {
1214
+ step.observationArchiveRowId = archiveRowId;
1215
+ }
1208
1216
  }
1209
1217
  }
1210
1218
 
@@ -4538,21 +4546,21 @@ function normalizeTrajectoryQuery(value: string): string {
4538
4546
  }
4539
4547
 
4540
4548
  function appendExpandedEvidence(
4541
- evidenceItems: Array<{
4542
- id: string;
4543
- sessionId: string;
4544
- turnIndex: number;
4549
+ evidenceItems: ExplicitCueEvidenceItem[],
4550
+ seenTurns: Set<string>,
4551
+ sessionId: string,
4552
+ expanded: Array<{
4553
+ id?: number;
4554
+ session_id?: string;
4555
+ turn_index: number;
4545
4556
  role: string;
4546
4557
  content: string;
4547
4558
  }>,
4548
- seenTurns: Set<string>,
4549
- sessionId: string,
4550
- expanded: Array<{ turn_index: number; role: string; content: string }>,
4551
4559
  ): void {
4552
4560
  for (const message of expanded) {
4553
4561
  appendEvidenceItem(evidenceItems, seenTurns, {
4554
- id: `${sessionId}:${message.turn_index}`,
4555
- sessionId,
4562
+ ...lcmEvidenceIdentity(message, sessionId),
4563
+ sessionId: message.session_id ?? sessionId,
4556
4564
  turnIndex: message.turn_index,
4557
4565
  role: message.role,
4558
4566
  content: message.content,
@@ -4689,14 +4697,7 @@ async function collectNamedMeetingFactEvidence(options: {
4689
4697
  sessionId?: string;
4690
4698
  query: string;
4691
4699
  maxReferences: number;
4692
- evidenceItems: Array<{
4693
- id: string;
4694
- sessionId: string;
4695
- turnIndex: number;
4696
- role: string;
4697
- content: string;
4698
- score?: number;
4699
- }>;
4700
+ evidenceItems: ExplicitCueEvidenceItem[];
4700
4701
  seenTurns: Set<string>;
4701
4702
  }): Promise<void> {
4702
4703
  const cues = collectMeetingFactCues(options.query)
@@ -4712,7 +4713,7 @@ async function collectNamedMeetingFactEvidence(options: {
4712
4713
  for (const result of results) {
4713
4714
  if (!isNamedMeetingFactEvidence(result.content, options.query)) continue;
4714
4715
  appendEvidenceItem(options.evidenceItems, options.seenTurns, {
4715
- id: `${result.session_id}:${result.turn_index}`,
4716
+ ...lcmEvidenceIdentity(result, result.session_id),
4716
4717
  sessionId: result.session_id,
4717
4718
  turnIndex: result.turn_index,
4718
4719
  role: result.role,
@@ -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
- id: `${result.session_id}:${result.turn_index}`,
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
- id: `${result.session_id}:${message.turn_index}`,
147
- sessionId: result.session_id,
148
- turnIndex: message.turn_index,
149
- role: message.role,
150
- content: message.content,
151
- ...(message.turn_index === result.turn_index &&
152
- typeof result.score === "number"
153
- ? { score: result.score }
154
- : {}),
155
- }));
156
- const hitIndex = candidates.findIndex((candidate) =>
157
- candidate.turnIndex === result.turn_index
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
- id: `${options.sessionId}:${message.turn_index}`,
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
+ });