@tangle-network/agent-runtime 0.350.2 → 0.353.2

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 (95) hide show
  1. package/dist/{activation-CfJMyzEs.js → activation-BkRaG5t0.js} +4 -4
  2. package/dist/{activation-CfJMyzEs.js.map → activation-BkRaG5t0.js.map} +1 -1
  3. package/dist/{activation-DyiHrTlS.d.ts → activation-CFS31SWI.d.ts} +2 -2
  4. package/dist/agent.d.ts +1 -1
  5. package/dist/agent.js +3 -3
  6. package/dist/{bundle-Ze8pd07a.js → bundle-DeDkJbsr.js} +2 -2
  7. package/dist/{bundle-Ze8pd07a.js.map → bundle-DeDkJbsr.js.map} +1 -1
  8. package/dist/candidate-execution/index.js +4 -4
  9. package/dist/chat-turn.d.ts +2 -0
  10. package/dist/chat-turn.js +1 -0
  11. package/dist/{coordination-manager-BkVfWlMz.js → coordination-manager-BrRkc55z.js} +4 -4
  12. package/dist/{coordination-manager-BkVfWlMz.js.map → coordination-manager-BrRkc55z.js.map} +1 -1
  13. package/dist/{delegate-DgEie4ad.js → delegate-CAdajnf8.js} +3 -3
  14. package/dist/{delegate-DgEie4ad.js.map → delegate-CAdajnf8.js.map} +1 -1
  15. package/dist/durable.d.ts +7 -130
  16. package/dist/durable.js +24 -952
  17. package/dist/durable.js.map +1 -1
  18. package/dist/{errors-wuhGvpE3.js → errors-AzSuGI23.js} +4 -1
  19. package/dist/errors-AzSuGI23.js.map +1 -0
  20. package/dist/execution-handle-Bd2_nUB6.js +167 -0
  21. package/dist/execution-handle-Bd2_nUB6.js.map +1 -0
  22. package/dist/execution-handle-CujvqgCn.d.ts +130 -0
  23. package/dist/{graph-Cx0FhKSE.js → graph-CyaIHVGR.js} +7 -6
  24. package/dist/{graph-Cx0FhKSE.js.map → graph-CyaIHVGR.js.map} +1 -1
  25. package/dist/{improve-i0HU1-aa.js → improve-DwG-yioQ.js} +10 -9
  26. package/dist/{improve-i0HU1-aa.js.map → improve-DwG-yioQ.js.map} +1 -1
  27. package/dist/index.d.ts +4 -3
  28. package/dist/index.js +14 -11
  29. package/dist/index.js.map +1 -1
  30. package/dist/intelligence.d.ts +2 -2
  31. package/dist/intelligence.js +12 -10
  32. package/dist/intelligence.js.map +1 -1
  33. package/dist/journal-store-BJPMUydp.js +3925 -0
  34. package/dist/journal-store-BJPMUydp.js.map +1 -0
  35. package/dist/kernel.d.ts +1 -1
  36. package/dist/kernel.js +11 -10
  37. package/dist/{loop-runner-DvfOXVx9.d.ts → loop-runner-D9rcTNf2.d.ts} +2 -2
  38. package/dist/{loop-runner-BwN9MhdP.js → loop-runner-DwS3jARE.js} +3 -3
  39. package/dist/{loop-runner-BwN9MhdP.js.map → loop-runner-DwS3jARE.js.map} +1 -1
  40. package/dist/loop-runner-bin.d.ts +1 -1
  41. package/dist/loop-runner-bin.js +1 -1
  42. package/dist/mcp/bin.js +4 -4
  43. package/dist/mcp/index.d.ts +1 -1
  44. package/dist/mcp/index.js +8 -8
  45. package/dist/mcp/memory-bin.js +2 -2
  46. package/dist/{model-policy-GSxWwP-b.js → model-policy-C-RGAUW4.js} +2 -2
  47. package/dist/{model-policy-GSxWwP-b.js.map → model-policy-C-RGAUW4.js.map} +1 -1
  48. package/dist/{prepare-DE8TQnLu.js → prepare-XWwsOh8O.js} +4 -3
  49. package/dist/{prepare-DE8TQnLu.js.map → prepare-XWwsOh8O.js.map} +1 -1
  50. package/dist/{profile-CAhZDW2T.js → profile-C01SQZKa.js} +3 -3
  51. package/dist/{profile-CAhZDW2T.js.map → profile-C01SQZKa.js.map} +1 -1
  52. package/dist/profiles.d.ts +1 -1
  53. package/dist/profiles.js +2 -2
  54. package/dist/{provision-supervisor-Bde_FF0R.js → provision-supervisor-AEnei2Jq.js} +7 -7
  55. package/dist/{provision-supervisor-Bde_FF0R.js.map → provision-supervisor-AEnei2Jq.js.map} +1 -1
  56. package/dist/{run-cancellation-BLy_Zz-w.js → run-cancellation-CoJRKvih.js} +2 -2
  57. package/dist/{run-cancellation-BLy_Zz-w.js.map → run-cancellation-CoJRKvih.js.map} +1 -1
  58. package/dist/{runtime-6Ec9sHRe.js → runtime-DCYSaYHW.js} +42 -39
  59. package/dist/{runtime-6Ec9sHRe.js.map → runtime-DCYSaYHW.js.map} +1 -1
  60. package/dist/{sandbox-profile-compat-BxMQ1VXv.js → sandbox-profile-compat-CXjozdWL.js} +4 -623
  61. package/dist/{sandbox-profile-compat-BxMQ1VXv.js.map → sandbox-profile-compat-CXjozdWL.js.map} +1 -1
  62. package/dist/{search-executor-B7g_5Wjl.js → search-executor-B2jc63hZ.js} +14 -11
  63. package/dist/{search-executor-B7g_5Wjl.js.map → search-executor-B2jc63hZ.js.map} +1 -1
  64. package/dist/{server-fvEMwGMQ.js → server-CKBTwWM8.js} +6 -6
  65. package/dist/{server-fvEMwGMQ.js.map → server-CKBTwWM8.js.map} +1 -1
  66. package/dist/{structural-rollout-B86bSixU.js → structural-rollout-rRT3ypqz.js} +7 -5
  67. package/dist/{structural-rollout-B86bSixU.js.map → structural-rollout-rRT3ypqz.js.map} +1 -1
  68. package/dist/{supervise-CaeZcgAW.js → supervise-B3_wtgHS.js} +18 -21
  69. package/dist/supervise-B3_wtgHS.js.map +1 -0
  70. package/dist/supervision-discovery-CewADdEs.js +772 -0
  71. package/dist/supervision-discovery-CewADdEs.js.map +1 -0
  72. package/dist/{supervisor-C1OePYE-.js → supervisor-C_sDmX5H.js} +156 -3984
  73. package/dist/supervisor-C_sDmX5H.js.map +1 -0
  74. package/dist/testing.d.ts +1 -1
  75. package/dist/testing.js +13 -13
  76. package/dist/{tool-server-CwuYeW_Z.js → tool-server-CZWd_Opp.js} +2 -2
  77. package/dist/{tool-server-CwuYeW_Z.js.map → tool-server-CZWd_Opp.js.map} +1 -1
  78. package/dist/{top-snapshot-ChwpTr7A.js → top-snapshot-DcV7VG4m.js} +178 -161
  79. package/dist/top-snapshot-DcV7VG4m.js.map +1 -0
  80. package/dist/tui/bin.js +45 -74
  81. package/dist/tui/bin.js.map +1 -1
  82. package/dist/tui/index.d.ts +25 -54
  83. package/dist/tui/index.js +2 -2
  84. package/dist/{types-Fowuwdz2.d.ts → types-DTHxh4X9.d.ts} +47 -63
  85. package/dist/util-ebWMWzY0.js +624 -0
  86. package/dist/util-ebWMWzY0.js.map +1 -0
  87. package/dist/workspace-store/index.js +9 -3
  88. package/dist/workspace-store/index.js.map +1 -1
  89. package/package.json +18 -13
  90. package/dist/errors-wuhGvpE3.js.map +0 -1
  91. package/dist/record-events-B2GUgYdz.js +0 -261
  92. package/dist/record-events-B2GUgYdz.js.map +0 -1
  93. package/dist/supervise-CaeZcgAW.js.map +0 -1
  94. package/dist/supervisor-C1OePYE-.js.map +0 -1
  95. package/dist/top-snapshot-ChwpTr7A.js.map +0 -1
