@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
@@ -1,9 +1,16 @@
1
- import { buildEvidencePack } from "./evidence-pack.js";
1
+ import {
2
+ buildEvidencePack,
3
+ type EvidencePackSelectionReceipt,
4
+ } from "./evidence-pack.js";
2
5
  import {
3
6
  gatherAcrossReadSessions,
4
7
  resolveLcmReadSessionIds,
5
8
  } from "./lcm-fallback-read.js";
6
9
  import { unifiedDedupeAndRank } from "./recall-pipeline-stages.js";
10
+ import {
11
+ lcmArchiveRowId,
12
+ lcmEvidenceIdentity,
13
+ } from "./lcm/evidence-identity.js";
7
14
 
8
15
  export interface ExplicitCueRecallEngine {
9
16
  expandContext(
@@ -11,7 +18,15 @@ export interface ExplicitCueRecallEngine {
11
18
  fromTurn: number,
12
19
  toTurn: number,
13
20
  maxTokens: number,
14
- ): Promise<Array<{ turn_index: number; role: string; content: string }>>;
21
+ ): Promise<
22
+ Array<{
23
+ id?: number;
24
+ session_id?: string;
25
+ turn_index: number;
26
+ role: string;
27
+ content: string;
28
+ }>
29
+ >;
15
30
  getStats?(sessionId?: string): Promise<{
16
31
  totalMessages: number;
17
32
  maxTurnIndex?: number;
@@ -22,6 +37,7 @@ export interface ExplicitCueRecallEngine {
22
37
  sessionId?: string,
23
38
  ): Promise<
24
39
  Array<{
40
+ id?: number;
25
41
  turn_index: number;
26
42
  role: string;
27
43
  content: string;
@@ -48,6 +64,8 @@ export interface ExplicitCueRecallOptions {
48
64
  includeBenchmarkAnchorCues?: boolean;
49
65
  includeContentLexicalCues?: boolean;
50
66
  includeStructuredPlanCues?: boolean;
67
+ /** Observe selected evidence blocks without changing rendered output. */
68
+ onEvidenceSelected?: (receipt: EvidencePackSelectionReceipt) => void;
51
69
  }
52
70
 
53
71
  export interface TrajectoryAnalysisRecallOptions {
@@ -55,6 +73,18 @@ export interface TrajectoryAnalysisRecallOptions {
55
73
  sessionId?: string;
56
74
  query: string;
57
75
  maxChars: number;
76
+ /** Observe only emitted lines; unavailable lineage is never guessed. */
77
+ onLineSelected?: (receipt: TrajectoryAnalysisLineReceipt) => void;
78
+ }
79
+
80
+ export interface TrajectoryAnalysisLineReceipt {
81
+ /** Half-open offsets within the returned trajectory section. */
82
+ lineStart: number;
83
+ lineEnd: number;
84
+ /** Whether the source row set is structurally exact or unavailable. */
85
+ lineageStatus: "exact" | "unavailable";
86
+ actionArchiveRowIds: number[];
87
+ observationArchiveRowIds: number[];
58
88
  }
59
89
 
60
90
  export type ExplicitTurnReference = {
@@ -62,6 +92,16 @@ export type ExplicitTurnReference = {
62
92
  includeDirectTurn: boolean;
63
93
  };
64
94
 
95
+ interface ExplicitCueEvidenceItem {
96
+ id: string;
97
+ archiveRowId?: number;
98
+ sessionId: string;
99
+ turnIndex: number;
100
+ role: string;
101
+ content: string;
102
+ score?: number;
103
+ }
104
+
65
105
  const DEFAULT_MAX_CHARS = 2_400;
66
106
  const DEFAULT_MAX_ITEM_CHARS = 1_200;
67
107
  const DEFAULT_MAX_REFERENCES = 24;
@@ -326,23 +366,17 @@ export async function buildExplicitCueRecallSection(
326
366
  return "";
327
367
  }
328
368
 
329
- const evidenceItems: Array<{
330
- id: string;
331
- sessionId: string;
332
- turnIndex: number;
333
- role: string;
334
- content: string;
335
- score?: number;
336
- }> = [];
369
+ const evidenceItems: ExplicitCueEvidenceItem[] = [];
337
370
  const seenTurns = new Set<string>();
338
371
 
339
372
  // #1505 codex P2: gather cue evidence across the ordered LCM read key set
340
373
  // (primary overlay → project/root fallbacks) into ONE shared accumulator
341
374
  // (`evidenceItems` / `seenTurns`), so a branch-scoped session's project/root
342
375
  // 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
376
+ // first-non-empty short-circuit. `seenTurns` is the historical local name;
377
+ // its keys use archive-row identity when available and fall back to
378
+ // `session_id`+`turn_index` for legacy engines. The budget is applied exactly
379
+ // once in the `buildEvidencePack` call below. `gatherAcrossReadSessions` isolates a
346
380
  // per-key read failure (a corrupt/locked fallback index must not discard the
347
381
  // other keys' cues); single-key recall runs each collector directly, so a
348
382
  // failure propagates exactly as before.
@@ -438,6 +472,9 @@ export async function buildExplicitCueRecallSection(
438
472
  options.maxItemChars,
439
473
  DEFAULT_MAX_ITEM_CHARS,
440
474
  ),
475
+ ...(options.onEvidenceSelected
476
+ ? { onSelection: options.onEvidenceSelected }
477
+ : {}),
441
478
  });
442
479
  }
443
480
 
@@ -502,7 +539,32 @@ export async function buildTrajectoryAnalysisRecallSection(
502
539
  }
503
540
 
504
541
  const clipped = truncateTrajectoryAnalysisLines(lines, bodyBudget);
505
- return clipped.length === 0 ? "" : `${header}\n${clipped.join("\n")}`;
542
+ if (clipped.length === 0) return "";
543
+ const text = `${header}\n${clipped.map((line) => line.text).join("\n")}`;
544
+ if (options.onLineSelected) {
545
+ let lineStart = header.length + 1;
546
+ for (const line of clipped) {
547
+ const lineEnd = lineStart + line.text.length;
548
+ options.onLineSelected({
549
+ lineStart,
550
+ lineEnd,
551
+ lineageStatus: line.lineageStatus,
552
+ actionArchiveRowIds: [...line.actionArchiveRowIds],
553
+ observationArchiveRowIds: [...line.observationArchiveRowIds],
554
+ });
555
+ lineStart = lineEnd + 1;
556
+ }
557
+ }
558
+ return text;
559
+ }
560
+
561
+ function uniqueSortedArchiveRowIds(
562
+ values: readonly (number | undefined)[],
563
+ ): number[] {
564
+ return [...new Set(values.filter(
565
+ (value): value is number =>
566
+ typeof value === "number" && Number.isSafeInteger(value) && value > 0,
567
+ ))].sort((left, right) => left - right);
506
568
  }
507
569
 
508
570
  async function collectTurnReferenceEvidence(options: {
@@ -510,13 +572,7 @@ async function collectTurnReferenceEvidence(options: {
510
572
  sessionId?: string;
511
573
  query: string;
512
574
  maxReferences: number;
513
- evidenceItems: Array<{
514
- id: string;
515
- sessionId: string;
516
- turnIndex: number;
517
- role: string;
518
- content: string;
519
- }>;
575
+ evidenceItems: ExplicitCueEvidenceItem[];
520
576
  seenTurns: Set<string>;
521
577
  }): Promise<void> {
522
578
  if (!options.sessionId) {
@@ -575,13 +631,7 @@ async function collectFocusedTranscriptCueEvidence(options: {
575
631
  engine: ExplicitCueRecallEngine;
576
632
  sessionId?: string;
577
633
  query: string;
578
- evidenceItems: Array<{
579
- id: string;
580
- sessionId: string;
581
- turnIndex: number;
582
- role: string;
583
- content: string;
584
- }>;
634
+ evidenceItems: ExplicitCueEvidenceItem[];
585
635
  seenTurns: Set<string>;
586
636
  }): Promise<void> {
587
637
  if (!options.sessionId || !options.engine.getStats) {
@@ -611,8 +661,8 @@ async function collectFocusedTranscriptCueEvidence(options: {
611
661
  continue;
612
662
  }
613
663
  appendEvidenceItem(options.evidenceItems, options.seenTurns, {
614
- id: `${options.sessionId}:${message.turn_index}`,
615
- sessionId: options.sessionId,
664
+ ...lcmEvidenceIdentity(message, options.sessionId),
665
+ sessionId: message.session_id ?? options.sessionId,
616
666
  turnIndex: message.turn_index,
617
667
  role: message.role,
618
668
  content: message.content,
@@ -772,13 +822,7 @@ async function collectContentLabelReferenceEvidence(options: {
772
822
  sessionId?: string;
773
823
  query: string;
774
824
  references: ExplicitTurnReference[];
775
- evidenceItems: Array<{
776
- id: string;
777
- sessionId: string;
778
- turnIndex: number;
779
- role: string;
780
- content: string;
781
- }>;
825
+ evidenceItems: ExplicitCueEvidenceItem[];
782
826
  seenTurns: Set<string>;
783
827
  }): Promise<Set<number>> {
784
828
  const resolved = new Set<number>();
@@ -818,7 +862,7 @@ async function collectContentLabelReferenceEvidence(options: {
818
862
  }
819
863
  if (expanded.length === 0) {
820
864
  appendEvidenceItem(options.evidenceItems, options.seenTurns, {
821
- id: `${hit.session_id}:${hit.turn_index}`,
865
+ ...lcmEvidenceIdentity(hit, hit.session_id),
822
866
  sessionId: hit.session_id,
823
867
  turnIndex: hit.turn_index,
824
868
  role: hit.role,
@@ -845,6 +889,7 @@ async function searchReferenceContentLabels(
845
889
  sessionId?: string,
846
890
  ): Promise<
847
891
  Array<{
892
+ id?: number;
848
893
  turn_index: number;
849
894
  role: string;
850
895
  content: string;
@@ -855,6 +900,7 @@ async function searchReferenceContentLabels(
855
900
  const hits = new Map<
856
901
  string,
857
902
  {
903
+ id?: number;
858
904
  turn_index: number;
859
905
  role: string;
860
906
  content: string;
@@ -878,7 +924,8 @@ async function searchReferenceContentLabels(
878
924
  ) {
879
925
  continue;
880
926
  }
881
- hits.set(`${result.session_id}:${result.turn_index}:${labelKind}`, {
927
+ hits.set(`${lcmEvidenceIdentity(result, result.session_id).id}:${labelKind}`, {
928
+ ...(typeof result.id === "number" ? { id: result.id } : {}),
882
929
  turn_index: result.turn_index,
883
930
  role: result.role,
884
931
  content: result.content,
@@ -1054,14 +1101,7 @@ async function collectLexicalCueEvidence(options: {
1054
1101
  includeBenchmarkAnchorCues?: boolean;
1055
1102
  includeContentLexicalCues?: boolean;
1056
1103
  includeStructuredPlanCues?: boolean;
1057
- evidenceItems: Array<{
1058
- id: string;
1059
- sessionId: string;
1060
- turnIndex: number;
1061
- role: string;
1062
- content: string;
1063
- score?: number;
1064
- }>;
1104
+ evidenceItems: ExplicitCueEvidenceItem[];
1065
1105
  seenTurns: Set<string>;
1066
1106
  }): Promise<void> {
1067
1107
  const cues = prioritizeLexicalCueSearchCues(
@@ -1096,7 +1136,7 @@ async function collectLexicalCueEvidence(options: {
1096
1136
  );
1097
1137
  if (expanded.length === 0) {
1098
1138
  appendEvidenceItem(options.evidenceItems, options.seenTurns, {
1099
- id: `${result.session_id}:${result.turn_index}`,
1139
+ ...lcmEvidenceIdentity(result, result.session_id),
1100
1140
  sessionId: result.session_id,
1101
1141
  turnIndex: result.turn_index,
1102
1142
  role: result.role,
@@ -1107,7 +1147,7 @@ async function collectLexicalCueEvidence(options: {
1107
1147
  }
1108
1148
  if (exactHitFirst) {
1109
1149
  appendEvidenceItem(options.evidenceItems, options.seenTurns, {
1110
- id: `${result.session_id}:${result.turn_index}`,
1150
+ ...lcmEvidenceIdentity(result, result.session_id),
1111
1151
  sessionId: result.session_id,
1112
1152
  turnIndex: result.turn_index,
1113
1153
  role: result.role,
@@ -1131,6 +1171,67 @@ interface LabeledTrajectoryStep {
1131
1171
  observation?: string;
1132
1172
  actionTurnIndex?: number;
1133
1173
  observationTurnIndex?: number;
1174
+ actionArchiveRowId?: number;
1175
+ observationArchiveRowId?: number;
1176
+ }
1177
+
1178
+ interface TrajectoryLineSource {
1179
+ lineageStatus: "exact" | "unavailable";
1180
+ actionArchiveRowIds: number[];
1181
+ observationArchiveRowIds: number[];
1182
+ }
1183
+
1184
+ interface SelectedTrajectoryLine extends TrajectoryLineSource {
1185
+ text: string;
1186
+ }
1187
+
1188
+ class TrajectoryAnalysisLines extends Array<string> {
1189
+ private readonly sources = new Map<number, TrajectoryLineSource>();
1190
+
1191
+ pushWithSources(
1192
+ text: string,
1193
+ actionSteps: readonly LabeledTrajectoryStep[] = [],
1194
+ observationSteps: readonly LabeledTrajectoryStep[] = [],
1195
+ ): number {
1196
+ const index = this.length;
1197
+ const length = super.push(text);
1198
+ const hasUnavailableSource =
1199
+ actionSteps.some((step) =>
1200
+ lcmArchiveRowId({ id: step.actionArchiveRowId }) === undefined
1201
+ ) ||
1202
+ observationSteps.some((step) =>
1203
+ lcmArchiveRowId({ id: step.observationArchiveRowId }) === undefined
1204
+ );
1205
+ this.sources.set(index, {
1206
+ lineageStatus: hasUnavailableSource ? "unavailable" : "exact",
1207
+ actionArchiveRowIds: hasUnavailableSource
1208
+ ? []
1209
+ : uniqueSortedArchiveRowIds(
1210
+ actionSteps.map((step) => step.actionArchiveRowId),
1211
+ ),
1212
+ observationArchiveRowIds: hasUnavailableSource
1213
+ ? []
1214
+ : uniqueSortedArchiveRowIds(
1215
+ observationSteps.map((step) => step.observationArchiveRowId),
1216
+ ),
1217
+ });
1218
+ return length;
1219
+ }
1220
+
1221
+ sourceAt(index: number): TrajectoryLineSource {
1222
+ const source = this.sources.get(index);
1223
+ return source
1224
+ ? {
1225
+ lineageStatus: source.lineageStatus,
1226
+ actionArchiveRowIds: [...source.actionArchiveRowIds],
1227
+ observationArchiveRowIds: [...source.observationArchiveRowIds],
1228
+ }
1229
+ : {
1230
+ lineageStatus: "unavailable",
1231
+ actionArchiveRowIds: [],
1232
+ observationArchiveRowIds: [],
1233
+ };
1234
+ }
1134
1235
  }
1135
1236
 
1136
1237
  interface TrajectoryBounds {
@@ -1187,7 +1288,12 @@ function hasTrajectoryAnalysisIntent(query: string): boolean {
1187
1288
  }
1188
1289
 
1189
1290
  function parseLabeledTrajectory(
1190
- messages: Array<{ turn_index: number; role: string; content: string }>,
1291
+ messages: Array<{
1292
+ id?: number;
1293
+ turn_index: number;
1294
+ role: string;
1295
+ content: string;
1296
+ }>,
1191
1297
  ): LabeledTrajectoryStep[] {
1192
1298
  const byStep = new Map<number, LabeledTrajectoryStep>();
1193
1299
 
@@ -1197,6 +1303,8 @@ function parseLabeledTrajectory(
1197
1303
  const step = getOrCreateTrajectoryStep(byStep, action.step);
1198
1304
  step.action = action.value;
1199
1305
  step.actionTurnIndex = message.turn_index;
1306
+ const archiveRowId = lcmArchiveRowId(message);
1307
+ if (archiveRowId !== undefined) step.actionArchiveRowId = archiveRowId;
1200
1308
  continue;
1201
1309
  }
1202
1310
 
@@ -1205,6 +1313,10 @@ function parseLabeledTrajectory(
1205
1313
  const step = getOrCreateTrajectoryStep(byStep, observation.step);
1206
1314
  step.observation = observation.value;
1207
1315
  step.observationTurnIndex = message.turn_index;
1316
+ const archiveRowId = lcmArchiveRowId(message);
1317
+ if (archiveRowId !== undefined) {
1318
+ step.observationArchiveRowId = archiveRowId;
1319
+ }
1208
1320
  }
1209
1321
  }
1210
1322
 
@@ -1307,12 +1419,13 @@ function buildTrajectoryAnalysisLines(
1307
1419
  query: string,
1308
1420
  trajectory: readonly LabeledTrajectoryStep[],
1309
1421
  bounds: TrajectoryBounds,
1310
- ): string[] {
1422
+ ): TrajectoryAnalysisLines {
1311
1423
  const normalized = normalizeTrajectoryQuery(query);
1312
1424
  const explicitReferences = collectExplicitTurnReferences(query);
1313
- const lines: string[] = [
1425
+ const lines = new TrajectoryAnalysisLines();
1426
+ lines.pushWithSources(
1314
1427
  `Analyzed labeled action/observation transcript window: steps ${bounds.start}-${bounds.end} (${bounds.reason}).`,
1315
- ];
1428
+ );
1316
1429
  const hasQuotedObservationPair = extractQuotedObservations(query).length >= 2;
1317
1430
  const transition = findObservationTransition(query, trajectory);
1318
1431
  const entities = extractNumberedEntities(query);
@@ -1332,8 +1445,14 @@ function buildTrajectoryAnalysisLines(
1332
1445
  /\bwhat\s+will\s+be\s+the\s+resulting\b/.test(normalized);
1333
1446
 
1334
1447
  if (transition) {
1335
- lines.push(
1448
+ const fromStep = trajectory.find((step) => step.step === transition.fromStep);
1449
+ const toStep = trajectory.find((step) => step.step === transition.toStep);
1450
+ lines.pushWithSources(
1336
1451
  `Matched quoted observations: Observation ${transition.fromStep} -> Observation ${transition.toStep}.`,
1452
+ [],
1453
+ [fromStep, toStep].filter(
1454
+ (step): step is LabeledTrajectoryStep => step !== undefined,
1455
+ ),
1337
1456
  );
1338
1457
  appendActionSequenceSummary(
1339
1458
  lines,
@@ -1416,11 +1535,11 @@ function buildTrajectoryAnalysisLines(
1416
1535
  }
1417
1536
  }
1418
1537
 
1419
- return lines.length === 1 ? [] : lines;
1538
+ return lines.length === 1 ? new TrajectoryAnalysisLines() : lines;
1420
1539
  }
1421
1540
 
1422
1541
  function appendReferencedTrajectoryLines(
1423
- lines: string[],
1542
+ lines: TrajectoryAnalysisLines,
1424
1543
  trajectory: readonly LabeledTrajectoryStep[],
1425
1544
  references: readonly ExplicitTurnReference[],
1426
1545
  options: { includeObservations: boolean; heading: string },
@@ -1434,19 +1553,23 @@ function appendReferencedTrajectoryLines(
1434
1553
  return;
1435
1554
  }
1436
1555
 
1437
- lines.push(options.heading);
1556
+ lines.pushWithSources(options.heading);
1438
1557
  for (const step of window) {
1439
1558
  if (step.action) {
1440
- lines.push(`[Action ${step.step}]: ${step.action}`);
1559
+ lines.pushWithSources(`[Action ${step.step}]: ${step.action}`, [step]);
1441
1560
  }
1442
1561
  if (options.includeObservations && step.observation) {
1443
- lines.push(`[Observation ${step.step}]: ${oneLine(step.observation)}`);
1562
+ lines.pushWithSources(
1563
+ `[Observation ${step.step}]: ${oneLine(step.observation)}`,
1564
+ [],
1565
+ [step],
1566
+ );
1444
1567
  }
1445
1568
  }
1446
1569
  }
1447
1570
 
1448
1571
  function appendActionRangeLines(
1449
- lines: string[],
1572
+ lines: TrajectoryAnalysisLines,
1450
1573
  trajectory: readonly LabeledTrajectoryStep[],
1451
1574
  start: number,
1452
1575
  end: number,
@@ -1462,19 +1585,23 @@ function appendActionRangeLines(
1462
1585
  if (window.length === 0) {
1463
1586
  return;
1464
1587
  }
1465
- lines.push(options.heading);
1588
+ lines.pushWithSources(options.heading);
1466
1589
  for (const step of window) {
1467
1590
  if (step.action) {
1468
- lines.push(`[Action ${step.step}]: ${step.action}`);
1591
+ lines.pushWithSources(`[Action ${step.step}]: ${step.action}`, [step]);
1469
1592
  }
1470
1593
  if (options.includeObservations && step.observation) {
1471
- lines.push(`[Observation ${step.step}]: ${oneLine(step.observation)}`);
1594
+ lines.pushWithSources(
1595
+ `[Observation ${step.step}]: ${oneLine(step.observation)}`,
1596
+ [],
1597
+ [step],
1598
+ );
1472
1599
  }
1473
1600
  }
1474
1601
  }
1475
1602
 
1476
1603
  function appendActionSequenceSummary(
1477
- lines: string[],
1604
+ lines: TrajectoryAnalysisLines,
1478
1605
  trajectory: readonly LabeledTrajectoryStep[],
1479
1606
  start: number,
1480
1607
  end: number,
@@ -1482,17 +1609,18 @@ function appendActionSequenceSummary(
1482
1609
  ): void {
1483
1610
  const low = Math.min(start, end);
1484
1611
  const high = Math.max(start, end);
1485
- const actions = trajectory
1486
- .filter((step) => step.step >= low && step.step <= high && step.action)
1612
+ const actionSteps = trajectory
1613
+ .filter((step) => step.step >= low && step.step <= high && step.action);
1614
+ const actions = actionSteps
1487
1615
  .map((step) => `step ${step.step}: ${step.action}`);
1488
1616
  if (actions.length === 0) {
1489
1617
  return;
1490
1618
  }
1491
- lines.push(`${heading} ${actions.join("; ")}.`);
1619
+ lines.pushWithSources(`${heading} ${actions.join("; ")}.`, actionSteps);
1492
1620
  }
1493
1621
 
1494
1622
  function appendResultingObservationLine(
1495
- lines: string[],
1623
+ lines: TrajectoryAnalysisLines,
1496
1624
  trajectory: readonly LabeledTrajectoryStep[],
1497
1625
  stepNumber: number,
1498
1626
  ): void {
@@ -1500,11 +1628,15 @@ function appendResultingObservationLine(
1500
1628
  if (!step?.observation) {
1501
1629
  return;
1502
1630
  }
1503
- lines.push(`Resulting observation after Action ${step.step}: ${oneLine(step.observation)}`);
1631
+ lines.pushWithSources(
1632
+ `Resulting observation after Action ${step.step}: ${oneLine(step.observation)}`,
1633
+ [],
1634
+ [step],
1635
+ );
1504
1636
  }
1505
1637
 
1506
1638
  function appendSpatialTrajectoryInferenceLines(
1507
- lines: string[],
1639
+ lines: TrajectoryAnalysisLines,
1508
1640
  query: string,
1509
1641
  trajectory: readonly LabeledTrajectoryStep[],
1510
1642
  bounds: TrajectoryBounds,
@@ -1528,23 +1660,23 @@ function appendSpatialTrajectoryInferenceLines(
1528
1660
  const window = spatialTrajectoryWindowForQuery(query, trajectory, bounds);
1529
1661
  const counterfactualContactLines = collectCounterfactualContactLines(query, trajectory, normalized);
1530
1662
  if (counterfactualContactLines.length > 0) {
1531
- lines.push("Counterfactual contact cues:");
1663
+ lines.pushWithSources("Counterfactual contact cues:");
1532
1664
  lines.push(...counterfactualContactLines);
1533
1665
  }
1534
1666
 
1535
1667
  const selfReversingLines = collectSelfReversingProgressLines(query, window, normalized);
1536
1668
  if (selfReversingLines.length > 0) {
1537
- lines.push("Self-reversing sequence cues:");
1669
+ lines.pushWithSources("Self-reversing sequence cues:");
1538
1670
  lines.push(...selfReversingLines);
1539
1671
  }
1540
1672
  const onlyEffectiveActionLines = collectOnlyEffectiveActionLines(window, normalized);
1541
1673
  if (onlyEffectiveActionLines.length > 0) {
1542
- lines.push("Only-effective action cues:");
1674
+ lines.pushWithSources("Only-effective action cues:");
1543
1675
  lines.push(...onlyEffectiveActionLines);
1544
1676
  }
1545
1677
  const pushedPhraseGroupLines = collectPushedPhraseGroupShiftLines(query, window, normalized);
1546
1678
  if (pushedPhraseGroupLines.length > 0) {
1547
- lines.push("Pushed phrase-group shift cues:");
1679
+ lines.pushWithSources("Pushed phrase-group shift cues:");
1548
1680
  lines.push(...pushedPhraseGroupLines);
1549
1681
  }
1550
1682
  const suppressMovementProgressEvidence = onlyEffectiveActionLines.length > 0 ||
@@ -1558,7 +1690,7 @@ function appendSpatialTrajectoryInferenceLines(
1558
1690
  ? []
1559
1691
  : collectActionMovementSummaryLines(query, window, bounds);
1560
1692
  if (actionMovementLines.length > 0) {
1561
- lines.push("Action movement summary cues:");
1693
+ lines.pushWithSources("Action movement summary cues:");
1562
1694
  lines.push(...actionMovementLines);
1563
1695
  }
1564
1696
 
@@ -1566,7 +1698,7 @@ function appendSpatialTrajectoryInferenceLines(
1566
1698
  ? []
1567
1699
  : collectMovementDeltaLines(query, window);
1568
1700
  if (movementLines.length > 0) {
1569
- lines.push("Relative-position movement cues:");
1701
+ lines.pushWithSources("Relative-position movement cues:");
1570
1702
  lines.push(...movementLines);
1571
1703
  }
1572
1704
 
@@ -1577,37 +1709,37 @@ function appendSpatialTrajectoryInferenceLines(
1577
1709
  normalized,
1578
1710
  );
1579
1711
  if (objectAlignmentLines.length > 0) {
1580
- lines.push("Object alignment cues:");
1712
+ lines.pushWithSources("Object alignment cues:");
1581
1713
  lines.push(...objectAlignmentLines);
1582
1714
  }
1583
1715
 
1584
1716
  const alternativeActionLines = collectAlternativeActionLines(query, trajectory);
1585
1717
  if (alternativeActionLines.length > 0) {
1586
- lines.push("Counterfactual action cues:");
1718
+ lines.pushWithSources("Counterfactual action cues:");
1587
1719
  lines.push(...alternativeActionLines);
1588
1720
  }
1589
1721
 
1590
1722
  const adjacentRuleSetupLines = collectAdjacentRuleSetupLines(window, normalized);
1591
1723
  if (adjacentRuleSetupLines.length > 0) {
1592
- lines.push("Adjacent rule-block setup cues:");
1724
+ lines.pushWithSources("Adjacent rule-block setup cues:");
1593
1725
  lines.push(...adjacentRuleSetupLines);
1594
1726
  }
1595
1727
 
1596
1728
  const blockedMoveLines = collectBlockedMoveLines(query, window, normalized);
1597
1729
  if (blockedMoveLines.length > 0) {
1598
- lines.push("Blocked-move cues:");
1730
+ lines.pushWithSources("Blocked-move cues:");
1599
1731
  lines.push(...blockedMoveLines);
1600
1732
  }
1601
1733
 
1602
1734
  const failedEscapeLines = collectFailedEscapeLines(query, trajectory, normalized);
1603
1735
  if (failedEscapeLines.length > 0) {
1604
- lines.push("Failed-move escape cues:");
1736
+ lines.pushWithSources("Failed-move escape cues:");
1605
1737
  lines.push(...failedEscapeLines);
1606
1738
  }
1607
1739
 
1608
1740
  const sameRelativeTextPushLines = collectSameRelativeTextPushLines(window, normalized);
1609
1741
  if (sameRelativeTextPushLines.length > 0) {
1610
- lines.push("Same-relative text-push cues:");
1742
+ lines.pushWithSources("Same-relative text-push cues:");
1611
1743
  lines.push(...sameRelativeTextPushLines);
1612
1744
  }
1613
1745
 
@@ -1615,43 +1747,43 @@ function appendSpatialTrajectoryInferenceLines(
1615
1747
  ? []
1616
1748
  : collectFailedContactBoundaryLines(window, normalized);
1617
1749
  if (failedContactLines.length > 0) {
1618
- lines.push("Failed-push boundary cues:");
1750
+ lines.pushWithSources("Failed-push boundary cues:");
1619
1751
  lines.push(...failedContactLines);
1620
1752
  }
1621
1753
 
1622
1754
  const transformationLines = collectTemporaryRuleTransformationLines(window, normalized);
1623
1755
  if (transformationLines.length > 0) {
1624
- lines.push("Temporary transformation cues:");
1756
+ lines.pushWithSources("Temporary transformation cues:");
1625
1757
  lines.push(...transformationLines);
1626
1758
  }
1627
1759
 
1628
1760
  const wholeConfigurationShiftLines = collectWholeConfigurationShiftLines(window, normalized);
1629
1761
  if (wholeConfigurationShiftLines.length > 0) {
1630
- lines.push("Whole-configuration shift cues:");
1762
+ lines.pushWithSources("Whole-configuration shift cues:");
1631
1763
  lines.push(...wholeConfigurationShiftLines);
1632
1764
  }
1633
1765
 
1634
1766
  const ruleInterventionLines = collectRuleInterventionStrategyLines(window, normalized);
1635
1767
  if (ruleInterventionLines.length > 0) {
1636
- lines.push("Rule-intervention strategy cues:");
1768
+ lines.pushWithSources("Rule-intervention strategy cues:");
1637
1769
  lines.push(...ruleInterventionLines);
1638
1770
  }
1639
1771
 
1640
1772
  const missingPushTargetLines = collectMissingPushTargetLines(window, normalized);
1641
1773
  if (missingPushTargetLines.length > 0) {
1642
- lines.push("Missing-interaction cues:");
1774
+ lines.pushWithSources("Missing-interaction cues:");
1643
1775
  lines.push(...missingPushTargetLines);
1644
1776
  }
1645
1777
 
1646
1778
  const rulePhraseAlignmentLines = collectRulePhraseAlignmentLines(window, normalized);
1647
1779
  if (rulePhraseAlignmentLines.length > 0) {
1648
- lines.push("Rule-phrase alignment cues:");
1780
+ lines.pushWithSources("Rule-phrase alignment cues:");
1649
1781
  lines.push(...rulePhraseAlignmentLines);
1650
1782
  }
1651
1783
 
1652
1784
  const controlRuleInteractionLines = collectControlRuleInteractionLines(window, normalized);
1653
1785
  if (controlRuleInteractionLines.length > 0) {
1654
- lines.push("Control-rule interaction cues:");
1786
+ lines.pushWithSources("Control-rule interaction cues:");
1655
1787
  lines.push(...controlRuleInteractionLines);
1656
1788
  }
1657
1789
 
@@ -1659,13 +1791,13 @@ function appendSpatialTrajectoryInferenceLines(
1659
1791
  ? []
1660
1792
  : collectRuleTextPositionLines(window, normalized);
1661
1793
  if (ruleTextPositionLines.length > 0) {
1662
- lines.push("Rule-text positioning cues:");
1794
+ lines.pushWithSources("Rule-text positioning cues:");
1663
1795
  lines.push(...ruleTextPositionLines);
1664
1796
  }
1665
1797
 
1666
1798
  const ruleLines = collectRuleStateLines(window, normalized);
1667
1799
  if (ruleLines.length > 0) {
1668
- lines.push("Rule-state cues:");
1800
+ lines.pushWithSources("Rule-state cues:");
1669
1801
  lines.push(...ruleLines);
1670
1802
  }
1671
1803
  }
@@ -3458,12 +3590,15 @@ function uniqueLines(lines: readonly string[]): string[] {
3458
3590
  }
3459
3591
 
3460
3592
  function appendActionFrequencyLines(
3461
- lines: string[],
3593
+ lines: TrajectoryAnalysisLines,
3462
3594
  trajectory: readonly LabeledTrajectoryStep[],
3463
3595
  bounds: TrajectoryBounds,
3464
3596
  ): void {
3597
+ const actionSteps = boundedTrajectory(trajectory, bounds).filter(
3598
+ (step) => step.action !== undefined,
3599
+ );
3465
3600
  const counts = new Map<string, number>();
3466
- for (const step of boundedTrajectory(trajectory, bounds)) {
3601
+ for (const step of actionSteps) {
3467
3602
  if (!step.action) continue;
3468
3603
  const verb = normalizeActionVerb(step.action);
3469
3604
  if (!verb) continue;
@@ -3475,15 +3610,16 @@ function appendActionFrequencyLines(
3475
3610
  const frequencies = [...counts.entries()].sort((left, right) =>
3476
3611
  right[1] - left[1] || left[0].localeCompare(right[0]),
3477
3612
  );
3478
- lines.push(
3613
+ lines.pushWithSources(
3479
3614
  `Action frequency through requested window: ${frequencies
3480
3615
  .map(([verb, count]) => `${verb}=${count}`)
3481
3616
  .join(", ")}.`,
3617
+ actionSteps,
3482
3618
  );
3483
3619
  }
3484
3620
 
3485
3621
  function appendInventoryChangeLines(
3486
- lines: string[],
3622
+ lines: TrajectoryAnalysisLines,
3487
3623
  trajectory: readonly LabeledTrajectoryStep[],
3488
3624
  bounds: TrajectoryBounds,
3489
3625
  ): void {
@@ -3499,16 +3635,24 @@ function appendInventoryChangeLines(
3499
3635
  if (changes.length === 0) {
3500
3636
  return;
3501
3637
  }
3502
- lines.push("Inventory changes from action transcript:");
3638
+ const changeSteps = changes
3639
+ .map((change) => trajectory.find((step) => step.step === change.step))
3640
+ .filter((step): step is LabeledTrajectoryStep => step !== undefined);
3641
+ lines.pushWithSources("Inventory changes from action transcript:");
3503
3642
  if (changes.length > 5) {
3504
- lines.push(
3643
+ lines.pushWithSources(
3505
3644
  `First five inventory changes: ${formatInventoryChangeSummary(changes.slice(0, 5))}.`,
3645
+ changeSteps.slice(0, 5),
3506
3646
  );
3507
- lines.push(
3647
+ lines.pushWithSources(
3508
3648
  `Complete inventory changes: ${formatInventoryChangeSummary(changes)}.`,
3649
+ changeSteps,
3509
3650
  );
3510
3651
  }
3511
3652
  for (const change of changes) {
3653
+ // The rendered change is stateful: whether a place/move removes an item
3654
+ // depends on earlier take actions in `held`. Until the parser returns that
3655
+ // full dependency chain, do not publish a misleading partial row receipt.
3512
3656
  lines.push(`[Action ${change.step}]: ${change.action} => ${change.change}`);
3513
3657
  }
3514
3658
  }
@@ -3535,7 +3679,7 @@ function summarizeInventoryChange(change: string): string {
3535
3679
  }
3536
3680
 
3537
3681
  function appendContainerStateChangeLines(
3538
- lines: string[],
3682
+ lines: TrajectoryAnalysisLines,
3539
3683
  trajectory: readonly LabeledTrajectoryStep[],
3540
3684
  bounds: TrajectoryBounds,
3541
3685
  ): void {
@@ -3543,14 +3687,18 @@ function appendContainerStateChangeLines(
3543
3687
  if (changes.length === 0) {
3544
3688
  return;
3545
3689
  }
3546
- lines.push("Container open/close state changes:");
3690
+ lines.pushWithSources("Container open/close state changes:");
3547
3691
  for (const change of changes) {
3548
- lines.push(`[Action ${change.step}]: ${change.action} => ${change.entity} ${change.state}`);
3692
+ const step = trajectory.find((candidate) => candidate.step === change.step);
3693
+ lines.pushWithSources(
3694
+ `[Action ${change.step}]: ${change.action} => ${change.entity} ${change.state}`,
3695
+ step ? [step] : [],
3696
+ );
3549
3697
  }
3550
3698
  }
3551
3699
 
3552
3700
  function appendEntityTimelineLines(
3553
- lines: string[],
3701
+ lines: TrajectoryAnalysisLines,
3554
3702
  trajectory: readonly LabeledTrajectoryStep[],
3555
3703
  bounds: TrajectoryBounds,
3556
3704
  entities: readonly string[],
@@ -3586,28 +3734,37 @@ function appendEntityTimelineLines(
3586
3734
  continue;
3587
3735
  }
3588
3736
 
3589
- lines.push(`Timeline for ${entity}:`);
3737
+ lines.pushWithSources(`Timeline for ${entity}:`);
3590
3738
  for (const step of directActions) {
3591
- lines.push(`[Action ${step.step}]: ${step.action}`);
3739
+ lines.pushWithSources(`[Action ${step.step}]: ${step.action}`, [step]);
3592
3740
  }
3593
3741
  if (objectTransfers.length > 0) {
3594
- lines.push(`Object transfers involving ${entity}:`);
3742
+ lines.pushWithSources(`Object transfers involving ${entity}:`);
3595
3743
  for (const transfer of objectTransfers) {
3596
- lines.push(`[Action ${transfer.step}]: ${transfer.action} => ${formatContainerTransfer(transfer)}`);
3744
+ const step = trajectory.find((candidate) => candidate.step === transfer.step);
3745
+ lines.pushWithSources(
3746
+ `[Action ${transfer.step}]: ${transfer.action} => ${formatContainerTransfer(transfer)}`,
3747
+ step ? [step] : [],
3748
+ );
3597
3749
  }
3598
3750
  }
3599
3751
  const inferredLocation = inferEntityLocation(trajectory, bounds, entity);
3600
3752
  if (inferredLocation) {
3601
- lines.push(
3602
- `Inferred ${entity} location at step ${bounds.end}: ${inferredLocation}.`,
3753
+ lines.pushWithSources(
3754
+ `Inferred ${entity} location at step ${bounds.end}: ${inferredLocation.location}.`,
3755
+ [inferredLocation.sourceStep],
3603
3756
  );
3604
3757
  }
3605
3758
  if (stateChanges.length > 0) {
3606
3759
  const latest = stateChanges[stateChanges.length - 1]!;
3607
- lines.push(
3760
+ const stateSteps = stateChanges
3761
+ .map((change) => trajectory.find((step) => step.step === change.step))
3762
+ .filter((step): step is LabeledTrajectoryStep => step !== undefined);
3763
+ lines.pushWithSources(
3608
3764
  `Latest ${entity} state at step ${bounds.end}: ${latest.state}; state changes: ${stateChanges
3609
3765
  .map((change) => `${change.step}:${change.state}`)
3610
3766
  .join(", ")}.`,
3767
+ stateSteps,
3611
3768
  );
3612
3769
  }
3613
3770
  }
@@ -3958,15 +4115,17 @@ function inferEntityLocation(
3958
4115
  trajectory: readonly LabeledTrajectoryStep[],
3959
4116
  bounds: TrajectoryBounds,
3960
4117
  entity: string,
3961
- ): string | undefined {
4118
+ ): { location: string; sourceStep: LabeledTrajectoryStep } | undefined {
3962
4119
  const normalizedEntity = normalizeEntity(entity);
3963
4120
  let location: string | undefined;
4121
+ let sourceStep: LabeledTrajectoryStep | undefined;
3964
4122
  for (const step of boundedTrajectory(trajectory, bounds)) {
3965
4123
  if (!step.action) continue;
3966
4124
  const action = step.action.trim();
3967
4125
  const take = /^take\s+(.+?)\s+from\s+(.+)$/i.exec(action);
3968
4126
  if (take && normalizeEntity(take[1]!) === normalizedEntity) {
3969
4127
  location = "inventory";
4128
+ sourceStep = step;
3970
4129
  continue;
3971
4130
  }
3972
4131
  const move = /^(?:move|put|place|insert)\s+(.+?)\s+(?:to|in|into|on)\s+(.+)$/i.exec(
@@ -3974,9 +4133,10 @@ function inferEntityLocation(
3974
4133
  );
3975
4134
  if (move && normalizeEntity(move[1]!) === normalizedEntity) {
3976
4135
  location = move[2]!.trim();
4136
+ sourceStep = step;
3977
4137
  }
3978
4138
  }
3979
- return location;
4139
+ return location && sourceStep ? { location, sourceStep } : undefined;
3980
4140
  }
3981
4141
 
3982
4142
  function normalizeActionVerb(action: string): string | undefined {
@@ -4020,10 +4180,15 @@ export function normalizeTurnExpansionEnd(stats: {
4020
4180
  return Math.max(messageCountEnd, Math.floor(stats.maxTurnIndex));
4021
4181
  }
4022
4182
 
4023
- function truncateTrajectoryAnalysisLines(lines: string[], maxChars: number): string[] {
4024
- const result: string[] = [];
4183
+ function truncateTrajectoryAnalysisLines(
4184
+ lines: TrajectoryAnalysisLines,
4185
+ maxChars: number,
4186
+ ): SelectedTrajectoryLine[] {
4187
+ const result: SelectedTrajectoryLine[] = [];
4025
4188
  let used = 0;
4026
- for (const line of lines.slice(0, TRAJECTORY_ANALYSIS_MAX_LINES)) {
4189
+ const limit = Math.min(lines.length, TRAJECTORY_ANALYSIS_MAX_LINES);
4190
+ for (let index = 0; index < limit; index += 1) {
4191
+ const line = lines[index]!;
4027
4192
  const separator = result.length === 0 ? 0 : 1;
4028
4193
  const remaining = maxChars - used - separator;
4029
4194
  if (remaining <= 0) {
@@ -4037,7 +4202,7 @@ function truncateTrajectoryAnalysisLines(lines: string[], maxChars: number): str
4037
4202
  if (clipped.length === 0) {
4038
4203
  break;
4039
4204
  }
4040
- result.push(clipped);
4205
+ result.push({ text: clipped, ...lines.sourceAt(index) });
4041
4206
  used += separator + clipped.length;
4042
4207
  }
4043
4208
  return result;
@@ -4538,21 +4703,21 @@ function normalizeTrajectoryQuery(value: string): string {
4538
4703
  }
4539
4704
 
4540
4705
  function appendExpandedEvidence(
4541
- evidenceItems: Array<{
4542
- id: string;
4543
- sessionId: string;
4544
- turnIndex: number;
4706
+ evidenceItems: ExplicitCueEvidenceItem[],
4707
+ seenTurns: Set<string>,
4708
+ sessionId: string,
4709
+ expanded: Array<{
4710
+ id?: number;
4711
+ session_id?: string;
4712
+ turn_index: number;
4545
4713
  role: string;
4546
4714
  content: string;
4547
4715
  }>,
4548
- seenTurns: Set<string>,
4549
- sessionId: string,
4550
- expanded: Array<{ turn_index: number; role: string; content: string }>,
4551
4716
  ): void {
4552
4717
  for (const message of expanded) {
4553
4718
  appendEvidenceItem(evidenceItems, seenTurns, {
4554
- id: `${sessionId}:${message.turn_index}`,
4555
- sessionId,
4719
+ ...lcmEvidenceIdentity(message, sessionId),
4720
+ sessionId: message.session_id ?? sessionId,
4556
4721
  turnIndex: message.turn_index,
4557
4722
  role: message.role,
4558
4723
  content: message.content,
@@ -4689,14 +4854,7 @@ async function collectNamedMeetingFactEvidence(options: {
4689
4854
  sessionId?: string;
4690
4855
  query: string;
4691
4856
  maxReferences: number;
4692
- evidenceItems: Array<{
4693
- id: string;
4694
- sessionId: string;
4695
- turnIndex: number;
4696
- role: string;
4697
- content: string;
4698
- score?: number;
4699
- }>;
4857
+ evidenceItems: ExplicitCueEvidenceItem[];
4700
4858
  seenTurns: Set<string>;
4701
4859
  }): Promise<void> {
4702
4860
  const cues = collectMeetingFactCues(options.query)
@@ -4712,7 +4870,7 @@ async function collectNamedMeetingFactEvidence(options: {
4712
4870
  for (const result of results) {
4713
4871
  if (!isNamedMeetingFactEvidence(result.content, options.query)) continue;
4714
4872
  appendEvidenceItem(options.evidenceItems, options.seenTurns, {
4715
- id: `${result.session_id}:${result.turn_index}`,
4873
+ ...lcmEvidenceIdentity(result, result.session_id),
4716
4874
  sessionId: result.session_id,
4717
4875
  turnIndex: result.turn_index,
4718
4876
  role: result.role,