@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.
- package/dist/access-admin-ops-surface.d.ts +2 -2
- package/dist/access-admin-ops-surface.js +1 -1
- package/dist/access-boundary.d.ts +2 -2
- package/dist/access-boundary.js +2 -2
- package/dist/access-cli.js +8 -8
- package/dist/access-http.d.ts +2 -2
- package/dist/access-http.js +6 -6
- package/dist/access-identity-continuity-surface.d.ts +1 -1
- package/dist/access-identity-continuity-surface.js +1 -1
- package/dist/access-lcm-surface.d.ts +2 -2
- package/dist/access-lcm-surface.js +1 -1
- package/dist/access-mcp.d.ts +2 -2
- package/dist/access-mcp.js +5 -5
- package/dist/access-observe-write-surface.d.ts +2 -2
- package/dist/access-observe-write-surface.js +1 -1
- package/dist/access-operations-batch.js +3 -3
- package/dist/access-operations.d.ts +2 -2
- package/dist/access-operations.js +4 -4
- package/dist/access-recall-surface.d.ts +4 -3
- package/dist/access-recall-surface.js +1 -1
- package/dist/{access-service-DK7s3wMj.d.ts → access-service-BofDzaPT.d.ts} +3 -2
- package/dist/access-service.d.ts +2 -2
- package/dist/access-service.js +1 -1
- package/dist/access-surface-catalog.d.ts +2 -2
- package/dist/bootstrap.d.ts +1 -1
- package/dist/{chunk-43VNWTBB.js → chunk-3RROR5KV.js} +4 -4
- package/dist/chunk-3RROR5KV.js.map +1 -0
- package/dist/{chunk-YDF4HL3W.js → chunk-5BHIGBHQ.js} +30 -37
- package/dist/chunk-5BHIGBHQ.js.map +1 -0
- package/dist/{chunk-4273YO6I.js → chunk-5WJ4LBFR.js} +106 -6
- package/dist/chunk-5WJ4LBFR.js.map +1 -0
- package/dist/{chunk-N6VIXDT7.js → chunk-C2SEH754.js} +4 -4
- package/dist/{chunk-MRQN5Q3I.js → chunk-EAREADWV.js} +2 -2
- package/dist/{chunk-GXLFDLX2.js → chunk-IF7THCGJ.js} +3 -3
- package/dist/{chunk-ARDRBJHD.js → chunk-SGYTQKC2.js} +3 -3
- package/dist/{chunk-QLYLTKUG.js → chunk-XA4PWKBW.js} +3 -3
- package/dist/{cli-CLSdmOVU.d.ts → cli-3_x5sJTL.d.ts} +2 -2
- package/dist/cli.d.ts +3 -3
- package/dist/cli.js +7 -7
- package/dist/explicit-capture.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +8 -8
- package/dist/mcp-memory-inspector-app.d.ts +2 -2
- package/dist/{orchestrator-BzXGNHco.d.ts → orchestrator-CAH5-PKN.d.ts} +20 -3
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +8 -8
- package/package.json +2 -2
- package/src/access-recall-surface.ts +46 -49
- package/src/access-service.ts +2 -7
- package/src/cli.ts +4 -10
- package/src/orchestration/xray-capture-queue.test.ts +42 -0
- package/src/orchestration/xray-capture-queue.ts +78 -0
- package/src/orchestrator-xray-capture.test.ts +319 -0
- package/src/orchestrator.ts +76 -8
- package/dist/chunk-4273YO6I.js.map +0 -1
- package/dist/chunk-43VNWTBB.js.map +0 -1
- package/dist/chunk-YDF4HL3W.js.map +0 -1
- /package/dist/{chunk-N6VIXDT7.js.map → chunk-C2SEH754.js.map} +0 -0
- /package/dist/{chunk-MRQN5Q3I.js.map → chunk-EAREADWV.js.map} +0 -0
- /package/dist/{chunk-GXLFDLX2.js.map → chunk-IF7THCGJ.js.map} +0 -0
- /package/dist/{chunk-ARDRBJHD.js.map → chunk-SGYTQKC2.js.map} +0 -0
- /package/dist/{chunk-QLYLTKUG.js.map → chunk-XA4PWKBW.js.map} +0 -0
package/src/access-service.ts
CHANGED
|
@@ -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
|
-
//
|
|
5760
|
-
//
|
|
5761
|
-
//
|
|
5762
|
-
//
|
|
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
|
+
});
|
package/src/orchestrator.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
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> {
|