package/dist/durable.js CHANGED
@@ -1,15 +1,15 @@
1
- import { a as publishExclusiveDurableFile, c as captureRunWriteContext, d as runGenerationPath, f as runWithWriteClaim, l as fenceRunWrite, o as syncDurableDirectory, s as writeAtomicDurableFile, t as spawnJournalRecordEvents, u as mutateRunClaim } from "./record-events-B2GUgYdz.js";
2
- import { Bi as detachedSnapshot, Ct as readCommittedJsonLines, Di as runtimeFailureCauseSchema, Sr as isFiniteNumber, Vi as contentAddress, Yi as assertValidBudget, bt as isNoEntError, i as RunParkedError, k as FileSpawnJournal, mt as loadSpawnForest, oi as authoredProfileDigest, r as assertResumeLimits, s as readRunParking, xr as failedItems, yt as appendCommittedJsonLine } from "./supervisor-C1OePYE-.js";
3
- import { f as RuntimeRunStateError, m as ValidationError } from "./errors-wuhGvpE3.js";
4
- import { G as addSpend, J as cloneSpend, Z as errorMessage, pt as zeroSpend } from "./sandbox-profile-compat-BxMQ1VXv.js";
1
+ import { C as runGenerationPath, D as runtimeFailureCause, S as mutateRunClaim, Tt as assertValidBudget, _ as publishExclusiveDurableFile, a as isNoEntError, rt as contentAddress, t as FileSpawnJournal, v as syncDurableDirectory, w as runWithWriteClaim, x as fenceRunWrite, y as writeAtomicDurableFile } from "./journal-store-BJPMUydp.js";
2
+ import { Jr as authoredProfileDigest, i as RunParkedError, lr as failedItems, pt as loadSpawnForest, r as assertResumeLimits, s as readRunParking, ur as isFiniteNumber } from "./supervisor-C_sDmX5H.js";
3
+ import { f as RuntimeRunStateError, m as ValidationError } from "./errors-AzSuGI23.js";
4
+ import { c as errorMessage } from "./util-ebWMWzY0.js";
5
5
  import { a as withPursuitContext, t as composeRuntimeHooks } from "./runtime-hooks-CsW2pfFk.js";
6
- import { a as applyExactAgentProfileDiff } from "./profile-CAhZDW2T.js";
7
- import { a as superviseRootProfile, gt as readRootStreamReceipt, ht as readRootStream, t as supervise } from "./supervise-CaeZcgAW.js";
8
- import { a as runCancelRequestFile, o as runCancellationFile, v as supervisorRunDir, x as workerCancellationsDir } from "./run-cancellation-BLy_Zz-w.js";
9
- import { a as graphExecutionOptions, i as superviseAgentGraph } from "./graph-Cx0FhKSE.js";
6
+ import { a as applyExactAgentProfileDiff } from "./profile-C01SQZKa.js";
7
+ import { a as superviseRootProfile, gt as readRootStreamReceipt, ht as readRootStream, t as supervise } from "./supervise-B3_wtgHS.js";
8
+ import { a as runCancelRequestFile, o as runCancellationFile, v as supervisorRunDir, x as workerCancellationsDir } from "./run-cancellation-CoJRKvih.js";
9
+ import { a as graphExecutionOptions, i as superviseAgentGraph } from "./graph-CyaIHVGR.js";
10
+ import { n as handleChatTurn, t as deriveExecutionId } from "./execution-handle-Bd2_nUB6.js";
11
+ import { a as observerRecordDigest, i as createFileObserverHooks, n as projectPursuit, r as FileObserverJournal, t as discoverDurableSupervisionRun } from "./supervision-discovery-CewADdEs.js";
10
12
  import { agentProfileSchema, canonicalAgentProfileDigest, canonicalCandidateDigest, canonicalCandidateJson, sha256Bytes } from "@tangle-network/agent-interface";
11
- import { isRecord } from "@tangle-network/agent-core";
12
- import { createHash } from "node:crypto";
13
13
  import { closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, unlinkSync } from "node:fs";
14
14
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
15
15
  import { access, readFile, realpath } from "node:fs/promises";
