@remnic/core 9.6.28 → 9.6.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/access-admin-ops-surface.d.ts +2 -2
  2. package/dist/access-admin-ops-surface.js +2 -1
  3. package/dist/access-boundary.d.ts +2 -2
  4. package/dist/access-boundary.js +3 -2
  5. package/dist/access-cli.js +16 -15
  6. package/dist/access-cli.js.map +1 -1
  7. package/dist/access-http.d.ts +2 -2
  8. package/dist/access-http.js +7 -6
  9. package/dist/access-identity-continuity-surface.d.ts +1 -1
  10. package/dist/access-identity-continuity-surface.js +2 -1
  11. package/dist/access-lcm-surface.d.ts +2 -2
  12. package/dist/access-lcm-surface.js +2 -1
  13. package/dist/access-mcp.d.ts +2 -2
  14. package/dist/access-mcp.js +6 -5
  15. package/dist/access-observe-write-surface.d.ts +2 -2
  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 +2 -2
  19. package/dist/access-operations.js +5 -4
  20. package/dist/access-recall-surface.d.ts +4 -3
  21. package/dist/access-recall-surface.js +2 -1
  22. package/dist/{access-service-DK7s3wMj.d.ts → access-service-BofDzaPT.d.ts} +3 -2
  23. package/dist/access-service.d.ts +2 -2
  24. package/dist/access-service.js +2 -1
  25. package/dist/access-surface-catalog.d.ts +2 -2
  26. package/dist/bootstrap.d.ts +1 -1
  27. package/dist/{chunk-4273YO6I.js → chunk-3VSTTB74.js} +111 -11
  28. package/dist/chunk-3VSTTB74.js.map +1 -0
  29. package/dist/{chunk-4463LUIE.js → chunk-77UPM7P3.js} +22 -11
  30. package/dist/chunk-77UPM7P3.js.map +1 -0
  31. package/dist/{chunk-FV5FVO2J.js → chunk-7O2JL5RY.js} +9 -5
  32. package/dist/chunk-7O2JL5RY.js.map +1 -0
  33. package/dist/{chunk-LZTFCAKE.js → chunk-BFU7H32D.js} +28 -15
  34. package/dist/chunk-BFU7H32D.js.map +1 -0
  35. package/dist/chunk-C37ZRXX7.js +36 -0
  36. package/dist/chunk-C37ZRXX7.js.map +1 -0
  37. package/dist/{chunk-ARDRBJHD.js → chunk-C6TKSI35.js} +3 -3
  38. package/dist/{chunk-N6VIXDT7.js → chunk-DAAJUA6Q.js} +4 -4
  39. package/dist/{chunk-MRQN5Q3I.js → chunk-GQURC4B7.js} +2 -2
  40. package/dist/{chunk-4SBW47WK.js → chunk-IRMLYBMU.js} +28 -15
  41. package/dist/chunk-IRMLYBMU.js.map +1 -0
  42. package/dist/{chunk-QLYLTKUG.js → chunk-MXAGJHTA.js} +3 -3
  43. package/dist/{chunk-YDF4HL3W.js → chunk-OXVKR5OU.js} +35 -39
  44. package/dist/chunk-OXVKR5OU.js.map +1 -0
  45. package/dist/{chunk-GXLFDLX2.js → chunk-PUQZIS6A.js} +3 -3
  46. package/dist/{chunk-PYPOFEMK.js → chunk-R26WO46Z.js} +1 -1
  47. package/dist/chunk-R26WO46Z.js.map +1 -0
  48. package/dist/chunk-VDC6RZI4.js +1 -0
  49. package/dist/{chunk-43VNWTBB.js → chunk-VJRJTRYF.js} +4 -4
  50. package/dist/chunk-VJRJTRYF.js.map +1 -0
  51. package/dist/{chunk-L6W77GWW.js → chunk-XGSOXFD4.js} +25 -13
  52. package/dist/chunk-XGSOXFD4.js.map +1 -0
  53. package/dist/{cli-CLSdmOVU.d.ts → cli-3_x5sJTL.d.ts} +2 -2
  54. package/dist/cli.d.ts +3 -3
  55. package/dist/cli.js +8 -7
  56. package/dist/event-order-recall.js +3 -2
  57. package/dist/evidence-pack.d.ts +2 -0
  58. package/dist/evidence-pack.js +1 -1
  59. package/dist/explicit-capture.d.ts +1 -1
  60. package/dist/explicit-cue-recall.d.ts +3 -0
  61. package/dist/explicit-cue-recall.js +3 -2
  62. package/dist/focused-list-recall.js +3 -2
  63. package/dist/index.d.ts +4 -4
  64. package/dist/index.js +16 -15
  65. package/dist/lcm/index.d.ts +22 -0
  66. package/dist/lcm/index.js +9 -1
  67. package/dist/mcp-memory-inspector-app.d.ts +2 -2
  68. package/dist/{orchestrator-BzXGNHco.d.ts → orchestrator-CAH5-PKN.d.ts} +20 -3
  69. package/dist/orchestrator.d.ts +1 -1
  70. package/dist/orchestrator.js +16 -15
  71. package/dist/response-guidance-recall.js +3 -2
  72. package/dist/targeted-fact-recall.js +3 -2
  73. package/package.json +2 -2
  74. package/src/access-lcm-surface.ts +20 -5
  75. package/src/access-recall-surface.ts +48 -50
  76. package/src/access-service.ts +2 -7
  77. package/src/cli.ts +4 -10
  78. package/src/event-order-recall.ts +5 -3
  79. package/src/evidence-pack.ts +2 -0
  80. package/src/explicit-cue-recall.ts +67 -66
  81. package/src/focused-list-recall.ts +25 -16
  82. package/src/lcm/evidence-identity.ts +59 -0
  83. package/src/lcm/index.ts +7 -0
  84. package/src/lcm-retrieval-lineage.test.ts +236 -0
  85. package/src/orchestration/xray-capture-queue.test.ts +42 -0
  86. package/src/orchestration/xray-capture-queue.ts +78 -0
  87. package/src/orchestrator-xray-capture.test.ts +319 -0
  88. package/src/orchestrator.ts +76 -8
  89. package/src/response-guidance-recall.ts +22 -14
  90. package/src/targeted-fact-recall.ts +25 -16
  91. package/dist/chunk-4273YO6I.js.map +0 -1
  92. package/dist/chunk-43VNWTBB.js.map +0 -1
  93. package/dist/chunk-4463LUIE.js.map +0 -1
  94. package/dist/chunk-4SBW47WK.js.map +0 -1
  95. package/dist/chunk-FV5FVO2J.js.map +0 -1
  96. package/dist/chunk-HHLLAQGZ.js +0 -1
  97. package/dist/chunk-L6W77GWW.js.map +0 -1
  98. package/dist/chunk-LZTFCAKE.js.map +0 -1
  99. package/dist/chunk-PYPOFEMK.js.map +0 -1
  100. package/dist/chunk-YDF4HL3W.js.map +0 -1
  101. /package/dist/{chunk-ARDRBJHD.js.map → chunk-C6TKSI35.js.map} +0 -0
  102. /package/dist/{chunk-N6VIXDT7.js.map → chunk-DAAJUA6Q.js.map} +0 -0
  103. /package/dist/{chunk-MRQN5Q3I.js.map → chunk-GQURC4B7.js.map} +0 -0
  104. /package/dist/{chunk-QLYLTKUG.js.map → chunk-MXAGJHTA.js.map} +0 -0
  105. /package/dist/{chunk-GXLFDLX2.js.map → chunk-PUQZIS6A.js.map} +0 -0
  106. /package/dist/{chunk-HHLLAQGZ.js.map → chunk-VDC6RZI4.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.6.28",
3
+ "version": "9.6.30",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3000,7 +3000,7 @@
3000
3000
  "core"
3001
3001
  ],
