@tangle-network/agent-runtime 0.116.0 → 0.117.0
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/README.md +7 -1
- package/dist/{activation-DuqQhee6.js → activation-BZ2FyX0O.js} +3 -3
- package/dist/{activation-DuqQhee6.js.map → activation-BZ2FyX0O.js.map} +1 -1
- package/dist/agent.d.ts +3 -64
- package/dist/agent.js +5 -207
- package/dist/agent.js.map +1 -1
- package/dist/{analyst-loop-BoNIG2hA.js → analyst-loop-DvSciOfB.js} +2 -2
- package/dist/{analyst-loop-BoNIG2hA.js.map → analyst-loop-DvSciOfB.js.map} +1 -1
- package/dist/analyst-loop.js +1 -1
- package/dist/candidate-execution/index.d.ts +3 -3
- package/dist/candidate-execution/index.js +5 -5
- package/dist/{candidate-execution-CfpJrd3o.js → candidate-execution-PX61zXd2.js} +4 -4
- package/dist/{candidate-execution-CfpJrd3o.js.map → candidate-execution-PX61zXd2.js.map} +1 -1
- package/dist/{environment-provider-CWsRh6Uz.d.ts → environment-provider-CTZjdIUg.d.ts} +298 -32
- package/dist/{environment-provider-CCaEhA-l.js → environment-provider-CzcEyRL7.js} +140 -31
- package/dist/environment-provider-CzcEyRL7.js.map +1 -0
- package/dist/environment-provider.d.ts +1 -1
- package/dist/environment-provider.js +1 -1
- package/dist/{improvement-cycle-C1cmjvPD.js → improvement-cycle-Bcqz8rZI.js} +160 -19
- package/dist/improvement-cycle-Bcqz8rZI.js.map +1 -0
- package/dist/{index-CYkDeM5L.d.ts → index-BHSYaWrC.d.ts} +3 -3
- package/dist/{index-DcLMNnG5.d.ts → index-BTUG4Xln.d.ts} +6 -6
- package/dist/{index-COumPQka.d.ts → index-DPWJxSRa.d.ts} +847 -275
- package/dist/{index-C-FYUuFG.d.ts → index-X2YwyWkc.d.ts} +2 -2
- package/dist/index.d.ts +8 -8
- package/dist/index.js +12 -12
- package/dist/intelligence.d.ts +8 -7
- package/dist/intelligence.js +15 -9
- package/dist/intelligence.js.map +1 -1
- package/dist/kernel.d.ts +3 -3
- package/dist/kernel.js +8 -8
- package/dist/{knowledge-DOzbywZT.js → knowledge-CDlrayRc.js} +7 -7
- package/dist/knowledge-CDlrayRc.js.map +1 -0
- package/dist/knowledge.d.ts +1 -1
- package/dist/knowledge.js +1 -1
- package/dist/{loop-runner-bin-BuQjc5DR.js → loop-runner-bin-BxhhDtmG.js} +4 -4
- package/dist/{loop-runner-bin-BuQjc5DR.js.map → loop-runner-bin-BxhhDtmG.js.map} +1 -1
- package/dist/{loop-runner-bin-BFrhPLKt.d.ts → loop-runner-bin-oyL7it5O.d.ts} +3 -3
- package/dist/loop-runner-bin.d.ts +1 -1
- package/dist/loop-runner-bin.js +1 -1
- package/dist/mcp/bin.js +43 -10
- package/dist/mcp/bin.js.map +1 -1
- package/dist/mcp/index.d.ts +3 -43
- package/dist/mcp/index.js +5 -204
- package/dist/mcp/index.js.map +1 -1
- package/dist/{openai-tools-_Wyp4udO.js → openai-tools-CO9X36zy.js} +2 -2
- package/dist/{openai-tools-_Wyp4udO.js.map → openai-tools-CO9X36zy.js.map} +1 -1
- package/dist/{prepare-BHQBb02e.js → prepare-BbI6qGOW.js} +5 -2
- package/dist/prepare-BbI6qGOW.js.map +1 -0
- package/dist/primeintellect/index.d.ts +1 -1
- package/dist/{protected-model-port-BP6Z4eau.d.ts → protected-model-port-B0yXMpLL.d.ts} +10 -2
- package/dist/{protected-model-port-DqAH1Z2M.js → protected-model-port-COnyZfML.js} +2 -2
- package/dist/{protected-model-port-DqAH1Z2M.js.map → protected-model-port-COnyZfML.js.map} +1 -1
- package/dist/{runtime-Ut1pkd2n.js → runtime-Ce39zfqn.js} +145 -53
- package/dist/runtime-Ce39zfqn.js.map +1 -0
- package/dist/{sandbox-events-DeI5xX8P.js → sandbox-events-Yhd1GYWl.js} +4 -2
- package/dist/sandbox-events-Yhd1GYWl.js.map +1 -0
- package/dist/spawn-journal-B_mu_oH2.js +857 -0
- package/dist/spawn-journal-B_mu_oH2.js.map +1 -0
- package/dist/{structural-rollout-CVY_0hJp.js → structural-rollout-DLwW69R4.js} +4 -4
- package/dist/structural-rollout-DLwW69R4.js.map +1 -0
- package/dist/{supervise-BUR9ByF7.js → supervise-U7_K5yuV.js} +2598 -821
- package/dist/supervise-U7_K5yuV.js.map +1 -0
- package/dist/{supervisor-BBbPBXpe.js → supervisor-CAXiCKnX.js} +2808 -973
- package/dist/supervisor-CAXiCKnX.js.map +1 -0
- package/dist/testing.js +98 -76
- package/dist/testing.js.map +1 -1
- package/dist/{workspace-archive-DXzJq7WP.js → workspace-archive-CqPIijFA.js} +2 -2
- package/dist/{workspace-archive-DXzJq7WP.js.map → workspace-archive-CqPIijFA.js.map} +1 -1
- package/package.json +1 -1
- package/dist/environment-provider-CCaEhA-l.js.map +0 -1
- package/dist/improvement-cycle-C1cmjvPD.js.map +0 -1
- package/dist/knowledge-DOzbywZT.js.map +0 -1
- package/dist/prepare-BHQBb02e.js.map +0 -1
- package/dist/runtime-Ut1pkd2n.js.map +0 -1
- package/dist/sandbox-events-DeI5xX8P.js.map +0 -1
- package/dist/spawn-journal-DCPbicXB.js +0 -457
- package/dist/spawn-journal-DCPbicXB.js.map +0 -1
- package/dist/structural-rollout-CVY_0hJp.js.map +0 -1
- package/dist/supervise-BUR9ByF7.js.map +0 -1
- package/dist/supervisor-BBbPBXpe.js.map +0 -1
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
import { u as ValidationError } from "./errors-DEAvWQPy.js";
|
|
2
|
+
import { p as zeroTokenUsage } from "./util-Cc9g9Y-o.js";
|
|
3
|
+
import { isToolSpan, toolSpansToTraceAnalysisStore } from "@tangle-network/agent-eval";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
//#region src/runtime/supervise/snapshot.ts
|
|
6
|
+
/** Deeply detach and freeze untrusted data at a runtime decision boundary. The clone prevents the
|
|
7
|
+
* caller from mutating it later; the freeze prevents downstream code from mutating the snapshot. */
|
|
8
|
+
function detachedSnapshot(value, context) {
|
|
9
|
+
try {
|
|
10
|
+
return deepFreeze(structuredClone(value));
|
|
11
|
+
} catch (error) {
|
|
12
|
+
throw new ValidationError(`${context}: input must be structured-cloneable`, { cause: error });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function deepFreeze(value, seen = /* @__PURE__ */ new Set()) {
|
|
16
|
+
if (value === null || typeof value !== "object" || seen.has(value)) return value;
|
|
17
|
+
seen.add(value);
|
|
18
|
+
for (const child of Object.values(value)) deepFreeze(child, seen);
|
|
19
|
+
return Object.freeze(value);
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/durable/content-address.ts
|
|
23
|
+
/** Stable content address shared by result and trace artifacts. */
|
|
24
|
+
function contentAddress(artifact) {
|
|
25
|
+
return `sha256:${createHash("sha256").update(stableStringify(artifact), "utf-8").digest("hex")}`;
|
|
26
|
+
}
|
|
27
|
+
function stableStringify(value) {
|
|
28
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
29
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
30
|
+
return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/runtime/supervise/trace-evidence.ts
|
|
34
|
+
/**
|
|
35
|
+
* Durable structured tool evidence for supervised workers.
|
|
36
|
+
*
|
|
37
|
+
* The executor-owned `TraceSource` is collected before teardown, snapshotted into the shared
|
|
38
|
+
* content-addressed blob store, and only then referenced by a settlement. Analysis reconstructs
|
|
39
|
+
* agent-eval's bounded `TraceAnalysisStore` from those exact bytes. Worker output is never accepted
|
|
40
|
+
* as a substitute for tool evidence.
|
|
41
|
+
*
|
|
42
|
+
* @experimental
|
|
43
|
+
*/
|
|
44
|
+
/** Schema version for content-addressed worker tool-trace artifacts. */
|
|
45
|
+
const WORKER_TOOL_TRACE_SCHEMA_VERSION = 1;
|
|
46
|
+
/** Structural guard for the official bounded trace-analysis read contract. */
|
|
47
|
+
function isTraceAnalysisStore(value) {
|
|
48
|
+
if (value === null || typeof value !== "object") return false;
|
|
49
|
+
const store = value;
|
|
50
|
+
return typeof store.getOverview === "function" && typeof store.queryTraces === "function" && typeof store.countTraces === "function" && typeof store.viewTrace === "function" && typeof store.viewSpans === "function" && typeof store.searchTrace === "function" && typeof store.searchSpan === "function";
|
|
51
|
+
}
|
|
52
|
+
/** Collect and persist one executor's structured tool trace without changing its task outcome. */
|
|
53
|
+
async function captureWorkerTraceEvidence(readSource, blobs, executed) {
|
|
54
|
+
if (!executed) return unavailable("execution-did-not-start");
|
|
55
|
+
if (readSource === void 0) return unavailable("executor-did-not-expose-trace-source");
|
|
56
|
+
let source;
|
|
57
|
+
try {
|
|
58
|
+
source = readSource();
|
|
59
|
+
} catch {
|
|
60
|
+
return unavailable("trace-source-unavailable");
|
|
61
|
+
}
|
|
62
|
+
if (source === void 0) return unavailable("trace-source-unavailable");
|
|
63
|
+
let collected;
|
|
64
|
+
try {
|
|
65
|
+
collected = await source.collect();
|
|
66
|
+
} catch {
|
|
67
|
+
return unavailable("trace-collection-failed");
|
|
68
|
+
}
|
|
69
|
+
if (!Array.isArray(collected) || collected.some((span) => !isToolSpanValue(span))) return unavailable("invalid-tool-spans");
|
|
70
|
+
if (collected.length === 0) return unavailable("no-tool-spans-captured");
|
|
71
|
+
let artifact;
|
|
72
|
+
let traceRef;
|
|
73
|
+
try {
|
|
74
|
+
const snapshot = structuredClone(collected);
|
|
75
|
+
toolSpansToTraceAnalysisStore(snapshot);
|
|
76
|
+
artifact = {
|
|
77
|
+
schemaVersion: 1,
|
|
78
|
+
spans: snapshot
|
|
79
|
+
};
|
|
80
|
+
traceRef = contentAddress(artifact);
|
|
81
|
+
} catch {
|
|
82
|
+
return unavailable("invalid-tool-spans");
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
await blobs.put(traceRef, artifact);
|
|
86
|
+
} catch {
|
|
87
|
+
return unavailable("trace-persistence-failed");
|
|
88
|
+
}
|
|
89
|
+
return Object.freeze({
|
|
90
|
+
status: "available",
|
|
91
|
+
traceRef,
|
|
92
|
+
spanCount: artifact.spans.length
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/** Rehydrate exact persisted spans through agent-eval's one bounded trace-analysis adapter. */
|
|
96
|
+
async function workerTraceAnalysisStore(evidence, blobs) {
|
|
97
|
+
if (evidence.status === "unavailable") return toolSpansToTraceAnalysisStore(void 0);
|
|
98
|
+
const raw = await blobs.get(evidence.traceRef);
|
|
99
|
+
if (raw === void 0) throw new ValidationError(`worker trace blob '${evidence.traceRef}' is missing; the settlement evidence is incomplete`);
|
|
100
|
+
const artifact = parseWorkerToolTraceArtifact(raw, evidence.traceRef);
|
|
101
|
+
if (artifact.spans.length !== evidence.spanCount) throw new ValidationError(`worker trace blob '${evidence.traceRef}' has ${artifact.spans.length} spans but its settlement records ${evidence.spanCount}`);
|
|
102
|
+
return toolSpansToTraceAnalysisStore(artifact.spans);
|
|
103
|
+
}
|
|
104
|
+
/** Validate a stored trace artifact before an analyst or replay trusts it. */
|
|
105
|
+
function parseWorkerToolTraceArtifact(value, traceRef = "<unknown>") {
|
|
106
|
+
if (value === null || typeof value !== "object" || value.schemaVersion !== 1 || !Array.isArray(value.spans)) throw new ValidationError(`worker trace blob '${traceRef}' is not a version-1 tool trace artifact`);
|
|
107
|
+
const spans = value.spans;
|
|
108
|
+
if (spans.length === 0) return {
|
|
109
|
+
schemaVersion: 1,
|
|
110
|
+
spans
|
|
111
|
+
};
|
|
112
|
+
if (spans.some((span) => !isToolSpanValue(span))) throw new ValidationError(`worker trace blob '${traceRef}' contains a non-tool span`);
|
|
113
|
+
return {
|
|
114
|
+
schemaVersion: 1,
|
|
115
|
+
spans
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function unavailable(reason) {
|
|
119
|
+
return Object.freeze({
|
|
120
|
+
status: "unavailable",
|
|
121
|
+
reason
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function isToolSpanValue(value) {
|
|
125
|
+
try {
|
|
126
|
+
return isToolSpan(value);
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/runtime/supervise/tree-key.ts
|
|
133
|
+
/** Runtime tag used only for a child that owns another supervised scope. */
|
|
134
|
+
const driverRuntime = "driver";
|
|
135
|
+
const nestedDriverTreeOwners = /* @__PURE__ */ new WeakSet();
|
|
136
|
+
/** Canonical journal key for the scope owned by one nested driver node. */
|
|
137
|
+
function nestedDriverTreeRoot(parentTreeRoot, driverNodeId) {
|
|
138
|
+
return `${parentTreeRoot}/${driverNodeId}`;
|
|
139
|
+
}
|
|
140
|
+
/** Privately mark the exact recursive executor that will create a nested journal tree. */
|
|
141
|
+
function attestNestedDriverTreeOwner(owner) {
|
|
142
|
+
nestedDriverTreeOwners.add(owner);
|
|
143
|
+
return owner;
|
|
144
|
+
}
|
|
145
|
+
/** Return the canonical owned tree only for an exact privately marked recursive executor. */
|
|
146
|
+
function runtimeOwnedNestedDriverTreeRoot(owner, parentTreeRoot, driverNodeId) {
|
|
147
|
+
return nestedDriverTreeOwners.has(owner) ? nestedDriverTreeRoot(parentTreeRoot, driverNodeId) : void 0;
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region src/durable/jsonl-file.ts
|
|
151
|
+
/** Parse an append-only JSONL file without treating a torn final write as committed data.
|
|
152
|
+
* A malformed newline-terminated or non-final record is corruption and fails loud. */
|
|
153
|
+
function parseCommittedJsonLines(text, source) {
|
|
154
|
+
const lines = text.split("\n");
|
|
155
|
+
const finalIndex = lines.length - 1;
|
|
156
|
+
const records = [];
|
|
157
|
+
for (const [index, line] of lines.entries()) {
|
|
158
|
+
if (line.length === 0) continue;
|
|
159
|
+
try {
|
|
160
|
+
records.push(JSON.parse(line));
|
|
161
|
+
} catch (cause) {
|
|
162
|
+
if (index === finalIndex && !text.endsWith("\n")) break;
|
|
163
|
+
throw new Error(`${source}: malformed JSONL record at line ${index + 1}`, { cause });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return records;
|
|
167
|
+
}
|
|
168
|
+
/** FileHandle.write may legally make a short write. Loop until every byte is appended. */
|
|
169
|
+
async function writeAllBytes(handle, value) {
|
|
170
|
+
const bytes = typeof value === "string" ? Buffer.from(value) : Buffer.from(value);
|
|
171
|
+
let offset = 0;
|
|
172
|
+
while (offset < bytes.byteLength) {
|
|
173
|
+
const { bytesWritten } = await handle.write(bytes, offset, bytes.byteLength - offset, null);
|
|
174
|
+
if (bytesWritten <= 0) throw new Error(`append-only file write made no progress at byte ${offset}`);
|
|
175
|
+
offset += bytesWritten;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** Prepare a recovered JSONL file for its next append. A valid unterminated final value is retained
|
|
179
|
+
* and needs a separator; an invalid unterminated tail was never committed and is truncated. */
|
|
180
|
+
async function prepareJsonlAppend(path) {
|
|
181
|
+
const fs = await import("node:fs/promises");
|
|
182
|
+
let bytes;
|
|
183
|
+
try {
|
|
184
|
+
bytes = await fs.readFile(path);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (isNoEntError$1(error)) return false;
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
if (bytes.byteLength === 0 || bytes[bytes.byteLength - 1] === 10) return false;
|
|
190
|
+
const lastNewline = bytes.lastIndexOf(10);
|
|
191
|
+
const tail = bytes.subarray(lastNewline + 1).toString("utf8");
|
|
192
|
+
try {
|
|
193
|
+
JSON.parse(tail);
|
|
194
|
+
return true;
|
|
195
|
+
} catch {
|
|
196
|
+
await fs.truncate(path, lastNewline + 1);
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function isNoEntError$1(error) {
|
|
201
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/durable/spawn-journal.ts
|
|
205
|
+
/**
|
|
206
|
+
*
|
|
207
|
+
* Event-sourced spawn journal for the recursive execution atom (build steps 3 + 7).
|
|
208
|
+
*
|
|
209
|
+
* The supervision tree is journaled as an append-only event log: every `spawned`,
|
|
210
|
+
* `settled`, and `cancelled` is recorded AFTER it is observed-committed (never
|
|
211
|
+
* speculative), mirroring `ConversationJournal`'s begin/append/load shape. The log
|
|
212
|
+
* holds only the THIN decision record — ids, parentage, budget, the spend a decision
|
|
213
|
+
* consumed, and a content-addressed `outRef`. The payloads the driver branched on
|
|
214
|
+
* (the `out` artifacts) live in a separate `ResultBlobStore`, keyed by `outRef`, so
|
|
215
|
+
* the journal stays small (decisions) and replay rehydrates the exact `Settled` from
|
|
216
|
+
* the blob store (evidence). This is the decision/payload split the replay argument
|
|
217
|
+
* rests on (B1/B2).
|
|
218
|
+
*
|
|
219
|
+
* Replay determinism (B2): `seq` is the monotonic cursor order `scope.next()` yielded
|
|
220
|
+
* each settlement — NOT wall-clock. `replaySpawnTree` sorts strictly by `seq` before
|
|
221
|
+
* touching the blob store, so the order in which rehydration `get`s resolve can never
|
|
222
|
+
* reorder the replayed `Settled[]`; the result is identical regardless of blob latency.
|
|
223
|
+
*
|
|
224
|
+
* @experimental
|
|
225
|
+
*/
|
|
226
|
+
/**
|
|
227
|
+
* Mint the content-addressed `outRef` for a result artifact: `sha256:<hex>` over a
|
|
228
|
+
* stable JSON encoding. Producers call this to derive the `outRef` they journal and
|
|
229
|
+
* `put`; the FS/in-mem stores re-derive it on `put` to verify the supplied ref
|
|
230
|
+
* matches (fail loud on a mismatch — a forged ref breaks the replay invariant).
|
|
231
|
+
*
|
|
232
|
+
* Stable encoding: object keys are sorted recursively so two structurally-equal
|
|
233
|
+
* artifacts hash identically regardless of key insertion order.
|
|
234
|
+
*/
|
|
235
|
+
/**
|
|
236
|
+
* In-memory `ResultBlobStore`. Content-addressed: `put` verifies the supplied
|
|
237
|
+
* `outRef` matches the artifact's hash so a stale/forged ref fails loud rather than
|
|
238
|
+
* silently rehydrating the wrong payload. Idempotent on an identical re-put.
|
|
239
|
+
*/
|
|
240
|
+
var InMemoryResultBlobStore = class {
|
|
241
|
+
blobs = /* @__PURE__ */ new Map();
|
|
242
|
+
async put(outRef, artifact) {
|
|
243
|
+
assertContentAddress(outRef, artifact);
|
|
244
|
+
this.blobs.set(outRef, artifact);
|
|
245
|
+
}
|
|
246
|
+
async get(outRef) {
|
|
247
|
+
return this.blobs.has(outRef) ? this.blobs.get(outRef) : void 0;
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
/**
|
|
251
|
+
* FS `ResultBlobStore`. One JSON file per artifact under `dir`, named by a
|
|
252
|
+
* filesystem-safe encoding of the `outRef` (`sha256:<hex>` → `sha256-<hex>.json`).
|
|
253
|
+
* `put` fsyncs so a crash between writes never loses an acknowledged blob.
|
|
254
|
+
*/
|
|
255
|
+
var FileResultBlobStore = class {
|
|
256
|
+
dir;
|
|
257
|
+
constructor(dir) {
|
|
258
|
+
this.dir = dir;
|
|
259
|
+
}
|
|
260
|
+
async put(outRef, artifact) {
|
|
261
|
+
assertContentAddress(outRef, artifact);
|
|
262
|
+
const fs = await import("node:fs/promises");
|
|
263
|
+
await fs.mkdir(this.dir, { recursive: true });
|
|
264
|
+
const fh = await fs.open(this.blobPath(outRef), "w");
|
|
265
|
+
try {
|
|
266
|
+
await fh.write(JSON.stringify(artifact));
|
|
267
|
+
await fh.sync();
|
|
268
|
+
} finally {
|
|
269
|
+
await fh.close();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async get(outRef) {
|
|
273
|
+
const fs = await import("node:fs/promises");
|
|
274
|
+
let text;
|
|
275
|
+
try {
|
|
276
|
+
text = await fs.readFile(this.blobPath(outRef), "utf8");
|
|
277
|
+
} catch (err) {
|
|
278
|
+
if (isNoEntError(err)) return void 0;
|
|
279
|
+
throw err;
|
|
280
|
+
}
|
|
281
|
+
return JSON.parse(text);
|
|
282
|
+
}
|
|
283
|
+
blobPath(outRef) {
|
|
284
|
+
return `${this.dir}/${outRef.replace(/:/g, "-")}.json`;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
function assertContentAddress(outRef, artifact) {
|
|
288
|
+
const expected = contentAddress(artifact);
|
|
289
|
+
if (outRef !== expected) throw new Error(`blob outRef '${outRef}' does not match the artifact content hash '${expected}'; a content-addressed store refuses a mismatched ref (breaks the replay invariant)`);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* In-memory `SpawnJournal`. Appends are observed-committed only; the impl enforces
|
|
293
|
+
* the corruption guards a durable replay rests on:
|
|
294
|
+
* - an event before `beginTree` is a corrupted tree (fail loud),
|
|
295
|
+
* - a duplicate `seq` within a tree is a corrupted cursor (fail loud) — two
|
|
296
|
+
* settlements cannot share the cursor position replay orders by.
|
|
297
|
+
*/
|
|
298
|
+
var InMemorySpawnJournal = class {
|
|
299
|
+
trees = /* @__PURE__ */ new Map();
|
|
300
|
+
async loadTree(root) {
|
|
301
|
+
const tree = this.trees.get(root);
|
|
302
|
+
if (!tree) return void 0;
|
|
303
|
+
return tree.events.map((ev) => ({ ...ev }));
|
|
304
|
+
}
|
|
305
|
+
async beginTree(root, at) {
|
|
306
|
+
const existing = this.trees.get(root);
|
|
307
|
+
if (existing) {
|
|
308
|
+
if (existing.begunAt !== at) throw new Error(`spawn tree '${root}' already begun at ${existing.begunAt}; refusing to overwrite with ${at}`);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
this.trees.set(root, {
|
|
312
|
+
begunAt: at,
|
|
313
|
+
events: []
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
async appendEvent(root, ev) {
|
|
317
|
+
const tree = this.trees.get(root);
|
|
318
|
+
if (!tree) throw new Error(`appendEvent called for unknown spawn tree '${root}'; call beginTree first`);
|
|
319
|
+
assertSeqUnique(root, tree.events, ev);
|
|
320
|
+
tree.events.push({ ...ev });
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
/**
|
|
324
|
+
* JSONL on disk. One line per record: the first record is `begin`, subsequent records
|
|
325
|
+
* are `event` envelopes wrapping a `SpawnEvent`. `loadTree` replays the whole file,
|
|
326
|
+
* filtering by `root`, and applies the same begin-precedes-events + unique-seq
|
|
327
|
+
* corruption guards as the in-memory impl. Each append fsyncs so a crash between
|
|
328
|
+
* writes never loses an acknowledged event.
|
|
329
|
+
*/
|
|
330
|
+
var FileSpawnJournal = class {
|
|
331
|
+
path;
|
|
332
|
+
appendTail = Promise.resolve();
|
|
333
|
+
constructor(path) {
|
|
334
|
+
this.path = path;
|
|
335
|
+
}
|
|
336
|
+
async loadTree(root) {
|
|
337
|
+
const fs = await import("node:fs/promises");
|
|
338
|
+
let text;
|
|
339
|
+
try {
|
|
340
|
+
text = await fs.readFile(this.path, "utf8");
|
|
341
|
+
} catch (err) {
|
|
342
|
+
if (isNoEntError(err)) return void 0;
|
|
343
|
+
throw err;
|
|
344
|
+
}
|
|
345
|
+
let begun = false;
|
|
346
|
+
const events = [];
|
|
347
|
+
for (const record of parseCommittedJsonLines(text, this.path)) {
|
|
348
|
+
if (record.root !== root) continue;
|
|
349
|
+
if (record.kind === "begin") begun = true;
|
|
350
|
+
else {
|
|
351
|
+
if (!begun) throw new Error(`spawn journal corrupted: event for tree '${root}' precedes its begin record`);
|
|
352
|
+
assertSeqUnique(root, events, record.event);
|
|
353
|
+
events.push(record.event);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return begun ? events : void 0;
|
|
357
|
+
}
|
|
358
|
+
async beginTree(root, at) {
|
|
359
|
+
const existing = await this.loadTreeBegin(root);
|
|
360
|
+
if (existing) {
|
|
361
|
+
if (existing !== at) throw new Error(`spawn tree '${root}' already begun in ${this.path} at ${existing}; refusing to overwrite with ${at}`);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
await this.appendRecord({
|
|
365
|
+
kind: "begin",
|
|
366
|
+
root,
|
|
367
|
+
at
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
async appendEvent(root, ev) {
|
|
371
|
+
const events = await this.loadTree(root);
|
|
372
|
+
if (events === void 0) throw new Error(`appendEvent called for unknown spawn tree '${root}'; call beginTree first`);
|
|
373
|
+
assertSeqUnique(root, events, ev);
|
|
374
|
+
await this.appendRecord({
|
|
375
|
+
kind: "event",
|
|
376
|
+
root,
|
|
377
|
+
event: ev
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
async loadTreeBegin(root) {
|
|
381
|
+
const fs = await import("node:fs/promises");
|
|
382
|
+
let text;
|
|
383
|
+
try {
|
|
384
|
+
text = await fs.readFile(this.path, "utf8");
|
|
385
|
+
} catch (err) {
|
|
386
|
+
if (isNoEntError(err)) return void 0;
|
|
387
|
+
throw err;
|
|
388
|
+
}
|
|
389
|
+
for (const record of parseCommittedJsonLines(text, this.path)) if (record.root === root && record.kind === "begin") return record.at;
|
|
390
|
+
}
|
|
391
|
+
async appendRecord(record) {
|
|
392
|
+
const append = this.appendTail.then(() => this.writeRecord(record));
|
|
393
|
+
this.appendTail = append.catch(() => void 0);
|
|
394
|
+
return append;
|
|
395
|
+
}
|
|
396
|
+
async writeRecord(record) {
|
|
397
|
+
const fs = await import("node:fs/promises");
|
|
398
|
+
const path = await import("node:path");
|
|
399
|
+
await fs.mkdir(path.dirname(this.path), { recursive: true });
|
|
400
|
+
const needsSeparator = await prepareJsonlAppend(this.path);
|
|
401
|
+
const fh = await fs.open(this.path, "a");
|
|
402
|
+
try {
|
|
403
|
+
await writeAllBytes(fh, `${needsSeparator ? "\n" : ""}${JSON.stringify(record)}\n`);
|
|
404
|
+
await fh.sync();
|
|
405
|
+
} finally {
|
|
406
|
+
await fh.close();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
/**
|
|
411
|
+
* Load every journal tree owned by one recursive supervision run and flatten its nodes/events.
|
|
412
|
+
*
|
|
413
|
+
* Nested driver tree keys are a Runtime implementation detail; callers should use this reader
|
|
414
|
+
* instead of deriving or scanning keys themselves. The reader follows only the explicit
|
|
415
|
+
* `ownedTreeRoot` written after Runtime privately attested a recursive executor; the open runtime
|
|
416
|
+
* string `driver` is never treated as ownership. Legacy records without `ownedTreeRoot` are
|
|
417
|
+
* intentionally treated as leaves rather than guessing or scanning convention-derived keys.
|
|
418
|
+
* This preserves each tree's independent cursor namespace on flattened events.
|
|
419
|
+
* A driver whose subtree was never begun is reported in `missingTrees`; any spawned non-root node
|
|
420
|
+
* without a terminal record is reported in `inDoubt`, matching resume's conservative lost-work
|
|
421
|
+
* interpretation.
|
|
422
|
+
*
|
|
423
|
+
* This is a cold/quiescent reader, not a transaction across an actively mutating file. Every value
|
|
424
|
+
* returned is a detached immutable snapshot, so later journal writes or caller mutation cannot
|
|
425
|
+
* change the result already observed.
|
|
426
|
+
*/
|
|
427
|
+
async function loadSpawnForest(journal, root) {
|
|
428
|
+
const rootEvents = await journal.loadTree(root);
|
|
429
|
+
if (rootEvents === void 0) throw new Error(`loadSpawnForest: no journaled tree for root '${root}'`);
|
|
430
|
+
const trees = [];
|
|
431
|
+
const nodes = [];
|
|
432
|
+
const events = [];
|
|
433
|
+
const inDoubt = [];
|
|
434
|
+
const missingTrees = [];
|
|
435
|
+
const visited = /* @__PURE__ */ new Set();
|
|
436
|
+
const queue = [{
|
|
437
|
+
root,
|
|
438
|
+
events: rootEvents
|
|
439
|
+
}];
|
|
440
|
+
for (let cursor = 0; cursor < queue.length; cursor += 1) {
|
|
441
|
+
const current = queue[cursor];
|
|
442
|
+
if (visited.has(current.root)) throw new Error(`loadSpawnForest: recursive journal tree '${current.root}' was reached twice`);
|
|
443
|
+
visited.add(current.root);
|
|
444
|
+
const stableEvents = detachedSnapshot(current.events, `loadSpawnForest tree ${JSON.stringify(current.root)}`);
|
|
445
|
+
const view = detachedSnapshot(materializeTreeView([...stableEvents]), `loadSpawnForest view ${JSON.stringify(current.root)}`);
|
|
446
|
+
trees.push({
|
|
447
|
+
root: current.root,
|
|
448
|
+
...current.ownerNodeId === void 0 ? {} : { ownerNodeId: current.ownerNodeId },
|
|
449
|
+
...current.parentTreeRoot === void 0 ? {} : { parentTreeRoot: current.parentTreeRoot },
|
|
450
|
+
events: stableEvents,
|
|
451
|
+
view
|
|
452
|
+
});
|
|
453
|
+
nodes.push(...view.nodes.map((node) => ({
|
|
454
|
+
treeRoot: current.root,
|
|
455
|
+
...node
|
|
456
|
+
})));
|
|
457
|
+
events.push(...stableEvents.map((event) => ({
|
|
458
|
+
treeRoot: current.root,
|
|
459
|
+
event
|
|
460
|
+
})));
|
|
461
|
+
const terminal = /* @__PURE__ */ new Set();
|
|
462
|
+
for (const event of stableEvents) if (event.kind === "settled" || event.kind === "cancelled") terminal.add(event.id);
|
|
463
|
+
const spawns = stableEvents.filter((event) => event.kind === "spawned").sort((a, b) => a.seq - b.seq || a.id.localeCompare(b.id));
|
|
464
|
+
for (const spawn of spawns) {
|
|
465
|
+
if (spawn.parent !== void 0 && !terminal.has(spawn.id)) inDoubt.push({
|
|
466
|
+
treeRoot: current.root,
|
|
467
|
+
nodeId: spawn.id,
|
|
468
|
+
label: spawn.label,
|
|
469
|
+
runtime: spawn.runtime
|
|
470
|
+
});
|
|
471
|
+
if (spawn.ownedTreeRoot === void 0) continue;
|
|
472
|
+
const expectedRoot = nestedDriverTreeRoot(current.root, spawn.id);
|
|
473
|
+
if (spawn.ownedTreeRoot !== expectedRoot) throw new Error(`loadSpawnForest: node '${spawn.id}' owns non-canonical tree '${spawn.ownedTreeRoot}'; expected '${expectedRoot}'`);
|
|
474
|
+
const nestedRoot = spawn.ownedTreeRoot;
|
|
475
|
+
const nestedEvents = await journal.loadTree(nestedRoot);
|
|
476
|
+
if (nestedEvents === void 0) {
|
|
477
|
+
missingTrees.push({
|
|
478
|
+
parentTreeRoot: current.root,
|
|
479
|
+
ownerNodeId: spawn.id,
|
|
480
|
+
root: nestedRoot
|
|
481
|
+
});
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
queue.push({
|
|
485
|
+
root: nestedRoot,
|
|
486
|
+
events: nestedEvents,
|
|
487
|
+
ownerNodeId: spawn.id,
|
|
488
|
+
parentTreeRoot: current.root
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return detachedSnapshot({
|
|
493
|
+
root,
|
|
494
|
+
trees,
|
|
495
|
+
nodes,
|
|
496
|
+
events,
|
|
497
|
+
inDoubt,
|
|
498
|
+
missingTrees
|
|
499
|
+
}, "loadSpawnForest result");
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Two `seq` namespaces share the journal: a `spawned` event's `seq` is the spawn ordinal
|
|
503
|
+
* (the order children were created), and a `settled`/`cancelled` event's `seq` is the
|
|
504
|
+
* monotonic CURSOR order `scope.next()` yielded that settlement (B2). The uniqueness
|
|
505
|
+
* replay rests on is the cursor namespace — two settlements cannot share the position
|
|
506
|
+
* replay orders by — so the guard checks only settled/cancelled events. A `spawned`
|
|
507
|
+
* ordinal legitimately equals a later `settled` cursor seq and is not a collision.
|
|
508
|
+
*/
|
|
509
|
+
function assertSeqUnique(root, events, ev) {
|
|
510
|
+
if (ev.kind === "materialized") {
|
|
511
|
+
if (events.some((event) => event.kind === "materialized" && event.id === ev.id)) throw new Error(`spawn journal corrupted: duplicate materialization receipt for node '${ev.id}' in tree '${root}'`);
|
|
512
|
+
if (!events.some((event) => event.kind === "spawned" && event.id === ev.id)) throw new Error(`spawn journal corrupted: materialization for node '${ev.id}' precedes its spawn in tree '${root}'`);
|
|
513
|
+
}
|
|
514
|
+
if (ev.kind === "execution-bound") {
|
|
515
|
+
if (events.some((event) => event.kind === "execution-bound" && event.id === ev.id && event.binding.attemptId === ev.binding.attemptId)) throw new Error(`spawn journal corrupted: duplicate execution binding for node '${ev.id}' attempt '${ev.binding.attemptId}' in tree '${root}'`);
|
|
516
|
+
if (!events.some((event) => event.kind === "materialized" && event.id === ev.id)) throw new Error(`spawn journal corrupted: execution binding for node '${ev.id}' precedes materialization in tree '${root}'`);
|
|
517
|
+
}
|
|
518
|
+
if (outsideCursorNamespace(ev)) return;
|
|
519
|
+
if (events.some((e) => !outsideCursorNamespace(e) && e.seq === ev.seq)) throw new Error(`spawn journal corrupted: duplicate cursor seq ${ev.seq} in tree '${root}'; the cursor order replay relies on is not unique`);
|
|
520
|
+
}
|
|
521
|
+
/** Node-CREATION and informational records — outside the cursor namespace whose uniqueness replay
|
|
522
|
+
* ordering rests on. The single predicate both the guard's sides read, so a new event kind is
|
|
523
|
+
* classified once. */
|
|
524
|
+
function outsideCursorNamespace(ev) {
|
|
525
|
+
return ev.kind === "spawned" || ev.kind === "waiting" || ev.kind === "metered" || ev.kind === "materialized" || ev.kind === "execution-bound";
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Re-feed a journaled spawn tree in strict `seq` order, rehydrating each settled
|
|
529
|
+
* child's `out` from the blob store by `outRef`, and return the `Settled[]` exactly
|
|
530
|
+
* as `scope.next()` originally delivered them.
|
|
531
|
+
*
|
|
532
|
+
* Determinism (B2): the events are sorted by `seq` BEFORE any blob `get`, so the
|
|
533
|
+
* replay order is the recorded cursor order regardless of how fast each rehydration
|
|
534
|
+
* resolves. `at` (wall-clock) is never a replay input. Fail loud on a tree that was
|
|
535
|
+
* never begun, a settled-done event missing its `outRef`, or a blob the store can't
|
|
536
|
+
* rehydrate — a silent gap would let `act` branch on the wrong evidence.
|
|
537
|
+
*/
|
|
538
|
+
async function replaySpawnTree(journal, blobs, root) {
|
|
539
|
+
const events = await journal.loadTree(root);
|
|
540
|
+
if (events === void 0) throw new Error(`replaySpawnTree: no journaled tree for root '${root}'`);
|
|
541
|
+
const ordered = [...events].sort((a, b) => a.seq - b.seq);
|
|
542
|
+
const labels = /* @__PURE__ */ new Map();
|
|
543
|
+
const assignmentIds = /* @__PURE__ */ new Map();
|
|
544
|
+
const identities = /* @__PURE__ */ new Map();
|
|
545
|
+
const materializations = /* @__PURE__ */ new Map();
|
|
546
|
+
const executionBindings = /* @__PURE__ */ new Map();
|
|
547
|
+
for (const ev of ordered) {
|
|
548
|
+
if (ev.kind === "spawned" || ev.kind === "waiting") labels.set(ev.id, ev.label);
|
|
549
|
+
if (ev.kind === "spawned" && ev.assignmentId !== void 0) assignmentIds.set(ev.id, ev.assignmentId);
|
|
550
|
+
if (ev.kind === "spawned" && ev.identity !== void 0) identities.set(ev.id, copyFrozenIdentity(ev.identity));
|
|
551
|
+
if (ev.kind === "materialized") materializations.set(ev.id, ev.receipt);
|
|
552
|
+
if (ev.kind === "execution-bound") {
|
|
553
|
+
const bindings = executionBindings.get(ev.id) ?? [];
|
|
554
|
+
bindings.push(ev.binding);
|
|
555
|
+
executionBindings.set(ev.id, bindings);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const handleFor = (id, status) => replayHandle(id, labels.get(id) ?? id, status, {
|
|
559
|
+
assignmentId: assignmentIds.get(id),
|
|
560
|
+
identity: identities.get(id),
|
|
561
|
+
materialization: materializations.get(id),
|
|
562
|
+
executionBindings: executionBindings.get(id)
|
|
563
|
+
});
|
|
564
|
+
const settlementTime = (at) => {
|
|
565
|
+
const settledAt = Date.parse(at);
|
|
566
|
+
return Number.isFinite(settledAt) ? { settledAt } : {};
|
|
567
|
+
};
|
|
568
|
+
const settled = [];
|
|
569
|
+
for (const ev of ordered) {
|
|
570
|
+
if (ev.kind === "spawned") continue;
|
|
571
|
+
if (ev.kind === "waiting") continue;
|
|
572
|
+
if (ev.kind === "metered") continue;
|
|
573
|
+
if (ev.kind === "materialized") continue;
|
|
574
|
+
if (ev.kind === "execution-bound") continue;
|
|
575
|
+
if (ev.kind === "woken") {
|
|
576
|
+
if (ev.by === "cancelled" || ev.outRef === void 0) {
|
|
577
|
+
settled.push({
|
|
578
|
+
kind: "down",
|
|
579
|
+
handle: handleFor(ev.id, "cancelled"),
|
|
580
|
+
reason: "wait cancelled",
|
|
581
|
+
infra: false,
|
|
582
|
+
restartCount: 0,
|
|
583
|
+
trace: {
|
|
584
|
+
status: "unavailable",
|
|
585
|
+
reason: "not-an-executor"
|
|
586
|
+
},
|
|
587
|
+
...settlementTime(ev.at),
|
|
588
|
+
seq: ev.seq
|
|
589
|
+
});
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
const outcome = await blobs.get(ev.outRef);
|
|
593
|
+
if (outcome === void 0) throw new Error(`replaySpawnTree: blob store has no wait outcome for outRef '${ev.outRef}' (node '${ev.id}', seq ${ev.seq})`);
|
|
594
|
+
settled.push({
|
|
595
|
+
kind: "done",
|
|
596
|
+
handle: handleFor(ev.id, "done"),
|
|
597
|
+
out: outcome,
|
|
598
|
+
outRef: ev.outRef,
|
|
599
|
+
spent: zeroSpend(),
|
|
600
|
+
trace: {
|
|
601
|
+
status: "unavailable",
|
|
602
|
+
reason: "not-an-executor"
|
|
603
|
+
},
|
|
604
|
+
...settlementTime(ev.at),
|
|
605
|
+
seq: ev.seq
|
|
606
|
+
});
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
if (ev.kind === "cancelled") {
|
|
610
|
+
settled.push({
|
|
611
|
+
kind: "down",
|
|
612
|
+
handle: handleFor(ev.id, "cancelled"),
|
|
613
|
+
reason: ev.reason,
|
|
614
|
+
infra: false,
|
|
615
|
+
restartCount: 0,
|
|
616
|
+
trace: {
|
|
617
|
+
status: "unavailable",
|
|
618
|
+
reason: "execution-did-not-start"
|
|
619
|
+
},
|
|
620
|
+
...settlementTime(ev.at),
|
|
621
|
+
seq: ev.seq
|
|
622
|
+
});
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (ev.status === "down") {
|
|
626
|
+
const trace = traceEvidenceFor(ev);
|
|
627
|
+
if (trace.status === "available") await workerTraceAnalysisStore(trace, blobs);
|
|
628
|
+
settled.push({
|
|
629
|
+
kind: "down",
|
|
630
|
+
handle: handleFor(ev.id, "failed"),
|
|
631
|
+
reason: ev.reason ?? ev.verdict?.notes ?? "child down",
|
|
632
|
+
infra: ev.infra === true,
|
|
633
|
+
restartCount: 0,
|
|
634
|
+
trace,
|
|
635
|
+
...settlementTime(ev.at),
|
|
636
|
+
seq: ev.seq
|
|
637
|
+
});
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if (ev.outRef === void 0) throw new Error(`replaySpawnTree: settled-done event for '${ev.id}' (seq ${ev.seq}) has no outRef; cannot rehydrate the result the driver branched on`);
|
|
641
|
+
const out = await blobs.get(ev.outRef);
|
|
642
|
+
if (out === void 0) throw new Error(`replaySpawnTree: blob store has no artifact for outRef '${ev.outRef}' (node '${ev.id}', seq ${ev.seq})`);
|
|
643
|
+
const trace = traceEvidenceFor(ev);
|
|
644
|
+
if (trace.status === "available") await workerTraceAnalysisStore(trace, blobs);
|
|
645
|
+
settled.push({
|
|
646
|
+
kind: "done",
|
|
647
|
+
handle: handleFor(ev.id, "done"),
|
|
648
|
+
out,
|
|
649
|
+
outRef: ev.outRef,
|
|
650
|
+
verdict: ev.verdict,
|
|
651
|
+
spent: ev.spent,
|
|
652
|
+
trace,
|
|
653
|
+
...settlementTime(ev.at),
|
|
654
|
+
seq: ev.seq
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
return settled;
|
|
658
|
+
}
|
|
659
|
+
function replayHandle(id, label, status, evidence = {}) {
|
|
660
|
+
return Object.freeze({
|
|
661
|
+
id,
|
|
662
|
+
label,
|
|
663
|
+
status,
|
|
664
|
+
...evidence.assignmentId === void 0 ? {} : { assignmentId: evidence.assignmentId },
|
|
665
|
+
...evidence.identity === void 0 ? {} : { identity: evidence.identity },
|
|
666
|
+
...evidence.materialization === void 0 ? {} : { materialization: evidence.materialization },
|
|
667
|
+
...evidence.executionBindings === void 0 ? {} : { executionBindings: Object.freeze([...evidence.executionBindings]) },
|
|
668
|
+
abort() {
|
|
669
|
+
throw new Error(`cannot abort node '${id}': replayed handles are terminal, not live`);
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
/** A replayed handle must not retain mutable references into the loaded journal event. */
|
|
674
|
+
function copyFrozenIdentity(identity) {
|
|
675
|
+
const correlation = identity.correlation === void 0 ? void 0 : Object.freeze({ ...identity.correlation });
|
|
676
|
+
return Object.freeze({
|
|
677
|
+
...identity.profileDigest === void 0 ? {} : { profileDigest: identity.profileDigest },
|
|
678
|
+
...identity.taskDigest === void 0 ? {} : { taskDigest: identity.taskDigest },
|
|
679
|
+
...identity.candidateDigest === void 0 ? {} : { candidateDigest: identity.candidateDigest },
|
|
680
|
+
...correlation === void 0 ? {} : { correlation }
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Materialize a recorded `TreeView` from a journaled event list for inspection. Folds
|
|
685
|
+
* `spawned`/`settled`/`cancelled` into a per-node snapshot in `seq` order, then adds each
|
|
686
|
+
* `metered` event's driver-inference spend onto its node in a separate additive pass so the view
|
|
687
|
+
* matches the recorded cursor. It does not recover live executors or driver state after restart.
|
|
688
|
+
*/
|
|
689
|
+
function materializeTreeView(events) {
|
|
690
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
691
|
+
let root;
|
|
692
|
+
const spawns = events.filter((ev) => ev.kind === "spawned" || ev.kind === "waiting").sort((a, b) => a.seq - b.seq);
|
|
693
|
+
const settlements = events.filter((ev) => ev.kind !== "spawned" && ev.kind !== "waiting" && ev.kind !== "metered" && ev.kind !== "materialized" && ev.kind !== "execution-bound").sort((a, b) => a.seq - b.seq);
|
|
694
|
+
for (const ev of spawns) {
|
|
695
|
+
if (ev.kind === "waiting") {
|
|
696
|
+
nodes.set(ev.id, {
|
|
697
|
+
id: ev.id,
|
|
698
|
+
parent: ev.parent,
|
|
699
|
+
label: ev.label,
|
|
700
|
+
status: "waiting",
|
|
701
|
+
runtime: "wait",
|
|
702
|
+
budget: {
|
|
703
|
+
maxIterations: 0,
|
|
704
|
+
maxTokens: 0
|
|
705
|
+
},
|
|
706
|
+
spent: zeroSpend()
|
|
707
|
+
});
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
if (ev.parent === void 0 && root === void 0) root = ev.id;
|
|
711
|
+
nodes.set(ev.id, {
|
|
712
|
+
id: ev.id,
|
|
713
|
+
parent: ev.parent,
|
|
714
|
+
label: ev.label,
|
|
715
|
+
status: "pending",
|
|
716
|
+
runtime: ev.runtime,
|
|
717
|
+
budget: ev.budget,
|
|
718
|
+
...ev.ownedTreeRoot === void 0 ? {} : { ownedTreeRoot: ev.ownedTreeRoot },
|
|
719
|
+
...ev.assignmentId === void 0 ? {} : { assignmentId: ev.assignmentId },
|
|
720
|
+
...ev.identity ? { identity: ev.identity } : {},
|
|
721
|
+
spent: zeroSpend()
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
for (const ev of settlements) if (ev.kind === "settled") {
|
|
725
|
+
const node = requireNode(nodes, ev.id);
|
|
726
|
+
node.status = ev.status === "done" ? "done" : "failed";
|
|
727
|
+
node.spent = ev.spent;
|
|
728
|
+
node.outRef = ev.outRef;
|
|
729
|
+
node.trace = traceEvidenceFor(ev);
|
|
730
|
+
const settledAt = Date.parse(ev.at);
|
|
731
|
+
if (Number.isFinite(settledAt)) node.settledAt = settledAt;
|
|
732
|
+
} else if (ev.kind === "woken") {
|
|
733
|
+
const node = requireNode(nodes, ev.id);
|
|
734
|
+
node.status = ev.by === "cancelled" ? "cancelled" : "done";
|
|
735
|
+
node.outRef = ev.outRef;
|
|
736
|
+
node.trace = {
|
|
737
|
+
status: "unavailable",
|
|
738
|
+
reason: "not-an-executor"
|
|
739
|
+
};
|
|
740
|
+
const settledAt = Date.parse(ev.at);
|
|
741
|
+
if (Number.isFinite(settledAt)) node.settledAt = settledAt;
|
|
742
|
+
} else {
|
|
743
|
+
const node = requireNode(nodes, ev.id);
|
|
744
|
+
node.status = "cancelled";
|
|
745
|
+
node.trace = {
|
|
746
|
+
status: "unavailable",
|
|
747
|
+
reason: "execution-did-not-start"
|
|
748
|
+
};
|
|
749
|
+
const settledAt = Date.parse(ev.at);
|
|
750
|
+
if (Number.isFinite(settledAt)) node.settledAt = settledAt;
|
|
751
|
+
}
|
|
752
|
+
for (const ev of events) {
|
|
753
|
+
if (ev.kind !== "materialized") continue;
|
|
754
|
+
const node = requireNode(nodes, ev.id);
|
|
755
|
+
node.materialization = ev.receipt;
|
|
756
|
+
node.runtime = ev.receipt.runtime;
|
|
757
|
+
}
|
|
758
|
+
for (const ev of events) {
|
|
759
|
+
if (ev.kind !== "execution-bound") continue;
|
|
760
|
+
const node = requireNode(nodes, ev.id);
|
|
761
|
+
node.executionBindings ??= [];
|
|
762
|
+
node.executionBindings.push(ev.binding);
|
|
763
|
+
}
|
|
764
|
+
for (const ev of events) {
|
|
765
|
+
if (ev.kind !== "metered") continue;
|
|
766
|
+
const node = requireNode(nodes, ev.id);
|
|
767
|
+
node.spent = addJournalSpend(node.spent, ev.spend);
|
|
768
|
+
}
|
|
769
|
+
const snapshots = [...nodes.values()].map(freezeSnapshot);
|
|
770
|
+
return {
|
|
771
|
+
root: root ?? snapshots[0]?.id ?? "",
|
|
772
|
+
nodes: snapshots,
|
|
773
|
+
inFlight: snapshots.filter((n) => n.status === "running" || n.status === "acquiring").length,
|
|
774
|
+
waiting: snapshots.filter((n) => n.status === "waiting").length
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* The waits a journaled tree shows as ARMED but never woken — what a resumed run re-arms with the
|
|
779
|
+
* ORIGINAL absolute deadline. Reading it from the journal (rather than from any live state) is
|
|
780
|
+
* what makes "SIGKILL a waiting tree, a new process keeps waiting to the same instant" true.
|
|
781
|
+
*/
|
|
782
|
+
function pendingWaits(events) {
|
|
783
|
+
const woken = /* @__PURE__ */ new Set();
|
|
784
|
+
for (const ev of events) if (ev.kind === "woken") woken.add(ev.id);
|
|
785
|
+
const pending = [];
|
|
786
|
+
for (const ev of events) {
|
|
787
|
+
if (ev.kind !== "waiting" || woken.has(ev.id)) continue;
|
|
788
|
+
pending.push({
|
|
789
|
+
id: ev.id,
|
|
790
|
+
label: ev.label,
|
|
791
|
+
spec: ev.spec,
|
|
792
|
+
armedAt: ev.armedAt,
|
|
793
|
+
ordinal: ev.seq
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
return pending.sort((a, b) => a.ordinal - b.ordinal);
|
|
797
|
+
}
|
|
798
|
+
function zeroSpend() {
|
|
799
|
+
return {
|
|
800
|
+
iterations: 0,
|
|
801
|
+
tokens: zeroTokenUsage(),
|
|
802
|
+
usd: 0,
|
|
803
|
+
ms: 0
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
/** Add a `metered` spend record onto a node's accumulated spend (per channel). */
|
|
807
|
+
function addJournalSpend(a, b) {
|
|
808
|
+
return {
|
|
809
|
+
iterations: a.iterations + b.iterations,
|
|
810
|
+
tokens: {
|
|
811
|
+
input: a.tokens.input + b.tokens.input,
|
|
812
|
+
output: a.tokens.output + b.tokens.output
|
|
813
|
+
},
|
|
814
|
+
...a.tokensKnown === false || b.tokensKnown === false ? { tokensKnown: false } : {},
|
|
815
|
+
usd: a.usd + b.usd,
|
|
816
|
+
...a.tokensKnown === false || b.tokensKnown === false ? { tokensKnown: false } : {},
|
|
817
|
+
...a.usdKnown === false || b.usdKnown === false ? { usdKnown: false } : {},
|
|
818
|
+
ms: a.ms + b.ms
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function requireNode(nodes, id) {
|
|
822
|
+
const node = nodes.get(id);
|
|
823
|
+
if (!node) throw new Error(`spawn journal corrupted: settle/cancel for node '${id}' with no prior spawn`);
|
|
824
|
+
return node;
|
|
825
|
+
}
|
|
826
|
+
function freezeSnapshot(node) {
|
|
827
|
+
return {
|
|
828
|
+
id: node.id,
|
|
829
|
+
parent: node.parent,
|
|
830
|
+
label: node.label,
|
|
831
|
+
status: node.status,
|
|
832
|
+
runtime: node.runtime,
|
|
833
|
+
budget: node.budget,
|
|
834
|
+
ownedTreeRoot: node.ownedTreeRoot,
|
|
835
|
+
assignmentId: node.assignmentId,
|
|
836
|
+
identity: node.identity,
|
|
837
|
+
materialization: node.materialization,
|
|
838
|
+
executionBindings: node.executionBindings === void 0 ? void 0 : Object.freeze([...node.executionBindings]),
|
|
839
|
+
spent: node.spent,
|
|
840
|
+
outRef: node.outRef,
|
|
841
|
+
trace: node.trace,
|
|
842
|
+
settledAt: node.settledAt
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
function traceEvidenceFor(event) {
|
|
846
|
+
return event.trace ?? {
|
|
847
|
+
status: "unavailable",
|
|
848
|
+
reason: "legacy-settlement-without-trace-evidence"
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
function isNoEntError(err) {
|
|
852
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
853
|
+
}
|
|
854
|
+
//#endregion
|
|
855
|
+
export { detachedSnapshot as S, captureWorkerTraceEvidence as _, loadSpawnForest as a, workerTraceAnalysisStore as b, replaySpawnTree as c, writeAllBytes as d, attestNestedDriverTreeOwner as f, WORKER_TOOL_TRACE_SCHEMA_VERSION as g, runtimeOwnedNestedDriverTreeRoot as h, InMemorySpawnJournal as i, parseCommittedJsonLines as l, nestedDriverTreeRoot as m, FileSpawnJournal as n, materializeTreeView as o, driverRuntime as p, InMemoryResultBlobStore as r, pendingWaits as s, FileResultBlobStore as t, prepareJsonlAppend as u, isTraceAnalysisStore as v, contentAddress as x, parseWorkerToolTraceArtifact as y };
|
|
856
|
+
|
|
857
|
+
//# sourceMappingURL=spawn-journal-B_mu_oH2.js.map
|