@@ -17,873 +17,6 @@ import { SearchRecorder, developmentClaim, incumbent, openSearchLedger, parseSea
17
17
  import { pathToFileURL } from "node:url";
18
18
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
19
19
  import { lockHolderVerdict, lockIdentity, parseLockHolderRecord, readLockHolderIdentity } from "@tangle-network/agent-eval/lock-holder";
20
- // src/durable/chat-engine.ts
21
- const encoder = new TextEncoder();
22
- function encodeLine(event) {
23
- return encoder.encode(`${JSON.stringify(event)}\n`);
24
- }
25
- function defaultLog(message, meta) {
26
- if (meta) console.error(message, meta);
27
- else console.error(message);
28
- }
29
- /**
30
- * Preserve the useful part of a thrown HTTP Response at the stream boundary.
31
- *
32
- * Product routes commonly throw a typed Response for an upstream failure. A
33
- * Response is not an Error, so String(response) produces the unusable
34
- * "[object Response]" and hides the status that decides the next action.
35
- * Read a clone so the caller's response body remains available to its owner.
36
- */
37
- async function thrownErrorMessage(error) {
38
- if (!(error instanceof Response)) return error instanceof Error ? error.message : String(error);
39
- const prefix = `HTTP ${error.status}`;
40
- try {
41
- const text = await error.clone().text();
42
- if (!text.trim()) return prefix;
43
- try {
44
- const body = JSON.parse(text);
45
- const nested = typeof body.error === "object" && body.error !== null ? body.error.message : body.error;
46
- const message = typeof nested === "string" ? nested : typeof body.message === "string" ? body.message : void 0;
47
- if (message) return `${prefix}: ${message.slice(0, 500)}`;
48
- } catch {}
49
- } catch {}
50
- return prefix;
51
- }
52
- /**
53
- * Run one chat turn. Returns immediately with a `ReadableStream` body;
54
- * execution starts while the stream is constructed. Backend
55
- * failures surface as `error` + `session.run.failed` events.
56
- */
57
- function handleChatTurn(input) {
58
- const log = input.log ?? defaultLog;
59
- const { identity, hooks } = input;
60
- return {
61
- body: new ReadableStream({ start: async (controller) => {
62
- const emit = async (event) => {
63
- controller.enqueue(encodeLine(event));
64
- if (hooks.onEvent) try {
65
- await hooks.onEvent(event);
66
- } catch (err) {
67
- log("[chat-engine] onEvent hook threw", { error: err instanceof Error ? err.message : String(err) });
68
- }
69
- };
70
- try {
71
- await emit({
72
- type: "session.run.started",
73
- data: {
74
- sessionId: identity.sessionId,
75
- tenantId: identity.tenantId,
76
- turnIndex: identity.turnIndex
77
- }
78
- });
79
- const producer = hooks.produce();
80
- let failureData;
81
- for await (const event of producer.stream) {
82
- if (event.type === "session.run.failed") {
83
- if (!failureData) await emit({
84
- type: "error",
85
- data: event.data
86
- });
87
- failureData = {
88
- ...failureData,
89
- ...event.data
90
- };
91
- continue;
92
- }
93
- if (event.type === "error") failureData = {
94
- ...failureData,
95
- ...event.data
96
- };
97
- if (event.type === "session.run.completed") continue;
98
- await emit(event);
99
- }
100
- const rawFinal = producer.finalText();
101
- const finalText = hooks.transformFinalText ? await hooks.transformFinalText(rawFinal) : rawFinal;
102
- await hooks.persistAssistantMessage({
103
- identity,
104
- finalText
105
- });
106
- if (hooks.onTurnComplete) try {
107
- await hooks.onTurnComplete({
108
- identity,
109
- finalText
110
- });
111
- } catch (err) {
112
- log("[chat-engine] onTurnComplete threw", { error: err instanceof Error ? err.message : String(err) });
113
- }
114
- await emit({
115
- type: failureData ? "session.run.failed" : "session.run.completed",
116
- data: {
117
- ...failureData,
118
- sessionId: identity.sessionId
119
- }
120
- });
121
- } catch (err) {
122
- const message = await thrownErrorMessage(err);
123
- log("[chat-engine] turn failed", { error: message });
124
- await emit({
125
- type: "error",
126
- data: { message }
127
- });
128
- await emit({
129
- type: "session.run.failed",
130
- data: {
131
- sessionId: identity.sessionId,
132
- message
133
- }
134
- });
135
- } finally {
136
- if (hooks.traceFlush) {
137
- const flush = hooks.traceFlush().catch((err) => log("[chat-engine] traceFlush threw", { error: err instanceof Error ? err.message : String(err) }));
138
- if (input.waitUntil) input.waitUntil(flush);
139
- else await flush;
140
- }
141
- controller.close();
142
- }
143
- } }),
144
- contentType: "application/x-ndjson"
145
- };
146
- }
147
- // src/durable/execution-handle.ts
148
- /**
149
- * Derive a stable execution id from the run identity.
150
- * The same `(projectId, sessionId, turnIndex)` tuple yields the same id.
151
- *
152
- * Use the result as both `PromptOptions.executionId` and
153
- * `PromptOptions.turnId` on the first dispatch.
154
- * The execution id addresses the server-side execution for reconnect and
155
- * replay; the turn id makes a repeated dispatch idempotent.
156
- * An execution id alone does not make a repeated POST idempotent.
157
- *
158
- * Format is readable, not hashed: operators grepping orchestrator logs
159
- * for `gtm-agent:thread-abc:3` find the run without translating an
160
- * opaque id. Components are URL-encoded so delimiters inside caller ids
161
- * cannot collapse distinct tuples. The final id is limited to the
162
- * orchestrator replay route's 256-byte maximum. Execution ids are not a
163
- * secrecy boundary.
164
- *
165
- * Wire integration:
166
- * - Initial dispatch: pass the result as `executionId` and `turnId`.
167
- * - Stream replay: pass it as `executionId` with `lastEventId`.
168
- *
169
- * @throws `TypeError` when either string id is blank.
170
- * @throws `RangeError` when `turnIndex` is invalid or the result exceeds 256 bytes.
171
- */
172
- function deriveExecutionId(input) {
173
- if (input.projectId.trim().length === 0) throw new TypeError("projectId must be a non-empty string");
174
- if (input.sessionId.trim().length === 0) throw new TypeError("sessionId must be a non-empty string");
175
- if (!Number.isSafeInteger(input.turnIndex) || input.turnIndex < 0) throw new RangeError("turnIndex must be a non-negative safe integer");
176
- const executionId = [
177
- encodeURIComponent(input.projectId),
178
- encodeURIComponent(input.sessionId),
179
- String(input.turnIndex)
180
- ].join(":");
181
- if (executionId.length > 256) throw new RangeError("derived execution id must not exceed 256 bytes");
182
- return executionId;
183
- }
184
- // src/durable/observer-journal.ts
185
- /**
186
- * Durable, append-only third-person history for one concrete Runtime execution.
187
- * It consumes Runtime's existing hook stream and does not participate in execution
188
- * decisions. A broken observer therefore cannot change what an agent is allowed to do.
189
- *
190
- * The write discipline deliberately matches `FileSpawnJournal`: serialized appends,
191
- * torn-tail recovery, short-write handling, and fsync before acknowledgement. One
192
- * execution owns one journal file; higher-level pursuit aggregation joins isolated
193
- * journals by `pursuitId` instead of making independent processes share a write head.
194
- */
195
- var FileObserverJournal = class {
196
- path;
197
- pursuitId;
198
- writeContext = captureRunWriteContext();
199
- tail = Promise.resolve();
200
- initialized = false;
201
- sequence = 0;
202
- previousDigest;
203
- appendFailure;
204
- constructor(path, pursuitId) {
205
- const stableId = pursuitId.trim();
206
- if (stableId.length === 0) throw new TypeError("FileObserverJournal: pursuitId must be non-empty");
207
- this.path = resolve(path);
208
- this.pursuitId = stableId;
209
- }
210
- hooks() {
211
- return withPursuitContext(this.pursuitId, {
212
- onEvent: (event) => this.appendEvent(event).then(() => void 0),
213
- onDecisionPoint: (point) => this.appendDecision(point).then(() => void 0)
214
- });
215
- }
216
- appendEvent(event) {
217
- return this.enqueue("event", event);
218
- }
219
- appendDecision(point) {
220
- return this.enqueue("decision", point);
221
- }
222
- async read() {
223
- await this.tail;
224
- this.assertComplete();
225
- const records = [];
226
- const verify = observerVerifier(this.pursuitId);
227
- for await (const record of this.readExistingUnsafe()) {
228
- verify(record);
229
- records.push(record);
230
- }
231
- return Object.freeze(records);
232
- }
233
- enqueue(kind, value) {
234
- if (value.pursuitId !== this.pursuitId) return Promise.reject(/* @__PURE__ */ new Error(`FileObserverJournal: ${kind} pursuitId ${String(value.pursuitId)} does not match ${this.pursuitId}`));
235
- let snapshot;
236
- try {
237
- snapshot = detachedSnapshot(value, "observer record");
238
- } catch (error) {
239
- return Promise.reject(error);
240
- }
241
- let result;
242
- const operation = this.tail.then(() => this.writeContext(async () => {
243
- this.assertComplete();
244
- try {
245
- await this.initialize();
246
- } catch (error) {
247
- this.appendFailure ??= toError(error);
248
- throw error;
249
- }
250
- const unsigned = {
251
- schemaVersion: 1,
252
- pursuitId: this.pursuitId,
253
- sequence: this.sequence + 1,
254
- kind,
255
- observedAt: Date.now(),
256
- ...this.previousDigest ? { previousDigest: this.previousDigest } : {},
257
- ...kind === "event" ? { event: snapshot } : { decision: snapshot }
258
- };
259
- const record = Object.freeze({
260
- ...unsigned,
261
- digest: observerRecordDigest(unsigned)
262
- });
263
- try {
264
- const fs = await import("node:fs/promises");
265
- const { dirname } = await import("node:path");
266
- await fs.mkdir(dirname(this.path), { recursive: true });
267
- await appendCommittedJsonLine(this.path, record);
268
- } catch (error) {
269
- this.appendFailure ??= toError(error);
270
- throw error;
271
- }
272
- this.sequence = record.sequence;
273
- this.previousDigest = record.digest;
274
- result = record;
275
- }));
276
- this.tail = operation.then(() => void 0, () => void 0);
277
- return operation.then(() => {
278
- if (!result) throw new Error("FileObserverJournal: append completed without a record");
279
- return result;
280
- });
281
- }
282
- async initialize() {
283
- if (this.initialized) return;
284
- const verify = observerVerifier(this.pursuitId);
285
- let tail;
286
- for await (const record of this.readExistingUnsafe()) {
287
- verify(record);
288
- tail = record;
289
- }
290
- this.sequence = tail?.sequence ?? 0;
291
- this.previousDigest = tail?.digest;
292
- this.initialized = true;
293
- }
294
- readExistingUnsafe() {
295
- return readCommittedJsonLines(this.path, { allowMissing: true });
296
- }
297
- assertComplete() {
298
- if (!this.appendFailure) return;
299
- throw new Error("FileObserverJournal: a prior durable append failed; observer completeness is unknown", { cause: this.appendFailure });
300
- }
301
- };
302
- /** Verify identity, monotonic sequence, payload shape, and the complete digest chain. */
303
- function verifyObserverRecords(records, pursuitId) {
304
- const verify = observerVerifier(pursuitId);
305
- for (const record of records) verify(record);
306
- return Object.freeze([...records]);
307
- }
308
- /** The same full chain validation is used for replay, startup and in-memory evidence. */
309
- function observerVerifier(pursuitId) {
310
- let previousDigest;
311
- let expectedSequence = 1;
312
- return (record) => {
313
- if (record.schemaVersion !== 1) throw new Error("observer journal: unsupported schemaVersion");
314
- if (pursuitId !== void 0 && record.pursuitId !== pursuitId) throw new Error(`observer journal: pursuit identity mismatch at sequence ${record.sequence}`);
315
- if (record.sequence !== expectedSequence) throw new Error(`observer journal: non-contiguous sequence ${record.sequence}; expected ${expectedSequence}`);
316
- if (record.previousDigest !== previousDigest) throw new Error(`observer journal: digest-chain break at sequence ${record.sequence}`);
317
- if (record.kind === "event" === (record.event === void 0)) throw new Error(`observer journal: invalid event payload at sequence ${record.sequence}`);
318
- if (record.kind === "decision" === (record.decision === void 0)) throw new Error(`observer journal: invalid decision payload at sequence ${record.sequence}`);
319
- if (record.event !== void 0 && record.event.pursuitId !== record.pursuitId) throw new Error(`observer journal: nested event pursuit mismatch at sequence ${record.sequence}`);
320
- if (record.decision !== void 0 && record.decision.pursuitId !== record.pursuitId) throw new Error(`observer journal: nested decision pursuit mismatch at sequence ${record.sequence}`);
321
- const { digest, ...unsigned } = record;
322
- if (digest !== observerRecordDigest(unsigned)) throw new Error(`observer journal: digest mismatch at sequence ${record.sequence}`);
323
- previousDigest = digest;
324
- expectedSequence += 1;
325
- };
326
- }
327
- /** Compute the canonical SHA-256 digest for an unsigned observer record. */
328
- function observerRecordDigest(record) {
329
- return createHash("sha256").update(JSON.stringify(record)).digest("hex");
330
- }
331
- /** Build the canonical durable observer hook in one call. */
332
- function createFileObserverHooks(path, pursuitId) {
333
- const journal = new FileObserverJournal(path, pursuitId);
334
- return {
335
- journal,
336
- hooks: journal.hooks()
337
- };
338
- }
339
- function toError(error) {
340
- return error instanceof Error ? error : new Error(String(error));
341
- }
342
- // src/durable/observer-projection.ts
343
- /**
344
- * Fold one append-only execution journal into a deterministic operator projection.
345
- *
346
- * This is intentionally a READ model, not another state machine: it does not own
347
- * execution, cannot steer agents, and can be rebuilt from the journal at any time.
348
- * Projection verifies the complete hash chain first, so an operator view can never
349
- * silently render a mutated or reordered observer history as trustworthy state.
350
- *
351
- * Topology comes only from Runtime's canonical `agent.spawn` facts. Terminal node
352
- * state comes only from `agent.child`; concrete run state comes only from the root
353
- * `agent.run` lifecycle emitted by `supervisePursuit`, one row per attempt. Node identity is
354
- * scoped to the concrete Runtime run so independent trees may both contain `root:s0` without
355
- * aliasing, and a node belongs to the attempt that spawned it.
356
- *
357
- * Usage, cost and timing are reported at the class the runtime measured them at. A missing
358
- * class stays ABSENT and the run names the node in `spendGaps`; nothing here converts an
359
- * unmeasured channel into a zero, because a fabricated zero is indistinguishable from free work.
360
- */
361
- function projectPursuit(records) {
362
- if (records.length === 0) throw new TypeError("projectPursuit: at least one observer record is required");
363
- const pursuitId = records[0].pursuitId;
364
- const verified = verifyObserverRecords(records, pursuitId);
365
- const attempts = /* @__PURE__ */ new Map();
366
- const nodes = /* @__PURE__ */ new Map();
367
- let eventCount = 0;
368
- let decisionCount = 0;
369
- for (const record of verified) {
370
- const observed = record.event ?? record.decision;
371
- if (!observed) throw new Error(`projectPursuit: record ${record.sequence} has no observation`);
372
- const run = currentAttempt(attempts, observed.runId, record);
373
- run.lastSequence = record.sequence;
374
- run.lastObservedAt = record.observedAt;
375
- if (record.event) {
376
- eventCount += 1;
377
- run.eventCount += 1;
378
- increment(run.targets, record.event.target);
379
- projectRunActivity(run, record);
380
- projectSpawnNode(nodes, record, run.attemptIndex);
381
- projectNodeActivity(nodes, record);
382
- projectSeat(run, nodes, record);
383
- projectTurn(run, nodes, record);
384
- } else if (record.decision) {
385
- decisionCount += 1;
386
- run.decisionCount += 1;
387
- increment(run.decisions, record.decision.kind);
388
- }
389
- }
390
- const byAttempt = /* @__PURE__ */ new Map();
391
- for (const node of nodes.values()) {
392
- const key = attemptKey(node.runId, node.attemptIndex);
393
- const list = byAttempt.get(key);
394
- if (list) list.push(node);
395
- else byAttempt.set(key, [node]);
396
- }
397
- const first = verified[0];
398
- const last = verified.at(-1);
399
- return Object.freeze({
400
- pursuitId,
401
- sequence: last.sequence,
402
- chainTip: last.digest,
403
- firstObservedAt: first.observedAt,
404
- lastObservedAt: last.observedAt,
405
- runs: Object.freeze([...attempts.values()].flat().sort((a, b) => a.firstSequence - b.firstSequence || a.runId.localeCompare(b.runId)).map((run) => freezeRun(run, byAttempt.get(attemptKey(run.runId, run.attemptIndex)) ?? []))),
406
- nodes: Object.freeze([...nodes.values()].sort((a, b) => a.firstSequence - b.firstSequence || a.runId.localeCompare(b.runId) || a.id.localeCompare(b.id)).map(freezeNode)),
407
- eventCount,
408
- decisionCount
409
- });
410
- }
411
- function projectSeat(run, nodes, record) {
412
- const event = record.event;
413
- if (event?.target !== "agent.seat") return;
414
- const payload = objectRecord(event.payload);
415
- const phase = stringField(payload, "phase");
416
- const owner = nodes.get(nodeKey(event.runId, stringField(payload, "nodeId") ?? "")) ?? run;
417
- if (phase === "paused") {
418
- const resumeAt = stringField(payload, "resumeAt");
419
- if (resumeAt) owner.resumeAt = resumeAt;
420
- return;
421
- }
422
- if (phase === "resumed") {
423
- delete owner.resumeAt;
424
- return;
425
- }
426
- const raw = objectRecord(payload?.segment);
427
- const seat = stringField(raw, "seat");
428
- const harness = stringField(raw, "harness");
429
- const provider = stringField(raw, "provider");
430
- const model = stringField(raw, "model");
431
- const startedAt = stringField(raw, "startedAt");
432
- if (!seat || !harness || !provider || !model || !startedAt) return;
433
- if (owner.segments === void 0) owner.segments = [];
434
- const segments = owner.segments;
435
- if (phase === "started") {
436
- if (!segments.some((segment) => segment.startedAt === startedAt && segment.seat === seat)) segments.push({
437
- seat,
438
- harness,
439
- provider,
440
- model,
441
- startedAt
442
- });
443
- } else if (phase === "ended") {
444
- const endedAt = stringField(raw, "endedAt");
445
- const reason = stringField(raw, "reason");
446
- if (!endedAt || !reason) return;
447
- const index = segments.findIndex((segment) => segment.startedAt === startedAt && segment.seat === seat);
448
- if (index >= 0) segments[index] = {
449
- seat,
450
- harness,
451
- provider,
452
- model,
453
- startedAt,
454
- endedAt,
455
- reason
456
- };
457
- }
458
- }
459
- function attemptKey(runId, attemptIndex) {
460
- return `${runId}\u0000${attemptIndex}`;
461
- }
462
- /**
463
- * The attempt a record belongs to. An `agent.run` `before` opens a new attempt when the run has
464
- * none or its latest attempt already settled; on an open attempt it is a resume. Every other
465
- * record joins the latest attempt, and a journal that never emitted `before` gets attempt 0.
466
- */
467
- function currentAttempt(attempts, runId, record) {
468
- const list = attempts.get(runId) ?? [];
469
- if (list.length === 0) attempts.set(runId, list);
470
- const latest = list.at(-1);
471
- const isStart = record.event?.target === "agent.run" && record.event.phase === "before";
472
- if (latest !== void 0 && (!isStart || latest.status === "running")) {
473
- if (isStart) {
474
- if (latest.started) latest.resumeCount += 1;
475
- latest.started = true;
476
- }
477
- return latest;
478
- }
479
- const created = {
480
- runId,
481
- attemptIndex: list.length,
482
- resumeCount: 0,
483
- started: isStart,
484
- status: "running",
485
- firstSequence: record.sequence,
486
- lastSequence: record.sequence,
487
- firstObservedAt: record.observedAt,
488
- lastObservedAt: record.observedAt,
489
- eventCount: 0,
490
- decisionCount: 0,
491
- targets: {},
492
- decisions: {}
493
- };
494
- list.push(created);
495
- return created;
496
- }
497
- function projectRunActivity(run, record) {
498
- const event = record.event;
499
- if (event?.target !== "agent.run") return;
500
- const payload = objectRecord(event.payload);
501
- const status = stringField(payload, "status");
502
- if (event.phase === "after" || status === "done") {
503
- run.status = "done";
504
- run.settledAt = record.observedAt;
505
- return;
506
- }
507
- if (event.phase !== "error" && status !== "failed") return;
508
- run.status = "down";
509
- run.settledAt = record.observedAt;
510
- const error = stringField(payload, "error");
511
- if (error) run.error = error;
512
- }
513
- function nodeKey(runId, nodeId) {
514
- return `${runId}\u0000${nodeId}`;
515
- }
516
- function projectSpawnNode(nodes, record, attemptIndex) {
517
- const event = record.event;
518
- if (event?.target !== "agent.spawn") return;
519
- const payload = objectRecord(event.payload);
520
- const childId = stringField(payload, "childId");
521
- if (!childId) return;
522
- const key = nodeKey(event.runId, childId);
523
- const existing = nodes.get(key);
524
- if (existing) {
525
- existing.lastSequence = record.sequence;
526
- existing.lastObservedAt = record.observedAt;
527
- existing.eventCount += 1;
528
- return;
529
- }
530
- const label = stringField(payload, "label");
531
- const runtime = stringField(payload, "runtime");
532
- const depth = numberField(payload, "depth");
533
- const assignmentId = stringField(payload, "assignmentId");
534
- const successorOf = stringField(payload, "successorOf");
535
- const attemptId = stringField(payload, "attemptId");
536
- const startedAt = numberField(payload, "startedAt");
537
- nodes.set(key, {
538
- id: childId,
539
- ...event.parentId ? { parentId: event.parentId } : {},
540
- runId: event.runId,
541
- attemptIndex,
542
- ...label ? { label } : {},
543
- ...runtime ? { runtime } : {},
544
- ...depth !== void 0 ? { depth } : {},
545
- ...assignmentId ? { assignmentId } : {},
546
- ...successorOf ? { successorOf } : {},
547
- ...attemptId ? { attemptId } : {},
548
- startedAt: startedAt ?? record.observedAt,
549
- ...payload && Object.hasOwn(payload, "identity") ? { identity: payload.identity } : {},
550
- ...payload && Object.hasOwn(payload, "budget") ? { budget: payload.budget } : {},
551
- status: "running",
552
- modelCalls: [],
553
- firstSequence: record.sequence,
554
- lastSequence: record.sequence,
555
- firstObservedAt: record.observedAt,
556
- lastObservedAt: record.observedAt,
557
- eventCount: 1,
558
- turnCount: 0
559
- });
560
- }
561
- function projectNodeActivity(nodes, record) {
562
- const event = record.event;
563
- if (event === void 0) return;
564
- if (event.target === "agent.spawn") return;
565
- const payload = objectRecord(event.payload);
566
- const nodeId = stringField(payload, "childId") ?? stringField(payload, "nodeId") ?? stringField(payload, "workerId");
567
- if (!nodeId) return;
568
- const node = nodes.get(nodeKey(event.runId, nodeId));
569
- if (!node) return;
570
- node.lastSequence = record.sequence;
571
- node.lastObservedAt = record.observedAt;
572
- node.eventCount += 1;
573
- if (event.target !== "agent.child") return;
574
- const status = stringField(payload, "status");
575
- if (status !== "done" && status !== "down") return;
576
- node.status = status;
577
- node.settledAt = numberField(payload, "settledAt") ?? record.observedAt;
578
- const startedAt = numberField(payload, "startedAt");
579
- if (startedAt !== void 0) node.startedAt = startedAt;
580
- if (payload && Object.hasOwn(payload, "spent")) node.spent = spendField(payload, "spent");
581
- const metered = spendField(payload, "metered");
582
- if (metered) node.ownInference = node.ownInference ? addSpend(node.ownInference, metered) : metered;
583
- const runtime = stringField(payload, "runtime");
584
- if (runtime) node.runtime = runtime;
585
- const outRef = stringField(payload, "outRef");
586
- if (outRef) node.outRef = outRef;
587
- const score = numberField(payload, "score");
588
- if (score !== void 0) node.score = score;
589
- const valid = booleanField(payload, "valid");
590
- if (valid !== void 0) node.valid = valid;
591
- const reason = stringField(payload, "reason");
592
- if (reason) node.reason = reason;
593
- if (status === "done") node.failure = void 0;
594
- else if (payload && Object.hasOwn(payload, "failure")) node.failure = runtimeFailureCauseSchema.parse(payload.failure);
595
- const infra = booleanField(payload, "infra");
596
- if (infra !== void 0) node.infra = infra;
597
- const retained = stringField(payload, "retainedExecution");
598
- if (retained === "pending" || retained === "released" || retained === "release-unconfirmed") node.retainedExecution = retained;
599
- const cause = stringField(payload, "retainedPendingCause");
600
- if (cause === "unobservable" || cause === "provider-contract" || cause === "request-rejected" || cause === "transport" || cause === "nested-recovery") node.retainedPendingCause = cause;
601
- const releasedAt = numberField(payload, "releasedAt");
602
- if (releasedAt !== void 0) node.releasedAt = releasedAt;
603
- const budgetViolation = budgetViolationField(payload);
604
- if (budgetViolation) node.budgetViolation = budgetViolation;
605
- attachSettlementEvidence(node, payload);
606
- }
607
- /**
608
- * Read the receipts a settlement carries. Each is taken as reported: an `unknown` receipt keeps
609
- * its `status` and `reason` so a client can show WHY a model or backend is missing instead of
610
- * showing nothing, and a receipt that never landed leaves every derived field absent.
611
- */
612
- function attachSettlementEvidence(node, payload) {
613
- const providerModel = payload?.providerModel;
614
- if (providerModel && typeof providerModel === "object") node.providerModel = providerModel;
615
- const trace = payload?.trace;
616
- if (trace && typeof trace === "object") node.trace = trace;
617
- const bindings = payload?.executionBindings;
618
- if (Array.isArray(bindings) && bindings.length > 0) {
619
- node.executionBindings = bindings;
620
- const known = bindings.find((binding) => objectRecord(binding)?.status === "known");
621
- if (known) {
622
- node.placement = known.descriptor;
623
- node.attemptId ??= known.attemptId;
624
- }
625
- }
626
- const receipt = payload?.materialization;
627
- if (!receipt || typeof receipt !== "object") return;
628
- node.materialization = receipt;
629
- if (receipt.status !== "known") return;
630
- node.backend = receipt.backend;
631
- if (receipt.model.status === "known") node.model = receipt.model.id;
632
- node.execution = {
633
- kind: receipt.execution.kind,
634
- id: receipt.execution.id
635
- };
636
- }
637
- /**
638
- * Fold one metered turn onto whoever drove it. `agent.turn` names its subject in `parentId` — the
639
- * node making the call — not in the payload, because the caller IS the subject. A turn the run
640
- * root drove belongs to no spawned node and lands on the run instead, so the run's totals stay
641
- * complete without inventing a node for the root.
642
- */
643
- function projectTurn(run, nodes, record) {
644
- const event = record.event;
645
- if (event?.target !== "agent.turn") return;
646
- const payload = objectRecord(event.payload);
647
- const spend = spendField(payload, "spend");
648
- const subject = event.parentId;
649
- if (subject === void 0) return;
650
- const node = nodes.get(nodeKey(event.runId, subject));
651
- if (!node) {
652
- if (!spend) return;
653
- run.rootInference = run.rootInference ? addSpend(run.rootInference, spend) : spend;
654
- return;
655
- }
656
- node.turnCount += 1;
657
- node.lastSequence = record.sequence;
658
- node.lastObservedAt = record.observedAt;
659
- if (spend) {
660
- node.ownInference = node.ownInference ? addSpend(node.ownInference, spend) : spend;
661
- node.firstOutputAt ??= record.observedAt;
662
- }
663
- const reasoning = numberField(payload, "reasoningTokens");
664
- if (reasoning !== void 0) node.reasoningTokens = (node.reasoningTokens ?? 0) + reasoning;
665
- const firstTokenAt = numberField(payload, "firstTokenAt");
666
- if (firstTokenAt !== void 0) node.firstTokenAt ??= firstTokenAt;
667
- const model = stringField(payload, "model");
668
- if (model) node.model ??= model;
669
- const callId = stringField(payload, "callId");
670
- if (callId && !node.modelCalls.includes(callId)) node.modelCalls.push(callId);
671
- }
672
- /**
673
- * A node's whole reported cost: the child work its settlement reported plus the inference it drove
674
- * itself. `undefined` when neither landed — the run's `spendGaps` then names the node, so an
675
- * unaccounted node is visible instead of reading as free.
676
- */
677
- function nodeTotal(node) {
678
- if (node.spent && node.ownInference) return addSpend(node.spent, node.ownInference);
679
- if (node.spent) return cloneSpend(node.spent);
680
- if (node.ownInference) return cloneSpend(node.ownInference);
681
- }
682
- /**
683
- * The run counted once, and each node's own share of it.
684
- *
685
- * A settled node's `spent` already contains the child work its nested tree reported, so summing
686
- * every node would count a driver's descendants twice. Inclusive therefore sums only the run's
687
- * TOP-LEVEL nodes — those whose parent is not another node of this run — plus the root's own
688
- * turns. Exclusive subtracts each node's direct children from its own total, which telescopes:
689
- * the exclusive entries sum back to `inclusive` exactly.
690
- */
691
- function runTotals(run, nodes) {
692
- const present = new Set(nodes.map((node) => node.id));
693
- const children = /* @__PURE__ */ new Map();
694
- for (const node of nodes) {
695
- if (node.parentId === void 0 || !present.has(node.parentId)) continue;
696
- const list = children.get(node.parentId);
697
- if (list) list.push(node);
698
- else children.set(node.parentId, [node]);
699
- }
700
- const exclusiveByNode = {};
701
- let inclusive = run.rootInference ? cloneSpend(run.rootInference) : zeroSpend();
702
- if (run.rootInference) exclusiveByNode[run.runId] = cloneSpend(run.rootInference);
703
- for (const node of nodes) {
704
- const total = nodeTotal(node);
705
- if (total === void 0) continue;
706
- if (node.parentId === void 0 || !present.has(node.parentId)) inclusive = addSpend(inclusive, total);
707
- let exclusive = total;
708
- for (const child of children.get(node.id) ?? []) {
709
- const childTotal = nodeTotal(child);
710
- if (childTotal !== void 0) exclusive = subtractSpend(exclusive, childTotal);
711
- }
712
- exclusiveByNode[node.id] = exclusive;
713
- }
714
- return Object.freeze({
715
- inclusive: Object.freeze(inclusive),
716
- exclusiveByNode: Object.freeze(exclusiveByNode)
717
- });
718
- }
719
- /**
720
- * Remove a child's reported total from its parent's. Per channel, never below zero: a parent whose
721
- * executor reported less than its children did is a reporting gap, and a negative exclusive share
722
- * would be a fabricated number rather than a measurement.
723
- */
724
- function subtractSpend(a, b) {
725
- const tokens = { ...a.tokens };
726
- tokens.input = Math.max(0, tokens.input - b.tokens.input);
727
- tokens.output = Math.max(0, tokens.output - b.tokens.output);
728
- if (a.tokens.cacheRead !== void 0) tokens.cacheRead = Math.max(0, a.tokens.cacheRead - (b.tokens.cacheRead ?? 0));
729
- if (a.tokens.cacheWrite !== void 0) tokens.cacheWrite = Math.max(0, a.tokens.cacheWrite - (b.tokens.cacheWrite ?? 0));
730
- if (a.tokens.freshInput !== void 0) tokens.freshInput = Math.max(0, a.tokens.freshInput - (b.tokens.freshInput ?? 0));
731
- return {
732
- iterations: Math.max(0, a.iterations - b.iterations),
733
- tokens,
734
- ...a.tokensKnown === false || b.tokensKnown === false ? { tokensKnown: false } : {},
735
- usd: Math.max(0, a.usd - b.usd),
736
- ...a.usdKnown === false || b.usdKnown === false ? { usdKnown: false } : {},
737
- ...a.usdEstimated !== void 0 ? { usdEstimated: Math.max(0, a.usdEstimated - (b.usdEstimated ?? 0)) } : {},
738
- ms: Math.max(0, a.ms - b.ms)
739
- };
740
- }
741
- /** The nodes of one run whose accounting is incomplete, in the vocabulary the supervisor already
742
- * uses: `never-settled` = no terminal record, so every channel is unaccounted; `unreported` = a
743
- * record landed with a channel the provider did not report, so that channel is a floor. */
744
- function runSpendGaps(nodes) {
745
- const gaps = [];
746
- for (const node of nodes) {
747
- const label = node.label;
748
- if (node.status === "running") {
749
- gaps.push({
750
- id: node.id,
751
- ...label !== void 0 ? { label } : {},
752
- kind: "never-settled",
753
- channels: ["tokens", "usd"]
754
- });
755
- continue;
756
- }
757
- const total = nodeTotal(node);
758
- const channels = [];
759
- if (total === void 0 || total.tokensKnown === false) channels.push("tokens");
760
- if (total === void 0 || total.usdKnown === false) channels.push("usd");
761
- if (channels.length === 0) continue;
762
- gaps.push({
763
- id: node.id,
764
- ...label !== void 0 ? { label } : {},
765
- kind: "unreported",
766
- channels
767
- });
768
- }
769
- return gaps;
770
- }
771
- function usageOf(total, reasoning) {
772
- return {
773
- input: total.tokens.input,
774
- output: total.tokens.output,
775
- ...total.tokens.cacheRead !== void 0 ? { cacheRead: total.tokens.cacheRead } : {},
776
- ...total.tokens.cacheWrite !== void 0 ? { cacheWrite: total.tokens.cacheWrite } : {},
777
- ...reasoning !== void 0 ? { reasoning } : {},
778
- tokensKnown: total.tokensKnown !== false && total.tokens.tokensKnown !== false
779
- };
780
- }
781
- function costOf(total) {
782
- const usdKnown = total.usdKnown !== false;
783
- const estimated = total.usdEstimated;
784
- const provenance = usdKnown ? estimated === void 0 || estimated === 0 ? "reported" : "partial" : estimated === void 0 ? "unknown" : estimated >= total.usd ? "estimated" : "partial";
785
- return {
786
- usd: total.usd,
787
- usdKnown,
788
- ...estimated !== void 0 ? { usdEstimated: estimated } : {},
789
- provenance
790
- };
791
- }
792
- /** The platform block, or nothing at all when the node's spend names no box channel. */
793
- function platformOf(total) {
794
- if (total.boxMinutesProvenance === void 0) return void 0;
795
- return {
796
- ...total.boxMinutes !== void 0 ? { boxMinutes: total.boxMinutes } : {},
797
- boxMinutesKnown: total.boxMinutesKnown === true,
798
- provenance: total.boxMinutesProvenance
799
- };
800
- }
801
- function timingOf(node) {
802
- if (node.startedAt === void 0) return void 0;
803
- return {
804
- startedAt: node.startedAt,
805
- ...node.firstOutputAt !== void 0 ? { firstOutputAt: node.firstOutputAt } : {},
806
- ...node.firstTokenAt !== void 0 ? { firstTokenAt: node.firstTokenAt } : {},
807
- ...node.settledAt !== void 0 ? { settledAt: node.settledAt } : {},
808
- ...node.settledAt !== void 0 ? { wallMs: Math.max(0, node.settledAt - node.startedAt) } : {}
809
- };
810
- }
811
- function freezeRun(run, nodes) {
812
- const gaps = runSpendGaps(nodes);
813
- const { rootInference: _rootInference, started: _started, segments, ...rest } = run;
814
- return Object.freeze({
815
- ...rest,
816
- ...segments?.length ? { segments: Object.freeze(segments.map((segment) => Object.freeze(segment))) } : {},
817
- targets: Object.freeze({ ...run.targets }),
818
- decisions: Object.freeze({ ...run.decisions }),
819
- totals: runTotals(run, nodes),
820
- ...gaps.length > 0 ? { spendGaps: Object.freeze(gaps) } : {}
821
- });
822
- }
823
- function freezeNode(node) {
824
- const { modelCalls, segments, reasoningTokens, startedAt: _startedAt, attemptIndex: _attemptIndex, ...rest } = node;
825
- const total = nodeTotal(node);
826
- const timing = timingOf(node);
827
- return Object.freeze({
828
- ...rest,
829
- ...segments?.length ? { segments: Object.freeze(segments.map((segment) => Object.freeze(segment))) } : {},
830
- ...modelCalls.length > 0 ? { modelCalls: Object.freeze([...modelCalls]) } : {},
831
- ...total !== void 0 ? {
832
- usage: usageOf(total, reasoningTokens),
833
- cost: costOf(total)
834
- } : {},
835
- ...total !== void 0 && platformOf(total) !== void 0 ? { platform: platformOf(total) } : {},
836
- ...timing !== void 0 ? { timing } : {}
837
- });
838
- }
839
- function increment(target, key) {
840
- target[key] = (target[key] ?? 0) + 1;
841
- }
842
- function objectRecord(value) {
843
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
844
- }
845
- /** Read a journaled `Spend` without trusting the wire: a record missing the conserved channels is
846
- * not a zero-cost turn, it is an unparseable one, and it must not enter a total. */
847
- function spendField(value, key) {
848
- const field = objectRecord(value?.[key]);
849
- if (!field) return void 0;
850
- const tokens = objectRecord(field.tokens);
851
- if (typeof field.usd !== "number" || !Number.isFinite(field.usd)) return void 0;
852
- if (!tokens || typeof tokens.input !== "number" || typeof tokens.output !== "number") return;
853
- return cloneSpend(field);
854
- }
855
- /** Read a journaled overspend without trusting the wire: one malformed channel drops the record,
856
- * so an unparseable entry is never reported as an overspend of some size. */
857
- function budgetViolationField(value) {
858
- const overspent = objectRecord(value?.budgetViolation)?.overspent;
859
- if (!Array.isArray(overspent) || overspent.length === 0) return void 0;
860
- const entries = [];
861
- for (const raw of overspent) {
862
- const entry = objectRecord(raw);
863
- const channel = stringField(entry, "channel");
864
- const reserved = numberField(entry, "reserved");
865
- const spent = numberField(entry, "spent");
866
- if (channel === void 0 || reserved === void 0 || spent === void 0) return void 0;
867
- entries.push({
868
- channel,
869
- reserved,
870
- spent
871
- });
872
- }
873
- return { overspent: entries };
874
- }
875
- function stringField(value, key) {
876
- const field = value?.[key];
877
- return typeof field === "string" && field.length > 0 ? field : void 0;
878
- }
879
- function numberField(value, key) {
880
- const field = value?.[key];
881
- return typeof field === "number" && Number.isFinite(field) ? field : void 0;
882
- }
883
- function booleanField(value, key) {
884
- const field = value?.[key];
885
- return typeof field === "boolean" ? field : void 0;
886
- }
887
20
  // src/durable/settle-record.ts
888
21
  /** The settle record: the returned `SupervisedResult` as canonical JSON, written once. */
889
22
  const SETTLE_RECORD_FILE = "result.json";
@@ -2518,8 +1651,7 @@ async function superviseGraphPursuit(graph, opts) {
2518
1651
  }
2519
1652
  /** The pursuit identity and run directory both durable entries require. */
2520
1653
  function assertPursuitDirectory(opts, entry) {
2521
- if (opts.pursuitId.trim().length === 0) throw new TypeError(`${entry}: pursuitId must be non-empty`);
2522
- if (opts.runDir.trim().length === 0) throw new TypeError(`${entry}: runDir must be non-empty`);
1654
+ for (const field of ["pursuitId", "runDir"]) if (opts[field].trim().length === 0) throw new TypeError(`${entry}: ${field} must be non-empty`);
2523
1655
  if (opts.retainedAtSettlement === "keep") throw new TypeError(`${entry}: retainedAtSettlement 'keep' cannot hold, because a settled pursuit is never re-entered`);
2524
1656
  }
2525
1657
  /**
@@ -2560,17 +1692,16 @@ async function runDurablePursuit(opts, execute, settled, reopen) {
2560
1692
  throw error;
2561
1693
  }
2562
1694
  let pursuit;
2563
- let observerError;
2564
1695
  try {
2565
1696
  await observer.journal.appendEvent(rootEvent(pursuitId, runId, "error", now(), {
2566
1697
  status: "failed",
2567
- error: errorMessage(error)
1698
+ error: errorMessage(error),
1699
+ failure: runtimeFailureCause(error)
2568
1700
  }));
2569
1701
  pursuit = projectPursuit(await observer.journal.read());
2570
- } catch (failure) {
2571
- observerError = failure;
1702
+ } catch (observerError) {
1703
+ throw new Error("supervisePursuit: Runtime failed and durable observer completeness could not be proven", { cause: new AggregateError([error, observerError]) });
2572
1704
  }
2573
- if (observerError !== void 0 || pursuit === void 0) throw new Error("supervisePursuit: Runtime failed and durable observer completeness could not be proven", { cause: new AggregateError(observerError === void 0 ? [error] : [error, observerError]) });
2574
1705
  try {
2575
1706
  const rootStream = await readRootStreamReceipt(runDir);
2576
1707
  await writeFailureRecord(runDir, {
@@ -2588,12 +1719,19 @@ async function runDurablePursuit(opts, execute, settled, reopen) {
2588
1719
  }
2589
1720
  throw new SupervisePursuitError(error, pursuit, observerPath, failurePath);
2590
1721
  }
1722
+ const terminal = settled(result);
2591
1723
  try {
2592
- await writeSettleRecord(runDir, settled(result));
1724
+ await writeSettleRecord(runDir, terminal);
2593
1725
  } catch (cause) {
2594
1726
  throw new Error(`supervisePursuit: Runtime settled but the settle record ${settlePath} could not be written`, { cause });
2595
1727
  }
2596
- await observer.journal.appendEvent(rootEvent(pursuitId, runId, "after", now(), { status: "done" }));
1728
+ await observer.journal.appendEvent(rootEvent(pursuitId, runId, "after", now(), {
1729
+ status: "done",
1730
+ ...terminal.kind === "no-winner" && terminal.reason === "driver-failed" && terminal.error.failure ? {
1731
+ error: terminal.error.message,
1732
+ failure: terminal.error.failure
1733
+ } : {}
1734
+ }));
2597
1735
  terminalState = "done";
2598
1736
  return Object.freeze({
2599
1737
  result,
@@ -2621,72 +1759,6 @@ function rootEvent(pursuitId, runId, phase, timestamp, payload) {
2621
1759
  function errorName(error) {
2622
1760
  return error instanceof Error ? error.name : "NonError";
2623
1761
  }
2624
- // src/durable/supervision-discovery.ts
2625
- /**
2626
- * Discover the stable identities recorded by Runtime's durable supervision
2627
- * files. This is the developer-facing first step before calling
2628
- * `FileSpawnJournal.loadTree(root)`, `loadSpawnForest(journal, root)`, or
2629
- * `FileCoordinationLog.load(runId, ownerId)`.
2630
- *
2631
- * Missing files produce empty collections. A malformed committed JSONL record
2632
- * still fails loud through the same parser used by the runtime; a torn final
2633
- * append is ignored because it was never acknowledged as committed.
2634
- */
2635
- async function discoverDurableSupervisionRun(runDir) {
2636
- if (typeof runDir !== "string" || runDir.trim().length === 0) throw new TypeError("discoverDurableSupervisionRun: runDir must be a non-empty string");
2637
- const canonicalRunDir = resolve(runDir);
2638
- const spawnJournalPath = `${canonicalRunDir}/spawn-journal.jsonl`;
2639
- const coordinationLogPath = `${canonicalRunDir}/coordination-log.jsonl`;
2640
- const allRoots = /* @__PURE__ */ new Set();
2641
- const nestedRoots = /* @__PURE__ */ new Set();
2642
- const rootsBegunAt = /* @__PURE__ */ new Map();
2643
- for await (const record of readCommittedJsonLines(spawnJournalPath, { allowMissing: true })) {
2644
- if (record.kind === "begin") {
2645
- if (typeof record.root !== "string" || record.root.length === 0) throw new Error(`${spawnJournalPath}: begin record has no non-empty string root identity`);
2646
- allRoots.add(record.root);
2647
- rootsBegunAt.set(record.root, typeof record.at === "string" ? record.at : null);
2648
- continue;
2649
- }
2650
- for (const event of spawnJournalRecordEvents(record)) {
2651
- if (!isRecord(event) || event.kind !== "spawned") continue;
2652
- if (!Object.hasOwn(event, "ownedTreeRoot")) continue;
2653
- if (typeof event.ownedTreeRoot !== "string" || event.ownedTreeRoot.length === 0) throw new Error(`${spawnJournalPath}: spawned event ownedTreeRoot must be a non-empty string when present`);
2654
- nestedRoots.add(event.ownedTreeRoot);
2655
- }
2656
- }
2657
- const streams = /* @__PURE__ */ new Map();
2658
- for await (const record of readCommittedJsonLines(coordinationLogPath, { allowMissing: true })) {
2659
- if (typeof record.runId !== "string" || record.runId.length === 0) throw new Error(`${coordinationLogPath}: record has no non-empty string runId identity`);
2660
- const stream = streams.get(record.runId) ?? {
2661
- owners: /* @__PURE__ */ new Set(),
2662
- unscopedRecords: 0,
2663
- recordCount: 0
2664
- };
2665
- stream.recordCount += 1;
2666
- if (record.ownerId === void 0) stream.unscopedRecords += 1;
2667
- else if (typeof record.ownerId === "string" && record.ownerId.length > 0) stream.owners.add(record.ownerId);
2668
- else throw new Error(`${coordinationLogPath}: ownerId must be a non-empty string when present`);
2669
- streams.set(record.runId, stream);
2670
- }
2671
- const coordinationStreams = [...streams.entries()].sort(([left], [right]) => compareText(left, right)).map(([runId, stream]) => Object.freeze({
2672
- runId,
2673
- ownerIds: Object.freeze([...stream.owners].sort(compareText)),
2674
- unscopedRecords: stream.unscopedRecords,
2675
- recordCount: stream.recordCount
2676
- }));
2677
- const roots = [...allRoots].filter((root) => !nestedRoots.has(root)).sort(compareText);
2678
- return Object.freeze({
2679
- runDir: canonicalRunDir,
2680
- spawnJournalPath,
2681
- coordinationLogPath,
2682
- roots: Object.freeze(roots),
2683
- rootsBegunAt: Object.freeze(roots.map((root) => rootsBegunAt.get(root) ?? null)),
2684
- coordinationStreams: Object.freeze(coordinationStreams)
2685
- });
2686
- }
2687
- function compareText(left, right) {
2688
- return left < right ? -1 : left > right ? 1 : 0;
2689
- }
2690
1762
  export { FileObserverJournal, RUN_FORK_CORRELATION_KEYS, RunDirectoryLockedError, RunParkedError, SettledRunDirectoryError, SupervisePursuitError, acquireRunDirectoryLock, assertPursuitVersions, deriveExecutionId, discoverDurableSupervisionRun, handleChatTurn, observerRecordDigest, projectPursuit, readFailureRecord, readRootStream, readRootStreamReceipt, readRunDirectoryLock, readRunParking, readSettleRecord, runDirectoryHolderIsLive, superviseGraphPursuit, supervisePursuit };
2691
1763
 
2692
1764
  //# sourceMappingURL=durable.js.map