@tangle-network/agent-runtime 0.104.0 → 0.106.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.
Files changed (217) hide show
  1. package/README.md +176 -42
  2. package/dist/activation-CRjVfOiy.js +203 -0
  3. package/dist/activation-CRjVfOiy.js.map +1 -0
  4. package/dist/activation-DyWB0K6E.d.ts +98 -0
  5. package/dist/agent.d.ts +216 -234
  6. package/dist/agent.js +615 -547
  7. package/dist/agent.js.map +1 -1
  8. package/dist/analyst-loop-DGdtVENg.js +517 -0
  9. package/dist/analyst-loop-DGdtVENg.js.map +1 -0
  10. package/dist/analyst-loop.d.ts +10 -43
  11. package/dist/analyst-loop.js +2 -11
  12. package/dist/backends-Dy8NujB4.js +713 -0
  13. package/dist/backends-Dy8NujB4.js.map +1 -0
  14. package/dist/candidate-execution/index.d.ts +4 -192
  15. package/dist/candidate-execution/index.js +5 -66
  16. package/dist/candidate-execution-DqMbv1rU.js +488 -0
  17. package/dist/candidate-execution-DqMbv1rU.js.map +1 -0
  18. package/dist/coder-yhVWbdWc.js +40 -0
  19. package/dist/coder-yhVWbdWc.js.map +1 -0
  20. package/dist/conversation-XucG_K3l.js +1265 -0
  21. package/dist/conversation-XucG_K3l.js.map +1 -0
  22. package/dist/conversation.d.ts +2 -804
  23. package/dist/conversation.js +2 -55
  24. package/dist/environment-provider-CfLUmKmf.js +1168 -0
  25. package/dist/environment-provider-CfLUmKmf.js.map +1 -0
  26. package/dist/environment-provider-IUGU3epE.d.ts +964 -0
  27. package/dist/environment-provider.d.ts +2 -78
  28. package/dist/environment-provider.js +2 -18
  29. package/dist/errors-DEAvWQPy.js +111 -0
  30. package/dist/errors-DEAvWQPy.js.map +1 -0
  31. package/dist/improvement-adapter-Cr5TBSji.d.ts +166 -0
  32. package/dist/improvement-cycle-C095kS_g.js +2624 -0
  33. package/dist/improvement-cycle-C095kS_g.js.map +1 -0
  34. package/dist/index-BGdxAwfO.d.ts +838 -0
  35. package/dist/index-C-FYUuFG.d.ts +193 -0
  36. package/dist/index-CbNM40xG.d.ts +140 -0
  37. package/dist/index-DruSKs3Z.d.ts +7224 -0
  38. package/dist/index-Efjb3nrQ.d.ts +671 -0
  39. package/dist/index.d.ts +14 -1134
  40. package/dist/index.js +2134 -1972
  41. package/dist/index.js.map +1 -1
  42. package/dist/intelligence.d.ts +776 -888
  43. package/dist/intelligence.js +1466 -1410
  44. package/dist/intelligence.js.map +1 -1
  45. package/dist/{kb-gate-CwHO0vz6.d.ts → kb-gate-C8z2juK8.d.ts} +33 -31
  46. package/dist/kb-gate-DpaSwXVx.js +80 -0
  47. package/dist/kb-gate-DpaSwXVx.js.map +1 -0
  48. package/dist/knowledge-Ku-mPP4f.js +433 -0
  49. package/dist/knowledge-Ku-mPP4f.js.map +1 -0
  50. package/dist/knowledge.d.ts +2 -150
  51. package/dist/knowledge.js +2 -40
  52. package/dist/local-harness-Dh8PJ0ot.d.ts +455 -0
  53. package/dist/loop-runner-bin-BXsFEUcf.d.ts +162 -0
  54. package/dist/loop-runner-bin-BwjQ1NfH.js +268 -0
  55. package/dist/loop-runner-bin-BwjQ1NfH.js.map +1 -0
  56. package/dist/loop-runner-bin.d.ts +2 -17
  57. package/dist/loop-runner-bin.js +2 -24
  58. package/dist/loops.d.ts +5 -4164
  59. package/dist/loops.js +8 -419
  60. package/dist/mcp/bin.d.ts +1 -1
  61. package/dist/mcp/bin.js +116 -146
  62. package/dist/mcp/bin.js.map +1 -1
  63. package/dist/mcp/index.d.ts +295 -437
  64. package/dist/mcp/index.js +1242 -1177
  65. package/dist/mcp/index.js.map +1 -1
  66. package/dist/mcp/memory-bin.d.ts +1 -1
  67. package/dist/mcp/memory-bin.js +37 -16
  68. package/dist/mcp/memory-bin.js.map +1 -1
  69. package/dist/memory-server-DL6cE2Ag.js +234 -0
  70. package/dist/memory-server-DL6cE2Ag.js.map +1 -0
  71. package/dist/model-resolution-Btd9iIKV.js +98 -0
  72. package/dist/model-resolution-Btd9iIKV.js.map +1 -0
  73. package/dist/openai-tools-B68JaOCx.d.ts +24 -0
  74. package/dist/openai-tools-CKLy1C7M.js +44 -0
  75. package/dist/openai-tools-CKLy1C7M.js.map +1 -0
  76. package/dist/otel-export-D34IQW1C.js +843 -0
  77. package/dist/otel-export-D34IQW1C.js.map +1 -0
  78. package/dist/platform.d.ts +188 -185
  79. package/dist/platform.js +185 -218
  80. package/dist/platform.js.map +1 -1
  81. package/dist/prepare-B53sPV7M.js +4292 -0
  82. package/dist/prepare-B53sPV7M.js.map +1 -0
  83. package/dist/primeintellect/index.d.ts +150 -147
  84. package/dist/primeintellect/index.js +626 -1004
  85. package/dist/primeintellect/index.js.map +1 -1
  86. package/dist/profiles.d.ts +296 -490
  87. package/dist/profiles.js +722 -749
  88. package/dist/profiles.js.map +1 -1
  89. package/dist/protected-model-port-BLPH5VVv.js +722 -0
  90. package/dist/protected-model-port-BLPH5VVv.js.map +1 -0
  91. package/dist/protected-model-port-BP6Z4eau.d.ts +283 -0
  92. package/dist/protected-redaction--F3v1oo8.js +200 -0
  93. package/dist/protected-redaction--F3v1oo8.js.map +1 -0
  94. package/dist/redact-D-u-rrcn.js +82 -0
  95. package/dist/redact-D-u-rrcn.js.map +1 -0
  96. package/dist/redact-kbIOjfjM.d.ts +452 -0
  97. package/dist/researcher-CoVqNhfI.js +433 -0
  98. package/dist/researcher-CoVqNhfI.js.map +1 -0
  99. package/dist/runtime-BG0BziaP.js +5408 -0
  100. package/dist/runtime-BG0BziaP.js.map +1 -0
  101. package/dist/sandbox-events-DeI5xX8P.js +316 -0
  102. package/dist/sandbox-events-DeI5xX8P.js.map +1 -0
  103. package/dist/spawn-journal-CwPvKUTa.js +456 -0
  104. package/dist/spawn-journal-CwPvKUTa.js.map +1 -0
  105. package/dist/structural-rollout-IXUEplky.js +1445 -0
  106. package/dist/structural-rollout-IXUEplky.js.map +1 -0
  107. package/dist/substrate-B0TYNrXn.js +27 -0
  108. package/dist/substrate-B0TYNrXn.js.map +1 -0
  109. package/dist/{substrate-Dyiqmd3o.d.ts → substrate-BcnuSHXm.d.ts} +57 -54
  110. package/dist/supervise-BiRutHS9.js +7321 -0
  111. package/dist/supervise-BiRutHS9.js.map +1 -0
  112. package/dist/supervisor-DTKhF-RV.js +3826 -0
  113. package/dist/supervisor-DTKhF-RV.js.map +1 -0
  114. package/dist/testing.d.ts +4 -3
  115. package/dist/testing.js +2478 -2637
  116. package/dist/testing.js.map +1 -1
  117. package/dist/tool-server-RcWgLIsL.js +141 -0
  118. package/dist/tool-server-RcWgLIsL.js.map +1 -0
  119. package/dist/trace-propagation-CJJC7SVB.js +57 -0
  120. package/dist/trace-propagation-CJJC7SVB.js.map +1 -0
  121. package/dist/types-BaIpb0XT.d.ts +492 -0
  122. package/dist/types-BevOjfTY.d.ts +765 -0
  123. package/dist/types-C9j4qg6l.d.ts +500 -0
  124. package/dist/types-Cja5OTGh.d.ts +190 -0
  125. package/dist/util-MVgdwuIS.js +145 -0
  126. package/dist/util-MVgdwuIS.js.map +1 -0
  127. package/dist/workspace-archive-BosQiotA.js +575 -0
  128. package/dist/workspace-archive-BosQiotA.js.map +1 -0
  129. package/package.json +43 -55
  130. package/skills/build-with-agent-runtime/SKILL.md +16 -9
  131. package/skills/loop-writer/SKILL.md +1 -1
  132. package/dist/activation-Cm-9MMX1.d.ts +0 -69
  133. package/dist/analyst-loop.js.map +0 -1
  134. package/dist/backends-BO4Jqoe2.d.ts +0 -134
  135. package/dist/candidate-execution/index.js.map +0 -1
  136. package/dist/chunk-2KGAN2HM.js +0 -1256
  137. package/dist/chunk-2KGAN2HM.js.map +0 -1
  138. package/dist/chunk-2QJSNOGK.js +0 -48
  139. package/dist/chunk-2QJSNOGK.js.map +0 -1
  140. package/dist/chunk-2UABGGUB.js +0 -474
  141. package/dist/chunk-2UABGGUB.js.map +0 -1
  142. package/dist/chunk-2VZYRB55.js +0 -834
  143. package/dist/chunk-2VZYRB55.js.map +0 -1
  144. package/dist/chunk-2X65HYMI.js +0 -3526
  145. package/dist/chunk-2X65HYMI.js.map +0 -1
  146. package/dist/chunk-3MDZX7YU.js +0 -125
  147. package/dist/chunk-3MDZX7YU.js.map +0 -1
  148. package/dist/chunk-3PPVCS75.js +0 -199
  149. package/dist/chunk-3PPVCS75.js.map +0 -1
  150. package/dist/chunk-5HNDYWUT.js +0 -6424
  151. package/dist/chunk-5HNDYWUT.js.map +0 -1
  152. package/dist/chunk-5IBQAPVB.js +0 -29
  153. package/dist/chunk-5IBQAPVB.js.map +0 -1
  154. package/dist/chunk-5QOB7H74.js +0 -387
  155. package/dist/chunk-5QOB7H74.js.map +0 -1
  156. package/dist/chunk-ATDFUJYI.js +0 -4728
  157. package/dist/chunk-ATDFUJYI.js.map +0 -1
  158. package/dist/chunk-B7K7V22Y.js +0 -699
  159. package/dist/chunk-B7K7V22Y.js.map +0 -1
  160. package/dist/chunk-DPEUKJRO.js +0 -27
  161. package/dist/chunk-DPEUKJRO.js.map +0 -1
  162. package/dist/chunk-FD2MBMOH.js +0 -494
  163. package/dist/chunk-FD2MBMOH.js.map +0 -1
  164. package/dist/chunk-FJI3EXU3.js +0 -2402
  165. package/dist/chunk-FJI3EXU3.js.map +0 -1
  166. package/dist/chunk-HAO4CXHP.js +0 -200
  167. package/dist/chunk-HAO4CXHP.js.map +0 -1
  168. package/dist/chunk-ISPWRSEC.js +0 -408
  169. package/dist/chunk-ISPWRSEC.js.map +0 -1
  170. package/dist/chunk-KRBFHMV6.js +0 -1228
  171. package/dist/chunk-KRBFHMV6.js.map +0 -1
  172. package/dist/chunk-L5DST3QC.js +0 -351
  173. package/dist/chunk-L5DST3QC.js.map +0 -1
  174. package/dist/chunk-M22PL4GH.js +0 -171
  175. package/dist/chunk-M22PL4GH.js.map +0 -1
  176. package/dist/chunk-NURIQBJQ.js +0 -72
  177. package/dist/chunk-NURIQBJQ.js.map +0 -1
  178. package/dist/chunk-PZZKQVQV.js +0 -52
  179. package/dist/chunk-PZZKQVQV.js.map +0 -1
  180. package/dist/chunk-Q2JSAVQ3.js +0 -671
  181. package/dist/chunk-Q2JSAVQ3.js.map +0 -1
  182. package/dist/chunk-SBTWKPVR.js +0 -5576
  183. package/dist/chunk-SBTWKPVR.js.map +0 -1
  184. package/dist/chunk-SGKPNBXE.js +0 -60
  185. package/dist/chunk-SGKPNBXE.js.map +0 -1
  186. package/dist/chunk-SNSMRT6H.js +0 -680
  187. package/dist/chunk-SNSMRT6H.js.map +0 -1
  188. package/dist/chunk-STJEHM5F.js +0 -1338
  189. package/dist/chunk-STJEHM5F.js.map +0 -1
  190. package/dist/chunk-UPWGXKXB.js +0 -254
  191. package/dist/chunk-UPWGXKXB.js.map +0 -1
  192. package/dist/chunk-YEJR7IXO.js +0 -68
  193. package/dist/chunk-YEJR7IXO.js.map +0 -1
  194. package/dist/chunk-ZXICDSAK.js +0 -907
  195. package/dist/chunk-ZXICDSAK.js.map +0 -1
  196. package/dist/completion-gate-Bv47cOnn.d.ts +0 -256
  197. package/dist/conversation.js.map +0 -1
  198. package/dist/coordination-CxtzMxSM.d.ts +0 -2570
  199. package/dist/environment-provider.js.map +0 -1
  200. package/dist/improve--2A0gK1-.d.ts +0 -440
  201. package/dist/improvement-adapter-HAZz-7vK.d.ts +0 -203
  202. package/dist/knowledge.js.map +0 -1
  203. package/dist/local-harness-CtK9dSny.d.ts +0 -175
  204. package/dist/loop-runner-bin-DUOGtvOl.d.ts +0 -207
  205. package/dist/loop-runner-bin.js.map +0 -1
  206. package/dist/loops.js.map +0 -1
  207. package/dist/openai-tools-fnj6SRVg.d.ts +0 -46
  208. package/dist/protected-model-port-DzkUY3HQ.d.ts +0 -271
  209. package/dist/sanitize-DEbPNtyI.d.ts +0 -328
  210. package/dist/structural-rollout-C4mabf87.d.ts +0 -446
  211. package/dist/supervise-7q9F5brx.d.ts +0 -379
  212. package/dist/types-BCoemcXU.d.ts +0 -802
  213. package/dist/types-BwoZWq-i.d.ts +0 -510
  214. package/dist/types-Cbp72qyu.d.ts +0 -491
  215. package/dist/types-CmYCMbFT.d.ts +0 -203
  216. package/dist/types-CvNwMzQt.d.ts +0 -957
  217. package/dist/worktree-fanout-WsOzt8qu.d.ts +0 -999
