@remnic/core 9.6.28 → 9.6.29

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 (62) hide show
  1. package/dist/access-admin-ops-surface.d.ts +2 -2
  2. package/dist/access-admin-ops-surface.js +1 -1
  3. package/dist/access-boundary.d.ts +2 -2
  4. package/dist/access-boundary.js +2 -2
  5. package/dist/access-cli.js +8 -8
  6. package/dist/access-http.d.ts +2 -2
  7. package/dist/access-http.js +6 -6
  8. package/dist/access-identity-continuity-surface.d.ts +1 -1
  9. package/dist/access-identity-continuity-surface.js +1 -1
  10. package/dist/access-lcm-surface.d.ts +2 -2
  11. package/dist/access-lcm-surface.js +1 -1
  12. package/dist/access-mcp.d.ts +2 -2
  13. package/dist/access-mcp.js +5 -5
  14. package/dist/access-observe-write-surface.d.ts +2 -2
  15. package/dist/access-observe-write-surface.js +1 -1
  16. package/dist/access-operations-batch.js +3 -3
  17. package/dist/access-operations.d.ts +2 -2
  18. package/dist/access-operations.js +4 -4
  19. package/dist/access-recall-surface.d.ts +4 -3
  20. package/dist/access-recall-surface.js +1 -1
  21. package/dist/{access-service-DK7s3wMj.d.ts → access-service-BofDzaPT.d.ts} +3 -2
  22. package/dist/access-service.d.ts +2 -2
  23. package/dist/access-service.js +1 -1
  24. package/dist/access-surface-catalog.d.ts +2 -2
  25. package/dist/bootstrap.d.ts +1 -1
  26. package/dist/{chunk-43VNWTBB.js → chunk-3RROR5KV.js} +4 -4
  27. package/dist/chunk-3RROR5KV.js.map +1 -0
  28. package/dist/{chunk-YDF4HL3W.js → chunk-5BHIGBHQ.js} +30 -37
  29. package/dist/chunk-5BHIGBHQ.js.map +1 -0
  30. package/dist/{chunk-4273YO6I.js → chunk-5WJ4LBFR.js} +106 -6
  31. package/dist/chunk-5WJ4LBFR.js.map +1 -0
  32. package/dist/{chunk-N6VIXDT7.js → chunk-C2SEH754.js} +4 -4
  33. package/dist/{chunk-MRQN5Q3I.js → chunk-EAREADWV.js} +2 -2
  34. package/dist/{chunk-GXLFDLX2.js → chunk-IF7THCGJ.js} +3 -3
  35. package/dist/{chunk-ARDRBJHD.js → chunk-SGYTQKC2.js} +3 -3
  36. package/dist/{chunk-QLYLTKUG.js → chunk-XA4PWKBW.js} +3 -3
  37. package/dist/{cli-CLSdmOVU.d.ts → cli-3_x5sJTL.d.ts} +2 -2
  38. package/dist/cli.d.ts +3 -3
  39. package/dist/cli.js +7 -7
  40. package/dist/explicit-capture.d.ts +1 -1
  41. package/dist/index.d.ts +4 -4
  42. package/dist/index.js +8 -8
  43. package/dist/mcp-memory-inspector-app.d.ts +2 -2
  44. package/dist/{orchestrator-BzXGNHco.d.ts → orchestrator-CAH5-PKN.d.ts} +20 -3
  45. package/dist/orchestrator.d.ts +1 -1
  46. package/dist/orchestrator.js +8 -8
  47. package/package.json +2 -2
  48. package/src/access-recall-surface.ts +46 -49
  49. package/src/access-service.ts +2 -7
  50. package/src/cli.ts +4 -10
  51. package/src/orchestration/xray-capture-queue.test.ts +42 -0
  52. package/src/orchestration/xray-capture-queue.ts +78 -0
  53. package/src/orchestrator-xray-capture.test.ts +319 -0
  54. package/src/orchestrator.ts +76 -8
  55. package/dist/chunk-4273YO6I.js.map +0 -1
  56. package/dist/chunk-43VNWTBB.js.map +0 -1
  57. package/dist/chunk-YDF4HL3W.js.map +0 -1
  58. /package/dist/{chunk-N6VIXDT7.js.map → chunk-C2SEH754.js.map} +0 -0
  59. /package/dist/{chunk-MRQN5Q3I.js.map → chunk-EAREADWV.js.map} +0 -0
  60. /package/dist/{chunk-GXLFDLX2.js.map → chunk-IF7THCGJ.js.map} +0 -0
  61. /package/dist/{chunk-ARDRBJHD.js.map → chunk-SGYTQKC2.js.map} +0 -0
  62. /package/dist/{chunk-QLYLTKUG.js.map → chunk-XA4PWKBW.js.map} +0 -0