3002
3002
  "peerDependencies": {
3003
- "@remnic/coding-graph": "^9.6.28"
3003
+ "@remnic/coding-graph": "^9.6.30"
3004
3004
  },
3005
3005
  "peerDependenciesMeta": {
3006
3006
  "@remnic/coding-graph": {
@@ -11,6 +11,7 @@
11
11
  import { resolveNamespaceCapabilities } from "./capabilities.js";
12
12
  import { combineNamespaces, lcmSessionKeyForNamespace, resolveCodingNamespaceOverlay } from "./coding/coding-namespace.js";
13
13
  import { log } from "./logger.js";
14
+ import { lcmEvidenceIdentity } from "./lcm/evidence-identity.js";
14
15
  import { canWriteNamespace, defaultNamespaceForPrincipal, recallNamespacesForPrincipal } from "./namespaces/principal.js";
15
16
  import type { Orchestrator } from "./orchestrator.js";
16
17
  import {
@@ -207,8 +208,8 @@ export class AccessLcmSurface {
207
208
  lcmEnabled: true,
208
209
  };
209
210
  }
210
- // Query each LCM read key in order, merging + deduping rows (by
211
- // sessionId+turnIndex) and preserving first-seen order, capped at `limit`.
211
+ // Query each LCM read key in order, merging + deduping by stable archive-row
212
+ // identity and preserving first-seen order, capped at `limit`.
212
213
  // Use allSettled so one corrupt/failed namespace key cannot discard sibling
213
214
  // results from other authorized profile namespaces.
214
215
  const seenRows = new Set<string>();
@@ -216,7 +217,14 @@ export class AccessLcmSurface {
216
217
  const lcmSearches: Array<{
217
218
  key: string | undefined;
218
219
  prefix: string | undefined;
219
- promise: Promise<Array<{ session_id: string; content: string; turn_index: number }>>;
220
+ promise: Promise<
221
+ Array<{
222
+ id?: number;
223
+ session_id: string;
224
+ content: string;
225
+ turn_index: number;
226
+ }>
227
+ >;
220
228
  }> = [];
221
229
  for (const lcmSessionKey of lcmSessionKeyIds) {
222
230
  for (const lcmSessionPrefix of lcmSessionPrefixes) {
@@ -228,7 +236,14 @@ export class AccessLcmSurface {
228
236
  limit,
229
237
  lcmSessionKey,
230
238
  lcmSessionPrefix,
231
- ) as Promise<Array<{ session_id: string; content: string; turn_index: number }>>,
239
+ ) as Promise<
240
+ Array<{
241
+ id?: number;
242
+ session_id: string;
243
+ content: string;
244
+ turn_index: number;
245
+ }>
246
+ >,
232
247
  });
233
248
  }
234
249
  }