@@ -0,0 +1,3826 @@
1
+ import { c as RuntimeRunStateError, u as ValidationError } from "./errors-DEAvWQPy.js";
2
+ import { f as zeroTokenUsage, t as addTokenUsage } from "./util-MVgdwuIS.js";
3
+ import { a as contentAddress, c as replaySpawnTree, o as materializeTreeView, s as pendingWaits } from "./spawn-journal-CwPvKUTa.js";
4
+ import { estimateCost, isModelPriced } from "@tangle-network/agent-eval";
5
+ import { createHash, randomUUID } from "node:crypto";
6
+ import { applyWorkspacePlan, materializeProfile } from "@tangle-network/agent-profile-materialize";
7
+ import { access, chmod, copyFile, mkdir, mkdtemp, open, readFile, realpath, rm, symlink, writeFile } from "node:fs/promises";
8
+ import { constants, createReadStream } from "node:fs";
9
+ import { basename, delimiter, dirname, isAbsolute, join, resolve, sep, win32 } from "node:path";
10
+ import { spawn } from "node:child_process";
11
+ import { homedir, tmpdir } from "node:os";
12
+ import { stripVTControlCharacters } from "node:util";
13
+ //#region src/mcp/codex-diagnostics.ts
14
+ /** Thrown when reproducible Codex exits without one valid terminal usage event. */
15
+ var CodexExecutionDiagnosticError = class extends Error {
16
+ reason;
17
+ diagnostic;
18
+ code = "CODEX_EXECUTION_DIAGNOSTIC";
19
+ constructor(reason, diagnostic, cause) {
20
+ super(`runLocalHarness: reproducible Codex execution failed: ${reason}; diagnostic=${JSON.stringify(diagnostic)}`, { cause });
21
+ this.reason = reason;
22
+ this.diagnostic = diagnostic;
23
+ this.name = "CodexExecutionDiagnosticError";
24
+ }
25
+ };
26
+ const CODEX_FAILURE_STREAM_MAX_CHARS = 4096;
27
+ const CODEX_AUTH_MAX_BYTES = 1024 * 1024;
28
+ const CODEX_REDACTED = "<REDACTED_CREDENTIAL>";
29
+ const codexSensitiveEnvironmentName = /(TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL|AUTH|COOKIE)/i;
30
+ function createCodexExecutionDiagnosticError(opts) {
31
+ const redact = (value) => redactCodexDiagnostic(value, opts.codexHome, opts.redactionValues);
32
+ const boundedStdout = boundCodexDiagnostic(redact(opts.stdout));
33
+ const boundedStderr = boundCodexDiagnostic(redact(opts.stderr));
34
+ const diagnostic = {
35
+ exitCode: opts.exitCode,
36
+ killedBySignal: opts.killedBySignal,
37
+ timedOut: opts.timedOut,
38
+ ...opts.aborted !== void 0 ? { aborted: opts.aborted } : {},
39
+ durationMs: opts.durationMs,
40
+ stdout: boundedStdout.value,
41
+ stderr: boundedStderr.value,
42
+ stdoutTruncated: boundedStdout.truncated,
43
+ stderrTruncated: boundedStderr.truncated
44
+ };
45
+ const safeReason = boundCodexDiagnostic(redact(opts.reason), 1024).value;
46
+ return new CodexExecutionDiagnosticError(safeReason, diagnostic, redactCodexErrorCause(opts.cause, safeReason, redact));
47
+ }
48
+ function redactCodexHome(value, codexHome) {
49
+ return codexHome ? value.replaceAll(codexHome, "<ISOLATED_CODEX_HOME>") : value;
50
+ }
51
+ function collectCodexDiagnosticRedactionValues(env, authText) {
52
+ const values = Object.entries(env).filter(([name, value]) => codexSensitiveEnvironmentName.test(name) && typeof value === "string").map(([, value]) => value);
53
+ try {
54
+ collectStringLeaves(JSON.parse(authText), values);
55
+ } catch {}
56
+ const encoded = values.filter((value) => value.length >= 8).flatMap((value) => [
57
+ Buffer.from(value, "utf8").toString("base64"),
58
+ Buffer.from(value, "utf8").toString("base64url"),
59
+ encodeURIComponent(value),
60
+ JSON.stringify(value).slice(1, -1)
61
+ ]);
62
+ return [.../* @__PURE__ */ new Set([...values, ...encoded])];
63
+ }
64
+ async function readCodexAuthRedactionText(path) {
65
+ const handle = await open(path, constants.O_RDONLY | constants.O_NONBLOCK);
66
+ try {
67
+ const stats = await handle.stat();
68
+ if (!stats.isFile() || !Number.isSafeInteger(stats.size) || stats.size > CODEX_AUTH_MAX_BYTES) throw new Error("Codex auth.json must be a regular file no larger than 1 MiB");
69
+ const buffer = Buffer.alloc(stats.size);
70
+ let offset = 0;
71
+ while (offset < buffer.length) {
72
+ const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
73
+ if (bytesRead === 0) break;
74
+ offset += bytesRead;
75
+ }
76
+ return buffer.subarray(0, offset).toString("utf8");
77
+ } finally {
78
+ await handle.close();
79
+ }
80
+ }
81
+ function boundCodexDiagnostic(value, maxChars = CODEX_FAILURE_STREAM_MAX_CHARS) {
82
+ if (value.length <= maxChars) return {
83
+ value,
84
+ truncated: false
85
+ };
86
+ const marker = "\n<... OUTPUT TRUNCATED ...>\n";
87
+ const contentChars = maxChars - 28;
88
+ const headChars = Math.floor(contentChars / 2);
89
+ const tailChars = contentChars - headChars;
90
+ return {
91
+ value: `${value.slice(0, headChars)}${marker}${value.slice(-tailChars)}`,
92
+ truncated: true
93
+ };
94
+ }
95
+ function redactCodexDiagnostic(value, codexHome, protectedValues) {
96
+ let redacted = redactCodexHome(stripVTControlCharacters(value), codexHome);
97
+ const exactValues = [...new Set(protectedValues.filter((item) => item.length >= 6))].sort((a, b) => b.length - a.length);
98
+ for (const secret of exactValues) redacted = redacted.replaceAll(secret, CODEX_REDACTED);
99
+ redacted = redacted.replace(/(\bBearer\s+)[A-Za-z0-9._~+/=-]{6,}/gi, `$1${CODEX_REDACTED}`).replace(/\b(?:sk|rk|pk)-[A-Za-z0-9._-]{8,}\b/g, CODEX_REDACTED).replace(/\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\b/g, CODEX_REDACTED).replace(/((?:["']?(?:api[-_]?key|access[-_]?token|refresh[-_]?token|id[-_]?token|client[-_]?secret|private[-_]?key|session[-_]?(?:id|token)|secret|password|passwd|authorization|credential|cookie)["']?)\s*[:=]\s*)(["'])[^"'\r\n]*\2/gi, `$1$2${CODEX_REDACTED}$2`).replace(/((?:["']?(?:api[-_]?key|access[-_]?token|refresh[-_]?token|id[-_]?token|client[-_]?secret|private[-_]?key|session[-_]?(?:id|token)|secret|password|passwd|authorization|credential|cookie)["']?)\s*[:=]\s*)([^\s,;}]+)/gi, `$1${CODEX_REDACTED}`);
100
+ for (const secret of exactValues) redacted = redacted.replaceAll(secret, CODEX_REDACTED);
101
+ return redacted;
102
+ }
103
+ function redactCodexErrorCause(cause, safeReason, redact) {
104
+ if (!(cause instanceof Error)) return void 0;
105
+ const safeCause = new Error(safeReason);
106
+ safeCause.name = cause.name;
107
+ if (cause.stack) safeCause.stack = boundCodexDiagnostic(redact(cause.stack)).value;
108
+ return safeCause;
109
+ }
110
+ function collectStringLeaves(value, output) {
111
+ if (typeof value === "string") {
112
+ output.push(value);
113
+ return;
114
+ }
115
+ if (Array.isArray(value)) {
116
+ for (const item of value) collectStringLeaves(item, output);
117
+ return;
118
+ }
119
+ if (value !== null && typeof value === "object") for (const item of Object.values(value)) collectStringLeaves(item, output);
120
+ }
121
+ //#endregion
122
+ //#region src/mcp/local-harness.ts
123
+ /**
124
+ *
125
+ * Subprocess wrappers for the local coding-harness CLIs installed in the
126
+ * sandbox image (claude-code, codex, opencode). Used by the in-process
127
+ * delegation executor (`createInProcessExecutor`) so a delegated coding task
128
+ * spawns a real harness on a real git worktree instead of provisioning a
129
+ * sibling sandbox.
130
+ *
131
+ * All harness invocations:
132
+ * - run with `cwd` set to the worktree
133
+ * - inherit env from the parent (the MCP server inside the sandbox has
134
+ * the harness's auth already)
135
+ * - capture stdout/stderr
136
+ * - support cancellation via AbortSignal
137
+ * - enforce a wall-clock timeout
138
+ *
139
+ * @experimental
140
+ */
141
+ const codexReasoningEffort = {
142
+ none: "none",
143
+ minimal: "minimal",
144
+ low: "low",
145
+ medium: "medium",
146
+ high: "high",
147
+ xhigh: "xhigh",
148
+ ultracode: "xhigh"
149
+ };
150
+ function codexReasoningArgs(reasoningEffort) {
151
+ const mapped = codexReasoningEffort[reasoningEffort];
152
+ if (mapped === void 0) throw new Error(`harnessInvocation: unsupported Codex reasoning effort ${String(reasoningEffort)}`);
153
+ return ["-c", `model_reasoning_effort="${mapped}"`];
154
+ }
155
+ /**
156
+ * Default per-harness command + arg shape. `buildArgs` takes ONLY the task prompt and
157
+ * emits the prompt-only invocation (no model, no system prompt) — the safe default shape
158
+ * the in-process executor's `streamPrompt` drives. `modelArgs` maps a resolved model to
159
+ * the harness's selector flag (every supported harness takes `-m <model>`). The §1.5
160
+ * profile-aware mapper `harnessInvocation` composes these to thread the full
161
+ * supervisor-authored profile (systemPrompt + model) into argv.
162
+ */
163
+ const HARNESS_INVOCATIONS = {
164
+ claude: {
165
+ command: "claude",
166
+ buildArgs: (taskPrompt) => ["-p", taskPrompt],
167
+ modelArgs: (model) => ["-m", model]
168
+ },
169
+ codex: {
170
+ command: "codex",
171
+ buildArgs: (taskPrompt) => ["exec", taskPrompt],
172
+ modelArgs: (model) => ["-m", model],
173
+ reasoningArgs: codexReasoningArgs
174
+ },
175
+ opencode: {
176
+ command: "opencode",
177
+ buildArgs: (taskPrompt) => ["run", taskPrompt],
178
+ modelArgs: (model) => ["-m", model]
179
+ }
180
+ };
181
+ const CODEX_REPRODUCIBLE_ARGS = [
182
+ "--ephemeral",
183
+ "--ignore-rules",
184
+ "--json",
185
+ "-c",
186
+ "approval_policy=\"never\"",
187
+ "-c",
188
+ "web_search=\"disabled\"",
189
+ "-c",
190
+ "project_doc_max_bytes=0",
191
+ "-c",
192
+ "skills.include_instructions=false",
193
+ "-c",
194
+ "include_apps_instructions=false",
195
+ "--disable",
196
+ "tool_suggest",
197
+ "-c",
198
+ "features.multi_agent_v2.root_agent_usage_hint_text=\"\"",
199
+ "-c",
200
+ "features.multi_agent_v2.subagent_usage_hint_text=\"\"",
201
+ "-c",
202
+ "features.multi_agent_v2.multi_agent_mode_hint_text=\"\"",
203
+ "--strict-config"
204
+ ];
205
+ function buildHarnessArgs(harness, taskPrompt, options = {}) {
206
+ const args = HARNESS_INVOCATIONS[harness].buildArgs(taskPrompt);
207
+ if (harness === "claude" && options.dangerouslySkipPermissions) args.push("--dangerously-skip-permissions");
208
+ if (options.codexReproducible) {
209
+ if (harness !== "codex") throw new Error("harnessInvocation: codexReproducible requires the Codex harness");
210
+ args.push(...CODEX_REPRODUCIBLE_ARGS);
211
+ }
212
+ return args;
213
+ }
214
+ /**
215
+ * Map a supervisor-authored `AgentProfile` + the per-task prompt onto a concrete harness
216
+ * `command` + `args` (the §1.5 fix). UNLIKE the prompt-only `HARNESS_INVOCATIONS.buildArgs`
217
+ * — which drops both the authored model and the system prompt — this threads the FULL
218
+ * profile payload into argv:
219
+ *
220
+ * - `profile.prompt.systemPrompt` → the PROMPT channel: a portable, harness-agnostic
221
+ * default that prepends the system prompt above the task prompt (`<system>\n\n<task>`),
222
+ * so the authored standing instructions reach EVERY harness (none of the three CLIs
223
+ * expose a portable replace-system-prompt flag for a one-shot non-interactive run).
224
+ * - `profile.prompt.instructions[]` → appended prompt sections in authored order.
225
+ * - `profile.model.default` → the harness's `-m <model>` selector.
226
+ *
227
+ * The task prompt alone is the floor; an empty/absent profile yields exactly the legacy
228
+ * `buildArgs(taskPrompt)` shape so existing callers are byte-identical.
229
+ */
230
+ function harnessInvocation(harness, profile, taskPrompt, options = {}) {
231
+ const invocation = HARNESS_INVOCATIONS[harness];
232
+ if (!invocation) throw new Error(`harnessInvocation: unknown harness ${String(harness)}`);
233
+ if (options.codexReproducible) {
234
+ const model = profile.model?.default;
235
+ if (typeof model !== "string" || model.trim().length === 0) throw new Error("harnessInvocation: codexReproducible requires profile.model.default");
236
+ if (profile.model?.reasoningEffort === void 0) throw new Error("harnessInvocation: codexReproducible requires profile.model.reasoningEffort");
237
+ }
238
+ const systemPrompt = profile.prompt?.systemPrompt;
239
+ const instructions = profile.prompt?.instructions;
240
+ if (instructions !== void 0 && (!Array.isArray(instructions) || instructions.some((instruction) => typeof instruction !== "string"))) throw new Error("harnessInvocation: profile.prompt.instructions must be an array of strings");
241
+ const composedPrompt = [
242
+ ...typeof systemPrompt === "string" && systemPrompt.trim().length > 0 ? [systemPrompt] : [],
243
+ ...instructions?.filter((instruction) => instruction.trim().length > 0) ?? [],
244
+ taskPrompt
245
+ ].join("\n\n");
246
+ const args = buildHarnessArgs(harness, composedPrompt, options);
247
+ const model = profile.model?.default;
248
+ if (typeof model === "string" && model.length > 0) args.push(...invocation.modelArgs(model));
249
+ const reasoningEffort = profile.model?.reasoningEffort;
250
+ if (reasoningEffort !== void 0 && invocation.reasoningArgs) args.push(...invocation.reasoningArgs(reasoningEffort));
251
+ return {
252
+ command: invocation.command,
253
+ args,
254
+ prompt: composedPrompt
255
+ };
256
+ }
257
+ const DEFAULT_TIMEOUT_MS = 300 * 1e3;
258
+ const DEFAULT_MAX_OUTPUT_BYTES = 64 * 1024 * 1024;
259
+ const processKillGraceMs = 250;
260
+ const processGroupExitConfirmMs = 1e3;
261
+ const processGroupExitPollMs = 10;
262
+ var RollingByteCapture = class {
263
+ maxBytes;
264
+ chunks = [];
265
+ bytes = 0;
266
+ constructor(maxBytes) {
267
+ this.maxBytes = maxBytes;
268
+ }
269
+ append(chunk) {
270
+ if (this.maxBytes === 0) return;
271
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
272
+ this.chunks.push(buffer);
273
+ this.bytes += buffer.length;
274
+ let excess = this.bytes - this.maxBytes;
275
+ while (excess > 0 && this.chunks.length > 0) {
276
+ const first = this.chunks[0];
277
+ if (first.length <= excess) {
278
+ this.chunks.shift();
279
+ this.bytes -= first.length;
280
+ excess -= first.length;
281
+ } else {
282
+ this.chunks[0] = first.subarray(excess);
283
+ this.bytes -= excess;
284
+ excess = 0;
285
+ }
286
+ }
287
+ }
288
+ text() {
289
+ return Buffer.concat(this.chunks, this.bytes).toString("utf8");
290
+ }
291
+ };
292
+ /**
293
+ * Spawn a local coding harness CLI as a subprocess + collect its output.
294
+ *
295
+ * NOT responsible for parsing the harness's output or extracting a diff —
296
+ * the in-process executor's `streamPrompt` orchestrates `git diff` against
297
+ * the worktree after this resolves. This function is intentionally narrow:
298
+ * spawn, wait, capture, return.
299
+ *
300
+ * Fails loud — throws when:
301
+ * - `cwd` doesn't exist (subprocess emits ENOENT; surfaced as Error)
302
+ * - the harness binary is not on PATH (ENOENT)
303
+ * - the caller signal was already aborted before process launch
304
+ *
305
+ * Does NOT throw when:
306
+ * - the subprocess exits non-zero (`result.exitCode` carries the code)
307
+ * - a non-reproducible subprocess is aborted / timed out (`result.aborted` /
308
+ * `result.timedOut` carries the reason even when a TERM-aware child exits zero)
309
+ *
310
+ * Reproducible Codex additionally requires a terminal usage event. If cancellation
311
+ * prevents that event, this rejects with `CodexExecutionDiagnosticError` instead of
312
+ * returning an incomplete reproducibility receipt.
313
+ *
314
+ * @experimental
315
+ */
316
+ async function runLocalHarness(options) {
317
+ const { harness, cwd, taskPrompt } = options;
318
+ if (options.codexReproducible && harness !== "codex") throw new Error("runLocalHarness: codexReproducible requires the Codex harness");
319
+ if (options.codexReproducible && process.platform !== "linux") throw new Error("runLocalHarness: codexReproducible currently requires Linux");
320
+ if (options.codexReadDeniedPaths !== void 0 && !options.codexReproducible) throw new Error("runLocalHarness: codexReadDeniedPaths requires codexReproducible");
321
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
322
+ const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
323
+ if (!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 0) throw new Error("runLocalHarness: maxOutputBytes must be a non-negative safe integer");
324
+ const spawnImpl = options.spawn ?? spawn;
325
+ const invocation = HARNESS_INVOCATIONS[harness];
326
+ if (!invocation) throw new Error(`runLocalHarness: unknown harness ${String(harness)}`);
327
+ const startedAt = Date.now();
328
+ const requestedCommand = options.invocation?.command ?? invocation.command;
329
+ const args = options.invocation ? [...options.invocation.args] : buildHarnessArgs(harness, taskPrompt, options);
330
+ if (options.codexReproducible) assertCodexReproducibleInvocation(requestedCommand, args);
331
+ options.signal?.throwIfAborted();
332
+ const baseEnv = {
333
+ ...options.env ?? process.env,
334
+ PWD: cwd
335
+ };
336
+ const isolated = options.codexReproducible ? await isolateCodexHome({
337
+ baseEnv,
338
+ cwd,
339
+ command: requestedCommand,
340
+ readDeniedPaths: options.codexReadDeniedPaths ?? [],
341
+ resolveExecutable: options.resolveCodexExecutable ?? resolveCodexNativeExecutable
342
+ }) : {
343
+ env: baseEnv,
344
+ command: requestedCommand,
345
+ cleanup: async () => void 0,
346
+ controlledConfigSha256: "",
347
+ executableSha256: "",
348
+ readDeniedPathsSha256: "",
349
+ readDeniedPathCount: 0,
350
+ readDeniedPaths: [],
351
+ diagnosticRedactionValues: [],
352
+ ambientAuth: "",
353
+ isolatedAuth: "",
354
+ writeProbe: "",
355
+ stagedWriteProbe: "",
356
+ deniedProbePaths: []
357
+ };
358
+ const env = isolated.env;
359
+ const command = isolated.command;
360
+ if (options.codexReproducible) assertCodexReproducibleInvocation(command, args);
361
+ try {
362
+ const evidence = options.codexReproducible ? await collectCodexExecutionEvidence({
363
+ command,
364
+ args,
365
+ cwd,
366
+ env,
367
+ spawn: spawnImpl,
368
+ controlledConfigSha256: isolated.controlledConfigSha256,
369
+ executableSha256: isolated.executableSha256,
370
+ readDeniedPathsSha256: isolated.readDeniedPathsSha256,
371
+ readDeniedPathCount: isolated.readDeniedPathCount,
372
+ readDeniedPaths: isolated.readDeniedPaths,
373
+ ambientAuth: isolated.ambientAuth,
374
+ isolatedAuth: isolated.isolatedAuth,
375
+ writeProbe: isolated.writeProbe,
376
+ stagedWriteProbe: isolated.stagedWriteProbe,
377
+ deniedProbePaths: isolated.deniedProbePaths,
378
+ ...options.signal ? { signal: options.signal } : {}
379
+ }) : void 0;
380
+ return await new Promise((resolve, reject) => {
381
+ let child;
382
+ try {
383
+ options.signal?.throwIfAborted();
384
+ child = spawnImpl(command, args, {
385
+ cwd,
386
+ env,
387
+ stdio: "pipe",
388
+ detached: process.platform !== "win32"
389
+ });
390
+ } catch (err) {
391
+ reject(err instanceof Error ? err : new Error(String(err)));
392
+ return;
393
+ }
394
+ child.stdin?.end();
395
+ const stdoutCapture = new RollingByteCapture(maxOutputBytes);
396
+ const stderrCapture = new RollingByteCapture(maxOutputBytes);
397
+ let timedOut = false;
398
+ let aborted = false;
399
+ let settled = false;
400
+ let leaderClosed = false;
401
+ let timer = null;
402
+ let termination = null;
403
+ const clearLifecycle = () => {
404
+ if (timer) clearTimeout(timer);
405
+ options.signal?.removeEventListener("abort", onAbort);
406
+ };
407
+ const rejectOnce = (err) => {
408
+ if (settled) return;
409
+ settled = true;
410
+ clearLifecycle();
411
+ reject(err instanceof Error ? err : new Error(String(err)));
412
+ };
413
+ const terminate = () => {
414
+ if (!termination) {
415
+ termination = terminateProcessTreeAndConfirm(child, () => leaderClosed);
416
+ termination.catch(rejectOnce);
417
+ }
418
+ return termination;
419
+ };
420
+ timer = timeoutMs > 0 ? setTimeout(() => {
421
+ timedOut = true;
422
+ terminate();
423
+ }, timeoutMs) : null;
424
+ if (timer && typeof timer.unref === "function") timer.unref();
425
+ const onAbort = () => {
426
+ aborted = true;
427
+ terminate();
428
+ };
429
+ if (options.signal) if (options.signal.aborted) onAbort();
430
+ else options.signal.addEventListener("abort", onAbort, { once: true });
431
+ child.stdout?.on("data", (chunk) => {
432
+ stdoutCapture.append(chunk);
433
+ });
434
+ child.stderr?.on("data", (chunk) => {
435
+ stderrCapture.append(chunk);
436
+ });
437
+ const finalize = (result) => {
438
+ if (settled) return;
439
+ settled = true;
440
+ clearLifecycle();
441
+ resolve(result);
442
+ };
443
+ child.on("error", (err) => {
444
+ leaderClosed = true;
445
+ if (timer) clearTimeout(timer);
446
+ (async () => {
447
+ try {
448
+ if (termination) await termination;
449
+ rejectOnce(err);
450
+ } catch (terminationError) {
451
+ rejectOnce(terminationError);
452
+ }
453
+ })();
454
+ });
455
+ child.on("close", (code, signal) => {
456
+ leaderClosed = true;
457
+ if (timer) clearTimeout(timer);
458
+ (async () => {
459
+ const stdout = stdoutCapture.text();
460
+ const stderr = stderrCapture.text();
461
+ try {
462
+ if (!termination && process.platform !== "win32" && typeof child.pid === "number" && processGroupExists(child.pid)) await terminate();
463
+ if (termination) await termination;
464
+ const durationMs = Date.now() - startedAt;
465
+ const usage = options.codexReproducible ? parseCodexTokenUsage(stdout) : void 0;
466
+ finalize({
467
+ exitCode: code,
468
+ stdout,
469
+ stderr: redactCodexHome(stderr, env.CODEX_HOME),
470
+ killedBySignal: signal,
471
+ durationMs,
472
+ timedOut,
473
+ aborted,
474
+ ...usage ? { usage } : {},
475
+ ...evidence ? { evidence } : {}
476
+ });
477
+ } catch (err) {
478
+ if (settled) return;
479
+ settled = true;
480
+ clearLifecycle();
481
+ const durationMs = Date.now() - startedAt;
482
+ const reason = err instanceof Error ? err.message : String(err);
483
+ reject(options.codexReproducible ? createCodexExecutionDiagnosticError({
484
+ reason,
485
+ exitCode: code,
486
+ killedBySignal: signal,
487
+ timedOut,
488
+ aborted,
489
+ durationMs,
490
+ stdout,
491
+ stderr,
492
+ codexHome: env.CODEX_HOME,
493
+ redactionValues: isolated.diagnosticRedactionValues,
494
+ cause: err
495
+ }) : err instanceof Error ? err : new Error(reason));
496
+ }
497
+ })();
498
+ });
499
+ });
500
+ } finally {
501
+ await isolated.cleanup();
502
+ }
503
+ }
504
+ const CODEX_EXECUTION_POLICY = {
505
+ sessionPersistence: "ephemeral",
506
+ userConfig: false,
507
+ rules: false,
508
+ projectInstructions: false,
509
+ skillInstructions: false,
510
+ appInstructions: false,
511
+ toolSuggestions: false,
512
+ multiAgentInstructions: false,
513
+ sandbox: "workspace-write",
514
+ permissionProfile: "agent_runtime_reproducible",
515
+ approvalPolicy: "never",
516
+ shellNetwork: false,
517
+ webSearch: false,
518
+ serviceTier: "default",
519
+ shellEnvironment: "core-filtered",
520
+ loginShell: false,
521
+ credentialsReadable: false,
522
+ hostHomeReadable: false,
523
+ procEnvironment: "private-sanitized",
524
+ sensitiveEnvironmentNamesVisible: false,
525
+ parentRepoRead: false,
526
+ gitMetadata: false,
527
+ temporaryDirectory: "workspace-private",
528
+ stagedExecutable: "static-elf-read-only",
529
+ callerReadDeniedPaths: "enforced",
530
+ containerSockets: false
531
+ };
532
+ const FORBIDDEN_CODEX_PROMPT_MARKERS = [
533
+ "<skills_instructions>",
534
+ "# AGENTS.md instructions",
535
+ "<apps_instructions>",
536
+ "<recommended_plugins>",
537
+ "<multi_agent_mode>",
538
+ "You are `/root`, the primary agent"
539
+ ];
540
+ async function collectCodexExecutionEvidence(opts) {
541
+ assertCodexReproducibleInvocation(opts.command, opts.args);
542
+ const prompt = opts.args[1];
543
+ if (opts.args[0] !== "exec" || typeof prompt !== "string" || prompt.length === 0) throw new Error("runLocalHarness: reproducible Codex invocation must be codex exec <prompt>");
544
+ const cliVersion = (await runCodexProbe(opts, ["--version"])).stdout.trim();
545
+ if (!/^codex-cli \d+\.\d+\.\d+(?:[-+].+)?$/.test(cliVersion)) throw new Error(`runLocalHarness: unexpected Codex CLI version output ${JSON.stringify(cliVersion)}`);
546
+ const sandboxProof = [
547
+ [
548
+ "CODEX_HOME",
549
+ "CODEX_ACCESS_TOKEN",
550
+ "CODEX_API_KEY",
551
+ "OPENAI_API_KEY"
552
+ ].map((name) => `test -z "\${${name}:-}"`).join(" && "),
553
+ "test -r .",
554
+ "! test -r .git",
555
+ "touch \"$1\"",
556
+ "rm \"$1\"",
557
+ "staged_dir=$(dirname \"$2\")",
558
+ "test -x \"$staged_dir/codex\"",
559
+ "! chmod 700 \"$staged_dir\" 2>/dev/null",
560
+ "! rm \"$staged_dir/codex\" 2>/dev/null",
561
+ "! touch \"$2\" 2>/dev/null",
562
+ "shift 2",
563
+ "for path in \"$@\"; do ! test -r \"$path\" || exit 1; done",
564
+ "touch \"$TMPDIR/.agent-runtime-tmp-proof\"",
565
+ "rm \"$TMPDIR/.agent-runtime-tmp-proof\""
566
+ ].join(" && ");
567
+ await runCodexProbe(opts, [
568
+ "sandbox",
569
+ "-P",
570
+ CODEX_PERMISSION_PROFILE,
571
+ "-C",
572
+ opts.cwd,
573
+ "sh",
574
+ "-c",
575
+ sandboxProof,
576
+ "sh",
577
+ opts.writeProbe,
578
+ opts.stagedWriteProbe,
579
+ ...opts.deniedProbePaths
580
+ ]);
581
+ await runCodexProbe(opts, [
582
+ "sandbox",
583
+ "-P",
584
+ CODEX_PERMISSION_PROFILE,
585
+ "-C",
586
+ opts.cwd,
587
+ "python3",
588
+ "-c",
589
+ `import os,re
590
+ pattern=re.compile(br'(TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL|AUTH|COOKIE|CODEX_HOME)', re.I)
591
+ paths=('/proc/1/environ', '/proc/self/environ', f'/proc/{os.getppid()}/environ')
592
+ for path in dict.fromkeys(paths):
593
+ data=open(path,'rb').read().split(b'\\0')
594
+ names=(item.split(b'=',1)[0] for item in data if b'=' in item)
595
+ assert not any(pattern.search(name) for name in names), path`
596
+ ]);
597
+ await runCodexProbe(opts, [
598
+ "sandbox",
599
+ "-P",
600
+ CODEX_PERMISSION_PROFILE,
601
+ "-C",
602
+ opts.cwd,
603
+ "python3",
604
+ "-c",
605
+ "import socket,sys\nfor path in sys.argv[1:]:\n s=socket.socket(socket.AF_UNIX)\n try: s.connect(path)\n except OSError: continue\n raise SystemExit(1)",
606
+ "/var/run/docker.sock",
607
+ "/run/docker.sock",
608
+ "/var/run/containerd/containerd.sock",
609
+ "/run/containerd/containerd.sock"
610
+ ]);
611
+ const renderedPrompt = (await runCodexProbe(opts, codexPromptDebugArgs(opts.args, prompt))).stdout;
612
+ let parsedPrompt;
613
+ try {
614
+ parsedPrompt = JSON.parse(renderedPrompt);
615
+ } catch {
616
+ throw new Error("runLocalHarness: codex debug prompt-input returned invalid JSON");
617
+ }
618
+ const promptEvidenceText = normalizeCodexDebugPromptNewlines(prompt);
619
+ if (!Array.isArray(parsedPrompt) || !jsonTextContains(parsedPrompt, promptEvidenceText)) throw new Error("runLocalHarness: Codex prompt evidence did not contain the exact task prompt");
620
+ const promptContextWithoutTask = JSON.stringify(redactJsonText(parsedPrompt, promptEvidenceText));
621
+ for (const marker of FORBIDDEN_CODEX_PROMPT_MARKERS) if (promptContextWithoutTask.includes(marker)) throw new Error(`runLocalHarness: Codex prompt evidence contains forbidden marker ${marker}`);
622
+ for (const marker of ["<permissions instructions>", "<environment_context>"]) if (!renderedPrompt.includes(marker)) throw new Error(`runLocalHarness: Codex prompt evidence is missing required marker ${marker}`);
623
+ const nonPromptArgs = ["codex", ...opts.args.map((arg, index) => index === 1 ? "<PROMPT>" : arg)];
624
+ return {
625
+ cliVersion,
626
+ executableSha256: opts.executableSha256,
627
+ requestedPromptSha256: sha256(prompt),
628
+ effectivePromptSha256: sha256(renderedPrompt),
629
+ nonPromptArgsSha256: sha256(JSON.stringify(nonPromptArgs)),
630
+ controlledConfigSha256: opts.controlledConfigSha256,
631
+ readDeniedPathsSha256: opts.readDeniedPathsSha256,
632
+ readDeniedPathCount: opts.readDeniedPathCount,
633
+ readDeniedPaths: [...opts.readDeniedPaths],
634
+ policy: { ...CODEX_EXECUTION_POLICY }
635
+ };
636
+ }
637
+ function assertCodexReproducibleInvocation(command, args) {
638
+ const executable = basename(command);
639
+ if (executable !== "codex" && executable !== "codex.exe") throw new Error("runLocalHarness: reproducible Codex invocation must use the Codex executable");
640
+ const expectedPrefix = [
641
+ "exec",
642
+ args[1],
643
+ ...CODEX_REPRODUCIBLE_ARGS
644
+ ];
645
+ const hasExactPrefix = expectedPrefix.every((arg, index) => args[index] === arg);
646
+ const tail = args.slice(expectedPrefix.length);
647
+ const validModel = tail[0] === "-m" && typeof tail[1] === "string" && tail[1].length > 0 && !tail[1].startsWith("-");
648
+ const validReasoning = tail[2] === "-c" && typeof tail[3] === "string" && /^model_reasoning_effort="(?:none|minimal|low|medium|high|xhigh)"$/.test(tail[3]);
649
+ if (!hasExactPrefix || tail.length !== 4 || !validModel || !validReasoning) throw new Error("runLocalHarness: reproducible Codex invocation does not match the required isolated argv");
650
+ }
651
+ function codexPromptDebugArgs(execArgs, prompt) {
652
+ const debugArgs = ["debug", "prompt-input"];
653
+ for (let index = 2; index < execArgs.length; index += 1) {
654
+ const arg = execArgs[index];
655
+ const value = execArgs[index + 1];
656
+ if (arg === "-c" && value !== void 0) {
657
+ debugArgs.push("-c", value);
658
+ index += 1;
659
+ } else if (arg === "--disable" && value !== void 0) {
660
+ debugArgs.push("--disable", value);
661
+ index += 1;
662
+ } else if (arg === "-m" && value !== void 0) {
663
+ debugArgs.push("-c", `model=${JSON.stringify(value)}`);
664
+ index += 1;
665
+ } else if (arg === "-s" && value !== void 0) {
666
+ debugArgs.push("-c", `sandbox_mode=${JSON.stringify(value)}`);
667
+ index += 1;
668
+ }
669
+ }
670
+ debugArgs.push(prompt);
671
+ return debugArgs;
672
+ }
673
+ function runCodexProbe(opts, args) {
674
+ return new Promise((resolve, reject) => {
675
+ let child;
676
+ try {
677
+ child = opts.spawn(opts.command, args, {
678
+ cwd: opts.cwd,
679
+ env: opts.env,
680
+ stdio: "pipe",
681
+ detached: process.platform !== "win32"
682
+ });
683
+ } catch (err) {
684
+ reject(err instanceof Error ? err : new Error(String(err)));
685
+ return;
686
+ }
687
+ child.stdin?.end();
688
+ let stdout = "";
689
+ let stderr = "";
690
+ let settled = false;
691
+ let leaderClosed = false;
692
+ let termination = null;
693
+ let terminationReason = null;
694
+ const clearLifecycle = () => {
695
+ clearTimeout(timer);
696
+ opts.signal?.removeEventListener("abort", onAbort);
697
+ };
698
+ const rejectOnce = (err) => {
699
+ if (settled) return;
700
+ settled = true;
701
+ clearLifecycle();
702
+ reject(err instanceof Error ? err : new Error(String(err)));
703
+ };
704
+ const terminate = (reason) => {
705
+ if (reason) terminationReason ??= reason;
706
+ if (!termination) {
707
+ termination = terminateProcessTreeAndConfirm(child, () => leaderClosed);
708
+ termination.catch(rejectOnce);
709
+ }
710
+ return termination;
711
+ };
712
+ const timer = setTimeout(() => {
713
+ terminate("timed out");
714
+ }, 1e4);
715
+ timer.unref?.();
716
+ const onAbort = () => {
717
+ terminate("aborted");
718
+ };
719
+ if (opts.signal) if (opts.signal.aborted) onAbort();
720
+ else opts.signal.addEventListener("abort", onAbort, { once: true });
721
+ child.stdout?.on("data", (chunk) => {
722
+ stdout += String(chunk);
723
+ });
724
+ child.stderr?.on("data", (chunk) => {
725
+ stderr += String(chunk);
726
+ });
727
+ child.on("error", (err) => {
728
+ leaderClosed = true;
729
+ clearTimeout(timer);
730
+ (async () => {
731
+ try {
732
+ if (termination) await termination;
733
+ rejectOnce(err);
734
+ } catch (terminationError) {
735
+ rejectOnce(terminationError);
736
+ }
737
+ })();
738
+ });
739
+ child.on("close", (code, signal) => {
740
+ leaderClosed = true;
741
+ clearTimeout(timer);
742
+ (async () => {
743
+ try {
744
+ if (!termination && process.platform !== "win32" && typeof child.pid === "number" && processGroupExists(child.pid)) await terminate();
745
+ if (termination) await termination;
746
+ if (settled) return;
747
+ settled = true;
748
+ clearLifecycle();
749
+ if (code !== 0 || terminationReason) {
750
+ const safeStderr = redactCodexHome(stderr, opts.env.CODEX_HOME);
751
+ reject(/* @__PURE__ */ new Error(`runLocalHarness: Codex evidence probe ${terminationReason ?? "failed"} (exit ${String(code)}, signal ${String(signal)}): ${safeStderr.trim()}`));
752
+ return;
753
+ }
754
+ resolve({
755
+ stdout,
756
+ stderr
757
+ });
758
+ } catch (err) {
759
+ rejectOnce(err);
760
+ }
761
+ })();
762
+ });
763
+ });
764
+ }
765
+ function sha256(value) {
766
+ return createHash("sha256").update(value).digest("hex");
767
+ }
768
+ /** Codex's debug renderer converts CRLF pairs to LF. No other whitespace is equivalent. */
769
+ function normalizeCodexDebugPromptNewlines(value) {
770
+ return value.replaceAll("\r\n", "\n");
771
+ }
772
+ function jsonTextContains(value, needle) {
773
+ if (typeof value === "string") return value.includes(needle);
774
+ if (Array.isArray(value)) return value.some((item) => jsonTextContains(item, needle));
775
+ if (isRecord(value)) return Object.values(value).some((item) => jsonTextContains(item, needle));
776
+ return false;
777
+ }
778
+ function redactJsonText(value, text) {
779
+ if (typeof value === "string") return value.replaceAll(text, "");
780
+ if (Array.isArray(value)) return value.map((item) => redactJsonText(item, text));
781
+ if (isRecord(value)) return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactJsonText(item, text)]));
782
+ return value;
783
+ }
784
+ const CODEX_AUTH_ENV = [
785
+ "CODEX_ACCESS_TOKEN",
786
+ "CODEX_API_KEY",
787
+ "OPENAI_API_KEY"
788
+ ];
789
+ const CODEX_PERMISSION_PROFILE = "agent_runtime_reproducible";
790
+ async function isolateCodexHome(opts) {
791
+ const isolatedHome = await mkdtemp(join(tmpdir(), "agent-runtime-codex-"));
792
+ await chmod(isolatedHome, 448);
793
+ let workspaceTemp = "";
794
+ let stagedExecutableDir = "";
795
+ let ownsStagedExecutableDir = false;
796
+ let writeProbe = "";
797
+ const cleanup = async () => {
798
+ if (ownsStagedExecutableDir) await chmod(stagedExecutableDir, 448).catch(() => void 0);
799
+ await Promise.all([
800
+ ...writeProbe ? [rm(writeProbe, { force: true })] : [],
801
+ ...workspaceTemp ? [rm(workspaceTemp, {
802
+ recursive: true,
803
+ force: true
804
+ })] : [],
805
+ ...ownsStagedExecutableDir ? [rm(stagedExecutableDir, {
806
+ recursive: true,
807
+ force: true
808
+ })] : [],
809
+ rm(isolatedHome, {
810
+ recursive: true,
811
+ force: true
812
+ })
813
+ ]);
814
+ };
815
+ try {
816
+ const readDeniedPaths = await normalizeReadDeniedPaths(opts.readDeniedPaths);
817
+ const candidateRoot = await realpath(opts.cwd);
818
+ writeProbe = join(candidateRoot, `.agent-runtime-write-proof-${randomUUID()}`);
819
+ const stagedPath = join(candidateRoot, ".agent-runtime-bin");
820
+ if (readDeniedPaths.some((path) => isSameOrDescendant(candidateRoot, path) || isSameOrDescendant(stagedPath, path) || isSameOrDescendant(path, stagedPath))) throw new Error("runLocalHarness: codexReadDeniedPaths cannot contain the candidate root or staged executable");
821
+ const nativeExecutable = await opts.resolveExecutable(opts.command, opts.baseEnv);
822
+ if (!await isStaticElfExecutable(nativeExecutable)) throw new Error("runLocalHarness: reproducible Codex mode requires a statically linked Linux Codex ELF");
823
+ const sourceExecutableSha256 = await sha256File(nativeExecutable);
824
+ stagedExecutableDir = stagedPath;
825
+ try {
826
+ await mkdir(stagedExecutableDir, { mode: 448 });
827
+ ownsStagedExecutableDir = true;
828
+ } catch (err) {
829
+ throw new Error(`runLocalHarness: candidate-private .agent-runtime-bin must not already exist: ${String(err.code ?? err)}`);
830
+ }
831
+ const stagedExecutable = join(stagedExecutableDir, "codex");
832
+ await copyFile(nativeExecutable, stagedExecutable, constants.COPYFILE_FICLONE);
833
+ await chmod(stagedExecutable, 320);
834
+ const executableSha256 = await sha256File(stagedExecutable);
835
+ if (executableSha256 !== sourceExecutableSha256 || !await isStaticElfExecutable(stagedExecutable)) throw new Error("runLocalHarness: staged Codex executable digest mismatch");
836
+ await chmod(stagedExecutableDir, 320);
837
+ workspaceTemp = await mkdtemp(join(candidateRoot, ".agent-runtime-tmp-"));
838
+ await chmod(workspaceTemp, 448);
839
+ const ambientHome = resolve(opts.baseEnv.CODEX_HOME?.trim() || join(homedir(), ".codex"));
840
+ const ambientAuth = join(ambientHome, "auth.json");
841
+ const isolatedAuth = join(isolatedHome, "auth.json");
842
+ let authText = "";
843
+ try {
844
+ await access(ambientAuth, constants.R_OK);
845
+ authText = await readCodexAuthRedactionText(ambientAuth);
846
+ await symlink(ambientAuth, isolatedAuth);
847
+ } catch (err) {
848
+ throw new Error(`runLocalHarness: reproducible Codex mode requires readable auth.json: ${String(err.code ?? err)}`);
849
+ }
850
+ const deniedHostRoots = [
851
+ homedir(),
852
+ opts.baseEnv.HOME?.trim(),
853
+ ambientHome
854
+ ].filter((path) => Boolean(path)).map((path) => resolve(path)).filter((path, index, paths) => paths.indexOf(path) === index);
855
+ const controlledConfig = codexControlledConfig({
856
+ ambientTmp: opts.baseEnv.TMPDIR?.trim() || tmpdir(),
857
+ deniedHostRoots,
858
+ workspaceTemp,
859
+ stagedExecutableDir,
860
+ readDeniedPaths
861
+ });
862
+ await writeFile(join(isolatedHome, "config.toml"), controlledConfig, { mode: 384 });
863
+ const env = { ...opts.baseEnv };
864
+ for (const name of Object.keys(env)) if (codexSensitiveEnvironmentName.test(name) || CODEX_AUTH_ENV.some((authName) => authName === name)) delete env[name];
865
+ env.CODEX_HOME = isolatedHome;
866
+ const home = homedir();
867
+ const deniedProbePaths = [
868
+ ...deniedHostRoots,
869
+ ambientAuth,
870
+ isolatedAuth,
871
+ join(home, ".claude", ".credentials.json"),
872
+ join(home, ".claude.json"),
873
+ join(home, ".local", "share", "opencode", "auth.json"),
874
+ join(home, ".agents"),
875
+ join(home, "Documents"),
876
+ join(home, "Downloads"),
877
+ join(home, ".npm"),
878
+ join(home, "code"),
879
+ ...readDeniedPaths
880
+ ].filter((path, index, paths) => paths.indexOf(path) === index);
881
+ return {
882
+ env,
883
+ command: stagedExecutable,
884
+ cleanup,
885
+ controlledConfigSha256: sha256(controlledConfig),
886
+ executableSha256,
887
+ readDeniedPaths,
888
+ readDeniedPathsSha256: sha256(JSON.stringify(readDeniedPaths)),
889
+ readDeniedPathCount: readDeniedPaths.length,
890
+ diagnosticRedactionValues: collectCodexDiagnosticRedactionValues(opts.baseEnv, authText),
891
+ ambientAuth,
892
+ isolatedAuth,
893
+ writeProbe,
894
+ stagedWriteProbe: join(stagedExecutableDir, "write-proof"),
895
+ deniedProbePaths
896
+ };
897
+ } catch (err) {
898
+ await cleanup();
899
+ throw err;
900
+ }
901
+ }
902
+ function codexControlledConfig(opts) {
903
+ const rules = /* @__PURE__ */ new Map();
904
+ const broadDeniedPaths = minimalPathRoots([
905
+ ...opts.deniedHostRoots.map((path) => resolve(path)),
906
+ tmpdir(),
907
+ opts.ambientTmp,
908
+ "/proc",
909
+ "/run/docker",
910
+ "/run/containerd"
911
+ ]);
912
+ for (const path of broadDeniedPaths) rules.set(path, "deny");
913
+ const workspaceRoot = dirname(opts.workspaceTemp);
914
+ for (const path of opts.readDeniedPaths) if (!broadDeniedPaths.some((root) => isSameOrDescendant(path, root)) || isSameOrDescendant(path, workspaceRoot)) rules.set(path, "deny");
915
+ if (!rules.has(opts.workspaceTemp)) rules.set(opts.workspaceTemp, "write");
916
+ if (!rules.has(opts.stagedExecutableDir)) rules.set(opts.stagedExecutableDir, "read");
917
+ const filesystemRules = [...rules].sort(([a], [b]) => a.localeCompare(b)).map(([path, access]) => `${JSON.stringify(path)} = ${JSON.stringify(access)}`).join("\n");
918
+ return `default_permissions = "${CODEX_PERMISSION_PROFILE}"
919
+ approval_policy = "never"
920
+ allow_login_shell = false
921
+ service_tier = "default"
922
+
923
+ [shell_environment_policy]
924
+ inherit = "core"
925
+ ignore_default_excludes = false
926
+ exclude = ["^CODEX_HOME$", "(?i).*TOKEN.*", "(?i).*KEY.*", "(?i).*SECRET.*", "(?i).*PASSWORD.*", "(?i).*CREDENTIAL.*", "(?i).*AUTH.*", "(?i).*COOKIE.*"]
927
+ set = { TMPDIR = ${JSON.stringify(opts.workspaceTemp)} }
928
+
929
+ [permissions.${CODEX_PERMISSION_PROFILE}.filesystem]
930
+ ":minimal" = "read"
931
+ ${filesystemRules}
932
+ ":slash_tmp" = "deny"
933
+
934
+ [permissions.${CODEX_PERMISSION_PROFILE}.filesystem.":workspace_roots"]
935
+ "." = "write"
936
+ ".git" = "deny"
937
+
938
+ [permissions.${CODEX_PERMISSION_PROFILE}.network]
939
+ enabled = false
940
+
941
+ [permissions.${CODEX_PERMISSION_PROFILE}.network.unix_sockets]
942
+ "/var/run/docker.sock" = "deny"
943
+ "/run/docker.sock" = "deny"
944
+ "/var/run/containerd/containerd.sock" = "deny"
945
+ "/run/containerd/containerd.sock" = "deny"
946
+ `;
947
+ }
948
+ function isSameOrDescendant(path, root) {
949
+ const normalizedPath = resolve(path);
950
+ const normalizedRoot = resolve(root);
951
+ return normalizedPath === normalizedRoot || normalizedPath.startsWith(`${normalizedRoot}${sep}`);
952
+ }
953
+ function minimalPathRoots(paths) {
954
+ const ordered = [...new Set(paths.map((path) => resolve(path)))].sort((a, b) => a.length - b.length || a.localeCompare(b));
955
+ return ordered.filter((path, index) => !ordered.slice(0, index).some((root) => isSameOrDescendant(path, root)));
956
+ }
957
+ async function normalizeReadDeniedPaths(paths) {
958
+ if (!Array.isArray(paths)) throw new Error("runLocalHarness: codexReadDeniedPaths must be an array of absolute paths");
959
+ const normalized = /* @__PURE__ */ new Set();
960
+ for (const path of paths) {
961
+ if (typeof path !== "string" || path.length === 0 || path.includes("\0") || !isAbsolute(path)) throw new Error("runLocalHarness: codexReadDeniedPaths must contain absolute paths");
962
+ const absolute = resolve(path);
963
+ normalized.add(absolute);
964
+ try {
965
+ normalized.add(await realpath(absolute));
966
+ } catch (err) {
967
+ if (err.code !== "ENOENT") throw err;
968
+ }
969
+ }
970
+ return [...normalized].sort();
971
+ }
972
+ async function resolveCodexNativeExecutable(command, env) {
973
+ const candidates = command.includes(sep) || isAbsolute(command) ? [resolve(command)] : (env.PATH ?? "").split(delimiter).filter(Boolean).map((entry) => join(entry, command));
974
+ const seen = /* @__PURE__ */ new Set();
975
+ for (const candidate of candidates) {
976
+ let resolved;
977
+ try {
978
+ await access(candidate, constants.X_OK);
979
+ resolved = await realpath(candidate);
980
+ } catch {
981
+ continue;
982
+ }
983
+ if (seen.has(resolved)) continue;
984
+ seen.add(resolved);
985
+ if (await isStaticElfExecutable(resolved)) return resolved;
986
+ const packageRoot = await findCodexPackageRoot(resolved);
987
+ if (!packageRoot) continue;
988
+ const bundled = await findBundledCodexExecutable(packageRoot);
989
+ if (bundled) return bundled;
990
+ }
991
+ throw new Error("runLocalHarness: could not resolve the native Codex executable from the requested command");
992
+ }
993
+ async function findCodexPackageRoot(file) {
994
+ let cursor = dirname(file);
995
+ while (true) {
996
+ try {
997
+ if (JSON.parse(await readFile(join(cursor, "package.json"), "utf8")).name === "@openai/codex") return cursor;
998
+ } catch {}
999
+ const parent = dirname(cursor);
1000
+ if (parent === cursor) return void 0;
1001
+ cursor = parent;
1002
+ }
1003
+ }
1004
+ async function findBundledCodexExecutable(packageRoot) {
1005
+ const target = linuxCodexTarget();
1006
+ const candidates = [join(packageRoot, "node_modules", "@openai", target.packageName, "vendor", target.triple, "bin", "codex"), join(packageRoot, "vendor", target.triple, "bin", "codex")];
1007
+ for (const executable of candidates) if (await isStaticElfExecutable(executable)) return executable;
1008
+ }
1009
+ function linuxCodexTarget() {
1010
+ if (process.platform !== "linux") throw new Error("runLocalHarness: reproducible Codex mode requires Linux");
1011
+ if (process.arch === "x64") return {
1012
+ packageName: "codex-linux-x64",
1013
+ triple: "x86_64-unknown-linux-musl"
1014
+ };
1015
+ if (process.arch === "arm64") return {
1016
+ packageName: "codex-linux-arm64",
1017
+ triple: "aarch64-unknown-linux-musl"
1018
+ };
1019
+ throw new Error(`runLocalHarness: unsupported Linux architecture ${process.arch}`);
1020
+ }
1021
+ async function isStaticElfExecutable(path) {
1022
+ let handle;
1023
+ try {
1024
+ await access(path, constants.X_OK);
1025
+ handle = await open(path, "r");
1026
+ const header = Buffer.alloc(64);
1027
+ const { bytesRead } = await handle.read(header, 0, header.length, 0);
1028
+ if (bytesRead !== header.length || !header.subarray(0, 4).equals(Buffer.from([
1029
+ 127,
1030
+ 69,
1031
+ 76,
1032
+ 70
1033
+ ])) || header[4] !== 2 || header[5] !== 1) return false;
1034
+ const programHeaderOffset = Number(header.readBigUInt64LE(32));
1035
+ const programHeaderSize = header.readUInt16LE(54);
1036
+ const programHeaderCount = header.readUInt16LE(56);
1037
+ const expectedMachine = process.arch === "x64" ? 62 : process.arch === "arm64" ? 183 : -1;
1038
+ if (![2, 3].includes(header.readUInt16LE(16)) || header.readUInt16LE(18) !== expectedMachine || !Number.isSafeInteger(programHeaderOffset) || programHeaderOffset < 64 || programHeaderSize < 56 || programHeaderSize > 1024 || programHeaderCount === 0 || programHeaderCount > 1024) return false;
1039
+ const tableSize = programHeaderSize * programHeaderCount;
1040
+ const table = Buffer.alloc(tableSize);
1041
+ if ((await handle.read(table, 0, tableSize, programHeaderOffset)).bytesRead !== tableSize) return false;
1042
+ for (let index = 0; index < programHeaderCount; index += 1) if (table.readUInt32LE(index * programHeaderSize) === 3) return false;
1043
+ return true;
1044
+ } catch {
1045
+ return false;
1046
+ } finally {
1047
+ await handle?.close();
1048
+ }
1049
+ }
1050
+ function sha256File(path) {
1051
+ return new Promise((resolveDigest, reject) => {
1052
+ const hash = createHash("sha256");
1053
+ const stream = createReadStream(path);
1054
+ stream.on("error", reject);
1055
+ stream.on("data", (chunk) => hash.update(chunk));
1056
+ stream.on("end", () => resolveDigest(hash.digest("hex")));
1057
+ });
1058
+ }
1059
+ function signalProcessTree(child, signal) {
1060
+ if (process.platform !== "win32" && typeof child.pid === "number") try {
1061
+ process.kill(-child.pid, signal);
1062
+ return;
1063
+ } catch (err) {
1064
+ if (err.code === "ESRCH") return;
1065
+ }
1066
+ try {
1067
+ child.kill(signal);
1068
+ } catch {}
1069
+ }
1070
+ async function terminateProcessTreeAndConfirm(child, leaderClosed, context = "runLocalHarness") {
1071
+ signalProcessTree(child, "SIGTERM");
1072
+ if (process.platform === "win32" || typeof child.pid !== "number") {
1073
+ const graceDeadline = Date.now() + processKillGraceMs;
1074
+ while (!leaderClosed() && Date.now() < graceDeadline) await delayUntilNextProcessCheck(graceDeadline);
1075
+ if (!leaderClosed()) signalProcessTree(child, "SIGKILL");
1076
+ return;
1077
+ }
1078
+ const processGroupId = child.pid;
1079
+ if (await waitForProcessGroupExit(processGroupId, Date.now() + processKillGraceMs)) return;
1080
+ signalProcessTree(child, "SIGKILL");
1081
+ if (await waitForProcessGroupExit(processGroupId, Date.now() + processGroupExitConfirmMs)) return;
1082
+ throw new Error(`${context}: process group ${processGroupId} survived SIGKILL`);
1083
+ }
1084
+ async function waitForProcessGroupExit(processGroupId, deadline) {
1085
+ while (processGroupExists(processGroupId)) {
1086
+ if (Date.now() >= deadline) return false;
1087
+ await delayUntilNextProcessCheck(deadline);
1088
+ }
1089
+ return true;
1090
+ }
1091
+ async function delayUntilNextProcessCheck(deadline) {
1092
+ const delayMs = Math.min(processGroupExitPollMs, Math.max(0, deadline - Date.now()));
1093
+ if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
1094
+ }
1095
+ function processGroupExists(processGroupId) {
1096
+ try {
1097
+ process.kill(-processGroupId, 0);
1098
+ return true;
1099
+ } catch (err) {
1100
+ if (err.code === "ESRCH") return false;
1101
+ return true;
1102
+ }
1103
+ }
1104
+ /** Parse and validate the one terminal usage event emitted by `codex exec --json`. */
1105
+ function parseCodexTokenUsage(stdout) {
1106
+ const completed = [];
1107
+ for (const [index, line] of stdout.split(/\r?\n/).entries()) {
1108
+ if (line.trim().length === 0) continue;
1109
+ let event;
1110
+ try {
1111
+ event = JSON.parse(line);
1112
+ } catch {
1113
+ throw new Error(`runLocalHarness: Codex JSONL line ${index + 1} is not valid JSON`);
1114
+ }
1115
+ if (isRecord(event) && event.type === "turn.completed") completed.push(event);
1116
+ }
1117
+ if (completed.length !== 1) throw new Error(`runLocalHarness: expected exactly one Codex turn.completed usage event, received ${completed.length}`);
1118
+ const event = completed[0];
1119
+ if (!isRecord(event) || !isRecord(event.usage)) throw new Error("runLocalHarness: Codex turn.completed event is missing usage");
1120
+ const usage = event.usage;
1121
+ const inputTokens = naturalNumber(usage.input_tokens, "input_tokens");
1122
+ const cachedInputTokens = naturalNumber(usage.cached_input_tokens, "cached_input_tokens");
1123
+ const outputTokens = naturalNumber(usage.output_tokens, "output_tokens");
1124
+ const reasoningOutputTokens = naturalNumber(usage.reasoning_output_tokens, "reasoning_output_tokens");
1125
+ if (cachedInputTokens > inputTokens) throw new Error("runLocalHarness: Codex cached_input_tokens exceeds input_tokens");
1126
+ if (reasoningOutputTokens > outputTokens) throw new Error("runLocalHarness: Codex reasoning_output_tokens exceeds output_tokens");
1127
+ return {
1128
+ inputTokens,
1129
+ cachedInputTokens,
1130
+ outputTokens,
1131
+ reasoningOutputTokens
1132
+ };
1133
+ }
1134
+ function isRecord(value) {
1135
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1136
+ }
1137
+ function naturalNumber(value, field) {
1138
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error(`runLocalHarness: Codex usage.${field} must be a non-negative safe integer`);
1139
+ return value;
1140
+ }
1141
+ //#endregion
1142
+ //#region src/mcp/worktree.ts
1143
+ /**
1144
+ *
1145
+ * Git worktree helpers for the in-process delegation executor. Each
1146
+ * delegation runs in its own worktree so multiple parallel harness
1147
+ * subprocesses (claude / codex / opencode in a 3-way fanout) don't clobber
1148
+ * each other's edits on the shared workspace.
1149
+ *
1150
+ * Worktrees live under `<repoRoot>/.agent-worktrees/<runId>/`. After the
1151
+ * harness exits + the diff is captured, the worktree is removed.
1152
+ *
1153
+ * All operations spawn `git` via `child_process.spawn` synchronously
1154
+ * (via a `runGit` helper). Stays narrow on purpose: no commits, no rebases.
1155
+ * Diff capture stages all changes (`git add -A`) into the ephemeral worktree's
1156
+ * index so created (untracked) files appear in the `--cached` diff.
1157
+ *
1158
+ * @experimental
1159
+ */
1160
+ async function runGitAsync(args, cwd, runner) {
1161
+ if (runner) return runner(args, { cwd });
1162
+ return new Promise((resolve, reject) => {
1163
+ const proc = spawn("git", args, {
1164
+ cwd,
1165
+ stdio: "pipe"
1166
+ });
1167
+ let stdout = "";
1168
+ let stderr = "";
1169
+ proc.stdout?.on("data", (c) => {
1170
+ stdout += String(c);
1171
+ });
1172
+ proc.stderr?.on("data", (c) => {
1173
+ stderr += String(c);
1174
+ });
1175
+ proc.on("error", reject);
1176
+ proc.on("close", (code) => resolve({
1177
+ stdout,
1178
+ stderr,
1179
+ exitCode: code ?? -1
1180
+ }));
1181
+ });
1182
+ }
1183
+ function ensureGitOk(step, result) {
1184
+ if (result.exitCode !== 0) throw gitFailure(step, result);
1185
+ }
1186
+ function gitFailure(step, result) {
1187
+ return /* @__PURE__ */ new Error(`worktree: git ${step} failed (exit ${result.exitCode}): ${result.stderr.slice(0, 400)}`);
1188
+ }
1189
+ /** Checkout a fresh git worktree for a delegation run on a new branch under `variantsDir`. @experimental */
1190
+ async function createWorktree(options) {
1191
+ const variants = options.variantsDir ?? ".agent-worktrees";
1192
+ const baseRef = options.baseRef ?? "HEAD";
1193
+ const branch = `delegate/${options.runId}`;
1194
+ const path = `${options.repoRoot.replace(/\/+$/, "")}/${variants}/${options.runId}`;
1195
+ const headSha = await runGitAsync(["rev-parse", baseRef], options.repoRoot, options.runGit);
1196
+ ensureGitOk(`rev-parse ${baseRef}`, headSha);
1197
+ const add = await runGitAsync([
1198
+ "worktree",
1199
+ "add",
1200
+ "-b",
1201
+ branch,
1202
+ path,
1203
+ baseRef
1204
+ ], options.repoRoot, options.runGit);
1205
+ ensureGitOk(`worktree add ${path}`, add);
1206
+ return {
1207
+ path,
1208
+ baseSha: headSha.stdout.trim(),
1209
+ branch
1210
+ };
1211
+ }
1212
+ /** Stage worker changes and return the diff + shortstat, excluding declared input paths. @experimental */
1213
+ async function captureWorktreeDiff(options) {
1214
+ const baseRef = options.baseRef ?? options.worktree.baseSha;
1215
+ const pathspecs = worktreeDiffPathspecs(options.excludePaths ?? []);
1216
+ ensureGitOk("add -A", await runGitAsync([
1217
+ "add",
1218
+ "-A",
1219
+ ...pathspecs.length > 0 ? [
1220
+ "--",
1221
+ ".",
1222
+ ...pathspecs
1223
+ ] : []
1224
+ ], options.worktree.path, options.runGit));
1225
+ const patch = await runGitAsync([
1226
+ "diff",
1227
+ "--cached",
1228
+ baseRef,
1229
+ ...pathspecs.length > 0 ? [
1230
+ "--",
1231
+ ".",
1232
+ ...pathspecs
1233
+ ] : []
1234
+ ], options.worktree.path, options.runGit);
1235
+ ensureGitOk(`diff --cached ${baseRef}`, patch);
1236
+ const shortstat = await runGitAsync([
1237
+ "diff",
1238
+ "--cached",
1239
+ "--shortstat",
1240
+ baseRef,
1241
+ ...pathspecs.length > 0 ? [
1242
+ "--",
1243
+ ".",
1244
+ ...pathspecs
1245
+ ] : []
1246
+ ], options.worktree.path, options.runGit);
1247
+ ensureGitOk(`diff --cached --shortstat ${baseRef}`, shortstat);
1248
+ const stats = parseShortstat(shortstat.stdout);
1249
+ return {
1250
+ patch: patch.stdout,
1251
+ stats
1252
+ };
1253
+ }
1254
+ function worktreeDiffPathspecs(paths) {
1255
+ const normalized = /* @__PURE__ */ new Set();
1256
+ for (const path of paths) {
1257
+ if (typeof path !== "string" || path.length === 0 || path.includes("\\") || hasControlCharacter(path) || isAbsolute(path) || win32.isAbsolute(path)) throw new Error(`worktree: excluded path must be a canonical repository-relative path: ${path}`);
1258
+ if (path.split("/").some((segment) => segment.length === 0 || segment === "." || segment === "..")) throw new Error(`worktree: excluded path must be a canonical repository-relative path: ${path}`);
1259
+ normalized.add(path);
1260
+ }
1261
+ return [...normalized].sort((left, right) => left.localeCompare(right)).map((path) => `:(top,exclude,literal)${path}`);
1262
+ }
1263
+ function hasControlCharacter(value) {
1264
+ for (let index = 0; index < value.length; index += 1) {
1265
+ const code = value.charCodeAt(index);
1266
+ if (code < 32 || code === 127) return true;
1267
+ }
1268
+ return false;
1269
+ }
1270
+ function parseShortstat(text) {
1271
+ const out = {
1272
+ filesChanged: 0,
1273
+ insertions: 0,
1274
+ deletions: 0
1275
+ };
1276
+ const filesMatch = text.match(/(\d+)\s+files?\s+changed/);
1277
+ if (filesMatch?.[1]) out.filesChanged = Number(filesMatch[1]);
1278
+ const insertMatch = text.match(/(\d+)\s+insertions?/);
1279
+ if (insertMatch?.[1]) out.insertions = Number(insertMatch[1]);
1280
+ const deleteMatch = text.match(/(\d+)\s+deletions?/);
1281
+ if (deleteMatch?.[1]) out.deletions = Number(deleteMatch[1]);
1282
+ return out;
1283
+ }
1284
+ /**
1285
+ * Remove a git worktree and delete its branch. Already-removed paths are harmless; every other
1286
+ * Git failure rejects so callers cannot report a worktree as destroyed when cleanup failed.
1287
+ * @experimental
1288
+ */
1289
+ async function removeWorktree(options) {
1290
+ const force = options.force ?? true;
1291
+ const args = ["worktree", "remove"];
1292
+ if (force) args.push("--force");
1293
+ args.push(options.worktree.path);
1294
+ const removal = await runGitAsync(args, options.repoRoot, options.runGit);
1295
+ const removalError = removal.exitCode !== 0 && !/not a working tree/iu.test(removal.stderr) ? gitFailure(`worktree remove ${options.worktree.path}`, removal) : void 0;
1296
+ const branchRemoval = await runGitAsync([
1297
+ "branch",
1298
+ "-D",
1299
+ options.worktree.branch
1300
+ ], options.repoRoot, options.runGit);
1301
+ const branchError = branchRemoval.exitCode !== 0 && !/branch .+ not found/iu.test(branchRemoval.stderr) ? gitFailure(`branch -D ${options.worktree.branch}`, branchRemoval) : void 0;
1302
+ if (removalError && branchError) throw new AggregateError([removalError, branchError], "worktree: failed to remove worktree and branch");
1303
+ if (removalError) throw removalError;
1304
+ if (branchError) throw branchError;
1305
+ }
1306
+ //#endregion
1307
+ //#region src/mcp/worktree-harness.ts
1308
+ /**
1309
+ *
1310
+ * The ONE worktree-harness execution core. The physical act — run a supervisor-authored
1311
+ * `AgentProfile` on a local coding-harness CLI (claude / codex / opencode) against a fresh git
1312
+ * worktree off `repoRoot`, capture the diff, derive the test/typecheck PASS signals, then clean
1313
+ * up — lives here ONCE. Two executors adapt it to two ports without re-implementing it:
1314
+ * - `createWorktreeCliExecutor` — the `Scope`/`Supervisor` leaf `Executor`.
1315
+ * - `createInProcessExecutor` — the `runAgentRounds` `SandboxClient` / coder-delegate path.
1316
+ *
1317
+ * §1.5 by construction: prompt + model reach the direct invocation, while file-backed resources
1318
+ * are lowered by the shared profile materializer and applied before spawn. Resource instructions
1319
+ * join the direct prompt because reproducible Codex intentionally disables ambient project docs.
1320
+ *
1321
+ * Lifecycle: `createWorktree` → materialize profile inputs → `harnessInvocation` +
1322
+ * `runLocalHarness` → `captureWorktreeDiff` excluding those inputs (BEFORE checks, so the patch is
1323
+ * the harness's output, not polluted by files a test run writes) → configured checks → return the
1324
+ * result + caller-owned `cleanup`. A throw cleans up before propagating.
1325
+ *
1326
+ * @experimental
1327
+ */
1328
+ const defaultCheckOutputCap = 16e3;
1329
+ function materializerHarness(harness) {
1330
+ return harness === "claude" ? "claude-code" : harness;
1331
+ }
1332
+ /**
1333
+ * Run the one worktree-harness operation. A failed run attempts cleanup before propagating; if
1334
+ * cleanup also fails, both errors are preserved. The caller cleans up a successful run.
1335
+ */
1336
+ async function runWorktreeHarness(opts) {
1337
+ opts.signal?.throwIfAborted();
1338
+ const runHarness = opts.runHarness ?? runLocalHarness;
1339
+ const runCommand = opts.runCommand ?? defaultRunCommand;
1340
+ const checkTimeoutMs = opts.checkTimeoutMs ?? opts.harnessTimeoutMs ?? 300 * 1e3;
1341
+ const cap = opts.checkOutputCap ?? defaultCheckOutputCap;
1342
+ const worktree = await createWorktree({
1343
+ repoRoot: opts.repoRoot,
1344
+ runId: opts.runId,
1345
+ ...opts.baseRef ? { baseRef: opts.baseRef } : {},
1346
+ ...opts.runGit ? { runGit: opts.runGit } : {}
1347
+ });
1348
+ const cleanup = () => removeWorktree({
1349
+ worktree,
1350
+ repoRoot: opts.repoRoot,
1351
+ ...opts.runGit ? { runGit: opts.runGit } : {}
1352
+ });
1353
+ try {
1354
+ assertSupportedWorktreeProfile(opts.profile, opts.harness);
1355
+ assertSafeProfileResourcePaths(opts.profile);
1356
+ const resourceInstructions = resolveResourceInstructions(opts.profile);
1357
+ const plan = materializeProfile(materializationOnlyProfile(opts.profile), materializerHarness(opts.harness));
1358
+ if (plan.unsupported.length > 0) throw new Error(`runWorktreeHarness: profile cannot be materialized for ${opts.harness}: ${plan.unsupported.map(({ dimension, reason }) => `${dimension}: ${reason}`).join("; ")}`);
1359
+ assertSafeMaterializedPaths(plan);
1360
+ const applied = applyWorkspacePlan(plan, worktree.path, { existingFiles: "reject" });
1361
+ if (applied.unsupported.length > 0) throw new Error("runWorktreeHarness: applied profile unexpectedly retained unsupported rows");
1362
+ const invocationProfile = profileWithResourceInstructions(opts.profile, resourceInstructions);
1363
+ const { command, args } = harnessInvocation(opts.harness, invocationProfile, opts.taskPrompt, {
1364
+ dangerouslySkipPermissions: opts.harness === "claude",
1365
+ ...opts.codexReproducible ? { codexReproducible: true } : {}
1366
+ });
1367
+ const harnessResult = await runHarness({
1368
+ harness: opts.harness,
1369
+ cwd: worktree.path,
1370
+ taskPrompt: opts.taskPrompt,
1371
+ invocation: {
1372
+ command,
1373
+ args: [...args, ...applied.flags]
1374
+ },
1375
+ env: {
1376
+ ...process.env,
1377
+ ...applied.env
1378
+ },
1379
+ ...opts.codexReproducible ? { codexReproducible: true } : {},
1380
+ ...opts.codexReadDeniedPaths ? { codexReadDeniedPaths: opts.codexReadDeniedPaths } : {},
1381
+ ...opts.harnessTimeoutMs !== void 0 ? { timeoutMs: opts.harnessTimeoutMs } : {},
1382
+ ...opts.signal ? { signal: opts.signal } : {}
1383
+ });
1384
+ if (harnessResult.aborted || opts.signal?.aborted) throw new Error("runWorktreeHarness: harness was cancelled by the caller", { cause: opts.signal?.reason });
1385
+ opts.signal?.throwIfAborted();
1386
+ const diff = await captureWorktreeDiff({
1387
+ worktree,
1388
+ excludePaths: applied.written,
1389
+ ...opts.runGit ? { runGit: opts.runGit } : {}
1390
+ });
1391
+ opts.signal?.throwIfAborted();
1392
+ const checks = await runWorktreeChecks({
1393
+ worktreePath: worktree.path,
1394
+ ...opts.testCmd !== void 0 ? { testCmd: opts.testCmd } : {},
1395
+ ...opts.typecheckCmd !== void 0 ? { typecheckCmd: opts.typecheckCmd } : {},
1396
+ timeoutMs: checkTimeoutMs,
1397
+ cap,
1398
+ runCommand,
1399
+ ...opts.signal ? { signal: opts.signal } : {}
1400
+ });
1401
+ opts.signal?.throwIfAborted();
1402
+ return {
1403
+ worktree,
1404
+ result: {
1405
+ branch: worktree.branch,
1406
+ patch: diff.patch,
1407
+ stats: diff.stats,
1408
+ profileMaterialization: profileMaterializationReceipt(applied, resourceInstructions),
1409
+ harness: {
1410
+ name: opts.harness,
1411
+ exitCode: harnessResult.exitCode,
1412
+ timedOut: harnessResult.timedOut,
1413
+ killedBySignal: harnessResult.killedBySignal,
1414
+ durationMs: harnessResult.durationMs,
1415
+ stdout: harnessResult.stdout,
1416
+ stderr: harnessResult.stderr,
1417
+ ...harnessResult.usage ? { usage: harnessResult.usage } : {},
1418
+ ...harnessResult.evidence ? {
1419
+ cliVersion: harnessResult.evidence.cliVersion,
1420
+ executableSha256: harnessResult.evidence.executableSha256,
1421
+ requestedPromptSha256: harnessResult.evidence.requestedPromptSha256,
1422
+ effectivePromptSha256: harnessResult.evidence.effectivePromptSha256,
1423
+ nonPromptArgsSha256: harnessResult.evidence.nonPromptArgsSha256,
1424
+ controlledConfigSha256: harnessResult.evidence.controlledConfigSha256,
1425
+ readDeniedPaths: [...harnessResult.evidence.readDeniedPaths],
1426
+ readDeniedPathsSha256: harnessResult.evidence.readDeniedPathsSha256,
1427
+ readDeniedPathCount: harnessResult.evidence.readDeniedPathCount,
1428
+ executionPolicy: harnessResult.evidence.policy
1429
+ } : {}
1430
+ },
1431
+ ...checks ? { checks } : {}
1432
+ },
1433
+ cleanup
1434
+ };
1435
+ } catch (err) {
1436
+ try {
1437
+ await cleanup();
1438
+ } catch (cleanupError) {
1439
+ throw new AggregateError([err, cleanupError], "runWorktreeHarness: run failed and worktree cleanup also failed");
1440
+ }
1441
+ throw err;
1442
+ }
1443
+ }
1444
+ function resolveResourceInstructions(profile) {
1445
+ const instructions = profile.resources?.instructions;
1446
+ if (instructions === void 0) return void 0;
1447
+ if (typeof instructions === "string") return instructions.trim().length > 0 ? instructions : void 0;
1448
+ if (instructions.kind === "inline" && typeof instructions.content === "string") return instructions.content.trim().length > 0 ? instructions.content : void 0;
1449
+ throw new Error("runWorktreeHarness: resources.instructions must be a string or inline resource; remote refs require pre-resolution");
1450
+ }
1451
+ function profileWithResourceInstructions(profile, resourceInstructions) {
1452
+ let resources;
1453
+ if (profile.resources) {
1454
+ const { instructions: _instructions, ...structuralResources } = profile.resources;
1455
+ resources = structuralResources;
1456
+ }
1457
+ return {
1458
+ ...profile,
1459
+ ...resources ? { resources } : {},
1460
+ ...resourceInstructions === void 0 ? {} : { prompt: {
1461
+ ...profile.prompt,
1462
+ instructions: [...profile.prompt?.instructions ?? [], resourceInstructions]
1463
+ } }
1464
+ };
1465
+ }
1466
+ function materializationOnlyProfile(profile) {
1467
+ const { prompt: _prompt, model: _model, resources: profileResources, ...structuralProfile } = profile;
1468
+ let resources;
1469
+ if (profileResources) {
1470
+ const { instructions: _instructions, ...fileBackedResources } = profileResources;
1471
+ resources = fileBackedResources;
1472
+ }
1473
+ return {
1474
+ ...structuralProfile,
1475
+ ...resources ? { resources } : {}
1476
+ };
1477
+ }
1478
+ function assertSupportedWorktreeProfile(profile, harness) {
1479
+ const unsupportedAxes = [
1480
+ hasEntries(profile.tools) ? "tools" : null,
1481
+ hasEntries(profile.permissions) ? "permissions" : null,
1482
+ profile.connections && profile.connections.length > 0 ? "connections" : null,
1483
+ hasEntries(profile.confidential) ? "confidential" : null,
1484
+ hasEntries(profile.modes) ? "modes" : null,
1485
+ hasEntries(profile.extensions) ? "extensions" : null
1486
+ ].filter((axis) => axis !== null);
1487
+ if (profile.model?.reasoningEffort !== void 0 && harness !== "codex") unsupportedAxes.push("model.reasoningEffort");
1488
+ for (const [name, server] of Object.entries(profile.mcp ?? {})) {
1489
+ const path = `mcp[${JSON.stringify(name)}]`;
1490
+ if (server.enabled === false && harness !== "opencode") unsupportedAxes.push(`${path}.enabled`);
1491
+ if (hasEntries(server.headers) && harness === "codex") unsupportedAxes.push(`${path}.headers`);
1492
+ if (server.cwd !== void 0 && harness === "opencode") unsupportedAxes.push(`${path}.cwd`);
1493
+ }
1494
+ if (harness === "claude") for (const [event, commands] of Object.entries(profile.hooks ?? {})) for (const [index, command] of commands.entries()) {
1495
+ const path = `hooks[${JSON.stringify(event)}][${index}]`;
1496
+ if (hasEntries(command.env)) unsupportedAxes.push(`${path}.env`);
1497
+ if (command.blocking !== void 0) unsupportedAxes.push(`${path}.blocking`);
1498
+ }
1499
+ for (const [name, subagent] of Object.entries(profile.subagents ?? {})) {
1500
+ const path = `subagents[${JSON.stringify(name)}]`;
1501
+ if (hasEntries(subagent.permissions)) unsupportedAxes.push(`${path}.permissions`);
1502
+ if (subagent.maxSteps !== void 0) unsupportedAxes.push(`${path}.maxSteps`);
1503
+ if (hasEntries(subagent.tools) && harness !== "claude") unsupportedAxes.push(`${path}.tools`);
1504
+ }
1505
+ if (unsupportedAxes.length > 0) throw new Error(`runWorktreeHarness: profile requests unsupported worktree behavior: ${unsupportedAxes.join(", ")}`);
1506
+ }
1507
+ function hasEntries(value) {
1508
+ return value !== void 0 && Object.keys(value).length > 0;
1509
+ }
1510
+ function assertSafeMaterializedPaths(plan) {
1511
+ for (const file of plan.files) if (file.relPath.split("/").some(isGitMetadataSegment)) throw new Error(`runWorktreeHarness: profile file cannot target reserved Git metadata: ${file.relPath}`);
1512
+ }
1513
+ function assertSafeProfileResourcePaths(profile) {
1514
+ for (const file of profile.resources?.files ?? []) if (file.path.split("/").some(isGitMetadataSegment)) throw new Error(`runWorktreeHarness: profile file cannot target reserved Git metadata: ${file.path}`);
1515
+ }
1516
+ function isGitMetadataSegment(segment) {
1517
+ const windowsCanonical = segment.replace(/[ .]+$/u, "").toLowerCase();
1518
+ return windowsCanonical === ".git" || windowsCanonical.startsWith(".git:");
1519
+ }
1520
+ function profileMaterializationReceipt(applied, resourceInstructions) {
1521
+ const instructionBytes = resourceInstructions === void 0 ? null : Buffer.from(resourceInstructions, "utf8");
1522
+ return {
1523
+ workspacePlanDigest: applied.workspacePlanDigest,
1524
+ writtenPaths: [...applied.written],
1525
+ unsupported: [...applied.unsupported],
1526
+ environmentNames: Object.keys(applied.env).sort(),
1527
+ flags: [...applied.flags],
1528
+ resourceInstructions: instructionBytes ? {
1529
+ delivery: "invocation-prompt",
1530
+ sha256: `sha256:${createHash("sha256").update(instructionBytes).digest("hex")}`,
1531
+ byteLength: instructionBytes.byteLength
1532
+ } : {
1533
+ delivery: "none",
1534
+ sha256: null,
1535
+ byteLength: 0
1536
+ }
1537
+ };
1538
+ }
1539
+ /** Run the configured test + typecheck commands in the live worktree, projecting exit codes into
1540
+ * `checks`. Returns `undefined` when neither was configured (so the result omits `checks`). */
1541
+ async function runWorktreeChecks(opts) {
1542
+ opts.signal?.throwIfAborted();
1543
+ if (opts.testCmd === void 0 && opts.typecheckCmd === void 0) return void 0;
1544
+ const runCommand = opts.runCommand ?? defaultRunCommand;
1545
+ const run = async (command) => {
1546
+ opts.signal?.throwIfAborted();
1547
+ const res = await runCommand({
1548
+ command,
1549
+ cwd: opts.worktreePath,
1550
+ timeoutMs: opts.timeoutMs,
1551
+ ...opts.signal ? { signal: opts.signal } : {}
1552
+ });
1553
+ opts.signal?.throwIfAborted();
1554
+ return {
1555
+ command,
1556
+ passed: res.exitCode === 0,
1557
+ exitCode: res.exitCode,
1558
+ output: res.output.length > opts.cap ? res.output.slice(-opts.cap) : res.output
1559
+ };
1560
+ };
1561
+ const checks = {};
1562
+ if (opts.testCmd !== void 0) checks.tests = await run(opts.testCmd);
1563
+ opts.signal?.throwIfAborted();
1564
+ if (opts.typecheckCmd !== void 0) checks.typecheck = await run(opts.typecheckCmd);
1565
+ opts.signal?.throwIfAborted();
1566
+ return checks;
1567
+ }
1568
+ /** Internal argv-safe process runner shared by worktree checks and improvement verifiers. */
1569
+ async function runSettledCommand(opts) {
1570
+ opts.signal?.throwIfAborted();
1571
+ return new Promise((resolve, reject) => {
1572
+ const child = spawn(opts.command, opts.args ?? [], {
1573
+ cwd: opts.cwd,
1574
+ stdio: [
1575
+ "ignore",
1576
+ "pipe",
1577
+ "pipe"
1578
+ ],
1579
+ detached: process.platform !== "win32"
1580
+ });
1581
+ const stdout = [];
1582
+ const stderr = [];
1583
+ let settled = false;
1584
+ let leaderClosed = false;
1585
+ let timedOut = false;
1586
+ let termination = null;
1587
+ const terminate = () => {
1588
+ termination ??= terminateProcessTreeAndConfirm(child, () => leaderClosed, "defaultRunCommand");
1589
+ return termination;
1590
+ };
1591
+ const timer = setTimeout(() => {
1592
+ timedOut = true;
1593
+ terminate().catch(fail);
1594
+ }, opts.timeoutMs);
1595
+ const onAbort = () => void terminate().catch(fail);
1596
+ if (opts.signal) if (opts.signal.aborted) onAbort();
1597
+ else opts.signal.addEventListener("abort", onAbort, { once: true });
1598
+ child.stdout?.on("data", (d) => stdout.push(String(d)));
1599
+ child.stderr?.on("data", (d) => stderr.push(String(d)));
1600
+ const finish = (fn) => {
1601
+ if (settled) return;
1602
+ settled = true;
1603
+ clearTimeout(timer);
1604
+ opts.signal?.removeEventListener("abort", onAbort);
1605
+ fn();
1606
+ };
1607
+ function fail(err) {
1608
+ finish(() => reject(err instanceof Error ? err : new Error(String(err))));
1609
+ }
1610
+ child.on("error", (err) => {
1611
+ leaderClosed = true;
1612
+ (termination ?? Promise.resolve()).then(() => fail(err), fail);
1613
+ });
1614
+ child.on("close", (code, killedBySignal) => {
1615
+ leaderClosed = true;
1616
+ (async () => {
1617
+ try {
1618
+ await (termination ?? terminate());
1619
+ opts.signal?.throwIfAborted();
1620
+ finish(() => resolve({
1621
+ exitCode: code,
1622
+ stdout: stdout.join(""),
1623
+ stderr: stderr.join(""),
1624
+ killedBySignal,
1625
+ timedOut
1626
+ }));
1627
+ } catch (err) {
1628
+ fail(err);
1629
+ }
1630
+ })();
1631
+ });
1632
+ });
1633
+ }
1634
+ /** Default verification-command runner — `/bin/sh -c <command>` in the worktree, capturing
1635
+ * combined stdout+stderr. Never throws on a non-zero exit (that IS the fail signal); spawn
1636
+ * failures and caller cancellation reject. Timeout terminates and confirms the whole process
1637
+ * group before returning. */
1638
+ async function defaultRunCommand(opts) {
1639
+ const result = await runSettledCommand({
1640
+ command: "/bin/sh",
1641
+ args: ["-c", opts.command],
1642
+ cwd: opts.cwd,
1643
+ timeoutMs: opts.timeoutMs,
1644
+ ...opts.signal ? { signal: opts.signal } : {}
1645
+ });
1646
+ return {
1647
+ exitCode: result.exitCode,
1648
+ output: result.stdout + result.stderr
1649
+ };
1650
+ }
1651
+ //#endregion
1652
+ //#region src/runtime/tool-loop.ts
1653
+ /** ≈ chars/4 over content + any tool-call arguments — a cheap, provider-agnostic size proxy. The
1654
+ * exact constant does not matter: it only has to track GROWTH so the threshold trips as history
1655
+ * accumulates, which a uniform chars/4 does. */
1656
+ function estimateConversationTokens(messages) {
1657
+ let chars = 0;
1658
+ for (const m of messages) {
1659
+ const content = m.content;
1660
+ chars += typeof content === "string" ? content.length : safeJsonLength(content);
1661
+ const calls = m.tool_calls;
1662
+ if (calls) for (const c of calls) chars += c.function?.arguments?.length ?? 0;
1663
+ }
1664
+ return Math.ceil(chars / 4);
1665
+ }
1666
+ function safeJsonLength(value) {
1667
+ if (value === void 0 || value === null) return 0;
1668
+ try {
1669
+ return JSON.stringify(value)?.length ?? 0;
1670
+ } catch {
1671
+ return String(value).length;
1672
+ }
1673
+ }
1674
+ /** Distill the conversation's accumulated middle into one note and splice it in, IF the estimated
1675
+ * size exceeds the threshold. Returns true when a compaction fired. The preserved head keeps the
1676
+ * system + task verbatim; everything after collapses to a single `user` progress note, so no
1677
+ * assistant `tool_calls` is left without its `tool` replies. */
1678
+ async function maybeCompact(messages, c, turn) {
1679
+ const head = c.preserveHead !== void 0 && c.preserveHead > 0 ? c.preserveHead : 2;
1680
+ if (messages.length <= head + 1) return false;
1681
+ const estimate = c.estimateTokens ?? estimateConversationTokens;
1682
+ const before = estimate(messages);
1683
+ if (before <= c.thresholdTokens) return false;
1684
+ const digest = await c.distill([...messages]);
1685
+ messages.splice(head, messages.length - head, {
1686
+ role: "user",
1687
+ content: `[earlier work compacted to save context — progress so far]\n${digest}`
1688
+ });
1689
+ c.onCompact?.({
1690
+ turn,
1691
+ beforeTokens: before,
1692
+ afterTokens: estimate(messages)
1693
+ });
1694
+ return true;
1695
+ }
1696
+ async function runBrainLoop(opts) {
1697
+ const maxTurns = opts.maxTurns ?? 4;
1698
+ const messages = [...opts.initialMessages];
1699
+ let toolCalls = 0;
1700
+ let lastText = "";
1701
+ const usage = {
1702
+ input: 0,
1703
+ output: 0
1704
+ };
1705
+ const toolTrace = [];
1706
+ for (let turn = 1; turn <= maxTurns; turn += 1) {
1707
+ if (opts.hooks?.stopBefore?.(turn)) break;
1708
+ await opts.hooks?.beforeTurn?.(turn, messages);
1709
+ if (opts.compaction) await maybeCompact(messages, opts.compaction, turn);
1710
+ const r = await opts.chat(messages, opts.tools);
1711
+ if (r.usage) {
1712
+ usage.input += r.usage.input;
1713
+ usage.output += r.usage.output;
1714
+ opts.hooks?.onUsage?.(r.usage);
1715
+ }
1716
+ if (r.content) lastText = r.content;
1717
+ if (r.toolCalls.length === 0) {
1718
+ if (r.content) messages.push({
1719
+ role: "assistant",
1720
+ content: r.content
1721
+ });
1722
+ return {
1723
+ final: lastText,
1724
+ turns: turn,
1725
+ toolCalls,
1726
+ toolTrace,
1727
+ usage,
1728
+ messages
1729
+ };
1730
+ }
1731
+ messages.push({
1732
+ role: "assistant",
1733
+ content: r.content ?? "",
1734
+ tool_calls: r.toolCalls.map((tc) => ({
1735
+ id: tc.id,
1736
+ type: "function",
1737
+ function: {
1738
+ name: tc.name,
1739
+ arguments: tc.arguments
1740
+ }
1741
+ }))
1742
+ });
1743
+ for (const tc of r.toolCalls) {
1744
+ toolCalls += 1;
1745
+ let args = {};
1746
+ try {
1747
+ args = JSON.parse(tc.arguments);
1748
+ } catch {
1749
+ messages.push({
1750
+ role: "tool",
1751
+ tool_call_id: tc.id,
1752
+ content: `error: arguments were not valid JSON: ${tc.arguments.slice(0, 200)}`
1753
+ });
1754
+ continue;
1755
+ }
1756
+ const out = await opts.execute(tc.name, args);
1757
+ messages.push({
1758
+ role: "tool",
1759
+ tool_call_id: tc.id,
1760
+ content: out
1761
+ });
1762
+ toolTrace.push({
1763
+ name: tc.name,
1764
+ args: tc.arguments,
1765
+ result: out
1766
+ });
1767
+ }
1768
+ }
1769
+ return {
1770
+ final: lastText,
1771
+ turns: maxTurns,
1772
+ toolCalls,
1773
+ toolTrace,
1774
+ usage,
1775
+ messages
1776
+ };
1777
+ }
1778
+ //#endregion
1779
+ //#region src/runtime/router-client.ts
1780
+ /**
1781
+ * The one router chat client: direct OpenAI-compatible completions through the
1782
+ * Tangle router — the cheapest dial, no sandbox. Three layers: `routerChatWithUsage`
1783
+ * (chat-only), `routerChatWithTools` (one completion with function tools), and
1784
+ * `routerToolLoop` (the off-box agentic loop over tool-calling). Shared by the
1785
+ * built-in executors and the bench/lab harnesses.
1786
+ *
1787
+ * Reports REAL token usage so the backend-integrity guard sees a real backend.
1788
+ * Returns `undefined` usage when the provider omitted it — never a fabricated 0
1789
+ * (a phantom 0 reads as a free call downstream, which the gate would act on).
1790
+ */
1791
+ /** One OpenAI-compatible chat completion through the Tangle router, returning text + REAL token usage (`undefined` when the provider omits it — never a fabricated 0). */
1792
+ async function routerChatWithUsage(cfg, messages, opts) {
1793
+ const url = `${cfg.routerBaseUrl.replace(/\/$/, "")}/chat/completions`;
1794
+ const headers = {
1795
+ "content-type": "application/json",
1796
+ authorization: `Bearer ${cfg.routerKey}`
1797
+ };
1798
+ let temperature = opts?.temperature ?? .2;
1799
+ const body = () => ({
1800
+ model: cfg.model,
1801
+ messages,
1802
+ temperature,
1803
+ max_tokens: opts?.maxTokens ?? 8192,
1804
+ ...opts?.reasoningEffort ? { reasoning_effort: opts.reasoningEffort } : {}
1805
+ });
1806
+ if (cfg.complete) return parseChatResult(await cfg.complete(body()), cfg.model);
1807
+ let lastErr = "";
1808
+ for (let attempt = 0; attempt < 5; attempt += 1) {
1809
+ const res = await fetch(url, {
1810
+ method: "POST",
1811
+ headers,
1812
+ body: JSON.stringify(body()),
1813
+ ...opts?.signal ? { signal: opts.signal } : {}
1814
+ });
1815
+ if (res.ok) return parseChatResult(await res.json(), cfg.model);
1816
+ const status = res.status;
1817
+ const text = (await res.text()).slice(0, 200);
1818
+ lastErr = `router ${status}: ${text}`;
1819
+ if (status === 400 && /temperature/i.test(text) && temperature !== 1) {
1820
+ temperature = 1;
1821
+ continue;
1822
+ }
1823
+ if (![
1824
+ 408,
1825
+ 425,
1826
+ 429,
1827
+ 500,
1828
+ 502,
1829
+ 503,
1830
+ 504,
1831
+ 520,
1832
+ 522,
1833
+ 524
1834
+ ].includes(status)) throw new Error(lastErr);
1835
+ if (attempt < 4) await new Promise((r) => setTimeout(r, 800 * 2 ** attempt));
1836
+ }
1837
+ throw new Error(`${lastErr} (exhausted retries)`);
1838
+ }
1839
+ function parseChatResult(json, model) {
1840
+ const data = json;
1841
+ const u = data.usage;
1842
+ const usage = u && typeof u.prompt_tokens === "number" && typeof u.completion_tokens === "number" ? {
1843
+ input: u.prompt_tokens,
1844
+ output: u.completion_tokens
1845
+ } : void 0;
1846
+ const costUsd = usage && isModelPriced(model) ? estimateCost(usage.input, usage.output, model) : void 0;
1847
+ const msg = data.choices?.[0]?.message;
1848
+ const { content, reasoning } = splitReasoning(msg?.content ?? "", msg?.reasoning ?? msg?.reasoning_content);
1849
+ return {
1850
+ content,
1851
+ ...reasoning ? { reasoning } : {},
1852
+ ...usage ? { usage } : {},
1853
+ ...costUsd !== void 0 ? { costUsd } : {}
1854
+ };
1855
+ }
1856
+ /**
1857
+ * Normalize the two ways providers surface thinking-model reasoning into one shape:
1858
+ * a separate field (OpenRouter: `reasoning`, DeepSeek/Kimi: `reasoning_content`) or a
1859
+ * `<think>...</think>` block inlined at the head of `content` (Groq, some local runtimes).
1860
+ * An UNCLOSED `<think>` (the model hit max_tokens mid-thought) yields empty content and
1861
+ * everything as reasoning — which is honest: no final answer was emitted.
1862
+ */
1863
+ function splitReasoning(rawContent, fieldReasoning) {
1864
+ const open = rawContent.indexOf("<think>");
1865
+ if (open !== -1) {
1866
+ const close = rawContent.indexOf("</think>", open);
1867
+ const inline = close !== -1 ? rawContent.slice(open + 7, close) : rawContent.slice(open + 7);
1868
+ const rest = close !== -1 ? rawContent.slice(0, open) + rawContent.slice(close + 8) : rawContent.slice(0, open);
1869
+ const reasoning = [fieldReasoning, inline.trim()].filter(Boolean).join("\n");
1870
+ return {
1871
+ content: rest.trim(),
1872
+ ...reasoning ? { reasoning } : {}
1873
+ };
1874
+ }
1875
+ return {
1876
+ content: rawContent,
1877
+ ...fieldReasoning ? { reasoning: fieldReasoning } : {}
1878
+ };
1879
+ }
1880
+ /**
1881
+ * A router completion WITH tool-calling — the operator driver's LLM seam. Passes OpenAI-shape
1882
+ * `messages` (system/user/assistant-with-tool_calls/tool roles) + function `tools`, and returns the
1883
+ * assistant text plus the tool calls the model wants run. Same fail-loud + real-usage discipline as
1884
+ * `routerChatWithUsage`. `tool_choice: 'auto'` lets the model decide; the driver loops on the result.
1885
+ */
1886
+ async function routerChatWithTools(cfg, messages, tools, opts) {
1887
+ const body = {
1888
+ model: cfg.model,
1889
+ messages,
1890
+ tools,
1891
+ tool_choice: opts?.toolChoice ?? "auto",
1892
+ temperature: opts?.temperature ?? .3,
1893
+ ...opts?.maxTokens ? { max_tokens: opts.maxTokens } : {}
1894
+ };
1895
+ const data = cfg.complete ? await cfg.complete(body) : await (async () => {
1896
+ const res = await fetch(`${cfg.routerBaseUrl.replace(/\/$/, "")}/chat/completions`, {
1897
+ method: "POST",
1898
+ headers: {
1899
+ "content-type": "application/json",
1900
+ authorization: `Bearer ${cfg.routerKey}`
1901
+ },
1902
+ body: JSON.stringify(body),
1903
+ ...opts?.signal ? { signal: opts.signal } : {}
1904
+ });
1905
+ if (!res.ok) throw new Error(`router ${res.status}: ${(await res.text()).slice(0, 200)}`);
1906
+ return res.json();
1907
+ })();
1908
+ const msg = data.choices?.[0]?.message;
1909
+ const toolCalls = (msg?.tool_calls ?? []).map((tc, i) => ({
1910
+ id: tc.id ?? `call_${i}`,
1911
+ name: tc.function?.name ?? "",
1912
+ arguments: tc.function?.arguments ?? "{}"
1913
+ }));
1914
+ const u = data.usage;
1915
+ const usage = u && typeof u.prompt_tokens === "number" && typeof u.completion_tokens === "number" ? {
1916
+ input: u.prompt_tokens,
1917
+ output: u.completion_tokens
1918
+ } : void 0;
1919
+ const costUsd = usage && isModelPriced(cfg.model) ? estimateCost(usage.input, usage.output, cfg.model) : void 0;
1920
+ return {
1921
+ content: msg?.content ?? null,
1922
+ toolCalls,
1923
+ ...usage ? { usage } : {},
1924
+ ...costUsd !== void 0 ? { costUsd } : {}
1925
+ };
1926
+ }
1927
+ /**
1928
+ * The tool-using router backend: a real agentic loop OVER the Tangle router (which
1929
+ * supports tool-calling), off-box — no sandbox. Each turn is one router completion
1930
+ * with `tools`; if the model emits tool_calls, `execute` runs them on the host and
1931
+ * their results are folded back as `tool` messages; the loop repeats until the
1932
+ * model answers without a tool call or the turn budget is hit. One turn = one
1933
+ * inference call, so `maxTurns` is the equal-compute unit against random@k.
1934
+ *
1935
+ * This is the depth substrate for agentic gates (the worker ACTS, observes the real
1936
+ * result, and continues) that the chat-only `routerChatWithUsage` cannot express.
1937
+ */
1938
+ async function routerToolLoop(cfg, system, user, tools, execute, opts) {
1939
+ return runBrainLoop({
1940
+ chat: (messages, toolSpecs) => routerChatWithTools(cfg, messages, toolSpecs, {
1941
+ ...opts?.temperature !== void 0 ? { temperature: opts.temperature } : {},
1942
+ ...opts?.maxTokens ? { maxTokens: opts.maxTokens } : {},
1943
+ ...opts?.signal ? { signal: opts.signal } : {}
1944
+ }),
1945
+ tools,
1946
+ execute,
1947
+ initialMessages: opts?.initialMessages ?? [{
1948
+ role: "system",
1949
+ content: system
1950
+ }, {
1951
+ role: "user",
1952
+ content: user
1953
+ }],
1954
+ maxTurns: opts?.maxTurns ?? 4
1955
+ });
1956
+ }
1957
+ /**
1958
+ * The router as a supervisor BRAIN: the canonical `ToolLoopChat` seam backed by the router's
1959
+ * tool-calling. The driver's spawn/observe/steer/await/stop turns become real router tool-calls.
1960
+ * The turnkey production brain — tests script a mock `ToolLoopChat`; production passes
1961
+ * `routerBrain(cfg)`. No message translation: the loop already speaks the router's OpenAI shape.
1962
+ */
1963
+ function routerBrain(cfg, opts = {}) {
1964
+ const temperature = opts.temperature ?? .4;
1965
+ return (messages, tools) => routerChatWithTools(cfg, messages, tools, {
1966
+ temperature,
1967
+ toolChoice: "auto"
1968
+ });
1969
+ }
1970
+ //#endregion
1971
+ //#region src/runtime-hooks.ts
1972
+ /** Identity helper that types a {@link RuntimeHooks} literal so the fields are inferred. */
1973
+ function defineRuntimeHooks(hooks) {
1974
+ return hooks;
1975
+ }
1976
+ /**
1977
+ * Merge several {@link RuntimeHooks} into one. Falsy entries are dropped (so you can
1978
+ * pass `flag && hooks`), and every observer's `onEvent`/`onDecisionPoint` fires for each
1979
+ * event. Use this to attach N observers to a loop instead of a second event bus.
1980
+ */
1981
+ function composeRuntimeHooks(...entries) {
1982
+ const hooks = entries.filter((entry) => !!entry);
1983
+ return {
1984
+ onEvent: hooks.some((hook) => hook.onEvent) ? (event, context) => {
1985
+ const pending = [];
1986
+ for (const hook of hooks) {
1987
+ const result = hook.onEvent?.(event, context);
1988
+ if (isThenable(result)) pending.push(Promise.resolve(result));
1989
+ }
1990
+ if (pending.length > 0) return Promise.all(pending).then(() => void 0);
1991
+ } : void 0,
1992
+ onDecisionPoint: hooks.some((hook) => hook.onDecisionPoint) ? (point, context) => {
1993
+ const pending = [];
1994
+ for (const hook of hooks) {
1995
+ const result = hook.onDecisionPoint?.(point, context);
1996
+ if (isThenable(result)) pending.push(Promise.resolve(result));
1997
+ }
1998
+ if (pending.length > 0) return Promise.all(pending).then(() => void 0);
1999
+ } : void 0,
2000
+ onHookError: hooks.some((hook) => hook.onHookError) ? (error, context) => {
2001
+ const pending = [];
2002
+ for (const hook of hooks) {
2003
+ const result = hook.onHookError?.(error, context);
2004
+ if (isThenable(result)) pending.push(Promise.resolve(result));
2005
+ }
2006
+ if (pending.length > 0) return Promise.all(pending).then(() => void 0);
2007
+ } : void 0
2008
+ };
2009
+ }
2010
+ /** Fire `hooks.onEvent`, swallowing sync throws and surfacing async failures to `onError`. */
2011
+ function notifyRuntimeHookEvent(hooks, event, context = {}) {
2012
+ const onEvent = hooks?.onEvent;
2013
+ if (!onEvent) return;
2014
+ try {
2015
+ const result = onEvent(event, context);
2016
+ if (isThenable(result)) result.catch((error) => {
2017
+ notifyRuntimeHookError(hooks, toError(error), {
2018
+ hook: "onEvent",
2019
+ eventId: event.id,
2020
+ target: event.target,
2021
+ phase: event.phase
2022
+ });
2023
+ });
2024
+ } catch (error) {
2025
+ notifyRuntimeHookError(hooks, toError(error), {
2026
+ hook: "onEvent",
2027
+ eventId: event.id,
2028
+ target: event.target,
2029
+ phase: event.phase
2030
+ });
2031
+ }
2032
+ }
2033
+ /** Fire `hooks.onDecisionPoint`, swallowing sync throws and surfacing async failures to `onError`. */
2034
+ function notifyRuntimeDecisionPoint(hooks, point, context = {}) {
2035
+ const onDecisionPoint = hooks?.onDecisionPoint;
2036
+ if (!onDecisionPoint) return;
2037
+ try {
2038
+ const result = onDecisionPoint(point, context);
2039
+ if (isThenable(result)) result.catch((error) => {
2040
+ notifyRuntimeHookError(hooks, toError(error), {
2041
+ hook: "onDecisionPoint",
2042
+ decisionId: point.id,
2043
+ decisionKind: point.kind
2044
+ });
2045
+ });
2046
+ } catch (error) {
2047
+ notifyRuntimeHookError(hooks, toError(error), {
2048
+ hook: "onDecisionPoint",
2049
+ decisionId: point.id,
2050
+ decisionKind: point.kind
2051
+ });
2052
+ }
2053
+ }
2054
+ function notifyRuntimeHookError(hooks, error, context) {
2055
+ try {
2056
+ const result = hooks?.onHookError?.(error, context);
2057
+ if (isThenable(result)) result.catch(() => void 0);
2058
+ } catch {}
2059
+ }
2060
+ function isThenable(value) {
2061
+ return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
2062
+ }
2063
+ function toError(error) {
2064
+ return error instanceof Error ? error : new Error(String(error));
2065
+ }
2066
+ //#endregion
2067
+ //#region src/runtime/supervise/progress.ts
2068
+ /** How long a worker may produce no metered activity before a `progress()` read calls it stalled.
2069
+ * Deliberately generous: a coding harness routinely spends minutes inside one tool call, and a
2070
+ * false stall that provokes a steer is worse than a late one. */
2071
+ const DEFAULT_STALL_AFTER_MS = 18e4;
2072
+ /** Create a bounded activity ring. `limit` caps memory for a worker that runs thousands of tools. */
2073
+ function createActivityLog(limit = 12) {
2074
+ const notes = [];
2075
+ const cap = Math.max(1, Math.floor(limit));
2076
+ return {
2077
+ push(note) {
2078
+ notes.push(note);
2079
+ if (notes.length > cap) notes.splice(0, notes.length - cap);
2080
+ },
2081
+ read: () => [...notes],
2082
+ last: () => notes[notes.length - 1],
2083
+ size: () => notes.length
2084
+ };
2085
+ }
2086
+ /** Fold the scope-derived facts and the executor's optional enrichment into one read. Pure: the
2087
+ * caller supplies `now`, so a test can observe a stall without waiting for one. */
2088
+ function readWorkerProgress(scope, executor, now, stallAfterMs = DEFAULT_STALL_AFTER_MS) {
2089
+ const live = scope.status !== "done" && scope.status !== "failed" && scope.status !== "cancelled";
2090
+ const executorActivityAt = executor?.recentActivity?.length ? executor.recentActivity[executor.recentActivity.length - 1].at : 0;
2091
+ const lastActivityAt = Math.max(scope.lastActivityAt, executorActivityAt);
2092
+ const idleMs = Math.max(0, now - lastActivityAt);
2093
+ const note = executor?.note;
2094
+ return {
2095
+ id: scope.id,
2096
+ status: scope.status,
2097
+ live,
2098
+ steerable: scope.steerable,
2099
+ startedAt: scope.startedAt,
2100
+ lastActivityAt,
2101
+ idleMs,
2102
+ stalled: live && idleMs > stallAfterMs,
2103
+ stallAfterMs,
2104
+ turns: executor?.turns ?? scope.turns,
2105
+ tokens: scope.tokens,
2106
+ usd: scope.usd,
2107
+ pendingMessages: executor?.pendingMessages ?? 0,
2108
+ recentActivity: executor?.recentActivity ?? [],
2109
+ ...note ? { note } : {}
2110
+ };
2111
+ }
2112
+ //#endregion
2113
+ //#region src/runtime/supervise/wait.ts
2114
+ /**
2115
+ *
2116
+ * WAIT-STATES — a supervision-tree node that waits on wall-clock time or an external condition
2117
+ * without holding a worker, an executor, a sandbox, or a single LLM turn.
2118
+ *
2119
+ * A long-horizon run spends most of its wall-clock NOT computing: waiting for CI to finish,
2120
+ * for a nightly job to land, for a human to reply, for a rate-limit window to reopen. Before
2121
+ * this, the only way to express that was to keep something alive and re-ask — which spends
2122
+ * driver tokens per re-ask and pins a process to the wait.
2123
+ *
2124
+ * Two node kinds, both first-class:
2125
+ * - `timer` — wake at an ABSOLUTE wall-clock instant (`untilMs`).
2126
+ * - `poll` — re-run a named predicate every `intervalMs` until it returns true, or until an
2127
+ * absolute `timeoutAtMs` passes (CI status, file existence, an HTTP probe, an
2128
+ * inbox message).
2129
+ *
2130
+ * ── How this differs from `await_event`'s 15s poll fence (`DEFAULT_AWAIT_EVENT_TIMEOUT_MS`) ──
2131
+ *
2132
+ * They look similar and are not the same mechanism. `await_event` is an IN-RUN RENDEZVOUS: the
2133
+ * driver blocks on the coordination bus for the next event from a live worker, and the 15s fence
2134
+ * exists only so a remote MCP request does not exceed the client's timeout — the caller re-polls.
2135
+ * Every re-poll is another driver inference turn (real tokens) against a process that must stay
2136
+ * up, and nothing about that wait is recorded: kill the process and the wait is simply gone.
2137
+ *
2138
+ * A wait-state is a NODE, not a call. It has a node id in the tree, a journal record carrying its
2139
+ * ABSOLUTE deadline, and it settles through the same `Scope.next()` cursor as any worker. Nobody
2140
+ * is blocked on it — the driver can stop reasoning entirely, and the process can die. Cost while
2141
+ * waiting: zero LLM calls, zero executor, zero sandbox, zero conserved budget (a wait reserves
2142
+ * nothing from the pool). The only in-process residue is one timer entry.
2143
+ *
2144
+ * ── Why probes are NAMED, not passed as closures ────────────────────────────────────────────
2145
+ *
2146
+ * A wait must survive a process restart with its original deadline intact, and a closure cannot
2147
+ * be journaled. So a `poll` names its predicate (`probe: 'ci-green'`) and the run resolves it
2148
+ * through a `WaitProbeRegistry`. A brand-new process re-resolves the SAME name against its own
2149
+ * registry and re-arms the wait — which is what makes "kill the box, the wait keeps waiting"
2150
+ * true rather than aspirational.
2151
+ *
2152
+ * Absolute instants for the same reason: `untilMs` / `timeoutAtMs` are epoch ms, not durations,
2153
+ * so a resumed wait counts down from the original arm, not from the restart. `timerAt`/`pollFor`
2154
+ * build them from a duration when that is what the caller has.
2155
+ *
2156
+ * @experimental
2157
+ */
2158
+ /** Build a `timer` spec from a DURATION. The instant is resolved once, at arm time — a resumed
2159
+ * wait re-uses the journaled instant, never a fresh `now + ms`. */
2160
+ function timerAt(ms, now) {
2161
+ return {
2162
+ kind: "timer",
2163
+ untilMs: now + ms
2164
+ };
2165
+ }
2166
+ /** Build a bounded `poll` spec from a duration. */
2167
+ function pollFor(probe, opts, now) {
2168
+ return {
2169
+ kind: "poll",
2170
+ probe,
2171
+ intervalMs: opts.intervalMs,
2172
+ ...opts.timeoutMs !== void 0 ? { timeoutAtMs: now + opts.timeoutMs } : {},
2173
+ ...opts.args ? { args: opts.args } : {}
2174
+ };
2175
+ }
2176
+ /** Registry over a plain name→predicate record. */
2177
+ function createWaitProbes(entries) {
2178
+ const map = new Map(Object.entries(entries));
2179
+ return { resolve: (name) => map.get(name) };
2180
+ }
2181
+ /** Narrow a settlement's `out` to a wait outcome — a wait settles on the SAME cursor as workers,
2182
+ * so a driver that mixes them tags them apart with this. */
2183
+ function isWaitOutcome(value) {
2184
+ return typeof value === "object" && value !== null && value.waitOutcome === true;
2185
+ }
2186
+ /** The absolute instant a spec is bounded by, or `undefined` for an unbounded poll. */
2187
+ function waitUntil(spec) {
2188
+ return spec.kind === "timer" ? spec.untilMs : spec.timeoutAtMs;
2189
+ }
2190
+ /** Structural validation, independent of the run. Returns null when the spec is usable. */
2191
+ function validateWaitSpec(spec) {
2192
+ if (spec.kind === "timer") return Number.isFinite(spec.untilMs) ? null : "timer.untilMs must be a finite epoch-ms instant";
2193
+ if (typeof spec.probe !== "string" || spec.probe.length === 0) return "poll.probe must be a non-empty probe name";
2194
+ if (!Number.isFinite(spec.intervalMs) || spec.intervalMs <= 0) return "poll.intervalMs must be > 0";
2195
+ if (spec.timeoutAtMs !== void 0 && !Number.isFinite(spec.timeoutAtMs)) return "poll.timeoutAtMs must be a finite epoch-ms instant";
2196
+ return null;
2197
+ }
2198
+ /**
2199
+ * A wait must not silently outlive the run's HARD wall-clock ceiling. When the conserved pool
2200
+ * carries an absolute `deadlineMs`, a wait bounded past it — or an unbounded poll — is REFUSED at
2201
+ * arm time (`deadline-exceeded`) rather than sleeping through a ceiling the caller already set.
2202
+ * The alternative (extending the pool's deadline to cover the wait) would let a wait override a
2203
+ * hard budget guard, which is exactly what mechanic D forbids; a run that intends week-long waits
2204
+ * either sets a deadline that covers them or sets none.
2205
+ */
2206
+ function assertWaitWithinDeadline(spec, deadlineMs) {
2207
+ if (deadlineMs <= 0) return true;
2208
+ const until = waitUntil(spec);
2209
+ if (until === void 0) return false;
2210
+ return until <= deadlineMs;
2211
+ }
2212
+ /**
2213
+ * Drive one wait to its resolution. Spends nothing: a `timer` is one sleep, a `poll` is a sleep
2214
+ * between predicate checks. No LLM call, no executor, no box, no budget reservation.
2215
+ */
2216
+ async function runWait(args) {
2217
+ const { spec, label, armedAt, resumed, signal, now } = args;
2218
+ const sleep = args.sleep ?? defaultSleep;
2219
+ if (signal.aborted) return {
2220
+ kind: "cancelled",
2221
+ reason: "aborted before arming"
2222
+ };
2223
+ if (spec.kind === "timer") {
2224
+ const remaining = spec.untilMs - now();
2225
+ if (remaining > 0) await sleep(remaining, signal);
2226
+ if (signal.aborted) return {
2227
+ kind: "cancelled",
2228
+ reason: "wait aborted"
2229
+ };
2230
+ return {
2231
+ kind: "woke",
2232
+ outcome: outcome("timer", "fired", label, spec.untilMs, armedAt, now(), 0, 0, resumed)
2233
+ };
2234
+ }
2235
+ const probe = args.probes?.resolve(spec.probe);
2236
+ if (!probe) throw new ValidationError(`wait '${label}': poll probe '${spec.probe}' is not registered in this run's WaitProbeRegistry`);
2237
+ let polls = 0;
2238
+ let probeErrors = 0;
2239
+ for (;;) {
2240
+ if (signal.aborted) return {
2241
+ kind: "cancelled",
2242
+ reason: "wait aborted"
2243
+ };
2244
+ polls += 1;
2245
+ let fired = false;
2246
+ try {
2247
+ fired = await probe(spec.args, signal);
2248
+ } catch {
2249
+ probeErrors += 1;
2250
+ }
2251
+ if (fired) return {
2252
+ kind: "woke",
2253
+ outcome: outcome("poll", "fired", label, spec.timeoutAtMs, armedAt, now(), polls, probeErrors, resumed)
2254
+ };
2255
+ if (spec.timeoutAtMs !== void 0 && now() >= spec.timeoutAtMs) return {
2256
+ kind: "woke",
2257
+ outcome: outcome("poll", "timeout", label, spec.timeoutAtMs, armedAt, now(), polls, probeErrors, resumed)
2258
+ };
2259
+ await sleep(spec.timeoutAtMs === void 0 ? spec.intervalMs : Math.min(spec.intervalMs, Math.max(0, spec.timeoutAtMs - now())), signal);
2260
+ }
2261
+ }
2262
+ function outcome(kind, settled, label, untilMs, armedAt, wokenAt, polls, probeErrors, resumed) {
2263
+ return {
2264
+ waitOutcome: true,
2265
+ kind,
2266
+ settled,
2267
+ label,
2268
+ ...untilMs !== void 0 ? { untilMs } : {},
2269
+ armedAt,
2270
+ wokenAt,
2271
+ polls,
2272
+ probeErrors,
2273
+ resumed
2274
+ };
2275
+ }
2276
+ /** `setTimeout` that resolves early (and clears) when the scope aborts, so a cancelled wait
2277
+ * releases the event loop immediately instead of holding the process to its deadline. */
2278
+ function defaultSleep(ms, signal) {
2279
+ if (ms <= 0 || signal.aborted) return Promise.resolve();
2280
+ return new Promise((resolve) => {
2281
+ const timer = setTimeout(() => {
2282
+ signal.removeEventListener("abort", onAbort);
2283
+ resolve();
2284
+ }, ms);
2285
+ function onAbort() {
2286
+ clearTimeout(timer);
2287
+ resolve();
2288
+ }
2289
+ signal.addEventListener("abort", onAbort, { once: true });
2290
+ });
2291
+ }
2292
+ //#endregion
2293
+ //#region src/runtime/supervise/scope.ts
2294
+ /**
2295
+ *
2296
+ * The reactive `Scope` impl (KEYSTONE, build step 4 + the step-8 adapter).
2297
+ *
2298
+ * An `Agent.act` runs inside a `Scope`. It `spawn`s children dynamically and reacts to
2299
+ * them via `next()`. The scope owns ONE in-memory nursery — the authoritative live set —
2300
+ * and is the single place that drives a child's lifecycle: reserve budget atomically,
2301
+ * resolve a `Executor` through the open registry, run it (one-shot OR streaming),
2302
+ * fold its normalized `UsageEvent`s into a conserved `Spend`, reconcile the reservation
2303
+ * (refunding the unspent remainder), persist the result blob + journal records, and
2304
+ * deliver the `Settled` through the `next()` cursor.
2305
+ *
2306
+ * Three invariants this impl enforces by construction:
2307
+ * - `next()` is a ray.wait n=1 cursor over THIS scope's live set; it assigns the
2308
+ * monotonic `seq` (the recorded cursor order) at the moment it yields a settlement, so
2309
+ * replay re-delivers in the identical order — `seq` is never wall-clock.
2310
+ * - Budget is reserved at spawn and reconciled at settle through the shared `BudgetPool`,
2311
+ * so `spawn` fails CLOSED on an exhausted pool and total ≡ free + reserved + committed.
2312
+ * - `view` reads the in-memory nursery, never the journal — O(live), synchronous.
2313
+ *
2314
+ * The settle path is the only writer of journal `settled` events; the spawn path the only
2315
+ * writer of `spawned` events. The result blob is `put` BEFORE the journal `settled` record
2316
+ * references its `outRef`, so a crash can never leave a journaled ref with no blob.
2317
+ *
2318
+ * @experimental
2319
+ */
2320
+ /**
2321
+ * The recursion seam key. A `Scope` seeds a value of this on each child's
2322
+ * `ExecutorContext.seams` so a child whose executor is a DRIVER can mount a NESTED `Scope`
2323
+ * over the SAME conserved pool at `depth+1`. A leaf executor never reads it. Single-sourced
2324
+ * here so the scope and the driver-executor agree on the seam without a circular import.
2325
+ */
2326
+ const nestedScopeSeamKey = "nested-scope";
2327
+ function makeNestedScopeSeam(args, childNodeId) {
2328
+ return {
2329
+ depth: args.depth,
2330
+ ...args.maxDepth !== void 0 ? { maxDepth: args.maxDepth } : {},
2331
+ journalRoot: args.root,
2332
+ mount(nestedRoot, signal) {
2333
+ return createScope({
2334
+ parentId: childNodeId,
2335
+ root: nestedRoot,
2336
+ pool: args.pool,
2337
+ journal: args.journal,
2338
+ blobs: args.blobs,
2339
+ executors: args.executors,
2340
+ seams: args.seams,
2341
+ depth: args.depth + 1,
2342
+ ...args.maxDepth !== void 0 ? { maxDepth: args.maxDepth } : {},
2343
+ signal,
2344
+ ...args.now ? { now: args.now } : {},
2345
+ ...args.hooks ? { hooks: args.hooks } : {}
2346
+ });
2347
+ }
2348
+ };
2349
+ }
2350
+ /** Create the reactive `Scope` a driver's `Agent.act` runs inside: spawn children on an atomically reserved conserved budget, settle via the `next()` cursor, journal for replay. */
2351
+ function createScope(args) {
2352
+ const children = /* @__PURE__ */ new Map();
2353
+ let spawnOrdinal = args.resumeFrom ? args.resumeFrom.maxSpawnOrdinal + 1 : 0;
2354
+ let cursorSeq = args.resumeFrom ? args.resumeFrom.maxCursorSeq + 1 : 0;
2355
+ let waitOrdinal = args.resumeFrom ? args.resumeFrom.maxWaitOrdinal + 1 : 0;
2356
+ let meterSeq = 0;
2357
+ const now = args.now ?? Date.now;
2358
+ const unclaimedWaits = new Map((args.resumeFrom?.waits ?? []).map((w) => [w.label, w]));
2359
+ function spawn(agent, task, opts) {
2360
+ if (args.maxDepth !== void 0 && args.depth >= args.maxDepth) return {
2361
+ ok: false,
2362
+ reason: "depth-exceeded"
2363
+ };
2364
+ const spec = agent.executorSpec;
2365
+ if (!isAgentSpec(spec)) throw new ValidationError(`scope.spawn: agent "${agent.name}" exposes no \`executorSpec\` (AgentSpec) to resolve a Executor`);
2366
+ const resolved = args.executors.resolve(spec);
2367
+ if (!resolved.succeeded) throw new ValidationError(`scope.spawn: ${resolved.error}`);
2368
+ const reservation = args.pool.reserve(opts.budget);
2369
+ if (!reservation.ok) return {
2370
+ ok: false,
2371
+ reason: reservation.reason
2372
+ };
2373
+ try {
2374
+ const ordinal = spawnOrdinal++;
2375
+ const id = `${args.parentId}:s${ordinal}`;
2376
+ const childAbort = new AbortController();
2377
+ const cascadeAbort = () => childAbort.abort();
2378
+ if (args.signal.aborted) childAbort.abort();
2379
+ else args.signal.addEventListener("abort", cascadeAbort, { once: true });
2380
+ const ctx = {
2381
+ signal: childAbort.signal,
2382
+ seams: {
2383
+ ...args.seams,
2384
+ [nestedScopeSeamKey]: makeNestedScopeSeam(args, id)
2385
+ }
2386
+ };
2387
+ const executor = resolved.value(spec, ctx);
2388
+ const handle = {
2389
+ id,
2390
+ label: opts.label,
2391
+ get status() {
2392
+ return children.get(id)?.status ?? "cancelled";
2393
+ },
2394
+ abort(reason) {
2395
+ childAbort.abort(reason);
2396
+ }
2397
+ };
2398
+ const live = {
2399
+ id,
2400
+ status: "acquiring",
2401
+ runtime: executor.runtime,
2402
+ budget: opts.budget,
2403
+ label: opts.label,
2404
+ spent: zeroSpend(),
2405
+ settled: void 0,
2406
+ delivered: false,
2407
+ executorDone: false,
2408
+ startedAt: now(),
2409
+ lastActivityAt: now(),
2410
+ ...executor.deliver ? { deliver: executor.deliver.bind(executor) } : {},
2411
+ ...executor.progress ? { readProgress: executor.progress.bind(executor) } : {},
2412
+ ...executor.traceSource ? { readTraceSource: executor.traceSource.bind(executor) } : {}
2413
+ };
2414
+ children.set(id, live);
2415
+ args.journal.appendEvent(args.root, {
2416
+ kind: "spawned",
2417
+ id,
2418
+ parent: args.parentId,
2419
+ label: opts.label,
2420
+ budget: opts.budget,
2421
+ runtime: executor.runtime,
2422
+ seq: ordinal,
2423
+ at: new Date(now()).toISOString()
2424
+ });
2425
+ notifyRuntimeHookEvent(args.hooks, {
2426
+ id: `${id}:spawn`,
2427
+ runId: args.root,
2428
+ target: "agent.spawn",
2429
+ phase: "after",
2430
+ timestamp: now(),
2431
+ stepIndex: ordinal,
2432
+ parentId: args.parentId,
2433
+ payload: {
2434
+ childId: id,
2435
+ label: opts.label,
2436
+ runtime: executor.runtime,
2437
+ budget: opts.budget,
2438
+ depth: args.depth
2439
+ }
2440
+ }, { signal: args.signal });
2441
+ live.settled = runChild(live, executor, childAbort, task, opts, args.pool, reservation.ticket, args.blobs, now).then((s) => {
2442
+ live.resolved = s;
2443
+ return s;
2444
+ }).finally(() => {
2445
+ args.signal.removeEventListener("abort", cascadeAbort);
2446
+ });
2447
+ return {
2448
+ ok: true,
2449
+ handle
2450
+ };
2451
+ } catch (err) {
2452
+ args.pool.reconcile(reservation.ticket, zeroSpend());
2453
+ throw err;
2454
+ }
2455
+ }
2456
+ async function next() {
2457
+ const undelivered = () => [...children.values()].filter((c) => !c.delivered);
2458
+ if (undelivered().length === 0) return null;
2459
+ for (;;) {
2460
+ const pending = undelivered();
2461
+ if (pending.length === 0) return null;
2462
+ const chosen = pending.find((c) => c.resolved !== void 0) ?? await raceFirstSettled(pending);
2463
+ if (chosen.delivered) continue;
2464
+ chosen.delivered = true;
2465
+ const seq = cursorSeq++;
2466
+ const settlement = chosen.resolved;
2467
+ if (!settlement) throw new ValidationError(`scope.next: child '${chosen.id}' won the settle race without a resolved value`);
2468
+ return finalizeSettlement(chosen, settlement, seq, args, now);
2469
+ }
2470
+ }
2471
+ async function nextResolved() {
2472
+ for (;;) {
2473
+ const candidates = [...children.values()].filter((c) => !c.delivered);
2474
+ const pick = candidates.find((c) => c.resolved !== void 0) ?? candidates.find((c) => c.executorDone);
2475
+ if (!pick) return null;
2476
+ const settlement = await pick.settled;
2477
+ if (pick.delivered) continue;
2478
+ pick.delivered = true;
2479
+ return finalizeSettlement(pick, settlement, cursorSeq++, args, now);
2480
+ }
2481
+ }
2482
+ function send(nodeId, msg) {
2483
+ const child = children.get(nodeId);
2484
+ if (!child || child.delivered || !child.deliver) return false;
2485
+ child.deliver(msg);
2486
+ child.lastActivityAt = now();
2487
+ return true;
2488
+ }
2489
+ /**
2490
+ * Arm a wait-state node. Deliberately NOT a `spawn` with a sleeping executor: it resolves no
2491
+ * `AgentSpec`, constructs no `Executor`, and — the point of the whole mechanic — reserves NOTHING
2492
+ * from the conserved pool, so a week-long wait costs zero tokens and zero dollars and cannot
2493
+ * starve a worker of budget.
2494
+ *
2495
+ * Fail-closed admission, mirroring `spawn`'s typed outcome. The deadline check is the one that
2496
+ * matters: a wait may not silently sleep past the pool's hard wall-clock ceiling, and it may not
2497
+ * extend that ceiling either (that would make a wait override a budget guard).
2498
+ */
2499
+ function wait(spec, opts) {
2500
+ if (validateWaitSpec(spec) !== null) return {
2501
+ ok: false,
2502
+ reason: "invalid-spec"
2503
+ };
2504
+ if (spec.kind === "poll" && args.probes?.resolve(spec.probe) === void 0) return {
2505
+ ok: false,
2506
+ reason: "unknown-probe"
2507
+ };
2508
+ const adopted = unclaimedWaits.get(opts.label);
2509
+ if (adopted) unclaimedWaits.delete(opts.label);
2510
+ const effectiveSpec = adopted?.spec ?? spec;
2511
+ const armedAt = adopted?.armedAt ?? now();
2512
+ if (!assertWaitWithinDeadline(effectiveSpec, args.pool.readout().deadlineMs)) return {
2513
+ ok: false,
2514
+ reason: "deadline-exceeded"
2515
+ };
2516
+ const id = adopted ? adopted.id : `${args.parentId}:w${waitOrdinal}`;
2517
+ const ordinal = adopted ? adopted.ordinal : waitOrdinal;
2518
+ if (!adopted) waitOrdinal += 1;
2519
+ const waitAbort = new AbortController();
2520
+ const cascadeAbort = () => waitAbort.abort();
2521
+ if (args.signal.aborted) waitAbort.abort();
2522
+ else args.signal.addEventListener("abort", cascadeAbort, { once: true });
2523
+ const handle = {
2524
+ id,
2525
+ label: opts.label,
2526
+ get status() {
2527
+ return children.get(id)?.status ?? "cancelled";
2528
+ },
2529
+ abort(reason) {
2530
+ waitAbort.abort(reason);
2531
+ }
2532
+ };
2533
+ const live = {
2534
+ id,
2535
+ status: "waiting",
2536
+ runtime: "wait",
2537
+ budget: {
2538
+ maxIterations: 0,
2539
+ maxTokens: 0
2540
+ },
2541
+ label: opts.label,
2542
+ spent: zeroSpend(),
2543
+ settled: void 0,
2544
+ delivered: false,
2545
+ executorDone: false,
2546
+ startedAt: armedAt,
2547
+ lastActivityAt: now(),
2548
+ wait: {
2549
+ spec: effectiveSpec,
2550
+ armedAt,
2551
+ label: opts.label
2552
+ }
2553
+ };
2554
+ children.set(id, live);
2555
+ if (!adopted) args.journal.appendEvent(args.root, {
2556
+ kind: "waiting",
2557
+ id,
2558
+ parent: args.parentId,
2559
+ label: opts.label,
2560
+ spec: effectiveSpec,
2561
+ armedAt,
2562
+ seq: ordinal,
2563
+ at: new Date(now()).toISOString()
2564
+ });
2565
+ notifyRuntimeHookEvent(args.hooks, {
2566
+ id: `${id}:waiting`,
2567
+ runId: args.root,
2568
+ target: "agent.spawn",
2569
+ phase: "after",
2570
+ timestamp: now(),
2571
+ stepIndex: ordinal,
2572
+ parentId: args.parentId,
2573
+ payload: {
2574
+ childId: id,
2575
+ label: opts.label,
2576
+ runtime: "wait",
2577
+ wait: effectiveSpec,
2578
+ armedAt,
2579
+ resumed: adopted !== void 0
2580
+ }
2581
+ }, { signal: args.signal });
2582
+ live.settled = runWait({
2583
+ spec: effectiveSpec,
2584
+ label: opts.label,
2585
+ armedAt,
2586
+ resumed: adopted !== void 0,
2587
+ signal: waitAbort.signal,
2588
+ ...args.probes ? { probes: args.probes } : {},
2589
+ now,
2590
+ ...args.waitSleep ? { sleep: args.waitSleep } : {}
2591
+ }).then(async (resolution) => {
2592
+ live.executorDone = true;
2593
+ live.lastActivityAt = now();
2594
+ if (resolution.kind === "cancelled") return {
2595
+ kind: "down",
2596
+ reason: resolution.reason,
2597
+ infra: false,
2598
+ restartCount: 0
2599
+ };
2600
+ const outRef = contentAddress(resolution.outcome);
2601
+ await args.blobs.put(outRef, resolution.outcome);
2602
+ return {
2603
+ kind: "done",
2604
+ out: resolution.outcome,
2605
+ outRef,
2606
+ spent: zeroSpend()
2607
+ };
2608
+ }).catch((err) => {
2609
+ live.executorDone = true;
2610
+ return {
2611
+ kind: "down",
2612
+ reason: errMessage(err),
2613
+ infra: true,
2614
+ restartCount: 0
2615
+ };
2616
+ }).then((s) => {
2617
+ live.resolved = s;
2618
+ return s;
2619
+ }).finally(() => {
2620
+ args.signal.removeEventListener("abort", cascadeAbort);
2621
+ });
2622
+ return {
2623
+ ok: true,
2624
+ handle
2625
+ };
2626
+ }
2627
+ function progress(nodeId, opts = {}) {
2628
+ const child = children.get(nodeId);
2629
+ if (!child) return void 0;
2630
+ let fromExecutor;
2631
+ try {
2632
+ fromExecutor = child.readProgress?.();
2633
+ } catch {
2634
+ fromExecutor = void 0;
2635
+ }
2636
+ return readWorkerProgress({
2637
+ id: child.id,
2638
+ status: child.status,
2639
+ steerable: child.deliver !== void 0 && !child.delivered,
2640
+ startedAt: child.startedAt,
2641
+ lastActivityAt: child.lastActivityAt,
2642
+ turns: child.spent.iterations,
2643
+ tokens: child.spent.tokens,
2644
+ usd: child.spent.usd
2645
+ }, fromExecutor, opts.now ?? now(), opts.stallAfterMs ?? 18e4);
2646
+ }
2647
+ function traceSource(nodeId) {
2648
+ const child = children.get(nodeId);
2649
+ if (!child) return void 0;
2650
+ try {
2651
+ return child.readTraceSource?.();
2652
+ } catch {
2653
+ return;
2654
+ }
2655
+ }
2656
+ async function meter(spend, detail) {
2657
+ const seq = meterSeq++;
2658
+ args.pool.observe(spend);
2659
+ await args.journal.appendEvent(args.root, {
2660
+ kind: "metered",
2661
+ id: args.parentId,
2662
+ spend,
2663
+ seq,
2664
+ at: new Date(now()).toISOString()
2665
+ });
2666
+ notifyRuntimeHookEvent(args.hooks, {
2667
+ id: `${args.parentId}:meter:${seq}`,
2668
+ runId: args.root,
2669
+ target: "agent.turn",
2670
+ phase: "after",
2671
+ timestamp: now(),
2672
+ parentId: args.parentId,
2673
+ payload: {
2674
+ spend,
2675
+ ...detail ?? {}
2676
+ }
2677
+ }, { signal: args.signal });
2678
+ }
2679
+ const resume = args.resumeFrom ? {
2680
+ settled: args.resumeFrom.settled,
2681
+ view: args.resumeFrom.view,
2682
+ waits: args.resumeFrom.waits
2683
+ } : void 0;
2684
+ return {
2685
+ spawn,
2686
+ next,
2687
+ nextResolved,
2688
+ send,
2689
+ wait,
2690
+ progress,
2691
+ traceSource,
2692
+ signal: args.signal,
2693
+ meter,
2694
+ ...resume ? { resume } : {},
2695
+ get view() {
2696
+ return makeTreeView(args.parentId, children);
2697
+ },
2698
+ get budget() {
2699
+ return args.pool.readout();
2700
+ }
2701
+ };
2702
+ }
2703
+ /** Await whichever pending child settles first, returning the child (its `resolved` is set
2704
+ * by the time this resolves because `runChild`'s `.then` sets it before the promise
2705
+ * resolves downstream). */
2706
+ async function raceFirstSettled(pending) {
2707
+ return Promise.race(pending.map((c) => c.settled.then(() => c)));
2708
+ }
2709
+ /** Stamp the cursor `seq`, write the `settled` journal record, and project the
2710
+ * `PreSeqSettled` into the frozen `Settled` the driver branches on. */
2711
+ async function finalizeSettlement(child, settlement, seq, args, now) {
2712
+ const handle = frozenHandle(child);
2713
+ if (child.wait) return finalizeWait(child, settlement, seq, args, now, handle);
2714
+ if (settlement.kind === "down") {
2715
+ child.status = "failed";
2716
+ await args.journal.appendEvent(args.root, {
2717
+ kind: "settled",
2718
+ id: child.id,
2719
+ status: "down",
2720
+ spent: child.spent,
2721
+ infra: settlement.infra,
2722
+ seq,
2723
+ at: new Date(now()).toISOString()
2724
+ });
2725
+ if (settlement.metered) await args.journal.appendEvent(args.root, {
2726
+ kind: "metered",
2727
+ id: child.id,
2728
+ spend: settlement.metered,
2729
+ seq,
2730
+ at: new Date(now()).toISOString()
2731
+ });
2732
+ notifyRuntimeHookEvent(args.hooks, {
2733
+ id: `${child.id}:settled`,
2734
+ runId: args.root,
2735
+ target: "agent.child",
2736
+ phase: "after",
2737
+ timestamp: now(),
2738
+ stepIndex: seq,
2739
+ parentId: args.parentId,
2740
+ payload: {
2741
+ childId: child.id,
2742
+ status: "down",
2743
+ reason: settlement.reason,
2744
+ infra: settlement.infra,
2745
+ spent: child.spent
2746
+ }
2747
+ }, { signal: args.signal });
2748
+ return {
2749
+ kind: "down",
2750
+ handle,
2751
+ reason: settlement.reason,
2752
+ infra: settlement.infra,
2753
+ restartCount: settlement.restartCount,
2754
+ seq
2755
+ };
2756
+ }
2757
+ child.status = "done";
2758
+ child.outRef = settlement.outRef;
2759
+ child.spent = settlement.spent;
2760
+ await args.journal.appendEvent(args.root, {
2761
+ kind: "settled",
2762
+ id: child.id,
2763
+ status: "done",
2764
+ outRef: settlement.outRef,
2765
+ ...settlement.verdict ? { verdict: settlement.verdict } : {},
2766
+ spent: settlement.spent,
2767
+ seq,
2768
+ at: new Date(now()).toISOString()
2769
+ });
2770
+ if (settlement.metered) await args.journal.appendEvent(args.root, {
2771
+ kind: "metered",
2772
+ id: child.id,
2773
+ spend: settlement.metered,
2774
+ seq,
2775
+ at: new Date(now()).toISOString()
2776
+ });
2777
+ notifyRuntimeHookEvent(args.hooks, {
2778
+ id: `${child.id}:settled`,
2779
+ runId: args.root,
2780
+ target: "agent.child",
2781
+ phase: "after",
2782
+ timestamp: now(),
2783
+ stepIndex: seq,
2784
+ parentId: args.parentId,
2785
+ payload: {
2786
+ childId: child.id,
2787
+ status: "done",
2788
+ outRef: settlement.outRef,
2789
+ score: settlement.verdict?.score,
2790
+ valid: settlement.verdict?.valid,
2791
+ spent: settlement.spent
2792
+ }
2793
+ }, { signal: args.signal });
2794
+ return {
2795
+ kind: "done",
2796
+ handle,
2797
+ out: settlement.out,
2798
+ outRef: settlement.outRef,
2799
+ ...settlement.verdict ? { verdict: settlement.verdict } : {},
2800
+ spent: settlement.spent,
2801
+ seq
2802
+ };
2803
+ }
2804
+ /** Journal a wait-state's settlement as a `woken` event and project it onto the same `Settled`
2805
+ * the driver branches on. `by` names WHY it woke — `fired` / `timeout` / `cancelled` — which is
2806
+ * the fact a resumed reader needs and cannot recover from a payload it may never fetch. */
2807
+ async function finalizeWait(child, settlement, seq, args, now, handle) {
2808
+ const at = new Date(now()).toISOString();
2809
+ if (settlement.kind === "down") {
2810
+ child.status = "cancelled";
2811
+ await args.journal.appendEvent(args.root, {
2812
+ kind: "woken",
2813
+ id: child.id,
2814
+ by: "cancelled",
2815
+ seq,
2816
+ at
2817
+ });
2818
+ return {
2819
+ kind: "down",
2820
+ handle,
2821
+ reason: settlement.reason,
2822
+ infra: settlement.infra,
2823
+ restartCount: settlement.restartCount,
2824
+ seq
2825
+ };
2826
+ }
2827
+ child.status = "done";
2828
+ child.outRef = settlement.outRef;
2829
+ const out = settlement.out;
2830
+ await args.journal.appendEvent(args.root, {
2831
+ kind: "woken",
2832
+ id: child.id,
2833
+ by: out.settled,
2834
+ outRef: settlement.outRef,
2835
+ seq,
2836
+ at
2837
+ });
2838
+ notifyRuntimeHookEvent(args.hooks, {
2839
+ id: `${child.id}:woken`,
2840
+ runId: args.root,
2841
+ target: "agent.child",
2842
+ phase: "after",
2843
+ timestamp: now(),
2844
+ stepIndex: seq,
2845
+ parentId: args.parentId,
2846
+ payload: {
2847
+ childId: child.id,
2848
+ status: "done",
2849
+ wait: out
2850
+ }
2851
+ }, { signal: args.signal });
2852
+ return {
2853
+ kind: "done",
2854
+ handle,
2855
+ out: settlement.out,
2856
+ outRef: settlement.outRef,
2857
+ spent: settlement.spent,
2858
+ seq
2859
+ };
2860
+ }
2861
+ /**
2862
+ * Drive one child's `Executor` to a terminal `PreSeqSettled`, folding usage into the
2863
+ * conserved `Spend`, reconciling the reservation, and persisting the result blob. Both
2864
+ * executor shapes are handled here: a one-shot `Promise<ExecutorResult>` and a streaming
2865
+ * `AsyncIterable<UsageEvent>` whose terminal artifact is read from `resultArtifact()`.
2866
+ *
2867
+ * A thrown executor (or a real abort) becomes a TYPED `down` — never re-thrown — so a
2868
+ * single failing child cannot reject the `next()` cursor (the M2 typed-result discipline,
2869
+ * applied per child). The reservation is reconciled on EVERY path (success, abort, throw)
2870
+ * so the conserved pool can never leak a reservation.
2871
+ */
2872
+ async function runChild(live, executor, childAbort, task, opts, pool, ticket, blobs, now) {
2873
+ let reconciled = false;
2874
+ const reconcileOnce = (spend) => {
2875
+ if (reconciled) return;
2876
+ reconciled = true;
2877
+ pool.reconcile(ticket, clampSpend(spend, opts.budget));
2878
+ };
2879
+ try {
2880
+ live.status = "running";
2881
+ const ran = executor.execute(task, childAbort.signal);
2882
+ let artifact;
2883
+ if (isAsyncIterable(ran)) {
2884
+ const spend = await foldStream(ran, (running) => {
2885
+ live.spent = running;
2886
+ live.lastActivityAt = now();
2887
+ });
2888
+ live.spent = spend;
2889
+ artifact = executor.resultArtifact();
2890
+ reconcileOnce(spend);
2891
+ } else {
2892
+ const terminal = await ran;
2893
+ live.spent = terminal.spent;
2894
+ artifact = terminal;
2895
+ reconcileOnce(terminal.spent);
2896
+ }
2897
+ live.executorDone = true;
2898
+ const ownMetered = executor.metered?.();
2899
+ if (childAbort.signal.aborted) {
2900
+ await teardownSafe(executor, opts.shutdown ?? "brutalKill");
2901
+ return downRecord("aborted before settle", true, ownMetered);
2902
+ }
2903
+ const outRef = contentAddress(artifact.out);
2904
+ await blobs.put(outRef, artifact.out);
2905
+ await teardownSafe(executor, opts.shutdown ?? "infinity");
2906
+ return {
2907
+ kind: "done",
2908
+ out: artifact.out,
2909
+ outRef,
2910
+ ...artifact.verdict ? { verdict: artifact.verdict } : {},
2911
+ spent: live.spent,
2912
+ ...ownMetered ? { metered: ownMetered } : {}
2913
+ };
2914
+ } catch (err) {
2915
+ live.executorDone = true;
2916
+ reconcileOnce(live.spent);
2917
+ await teardownSafe(executor, "brutalKill");
2918
+ const aborted = childAbort.signal.aborted || isAbortError(err);
2919
+ return downRecord(errMessage(err), aborted || isInfraError(err), executor.metered?.());
2920
+ }
2921
+ }
2922
+ /**
2923
+ * The step-8 merge-boundary adapter (M4): rehydrate a `Settled.done` into the kernel's
2924
+ * `Iteration` shape so `defaultSelectWinner` stays single-sourced — the supervisor selects
2925
+ * across settled children with the SAME argmax the loop kernel uses, not a forked copy.
2926
+ *
2927
+ * `index` is the cursor `seq` (the recorded, replay-stable order); `output`/`verdict`/
2928
+ * `tokenUsage`/`costUsd` are read straight off the settlement (already rehydrated from the
2929
+ * `outRef` blob by `next()`). Events are empty — a settled child is an opaque leaf result,
2930
+ * not a sandbox event stream — and the timing/cost fields project its conserved `Spend`.
2931
+ * Fail loud on a `down` settlement: only a `done` child is an iteration.
2932
+ */
2933
+ function settledToIteration(settled) {
2934
+ if (settled.kind === "down") throw new ValidationError(`settledToIteration: cannot adapt a 'down' settlement (node '${settled.handle.id}', seq ${settled.seq}) to an Iteration`);
2935
+ return {
2936
+ index: settled.seq,
2937
+ task: void 0,
2938
+ agentRunName: settled.handle.label,
2939
+ output: settled.out,
2940
+ ...settled.verdict ? { verdict: settled.verdict } : {},
2941
+ events: [],
2942
+ startedAt: 0,
2943
+ endedAt: settled.spent.ms,
2944
+ costUsd: settled.spent.usd,
2945
+ tokenUsage: {
2946
+ input: settled.spent.tokens.input,
2947
+ output: settled.spent.tokens.output
2948
+ }
2949
+ };
2950
+ }
2951
+ function makeTreeView(root, children) {
2952
+ const nodes = [...children.values()].map((c) => ({
2953
+ id: c.id,
2954
+ parent: root,
2955
+ label: c.label,
2956
+ status: c.status,
2957
+ runtime: c.runtime,
2958
+ budget: c.budget,
2959
+ spent: c.spent,
2960
+ ...c.outRef ? { outRef: c.outRef } : {}
2961
+ }));
2962
+ return {
2963
+ root,
2964
+ nodes,
2965
+ inFlight: nodes.filter((n) => n.status === "running" || n.status === "acquiring").length,
2966
+ waiting: nodes.filter((n) => n.status === "waiting").length
2967
+ };
2968
+ }
2969
+ function frozenHandle(child) {
2970
+ return {
2971
+ id: child.id,
2972
+ label: child.label,
2973
+ status: child.status,
2974
+ abort() {}
2975
+ };
2976
+ }
2977
+ /**
2978
+ * Fold a streaming executor's normalized usage into the conserved `Spend`, publishing the
2979
+ * running total after EVERY event via `onProgress`.
2980
+ *
2981
+ * The publish is what makes a running worker observable. Before it, `live.spent` was assigned
2982
+ * once — after the whole stream drained — so `observe_agent` on a RUNNING worker reported
2983
+ * `{tokens:0, iterations:0}` for the entire run and a driver had no way to tell a worker three
2984
+ * turns deep from one that had produced nothing. It also means a worker that THROWS mid-stream
2985
+ * now reconciles its real partial spend instead of refunding work the provider already billed.
2986
+ */
2987
+ async function foldStream(stream, onProgress) {
2988
+ const tokens = {
2989
+ input: 0,
2990
+ output: 0
2991
+ };
2992
+ let usd = 0;
2993
+ let iterations = 0;
2994
+ for await (const ev of stream) {
2995
+ if (ev.kind === "tokens") {
2996
+ tokens.input += ev.input;
2997
+ tokens.output += ev.output;
2998
+ } else if (ev.kind === "cost") usd += ev.usd;
2999
+ else iterations += 1;
3000
+ onProgress?.({
3001
+ iterations,
3002
+ tokens: { ...tokens },
3003
+ usd,
3004
+ ms: 0
3005
+ });
3006
+ }
3007
+ return {
3008
+ iterations,
3009
+ tokens,
3010
+ usd,
3011
+ ms: 0
3012
+ };
3013
+ }
3014
+ /** Clamp a child's reported spend to its reservation so the pool's fail-loud over-spend
3015
+ * guard never trips on a benign overshoot from an external usage report; the difference
3016
+ * refunds to the pool as if the child stopped at its ceiling. */
3017
+ function clampSpend(spend, budget) {
3018
+ const totalTokens = spend.tokens.input + spend.tokens.output;
3019
+ const tokensOk = totalTokens <= budget.maxTokens;
3020
+ const itersOk = spend.iterations <= budget.maxIterations;
3021
+ const usdOk = budget.maxUsd === void 0 || spend.usd <= budget.maxUsd;
3022
+ if (tokensOk && itersOk && usdOk) return spend;
3023
+ const ratio = !tokensOk && totalTokens > 0 ? budget.maxTokens / totalTokens : 1;
3024
+ return {
3025
+ iterations: Math.min(spend.iterations, budget.maxIterations),
3026
+ tokens: ratio < 1 ? {
3027
+ input: Math.floor(spend.tokens.input * ratio),
3028
+ output: Math.floor(spend.tokens.output * ratio)
3029
+ } : spend.tokens,
3030
+ usd: budget.maxUsd === void 0 ? spend.usd : Math.min(spend.usd, budget.maxUsd),
3031
+ ...spend.usdKnown === false ? { usdKnown: false } : {},
3032
+ ms: spend.ms
3033
+ };
3034
+ }
3035
+ async function teardownSafe(executor, grace) {
3036
+ try {
3037
+ await executor.teardown(grace);
3038
+ } catch {}
3039
+ }
3040
+ function downRecord(reason, infra, metered) {
3041
+ return {
3042
+ kind: "down",
3043
+ reason,
3044
+ infra,
3045
+ restartCount: 0,
3046
+ ...metered ? { metered } : {}
3047
+ };
3048
+ }
3049
+ function zeroSpend() {
3050
+ return {
3051
+ iterations: 0,
3052
+ tokens: {
3053
+ input: 0,
3054
+ output: 0
3055
+ },
3056
+ usd: 0,
3057
+ ms: 0
3058
+ };
3059
+ }
3060
+ function isAsyncIterable(value) {
3061
+ return typeof value === "object" && value !== null && typeof value[Symbol.asyncIterator] === "function";
3062
+ }
3063
+ /** An `AgentSpec` is identified structurally — it carries a `profile` and a `harness`
3064
+ * field (`null` or a `BackendType`) and optionally an `executor`. */
3065
+ function isAgentSpec(value) {
3066
+ if (typeof value !== "object" || value === null) return false;
3067
+ const v = value;
3068
+ return "profile" in v && "harness" in v;
3069
+ }
3070
+ function isAbortError(err) {
3071
+ return typeof err === "object" && err !== null && "name" in err && err.name === "AbortError";
3072
+ }
3073
+ /** External-boundary failures (network/FS/subprocess) are infra — excluded from the merge
3074
+ * `n` and the equal-k assertion. A `ValidationError` from a built-in executor wraps a
3075
+ * config/transport failure, so it counts as infra; other throws are a real bad result. */
3076
+ function isInfraError(err) {
3077
+ return err instanceof ValidationError;
3078
+ }
3079
+ function errMessage(err) {
3080
+ if (err instanceof Error) return err.message;
3081
+ return String(err);
3082
+ }
3083
+ //#endregion
3084
+ //#region src/runtime/supervise/driver-executor.ts
3085
+ /**
3086
+ *
3087
+ * The recursive driver-executor — the seam that lets a SPAWNED child be a DRIVER, so
3088
+ * agents drive agents drive agents over the one keystone atom.
3089
+ *
3090
+ * A spawned child resolves through the open registry to an `Executor`; the built-in
3091
+ * executors (router/inline, sandbox, cli) are LEAVES — `execute(task, signal)` runs the
3092
+ * work and settles. This executor is the recursive case: on `execute`, it mounts a NESTED
3093
+ * `Scope` (the scope hands it the mount via the `nested-scope` seam) over the SAME
3094
+ * conserved pool + shared journal/blobs + the same open registry, one `depth` deeper, then
3095
+ * runs the wrapped driver `Agent.act(task, nestedScope)`. The driver spawns its own
3096
+ * children into that nested scope; each resolves to EITHER a leaf executor (a worker child)
3097
+ * OR this same driver-executor (a driver child) — recursively. So a driver spawns a driver
3098
+ * spawns a worker, all on one budget-conserving tree.
3099
+ *
3100
+ * Why this preserves every keystone invariant (the scope owns the sharing; this executor
3101
+ * only runs the driver over what the scope mounts):
3102
+ * - Conserved budget: the nested scope reserves from the SAME `BudgetPool` the root owns
3103
+ * (the scope mounts it over `args.pool`), so `Σk` is conserved ACROSS depth by
3104
+ * construction — a deep tree cannot overspend the root ceiling (reserve-on-spawn fails
3105
+ * closed at any depth).
3106
+ * - Journal: the nested scope writes to its OWN tree key (`${journalRoot}/${nodeId}`) so
3107
+ * its cursor `seq`s never collide with the parent's in the per-tree uniqueness guard,
3108
+ * while every nested tree shares the one `SpawnJournal` — the whole recursion is one
3109
+ * journal, queryable tree by tree.
3110
+ * - Settlement bubbling: the driver child settles into its PARENT scope with the conserved
3111
+ * spend summed off its nested tree's settled events, so the parent's pool reconcile +
3112
+ * the supervisor's `spentTotal` see the whole sub-tree's spend rolled up — settlements
3113
+ * bubble to the root.
3114
+ * - Depth ceiling: the nested scope runs at `depth+1`, so the supervisor's `maxDepth`
3115
+ * (paired with the conserved pool per R3) fails a spawn closed once the recursion is too
3116
+ * deep — exactly as it does for a flat tree.
3117
+ *
3118
+ * Layering: pure keystone composition. It reuses the scope's `NestedScopeSeam` + the shared
3119
+ * `SpawnJournal`; it builds NO new budget, journal, or selection logic. The recursion rides
3120
+ * the existing atom.
3121
+ *
3122
+ * @experimental
3123
+ */
3124
+ /** The runtime tag the registry maps a driver child to. */
3125
+ const driverRuntime = "driver";
3126
+ /** The metadata marker on a driver child's spec the recursive registry routes on. */
3127
+ const driverRole = "driver";
3128
+ /** True when a spec is a driver child (carries the role marker + a driver Agent). */
3129
+ function isDriverSpec(spec) {
3130
+ if (spec.profile.metadata?.role !== driverRole) return false;
3131
+ const driver = spec.driver;
3132
+ if (!isAgent(driver)) throw new ValidationError("driverExecutor: a driver-role spec must carry a `driver` Agent to run inside its nested scope");
3133
+ return true;
3134
+ }
3135
+ /**
3136
+ * The recursive driver-executor factory. `withDriverExecutor` routes a child marked
3137
+ * `role: 'driver'` here; any other child resolves to a leaf built-in. On `execute`, it
3138
+ * reads the `nested-scope` seam the SCOPE seeded, mounts a nested `Scope` one `depth`
3139
+ * deeper over the shared pool/journal/blobs/registry, runs the driver
3140
+ * `Agent.act(task, nestedScope)`, and reports the conserved spend summed off the nested
3141
+ * tree's settled events — so the parent scope's reconcile rolls the whole sub-tree's spend
3142
+ * into the conserved total.
3143
+ *
3144
+ * A `down` from the nested driver (a thrown `act` or an aborted scope) propagates as a
3145
+ * thrown executor, which the parent scope types into a `down` settlement — the same
3146
+ * fail-loud-into-typed-down discipline a leaf gets.
3147
+ */
3148
+ const driverExecutorFactory = (spec, ctx) => {
3149
+ if (!isDriverSpec(spec)) throw new ValidationError("driverExecutorFactory: spec is not a driver child (no role:\"driver\" marker)");
3150
+ const driver = spec.driver;
3151
+ const journal = spec.journal;
3152
+ const seam = readNestedScopeSeam(ctx);
3153
+ let artifact;
3154
+ let meteredSpend;
3155
+ return {
3156
+ runtime: driverRuntime,
3157
+ async execute(task, signal) {
3158
+ const nestedRoot = nestedTreeKey(seam, journal);
3159
+ await journal.beginTree(nestedRoot, (/* @__PURE__ */ new Date(0)).toISOString());
3160
+ const nestedScope = seam.mount(nestedRoot, signal);
3161
+ try {
3162
+ const out = await driver.act(task, nestedScope);
3163
+ const events = await loadTreeEvents(journal, nestedRoot);
3164
+ const settled = events.filter(isSettled);
3165
+ meteredSpend = nonZeroOrUndef(sumMetered(events));
3166
+ const verdict = deriveDeliveryVerdict(settled);
3167
+ artifact = {
3168
+ outRef: `${driverRuntime}:${nestedRoot}`,
3169
+ out,
3170
+ spent: sumSpend(settled),
3171
+ ...verdict ? { verdict } : {}
3172
+ };
3173
+ return artifact;
3174
+ } catch (err) {
3175
+ meteredSpend = await safeSumMetered(journal, nestedRoot);
3176
+ throw err;
3177
+ }
3178
+ },
3179
+ metered() {
3180
+ return meteredSpend;
3181
+ },
3182
+ teardown() {
3183
+ return Promise.resolve({ destroyed: true });
3184
+ },
3185
+ resultArtifact() {
3186
+ if (!artifact) throw new ValidationError("driverExecutor: resultArtifact() read before execute()");
3187
+ return artifact;
3188
+ }
3189
+ };
3190
+ };
3191
+ /**
3192
+ * Register the driver-executor so a child marked `role: 'driver'` resolves to it. The base
3193
+ * registry resolves by harness alone (it does not read `role`), so a recursive run needs a
3194
+ * registry that routes the driver tag here FIRST. Returns a registry decorator: a
3195
+ * driver-role spec → the driver-executor; everything else → the base registry's resolution
3196
+ * (leaf built-ins + BYO).
3197
+ */
3198
+ function withDriverExecutor(base) {
3199
+ return {
3200
+ register: base.register.bind(base),
3201
+ resolve(spec) {
3202
+ if (spec.profile.metadata?.role === driverRole && !spec.executor) return {
3203
+ succeeded: true,
3204
+ value: driverExecutorFactory
3205
+ };
3206
+ return base.resolve(spec);
3207
+ }
3208
+ };
3209
+ }
3210
+ /** Mint a unique nested-tree key under the parent's journal root. Uses the parent's
3211
+ * `journalRoot` + a per-journal monotonic ordinal so two sibling driver trees never
3212
+ * collide their keys (each driver child mints exactly one nested tree). */
3213
+ function nestedTreeKey(seam, journal) {
3214
+ return `${seam.journalRoot}/d${nextNestOrdinal(journal)}`;
3215
+ }
3216
+ /** Per-journal monotonic nest counter — keyed on the journal instance so a single run's
3217
+ * nested-tree keys are unique without a shared module global. */
3218
+ const nestCounters = /* @__PURE__ */ new WeakMap();
3219
+ function nextNestOrdinal(journal) {
3220
+ let c = nestCounters.get(journal);
3221
+ if (!c) {
3222
+ c = { n: 0 };
3223
+ nestCounters.set(journal, c);
3224
+ }
3225
+ return c.n++;
3226
+ }
3227
+ /** The nested tree's full event list — the one evidence the spend, verdict, AND driver-inference
3228
+ * roll-ups read off the same journal the supervisor sums. */
3229
+ async function loadTreeEvents(journal, nestedRoot) {
3230
+ const events = await journal.loadTree(nestedRoot);
3231
+ if (events === void 0) throw new ValidationError(`driverExecutor: nested tree '${nestedRoot}' missing from the journal after run (corrupted log)`);
3232
+ return events;
3233
+ }
3234
+ function isSettled(ev) {
3235
+ return ev.kind === "settled";
3236
+ }
3237
+ /** Sum the conserved spend over the nested tree's settled events — the honest per-channel
3238
+ * roll-up of the whole sub-tree's child WORK. */
3239
+ function sumSpend(settled) {
3240
+ const total = {
3241
+ iterations: 0,
3242
+ tokens: {
3243
+ input: 0,
3244
+ output: 0
3245
+ },
3246
+ usd: 0,
3247
+ ms: 0
3248
+ };
3249
+ for (const ev of settled) {
3250
+ total.iterations += ev.spent.iterations;
3251
+ total.tokens.input += ev.spent.tokens.input;
3252
+ total.tokens.output += ev.spent.tokens.output;
3253
+ total.usd += ev.spent.usd;
3254
+ if (ev.spent.usdKnown === false) total.usdKnown = false;
3255
+ total.ms += ev.spent.ms;
3256
+ }
3257
+ return total;
3258
+ }
3259
+ /** Sum the nested tree's `metered` events — the sub-tree's whole driver INFERENCE (this driver's
3260
+ * own turns + any sub-driver inference already re-homed into this tree). Re-homed up to the parent
3261
+ * as one `metered` event; never reconciled (already pool-debited live via `observe`). */
3262
+ function sumMetered(events) {
3263
+ const total = {
3264
+ iterations: 0,
3265
+ tokens: {
3266
+ input: 0,
3267
+ output: 0
3268
+ },
3269
+ usd: 0,
3270
+ ms: 0
3271
+ };
3272
+ for (const ev of events) {
3273
+ if (ev.kind !== "metered") continue;
3274
+ total.iterations += ev.spend.iterations;
3275
+ total.tokens.input += ev.spend.tokens.input;
3276
+ total.tokens.output += ev.spend.tokens.output;
3277
+ total.usd += ev.spend.usd;
3278
+ if (ev.spend.usdKnown === false) total.usdKnown = false;
3279
+ total.ms += ev.spend.ms;
3280
+ }
3281
+ return total;
3282
+ }
3283
+ function isNonZeroSpend(s) {
3284
+ return s.iterations > 0 || s.tokens.input > 0 || s.tokens.output > 0 || s.usd > 0 || s.ms > 0;
3285
+ }
3286
+ /** A spend, or `undefined` when it is all-zero — so `metered()` returns undefined for a driver
3287
+ * whose sub-tree did no inference (and the parent journals no empty `metered` event). */
3288
+ function nonZeroOrUndef(s) {
3289
+ return isNonZeroSpend(s) ? s : void 0;
3290
+ }
3291
+ /** Sum the nested tree's metered events, tolerating a missing tree (a crash before `beginTree`
3292
+ * landed) — never throw here, or it would mask the original `act` error on the crash path. */
3293
+ async function safeSumMetered(journal, nestedRoot) {
3294
+ try {
3295
+ return nonZeroOrUndef(sumMetered(await loadTreeEvents(journal, nestedRoot)));
3296
+ } catch {
3297
+ return;
3298
+ }
3299
+ }
3300
+ /** Derive the driver child's delivery verdict from its DIRECT children's settlements:
3301
+ * `valid` iff any direct child settled `done` AND `valid` (the keep-best finalize's pick);
3302
+ * `score` = the best delivered score. Returns `undefined` when no child settled at all (the
3303
+ * driver itself produced nothing to bubble a verdict from). Fail-closed: a child whose verdict
3304
+ * carried no `valid` counts as not-delivered. */
3305
+ function deriveDeliveryVerdict(settled) {
3306
+ let sawChild = false;
3307
+ let anyValid = false;
3308
+ let bestValidScore;
3309
+ let bestDoneScore;
3310
+ for (const ev of settled) {
3311
+ sawChild = true;
3312
+ if (ev.status !== "done") continue;
3313
+ const score = ev.verdict?.score;
3314
+ if (score !== void 0 && (bestDoneScore === void 0 || score > bestDoneScore)) bestDoneScore = score;
3315
+ if (ev.verdict?.valid === true) {
3316
+ anyValid = true;
3317
+ if (score !== void 0 && (bestValidScore === void 0 || score > bestValidScore)) bestValidScore = score;
3318
+ }
3319
+ }
3320
+ if (!sawChild) return void 0;
3321
+ return {
3322
+ valid: anyValid,
3323
+ score: anyValid ? bestValidScore ?? 1 : bestDoneScore ?? 0
3324
+ };
3325
+ }
3326
+ function readNestedScopeSeam(ctx) {
3327
+ const seam = ctx.seams[nestedScopeSeamKey];
3328
+ if (!seam || typeof seam.mount !== "function") throw new ValidationError(`driverExecutor: missing required seam "${nestedScopeSeamKey}" — a driver child must be spawned through a Scope that seeds it (the keystone scope does)`);
3329
+ return seam;
3330
+ }
3331
+ function isAgent(value) {
3332
+ return typeof value === "object" && value !== null && typeof value.act === "function" && typeof value.name === "string";
3333
+ }
3334
+ //#endregion
3335
+ //#region src/runtime/supervise/budget.ts
3336
+ /**
3337
+ *
3338
+ * The conserved budget reservation pool — the invariant the whole instrument
3339
+ * rests on (critique M5/B3). One root `Budget` becomes a conserved pool of three
3340
+ * quantities (tokens, usd, iterations) plus an absolute deadline. Children reserve
3341
+ * atomically at spawn and reconcile at settle:
3342
+ *
3343
+ * total ≡ free + reserved + committed (invariant, always)
3344
+ *
3345
+ * `reserve` moves a child's whole ceiling from `free` → `reserved` and fails closed
3346
+ * when `free` can't cover it (never read-then-spawn overcommit, so `Σk(treatment) ≡
3347
+ * Σk(blind)` by construction). `reconcile` releases the reservation, commits ACTUAL
3348
+ * spend, and refunds the unspent remainder to `free`. Tokens and usd are separate
3349
+ * channels (`LoopTokenUsage` has no `usd`); iterations are conserved alongside them.
3350
+ *
3351
+ * Pure and deterministic: `now()` is injected, there is no I/O, and no wall-clock or
3352
+ * RNG read. A `reserve`/`reconcile` ticket is single-use (fail-loud on double or
3353
+ * unknown reconcile) so a child can never refund twice.
3354
+ *
3355
+ * @experimental
3356
+ */
3357
+ /** Fold a normalized `UsageEvent` array into a `Spend`. Tokens and usd are separate
3358
+ * channels; iterations come from `'iteration'` events. Pure; `ms` stays zero (the
3359
+ * pool does not read wall-clock). */
3360
+ function spendFromUsageEvents(events) {
3361
+ const tokens = zeroTokenUsage();
3362
+ let usd = 0;
3363
+ let iterations = 0;
3364
+ for (const ev of events) if (ev.kind === "tokens") addTokenUsage(tokens, {
3365
+ input: ev.input,
3366
+ output: ev.output
3367
+ });
3368
+ else if (ev.kind === "cost") usd += ev.usd;
3369
+ else iterations += 1;
3370
+ return {
3371
+ iterations,
3372
+ tokens,
3373
+ usd,
3374
+ ms: 0
3375
+ };
3376
+ }
3377
+ async function foldUsage(events) {
3378
+ if (Array.isArray(events)) return spendFromUsageEvents(events);
3379
+ const tokens = zeroTokenUsage();
3380
+ let usd = 0;
3381
+ let iterations = 0;
3382
+ for await (const ev of events) if (ev.kind === "tokens") addTokenUsage(tokens, {
3383
+ input: ev.input,
3384
+ output: ev.output
3385
+ });
3386
+ else if (ev.kind === "cost") usd += ev.usd;
3387
+ else iterations += 1;
3388
+ return {
3389
+ iterations,
3390
+ tokens,
3391
+ usd,
3392
+ ms: 0
3393
+ };
3394
+ }
3395
+ function totalTokens(usage) {
3396
+ return usage.input + usage.output;
3397
+ }
3398
+ /**
3399
+ * Create a conserved reservation pool from a root `Budget`. `now()` is injected so the
3400
+ * deadline readout is deterministic; defaults to `Date.now` for non-test callers. The
3401
+ * absolute deadline is fixed at construction (`now() + budget.deadlineMs`) so the
3402
+ * readout's `deadlineMs` is a stable wall-clock instant, not a shrinking remainder.
3403
+ */
3404
+ function createBudgetPool(root, now = Date.now) {
3405
+ let freeTokens = root.maxTokens;
3406
+ let reservedTokens = 0;
3407
+ let committedTokens = 0;
3408
+ const usdCapped = root.maxUsd !== void 0;
3409
+ let freeUsd = root.maxUsd ?? 0;
3410
+ let reservedUsd = 0;
3411
+ let committedUsd = 0;
3412
+ let freeIterations = root.maxIterations;
3413
+ let reservedIterations = 0;
3414
+ let committedIterations = 0;
3415
+ const absoluteDeadlineMs = root.deadlineMs !== void 0 ? now() + root.deadlineMs : 0;
3416
+ let nextTicketId = 0;
3417
+ const open = /* @__PURE__ */ new Set();
3418
+ function reserve(b) {
3419
+ const wantTokens = b.maxTokens;
3420
+ const wantUsd = b.maxUsd ?? 0;
3421
+ const wantIterations = b.maxIterations;
3422
+ if (wantTokens > freeTokens) return {
3423
+ ok: false,
3424
+ reason: "budget-exhausted"
3425
+ };
3426
+ if (wantIterations > freeIterations) return {
3427
+ ok: false,
3428
+ reason: "budget-exhausted"
3429
+ };
3430
+ if (wantUsd > 0 && (!usdCapped || wantUsd > freeUsd)) return {
3431
+ ok: false,
3432
+ reason: "budget-exhausted"
3433
+ };
3434
+ freeTokens -= wantTokens;
3435
+ reservedTokens += wantTokens;
3436
+ freeIterations -= wantIterations;
3437
+ reservedIterations += wantIterations;
3438
+ if (wantUsd > 0) {
3439
+ freeUsd -= wantUsd;
3440
+ reservedUsd += wantUsd;
3441
+ }
3442
+ const id = nextTicketId++;
3443
+ open.add(id);
3444
+ return {
3445
+ ok: true,
3446
+ ticket: {
3447
+ id,
3448
+ reserved: {
3449
+ tokens: wantTokens,
3450
+ usd: wantUsd,
3451
+ iterations: wantIterations
3452
+ }
3453
+ }
3454
+ };
3455
+ }
3456
+ function reconcile(ticket, spent) {
3457
+ if (!open.has(ticket.id)) throw new Error(`budget pool: reconcile of unknown or already-settled ticket ${ticket.id}`);
3458
+ open.delete(ticket.id);
3459
+ const { tokens: rTokens, usd: rUsd, iterations: rIterations } = ticket.reserved;
3460
+ if (usdCapped && spent.usdKnown === false) throw new Error(`budget pool: ticket ${ticket.id} reported unknown dollar cost under a dollar-capped budget`);
3461
+ const spentTokens = totalTokens(spent.tokens);
3462
+ if (spentTokens > rTokens) throw new Error(`budget pool: ticket ${ticket.id} spent ${spentTokens} tokens > reserved ${rTokens}`);
3463
+ if (spent.iterations > rIterations) throw new Error(`budget pool: ticket ${ticket.id} spent ${spent.iterations} iterations > reserved ${rIterations}`);
3464
+ if (usdCapped && spent.usd > rUsd) throw new Error(`budget pool: ticket ${ticket.id} spent $${spent.usd} > reserved $${rUsd}`);
3465
+ reservedTokens -= rTokens;
3466
+ committedTokens += spentTokens;
3467
+ freeTokens += rTokens - spentTokens;
3468
+ reservedIterations -= rIterations;
3469
+ committedIterations += spent.iterations;
3470
+ freeIterations += rIterations - spent.iterations;
3471
+ if (usdCapped && rUsd > 0) {
3472
+ reservedUsd -= rUsd;
3473
+ committedUsd += spent.usd;
3474
+ freeUsd += rUsd - spent.usd;
3475
+ } else committedUsd += spent.usd;
3476
+ }
3477
+ function observe(spend) {
3478
+ if (usdCapped && spend.usdKnown === false) throw new Error("budget pool: cannot observe unknown dollar cost under a dollar-capped budget");
3479
+ const tokens = totalTokens(spend.tokens);
3480
+ freeTokens -= tokens;
3481
+ committedTokens += tokens;
3482
+ freeIterations -= spend.iterations;
3483
+ committedIterations += spend.iterations;
3484
+ committedUsd += spend.usd;
3485
+ if (usdCapped) freeUsd -= spend.usd;
3486
+ }
3487
+ function readout() {
3488
+ return {
3489
+ tokensLeft: freeTokens,
3490
+ usdLeft: usdCapped ? freeUsd : 0,
3491
+ usdCapped,
3492
+ deadlineMs: absoluteDeadlineMs,
3493
+ reservedTokens
3494
+ };
3495
+ }
3496
+ function assertNoOpenTickets() {
3497
+ if (open.size > 0) throw new Error(`budget pool: ${open.size} reservation(s) still open at join barrier (leaked ticket ids: ${[...open].join(", ")}) — conserved-pool invariant violated`);
3498
+ }
3499
+ return {
3500
+ reserve,
3501
+ reconcile,
3502
+ spendFrom: foldUsage,
3503
+ readout,
3504
+ observe,
3505
+ assertNoOpenTickets
3506
+ };
3507
+ }
3508
+ //#endregion
3509
+ //#region src/runtime/supervise/supervisor.ts
3510
+ /**
3511
+ *
3512
+ * The `Supervisor` impl (KEYSTONE, build step 5).
3513
+ *
3514
+ * Owns the four things a free-running recursive `act` cannot own itself: the GLOBAL
3515
+ * conserved budget pool, the event-sourced spawn log, the abort cascade over the whole
3516
+ * live tree, and the OTP intensity breaker. `run` builds the root `Scope` over those,
3517
+ * runs the root `Agent.act`, and returns a TYPED `SupervisedResult` — a no-winner is
3518
+ * never coerced into a best-effort `Out`.
3519
+ *
3520
+ * Three lifecycle invariants this impl enforces by construction:
3521
+ * - Join barrier: when `act()` settles (resolve OR reject), every still-live child is
3522
+ * torn down before `run` returns — the generalization of the kernel's
3523
+ * `finally{ Promise.allSettled(destroy) }` barrier (run-loop.ts) from boxes to the
3524
+ * whole sub-tree. A teardown failure is `allSettled`'d and journaled as a
3525
+ * `cancelled` event; it NEVER masks act()'s own outcome. act()'s rejection is the
3526
+ * PRIMARY error (the kernel's firstError precedence), so a teardown throw during the
3527
+ * barrier can never overwrite the real failure.
3528
+ * - Abort cascade: a root abort (caller signal, `RootHandle.abort`, a tripped breaker,
3529
+ * or pool exhaustion) aborts ONE internal controller whose signal is the root scope's
3530
+ * signal. The scope cascades that into every live child's executor abort — which, for
3531
+ * an `acquiring` child, chains into the `acquireSandbox` signal and reaps the
3532
+ * find-by-name orphan box (M1). The supervisor never reaps children directly.
3533
+ * - The supervisor NEVER re-enters a child (m3): the kernel/`acquireSandbox` already
3534
+ * retried at the leaf, and a driver re-spawns through `scope.spawn`. The breaker only
3535
+ * COUNTS `down` settlements within the intensity window and trips to a typed
3536
+ * no-winner; it does not restart anything.
3537
+ *
3538
+ * Selection lives in the driver, not here (selector≠judge): `act` returns the synthesized
3539
+ * winner `Out`. The supervisor content-addresses that `Out` for its replay `outRef`, reads
3540
+ * `spentTotal` off the journal (`settled` child work + `metered` driver inference), and wraps
3541
+ * it as a typed `winner` — it does not re-rank children behind the driver's back.
3542
+ *
3543
+ * @experimental
3544
+ */
3545
+ /** Highest `seq` among events matching `pred`, or `-1` when none match (so a resumed scope's
3546
+ * first new ordinal/seq is 0 — the same start a fresh scope uses). */
3547
+ function maxSeqOf(events, pred) {
3548
+ let max = -1;
3549
+ for (const ev of events) if (pred(ev) && ev.seq > max) max = ev.seq;
3550
+ return max;
3551
+ }
3552
+ /** The default runtime recursion-depth ceiling, paired with the conserved pool so a
3553
+ * runaway recursion hits budget-exhaustion first and depth-exceeded second (R3). */
3554
+ const defaultMaxDepth = 4;
3555
+ /** Create a supervisor that owns one recursive agent execution tree. */
3556
+ function createSupervisor() {
3557
+ let attached;
3558
+ async function run(root, task, opts) {
3559
+ const now = opts.now ?? Date.now;
3560
+ const pool = createBudgetPool(opts.budget, now);
3561
+ const prior = opts.resume === true ? await opts.journal.loadTree(opts.runId) : void 0;
3562
+ const resuming = prior !== void 0 && prior.length > 0;
3563
+ let resumeFrom;
3564
+ if (resuming) resumeFrom = {
3565
+ settled: await replaySpawnTree(opts.journal, opts.blobs, opts.runId),
3566
+ view: materializeTreeView(prior),
3567
+ maxSpawnOrdinal: maxSeqOf(prior, (ev) => ev.kind === "spawned"),
3568
+ maxCursorSeq: maxSeqOf(prior, (ev) => ev.kind === "settled" || ev.kind === "cancelled" || ev.kind === "woken"),
3569
+ maxWaitOrdinal: maxSeqOf(prior, (ev) => ev.kind === "waiting"),
3570
+ waits: pendingWaits(prior)
3571
+ };
3572
+ else {
3573
+ await opts.journal.beginTree(opts.runId, new Date(now()).toISOString());
3574
+ await opts.journal.appendEvent(opts.runId, {
3575
+ kind: "spawned",
3576
+ id: opts.runId,
3577
+ label: "root",
3578
+ budget: opts.budget,
3579
+ runtime: "inline",
3580
+ seq: 0,
3581
+ at: new Date(now()).toISOString()
3582
+ });
3583
+ }
3584
+ const controller = new AbortController();
3585
+ const cascadeAbort = (reason) => {
3586
+ if (controller.signal.aborted) return;
3587
+ controller.abort(reason);
3588
+ };
3589
+ const onCallerAbort = () => cascadeAbort("caller signal aborted");
3590
+ if (opts.signal) if (opts.signal.aborted) cascadeAbort("caller signal aborted");
3591
+ else opts.signal.addEventListener("abort", onCallerAbort, { once: true });
3592
+ const breaker = createIntensityBreaker(opts, () => cascadeAbort("intensity breaker tripped"));
3593
+ const journal = wrapJournalForBreaker(opts.journal, breaker);
3594
+ const scope = createScope({
3595
+ parentId: opts.runId,
3596
+ root: opts.runId,
3597
+ pool,
3598
+ journal,
3599
+ blobs: opts.blobs,
3600
+ executors: opts.executors,
3601
+ seams: {},
3602
+ depth: 0,
3603
+ maxDepth: opts.maxDepth ?? defaultMaxDepth,
3604
+ signal: controller.signal,
3605
+ now,
3606
+ hooks: opts.hooks,
3607
+ ...opts.probes ? { probes: opts.probes } : {},
3608
+ ...resumeFrom ? { resumeFrom } : {}
3609
+ });
3610
+ const openScope = scope;
3611
+ if (attached) attached.bind({
3612
+ scope: openScope,
3613
+ cascadeAbort,
3614
+ signal: pushRootSignal(cascadeAbort)
3615
+ });
3616
+ let actOutcome;
3617
+ try {
3618
+ actOutcome = {
3619
+ ok: true,
3620
+ out: await root.act(task, scope)
3621
+ };
3622
+ } catch (error) {
3623
+ actOutcome = {
3624
+ ok: false,
3625
+ error
3626
+ };
3627
+ } finally {
3628
+ await drainLiveChildren(openScope, controller);
3629
+ if (opts.signal) opts.signal.removeEventListener("abort", onCallerAbort);
3630
+ if (attached) attached.unbind();
3631
+ }
3632
+ const tree = scope.view;
3633
+ if (actOutcome.ok) {
3634
+ pool.assertNoOpenTickets();
3635
+ const out = actOutcome.out;
3636
+ if (out !== void 0) {
3637
+ const outRef = contentAddress(out);
3638
+ await opts.blobs.put(outRef, out);
3639
+ const { childWork, driverInference } = await spentFromJournal(journal, opts.runId);
3640
+ return {
3641
+ kind: "winner",
3642
+ out,
3643
+ outRef,
3644
+ tree,
3645
+ spentTotal: addSpend(childWork, driverInference),
3646
+ ...isNonEmptySpend(driverInference) ? { spentBreakdown: {
3647
+ driverInference,
3648
+ childWork
3649
+ } } : {}
3650
+ };
3651
+ }
3652
+ return noWinner();
3653
+ }
3654
+ return noWinner();
3655
+ async function noWinner() {
3656
+ const { childWork, driverInference } = await spentFromJournal(journal, opts.runId);
3657
+ return {
3658
+ kind: "no-winner",
3659
+ reason: classifyNoWinner(controller, pool, opts, breaker),
3660
+ tree,
3661
+ downCount: breaker.downCount(),
3662
+ spentTotal: addSpend(childWork, driverInference)
3663
+ };
3664
+ }
3665
+ }
3666
+ function attach(h) {
3667
+ const control = rootControls.get(h);
3668
+ if (!control) throw new RuntimeRunStateError("supervisor.attach: handle was not minted by createRootHandle (no control channel)");
3669
+ attached = control;
3670
+ }
3671
+ return {
3672
+ run,
3673
+ attach
3674
+ };
3675
+ }
3676
+ /** Module-private channel from a minted `RootHandle` to its `RootControl`, so `attach`
3677
+ * can prove a handle is ours and reach its binding without leaking the control onto the
3678
+ * frozen `RootHandle` shape. */
3679
+ const rootControls = /* @__PURE__ */ new WeakMap();
3680
+ /** A `RootSignal` sink: `cancel` cascades an abort; pause/resume/ask are observability
3681
+ * signals the substrate accepts but does not act on here (the chat/pi-viz client owns
3682
+ * pause semantics — building them now would be mechanism ahead of the gate). */
3683
+ function pushRootSignal(cascadeAbort) {
3684
+ return (msg) => {
3685
+ if (msg.kind === "cancel") cascadeAbort(msg.reason ?? "root signal: cancel");
3686
+ };
3687
+ }
3688
+ function createIntensityBreaker(opts, trip) {
3689
+ const max = opts.maxRestarts;
3690
+ const within = opts.withinMs;
3691
+ const armed = max !== void 0 && within !== void 0;
3692
+ const recent = [];
3693
+ let total = 0;
3694
+ let isTripped = false;
3695
+ return {
3696
+ recordDown(at) {
3697
+ total += 1;
3698
+ if (!armed || isTripped) return;
3699
+ recent.push(at);
3700
+ const cutoff = at - within;
3701
+ while (recent.length > 0 && recent[0] < cutoff) recent.shift();
3702
+ if (recent.length > max) {
3703
+ isTripped = true;
3704
+ trip();
3705
+ }
3706
+ },
3707
+ tripped() {
3708
+ return isTripped;
3709
+ },
3710
+ downCount() {
3711
+ return total;
3712
+ }
3713
+ };
3714
+ }
3715
+ /** Decorate the journal so the breaker observes every `settled`-`down` event the scope
3716
+ * appends, without the supervisor intercepting `scope.next()`. The decorator is
3717
+ * transparent — it forwards every method verbatim and only reads the down events. */
3718
+ function wrapJournalForBreaker(journal, breaker) {
3719
+ return {
3720
+ loadTree: (root) => journal.loadTree(root),
3721
+ beginTree: (root, at) => journal.beginTree(root, at),
3722
+ appendEvent: (root, ev) => {
3723
+ if (ev.kind === "settled" && ev.status === "down") breaker.recordDown(Date.parse(ev.at));
3724
+ return journal.appendEvent(root, ev);
3725
+ }
3726
+ };
3727
+ }
3728
+ /**
3729
+ * Drain the root scope's live set so every still-running/acquiring child is torn down
3730
+ * before `run` returns — the join barrier. Abort the cascade controller first (so each
3731
+ * child's executor stops cleanly), then pull `next()` to completion so every aborted
3732
+ * child's teardown + reconcile runs and its `settled` event is journaled by the scope.
3733
+ * A child's own teardown failure is already swallowed inside `runChild`, and the cursor
3734
+ * itself never rejects (a failing child is typed into a `down`), so the whole barrier is
3735
+ * `allSettled`'d — a stray throw here is NOT the primary error (firstError precedence).
3736
+ */
3737
+ async function drainLiveChildren(scope, controller) {
3738
+ const view = scope.view;
3739
+ if (!(view.inFlight > 0 || view.waiting > 0)) return;
3740
+ if (!controller.signal.aborted) controller.abort();
3741
+ await Promise.allSettled([drainCursor(scope)]);
3742
+ }
3743
+ async function drainCursor(scope) {
3744
+ for (;;) if (await scope.next() === null) return;
3745
+ }
3746
+ function classifyNoWinner(controller, pool, opts, breaker) {
3747
+ if (breaker.tripped()) return "all-children-down";
3748
+ if (controller.signal.aborted) return "aborted";
3749
+ if (poolExhausted(pool, opts)) return "budget-exhausted";
3750
+ return "all-children-down";
3751
+ }
3752
+ function poolExhausted(pool, opts) {
3753
+ const r = pool.readout();
3754
+ if (r.tokensLeft <= 0) return true;
3755
+ if (opts.budget.maxUsd !== void 0 && r.usdLeft <= 0) return true;
3756
+ if (opts.budget.deadlineMs !== void 0 && r.deadlineMs > 0 && (opts.now ?? Date.now)() >= r.deadlineMs) return true;
3757
+ return false;
3758
+ }
3759
+ /**
3760
+ * Sum the conserved spend over every journaled `settled` event — the honest per-channel
3761
+ * total (input/output/usd/iterations all preserved), read off the same evidence replay
3762
+ * reads. Computed AFTER the join barrier so every child's settlement is recorded. Fails
3763
+ * loud if the tree was never journaled (the supervisor always `beginTree`s, so a missing
3764
+ * tree is a corrupted journal, not a normal path).
3765
+ */
3766
+ async function spentFromJournal(journal, root) {
3767
+ const events = await journal.loadTree(root);
3768
+ if (events === void 0) throw new RuntimeRunStateError(`supervisor: spawn tree '${root}' is missing from the journal after run (corrupted log)`);
3769
+ const childWork = {
3770
+ iterations: 0,
3771
+ tokens: {
3772
+ input: 0,
3773
+ output: 0
3774
+ },
3775
+ usd: 0,
3776
+ ms: 0
3777
+ };
3778
+ const driverInference = {
3779
+ iterations: 0,
3780
+ tokens: {
3781
+ input: 0,
3782
+ output: 0
3783
+ },
3784
+ usd: 0,
3785
+ ms: 0
3786
+ };
3787
+ for (const ev of events) if (ev.kind === "settled") accumulate(childWork, ev.spent);
3788
+ else if (ev.kind === "metered") accumulate(driverInference, ev.spend);
3789
+ return {
3790
+ childWork,
3791
+ driverInference
3792
+ };
3793
+ }
3794
+ /** Add `b` into `a` in place, per channel. */
3795
+ function accumulate(a, b) {
3796
+ a.iterations += b.iterations;
3797
+ a.tokens.input += b.tokens.input;
3798
+ a.tokens.output += b.tokens.output;
3799
+ a.usd += b.usd;
3800
+ if (b.usdKnown === false) a.usdKnown = false;
3801
+ a.ms += b.ms;
3802
+ }
3803
+ /** Sum two conserved-spend tallies per channel — the child-work journal sum + the drivers' own
3804
+ * metered inference, so `spentTotal` is the true cost of the run. */
3805
+ function addSpend(a, b) {
3806
+ return {
3807
+ iterations: a.iterations + b.iterations,
3808
+ tokens: {
3809
+ input: a.tokens.input + b.tokens.input,
3810
+ output: a.tokens.output + b.tokens.output
3811
+ },
3812
+ usd: a.usd + b.usd,
3813
+ ...a.usdKnown === false || b.usdKnown === false ? { usdKnown: false } : {},
3814
+ ms: a.ms + b.ms
3815
+ };
3816
+ }
3817
+ /** True when any driver metered inference this run (so the winner carries a `spentBreakdown`).
3818
+ * Checks every channel `addSpend` sums — including `ms` — so the gate stays consistent with the
3819
+ * total even though the coordination driver currently stamps `ms: 0`. */
3820
+ function isNonEmptySpend(s) {
3821
+ return s.iterations > 0 || s.tokens.input > 0 || s.tokens.output > 0 || s.usd > 0 || s.ms > 0;
3822
+ }
3823
+ //#endregion
3824
+ export { removeWorktree as A, routerToolLoop as C, runWorktreeHarness as D, runWorktreeChecks as E, parseCodexTokenUsage as M, runLocalHarness as N, captureWorktreeDiff as O, CodexExecutionDiagnosticError as P, routerChatWithUsage as S, runSettledCommand as T, defineRuntimeHooks as _, createScope as a, routerBrain as b, isWaitOutcome as c, validateWaitSpec as d, waitUntil as f, composeRuntimeHooks as g, readWorkerProgress as h, withDriverExecutor as i, harnessInvocation as j, createWorktree as k, pollFor as l, createActivityLog as m, createBudgetPool as n, settledToIteration as o, DEFAULT_STALL_AFTER_MS as p, spendFromUsageEvents as r, createWaitProbes as s, createSupervisor as t, timerAt as u, notifyRuntimeDecisionPoint as v, runBrainLoop as w, routerChatWithTools as x, notifyRuntimeHookEvent as y };
3825
+
3826
+ //# sourceMappingURL=supervisor-DTKhF-RV.js.map