@@ -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,
@@ -0,0 +1,42 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { XrayCaptureQueue } from "./xray-capture-queue.js";
5
+
6
+ test("initial snapshot read failure releases the next queued capture", async () => {
7
+ const queue = new XrayCaptureQueue();
8
+ let failNextRead = true;
9
+ let snapshot: string | null = "prior";
10
+ const operations: string[] = [];
11
+ const state = {
12
+ read(): string | null {
13
+ if (failNextRead) {
14
+ failNextRead = false;
15
+ throw new Error("snapshot clone failed");
16
+ }
17
+ return snapshot;
18
+ },
19
+ clear(): void {
20
+ snapshot = null;
21
+ },
22
+ restore(value: string | null): void {
23
+ snapshot = value;
24
+ },
25
+ };
26
+
27
+ const failed = queue.run(async () => {
28
+ operations.push("failed");
29
+ return "unexpected";
30
+ }, state);
31
+ const next = queue.run(async () => {
32
+ operations.push("next");
33
+ snapshot = "fresh";
34
+ return "next-result";
35
+ }, state);
36
+
37
+ await assert.rejects(failed, /snapshot clone failed/);
38
+ const result = await next;
39
+ assert.deepEqual(operations, ["next"]);
40
+ assert.equal(result.result, "next-result");
41
+ assert.equal(result.snapshot, "fresh");
42
+ });
@@ -0,0 +1,78 @@
1
+ import { abortError, throwIfAborted } from "../abort-error.js";
2
+
3
+ export interface AtomicCaptureState<TSnapshot> {
4
+ read(): TSnapshot | null;
5
+ clear(): void;
6
+ restore(snapshot: TSnapshot | null): void;
7
+ }
8
+
9
+ export interface AtomicCaptureResult<TResult, TSnapshot> {
10
+ result: TResult;
11
+ snapshot: TSnapshot | null;
12
+ recallStartedAt: number;
13
+ }
14
+
15
+ /** Per-owner abortable FIFO for operations that publish through one mutable slot. */
16
+ export class XrayCaptureQueue {
17
+ private tail: Promise<void> = Promise.resolve();
18
+
19
+ async run<TResult, TSnapshot>(
20
+ operation: () => Promise<TResult>,
21
+ state: AtomicCaptureState<TSnapshot>,
22
+ signal?: AbortSignal,
23
+ ): Promise<AtomicCaptureResult<TResult, TSnapshot>> {
24
+ throwIfAborted(signal, "x-ray capture aborted before queueing");
25
+ const previous = this.tail;
26
+ let release: () => void = () => {};
27
+ this.tail = new Promise<void>((resolve) => {
28
+ release = resolve;
29
+ });
30
+
31
+ try {
32
+ await this.waitForTurn(previous, signal);
33
+ } catch (error) {
34
+ // Keep this abandoned node as a barrier until its predecessor settles,
35
+ // so later callers cannot overtake the still-active operation.
36
+ void previous.then(release, release);
37
+ throw error;
38
+ }
39
+
40
+ let previousSnapshot: TSnapshot | null = null;
41
+ let didReadPreviousSnapshot = false;
42
+ try {
43
+ previousSnapshot = state.read();
44
+ didReadPreviousSnapshot = true;
45
+ const recallStartedAt = Date.now();
46
+ state.clear();
47
+ const result = await operation();
48
+ const snapshot = state.read();
49
+ if (!snapshot) state.restore(previousSnapshot);
50
+ return { result, snapshot, recallStartedAt };
51
+ } catch (error) {
52
+ if (didReadPreviousSnapshot) state.restore(previousSnapshot);
53
+ throw error;
54
+ } finally {
55
+ release();
56
+ }
57
+ }
58
+
59
+ private async waitForTurn(previous: Promise<void>, signal?: AbortSignal): Promise<void> {
60
+ throwIfAborted(signal, "x-ray capture aborted while queued");
61
+ if (!signal) {
62
+ await previous;
63
+ return;
64
+ }
65
+
66
+ let onAbort: (() => void) | undefined;
67
+ const aborted = new Promise<never>((_resolve, reject) => {
68
+ onAbort = () => reject(abortError("x-ray capture aborted while queued"));
69
+ signal.addEventListener("abort", onAbort, { once: true });
70
+ });
71
+ try {
72
+ await Promise.race([previous, aborted]);
73
+ throwIfAborted(signal, "x-ray capture aborted while queued");
74
+ } finally {
75
+ if (onAbort) signal.removeEventListener("abort", onAbort);
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,319 @@
1
+ import assert from "node:assert/strict";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ import test from "node:test";
5
+
6
+ import { parseConfig } from "./config.js";
7
+ import { Orchestrator } from "./orchestrator.js";
8
+ import { buildXraySnapshot, type RecallXraySnapshot } from "./recall-xray.js";
9
+
10
+ let orchestratorSequence = 0;
11
+
12
+ function makeOrchestrator(): Orchestrator {
13
+ orchestratorSequence += 1;
14
+ const memoryDir = path.join(
15
+ tmpdir(),
16
+ `remnic-xray-capture-unit-${process.pid}-${orchestratorSequence}`,
17
+ );
18
+ return new Orchestrator(parseConfig({
19
+ memoryDir,
20
+ workspaceDir: memoryDir,
21
+ qmdEnabled: false,
22
+ embeddingFallbackEnabled: false,
23
+ }));
24
+ }
25
+
26
+ function snapshot(query: string, snapshotId: string): RecallXraySnapshot {
27
+ return buildXraySnapshot({
28
+ query,
29
+ snapshotIdGenerator: () => snapshotId,
30
+ now: () => 1,
31
+ });
32
+ }
33
+
34
+ function setSnapshot(orchestrator: Orchestrator, value: RecallXraySnapshot): void {
35
+ (orchestrator as unknown as { lastXraySnapshot: RecallXraySnapshot | null })
36
+ .lastXraySnapshot = value;
37
+ }
38
+
39
+ function stubRecall(
40
+ orchestrator: Orchestrator,
41
+ invoke: (
42
+ prompt: string,
43
+ sessionKey: string | undefined,
44
+ options: Record<string, unknown>,
45
+ ) => Promise<string>,
46
+ ): void {
47
+ (orchestrator as unknown as { invokeRecall: typeof invoke }).invokeRecall = invoke;
48
+ }
49
+
50
+ function deferred<T>(): {
51
+ promise: Promise<T>;
52
+ resolve: (value: T) => void;
53
+ reject: (reason?: unknown) => void;
54
+ } {
55
+ let resolve!: (value: T) => void;
56
+ let reject!: (reason?: unknown) => void;
57
+ const promise = new Promise<T>((res, rej) => {
58
+ resolve = res;
59
+ reject = rej;
60
+ });
61
+ return { promise, resolve, reject };
62
+ }
63
+
64
+ test("recallWithXrayCapture clears stale state and returns an owned nested clone", async () => {
65
+ const orchestrator = makeOrchestrator();
66
+ setSnapshot(orchestrator, snapshot("stale", "stale-id"));
67
+ const captured = buildXraySnapshot({
68
+ query: "normalized query",
69
+ snapshotIdGenerator: () => "fresh-id",
70
+ now: () => 1,
71
+ results: [{
72
+ memoryId: "memory-1",
73
+ path: "memories/memory-1.md",
74
+ servedBy: "hybrid",
75
+ scoreDecomposition: { final: 1 },
76
+ admittedBy: ["retrieval"],
77
+ graphPath: ["a", "b"],
78
+ }],
79
+ filters: [{ name: "retrieval", considered: 1, admitted: 1 }],
80
+ });
81
+ stubRecall(orchestrator, async (_prompt, _sessionKey, options) => {
82
+ assert.equal(options.xrayCapture, true);
83
+ assert.equal(orchestrator.getLastXraySnapshot(), null);
84
+ setSnapshot(orchestrator, captured);
85
+ return "recall-result";
86
+ });
87
+
88
+ const result = await orchestrator.recallWithXrayCapture("raw query", "session");
89
+ assert.equal(result.result, "recall-result");
90
+ assert.deepEqual(result.snapshot, captured);
91
+ assert.notEqual(result.snapshot, captured);
92
+
93
+ result.snapshot!.results[0]!.admittedBy.push("tampered");
94
+ result.snapshot!.results[0]!.graphPath!.push("tampered");
95
+ result.snapshot!.filters[0]!.reason = "tampered";
96
+ const stored = orchestrator.getLastXraySnapshot();
97
+ assert.deepEqual(stored?.results[0]?.admittedBy, ["retrieval"]);
98
+ assert.deepEqual(stored?.results[0]?.graphPath, ["a", "b"]);
99
+ assert.equal(stored?.filters[0]?.reason, undefined);
100
+ });
101
+
102
+ test("recallWithXrayCapture serializes every consumer of one orchestrator slot", async () => {
103
+ const orchestrator = makeOrchestrator();
104
+ const firstGate = deferred<void>();
105
+ const firstStarted = deferred<void>();
106
+ const order: string[] = [];
107
+ stubRecall(orchestrator, async (prompt) => {
108
+ order.push(`${prompt}:start`);
109
+ if (prompt === "first") {
110
+ firstStarted.resolve();
111
+ await firstGate.promise;
112
+ }
113
+ setSnapshot(orchestrator, snapshot(prompt, `${prompt}-id`));
114
+ order.push(`${prompt}:end`);
115
+ return prompt;
116
+ });
117
+
118
+ const first = orchestrator.recallWithXrayCapture("first");
119
+ const second = orchestrator.recallWithXrayCapture("second");
120
+ await firstStarted.promise;
121
+ assert.deepEqual(order, ["first:start"]);
122
+ firstGate.resolve();
123
+
124
+ const [firstResult, secondResult] = await Promise.all([first, second]);
125
+ assert.deepEqual(order, ["first:start", "first:end", "second:start", "second:end"]);
126
+ assert.equal(firstResult.snapshot?.snapshotId, "first-id");
127
+ assert.equal(secondResult.snapshot?.snapshotId, "second-id");
128
+ });
129
+
130
+ test("recallWithXrayCapture queues are independent across orchestrators", async () => {
131
+ const firstOrchestrator = makeOrchestrator();
132
+ const secondOrchestrator = makeOrchestrator();
133
+ const firstGate = deferred<void>();
134
+ let secondStarted = false;
135
+ stubRecall(firstOrchestrator, async () => {
136
+ await firstGate.promise;
137
+ return "first";
138
+ });
139
+ stubRecall(secondOrchestrator, async () => {
140
+ secondStarted = true;
141
+ return "second";
142
+ });
143
+
144
+ const first = firstOrchestrator.recallWithXrayCapture("first");
145
+ await secondOrchestrator.recallWithXrayCapture("second");
146
+ assert.equal(secondStarted, true);
147
+ firstGate.resolve();
148
+ await first;
149
+ });
150
+
151
+ test("recallWithXrayCapture rejects a pre-aborted signal without clearing or invoking", async () => {
152
+ const orchestrator = makeOrchestrator();
153
+ const stale = snapshot("stale", "stale-id");
154
+ setSnapshot(orchestrator, stale);
155
+ const abortController = new AbortController();
156
+ abortController.abort();
157
+ let invoked = false;
158
+ stubRecall(orchestrator, async () => {
159
+ invoked = true;
160
+ return "unexpected";
161
+ });
162
+
163
+ await assert.rejects(
164
+ orchestrator.recallWithXrayCapture("aborted", undefined, {
165
+ abortSignal: abortController.signal,
166
+ }),
167
+ { name: "AbortError" },
168
+ );
169
+ assert.equal(invoked, false);
170
+ assert.deepEqual(orchestrator.getLastXraySnapshot(), stale);
171
+ });
172
+
173
+ test("queued abort rejects promptly while its barrier prevents a third capture overtaking", async () => {
174
+ const orchestrator = makeOrchestrator();
175
+ const firstGate = deferred<void>();
176
+ const abortController = new AbortController();
177
+ const order: string[] = [];
178
+ stubRecall(orchestrator, async (prompt) => {
179
+ order.push(`${prompt}:start`);
180
+ if (prompt === "first") await firstGate.promise;
181
+ setSnapshot(orchestrator, snapshot(prompt, `${prompt}-id`));
182
+ order.push(`${prompt}:end`);
183
+ return prompt;
184
+ });
185
+
186
+ const first = orchestrator.recallWithXrayCapture("first");
187
+ await Promise.resolve();
188
+ const queued = orchestrator.recallWithXrayCapture("aborted", undefined, {
189
+ abortSignal: abortController.signal,
190
+ });
191
+ const third = orchestrator.recallWithXrayCapture("third");
192
+ abortController.abort();
193
+
194
+ await assert.rejects(queued, { name: "AbortError" });
195
+ assert.deepEqual(order, ["first:start"]);
196
+ firstGate.resolve();
197
+
198
+ const [firstResult, thirdResult] = await Promise.all([first, third]);
199
+ assert.deepEqual(order, ["first:start", "first:end", "third:start", "third:end"]);
200
+ assert.equal(firstResult.snapshot?.snapshotId, "first-id");
201
+ assert.equal(thirdResult.snapshot?.snapshotId, "third-id");
202
+ });
203
+
204
+ test("started abort retains ownership until recall settles", async () => {
205
+ const orchestrator = makeOrchestrator();
206
+ const activeGate = deferred<void>();
207
+ const activeStarted = deferred<void>();
208
+ const abortController = new AbortController();
209
+ let nextStarted = false;
210
+ stubRecall(orchestrator, async (prompt) => {
211
+ if (prompt === "active") {
212
+ activeStarted.resolve();
213
+ await activeGate.promise;
214
+ setSnapshot(orchestrator, snapshot("active", "active-id"));
215
+ } else {
216
+ nextStarted = true;
217
+ setSnapshot(orchestrator, snapshot("next", "next-id"));
218
+ }
219
+ return prompt;
220
+ });
221
+
222
+ const active = orchestrator.recallWithXrayCapture("active", undefined, {
223
+ abortSignal: abortController.signal,
224
+ });
225
+ await activeStarted.promise;
226
+ abortController.abort();
227
+ const next = orchestrator.recallWithXrayCapture("next");
228
+ await Promise.resolve();
229
+ assert.equal(nextStarted, false);
230
+
231
+ activeGate.resolve();
232
+ const activeResult = await active;
233
+ const nextResult = await next;
234
+ assert.equal(activeResult.snapshot?.snapshotId, "active-id");
235
+ assert.equal(nextResult.snapshot?.snapshotId, "next-id");
236
+ });
237
+
238
+ test("failed and empty captures restore prior shared snapshot but return no stale snapshot", async () => {
239
+ const orchestrator = makeOrchestrator();
240
+ const prior = snapshot("prior", "prior-id");
241
+ setSnapshot(orchestrator, prior);
242
+ stubRecall(orchestrator, async (prompt) => {
243
+ if (prompt === "failure") throw new Error("recall failed");
244
+ return "empty";
245
+ });
246
+
247
+ await assert.rejects(
248
+ orchestrator.recallWithXrayCapture("failure"),
249
+ /recall failed/,
250
+ );
251
+ assert.equal(orchestrator.getLastXraySnapshot()?.snapshotId, "prior-id");
252
+
253
+ const empty = await orchestrator.recallWithXrayCapture("empty");
254
+ assert.equal(empty.snapshot, null);
255
+ assert.equal(orchestrator.getLastXraySnapshot()?.snapshotId, "prior-id");
256
+ });
257
+
258
+ test("legacy capturing recall shares the queue and preserves its string return", async () => {
259
+ const orchestrator = makeOrchestrator();
260
+ const firstGate = deferred<void>();
261
+ const order: string[] = [];
262
+ stubRecall(orchestrator, async (prompt) => {
263
+ order.push(`${prompt}:start`);
264
+ if (prompt === "atomic") await firstGate.promise;
265
+ setSnapshot(orchestrator, snapshot(prompt, `${prompt}-id`));
266
+ order.push(`${prompt}:end`);
267
+ return `${prompt}-result`;
268
+ });
269
+
270
+ const atomic = orchestrator.recallWithXrayCapture("atomic");
271
+ await Promise.resolve();
272
+ const legacy = orchestrator.recall("legacy", undefined, { xrayCapture: true });
273
+ await Promise.resolve();
274
+ assert.deepEqual(order, ["atomic:start"]);
275
+ firstGate.resolve();
276
+
277
+ const [atomicResult, legacyResult] = await Promise.all([atomic, legacy]);
278
+ assert.equal(atomicResult.result, "atomic-result");
279
+ assert.equal(legacyResult, "legacy-result");
280
+ assert.deepEqual(order, ["atomic:start", "atomic:end", "legacy:start", "legacy:end"]);
281
+ });
282
+
283
+ test("ordinary recall remains independent and does not overwrite a captured snapshot", async () => {
284
+ const orchestrator = makeOrchestrator();
285
+ const captureGate = deferred<void>();
286
+ const captureStarted = deferred<void>();
287
+ const order: string[] = [];
288
+ stubRecall(orchestrator, async (prompt, _sessionKey, options) => {
289
+ order.push(prompt);
290
+ if (prompt === "capture") {
291
+ captureStarted.resolve();
292
+ await captureGate.promise;
293
+ setSnapshot(orchestrator, snapshot("capture", "capture-id"));
294
+ }
295
+ assert.equal(options.xrayCapture === true, prompt === "capture");
296
+ return `${prompt}-result`;
297
+ });
298
+
299
+ const capture = orchestrator.recallWithXrayCapture("capture");
300
+ await captureStarted.promise;
301
+ const ordinary = await orchestrator.recall("ordinary");
302
+ assert.equal(ordinary, "ordinary-result");
303
+ assert.deepEqual(order, ["capture", "ordinary"]);
304
+ captureGate.resolve();
305
+ await capture;
306
+ await orchestrator.recall("ordinary-after");
307
+ assert.equal(orchestrator.getLastXraySnapshot()?.snapshotId, "capture-id");
308
+ });
309
+
310
+ test("recallWithXrayCapture accepts normalized snapshot queries without equality checks", async () => {
311
+ const orchestrator = makeOrchestrator();
312
+ stubRecall(orchestrator, async () => {
313
+ setSnapshot(orchestrator, snapshot("daily briefing", "normalized-id"));
314
+ return "context";
315
+ });
316
+
317
+ const result = await orchestrator.recallWithXrayCapture(":cron: daily briefing");
318
+ assert.equal(result.snapshot?.query, "daily briefing");
319
+ });
@@ -121,6 +121,7 @@ import { selfDeps } from "./orchestration/self-deps.js";
121
121
  import { RecallEntryCoordinator } from "./orchestration/recall-entry.js";
122
122
  import { SessionContextCoordinator } from "./orchestration/session-context.js";
123
123
  import { drainRecallWrites, trackRecallWrite } from "./orchestration/recall-background-writes.js";
124
+ import { XrayCaptureQueue } from "./orchestration/xray-capture-queue.js";
124
125
  import {
125
126
  abortRecallError,
126
127
  buildCompressionGuidelinesMarkdown,
@@ -258,10 +259,6 @@ import {
258
259
  type EvalShadowRecallRecord,
259
260
  } from "./evals.js";
260
261
  import { SessionObserverState } from "./session-observer-state.js";
261
- import {
262
- abortError as sharedAbortError,
263
- throwIfAborted as sharedThrowIfAborted,
264
- } from "./abort-error.js";
265
262
  import { CODEX_THREAD_KEY_PREFIX } from "./thread-key.js";
266
263
  import { isDisagreementPrompt } from "./signal.js";
267
264
  import { lintWorkspaceFiles, rotateMarkdownFileToArchive } from "./hygiene.js";
@@ -633,6 +630,8 @@ export class Orchestrator {
633
630
  * capturing caller can still read their snapshot back.
634
631
  */
635
632
  private lastXraySnapshot: RecallXraySnapshot | null = null;
633
+ /** Per-instance ordering domain for every writer of `lastXraySnapshot`. */
634
+ private readonly xrayCaptureQueue = new XrayCaptureQueue();
636
635
  readonly embeddingFallback: EmbeddingFallback;
637
636
  private readonly conversationIndexDir: string;
638
637
  private readonly extraction: ExtractionEngine;
@@ -2182,7 +2181,7 @@ export class Orchestrator {
2182
2181
  );
2183
2182
  }
2184
2183
 
2185
- async recall(
2184
+ private invokeRecall(
2186
2185
  prompt: string,
2187
2186
  sessionKey?: string,
2188
2187
  options: RecallInvocationOptions = {},
@@ -2196,23 +2195,92 @@ export class Orchestrator {
2196
2195
  );
2197
2196
  }
2198
2197
 
2198
+ async recall(
2199
+ prompt: string,
2200
+ sessionKey?: string,
2201
+ options: RecallInvocationOptions = {},
2202
+ ): Promise<string> {
2203
+ if (options.xrayCapture === true) {
2204
+ // Preserve the legacy string-returning surface and its soft-abort
2205
+ // behavior while still placing every X-ray writer in the same ordering
2206
+ // domain. In particular, a pre-aborted legacy recall continues to
2207
+ // resolve with an empty string rather than rejecting.
2208
+ const { result } = await this.runRecallWithXrayCapture(
2209
+ prompt,
2210
+ sessionKey,
2211
+ options,
2212
+ );
2213
+ return result;
2214
+ }
2215
+ return this.invokeRecall(prompt, sessionKey, options);
2216
+ }
2217
+
2199
2218
  /**
2200
- * Return the most recent X-ray snapshot captured during a
2201
- * `recall()` call that passed `xrayCapture: true` (issue #570 PR 1).
2219
+ * Return the most recent X-ray snapshot captured during a recall.
2202
2220
  * Returns `null` when no such capture has occurred on this
2203
2221
  * orchestrator instance. Returned snapshot is a deep copy so
2204
2222
  * caller mutation cannot tear the stored value.
2223
+ *
2224
+ * @deprecated Reading this after a separate `recall()` is not atomic. Use
2225
+ * `recallWithXrayCapture()` when the snapshot must belong to that call.
2205
2226
  */
2206
2227
  getLastXraySnapshot(): RecallXraySnapshot | null {
2207
2228
  if (!this.lastXraySnapshot) return null;
2208
2229
  return structuredClone(this.lastXraySnapshot);
2209
2230
  }
2210
2231
 
2211
- /** Clear the captured X-ray snapshot. Exposed for tests / explicit reset. */
2232
+ /**
2233
+ * Clear the captured X-ray snapshot. Exposed for tests / explicit reset.
2234
+ *
2235
+ * @deprecated A separate clear → recall → get sequence is not atomic. Use
2236
+ * `recallWithXrayCapture()` for capture operations.
2237
+ */
2212
2238
  clearLastXraySnapshot(): void {
2213
2239
  this.lastXraySnapshot = null;
2214
2240
  }
2215
2241
 
2242
+ /** Atomically run recall and return the cloned X-ray snapshot it published. */
2243
+ async recallWithXrayCapture(
2244
+ prompt: string,
2245
+ sessionKey?: string,
2246
+ options: Omit<RecallInvocationOptions, "xrayCapture"> = {},
2247
+ ): Promise<{
2248
+ result: string;
2249
+ snapshot: RecallXraySnapshot | null;
2250
+ recallStartedAt: number;
2251
+ }> {
2252
+ return this.runRecallWithXrayCapture(
2253
+ prompt,
2254
+ sessionKey,
2255
+ options,
2256
+ options.abortSignal,
2257
+ );
2258
+ }
2259
+
2260
+ private async runRecallWithXrayCapture(
2261
+ prompt: string,
2262
+ sessionKey: string | undefined,
2263
+ options: Omit<RecallInvocationOptions, "xrayCapture"> | RecallInvocationOptions,
2264
+ queueAbortSignal?: AbortSignal,
2265
+ ): Promise<{
2266
+ result: string;
2267
+ snapshot: RecallXraySnapshot | null;
2268
+ recallStartedAt: number;
2269
+ }> {
2270
+ return this.xrayCaptureQueue.run(
2271
+ () => this.invokeRecall(prompt, sessionKey, {
2272
+ ...options,
2273
+ xrayCapture: true,
2274
+ }),
2275
+ {
2276
+ read: () => this.getLastXraySnapshot(),
2277
+ clear: () => this.clearLastXraySnapshot(),
2278
+ restore: (snapshot) => { this.lastXraySnapshot = snapshot; },
2279
+ },
2280
+ queueAbortSignal,
2281
+ );
2282
+ }
2283
+
2216
2284
  async waitForDirectAnswerObservationIdle(
2217
2285
  timeoutMs: number = 60_000,
2218
2286
  ): Promise<boolean> {