@@ -246,7 +261,7 @@ export class AccessLcmSurface {
246
261
  continue;
247
262
  }
248
263
  for (const r of settled.value) {
249
- const dedupeKey = `${r.session_id}\0${r.turn_index}`;
264
+ const dedupeKey = lcmEvidenceIdentity(r, r.session_id).id;
250
265
  if (seenRows.has(dedupeKey)) continue;
251
266
  seenRows.add(dedupeKey);
252
267
  results.push({
@@ -14,9 +14,11 @@
14
14
  import { createHash } from "node:crypto";
15
15
  import * as nodePath from "node:path";
16
16
  import { AccessAuditAdapter, type AccessAuditResult } from "./access-audit.js";
17
+ import { throwIfAborted } from "./abort-error.js";
17
18
  import { resolveNamespaceCapabilities } from "./capabilities.js";
18
19
  import { resolveCodingNamespaceOverlay } from "./coding/coding-namespace.js";
19
20
  import { type BudgetDecision, CrossNamespaceBudget } from "./cross-namespace-budget.js";
21
+ import { lcmEvidenceIdentity } from "./lcm/evidence-identity.js";
20
22
  import { normalizeProjectionTags } from "./memory-projection-format.js";
21
23
  import { namespaceIdentityFromToken } from "./namespaces/identity.js";
22
24
  import { canReadNamespace, defaultNamespaceForPrincipal, recallNamespacesForPrincipal, resolvePrincipal } from "./namespaces/principal.js";
@@ -173,7 +175,6 @@ export interface AccessRecallSurfaceDeps {
173
175
  primaryNamespace: string,
174
176
  recallNamespaces?: readonly string[],
175
177
  ): Promise<{ storage: StorageManager; dir: string } | null>;
176
- xrayQueue: Promise<void>;
177
178
  }
178
179
 
179
180
  export class AccessRecallSurface {
@@ -593,7 +594,7 @@ export class AccessRecallSurface {
593
594
  if (excerpts.length >= limit) break;
594
595
  if (result.status !== "fulfilled") continue;
595
596
  for (const r of result.value) {
596
- const dedupeKey = `${r.session_id} ${r.turn_index}`;
597
+ const dedupeKey = lcmEvidenceIdentity(r, r.session_id).id;
597
598
  if (seenRows.has(dedupeKey)) continue;
598
599
  seenRows.add(dedupeKey);
599
600
  excerpts.push({
@@ -1117,6 +1118,8 @@ export class AccessRecallSurface {
1117
1118
  * regular X-ray API/CLI/MCP surfaces keep their existing payload shape.
1118
1119
  */
1119
1120
  includeRecall?: boolean;
1121
+ /** Cancel the capture before it starts and propagate cancellation to recall. */
1122
+ abortSignal?: AbortSignal;
1120
1123
  }): Promise<{
1121
1124
  snapshotFound: boolean;
1122
1125
  snapshot?: RecallXraySnapshot;
@@ -1195,21 +1198,9 @@ export class AccessRecallSurface {
1195
1198
  const mode = this.deps.normalizeRecallMode(request.mode);
1196
1199
  const disclosure = request.disclosure ?? DEFAULT_RECALL_DISCLOSURE;
1197
1200
 
1198
- // Serialize x-ray invocations behind a per-service mutex so the
1199
- // per-process `getLastXraySnapshot()` slot cannot be clobbered by
1200
- // a concurrent capturing call before this caller reads it back.
1201
- // Budget and principal are now threaded through
1202
- // `RecallInvocationOptions`, so global config mutation is gone
1203
- // (CLAUDE.md rule 47: no shared mutable state across async
1204
- // boundaries). The mutex stays only for the snapshot-slot
1205
- // ordering guarantee.
1206
- const previousQueue = this.deps.xrayQueue;
1207
- let release: () => void = () => {};
1208
- this.deps.xrayQueue = new Promise<void>((resolve) => {
1209
- release = resolve;
1210
- });
1211
- await previousQueue;
1212
- const recallStartedAt = Date.now();
1201
+ // Reset when the orchestrator-owned snapshot lock is actually acquired so
1202
+ // queue wait is not misreported as recall latency.
1203
+ let recallStartedAt = Date.now();
1213
1204
 
1214
1205
  const recallSessionKey = request.sessionKey?.trim() || undefined;
1215
1206
  let xrayResponse: {
@@ -1217,37 +1208,46 @@ export class AccessRecallSurface {
1217
1208
  snapshot?: RecallXraySnapshot;
1218
1209
  } = { snapshotFound: false };
1219
1210
 
1220
- try {
1221
- // Clear any prior snapshot so a capture failure surfaces as
1222
- // `{snapshotFound: false}` rather than returning stale data
1223
- // from an earlier call on the same orchestrator.
1224
- this.deps.orchestrator.clearLastXraySnapshot();
1225
- await this.deps.orchestrator.recall(query, recallSessionKey, {
1226
- xrayCapture: true,
1227
- ...(requestedNamespace ? { namespace: requestedNamespace } : {}),
1228
- ...(budgetOverride !== undefined
1229
- ? { budgetCharsOverride: budgetOverride }
1230
- : {}),
1231
- ...(mode !== undefined ? { mode } : {}),
1232
- // When the caller supplies an authenticated principal, forward
1233
- // it via the dedicated override channel so orchestrator-side
1234
- // ACL decisions use the SAME principal the access-surface
1235
- // pre-check above authorized. Threading an
1236
- // `authenticatedPrincipal` through `sessionKey` would be wrong:
1237
- // `resolvePrincipal(sessionKey)` only maps configured raw
1238
- // session keys and otherwise collapses to `"default"`, which
1239
- // in namespace-enabled deployments produces false denials /
1240
- // wrong-scope serving despite the pre-check passing
1241
- // (CLAUDE.md rule 42).
1242
- ...(authenticatedPrincipal
1243
- ? { principalOverride: authenticatedPrincipal }
1244
- : {}),
1245
- ...(request.currentContextScopes !== undefined
1246
- ? { currentContextScopes: request.currentContextScopes }
1247
- : {}),
1248
- });
1249
-
1250
- const rawSnapshot = this.deps.orchestrator.getLastXraySnapshot();
1211
+ {
1212
+ // Capture through the orchestrator-owned critical section so every
1213
+ // consumer of its mutable snapshot slot shares the same queue. The
1214
+ // returned clone remains owned by this call after the lock is released.
1215
+ const {
1216
+ snapshot: rawSnapshot,
1217
+ recallStartedAt: capturedRecallStartedAt,
1218
+ } = await this.deps.orchestrator.recallWithXrayCapture(
1219
+ query,
1220
+ recallSessionKey,
1221
+ {
1222
+ ...(requestedNamespace ? { namespace: requestedNamespace } : {}),
1223
+ ...(budgetOverride !== undefined
1224
+ ? { budgetCharsOverride: budgetOverride }
1225
+ : {}),
1226
+ ...(mode !== undefined ? { mode } : {}),
1227
+ // When the caller supplies an authenticated principal, forward
1228
+ // it via the dedicated override channel so orchestrator-side
1229
+ // ACL decisions use the SAME principal the access-surface
1230
+ // pre-check above authorized. Threading an
1231
+ // `authenticatedPrincipal` through `sessionKey` would be wrong:
1232
+ // `resolvePrincipal(sessionKey)` only maps configured raw
1233
+ // session keys and otherwise collapses to `"default"`, which
1234
+ // in namespace-enabled deployments produces false denials /
1235
+ // wrong-scope serving despite the pre-check passing
1236
+ // (CLAUDE.md rule 42).
1237
+ ...(authenticatedPrincipal
1238
+ ? { principalOverride: authenticatedPrincipal }
1239
+ : {}),
1240
+ ...(request.currentContextScopes !== undefined
1241
+ ? { currentContextScopes: request.currentContextScopes }
1242
+ : {}),
1243
+ ...(request.abortSignal ? { abortSignal: request.abortSignal } : {}),
1244
+ },
1245
+ );
1246
+ recallStartedAt = capturedRecallStartedAt;
1247
+ // Cancellation covers queueing + recall/capture. Post-capture shaping is
1248
+ // intentionally not wired for mid-I/O abort in this change, but do not
1249
+ // begin that work after the caller has already canceled.
1250
+ throwIfAborted(request.abortSignal, "recall X-ray aborted before postprocessing");
1251
1251
  // Re-check namespace after capture: the recall may have served
1252
1252
  // from a different namespace than the caller requested. Drop
1253
1253
  // the snapshot rather than leak cross-tenant data (CLAUDE.md
@@ -1483,8 +1483,6 @@ export class AccessRecallSurface {
1483
1483
  };
1484
1484
  }
1485
1485
  }
1486
- } finally {
1487
- release();
1488
1486
  }
1489
1487
 
1490
1488
  if (
@@ -2917,6 +2917,8 @@ export class EngramAccessService {
2917
2917
  * regular X-ray API/CLI/MCP surfaces keep their existing payload shape.
2918
2918
  */
2919
2919
  includeRecall?: boolean;
2920
+ /** Cancel the capture before it starts and propagate cancellation to recall. */
2921
+ abortSignal?: AbortSignal;
2920
2922
  }): Promise<{
2921
2923
  snapshotFound: boolean;
2922
2924
  snapshot?: RecallXraySnapshot;
@@ -2926,12 +2928,6 @@ export class EngramAccessService {
2926
2928
  request,
2927
2929
  );
2928
2930
  }
2929
- // Sequence lock for `recallXray` — see comment inside the method.
2930
- // Lives on the instance so every x-ray call on the same service
2931
- // shares it, and so separate services in the same process (e.g.
2932
- // per-tenant) do not block each other.
2933
- private xrayQueue: Promise<void> = Promise.resolve();
2934
-
2935
2931
  async memoryStore(
2936
2932
  request: EngramAccessMemoryStoreRequest,
2937
2933
  hooks?: { enforceWriteQuota?: () => void | Promise<void> },
@@ -5906,4 +5902,3 @@ export class EngramAccessService {
5906
5902
  );
5907
5903
  }
5908
5904
  }
5909
-
package/src/cli.ts CHANGED
@@ -5756,16 +5756,10 @@ export function registerCli(
5756
5756
  args[0],
5757
5757
  (args[1] ?? {}) as Record<string, unknown>,
5758
5758
  );
5759
- // Route the xray capture through `EngramAccessService` so
5760
- // the CLI shares the same `xrayQueue` mutex that the HTTP
5761
- // and MCP surfaces use otherwise the
5762
- // `clearLastXraySnapshot() recall() → getLastXraySnapshot()`
5763
- // sequence races with concurrent callers (e.g., a gateway
5764
- // agent hitting the same orchestrator) and could swap in
5765
- // their snapshot mid-flight, or our capture could overwrite
5766
- // theirs (cursor Medium + codex P1 review on #597). The
5767
- // service enforces CLAUDE.md rules 40 (serialized state) and
5768
- // 47 (no shared mutable state across async boundaries).
5759
+ // Delegate through `EngramAccessService` to the orchestrator's atomic
5760
+ // capture API, so CLI, HTTP, and MCP callers sharing an orchestrator
5761
+ // also share one X-ray ordering domain and receive the snapshot owned
5762
+ // by their invocation.
5769
5763
  const xrayService = new EngramAccessService(orchestrator);
5770
5764
  const response = await xrayService.recallXray({
5771
5765
  query: parsed.query,
@@ -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 { lcmEvidenceIdentity } from "./lcm/evidence-identity.js";
3
4
 
4
5
  import {
5
6
  unifiedDedupeAndRank,
@@ -205,13 +206,14 @@ async function collectEventOrderItems(
205
206
  windowTokens,
206
207
  );
207
208
  for (const message of messages) {
208
- const id = `${options.sessionId}:${message.turn_index}`;
209
+ const identity = lcmEvidenceIdentity(message, options.sessionId);
210
+ const id = identity.id;
209
211
  if (seen.has(id)) continue;
210
212
  if (!isEventOrderCandidate(message.content, message.role, options.query)) continue;
211
213
  seen.add(id);
212
214
  items.push({
213
- id,
214
- sessionId: options.sessionId,
215
+ ...identity,
216
+ sessionId: message.session_id ?? options.sessionId,
215
217
  turnIndex: message.turn_index,
216
218
  role: message.role,
217
219
  content: appendChronologicalCues(message.content, options.query),
@@ -1,5 +1,7 @@
1
1
  export interface EvidencePackItem {
2
2
  id?: string;
3
+ /** Stable SQLite row lineage for diagnostics; intentionally not rendered. */
4
+ archiveRowId?: number;
3
5
  sessionId?: string;
4
6
  turnIndex?: number;
5
7
  role?: string;
@@ -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,