@wrongstack/core 0.310.1 → 0.313.1

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 (187) hide show
  1. package/dist/chronicle/index.js +84 -6391
  2. package/dist/chronicle/project-server.js +84 -4868
  3. package/dist/chunk-2BUUIMQB.js +225 -0
  4. package/dist/chunk-2FK4WTFF.js +207 -0
  5. package/dist/chunk-2FQL4PDC.js +292 -0
  6. package/dist/chunk-2KKODIQU.js +2256 -0
  7. package/dist/chunk-2YU4WVBW.js +134 -0
  8. package/dist/chunk-3E244UDA.js +356 -0
  9. package/dist/chunk-3XPVB7BA.js +135 -0
  10. package/dist/chunk-4LJJYBAU.js +388 -0
  11. package/dist/chunk-4VWO3ZXL.js +342 -0
  12. package/dist/chunk-4XV6IR37.js +187 -0
  13. package/dist/chunk-4ZHTKJHM.js +183 -0
  14. package/dist/chunk-52RTVH4M.js +361 -0
  15. package/dist/chunk-57GEWK5X.js +26 -0
  16. package/dist/chunk-5DSMEVA4.js +931 -0
  17. package/dist/chunk-5RP3QTRL.js +1223 -0
  18. package/dist/chunk-5S437PRC.js +50 -0
  19. package/dist/chunk-5XDUGTKN.js +511 -0
  20. package/dist/chunk-62V2AFVG.js +422 -0
  21. package/dist/chunk-65RDIOR2.js +2470 -0
  22. package/dist/chunk-6AKOTGLM.js +597 -0
  23. package/dist/chunk-7ZHZE5EO.js +28 -0
  24. package/dist/chunk-A455ATZB.js +140 -0
  25. package/dist/chunk-AD7ZFZCS.js +46 -0
  26. package/dist/chunk-AI4LLQ2U.js +856 -0
  27. package/dist/chunk-ASX6AW36.js +789 -0
  28. package/dist/chunk-AT4V4XGI.js +3522 -0
  29. package/dist/chunk-AWNQ6ED6.js +311 -0
  30. package/dist/chunk-B4AMLZPB.js +9 -0
  31. package/dist/chunk-BAKVJLEV.js +417 -0
  32. package/dist/chunk-BJHVULHO.js +55 -0
  33. package/dist/chunk-CB2HFKTP.js +9 -0
  34. package/dist/chunk-CE4ZOCOI.js +1640 -0
  35. package/dist/chunk-CQQL4ZOQ.js +151 -0
  36. package/dist/chunk-DEIQMBD2.js +65 -0
  37. package/dist/chunk-E6I5RXVK.js +58 -0
  38. package/dist/chunk-EFEHDQGY.js +1589 -0
  39. package/dist/chunk-EODDVJBM.js +82 -0
  40. package/dist/chunk-EPC3GZD4.js +308 -0
  41. package/dist/chunk-FAYRX2AD.js +13 -0
  42. package/dist/chunk-FFSX2QDQ.js +362 -0
  43. package/dist/chunk-FHAMQS7Q.js +5856 -0
  44. package/dist/chunk-FIV4IJL7.js +2263 -0
  45. package/dist/chunk-FNXVT7HF.js +52 -0
  46. package/dist/chunk-FSWCWSTS.js +395 -0
  47. package/dist/chunk-FW5LDDMJ.js +16464 -0
  48. package/dist/chunk-FXQLIX3K.js +211 -0
  49. package/dist/chunk-G5AAKEEZ.js +228 -0
  50. package/dist/chunk-G5S3S5ZN.js +38 -0
  51. package/dist/chunk-GAZEWQEZ.js +231 -0
  52. package/dist/chunk-GFJOMSHG.js +125 -0
  53. package/dist/chunk-GFXZTHNI.js +158 -0
  54. package/dist/chunk-GM54GJBU.js +960 -0
  55. package/dist/chunk-GPQ3NJPR.js +900 -0
  56. package/dist/chunk-GTCU6DZ3.js +1 -0
  57. package/dist/chunk-H5E77KUJ.js +15 -0
  58. package/dist/chunk-H72H4XDU.js +1023 -0
  59. package/dist/chunk-HA4ZYA6T.js +178 -0
  60. package/dist/chunk-I45KAW2N.js +5020 -0
  61. package/dist/chunk-IQ35HMQL.js +1396 -0
  62. package/dist/chunk-IYFBFX34.js +86 -0
  63. package/dist/chunk-JJPMWPB6.js +109 -0
  64. package/dist/chunk-JTHKOYWU.js +663 -0
  65. package/dist/chunk-JVGPR3TS.js +365 -0
  66. package/dist/chunk-JVKB2TBL.js +48 -0
  67. package/dist/chunk-JX7VBZDK.js +31 -0
  68. package/dist/chunk-K3G6JBUC.js +5232 -0
  69. package/dist/chunk-K4YUNKU7.js +589 -0
  70. package/dist/chunk-KBZGE6LN.js +710 -0
  71. package/dist/chunk-KDQJCT2J.js +45 -0
  72. package/dist/chunk-KI4L2DUL.js +560 -0
  73. package/dist/chunk-KJ2ZTQSL.js +47 -0
  74. package/dist/chunk-KSJ5UTP7.js +154 -0
  75. package/dist/chunk-LHOA5DWA.js +28 -0
  76. package/dist/chunk-LX5QWLBL.js +27 -0
  77. package/dist/chunk-LZDDLNCZ.js +279 -0
  78. package/dist/chunk-M2WF6AXG.js +115 -0
  79. package/dist/chunk-M7BS2K6U.js +1 -0
  80. package/dist/chunk-NDBIPFNY.js +102 -0
  81. package/dist/chunk-NEMIWEXC.js +1199 -0
  82. package/dist/chunk-NFAYHPRH.js +323 -0
  83. package/dist/chunk-O37BQ65S.js +21 -0
  84. package/dist/chunk-O74MJHXU.js +187 -0
  85. package/dist/chunk-OJZEX73F.js +112 -0
  86. package/dist/chunk-PAAWLLZX.js +74 -0
  87. package/dist/chunk-PFXFQQRT.js +339 -0
  88. package/dist/chunk-PFZO3C6H.js +14 -0
  89. package/dist/chunk-PHH7LD45.js +113 -0
  90. package/dist/chunk-QRTG4XTI.js +188 -0
  91. package/dist/chunk-QVOVMXJR.js +1351 -0
  92. package/dist/chunk-RCQYT2I4.js +757 -0
  93. package/dist/chunk-RD6DEE36.js +1178 -0
  94. package/dist/chunk-RJIZEJ42.js +30 -0
  95. package/dist/chunk-RPOA7IAE.js +150 -0
  96. package/dist/chunk-SN2DQRL6.js +1172 -0
  97. package/dist/chunk-SQJVFEBV.js +41 -0
  98. package/dist/chunk-SQP64RCS.js +5391 -0
  99. package/dist/chunk-SVT4RNTR.js +49 -0
  100. package/dist/chunk-SWUFN7XM.js +43 -0
  101. package/dist/chunk-SXMVYC5H.js +174 -0
  102. package/dist/chunk-SZ6AYUIZ.js +23 -0
  103. package/dist/chunk-T3KGZBU5.js +3171 -0
  104. package/dist/chunk-TBRL2RCC.js +9 -0
  105. package/dist/chunk-TRT2HRWZ.js +18 -0
  106. package/dist/chunk-TYW2GI5I.js +1077 -0
  107. package/dist/chunk-UIWONWVG.js +4959 -0
  108. package/dist/chunk-VB3H5WWB.js +151 -0
  109. package/dist/chunk-W4A6QA3D.js +258 -0
  110. package/dist/chunk-WBPVKEEE.js +227 -0
  111. package/dist/chunk-WEBMSQP2.js +123 -0
  112. package/dist/chunk-WOEZV3Q4.js +333 -0
  113. package/dist/chunk-WQQON3SV.js +473 -0
  114. package/dist/chunk-WQTOVMGV.js +119 -0
  115. package/dist/chunk-WZO23AWO.js +2779 -0
  116. package/dist/chunk-XBDP4QPW.js +7 -0
  117. package/dist/chunk-XEBHSJLT.js +103 -0
  118. package/dist/chunk-XQ2ECNK3.js +1 -0
  119. package/dist/chunk-XQ2XGIBF.js +2086 -0
  120. package/dist/chunk-Y2GE5TSI.js +355 -0
  121. package/dist/chunk-YBUJCZ57.js +351 -0
  122. package/dist/chunk-YNCVSFGF.js +149 -0
  123. package/dist/chunk-YUPMXOGP.js +282 -0
  124. package/dist/chunk-YY6APYUM.js +49 -0
  125. package/dist/chunk-Z2IJUO45.js +691 -0
  126. package/dist/chunk-ZEHUCJHY.js +129 -0
  127. package/dist/chunk-ZYU2YOJ6.js +1496 -0
  128. package/dist/chunk-ZZ4V3WIZ.js +163 -0
  129. package/dist/coordination/agents/index.js +145 -6260
  130. package/dist/coordination/index.js +365 -34323
  131. package/dist/coordination/mailbox-project-server.js +48 -3220
  132. package/dist/coordination/mailbox-types.d.ts +13 -1
  133. package/dist/coordination/provider-status-tracker.d.ts +4 -0
  134. package/dist/coordination/sqlite-mailbox.d.ts +2 -1
  135. package/dist/core/agent.d.ts +14 -1
  136. package/dist/core/index.js +267 -12479
  137. package/dist/core/model-ref.js +5 -23
  138. package/dist/core/provider-runner.d.ts +36 -1
  139. package/dist/design/index.js +50 -1087
  140. package/dist/execution/index.d.ts +1 -1
  141. package/dist/execution/index.js +1599 -21887
  142. package/dist/execution/prompt-enhancer.js +16 -535
  143. package/dist/execution/retry-policy.d.ts +31 -1
  144. package/dist/extension/index.js +7 -311
  145. package/dist/goal/index.js +33 -3435
  146. package/dist/hooks/index.js +14 -1138
  147. package/dist/hq/index.js +112 -5139
  148. package/dist/hq/protocol.js +21 -668
  149. package/dist/index.js +1410 -92655
  150. package/dist/infrastructure/index.js +64 -4811
  151. package/dist/kernel/events/provider-events.d.ts +15 -0
  152. package/dist/kernel/events/wrongtrace-events.d.ts +26 -0
  153. package/dist/kernel/events.d.ts +16 -7
  154. package/dist/kernel/index.js +14 -992
  155. package/dist/models/index.js +40 -1117
  156. package/dist/notifications/index.js +3 -108
  157. package/dist/observability/index.js +17 -279
  158. package/dist/plugin/index.js +584 -14835
  159. package/dist/prompts/index.js +14 -948
  160. package/dist/registry/index.js +9 -822
  161. package/dist/registry-R23UBE7H.js +14 -0
  162. package/dist/replay/index.js +6 -84
  163. package/dist/review-finding-store-DKAV3AS6.js +21 -0
  164. package/dist/review-report-store-OWY3EC3N.js +19 -0
  165. package/dist/security/index.js +114 -2202
  166. package/dist/session-catalog/index.js +36 -2103
  167. package/dist/session-catalog/project-server.js +42 -1435
  168. package/dist/skills/index.js +35 -1578
  169. package/dist/storage/index.js +182 -15600
  170. package/dist/tasking/index.js +10 -676
  171. package/dist/tools/index.js +52 -9589
  172. package/dist/types/config/autonomy.d.ts +5 -0
  173. package/dist/types/config/tools.d.ts +2 -2
  174. package/dist/types/config/ui.d.ts +1 -1
  175. package/dist/types/errors.d.ts +18 -0
  176. package/dist/types/index.js +121 -1341
  177. package/dist/types/provider-runner.d.ts +8 -0
  178. package/dist/types/session-markers.js +8 -144
  179. package/dist/utils/error.js +3 -4
  180. package/dist/utils/expect-defined.js +3 -9
  181. package/dist/utils/heap-watchdog.js +6 -706
  182. package/dist/utils/index.js +269 -5685
  183. package/dist/utils/sage-output-block.js +5 -42
  184. package/dist/wiring/proxy-rewrite.d.ts +92 -4
  185. package/dist/wiring/proxy-rewrite.js +20 -61
  186. package/dist/worktree/index.js +6 -887
  187. package/package.json +4 -4
@@ -0,0 +1,3171 @@
1
+ import {
2
+ AGENT_CATALOG,
3
+ ALL_AGENT_DEFINITIONS,
4
+ ROLE_SKILL_SETS,
5
+ SHADOW_AGENT_SKILLS,
6
+ TOOLS,
7
+ agentPrompt
8
+ } from "./chunk-FHAMQS7Q.js";
9
+ import {
10
+ setBtwNote
11
+ } from "./chunk-SQJVFEBV.js";
12
+ import {
13
+ ToolCapabilities
14
+ } from "./chunk-A455ATZB.js";
15
+ import {
16
+ SECRET_FILE_MODE
17
+ } from "./chunk-H5E77KUJ.js";
18
+ import {
19
+ ProviderError
20
+ } from "./chunk-NFAYHPRH.js";
21
+ import {
22
+ safeParse
23
+ } from "./chunk-RPOA7IAE.js";
24
+ import {
25
+ readBundledInstructionText,
26
+ renderInstructionTemplate
27
+ } from "./chunk-5S437PRC.js";
28
+ import {
29
+ AgentError,
30
+ ERROR_CODES
31
+ } from "./chunk-WOEZV3Q4.js";
32
+ import {
33
+ toErrorMessage
34
+ } from "./chunk-TBRL2RCC.js";
35
+
36
+ // src/coordination/subagent-finish.ts
37
+ var SUBAGENT_FINISH_REQUESTED_EVENT = "subagent.finish_requested";
38
+ var DEFAULT_SUBAGENT_FINISH_GRACE_MS = 12e4;
39
+ function resolveGracefulFinish(config) {
40
+ const raw = config.gracefulFinish;
41
+ if (raw === void 0 || raw === false) return void 0;
42
+ if (raw === true) return { graceMs: DEFAULT_SUBAGENT_FINISH_GRACE_MS };
43
+ const graceMs = typeof raw.graceMs === "number" && Number.isFinite(raw.graceMs) && raw.graceMs > 0 ? Math.floor(raw.graceMs) : DEFAULT_SUBAGENT_FINISH_GRACE_MS;
44
+ return { graceMs };
45
+ }
46
+ function buildSubagentFinishNotice(input) {
47
+ const localTime = new Date(input.deadlineMs).toISOString();
48
+ const seconds = Math.max(1, Math.round(input.graceMs / 1e3));
49
+ const timeLeft = input.graceMs > 0 ? `You have roughly ${seconds} seconds (until ${localTime}) of legitimate working time left.` : `Your working-time window is already spent (deadline was ${localTime}) \u2014 finish now.`;
50
+ return [
51
+ "[SUBAGENT FINISH] The leader agent has finished its work.",
52
+ `Reason: ${input.reason}`,
53
+ timeLeft,
54
+ "Finish your task now, in this turn: complete the thought you are working on, stop",
55
+ "starting new tool calls unless one is strictly required to finish, and write your",
56
+ "final answer or report as your final output, then end your turn.",
57
+ "Do not restart the task and do not begin new work."
58
+ ].join("\n");
59
+ }
60
+
61
+ // src/coordination/subagent-budget.ts
62
+ var TIMEOUT_PREEMPT_FRACTION = 0.85;
63
+ var DECISION_TIMEOUT_MS = 6e4;
64
+ var BudgetExceededError = class extends Error {
65
+ kind;
66
+ limit;
67
+ observed;
68
+ constructor(kind, limit, observed) {
69
+ super(`Budget exceeded: ${kind} (limit=${limit}, observed=${observed})`);
70
+ this.name = "BudgetExceededError";
71
+ this.kind = kind;
72
+ this.limit = limit;
73
+ this.observed = observed;
74
+ }
75
+ };
76
+ var BudgetThresholdSignal = class extends Error {
77
+ kind;
78
+ limit;
79
+ used;
80
+ /** Resolves to 'extend' (with optional new limits) or 'stop' */
81
+ decision;
82
+ constructor(kind, limit, used, decision) {
83
+ super(`Budget soft limit: ${kind} (limit=${limit}, used=${used})`);
84
+ this.name = "BudgetThresholdSignal";
85
+ this.kind = kind;
86
+ this.limit = limit;
87
+ this.used = used;
88
+ this.decision = decision;
89
+ }
90
+ };
91
+ var SubagentBudget = class _SubagentBudget {
92
+ limits;
93
+ /** Patch one or more budget limits in-place after construction.
94
+ * Used by the coordinator watchdog when granting an extension.
95
+ * All fields are optional — only provided fields are updated.
96
+ * This is the single write path for limit mutations so that future
97
+ * validation or side-effects live in one place (M1). */
98
+ patchLimits(ext) {
99
+ if (ext.maxIterations !== void 0) {
100
+ this.limits.maxIterations = ext.maxIterations;
101
+ }
102
+ if (ext.maxToolCalls !== void 0) {
103
+ this.limits.maxToolCalls = ext.maxToolCalls;
104
+ }
105
+ if (ext.maxTokens !== void 0) {
106
+ this.limits.maxTokens = ext.maxTokens;
107
+ }
108
+ if (ext.maxCostUsd !== void 0) {
109
+ this.limits.maxCostUsd = ext.maxCostUsd;
110
+ }
111
+ if (ext.timeoutMs !== void 0) {
112
+ this.limits.timeoutMs = ext.timeoutMs;
113
+ }
114
+ if (ext.idleTimeoutMs !== void 0) {
115
+ this.limits.idleTimeoutMs = ext.idleTimeoutMs;
116
+ }
117
+ }
118
+ /**
119
+ * Graceful-finish state (see coordination/subagent-finish.ts).
120
+ * `_finishNotified` guards the single in-band emission; `_grace` records a
121
+ * granted working-time extension past the original wall-clock deadline.
122
+ * They are separate because the two callers want different semantics:
123
+ * the watchdog grants grace at the deadline crossing (notify + extend),
124
+ * while an explicit leader-finished request only notifies — a subagent
125
+ * well inside its budget keeps its full legitimate working time and simply
126
+ * accelerates.
127
+ */
128
+ _finishNotified = false;
129
+ _grace = null;
130
+ /** True once the in-band finish notification has been emitted. */
131
+ get finishNotified() {
132
+ return this._finishNotified;
133
+ }
134
+ /** True once a grace window has been granted past the original deadline. */
135
+ get graceGranted() {
136
+ return this._grace !== null;
137
+ }
138
+ /**
139
+ * Notify the subagent in-band to finish its task in its own turn:
140
+ * `subagent.finish_requested` is emitted on the wired EventBus and the
141
+ * agent loop folds the notice into the conversation between tool batches.
142
+ * Nothing aborts — this is a notification, never an interrupt.
143
+ *
144
+ * `opts.graceMs` additionally extends the wall-clock ceiling by that window
145
+ * (used by the watchdog at a deadline crossing, so the model gets working
146
+ * time instead of a kill). Omit it to notify without touching the budget —
147
+ * the subagent keeps its existing time budget and just accelerates.
148
+ *
149
+ * Returns `true` when this call did something (emitted the notification
150
+ * and/or granted grace); `false` when there was nothing to do (already
151
+ * notified, grace already granted, no EventBus wired, budget not started).
152
+ */
153
+ notifyFinish(reason, opts, now = Date.now) {
154
+ if (!this._events) return false;
155
+ if (this.startTime === null) return false;
156
+ const shouldEmit = !this._finishNotified;
157
+ const rawGrace = opts?.graceMs;
158
+ const shouldGrant = rawGrace !== void 0 && this._grace === null;
159
+ if (!shouldEmit && !shouldGrant) return false;
160
+ let grantedGraceMs = 0;
161
+ let graceDeadlineMs;
162
+ if (shouldGrant && rawGrace !== void 0) {
163
+ grantedGraceMs = Number.isFinite(rawGrace) && rawGrace > 0 ? Math.floor(rawGrace) : DEFAULT_SUBAGENT_FINISH_GRACE_MS;
164
+ graceDeadlineMs = now() + grantedGraceMs;
165
+ this._grace = { deadlineMs: graceDeadlineMs, graceMs: grantedGraceMs };
166
+ this.patchLimits({ timeoutMs: graceDeadlineMs - this.startTime });
167
+ }
168
+ if (shouldEmit) {
169
+ this._finishNotified = true;
170
+ const effectiveDeadlineMs = graceDeadlineMs ?? (this.limits.timeoutMs !== void 0 ? this.startTime + this.limits.timeoutMs : now() + DEFAULT_SUBAGENT_FINISH_GRACE_MS);
171
+ const effectiveGraceMs = Math.max(0, effectiveDeadlineMs - now());
172
+ const subagentId = this._subagentId;
173
+ this._events.emit(SUBAGENT_FINISH_REQUESTED_EVENT, {
174
+ // Omitted entirely when the budget was built without an id — an
175
+ // empty string is an address that matches nothing.
176
+ ...subagentId !== void 0 ? { subagentId } : {},
177
+ reason,
178
+ deadlineMs: effectiveDeadlineMs,
179
+ graceMs: effectiveGraceMs,
180
+ notice: buildSubagentFinishNotice({
181
+ reason,
182
+ deadlineMs: effectiveDeadlineMs,
183
+ graceMs: effectiveGraceMs
184
+ })
185
+ });
186
+ }
187
+ return true;
188
+ }
189
+ /** Epoch ms by which the subagent should have produced its final output,
190
+ * once a grace window was granted. Undefined before that. */
191
+ get finishDeadlineMs() {
192
+ return this._grace?.deadlineMs;
193
+ }
194
+ iterations = 0;
195
+ toolCalls = 0;
196
+ tokenInput = 0;
197
+ tokenOutput = 0;
198
+ costUsd = 0;
199
+ startTime = null;
200
+ /**
201
+ * Timestamp of the most recent activity (iteration / tool call / token
202
+ * usage / streamed progress). Drives the idle timeout — reset by
203
+ * `markActivity()`. Initialised to `start()` time so a never-active agent
204
+ * still eventually trips its idle window.
205
+ */
206
+ lastActivityTime = null;
207
+ _onThreshold;
208
+ _sessionId;
209
+ /** Owning subagent id — used to address the graceful-finish event. */
210
+ _subagentId;
211
+ /** True when only the coordinator watchdog may enforce wall-clock limits. */
212
+ _wallClockWatchdogOwned;
213
+ /**
214
+ * Hard cap on how long `_negotiateExtension` waits for the coordinator to
215
+ * respond before defaulting to 'stop'. Without this fallback an absent
216
+ * or hung listener (Director not built / event filter detached mid-run)
217
+ * leaves the budget over-limit and never enforces anything.
218
+ */
219
+ static DECISION_TIMEOUT_MS = DECISION_TIMEOUT_MS;
220
+ /**
221
+ * Injected by the runner when wiring the budget to its EventBus.
222
+ * Used to emit `budget.threshold_reached` events in `'auto'` mode.
223
+ */
224
+ _events;
225
+ /**
226
+ * Guard against dual-path races between the coordinator watchdog
227
+ * (`executeWithTimeout`) and the budget's own `checkTimeout()`.
228
+ * Both paths detect `elapsed >= timeoutMs` and can emit
229
+ * `budget.threshold_reached` for kind `'timeout'` simultaneously.
230
+ * Set to the current `timeoutMs` ceiling by the coordinator BEFORE
231
+ * calling `onThreshold`, and cleared after the negotiation resolves.
232
+ * `checkTimeout()` skips its wall-clock check while this is set so
233
+ * the coordinator's watchdog is the sole source of wall-clock timeout
234
+ * events — `checkTimeout()` focuses exclusively on `idle_timeout`.
235
+ */
236
+ _watchdogActive;
237
+ /** Returns the timeout ceiling currently being negotiated by the watchdog,
238
+ * or `undefined` when no wall-clock negotiation is in flight.
239
+ * Used by `executeWithTimeout` to detect a stale lock (M3). */
240
+ get watchdogActive() {
241
+ return this._watchdogActive;
242
+ }
243
+ /** Called by the coordinator watchdog BEFORE calling `onThreshold` so that
244
+ * `checkTimeout()` skips its wall-clock check for this ceiling. Prevents
245
+ * the budget's own `checkTimeout()` from emitting a second
246
+ * `budget.threshold_reached` event while the watchdog is already
247
+ * negotiating the same wall-clock deadline (C1). */
248
+ setWatchdogNegotiation(timeoutMs) {
249
+ this._watchdogActive = timeoutMs;
250
+ }
251
+ /** Clears the watchdog guard after negotiation resolves. Called in the
252
+ * `finally` block of both the pre-empt and deadline branches so it fires
253
+ * on every exit path: grant, deny, throw, or error. */
254
+ clearWatchdogNegotiation() {
255
+ this._watchdogActive = void 0;
256
+ }
257
+ /**
258
+ * Negotiation mode — controls whether a threshold hit tries to emit
259
+ * `budget.threshold_reached` and wait for a coordinator decision, or
260
+ * falls straight through to a synchronous hard stop.
261
+ *
262
+ * `'auto'` (default) — emit on the EventBus and wait; times out to 'stop'.
263
+ * `'sync'` — throw `BudgetExceededError` immediately regardless of listeners.
264
+ */
265
+ _mode;
266
+ /**
267
+ * Optional callback for soft-limit handling. When set, the budget will
268
+ * invoke it rather than throw immediately. The handler decides whether to
269
+ * throw synchronously, continue, or ask the coordinator for an extension.
270
+ */
271
+ get onThreshold() {
272
+ return this._onThreshold;
273
+ }
274
+ set onThreshold(fn) {
275
+ this._onThreshold = fn;
276
+ }
277
+ /** Returns the current negotiation mode. */
278
+ get mode() {
279
+ return this._mode;
280
+ }
281
+ constructor(limits = {}, mode = "auto", options = {}) {
282
+ this._mode = mode;
283
+ this._sessionId = options.sessionId;
284
+ this._subagentId = options.subagentId;
285
+ this._wallClockWatchdogOwned = options.wallClockWatchdogOwned === true;
286
+ this.limits = { ...limits };
287
+ }
288
+ currentSessionId() {
289
+ const value = typeof this._sessionId === "function" ? this._sessionId() : this._sessionId;
290
+ return typeof value === "string" && value.length > 0 ? value : void 0;
291
+ }
292
+ start() {
293
+ this.startTime = Date.now();
294
+ this.lastActivityTime = this.startTime;
295
+ }
296
+ /**
297
+ * Reset the idle clock. Called on any sign of forward progress —
298
+ * iterations, tool calls, token usage, and streamed tool/text progress —
299
+ * so a long-but-productive subagent never trips its `idleTimeoutMs`.
300
+ */
301
+ markActivity() {
302
+ this.lastActivityTime = Date.now();
303
+ }
304
+ /**
305
+ * Milliseconds since the last activity signal. Returns 0 before `start()`
306
+ * (nothing to measure yet). Used by the coordinator watchdog to decide
307
+ * whether to re-arm (still active) or reap (genuinely idle).
308
+ */
309
+ idleMs() {
310
+ const since = this.lastActivityTime ?? this.startTime;
311
+ return since === null ? 0 : Date.now() - since;
312
+ }
313
+ /** Returns true if we're within 10% of any limit — useful for pre-flight checks. */
314
+ isNearLimit() {
315
+ const { maxIterations, maxToolCalls, maxTokens, maxCostUsd } = this.limits;
316
+ if (maxIterations && this.iterations >= maxIterations * 0.9) return true;
317
+ if (maxToolCalls && this.toolCalls >= maxToolCalls * 0.9) return true;
318
+ if (maxTokens && this.tokenInput + this.tokenOutput >= maxTokens * 0.9) return true;
319
+ if (maxCostUsd && this.costUsd >= maxCostUsd * 0.9) return true;
320
+ return false;
321
+ }
322
+ /**
323
+ * Synchronous budget check. Always throws synchronously so callers (especially
324
+ * test event handlers using `expect().toThrow()`) get an unhandled rejection
325
+ * when the budget is exceeded without a handler.
326
+ *
327
+ * Decision table:
328
+ * - no `onThreshold` handler → throw `BudgetExceededError` (hard stop, always)
329
+ * - `mode === 'sync'` → throw `BudgetExceededError` (hard stop, always)
330
+ * - `mode === 'auto'` + no listener → throw `BudgetExceededError` (hard stop; no one to ask)
331
+ * - `mode === 'auto'` + listener → throw `BudgetThresholdSignal` with async decision promise
332
+ */
333
+ /**
334
+ * Collects all exceeded budget kinds into a single NOOP-free negotiation.
335
+ * Called by recordIteration / recordToolCall / recordUsage — each may call
336
+ * this for its own kind. The first call starts the negotiation and stores
337
+ * the Promise in _pendingNegotiation. Subsequent calls for DIFFERENT
338
+ * kinds (while a negotiation is in flight) are NOOPs — they don't start
339
+ * new conversations with the coordinator. This prevents an EventBus flood
340
+ * when multiple budget kinds are exceeded simultaneously in one iteration.
341
+ *
342
+ * Returns the kinds that were found to be exceeded (for logging/debugging).
343
+ */
344
+ checkLimits(elapsedMs) {
345
+ const exceeded = [];
346
+ if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
347
+ exceeded.push({ kind: "iterations", used: this.iterations, limit: this.limits.maxIterations });
348
+ }
349
+ if (this.limits.maxToolCalls !== void 0 && this.toolCalls > this.limits.maxToolCalls) {
350
+ exceeded.push({ kind: "tool_calls", used: this.toolCalls, limit: this.limits.maxToolCalls });
351
+ }
352
+ const totalTokens = this.tokenInput + this.tokenOutput;
353
+ if (this.limits.maxTokens !== void 0 && totalTokens > this.limits.maxTokens) {
354
+ exceeded.push({ kind: "tokens", used: totalTokens, limit: this.limits.maxTokens });
355
+ }
356
+ if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
357
+ exceeded.push({ kind: "cost", used: this.costUsd, limit: this.limits.maxCostUsd });
358
+ }
359
+ if (elapsedMs !== void 0) {
360
+ const idle = this.idleMs();
361
+ if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
362
+ exceeded.push({ kind: "idle_timeout", used: idle, limit: this.limits.idleTimeoutMs });
363
+ }
364
+ const wallOwnedByWatchdog = this._wallClockWatchdogOwned || this._onThreshold !== void 0 && this._watchdogActive === this.limits.timeoutMs;
365
+ if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs && !wallOwnedByWatchdog) {
366
+ exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
367
+ }
368
+ }
369
+ if (exceeded.length === 0) return [];
370
+ if (!this._onThreshold) {
371
+ const first2 = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
372
+ throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
373
+ }
374
+ if (this._mode === "sync") {
375
+ const first2 = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
376
+ throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
377
+ }
378
+ const bus = this._events;
379
+ if (!bus) {
380
+ const first2 = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
381
+ throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
382
+ }
383
+ const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
384
+ if (bus.hasListenerFor("budget.threshold_reached")) {
385
+ for (const entry of exceeded) {
386
+ if (this._pendingNegotiations.has(entry.kind)) continue;
387
+ this._pendingNegotiations.set(entry.kind, this._negotiateExtension(entry));
388
+ }
389
+ const decision = this._pendingNegotiations.get(first.kind);
390
+ if (!decision) throw new Error(`No pending negotiation for ${first.kind}`);
391
+ throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
392
+ }
393
+ let hardStop = null;
394
+ for (const entry of exceeded) {
395
+ if (this._pendingNegotiations.has(entry.kind)) continue;
396
+ const marker = Promise.resolve("stop");
397
+ this._pendingNegotiations.set(entry.kind, marker);
398
+ void marker.finally(() => this._pendingNegotiations.delete(entry.kind));
399
+ const sync = this._invokeHandlerSync(entry);
400
+ if (!sync) hardStop ??= new BudgetExceededError(entry.kind, entry.limit, entry.used);
401
+ }
402
+ if (hardStop) throw hardStop;
403
+ return exceeded;
404
+ }
405
+ /**
406
+ * Invoke `onThreshold` once for `entry` on the NO-LISTENER path and report
407
+ * whether it decided synchronously. Returns `true` when the handler returned
408
+ * a synchronous decision (already honored — an `extend` patched the limits),
409
+ * or `false` when it returned a Promise (async; the caller hard-stops, since
410
+ * there is no listener to resolve the negotiation). The handler is given the
411
+ * full info shape (`requestDecision` plus direct `extend`/`deny`) so both
412
+ * recording handlers and policy handlers work without a wired listener.
413
+ */
414
+ _invokeHandlerSync(entry) {
415
+ const handler = this._onThreshold;
416
+ if (!handler) return false;
417
+ let extendArg;
418
+ const result = handler({
419
+ kind: entry.kind,
420
+ used: entry.used,
421
+ limit: entry.limit,
422
+ requestDecision: () => this._busRequestDecision(entry),
423
+ // Direct hooks for synchronous policy/recording handlers.
424
+ extend: (extra) => {
425
+ extendArg = extra;
426
+ },
427
+ deny: () => {
428
+ }
429
+ });
430
+ if (result && typeof result.then === "function") return false;
431
+ if (result === "throw") return false;
432
+ if (result && typeof result === "object" && "extend" in result) {
433
+ extendArg = result.extend;
434
+ }
435
+ if (extendArg) this.patchLimits(extendArg);
436
+ return true;
437
+ }
438
+ /**
439
+ * Emit `budget.threshold_reached` and resolve to the listener's verdict.
440
+ * Resolves to `'stop'` immediately when there is no listener (or no bus) so
441
+ * no negotiation can hang and no fallback timer leaks. Mirrors the
442
+ * coordinator watchdog's own request path so both agree on the no-listener
443
+ * default.
444
+ */
445
+ _busRequestDecision(entry) {
446
+ const bus = this._events;
447
+ if (!bus?.hasListenerFor("budget.threshold_reached")) {
448
+ return Promise.resolve("stop");
449
+ }
450
+ return new Promise((resolve) => {
451
+ let resolved = false;
452
+ const respond = (d) => {
453
+ if (resolved) return;
454
+ resolved = true;
455
+ clearTimeout(fallback);
456
+ resolve(d);
457
+ };
458
+ const fallback = setTimeout(() => respond("stop"), _SubagentBudget.DECISION_TIMEOUT_MS);
459
+ const sessionId = this.currentSessionId();
460
+ bus.emit("budget.threshold_reached", {
461
+ ...sessionId ? { sessionId } : {},
462
+ kind: entry.kind,
463
+ used: entry.used,
464
+ limit: entry.limit,
465
+ timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
466
+ // deny() wins over a same-dispatch extend(): a listener that both grants
467
+ // and denies (or two listeners disagreeing) is resolved as a stop. The
468
+ // grant is deferred a microtask so a synchronous deny in the same emit
469
+ // pre-empts it; async grants still resolve normally.
470
+ extend: (extra) => queueMicrotask(() => respond({ extend: extra })),
471
+ deny: () => respond("stop")
472
+ });
473
+ });
474
+ }
475
+ /**
476
+ * Per-kind in-flight negotiation Promises. Each budget kind can have its
477
+ * own concurrent negotiation — e.g. iterations and timeout can both
478
+ * be exceeded simultaneously without blocking each other. The same kind
479
+ * cannot start two concurrent negotiations (dedup guard).
480
+ * Cleared in `_negotiateExtension`'s `finally` block.
481
+ */
482
+ _pendingNegotiations = /* @__PURE__ */ new Map();
483
+ /**
484
+ * Drive the threshold handler to a decision. Resolves with `'stop'`
485
+ * (signal the runner to abort) or `{ extend: ... }` (limits already
486
+ * patched in-place; the runner should not abort). Clears the
487
+ * per-kind slot in `_pendingNegotiations` in `finally`.
488
+ *
489
+ * The 'continue' return from a sync handler is treated as
490
+ * `{ extend: {} }` — keep going without patching; next overrun fires
491
+ * a fresh signal.
492
+ */
493
+ async _negotiateExtension(entry) {
494
+ if (!this._onThreshold) {
495
+ return "stop";
496
+ }
497
+ try {
498
+ const result = this._onThreshold({
499
+ kind: entry.kind,
500
+ used: entry.used,
501
+ limit: entry.limit,
502
+ // One event for THIS kind only — each exceeded kind has its own
503
+ // negotiation (and its own resolve), so there is no cross-kind
504
+ // first-wins drop and no O(N^2) re-emission.
505
+ requestDecision: () => this._busRequestDecision(entry),
506
+ extend: (extra) => {
507
+ this.patchLimits(extra);
508
+ },
509
+ deny: () => {
510
+ }
511
+ });
512
+ if (result === "throw") return "stop";
513
+ if (result === "continue") return { extend: {} };
514
+ const decision = await result;
515
+ if (decision === "stop") return "stop";
516
+ this.patchLimits(decision.extend);
517
+ return decision;
518
+ } finally {
519
+ this._pendingNegotiations.delete(entry.kind);
520
+ }
521
+ }
522
+ recordIteration() {
523
+ this.iterations++;
524
+ this.markActivity();
525
+ void this.checkLimits();
526
+ }
527
+ recordToolCall() {
528
+ this.toolCalls++;
529
+ this.markActivity();
530
+ void this.checkLimits();
531
+ }
532
+ recordUsage(usage, costUsd = 0) {
533
+ this.tokenInput += usage.input;
534
+ this.tokenOutput += usage.output;
535
+ this.costUsd += costUsd;
536
+ this.markActivity();
537
+ void this.checkLimits();
538
+ }
539
+ /**
540
+ * Wall-clock / idle budget check. Delegates to `checkLimits(elapsed)`, so
541
+ * `timeout` and `idle_timeout` follow the SAME negotiation path as the other
542
+ * kinds — they are NOT a special-cased hard stop. This is deliberate: a
543
+ * heartbeat-aware policy (see `attachAutoExtend` and `CollabSession`) grants
544
+ * a timeout extension only while the agent is making progress and denies it
545
+ * once the agent is genuinely stuck, which is safer than an unconditional
546
+ * hard kill of a long-but-working agent. The runner translates the resulting
547
+ * `BudgetThresholdSignal` decision (`extend` → patch limits in place,
548
+ * `stop` → abort) just like every other kind.
549
+ *
550
+ * Decision table (same as `checkLimits`):
551
+ * - no `onThreshold` handler → throw `BudgetExceededError` (hard stop)
552
+ * - `mode === 'sync'` → throw `BudgetExceededError` (hard stop)
553
+ * - `mode === 'auto'` + no listener → throw `BudgetExceededError` (no one to ask)
554
+ * - `mode === 'auto'` + listener → throw `BudgetThresholdSignal` (negotiated;
555
+ * a heartbeat-aware policy may extend the timeout)
556
+ */
557
+ checkTimeout() {
558
+ if (this.startTime === null) return;
559
+ const { timeoutMs, idleTimeoutMs } = this.limits;
560
+ if (timeoutMs === void 0 && idleTimeoutMs === void 0) return;
561
+ const elapsed = Date.now() - this.startTime;
562
+ const wallSkipped = this._onThreshold !== void 0 && this._watchdogActive !== void 0 && timeoutMs !== void 0 && this._watchdogActive === timeoutMs;
563
+ const wallTripped = this._wallClockWatchdogOwned || wallSkipped ? false : timeoutMs !== void 0 && elapsed > timeoutMs;
564
+ const idleTripped = idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs;
565
+ if (!wallTripped && !idleTripped) return;
566
+ void this.checkLimits(elapsed);
567
+ }
568
+ /** Returns true if a wall-clock or idle timeout has occurred without throwing. */
569
+ isTimedOut() {
570
+ if (this.startTime === null) return false;
571
+ const { timeoutMs, idleTimeoutMs } = this.limits;
572
+ if (timeoutMs !== void 0 && Date.now() - this.startTime > timeoutMs) return true;
573
+ if (idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs) return true;
574
+ return false;
575
+ }
576
+ usage() {
577
+ return {
578
+ iterations: this.iterations,
579
+ toolCalls: this.toolCalls,
580
+ tokens: {
581
+ input: this.tokenInput,
582
+ output: this.tokenOutput,
583
+ total: this.tokenInput + this.tokenOutput
584
+ },
585
+ costUsd: this.costUsd,
586
+ elapsedMs: this.startTime === null ? 0 : Date.now() - this.startTime
587
+ };
588
+ }
589
+ };
590
+
591
+ // src/coordination/subagent-result-tool.ts
592
+ var SUBAGENT_STRUCTURED_REPORT_META_KEY = "subagentStructuredReport";
593
+ var MAX_SUBAGENT_STRUCTURED_REPORT_CHARS = 8e3;
594
+ var MAX_SUMMARY_CHARS = 1500;
595
+ var MAX_FINDINGS = 16;
596
+ var MAX_FILES = 64;
597
+ var MAX_NEXT_STEPS = 12;
598
+ var MAX_ITEM_CHARS = 500;
599
+ function normalizeSubagentStructuredReport(value) {
600
+ if (!isRecord(value)) return void 0;
601
+ const summary = nonEmptyString(value["summary"]);
602
+ const findings = stringArray(value["findings"]);
603
+ const filesExamined = stringArray(value["files_examined"]);
604
+ const nextSteps = stringArray(value["suggested_next_steps"]);
605
+ const confidence = value["confidence"];
606
+ const completion = value["completion"];
607
+ const remainingWork = optionalString(value["remaining_work"]);
608
+ if (!summary || summary.length > MAX_SUMMARY_CHARS || !findings || findings.length > MAX_FINDINGS || !filesExamined || filesExamined.length > MAX_FILES || !nextSteps || nextSteps.length > MAX_NEXT_STEPS || [...findings, ...filesExamined, ...nextSteps].some(
609
+ (item) => item.trim().length > MAX_ITEM_CHARS
610
+ ) || typeof confidence !== "number" || !Number.isFinite(confidence) || confidence < 0 || confidence > 1 || completion !== void 0 && completion !== "complete" && completion !== "partial" || completion === "partial" && !remainingWork || remainingWork !== void 0 && remainingWork.length > MAX_SUMMARY_CHARS) {
611
+ return void 0;
612
+ }
613
+ const report = {
614
+ summary,
615
+ findings: findings.map((item) => item.trim()).filter(Boolean),
616
+ files_examined: filesExamined.map((item) => item.trim()).filter(Boolean),
617
+ confidence,
618
+ suggested_next_steps: nextSteps.map((item) => item.trim()).filter(Boolean),
619
+ ...completion === "complete" || completion === "partial" ? { completion } : {},
620
+ ...remainingWork ? { remaining_work: remainingWork } : {}
621
+ };
622
+ return JSON.stringify(report).length <= MAX_SUBAGENT_STRUCTURED_REPORT_CHARS ? report : void 0;
623
+ }
624
+ function readSubagentStructuredReport(ctx) {
625
+ return normalizeSubagentStructuredReport(ctx?.meta?.[SUBAGENT_STRUCTURED_REPORT_META_KEY]);
626
+ }
627
+ function formatSubagentStructuredReport(report) {
628
+ const findings = report.findings.length ? report.findings.map((item) => `- ${item}`).join("\n") : "- (none)";
629
+ const files = report.files_examined.length ? report.files_examined.join(", ") : "(none)";
630
+ const next = report.suggested_next_steps.length ? report.suggested_next_steps.map((item) => `- ${item}`).join("\n") : "- (none)";
631
+ return [
632
+ report.summary,
633
+ report.completion ? `Completion: ${report.completion}` : void 0,
634
+ report.remaining_work ? `Remaining work: ${report.remaining_work}` : void 0,
635
+ `
636
+ Findings:
637
+ ${findings}`,
638
+ `
639
+ Files examined: ${files}`,
640
+ `Confidence: ${report.confidence.toFixed(2)}`,
641
+ `
642
+ Suggested next steps:
643
+ ${next}`
644
+ ].filter((line) => typeof line === "string").join("\n");
645
+ }
646
+ function makeSubagentResultTool() {
647
+ return {
648
+ name: "submit_result",
649
+ description: "Submit the task result as a compact structured report for the Director. Call once near the end, before your final text response.",
650
+ permission: "auto",
651
+ mutating: false,
652
+ capabilities: [ToolCapabilities.COORDINATION_RESULT_SUBMIT],
653
+ inputSchema: {
654
+ type: "object",
655
+ properties: {
656
+ summary: {
657
+ type: "string",
658
+ maxLength: MAX_SUMMARY_CHARS,
659
+ description: "Concise outcome summary."
660
+ },
661
+ findings: {
662
+ type: "array",
663
+ maxItems: MAX_FINDINGS,
664
+ items: { type: "string" },
665
+ description: "Atomic findings or changes, with evidence when useful."
666
+ },
667
+ files_examined: {
668
+ type: "array",
669
+ maxItems: MAX_FILES,
670
+ items: { type: "string" },
671
+ description: "Project-relative files materially read or changed."
672
+ },
673
+ confidence: {
674
+ type: "number",
675
+ minimum: 0,
676
+ maximum: 1,
677
+ description: "Confidence from 0.0 to 1.0."
678
+ },
679
+ suggested_next_steps: {
680
+ type: "array",
681
+ maxItems: MAX_NEXT_STEPS,
682
+ items: { type: "string" },
683
+ description: "Concrete follow-ups; use an empty array when none remain."
684
+ },
685
+ completion: {
686
+ type: "string",
687
+ enum: ["complete", "partial"],
688
+ description: "Use `partial` only when the task is intentionally stopping at a clean checkpoint and another worker should continue."
689
+ },
690
+ remaining_work: {
691
+ type: "string",
692
+ maxLength: MAX_SUMMARY_CHARS,
693
+ description: "Concrete work left for a successor. Required by runtime validation when completion is `partial`."
694
+ }
695
+ },
696
+ required: ["summary", "findings", "files_examined", "confidence", "suggested_next_steps"],
697
+ additionalProperties: false
698
+ },
699
+ async execute(input, ctx) {
700
+ const report = normalizeSubagentStructuredReport(input);
701
+ if (!report) {
702
+ return {
703
+ ok: false,
704
+ error: "Invalid report: summary/findings/files_examined/confidence/suggested_next_steps are required and confidence must be 0..1."
705
+ };
706
+ }
707
+ ctx.meta[SUBAGENT_STRUCTURED_REPORT_META_KEY] = report;
708
+ return {
709
+ ok: true,
710
+ findings: report.findings.length,
711
+ filesExamined: report.files_examined.length,
712
+ confidence: report.confidence
713
+ };
714
+ }
715
+ };
716
+ }
717
+ function isRecord(value) {
718
+ return typeof value === "object" && value !== null && !Array.isArray(value);
719
+ }
720
+ function nonEmptyString(value) {
721
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
722
+ }
723
+ function optionalString(value) {
724
+ return value === void 0 ? void 0 : nonEmptyString(value);
725
+ }
726
+ function stringArray(value) {
727
+ return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
728
+ }
729
+
730
+ // src/coordination/agent-subagent-runner.ts
731
+ function withDisabledToolFiltering(factory) {
732
+ return async (config, task) => {
733
+ const result = await factory(config, task);
734
+ const disabled = config.disabledTools ?? [];
735
+ if (disabled.length === 0) return result;
736
+ const registry = result.agent.tools;
737
+ if (registry && typeof registry.unregister === "function") {
738
+ for (const toolName of disabled) {
739
+ registry.unregister(toolName);
740
+ }
741
+ }
742
+ return result;
743
+ };
744
+ }
745
+ function makeAgentSubagentRunner(opts) {
746
+ const format = opts.formatTaskInput ?? defaultFormatTaskInput;
747
+ return async (task, ctx) => {
748
+ const taskStartedAt = Date.now();
749
+ const factoryResult = await opts.factory(ctx.config, task);
750
+ const { agent, events } = factoryResult;
751
+ const taskContext = task.context ?? {};
752
+ const hasExplicitTelemetryTask = typeof taskContext["telemetryTaskId"] === "string";
753
+ const telemetryTaskId = hasExplicitTelemetryTask ? taskContext["telemetryTaskId"] : task.id;
754
+ const telemetryRunId = typeof taskContext["telemetryRunId"] === "string" ? taskContext["telemetryRunId"] : void 0;
755
+ const telemetryBoardId = typeof taskContext["telemetryBoardId"] === "string" ? taskContext["telemetryBoardId"] : void 0;
756
+ const agentContext = agent.ctx;
757
+ if (agentContext?.meta) {
758
+ agentContext.meta["subagentTaskId"] = task.id;
759
+ const kanbanContext = taskContext["kanban"];
760
+ if (kanbanContext && typeof kanbanContext === "object") {
761
+ agentContext.meta["kanban"] = kanbanContext;
762
+ }
763
+ if (hasExplicitTelemetryTask) {
764
+ agentContext.meta["telemetryTaskId"] = telemetryTaskId;
765
+ if (telemetryRunId) agentContext.meta["telemetryRunId"] = telemetryRunId;
766
+ }
767
+ }
768
+ if (hasExplicitTelemetryTask) {
769
+ agentContext?.setCurrentKanbanTask?.(telemetryTaskId, telemetryBoardId);
770
+ } else {
771
+ const kanbanContext = taskContext["kanban"];
772
+ if (kanbanContext && typeof kanbanContext === "object") {
773
+ const kanban = kanbanContext;
774
+ const boardId = typeof kanban["boardId"] === "string" ? kanban["boardId"] : void 0;
775
+ const taskId = typeof kanban["taskId"] === "string" ? kanban["taskId"] : void 0;
776
+ if (taskId) agentContext?.setCurrentKanbanTask?.(taskId, boardId);
777
+ }
778
+ }
779
+ const detachFleet = opts.fleetBus?.attach(ctx.subagentId, events, task.id);
780
+ const aborter = new AbortController();
781
+ ctx.budget._events = events;
782
+ ctx.budget.onThreshold = ({ requestDecision }) => requestDecision();
783
+ let budgetError = null;
784
+ const onBudgetError = (err) => {
785
+ if (err instanceof BudgetThresholdSignal) {
786
+ budgetError = err;
787
+ return;
788
+ }
789
+ aborter.abort();
790
+ budgetError = err instanceof BudgetExceededError ? err : new BudgetExceededError(
791
+ "tool_calls",
792
+ 0,
793
+ 0
794
+ );
795
+ if (budgetError !== err && err instanceof Error) {
796
+ budgetError.message += ` (caused by: ${err.message})`;
797
+ }
798
+ };
799
+ let lastToolFailed = null;
800
+ const unsub = [];
801
+ unsub.push(
802
+ events.on("tool.executed", (e) => {
803
+ if (hasExplicitTelemetryTask) {
804
+ opts.hostEvents?.emit("subagent.tool_executed", {
805
+ sessionId: e.sessionId,
806
+ subagentId: ctx.subagentId,
807
+ agentSessionId: e.sessionId,
808
+ agentName: e.agentName ?? ctx.config.name,
809
+ taskId: telemetryTaskId,
810
+ runId: telemetryRunId,
811
+ traceId: e.traceId,
812
+ id: e.id,
813
+ name: e.name,
814
+ durationMs: e.durationMs,
815
+ ok: e.ok,
816
+ // Raw tool inputs may contain credentials or sensitive arguments.
817
+ // Task telemetry crosses from the isolated worker bus to the shared
818
+ // host bus, so expose only execution metadata at this boundary.
819
+ outputBytes: e.outputBytes,
820
+ outputTokens: e.outputTokens,
821
+ outputLines: e.outputLines
822
+ });
823
+ }
824
+ try {
825
+ ctx.budget.recordToolCall();
826
+ } catch (eb) {
827
+ onBudgetError(eb);
828
+ }
829
+ if (e.ok === false) {
830
+ lastToolFailed = e.name;
831
+ } else if (e.ok === true) {
832
+ lastToolFailed = null;
833
+ }
834
+ }),
835
+ events.on("file.event", (e) => {
836
+ if (!hasExplicitTelemetryTask) return;
837
+ if (e.runId === telemetryRunId && e.taskId === telemetryTaskId) return;
838
+ opts.hostEvents?.emit("file.event", {
839
+ ...e,
840
+ scope: "task",
841
+ taskId: telemetryTaskId,
842
+ boardId: telemetryBoardId,
843
+ runId: telemetryRunId
844
+ });
845
+ }),
846
+ events.on("provider.response", (e) => {
847
+ try {
848
+ ctx.budget.recordUsage(e.usage);
849
+ } catch (e2) {
850
+ void onBudgetError(e2);
851
+ }
852
+ }),
853
+ events.on("iteration.started", () => {
854
+ try {
855
+ ctx.budget.recordIteration();
856
+ } catch (e) {
857
+ void onBudgetError(e);
858
+ }
859
+ const u = ctx.budget.usage();
860
+ const since = u.iterations - lastSummaryAtIteration;
861
+ if (since >= SUMMARY_INTERVAL) {
862
+ lastSummaryAtIteration = u.iterations;
863
+ events.emit("subagent.iteration_summary", {
864
+ subagentId: ctx.subagentId,
865
+ iteration: u.iterations,
866
+ toolCalls: u.toolCalls,
867
+ costUsd: u.costUsd,
868
+ currentTool: currentToolName,
869
+ partialText: streamingTextAcc.trim() || void 0
870
+ });
871
+ }
872
+ }),
873
+ // D3: cooperative timeout enforcement DURING a long tool call.
874
+ // The iteration-loop checkTimeout() only fires between agent
875
+ // iterations — a single `bash sleep 3600` call would otherwise
876
+ // park inside one tool execution while the timeout silently
877
+ // passes, relying solely on the coordinator's hard Promise.race
878
+ // to interrupt. Tools that emit `tool.progress` (bash chunks,
879
+ // fetch byte progress, spawn-stream stdout) give us a heartbeat
880
+ // we can hang the check on. When the budget trips here:
881
+ // 1. onBudgetError sets budgetError + aborter.abort()
882
+ // 2. aborter signal propagates to agent.run → tool executor
883
+ // 3. tool's own signal listener kills the child process
884
+ // Cheap: O(1) per progress event, and the budget short-circuits
885
+ // when timeoutMs is unset (most subagents have one set anyway).
886
+ events.on("tool.progress", () => {
887
+ ctx.budget.markActivity();
888
+ try {
889
+ ctx.budget.checkTimeout();
890
+ } catch (e) {
891
+ void onBudgetError(e);
892
+ }
893
+ })
894
+ );
895
+ let currentToolName;
896
+ let streamingTextAcc = "";
897
+ let lastSummaryAtIteration = 0;
898
+ const SUMMARY_INTERVAL = 25;
899
+ unsub.push(
900
+ events.on("tool.started", (e) => {
901
+ currentToolName = e.name;
902
+ }),
903
+ events.on("provider.text_delta", (e) => {
904
+ ctx.budget.markActivity();
905
+ streamingTextAcc = (streamingTextAcc + e.text).slice(-2e3);
906
+ const text = streamingTextAcc.trim();
907
+ if (text) {
908
+ ctx.reportProgress?.({ text, source: "stream", capturedAt: Date.now() });
909
+ }
910
+ })
911
+ );
912
+ const onParentAbort = () => aborter.abort();
913
+ ctx.signal.addEventListener("abort", onParentAbort);
914
+ if (resolveGracefulFinish(ctx.config)) {
915
+ unsub.push(
916
+ events.on("subagent.finish_requested", (e) => {
917
+ if (e.subagentId && e.subagentId !== ctx.subagentId) return;
918
+ setBtwNote(agent.ctx, e.notice);
919
+ })
920
+ );
921
+ }
922
+ let result;
923
+ try {
924
+ result = await agent.run(format(task, ctx.config), { signal: aborter.signal });
925
+ const finalText2 = result.finalText?.trim();
926
+ if (finalText2) {
927
+ ctx.reportProgress?.({ text: finalText2, source: "run_result", capturedAt: Date.now() });
928
+ }
929
+ events.emit("subagent.task_completed", {
930
+ subagentId: ctx.subagentId,
931
+ taskId: task.id,
932
+ status: result.status === "done" ? "success" : "failed",
933
+ iterations: result.iterations,
934
+ toolCalls: ctx.budget.usage().toolCalls,
935
+ durationMs: Date.now() - taskStartedAt,
936
+ finalText: result.finalText?.trim() || void 0
937
+ });
938
+ } finally {
939
+ detachFleet?.();
940
+ ctx.signal.removeEventListener("abort", onParentAbort);
941
+ for (const u of unsub) u();
942
+ if (factoryResult.dispose) {
943
+ try {
944
+ await factoryResult.dispose();
945
+ } catch {
946
+ }
947
+ }
948
+ }
949
+ if (budgetError) {
950
+ if ("decision" in budgetError) {
951
+ try {
952
+ const decision = await budgetError.decision;
953
+ if (decision === "stop") {
954
+ budgetError = new BudgetExceededError(
955
+ budgetError.kind,
956
+ budgetError.limit,
957
+ budgetError.used
958
+ );
959
+ } else {
960
+ budgetError = null;
961
+ }
962
+ } catch {
963
+ budgetError = new BudgetExceededError(
964
+ budgetError.kind,
965
+ budgetError.limit,
966
+ budgetError.used
967
+ );
968
+ }
969
+ }
970
+ if (budgetError) throw budgetError;
971
+ }
972
+ if (result.status === "failed") {
973
+ throw result.error instanceof AgentError ? result.error : new AgentError({
974
+ message: result.error instanceof Error ? result.error.message : String(result.error ?? "agent failed"),
975
+ code: ERROR_CODES.AGENT_RUN_FAILED,
976
+ cause: result.error
977
+ });
978
+ }
979
+ if (result.status === "aborted") {
980
+ throw new AgentError({
981
+ message: "agent aborted",
982
+ code: ERROR_CODES.AGENT_ABORTED
983
+ });
984
+ }
985
+ if (result.status === "max_iterations") {
986
+ throw new AgentError({
987
+ message: "agent exhausted iteration limit",
988
+ code: ERROR_CODES.AGENT_ITERATION_LIMIT,
989
+ recoverable: true
990
+ });
991
+ }
992
+ const usage = ctx.budget.usage();
993
+ const finalText = (result.finalText ?? "").trim();
994
+ if (finalText.length === 0 && usage.toolCalls === 0) {
995
+ throw new AgentError({
996
+ message: "empty response \u2014 agent produced no text and no tool calls",
997
+ code: ERROR_CODES.AGENT_RUN_FAILED,
998
+ context: { iterations: result.iterations }
999
+ });
1000
+ }
1001
+ if (finalText.length === 0 && lastToolFailed !== null) {
1002
+ throw new AgentError({
1003
+ message: `unrecovered tool failure: ${lastToolFailed} \u2014 agent ended turn without acknowledging the error`,
1004
+ code: ERROR_CODES.AGENT_RUN_FAILED,
1005
+ context: { tool: lastToolFailed, iterations: result.iterations }
1006
+ });
1007
+ }
1008
+ return {
1009
+ result: result.finalText,
1010
+ report: readSubagentStructuredReport(agentContext),
1011
+ iterations: result.iterations,
1012
+ toolCalls: usage.toolCalls
1013
+ };
1014
+ };
1015
+ }
1016
+ function defaultFormatTaskInput(task) {
1017
+ return task.description ?? "";
1018
+ }
1019
+
1020
+ // src/coordination/brain-heuristics.ts
1021
+ var BLOCKED_RESOLVED_MARKERS = /\b(?:resolved|fixed|completed|unblocked|available|done|merged|landed|shipped)\b/;
1022
+ var COMPETING_ALTERNATIVE = /\bor\b/;
1023
+ function isBlockedResolved(question, context, markers = BLOCKED_RESOLVED_MARKERS) {
1024
+ return question.includes("blocked") && !COMPETING_ALTERNATIVE.test(question) && markers.test(context);
1025
+ }
1026
+ var DEFAULT_BRAIN_HEURISTICS = {
1027
+ lowRiskAutoAnswer: true,
1028
+ blockedResolved: true,
1029
+ deadlockSkip: true,
1030
+ retryExhausted: true,
1031
+ continuePing: true
1032
+ };
1033
+ var escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1034
+ function compileResolutionMarkers(words) {
1035
+ const cleaned = (words ?? []).map((w) => w.trim()).filter((w) => w.length > 0);
1036
+ if (cleaned.length === 0) return BLOCKED_RESOLVED_MARKERS;
1037
+ return new RegExp(`\\b(?:${cleaned.map(escapeRegex).join("|")})\\b`, "i");
1038
+ }
1039
+ function resolveBrainHeuristics(cfg) {
1040
+ return {
1041
+ lowRiskAutoAnswer: cfg?.lowRiskAutoAnswer ?? DEFAULT_BRAIN_HEURISTICS.lowRiskAutoAnswer,
1042
+ blockedResolved: cfg?.blockedResolved ?? DEFAULT_BRAIN_HEURISTICS.blockedResolved,
1043
+ deadlockSkip: cfg?.deadlockSkip ?? DEFAULT_BRAIN_HEURISTICS.deadlockSkip,
1044
+ retryExhausted: cfg?.retryExhausted ?? DEFAULT_BRAIN_HEURISTICS.retryExhausted,
1045
+ continuePing: cfg?.continuePing ?? DEFAULT_BRAIN_HEURISTICS.continuePing,
1046
+ blockedResolvedMarkers: compileResolutionMarkers(cfg?.blockedResolvedMarkers)
1047
+ };
1048
+ }
1049
+
1050
+ // src/coordination/brain-telemetry.ts
1051
+ var DETERMINISTIC_BRAIN_TIERS = /* @__PURE__ */ new Set(
1052
+ ["rule", "policy", "heuristic", "cache", "ledger-guard", "terminal"]
1053
+ );
1054
+ var tierByRequest = /* @__PURE__ */ new WeakMap();
1055
+ function markDecisionTier(request, tier) {
1056
+ tierByRequest.set(request, tier);
1057
+ }
1058
+ function readDecisionTier(request) {
1059
+ return tierByRequest.get(request);
1060
+ }
1061
+ function isDeterministicTier(tier) {
1062
+ return tier !== void 0 && DETERMINISTIC_BRAIN_TIERS.has(tier);
1063
+ }
1064
+ var BrainTierCounter = class {
1065
+ counts = /* @__PURE__ */ new Map();
1066
+ total = 0;
1067
+ unattributed = 0;
1068
+ /** Count one decision. `undefined` counts toward `unattributed`. */
1069
+ record(tier) {
1070
+ this.total += 1;
1071
+ if (tier === void 0) {
1072
+ this.unattributed += 1;
1073
+ return;
1074
+ }
1075
+ this.counts.set(tier, (this.counts.get(tier) ?? 0) + 1);
1076
+ }
1077
+ snapshot() {
1078
+ const byTier = {};
1079
+ let deterministic = 0;
1080
+ let llmBacked = 0;
1081
+ for (const [tier, count] of this.counts) {
1082
+ byTier[tier] = count;
1083
+ if (DETERMINISTIC_BRAIN_TIERS.has(tier)) deterministic += count;
1084
+ else if (tier === "council" || tier === "llm") llmBacked += count;
1085
+ }
1086
+ return {
1087
+ byTier,
1088
+ total: this.total,
1089
+ deterministic,
1090
+ llmBacked,
1091
+ unattributed: this.unattributed
1092
+ };
1093
+ }
1094
+ reset() {
1095
+ this.counts.clear();
1096
+ this.total = 0;
1097
+ this.unattributed = 0;
1098
+ }
1099
+ };
1100
+
1101
+ // src/coordination/brain.ts
1102
+ var BRAIN_RISK_LEVELS = {
1103
+ low: 0,
1104
+ medium: 1,
1105
+ high: 2,
1106
+ critical: 3
1107
+ };
1108
+ var ObservableBrainArbiter = class {
1109
+ constructor(inner, events) {
1110
+ this.inner = inner;
1111
+ this.events = events;
1112
+ }
1113
+ inner;
1114
+ events;
1115
+ async decide(request) {
1116
+ this.events.emit("brain.decision_requested", { sessionId: request.sessionId, request, at: Date.now() });
1117
+ const decision = await this.inner.decide(request);
1118
+ const event = decision.type === "ask_human" ? "brain.decision_ask_human" : decision.type === "deny" ? "brain.decision_denied" : "brain.decision_answered";
1119
+ const tier = readDecisionTier(request);
1120
+ this.events.emit(event, {
1121
+ sessionId: request.sessionId,
1122
+ request,
1123
+ decision,
1124
+ at: Date.now(),
1125
+ ...tier ? { tier } : {}
1126
+ });
1127
+ return decision;
1128
+ }
1129
+ };
1130
+ var BrainDecisionQueue = class {
1131
+ constructor(events, opts = {}) {
1132
+ this.events = events;
1133
+ this.opts = opts;
1134
+ this.offAnswer = this.events.on("brain.human_answered", (answer) => {
1135
+ const pending = this.pending.get(answer.id);
1136
+ if (!pending) return;
1137
+ this.pending.delete(answer.id);
1138
+ if (pending.timer) clearTimeout(pending.timer);
1139
+ markDecisionTier(pending.request, "human");
1140
+ if (answer.deny) {
1141
+ pending.resolve({ type: "deny", reason: answer.text ?? "Denied by human." });
1142
+ return;
1143
+ }
1144
+ const option = pending.request.options?.find((o) => o.id === answer.optionId);
1145
+ pending.resolve({
1146
+ type: "answer",
1147
+ optionId: answer.optionId,
1148
+ text: answer.text ?? option?.label ?? answer.optionId ?? "Human answered.",
1149
+ rationale: "Human answered a Brain escalation prompt."
1150
+ });
1151
+ });
1152
+ }
1153
+ events;
1154
+ opts;
1155
+ pending = /* @__PURE__ */ new Map();
1156
+ offAnswer;
1157
+ async requestHumanDecision(request) {
1158
+ const ask = {
1159
+ type: "ask_human",
1160
+ prompt: formatHumanPrompt(request),
1161
+ options: request.options,
1162
+ rationale: "Decision escalated to human authority."
1163
+ };
1164
+ const pending = new Promise((resolve) => {
1165
+ const entry = { request, resolve };
1166
+ if (this.opts.timeoutMs && this.opts.timeoutMs > 0) {
1167
+ entry.timer = setTimeout(() => {
1168
+ this.pending.delete(request.id);
1169
+ markDecisionTier(request, "terminal");
1170
+ resolve(
1171
+ this.opts.onTimeout?.(request) ?? {
1172
+ type: "deny",
1173
+ reason: "Brain human decision timed out."
1174
+ }
1175
+ );
1176
+ }, this.opts.timeoutMs);
1177
+ }
1178
+ this.pending.set(request.id, entry);
1179
+ });
1180
+ this.events.emit("brain.decision_ask_human", { sessionId: request.sessionId, request, decision: ask, at: Date.now() });
1181
+ return pending;
1182
+ }
1183
+ dispose() {
1184
+ this.offAnswer();
1185
+ for (const [id, pending] of this.pending) {
1186
+ if (pending.timer) clearTimeout(pending.timer);
1187
+ pending.resolve({ type: "deny", reason: "Brain decision queue disposed." });
1188
+ this.pending.delete(id);
1189
+ }
1190
+ }
1191
+ };
1192
+ function terminalPolicyDecision(request, policy = "conservative") {
1193
+ if (policy === "deny-all") {
1194
+ return {
1195
+ type: "deny",
1196
+ reason: `Headless terminal policy "deny-all": "${request.question}" was not auto-approved (risk: ` + request.risk + "). The proposed action was not taken."
1197
+ };
1198
+ }
1199
+ const recommended = request.options?.find((option) => option.recommended);
1200
+ const acceptRecommended = recommended !== void 0 && (policy === "continue-on-recommended" || request.risk === "low" || request.risk === "medium");
1201
+ if (recommended && acceptRecommended) {
1202
+ return {
1203
+ type: "answer",
1204
+ optionId: recommended.id,
1205
+ text: recommended.label,
1206
+ rationale: `Headless terminal policy: caller-recommended option accepted at ${request.risk} risk (no human available by configuration).`
1207
+ };
1208
+ }
1209
+ if (request.fallback === "continue") {
1210
+ return {
1211
+ type: "answer",
1212
+ text: "Continue with the caller default.",
1213
+ rationale: "Headless terminal policy: request declares continue as its safe fallback."
1214
+ };
1215
+ }
1216
+ return {
1217
+ type: "deny",
1218
+ reason: `Headless terminal policy: no safe automatic option for "${request.question}" (risk: ${request.risk}). The proposed action was not taken.`
1219
+ };
1220
+ }
1221
+ var EscalationRoutingBrainArbiter = class {
1222
+ constructor(inner, queue, getMode, getTerminalPolicy) {
1223
+ this.inner = inner;
1224
+ this.queue = queue;
1225
+ this.getMode = getMode;
1226
+ this.getTerminalPolicy = getTerminalPolicy;
1227
+ }
1228
+ inner;
1229
+ queue;
1230
+ getMode;
1231
+ getTerminalPolicy;
1232
+ async decide(request) {
1233
+ const decision = await this.inner.decide(request);
1234
+ if (decision.type !== "ask_human") return decision;
1235
+ if (this.getMode() === "interactive" && this.queue) {
1236
+ return this.queue.requestHumanDecision(request);
1237
+ }
1238
+ markDecisionTier(request, "terminal");
1239
+ return terminalPolicyDecision(request, this.getTerminalPolicy?.() ?? "conservative");
1240
+ }
1241
+ };
1242
+ var HumanEscalatingBrainArbiter = class {
1243
+ constructor(inner, queue) {
1244
+ this.inner = inner;
1245
+ this.queue = queue;
1246
+ }
1247
+ inner;
1248
+ queue;
1249
+ async decide(request) {
1250
+ const decision = await this.inner.decide(request);
1251
+ if (decision.type !== "ask_human") return decision;
1252
+ return this.queue.requestHumanDecision(request);
1253
+ }
1254
+ };
1255
+ var DefaultBrainArbiter = class {
1256
+ heuristics;
1257
+ constructor(opts = {}) {
1258
+ const resolved = resolveBrainHeuristics(opts.heuristics);
1259
+ this.heuristics = {
1260
+ ...resolved,
1261
+ lowRiskAutoAnswer: opts.allowLowRiskAutoAnswer ?? resolved.lowRiskAutoAnswer
1262
+ };
1263
+ }
1264
+ async decide(request) {
1265
+ const recommended = request.options?.find((option) => option.recommended);
1266
+ if (this.heuristics.lowRiskAutoAnswer && request.risk === "low" && recommended) {
1267
+ markDecisionTier(request, "heuristic");
1268
+ return {
1269
+ type: "answer",
1270
+ optionId: recommended.id,
1271
+ text: recommended.label,
1272
+ rationale: "Low-risk request with an explicit recommended option."
1273
+ };
1274
+ }
1275
+ if (this.heuristics.blockedResolved && !request.options?.length && request.fallback === "continue" && request.context) {
1276
+ if (isBlockedResolved(
1277
+ request.question.toLowerCase(),
1278
+ request.context.toLowerCase(),
1279
+ this.heuristics.blockedResolvedMarkers
1280
+ )) {
1281
+ markDecisionTier(request, "heuristic");
1282
+ return {
1283
+ type: "answer",
1284
+ text: "Blocker resolved. Continue with the previously blocked work.",
1285
+ rationale: "Heuristic: blocking dependency explicitly resolved \u2014 resuming."
1286
+ };
1287
+ }
1288
+ }
1289
+ markDecisionTier(request, "policy");
1290
+ switch (request.fallback) {
1291
+ case "deny":
1292
+ return {
1293
+ type: "deny",
1294
+ reason: `Brain could not safely decide: ${request.question}`
1295
+ };
1296
+ case "continue":
1297
+ return {
1298
+ type: "answer",
1299
+ text: "Continue with the caller default.",
1300
+ rationale: "No safe Brain decision was available; request fallback is continue."
1301
+ };
1302
+ case "ask_human":
1303
+ return {
1304
+ type: "ask_human",
1305
+ prompt: formatHumanPrompt(request),
1306
+ options: request.options,
1307
+ rationale: "Decision requires human authority or lacks a safe automatic option."
1308
+ };
1309
+ default:
1310
+ return {
1311
+ type: "deny",
1312
+ reason: `Brain could not safely decide (unknown fallback: ${String(request.fallback)}): ${request.question}`
1313
+ };
1314
+ }
1315
+ }
1316
+ };
1317
+ function formatHumanPrompt(request) {
1318
+ const lines = [
1319
+ `Brain requires human decision for ${request.source}:`,
1320
+ `Question: ${request.question}`
1321
+ ];
1322
+ if (request.context?.trim()) {
1323
+ lines.push("", "Context:", request.context.trim());
1324
+ }
1325
+ if (request.options?.length) {
1326
+ lines.push("", "Options:");
1327
+ for (const option of request.options) {
1328
+ const risk = option.risk ? ` [risk: ${option.risk}]` : "";
1329
+ const consequence = option.consequence ? ` \u2014 ${option.consequence}` : "";
1330
+ lines.push(`- ${option.id}: ${option.label}${risk}${consequence}`);
1331
+ }
1332
+ }
1333
+ lines.push("", `Risk: ${request.risk}`);
1334
+ return lines.join("\n");
1335
+ }
1336
+
1337
+ // src/coordination/brain-ledger.ts
1338
+ import { createReadStream } from "node:fs";
1339
+ import { access, appendFile, mkdir, rename, stat } from "node:fs/promises";
1340
+ import { dirname } from "node:path";
1341
+ import { createInterface } from "node:readline";
1342
+ var QUESTION_MAX = 200;
1343
+ var DETAIL_MAX = 240;
1344
+ var truncate = (s, max) => s === void 0 ? void 0 : s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
1345
+ function brainDecisionKey(source, question) {
1346
+ const normalized = question.toLowerCase().replace(/[^\s]*\d[^\s]*/g, "#").replace(/\s+/g, " ").trim().slice(0, 100);
1347
+ return `${source ?? "?"}:${normalized}`;
1348
+ }
1349
+ function fmtAge(ms) {
1350
+ const s = Math.max(0, Math.round(ms / 1e3));
1351
+ if (s < 60) return `${s}s ago`;
1352
+ if (s < 3600) return `${Math.round(s / 60)}m ago`;
1353
+ return `${Math.round(s / 3600)}h ago`;
1354
+ }
1355
+ function createLedgerGuardBrainArbiter(opts) {
1356
+ const denyAfter = opts.denyAfter ?? 3;
1357
+ return {
1358
+ async decide(request) {
1359
+ if (denyAfter > 0) {
1360
+ const streak = opts.failureStreakFor(request);
1361
+ if (streak >= denyAfter) {
1362
+ markDecisionTier(request, "ledger-guard");
1363
+ return {
1364
+ type: "deny",
1365
+ reason: `Ledger guard: the last ${streak} approved decisions of this kind all ended in observed failures \u2014 denying deterministically until a success is recorded. (${request.question.slice(0, 120)})`
1366
+ };
1367
+ }
1368
+ }
1369
+ return opts.inner.decide(request);
1370
+ }
1371
+ };
1372
+ }
1373
+ var BrainDecisionLedger = class _BrainDecisionLedger {
1374
+ constructor(opts) {
1375
+ this.opts = opts;
1376
+ this.maxMemoryEntries = opts.maxMemoryEntries ?? 500;
1377
+ this.retryWindowMs = opts.interventionRetryWindowMs ?? 10 * 6e4;
1378
+ this.now = opts.now ?? Date.now;
1379
+ }
1380
+ opts;
1381
+ entries = [];
1382
+ outcomeByRequest = /* @__PURE__ */ new Map();
1383
+ /** subagentId → decision request ids awaiting that subagent's task result. */
1384
+ pendingBudgetOutcomes = /* @__PURE__ */ new Map();
1385
+ lastInterventionByKind = /* @__PURE__ */ new Map();
1386
+ unsubscribers = [];
1387
+ /** Serialized fire-and-forget writes — awaited by `stop()` (drain). */
1388
+ writeChain = Promise.resolve();
1389
+ pendingWriteCount = 0;
1390
+ pendingWriteBytes = 0;
1391
+ static MAX_PENDING_WRITES = 1e3;
1392
+ static MAX_PENDING_WRITE_BYTES = 8 * 1024 * 1024;
1393
+ /** Rotate the JSONL to `.1` past this size so it can't grow unbounded. */
1394
+ static MAX_FILE_BYTES = 5 * 1024 * 1024;
1395
+ /** Stat the file for rotation only once per this many writes. */
1396
+ static ROTATE_CHECK_EVERY = 100;
1397
+ writesSinceRotateCheck = 0;
1398
+ dirReady = false;
1399
+ maxMemoryEntries;
1400
+ retryWindowMs;
1401
+ now;
1402
+ /**
1403
+ * Subscribe to the EventBus and seed the in-memory ring from the JSONL
1404
+ * tail. The returned promise resolves once the seed load finished
1405
+ * (best-effort — a missing/corrupt file is an empty history, not an error).
1406
+ */
1407
+ async start() {
1408
+ const { events } = this.opts;
1409
+ this.unsubscribers.push(
1410
+ events.on("brain.decision_answered", (e) => {
1411
+ this.record({
1412
+ at: e.at,
1413
+ sessionId: e.sessionId,
1414
+ requestId: e.request.id,
1415
+ kind: "answered",
1416
+ source: e.request.source,
1417
+ risk: e.request.risk,
1418
+ question: truncate(e.request.question, QUESTION_MAX),
1419
+ optionId: e.decision.type === "answer" ? e.decision.optionId : void 0,
1420
+ detail: e.decision.type === "answer" ? truncate(e.decision.rationale ?? e.decision.text, DETAIL_MAX) : void 0
1421
+ });
1422
+ }),
1423
+ events.on("brain.decision_denied", (e) => {
1424
+ this.record({
1425
+ at: e.at,
1426
+ sessionId: e.sessionId,
1427
+ requestId: e.request.id,
1428
+ kind: "denied",
1429
+ source: e.request.source,
1430
+ risk: e.request.risk,
1431
+ question: truncate(e.request.question, QUESTION_MAX),
1432
+ detail: e.decision.type === "deny" ? truncate(e.decision.reason, DETAIL_MAX) : void 0
1433
+ });
1434
+ }),
1435
+ events.on("brain.decision_ask_human", (e) => {
1436
+ this.record({
1437
+ at: e.at,
1438
+ sessionId: e.sessionId,
1439
+ requestId: e.request.id,
1440
+ kind: "ask_human",
1441
+ source: e.request.source,
1442
+ risk: e.request.risk,
1443
+ question: truncate(e.request.question, QUESTION_MAX)
1444
+ });
1445
+ }),
1446
+ events.on("brain.intervention", (e) => {
1447
+ this.record({
1448
+ at: e.at,
1449
+ sessionId: e.sessionId,
1450
+ requestId: e.request.id,
1451
+ kind: "intervention",
1452
+ source: e.request.source,
1453
+ risk: e.request.risk,
1454
+ question: truncate(`[${e.kind}] ${e.request.question}`, QUESTION_MAX),
1455
+ detail: e.intervened ? "steered" : "observed only"
1456
+ });
1457
+ if (!e.intervened) return;
1458
+ const prev = this.lastInterventionByKind.get(e.kind);
1459
+ if (prev && e.at - prev.at <= this.retryWindowMs) {
1460
+ this.recordOutcome(
1461
+ prev.requestId,
1462
+ "failure",
1463
+ `same signal (${e.kind}) re-triggered ${fmtAge(e.at - prev.at).replace(" ago", " later")}`,
1464
+ e.sessionId
1465
+ );
1466
+ }
1467
+ this.lastInterventionByKind.set(e.kind, { requestId: e.request.id, at: e.at });
1468
+ }),
1469
+ // ── Outcome correlation: budget extensions ────────────────────────
1470
+ events.on("subagent.budget_extended", (e) => {
1471
+ const requestId = `director-budget-${e.subagentId}-${e.kind}`;
1472
+ if (!this.entries.some((entry) => entry.requestId === requestId)) return;
1473
+ let set = this.pendingBudgetOutcomes.get(e.subagentId);
1474
+ if (!set) {
1475
+ set = /* @__PURE__ */ new Set();
1476
+ this.pendingBudgetOutcomes.set(e.subagentId, set);
1477
+ }
1478
+ set.add(requestId);
1479
+ }),
1480
+ events.on("subagent.task_completed", (e) => {
1481
+ const pending = this.pendingBudgetOutcomes.get(e.subagentId);
1482
+ if (!pending) return;
1483
+ this.pendingBudgetOutcomes.delete(e.subagentId);
1484
+ const outcome = e.status === "success" ? "success" : "failure";
1485
+ for (const requestId of pending) {
1486
+ this.recordOutcome(
1487
+ requestId,
1488
+ outcome,
1489
+ `budget-extended subagent's task ${e.status}`,
1490
+ e.sessionId
1491
+ );
1492
+ }
1493
+ })
1494
+ );
1495
+ await this.load();
1496
+ }
1497
+ /** Unsubscribe and drain pending writes. Safe to call twice. */
1498
+ async stop() {
1499
+ for (const off of this.unsubscribers) off();
1500
+ this.unsubscribers.length = 0;
1501
+ await this.writeChain.catch(() => {
1502
+ });
1503
+ }
1504
+ /**
1505
+ * Attach a real-world outcome to an earlier decision. Appended to the
1506
+ * JSONL, folded into future digests, and re-emitted as `brain.outcome`.
1507
+ */
1508
+ recordOutcome(requestId, outcome, detail, sessionId) {
1509
+ this.outcomeByRequest.set(requestId, outcome);
1510
+ this.record({
1511
+ at: this.now(),
1512
+ sessionId,
1513
+ requestId,
1514
+ kind: "outcome",
1515
+ outcome,
1516
+ detail: truncate(detail, DETAIL_MAX)
1517
+ });
1518
+ this.opts.events.emit("brain.outcome", {
1519
+ sessionId,
1520
+ requestId,
1521
+ outcome,
1522
+ detail,
1523
+ at: this.now()
1524
+ });
1525
+ }
1526
+ /** Last `n` ledger rows, oldest first. */
1527
+ tail(n) {
1528
+ return this.entries.slice(-n);
1529
+ }
1530
+ /**
1531
+ * Consecutive most-recent FAILURE outcomes among answered decisions of
1532
+ * this request's group. Decisions whose outcome is still unknown are
1533
+ * skipped (they neither extend nor break the streak); the first known
1534
+ * SUCCESS ends it. Powers the deterministic ledger guard: "the last K
1535
+ * times we approved this, it went wrong — stop approving".
1536
+ */
1537
+ failureStreakFor(request) {
1538
+ const key = brainDecisionKey(request.source, request.question);
1539
+ let streak = 0;
1540
+ for (let i = this.entries.length - 1; i >= 0; i--) {
1541
+ const e = this.entries[i];
1542
+ if (e?.kind !== "answered" || e.requestId === request.id || e.question === void 0 || brainDecisionKey(e.source, e.question) !== key) {
1543
+ continue;
1544
+ }
1545
+ const outcome = this.outcomeByRequest.get(e.requestId);
1546
+ if (outcome === void 0) continue;
1547
+ if (outcome === "success") break;
1548
+ streak += 1;
1549
+ }
1550
+ return streak;
1551
+ }
1552
+ /**
1553
+ * Short decision-history block for the LLM/council prompt: how similar
1554
+ * past decisions went and — when known — how they turned OUT. Returns
1555
+ * undefined when there is no relevant history (no prompt noise).
1556
+ */
1557
+ digestFor(request) {
1558
+ const key = brainDecisionKey(request.source, request.question);
1559
+ const related = this.entries.filter(
1560
+ (e) => (e.kind === "answered" || e.kind === "denied") && e.requestId !== request.id && e.question !== void 0 && brainDecisionKey(e.source, e.question) === key
1561
+ );
1562
+ if (related.length === 0) return void 0;
1563
+ const recent = related.slice(-5).reverse();
1564
+ const now = this.now();
1565
+ const lines = recent.map((e) => {
1566
+ const what = e.kind === "denied" ? "denied" : e.optionId ? `chose [${e.optionId}]` : "answered";
1567
+ const outcome = this.outcomeByRequest.get(e.requestId);
1568
+ const suffix = outcome === "failure" ? " \u2192 later FAILED" : outcome === "success" ? " \u2192 later succeeded" : "";
1569
+ return `- ${fmtAge(now - e.at)}: ${what}${suffix}`;
1570
+ });
1571
+ const approved = related.filter((e) => e.kind === "answered");
1572
+ const failedAfter = approved.filter(
1573
+ (e) => this.outcomeByRequest.get(e.requestId) === "failure"
1574
+ ).length;
1575
+ const summary = `Summary: ${related.length} similar decision(s); ${approved.length} answered${failedAfter > 0 ? ` (${failedAfter} later failed)` : ""}, ${related.length - approved.length} denied.`;
1576
+ return [...lines, summary].join("\n");
1577
+ }
1578
+ // ── Private ───────────────────────────────────────────────────────────
1579
+ record(entry) {
1580
+ this.entries.push(entry);
1581
+ if (this.entries.length > this.maxMemoryEntries) {
1582
+ const evictedRequestIds = /* @__PURE__ */ new Set();
1583
+ const evictCount = this.entries.length - this.maxMemoryEntries;
1584
+ const evicted = this.entries.splice(0, evictCount);
1585
+ for (const e of evicted) {
1586
+ if (e.requestId) evictedRequestIds.add(e.requestId);
1587
+ }
1588
+ if (evictedRequestIds.size > 0) {
1589
+ const remainingRequestIds = /* @__PURE__ */ new Set();
1590
+ for (const e of this.entries) {
1591
+ if (e.requestId) remainingRequestIds.add(e.requestId);
1592
+ }
1593
+ for (const id of evictedRequestIds) {
1594
+ if (!remainingRequestIds.has(id)) this.outcomeByRequest.delete(id);
1595
+ }
1596
+ }
1597
+ }
1598
+ this.append(entry);
1599
+ }
1600
+ append(entry) {
1601
+ let line;
1602
+ try {
1603
+ line = `${JSON.stringify(entry)}
1604
+ `;
1605
+ } catch {
1606
+ return;
1607
+ }
1608
+ const bytes = Buffer.byteLength(line, "utf8");
1609
+ if (this.pendingWriteCount >= _BrainDecisionLedger.MAX_PENDING_WRITES || bytes > _BrainDecisionLedger.MAX_PENDING_WRITE_BYTES || this.pendingWriteBytes + bytes > _BrainDecisionLedger.MAX_PENDING_WRITE_BYTES) {
1610
+ return;
1611
+ }
1612
+ this.pendingWriteCount += 1;
1613
+ this.pendingWriteBytes += bytes;
1614
+ this.writeChain = this.writeChain.then(async () => {
1615
+ if (!this.dirReady) {
1616
+ await mkdir(dirname(this.opts.filePath), { recursive: true });
1617
+ this.dirReady = true;
1618
+ }
1619
+ await this.maybeRotate();
1620
+ await appendFile(this.opts.filePath, line, { encoding: "utf8", mode: SECRET_FILE_MODE });
1621
+ }).catch(() => {
1622
+ }).finally(() => {
1623
+ this.pendingWriteCount = Math.max(0, this.pendingWriteCount - 1);
1624
+ this.pendingWriteBytes = Math.max(0, this.pendingWriteBytes - bytes);
1625
+ });
1626
+ }
1627
+ /**
1628
+ * Size-bounded rotation: past {@link MAX_FILE_BYTES} the file moves to a
1629
+ * single `.1` sibling (previous `.1` is overwritten), bounding disk to
1630
+ * ~2× the cap. Checked once per {@link ROTATE_CHECK_EVERY} writes; runs on
1631
+ * the write chain, so it never races an append. The ring `load()` reads
1632
+ * only the tail, so rotation never costs queryable history.
1633
+ */
1634
+ async maybeRotate() {
1635
+ this.writesSinceRotateCheck += 1;
1636
+ if (this.writesSinceRotateCheck < _BrainDecisionLedger.ROTATE_CHECK_EVERY) return;
1637
+ this.writesSinceRotateCheck = 0;
1638
+ try {
1639
+ const info = await stat(this.opts.filePath);
1640
+ if (info.size < _BrainDecisionLedger.MAX_FILE_BYTES) return;
1641
+ await rename(this.opts.filePath, `${this.opts.filePath}.1`);
1642
+ } catch {
1643
+ }
1644
+ }
1645
+ /** Seed the in-memory ring from the JSONL tail (cross-session learning). */
1646
+ async load() {
1647
+ try {
1648
+ await access(this.opts.filePath);
1649
+ } catch {
1650
+ return;
1651
+ }
1652
+ const seed = [];
1653
+ const input = createReadStream(this.opts.filePath, { encoding: "utf8" });
1654
+ const lines = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
1655
+ try {
1656
+ for await (const line of lines) {
1657
+ if (!line.trim()) continue;
1658
+ try {
1659
+ const parsed = JSON.parse(line);
1660
+ if (typeof parsed?.requestId === "string" && typeof parsed.at === "number") {
1661
+ seed.push(parsed);
1662
+ if (seed.length > this.maxMemoryEntries) seed.shift();
1663
+ }
1664
+ } catch {
1665
+ }
1666
+ }
1667
+ } catch {
1668
+ return;
1669
+ } finally {
1670
+ lines.close();
1671
+ input.destroy();
1672
+ }
1673
+ this.entries.unshift(...seed);
1674
+ if (this.entries.length > this.maxMemoryEntries) {
1675
+ this.entries.splice(0, this.entries.length - this.maxMemoryEntries);
1676
+ }
1677
+ for (const e of this.entries) {
1678
+ if (e.kind === "outcome" && e.outcome) this.outcomeByRequest.set(e.requestId, e.outcome);
1679
+ }
1680
+ }
1681
+ };
1682
+
1683
+ // src/coordination/dispatcher.ts
1684
+ var DEFAULT_DISPATCH_ROLE = "executor";
1685
+ var FALLBACK_DEFINITION = {
1686
+ config: { role: "unknown", name: "Unknown Agent" },
1687
+ budget: {},
1688
+ capability: { phase: "meta", summary: "", keywords: [] }
1689
+ };
1690
+ function normalize(text) {
1691
+ return ` ${text.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim()} `;
1692
+ }
1693
+ function scoreAgents(task, catalog = AGENT_CATALOG) {
1694
+ const haySet = new Set(normalize(task).split(/\s+/).filter(Boolean));
1695
+ const out = [];
1696
+ for (const def of Object.values(catalog)) {
1697
+ if (!def?.config?.role) continue;
1698
+ let score = 0;
1699
+ const matched = [];
1700
+ for (const kw of def.capability.keywords) {
1701
+ const needleWords = normalize(kw).split(/\s+/).filter(Boolean);
1702
+ const allPresent = needleWords.every((w) => haySet.has(w));
1703
+ if (allPresent) {
1704
+ score += needleWords.length;
1705
+ matched.push(kw);
1706
+ }
1707
+ }
1708
+ if (score > 0) {
1709
+ out.push({ role: def.config.role, name: def.config.name, score, matched });
1710
+ }
1711
+ }
1712
+ out.sort((a, b) => b.score - a.score);
1713
+ return out;
1714
+ }
1715
+ function heuristicConfidence(candidates) {
1716
+ if (candidates.length === 0) return 0;
1717
+ const top = candidates[0]?.score ?? 0;
1718
+ const second = candidates[1]?.score ?? 0;
1719
+ const strength = Math.min(1, top / 3);
1720
+ const margin = (top - second + 1) / (top + 1);
1721
+ return Math.min(1, strength * margin);
1722
+ }
1723
+ async function dispatchAgent(task, opts = {}) {
1724
+ const catalog = opts.catalog ?? AGENT_CATALOG;
1725
+ const threshold = opts.confidenceThreshold ?? 0.4;
1726
+ const maxCandidates = opts.maxCandidates ?? 6;
1727
+ const candidates = scoreAgents(task, catalog);
1728
+ const confidence = heuristicConfidence(candidates);
1729
+ const top = candidates[0];
1730
+ if (top && confidence >= threshold) {
1731
+ return {
1732
+ role: top.role,
1733
+ definition: catalog[top.role] ?? FALLBACK_DEFINITION,
1734
+ confidence,
1735
+ method: "heuristic",
1736
+ reason: `Matched keywords: ${top.matched.slice(0, 4).join(", ")}`,
1737
+ alternatives: candidates.slice(1, maxCandidates)
1738
+ };
1739
+ }
1740
+ if (opts.classifier) {
1741
+ const pool = (candidates.length > 0 ? candidates.slice(0, maxCandidates).map((c) => catalog[c.role] ?? FALLBACK_DEFINITION) : Object.values(catalog)).map((d) => ({
1742
+ role: d.config.role,
1743
+ name: d.config.name,
1744
+ summary: d.capability.summary
1745
+ }));
1746
+ try {
1747
+ const choice = await opts.classifier(task, pool);
1748
+ if (choice && catalog[choice.role]) {
1749
+ return {
1750
+ role: choice.role,
1751
+ definition: catalog[choice.role] ?? FALLBACK_DEFINITION,
1752
+ confidence: 1,
1753
+ method: "llm",
1754
+ reason: choice.reason ?? "Selected by LLM classifier",
1755
+ alternatives: candidates.slice(0, maxCandidates).filter((c) => c.role !== choice.role)
1756
+ };
1757
+ }
1758
+ } catch {
1759
+ }
1760
+ }
1761
+ if (top) {
1762
+ return {
1763
+ role: top.role,
1764
+ definition: catalog[top.role] ?? FALLBACK_DEFINITION,
1765
+ confidence,
1766
+ method: "heuristic",
1767
+ reason: `Weak match (${top.matched.slice(0, 3).join(", ") || "low signal"})`,
1768
+ alternatives: candidates.slice(1, maxCandidates)
1769
+ };
1770
+ }
1771
+ const fallbackRole = catalog[DEFAULT_DISPATCH_ROLE] ? DEFAULT_DISPATCH_ROLE : Object.keys(catalog)[0] ?? DEFAULT_DISPATCH_ROLE;
1772
+ return {
1773
+ role: fallbackRole,
1774
+ definition: catalog[fallbackRole] ?? FALLBACK_DEFINITION,
1775
+ confidence: 0,
1776
+ method: "fallback",
1777
+ reason: "No keyword signal; defaulting to the generalist Executor",
1778
+ alternatives: []
1779
+ };
1780
+ }
1781
+ function makeLLMClassifier(complete) {
1782
+ return async (task, candidates) => {
1783
+ const list = candidates.map((c, i) => `${i + 1}. ${c.role} \u2014 ${c.summary}`).join("\n");
1784
+ const prompt = renderInstructionTemplate(readBundledInstructionText("llm/agent-router.md"), {
1785
+ task,
1786
+ agents: list
1787
+ });
1788
+ const raw = (await complete(prompt)).trim();
1789
+ const match = raw.match(/\{[\s\S]*\}/);
1790
+ if (!match) return null;
1791
+ const parsed = safeParse(
1792
+ match[0]
1793
+ );
1794
+ if (!parsed.ok || !parsed.value || typeof parsed.value.role !== "string") return null;
1795
+ const role = parsed.value.role.trim();
1796
+ const valid = candidates.some((c) => c.role === role);
1797
+ if (!valid) return null;
1798
+ return {
1799
+ role,
1800
+ reason: typeof parsed.value.reason === "string" ? parsed.value.reason : void 0
1801
+ };
1802
+ };
1803
+ }
1804
+
1805
+ // src/coordination/subagent-nicknames.ts
1806
+ var NICKNAME_POOL = {
1807
+ // Physics & fundamental sciences
1808
+ "einstein": { name: "Einstein", domain: "physics" },
1809
+ "newton": { name: "Newton", domain: "physics" },
1810
+ "feynman": { name: "Feynman", domain: "physics" },
1811
+ "dirac": { name: "Dirac", domain: "physics" },
1812
+ "bohr": { name: "Bohr", domain: "physics" },
1813
+ "planck": { name: "Planck", domain: "physics" },
1814
+ "curie": { name: "Curie", domain: "physics" },
1815
+ "fermi": { name: "Fermi", domain: "physics" },
1816
+ "heisenberg": { name: "Heisenberg", domain: "physics" },
1817
+ "schrodinger": { name: "Schr\xF6dinger", domain: "physics" },
1818
+ // Mathematics
1819
+ "euclid": { name: "Euclid", domain: "math" },
1820
+ "gauss": { name: "Gauss", domain: "math" },
1821
+ "turing": { name: "Turing", domain: "math" },
1822
+ "poincare": { name: "Poincar\xE9", domain: "math" },
1823
+ "riemann": { name: "Riemann", domain: "math" },
1824
+ "hilbert": { name: "Hilbert", domain: "math" },
1825
+ "pythagoras": { name: "Pythagoras", domain: "math" },
1826
+ // Computing & information theory
1827
+ "von-neumann": { name: "Von Neumann", domain: "computing" },
1828
+ "shannon": { name: "Shannon", domain: "computing" },
1829
+ "hopper": { name: "Hopper", domain: "computing" },
1830
+ "backus": { name: "Backus", domain: "computing" },
1831
+ "knuth": { name: "Knuth", domain: "computing" },
1832
+ "torvalds": { name: "Torvalds", domain: "computing" },
1833
+ "stallman": { name: "Stallman", domain: "computing" },
1834
+ "berners-lee": { name: "Berners-Lee", domain: "computing" },
1835
+ "babbage": { name: "Babbage", domain: "computing" },
1836
+ "lovelace": { name: "Lovelace", domain: "computing" },
1837
+ "klein": { name: "Klein", domain: "computing" },
1838
+ // Electronics & electrical engineering
1839
+ "edison": { name: "Edison", domain: "ee" },
1840
+ "tesla": { name: "Tesla", domain: "ee" },
1841
+ "faraday": { name: "Faraday", domain: "ee" },
1842
+ "maxwell": { name: "Maxwell", domain: "ee" },
1843
+ "ohm": { name: "Ohm", domain: "ee" },
1844
+ "bell": { name: "Bell", domain: "ee" },
1845
+ "marconi": { name: "Marconi", domain: "ee" },
1846
+ "lamarr": { name: "Lamarr", domain: "ee" },
1847
+ // General science / multi-disciplinary
1848
+ "darwin": { name: "Darwin", domain: "biology" },
1849
+ "mendel": { name: "Mendel", domain: "biology" },
1850
+ "pasteur": { name: "Pasteur", domain: "biology" },
1851
+ "hawking": { name: "Hawking", domain: "cosmology" },
1852
+ "sagan": { name: "Sagan", domain: "cosmology" },
1853
+ // Exploration & navigation
1854
+ "columbus": { name: "Columbus", domain: "exploration" },
1855
+ "polo": { name: "Polo", domain: "exploration" },
1856
+ "magellan": { name: "Magellan", domain: "exploration" },
1857
+ // Chemistry / materials
1858
+ "lavoisier": { name: "Lavoisier", domain: "chemistry" },
1859
+ "mendeleev": { name: "Mendeleev", domain: "chemistry" }
1860
+ };
1861
+ var ALL_NICKNAMES = Object.entries(NICKNAME_POOL);
1862
+ var NAME_TO_KEY = Object.fromEntries(
1863
+ ALL_NICKNAMES.map(([key, entry]) => [entry.name, key])
1864
+ );
1865
+ var DOMAIN_PREFERENCES = {
1866
+ "security": ["shannon", "turing", "lamarr", "stallman"],
1867
+ "bug-hunter": ["darwin", "curie", "feynman", "fermi"],
1868
+ "refactor": ["gauss", "hilbert", "euclid", "planck"],
1869
+ "audit-log": ["sagan", "hawking", "poincare", "newton"],
1870
+ "planner": ["hilbert", "gauss", "turing", "euclid"],
1871
+ "researcher": ["sagan", "hawking", "darwin", "pasteur"],
1872
+ "explorer": ["marconi", "bell", "columbus", "polo"],
1873
+ "testing": ["pasteur", "curie", "fermi", "bohr"],
1874
+ "frontend": ["lovelace", "hopper", "babbage", "backus"],
1875
+ "backend": ["torvalds", "stallman", "von-neumann", "backus"],
1876
+ "database": ["turing", "shannon", "backus", "knuth"],
1877
+ "devops": ["tesla", "edison", "faraday", "bell"],
1878
+ "security-scanner": ["shannon", "turing", "lamarr", "stallman"],
1879
+ "refactor-planner": ["gauss", "hilbert", "planck", "newton"],
1880
+ "architect": ["von-neumann", "turing", "gauss", "hilbert"],
1881
+ "critic": ["einstein", "feynman", "dirac", "bohr"],
1882
+ "e2e": ["hopper", "bell", "marconi", "tesla"],
1883
+ "performance": ["knuth", "gauss", "planck", "feynman"],
1884
+ "chaos": ["tesla", "edison", "curie", "fermi"],
1885
+ "cost": ["ohm", "bell", "marconi", "tesla"],
1886
+ // default fallback
1887
+ "default": ["einstein", "newton", "curie", "tesla", "edison", "turing", "shannon", "hopper", "knuth", "stallman"]
1888
+ };
1889
+ function assignNickname(role, used) {
1890
+ const preferences = [
1891
+ ...DOMAIN_PREFERENCES[role] ?? [],
1892
+ ...DOMAIN_PREFERENCES["default"] ?? []
1893
+ ];
1894
+ for (const key of preferences) {
1895
+ const entry = NICKNAME_POOL[key];
1896
+ if (entry && !used.has(key)) {
1897
+ return { key, display: `${entry.name} (${formatRole(role)})` };
1898
+ }
1899
+ }
1900
+ for (const [key, entry] of ALL_NICKNAMES) {
1901
+ if (!used.has(key)) {
1902
+ return { key, display: `${entry.name} (${formatRole(role)})` };
1903
+ }
1904
+ }
1905
+ const counter = used.size + 1;
1906
+ return { key: `scientist-${counter}`, display: `Scientist #${counter} (${formatRole(role)})` };
1907
+ }
1908
+ function nicknameKeyFromDisplay(display) {
1909
+ const base = display.replace(/\s*\([^)]*\)\s*$/, "").trim();
1910
+ const key = NAME_TO_KEY[base];
1911
+ if (key) return key;
1912
+ const synthesized = base.match(/^Scientist #(\d+)$/);
1913
+ return synthesized ? `scientist-${synthesized[1]}` : void 0;
1914
+ }
1915
+ function formatRole(role) {
1916
+ return role.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1917
+ }
1918
+
1919
+ // src/coordination/fleet.ts
1920
+ function defineAgent(id, name, promptRole = id, provider) {
1921
+ return {
1922
+ id,
1923
+ name,
1924
+ role: id,
1925
+ prompt: agentPrompt(promptRole),
1926
+ ...provider === void 0 ? {} : { provider }
1927
+ };
1928
+ }
1929
+ var AUDIT_LOG_AGENT = defineAgent("audit-log", "Audit Log");
1930
+ var BUG_HUNTER_AGENT = defineAgent("bug-hunter", "Bug Hunter");
1931
+ var REFACTOR_PLANNER_AGENT = defineAgent("refactor-planner", "Refactor Planner");
1932
+ var SECURITY_SCANNER_AGENT = defineAgent("security-scanner", "Security Scanner");
1933
+ var SHADOW_AGENT = {
1934
+ ...defineAgent("shadow-agent", "Shadow"),
1935
+ skillNames: [...SHADOW_AGENT_SKILLS]
1936
+ };
1937
+ var EXPLORE_COMPANION_AGENT = {
1938
+ ...defineAgent("explore-companion", "Explore Companion"),
1939
+ tools: [...TOOLS.read, ...TOOLS.index],
1940
+ // Read-only, triple-enforced: allowlist has no write/bash, and the
1941
+ // disabled list blocks the escape hatches explicitly.
1942
+ disabledTools: [
1943
+ "write",
1944
+ "edit",
1945
+ "replace",
1946
+ "patch",
1947
+ "bash",
1948
+ "exec",
1949
+ "delegate",
1950
+ "spawn_subagent",
1951
+ "assign_task"
1952
+ ],
1953
+ skillNames: [...ROLE_SKILL_SETS["explore-companion"]],
1954
+ spawnBudgetExempt: true,
1955
+ // Findings travel via mailbox + submit_result, not the leader's stream.
1956
+ textStream: "silent",
1957
+ toolStream: "silent"
1958
+ };
1959
+ var CHAOS_MONKEY_AGENT = {
1960
+ ...defineAgent("chaos-monkey", "Chaos Monkey"),
1961
+ tools: [...TOOLS.build],
1962
+ skillNames: ["testing", "typescript-strict"],
1963
+ spawnBudgetExempt: true,
1964
+ // Run in the live checkout: mutation targets are usually freshly
1965
+ // written and uncommitted — a worktree spawned from HEAD would not
1966
+ // contain them and every mutant would drift. The mutation_test tool
1967
+ // honors this value as its default; callers can still override per
1968
+ // call via its `chaosWorktree` input when targets are committed and
1969
+ // isolation is wanted.
1970
+ worktree: "off",
1971
+ // Report travels via submit_result + final text, not the leader's stream.
1972
+ textStream: "silent",
1973
+ toolStream: "silent"
1974
+ };
1975
+ var CRITIC_AGENT = defineAgent("critic", "Critic");
1976
+ var GENERIC_AGENT = defineAgent("generic", "Generic Project Agent");
1977
+ function withDispatchMetadata(definition) {
1978
+ const summary = definition.capability?.summary?.trim();
1979
+ if (!summary) return definition.config;
1980
+ return {
1981
+ ...definition.config,
1982
+ dispatch: { summary, keywords: [...definition.capability.keywords ?? []] }
1983
+ };
1984
+ }
1985
+ var FLEET_ROSTER = {
1986
+ "audit-log": AUDIT_LOG_AGENT,
1987
+ "bug-hunter": BUG_HUNTER_AGENT,
1988
+ "refactor-planner": REFACTOR_PLANNER_AGENT,
1989
+ "security-scanner": SECURITY_SCANNER_AGENT,
1990
+ critic: CRITIC_AGENT,
1991
+ generic: GENERIC_AGENT,
1992
+ "shadow-agent": SHADOW_AGENT,
1993
+ "explore-companion": EXPLORE_COMPANION_AGENT,
1994
+ "chaos-monkey": CHAOS_MONKEY_AGENT,
1995
+ ...Object.fromEntries(
1996
+ ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, withDispatchMetadata(d)])
1997
+ )
1998
+ };
1999
+ var DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
2000
+ var FLEET_ROSTER_BUDGETS = {
2001
+ "audit-log": { timeoutMs: 3 * 60 * 60 * 1e3, maxIterations: 2500, maxToolCalls: 7500 },
2002
+ "bug-hunter": { timeoutMs: 5 * 60 * 60 * 1e3, maxIterations: 4e3, maxToolCalls: 1e4 },
2003
+ "refactor-planner": { timeoutMs: 4 * 60 * 60 * 1e3, maxIterations: 3e3, maxToolCalls: 9e3 },
2004
+ "security-scanner": { timeoutMs: 5 * 60 * 60 * 1e3, maxIterations: 4e3, maxToolCalls: 1e4 },
2005
+ critic: { timeoutMs: 3 * 60 * 60 * 1e3, maxIterations: 2e3, maxToolCalls: 6e3 },
2006
+ generic: { idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS, maxIterations: 2500, maxToolCalls: 7500 },
2007
+ "shadow-agent": {
2008
+ idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
2009
+ maxIterations: 1e3,
2010
+ maxToolCalls: 2500,
2011
+ // Monitoring passes routinely include a large system prompt plus fleet
2012
+ // context. 30k was below observed single-pass usage and therefore triggered
2013
+ // a post-response negotiation every run; 96k leaves headroom while the cost
2014
+ // and tool/iteration caps still bound runaway work.
2015
+ maxTokens: 96e3,
2016
+ maxCostUsd: 0.5
2017
+ },
2018
+ "explore-companion": {
2019
+ idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
2020
+ maxIterations: 3e3,
2021
+ maxToolCalls: 8e3,
2022
+ maxTokens: 96e3,
2023
+ maxCostUsd: 0.5
2024
+ },
2025
+ "chaos-monkey": {
2026
+ // A mutation pass is many short apply/run/restore cycles — per-mutant
2027
+ // work is tiny, but a large plan (25 mutants/file × N files) needs
2028
+ // headroom. Idle-based reaping covers a stalled pass.
2029
+ idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
2030
+ maxIterations: 2e3,
2031
+ maxToolCalls: 6e3,
2032
+ maxTokens: 96e3,
2033
+ maxCostUsd: 0.5
2034
+ },
2035
+ ...Object.fromEntries(
2036
+ ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.budget])
2037
+ )
2038
+ };
2039
+ var GENERIC_SUBAGENT_BUDGET = {
2040
+ idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
2041
+ maxIterations: 2500,
2042
+ maxToolCalls: 7500
2043
+ };
2044
+ function applyRosterBudget(cfg) {
2045
+ const roleBudget = cfg.role ? FLEET_ROSTER_BUDGETS[cfg.role] : void 0;
2046
+ const defaultBudget = roleBudget ?? (cfg.name ? GENERIC_SUBAGENT_BUDGET : void 0);
2047
+ if (!defaultBudget) return cfg;
2048
+ return {
2049
+ ...cfg,
2050
+ // Wall-clock cap is opt-in only: forward an explicit `cfg.timeoutMs`, but
2051
+ // do NOT impose the roster's historical multi-hour wall-clock default — it
2052
+ // killed agents that were still actively working. Reaping is idle-based.
2053
+ timeoutMs: cfg.timeoutMs,
2054
+ // Idle window is the default reaper. Resets on activity, so a long-but-
2055
+ // productive run is never killed; only a genuine stall is reaped.
2056
+ idleTimeoutMs: cfg.idleTimeoutMs ?? defaultBudget.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS,
2057
+ maxIterations: cfg.maxIterations ?? defaultBudget.maxIterations,
2058
+ maxToolCalls: cfg.maxToolCalls ?? defaultBudget.maxToolCalls,
2059
+ maxTokens: cfg.maxTokens ?? defaultBudget.maxTokens,
2060
+ maxCostUsd: cfg.maxCostUsd ?? defaultBudget.maxCostUsd
2061
+ };
2062
+ }
2063
+ var ALL_FLEET_AGENTS = Object.values(FLEET_ROSTER);
2064
+ var CLINE_AGENT = defineAgent("cline", "Cline", "acp-cline", "acp");
2065
+ var GEMINI_CLI_AGENT = defineAgent("gemini-cli", "Gemini CLI", "acp-gemini-cli", "acp");
2066
+ var COPILOT_AGENT = defineAgent("copilot", "GitHub Copilot", "acp-copilot", "acp");
2067
+ var OPENHANDS_AGENT = defineAgent("openhands", "OpenHands", "acp-openhands", "acp");
2068
+ var GOOSE_AGENT = defineAgent("goose", "Goose", "acp-goose", "acp");
2069
+ var ACP_AGENTS = [
2070
+ CLINE_AGENT,
2071
+ GEMINI_CLI_AGENT,
2072
+ COPILOT_AGENT,
2073
+ OPENHANDS_AGENT,
2074
+ GOOSE_AGENT
2075
+ ];
2076
+ var ACP_AGENT_BUDGET = {
2077
+ timeoutMs: 10 * 60 * 60 * 1e3,
2078
+ maxIterations: 8e3,
2079
+ maxToolCalls: 2e4
2080
+ };
2081
+ for (const agent of ACP_AGENTS) {
2082
+ FLEET_ROSTER_BUDGETS[agent.role] = { ...ACP_AGENT_BUDGET };
2083
+ }
2084
+ var FLEET_ROSTER_WITHACP = {
2085
+ ...FLEET_ROSTER,
2086
+ ...Object.fromEntries(ACP_AGENTS.map((a) => [a.role, a]))
2087
+ };
2088
+
2089
+ // src/coordination/multi-agent-coordinator.ts
2090
+ import { randomUUID } from "node:crypto";
2091
+ import { EventEmitter } from "node:events";
2092
+
2093
+ // src/coordination/coordinator/error-classifier.ts
2094
+ function classifySubagentError(err, hints = {}) {
2095
+ if (err instanceof AgentError && err.cause) {
2096
+ return classifySubagentError(err.cause, hints);
2097
+ }
2098
+ const cause = err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : void 0;
2099
+ if (ProviderError.isProviderError(err)) {
2100
+ const baseMessage2 = err.describe();
2101
+ return providerErrorToSubagentError(err, baseMessage2, cause);
2102
+ }
2103
+ const baseMessage = toErrorMessage(err);
2104
+ if (err instanceof BudgetExceededError) {
2105
+ const map = {
2106
+ iterations: "budget_iterations",
2107
+ tool_calls: "budget_tool_calls",
2108
+ tokens: "budget_tokens",
2109
+ cost: "budget_cost",
2110
+ timeout: "budget_timeout",
2111
+ idle_timeout: "budget_timeout"
2112
+ };
2113
+ return {
2114
+ kind: map[err.kind],
2115
+ message: baseMessage,
2116
+ retryable: false,
2117
+ cause
2118
+ };
2119
+ }
2120
+ if (hints.parentAborted) {
2121
+ return { kind: "aborted_by_parent", message: baseMessage, retryable: false, cause };
2122
+ }
2123
+ const lower = baseMessage.toLowerCase();
2124
+ if (/agent aborted$/i.test(baseMessage)) {
2125
+ return { kind: "aborted_by_parent", message: baseMessage, retryable: false, cause };
2126
+ }
2127
+ if (/agent exhausted iteration limit$/i.test(baseMessage)) {
2128
+ return { kind: "budget_iterations", message: baseMessage, retryable: false, cause };
2129
+ }
2130
+ if (/empty response/i.test(baseMessage)) {
2131
+ return { kind: "empty_response", message: baseMessage, retryable: false, cause };
2132
+ }
2133
+ if (/^tool failed: /i.test(baseMessage)) {
2134
+ return { kind: "tool_failed", message: baseMessage, retryable: false, cause };
2135
+ }
2136
+ if (lower.includes("bridge transport") || /bridge.*(closed|disconnect)/i.test(baseMessage)) {
2137
+ return { kind: "bridge_failed", message: baseMessage, retryable: false, cause };
2138
+ }
2139
+ if (/context length|max.*tokens?.*exceeded|prompt is too long/i.test(baseMessage)) {
2140
+ return { kind: "context_overflow", message: baseMessage, retryable: false, cause };
2141
+ }
2142
+ return { kind: "unknown", message: baseMessage, retryable: false, cause };
2143
+ }
2144
+ function providerErrorToSubagentError(err, message, cause) {
2145
+ switch (err.kind) {
2146
+ case "rate_limit":
2147
+ case "quota_exhausted":
2148
+ return {
2149
+ kind: "provider_rate_limit",
2150
+ message,
2151
+ retryable: true,
2152
+ backoffMs: err.body?.retryAfterMs ?? 5e3,
2153
+ cause
2154
+ };
2155
+ case "auth":
2156
+ return { kind: "provider_auth", message, retryable: false, cause };
2157
+ case "timeout":
2158
+ case "network":
2159
+ return { kind: "provider_timeout", message, retryable: true, cause };
2160
+ case "overloaded":
2161
+ case "server":
2162
+ case "stream_hang":
2163
+ return { kind: "provider_5xx", message, retryable: true, backoffMs: 3e3, cause };
2164
+ case "context_overflow":
2165
+ return { kind: "context_overflow", message, retryable: false, cause };
2166
+ case "invalid_request":
2167
+ case "content_filter":
2168
+ case "unknown":
2169
+ return { kind: "unknown", message, retryable: err.retryable, cause };
2170
+ }
2171
+ }
2172
+
2173
+ // src/coordination/multi-agent-timeout.ts
2174
+ async function executeSubagentWithTimeout({
2175
+ runner,
2176
+ task,
2177
+ ctx,
2178
+ budget,
2179
+ preemptFraction = TIMEOUT_PREEMPT_FRACTION,
2180
+ abortSubagent,
2181
+ currentSessionId,
2182
+ gracefulFinish
2183
+ }) {
2184
+ const initialTimeoutMs = budget.limits.timeoutMs;
2185
+ const idleLimitMs = budget.limits.idleTimeoutMs;
2186
+ if (initialTimeoutMs === void 0 && idleLimitMs === void 0) {
2187
+ return runner(task, ctx);
2188
+ }
2189
+ const start = Date.now();
2190
+ let timer = null;
2191
+ let PreemptState;
2192
+ ((PreemptState2) => {
2193
+ PreemptState2["ACTIVE"] = "active";
2194
+ PreemptState2["LOCKED"] = "locked";
2195
+ })(PreemptState || (PreemptState = {}));
2196
+ let preemptedCeiling = null;
2197
+ let preemptState = "active" /* ACTIVE */;
2198
+ let lastGrantActivityTs = -1;
2199
+ const timeoutPromise = new Promise((_, reject) => {
2200
+ const terminate = (kind, limit, used) => {
2201
+ abortSubagent(ctx.subagentId);
2202
+ reject(
2203
+ budget._events?.hasListenerFor("budget.threshold_reached") ? new Error(`subagent stopped: budget ${kind} (limit=${limit}, used=${used})`) : new BudgetExceededError(kind, limit, used)
2204
+ );
2205
+ };
2206
+ const armFor = (ms) => {
2207
+ if (timer) clearTimeout(timer);
2208
+ timer = setTimeout(onTick, Math.max(0, ms));
2209
+ };
2210
+ const scheduleNext = () => {
2211
+ const wallLimit = budget.limits.timeoutMs ?? initialTimeoutMs;
2212
+ const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : wallLimit - (Date.now() - start);
2213
+ const idleRemaining = idleLimitMs === void 0 || gracefulFinish !== void 0 && initialTimeoutMs !== void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
2214
+ const preemptRemaining = initialTimeoutMs === void 0 || preemptedCeiling === wallLimit || gracefulFinish !== void 0 ? Number.POSITIVE_INFINITY : wallLimit * preemptFraction - (Date.now() - start);
2215
+ const next = Math.min(wallRemaining, idleRemaining, preemptRemaining);
2216
+ if (!Number.isFinite(next)) {
2217
+ if (timer) {
2218
+ clearTimeout(timer);
2219
+ timer = null;
2220
+ }
2221
+ return;
2222
+ }
2223
+ armFor(Math.max(25, next));
2224
+ };
2225
+ const negotiateTimeout = async (used, limit) => {
2226
+ const handler = budget.onThreshold;
2227
+ if (!handler) return "stop";
2228
+ const result = handler({
2229
+ kind: "timeout",
2230
+ used,
2231
+ limit,
2232
+ requestDecision: () => {
2233
+ if (!budget._events?.hasListenerFor("budget.threshold_reached")) {
2234
+ return Promise.resolve("stop");
2235
+ }
2236
+ return new Promise((resolveDecision) => {
2237
+ let settled = false;
2238
+ const resolve = (d) => {
2239
+ if (settled) return;
2240
+ settled = true;
2241
+ resolveDecision(d);
2242
+ };
2243
+ const fallback = setTimeout(() => resolve("stop"), DECISION_TIMEOUT_MS);
2244
+ const sessionId = currentSessionId();
2245
+ budget._events?.emit("budget.threshold_reached", {
2246
+ ...sessionId ? { sessionId } : {},
2247
+ kind: "timeout",
2248
+ used,
2249
+ limit,
2250
+ timeoutMs: DECISION_TIMEOUT_MS,
2251
+ extend: (extra) => {
2252
+ clearTimeout(fallback);
2253
+ queueMicrotask(() => resolve({ extend: extra }));
2254
+ },
2255
+ deny: () => {
2256
+ clearTimeout(fallback);
2257
+ resolve("stop");
2258
+ }
2259
+ });
2260
+ });
2261
+ }
2262
+ });
2263
+ return typeof result === "string" ? result : await result;
2264
+ };
2265
+ const onTick = async () => {
2266
+ const elapsed = Date.now() - start;
2267
+ const wallLimit = initialTimeoutMs === void 0 ? void 0 : budget.limits.timeoutMs ?? initialTimeoutMs;
2268
+ const idleLimit = idleLimitMs === void 0 ? void 0 : budget.limits.idleTimeoutMs ?? idleLimitMs;
2269
+ const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
2270
+ const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
2271
+ if (idleExceeded && !wallExceeded) {
2272
+ if (gracefulFinish !== void 0 && initialTimeoutMs !== void 0) {
2273
+ scheduleNext();
2274
+ return;
2275
+ }
2276
+ const sessionId = currentSessionId();
2277
+ budget._events?.emit("budget.threshold_reached", {
2278
+ ...sessionId ? { sessionId } : {},
2279
+ kind: "idle_timeout",
2280
+ used: budget.idleMs(),
2281
+ limit: idleLimit ?? 0,
2282
+ timeoutMs: DECISION_TIMEOUT_MS,
2283
+ extend: () => {
2284
+ },
2285
+ deny: () => {
2286
+ }
2287
+ });
2288
+ abortSubagent(ctx.subagentId);
2289
+ reject(new BudgetExceededError("idle_timeout", idleLimit ?? 0, budget.idleMs()));
2290
+ return;
2291
+ }
2292
+ if (wallLimit !== void 0 && !wallExceeded && gracefulFinish === void 0 && budget.onThreshold && preemptState === "active" /* ACTIVE */ && elapsed >= wallLimit * preemptFraction) {
2293
+ const activityTs = Date.now() - budget.idleMs();
2294
+ if (activityTs <= lastGrantActivityTs) {
2295
+ preemptState = "locked" /* LOCKED */;
2296
+ preemptedCeiling = wallLimit;
2297
+ scheduleNext();
2298
+ return;
2299
+ }
2300
+ budget.setWatchdogNegotiation(wallLimit);
2301
+ try {
2302
+ const decision = await negotiateTimeout(elapsed, wallLimit);
2303
+ if (typeof decision !== "string" && decision.extend.timeoutMs !== void 0) {
2304
+ budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
2305
+ lastGrantActivityTs = Date.now() - budget.idleMs();
2306
+ preemptState = "active" /* ACTIVE */;
2307
+ preemptedCeiling = null;
2308
+ } else {
2309
+ preemptState = "locked" /* LOCKED */;
2310
+ preemptedCeiling = wallLimit;
2311
+ }
2312
+ } catch {
2313
+ preemptState = "locked" /* LOCKED */;
2314
+ preemptedCeiling = wallLimit;
2315
+ } finally {
2316
+ budget.clearWatchdogNegotiation();
2317
+ }
2318
+ scheduleNext();
2319
+ return;
2320
+ }
2321
+ if (!wallExceeded) {
2322
+ scheduleNext();
2323
+ return;
2324
+ }
2325
+ const limit = wallLimit ?? 0;
2326
+ if (gracefulFinish !== void 0) {
2327
+ if (!budget.graceGranted) {
2328
+ const reason = `wall-clock budget of ${Math.round(limit / 1e3)}s reached`;
2329
+ if (budget.notifyFinish(reason, { graceMs: gracefulFinish.graceMs })) {
2330
+ scheduleNext();
2331
+ return;
2332
+ }
2333
+ abortSubagent(ctx.subagentId);
2334
+ reject(new BudgetExceededError("timeout", limit, elapsed));
2335
+ return;
2336
+ } else {
2337
+ abortSubagent(ctx.subagentId);
2338
+ reject(new BudgetExceededError("timeout", limit, elapsed));
2339
+ return;
2340
+ }
2341
+ }
2342
+ if (!budget.onThreshold) {
2343
+ abortSubagent(ctx.subagentId);
2344
+ reject(new BudgetExceededError("timeout", limit, elapsed));
2345
+ return;
2346
+ }
2347
+ budget.setWatchdogNegotiation(limit);
2348
+ try {
2349
+ const decision = await negotiateTimeout(elapsed, limit);
2350
+ if (decision === "throw") {
2351
+ terminate("timeout", limit, elapsed);
2352
+ return;
2353
+ }
2354
+ if (decision === "continue") {
2355
+ preemptState = "locked" /* LOCKED */;
2356
+ preemptedCeiling = wallLimit;
2357
+ armFor(Math.max(1e3, limit));
2358
+ return;
2359
+ }
2360
+ if (decision === "stop") {
2361
+ terminate("timeout", limit, elapsed);
2362
+ return;
2363
+ }
2364
+ if (decision.extend.timeoutMs !== void 0) {
2365
+ budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
2366
+ lastGrantActivityTs = Date.now() - budget.idleMs();
2367
+ preemptState = "active" /* ACTIVE */;
2368
+ preemptedCeiling = null;
2369
+ scheduleNext();
2370
+ return;
2371
+ }
2372
+ terminate("timeout", limit, elapsed);
2373
+ return;
2374
+ } catch (err) {
2375
+ abortSubagent(ctx.subagentId);
2376
+ reject(
2377
+ err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
2378
+ );
2379
+ return;
2380
+ } finally {
2381
+ budget.clearWatchdogNegotiation();
2382
+ }
2383
+ };
2384
+ scheduleNext();
2385
+ });
2386
+ try {
2387
+ return await Promise.race([runner(task, ctx), timeoutPromise]);
2388
+ } finally {
2389
+ if (timer) clearTimeout(timer);
2390
+ }
2391
+ }
2392
+
2393
+ // src/coordination/multi-agent-coordinator.ts
2394
+ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends EventEmitter {
2395
+ coordinatorId;
2396
+ config;
2397
+ runner;
2398
+ sessionId;
2399
+ fleetBus;
2400
+ subagents = /* @__PURE__ */ new Map();
2401
+ /**
2402
+ * Base nickname keys already handed out this run (e.g. `einstein`, `tesla`).
2403
+ * Prevents two workers sharing a name. Direct `coordinator.spawn()` callers
2404
+ * (parallel/eternal engine, SDD parallel run) don't go through
2405
+ * `Director.spawn()` where nicknames are normally assigned, so the
2406
+ * coordinator upgrades placeholder names ("Executor", "slot-ab12cd", role
2407
+ * names) to memorable ones here — that's what surfaces in the fleet monitor.
2408
+ */
2409
+ usedNicknames = /* @__PURE__ */ new Set();
2410
+ /** Maps subagentId → nickname key (e.g. 'einstein'). Used to free the slot on remove(). */
2411
+ subagentNicknames = /* @__PURE__ */ new Map();
2412
+ pendingTasks = [];
2413
+ completedResults = [];
2414
+ /** Prevents completedResults from growing unbounded in long-running coordinators. */
2415
+ static MAX_COMPLETED_RESULTS = 2e5;
2416
+ /** Caps each subagent's retained task history (see assign()); bounds RAM + the recordCompletion lookup. */
2417
+ static MAX_SUBAGENT_TASK_HISTORY = 64;
2418
+ totalIterations = 0;
2419
+ inFlight = 0;
2420
+ /**
2421
+ * Subagents currently being stopped. Set on entry to `stop()`, cleared
2422
+ * once `recordCompletion` lands the terminal TaskResult. Used by
2423
+ * `runDispatched` and `findIdleSubagent` to refuse mid-flight dispatch
2424
+ * to a subagent the caller has already asked to terminate — closes the
2425
+ * assign+terminate race where a fresh task could land on a worker that
2426
+ * was about to be killed.
2427
+ */
2428
+ terminating = /* @__PURE__ */ new Set();
2429
+ constructor(config, options = {}) {
2430
+ super();
2431
+ this.setMaxListeners(0);
2432
+ this.coordinatorId = config.coordinatorId;
2433
+ this.config = config;
2434
+ this.runner = options.runner;
2435
+ this.sessionId = options.sessionId;
2436
+ }
2437
+ currentSessionId() {
2438
+ const value = typeof this.sessionId === "function" ? this.sessionId() : this.sessionId;
2439
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2440
+ }
2441
+ /**
2442
+ * Replace the runner after construction. Used when the runner depends
2443
+ * on infrastructure (e.g. FleetBus) that isn't available until after
2444
+ * the coordinator's owning Director is built.
2445
+ */
2446
+ setRunner(runner) {
2447
+ this.runner = runner;
2448
+ }
2449
+ /**
2450
+ * Wire a FleetBus for director-mode event emission. Call after the
2451
+ * FleetManager is constructed so the coordinator can emit lifecycle
2452
+ * events the TUI and monitoring tools subscribe to.
2453
+ */
2454
+ setFleetBus(fleet) {
2455
+ this.fleetBus = fleet;
2456
+ }
2457
+ /**
2458
+ * Change the in-flight dispatch ceiling at runtime. Lowering does NOT
2459
+ * preempt running tasks — already-dispatched subagents finish their
2460
+ * current task; only future dispatches respect the new cap. Raising
2461
+ * immediately tries to fill the freed slots from the pending queue.
2462
+ */
2463
+ setMaxConcurrent(n) {
2464
+ if (!Number.isFinite(n) || n < 1) {
2465
+ throw new Error(`maxConcurrent must be a finite integer >= 1, got ${n}`);
2466
+ }
2467
+ this.config.maxConcurrent = Math.floor(n);
2468
+ this.tryDispatchNext();
2469
+ }
2470
+ /**
2471
+ * Upgrade a placeholder/role-derived name to a memorable scientist nickname
2472
+ * (e.g. "Einstein (Executor)"). A name is treated as a placeholder when it is
2473
+ * empty, equals the role (case-insensitive), is a generic default
2474
+ * ("subagent"/"adhoc"/"generic"), or is an auto-generated `slot-…` id.
2475
+ * Explicit, human-chosen names — including nicknames already assigned by
2476
+ * `Director.spawn()` — are left untouched, so this never double-assigns.
2477
+ */
2478
+ withNickname(subagent, subagentId) {
2479
+ const role = subagent.role ?? "subagent";
2480
+ const name = subagent.name?.trim() ?? "";
2481
+ const isPlaceholder = name === "" || name.toLowerCase() === role.toLowerCase() || name === "subagent" || name === "adhoc" || name === "generic" || /^slot-/.test(name);
2482
+ if (!isPlaceholder) return subagent;
2483
+ const { key, display } = assignNickname(role, this.usedNicknames);
2484
+ this.usedNicknames.add(key);
2485
+ this.subagentNicknames.set(subagentId, key);
2486
+ return { ...subagent, name: display };
2487
+ }
2488
+ async spawn(subagent) {
2489
+ const id = subagent.id || randomUUID();
2490
+ const cfg = this.withNickname(subagent, id);
2491
+ if (this.subagents.has(id)) {
2492
+ throw new Error(`Subagent id "${id}" already exists \u2014 refusing to overwrite`);
2493
+ }
2494
+ const context = {
2495
+ subagentId: id,
2496
+ tasks: [],
2497
+ // Wired later by the caller via setSubagentBridge() once the
2498
+ // bidirectional bridge is created. Readers must null-check / use
2499
+ // hasParentBridge() — the type now reflects this.
2500
+ parentBridge: null,
2501
+ doneCondition: this.config.doneCondition,
2502
+ maxConcurrent: this.config.maxConcurrent ?? 16
2503
+ };
2504
+ this.subagents.set(id, {
2505
+ config: { ...cfg, id },
2506
+ context,
2507
+ status: "idle",
2508
+ abortController: new AbortController()
2509
+ });
2510
+ this.emit("subagent.started", { subagent: { ...cfg, id } });
2511
+ this.fleetBus?.emit({
2512
+ subagentId: id,
2513
+ ts: Date.now(),
2514
+ type: "subagent.assigned",
2515
+ payload: {
2516
+ subagentId: id,
2517
+ name: subagent.name,
2518
+ provider: subagent.provider,
2519
+ model: subagent.model
2520
+ }
2521
+ });
2522
+ this.emitCoordinatorStats();
2523
+ return { subagentId: id, agentId: id };
2524
+ }
2525
+ async assign(task) {
2526
+ this.pendingTasks.push(task);
2527
+ this.tryDispatchNext();
2528
+ }
2529
+ async delegate(to, msg) {
2530
+ const subagent = this.subagents.get(to);
2531
+ if (!subagent) throw new Error(`Subagent "${to}" not found`);
2532
+ if (!subagent.context.parentBridge) {
2533
+ throw new Error(`Subagent "${to}" has no parentBridge \u2014 call setSubagentBridge() first`);
2534
+ }
2535
+ await subagent.context.parentBridge.send(msg);
2536
+ }
2537
+ /**
2538
+ * Wire up the communication bridge for a subagent. Call after spawn() once
2539
+ * the caller has created the bidirectional connection.
2540
+ */
2541
+ setSubagentBridge(subagentId, bridge) {
2542
+ const subagent = this.subagents.get(subagentId);
2543
+ if (!subagent) throw new Error(`Subagent "${subagentId}" not found`);
2544
+ subagent.context.parentBridge = bridge;
2545
+ }
2546
+ async stop(subagentId) {
2547
+ const subagent = this.subagents.get(subagentId);
2548
+ if (!subagent) return;
2549
+ this.terminating.add(subagentId);
2550
+ subagent.abortController.abort();
2551
+ subagent.status = "stopped";
2552
+ subagent.currentTask = void 0;
2553
+ subagent.context.parentBridge = null;
2554
+ this.emit("subagent.stopped", { subagentId, reason: "stopped by coordinator" });
2555
+ this.fleetBus?.emit({
2556
+ subagentId,
2557
+ ts: Date.now(),
2558
+ type: "subagent.stopped",
2559
+ payload: { subagentId, reason: "stopped by coordinator" }
2560
+ });
2561
+ this.emitCoordinatorStats();
2562
+ }
2563
+ async stopAll() {
2564
+ this.drainPendingAsAborted("Coordinator stopAll() drained the pending queue");
2565
+ await Promise.allSettled([...this.subagents.keys()].map((id) => this.stop(id)));
2566
+ }
2567
+ /**
2568
+ * Get current coordinator stats for monitoring/debugging.
2569
+ */
2570
+ getStats() {
2571
+ let running = 0;
2572
+ let idle = 0;
2573
+ let stopped = 0;
2574
+ for (const [, entry] of this.subagents) {
2575
+ if (entry.status === "running") running++;
2576
+ else if (entry.status === "idle") idle++;
2577
+ else stopped++;
2578
+ }
2579
+ return {
2580
+ total: this.subagents.size,
2581
+ running,
2582
+ idle,
2583
+ stopped,
2584
+ inFlight: this.inFlight,
2585
+ pending: this.pendingTasks.length,
2586
+ completed: this.completedResults.length
2587
+ };
2588
+ }
2589
+ /** Emit a reactive coordinator.stats event on FleetBus so the TUI can subscribe. */
2590
+ emitCoordinatorStats() {
2591
+ const stats = this.getStats();
2592
+ const subagentStatuses = Array.from(this.subagents.entries()).map(([id, s]) => ({
2593
+ subagentId: id,
2594
+ taskId: s.currentTask ?? "",
2595
+ status: s.status,
2596
+ assigned: s.context.parentBridge !== null
2597
+ }));
2598
+ const sessionId = this.currentSessionId();
2599
+ this.fleetBus?.emit({
2600
+ subagentId: this.coordinatorId,
2601
+ ts: Date.now(),
2602
+ type: "coordinator.stats",
2603
+ payload: {
2604
+ ...sessionId ? { sessionId } : {},
2605
+ ...stats,
2606
+ subagentStatuses
2607
+ }
2608
+ });
2609
+ }
2610
+ getStatus() {
2611
+ return {
2612
+ coordinatorId: this.coordinatorId,
2613
+ subagents: Array.from(this.subagents.entries()).map(([id, s]) => ({
2614
+ id,
2615
+ name: s.config.name,
2616
+ status: s.status,
2617
+ currentTask: s.currentTask
2618
+ })),
2619
+ pendingTasks: this.pendingTasks.length,
2620
+ completedTasks: this.completedResults.length,
2621
+ totalIterations: this.totalIterations,
2622
+ done: this.isDone()
2623
+ };
2624
+ }
2625
+ /** Expose snapshot of completed results — useful for callers awaiting all done. */
2626
+ results() {
2627
+ return this.completedResults;
2628
+ }
2629
+ /** Defensive snapshot of the still-queued (not yet dispatched) tasks. */
2630
+ listPendingTasks() {
2631
+ return this.pendingTasks.map((t) => ({ ...t }));
2632
+ }
2633
+ /**
2634
+ * Re-pin a still-PENDING task to a different subagent (`undefined` =
2635
+ * unpin, any idle worker may take it), then try to dispatch. Returns
2636
+ * `false` when the task is not in the pending queue — already
2637
+ * dispatched, completed, or unknown. Running tasks can never be pulled;
2638
+ * they can only be steered or terminated. The mutation is synchronous
2639
+ * (same tick as the check), so it cannot race a concurrent dispatch:
2640
+ * `tryDispatchNext` runs on this same call stack or a later one.
2641
+ *
2642
+ * The task keeps its id, so waiters, the report-back notifier, and
2643
+ * checkpoint bookkeeping resolve unchanged when it eventually completes.
2644
+ */
2645
+ retargetPendingTask(taskId, subagentId) {
2646
+ const task = this.pendingTasks.find((t) => t.id === taskId);
2647
+ if (!task) return false;
2648
+ if (subagentId !== void 0 && !this.subagents.has(subagentId)) return false;
2649
+ task.subagentId = subagentId;
2650
+ this.tryDispatchNext();
2651
+ return true;
2652
+ }
2653
+ /**
2654
+ * Wait for one or more tasks to complete and return their results.
2655
+ * If a task is already done when called, returns immediately.
2656
+ * Resolves to an array in the same order as `taskIds`.
2657
+ */
2658
+ async awaitTasks(taskIds, opts) {
2659
+ const timeoutMs = opts?.timeoutMs ?? this.config.timeoutMs ?? 3e5;
2660
+ return Promise.all(
2661
+ taskIds.map((id) => {
2662
+ const cached = this.completedResults.find((r) => r.taskId === id);
2663
+ if (cached) return cached;
2664
+ return new Promise((resolve, reject) => {
2665
+ const timeout = setTimeout(() => {
2666
+ this.off("task.completed", handler);
2667
+ reject(new Error(`awaitTasks timed out waiting for task "${id}"`));
2668
+ }, timeoutMs);
2669
+ const handler = ({ result }) => {
2670
+ if (result.taskId === id) {
2671
+ clearTimeout(timeout);
2672
+ this.off("task.completed", handler);
2673
+ resolve(result);
2674
+ }
2675
+ };
2676
+ this.on("task.completed", handler);
2677
+ });
2678
+ })
2679
+ );
2680
+ }
2681
+ /**
2682
+ * Wait until AT LEAST ONE of the named tasks completes. Returns every
2683
+ * requested result already available at that moment (drain-what's-done)
2684
+ * plus the ids still outstanding, so callers can loop "handle finishers,
2685
+ * re-await the remainder" instead of blocking on the whole batch.
2686
+ *
2687
+ * Unlike `awaitTasks`, this never rejects and deliberately does NOT
2688
+ * inherit `config.timeoutMs` — a "return whatever is done" call has no
2689
+ * business timing out unless the caller asks for a window explicitly.
2690
+ */
2691
+ async awaitTasksAny(taskIds, opts) {
2692
+ const ids = new Set(taskIds);
2693
+ const completed = this.completedResults.filter((r) => ids.has(r.taskId));
2694
+ if (completed.length > 0 || ids.size === 0) {
2695
+ const done = new Set(completed.map((r) => r.taskId));
2696
+ return { completed, pending: taskIds.filter((id) => !done.has(id)) };
2697
+ }
2698
+ return new Promise((resolve) => {
2699
+ let timer;
2700
+ const handler = ({ result }) => {
2701
+ if (!ids.has(result.taskId)) return;
2702
+ if (timer) clearTimeout(timer);
2703
+ this.off("task.completed", handler);
2704
+ resolve({
2705
+ completed: [result],
2706
+ pending: taskIds.filter((id) => id !== result.taskId)
2707
+ });
2708
+ };
2709
+ if (opts?.timeoutMs !== void 0) {
2710
+ timer = setTimeout(() => {
2711
+ this.off("task.completed", handler);
2712
+ resolve({ completed: [], pending: [...taskIds], timedOut: true });
2713
+ }, opts.timeoutMs);
2714
+ }
2715
+ this.on("task.completed", handler);
2716
+ });
2717
+ }
2718
+ /**
2719
+ * Manual completion — for callers that drive subagents without a runner
2720
+ * (e.g. external orchestrators). When a runner is configured the coordinator
2721
+ * calls this itself.
2722
+ */
2723
+ completeTask(result) {
2724
+ this.recordCompletion(result);
2725
+ }
2726
+ /**
2727
+ * Ask every RUNNING subagent that opted into `gracefulFinish` to finish its
2728
+ * task in its own turn (see coordination/subagent-finish.ts). This is the
2729
+ * leader-side entry point for "the leader agent has finished": it delivers
2730
+ * an in-band notification between tool batches — never an interrupt, never
2731
+ * an abort. Each notified subagent keeps its existing time budget and
2732
+ * accelerates; the watchdog still bounds the maximum lifetime.
2733
+ *
2734
+ * Subagents without the policy opted in are deliberately untouched — their
2735
+ * lifecycle remains the legacy watchdog contract.
2736
+ *
2737
+ * Returns the number of subagents actually notified.
2738
+ */
2739
+ requestFinish(reason) {
2740
+ let notified = 0;
2741
+ for (const subagent of this.subagents.values()) {
2742
+ if (subagent.status !== "running") continue;
2743
+ if (!resolveGracefulFinish(subagent.config)) continue;
2744
+ const budget = subagent.activeBudget;
2745
+ if (!budget) continue;
2746
+ const usage = budget.usage();
2747
+ if (usage.iterations === 0 && usage.toolCalls === 0) continue;
2748
+ if (budget.notifyFinish(reason)) notified++;
2749
+ }
2750
+ return notified;
2751
+ }
2752
+ // --- internal dispatching ---------------------------------------------
2753
+ tryDispatchNext() {
2754
+ while (this.canDispatch()) {
2755
+ const dispatchable = this.takeNextDispatchableTask();
2756
+ if (!dispatchable) {
2757
+ if (this.pendingTasks.length > 0 && !this.hasLiveSubagent()) {
2758
+ this.drainPendingAsAborted(
2759
+ "No live subagent available \u2014 all stopped or mid-termination"
2760
+ );
2761
+ }
2762
+ return;
2763
+ }
2764
+ const { subagentId, task } = dispatchable;
2765
+ this.runDispatched(subagentId, task).catch((err) => {
2766
+ this.recordCompletion({
2767
+ subagentId,
2768
+ taskId: task.id,
2769
+ status: "failed",
2770
+ error: classifySubagentError(err),
2771
+ iterations: 0,
2772
+ toolCalls: 0,
2773
+ durationMs: 0
2774
+ });
2775
+ });
2776
+ }
2777
+ }
2778
+ canDispatch() {
2779
+ const max = this.config.maxConcurrent ?? 16;
2780
+ return this.inFlight < max && this.pendingTasks.length > 0;
2781
+ }
2782
+ takeNextDispatchableTask() {
2783
+ for (let i = 0; i < this.pendingTasks.length; i++) {
2784
+ const task = this.pendingTasks[i];
2785
+ if (!task) continue;
2786
+ const subagentId = task.subagentId ? this.isIdleSubagent(task.subagentId) ? task.subagentId : null : this.findIdleSubagent();
2787
+ if (!subagentId) continue;
2788
+ this.pendingTasks.splice(i, 1);
2789
+ return { subagentId, task };
2790
+ }
2791
+ return null;
2792
+ }
2793
+ findIdleSubagent() {
2794
+ for (const [id, s] of this.subagents) {
2795
+ if (s.status === "idle" && !this.terminating.has(id)) return id;
2796
+ }
2797
+ return null;
2798
+ }
2799
+ isIdleSubagent(id) {
2800
+ const subagent = this.subagents.get(id);
2801
+ return !!subagent && subagent.status === "idle" && !this.terminating.has(id);
2802
+ }
2803
+ /**
2804
+ * Returns true iff at least one spawned subagent could still
2805
+ * process a task. A "live" subagent is one that is not stopped
2806
+ * AND not mid-termination — `running` workers count because they
2807
+ * will eventually finish and become idle.
2808
+ *
2809
+ * When no subagent has ever been spawned, returns `true` so a
2810
+ * pre-spawn `assign()` simply queues (legacy behaviour). The
2811
+ * dead-end detection only fires after `stop()` has retired every
2812
+ * spawned worker.
2813
+ *
2814
+ * Used by `tryDispatchNext` to detect a dead-end pending queue.
2815
+ */
2816
+ hasLiveSubagent() {
2817
+ if (this.subagents.size === 0) return true;
2818
+ for (const [id, s] of this.subagents) {
2819
+ if (s.status !== "stopped" && !this.terminating.has(id)) return true;
2820
+ }
2821
+ return false;
2822
+ }
2823
+ /**
2824
+ * Drain every pending task with a synthetic `aborted_by_parent`
2825
+ * completion event. Same shape as the `stopAll()` drain — we go
2826
+ * around `recordCompletion` because pending tasks were never
2827
+ * counted in `inFlight` and routing them through would trip the
2828
+ * underflow guard on every task after the first.
2829
+ */
2830
+ drainPendingAsAborted(message) {
2831
+ const dropped = this.pendingTasks.splice(0, this.pendingTasks.length);
2832
+ for (const t of dropped) this.emitPendingAborted(t, message);
2833
+ }
2834
+ /**
2835
+ * Emit a synthetic `stopped`/`aborted_by_parent` completion for a single
2836
+ * PENDING task — one that was never counted in `inFlight`. This MUST bypass
2837
+ * `recordCompletion`: that path does `inFlight--`, which for a pending task
2838
+ * steals a decrement from a genuinely in-flight task and trips the underflow
2839
+ * guard — suppressing that real task's `task.completed` and hanging its
2840
+ * `awaitTasks()` caller. Pushes the result and fires the event directly.
2841
+ */
2842
+ emitPendingAborted(task, message) {
2843
+ const synthetic = {
2844
+ subagentId: task.subagentId ?? "unassigned",
2845
+ taskId: task.id,
2846
+ status: "stopped",
2847
+ error: {
2848
+ kind: "aborted_by_parent",
2849
+ message,
2850
+ retryable: false
2851
+ },
2852
+ iterations: 0,
2853
+ toolCalls: 0,
2854
+ durationMs: 0
2855
+ };
2856
+ this.completedResults.push(synthetic);
2857
+ this.trimCompletedResults();
2858
+ this.emit("task.completed", { task, result: synthetic });
2859
+ }
2860
+ trimCompletedResults() {
2861
+ if (this.completedResults.length > _DefaultMultiAgentCoordinator.MAX_COMPLETED_RESULTS) {
2862
+ this.completedResults.splice(
2863
+ 0,
2864
+ this.completedResults.length - _DefaultMultiAgentCoordinator.MAX_COMPLETED_RESULTS
2865
+ );
2866
+ }
2867
+ }
2868
+ async runDispatched(subagentId, task) {
2869
+ const subagent = this.subagents.get(subagentId);
2870
+ if (!subagent) return;
2871
+ if (this.terminating.has(subagentId) || subagent.status === "stopped") {
2872
+ this.recordCompletion({
2873
+ subagentId,
2874
+ taskId: task.id,
2875
+ status: "stopped",
2876
+ error: {
2877
+ kind: "aborted_by_parent",
2878
+ message: "Subagent was terminated before task could start",
2879
+ retryable: false
2880
+ },
2881
+ iterations: 0,
2882
+ toolCalls: 0,
2883
+ durationMs: 0
2884
+ });
2885
+ return;
2886
+ }
2887
+ subagent.status = "running";
2888
+ subagent.currentTask = task.id;
2889
+ task.subagentId = subagentId;
2890
+ subagent.context.tasks.push(task);
2891
+ if (subagent.context.tasks.length > _DefaultMultiAgentCoordinator.MAX_SUBAGENT_TASK_HISTORY) {
2892
+ subagent.context.tasks.splice(
2893
+ 0,
2894
+ subagent.context.tasks.length - _DefaultMultiAgentCoordinator.MAX_SUBAGENT_TASK_HISTORY
2895
+ );
2896
+ }
2897
+ this.fleetBus?.emit({
2898
+ subagentId,
2899
+ taskId: task.id,
2900
+ ts: Date.now(),
2901
+ type: "subagent.running",
2902
+ payload: { subagentId, taskId: task.id }
2903
+ });
2904
+ this.emit("task.assigned", { task, subagentId });
2905
+ this.emitCoordinatorStats();
2906
+ const rawMaxIterations = subagent.config.maxIterations;
2907
+ const rawMaxToolCalls = subagent.config.maxToolCalls;
2908
+ const rawMaxTokens = subagent.config.maxTokens;
2909
+ const rawMaxCostUsd = subagent.config.maxCostUsd;
2910
+ const rawTimeoutMs = subagent.config.timeoutMs;
2911
+ const rawIdleTimeoutMs = subagent.config.idleTimeoutMs;
2912
+ const configWithRosterDefaults = applyRosterBudget(subagent.config);
2913
+ const budget = new SubagentBudget(
2914
+ {
2915
+ maxIterations: rawMaxIterations ?? this.config.defaultBudget?.maxIterations ?? configWithRosterDefaults.maxIterations,
2916
+ maxToolCalls: rawMaxToolCalls ?? this.config.defaultBudget?.maxToolCalls ?? configWithRosterDefaults.maxToolCalls,
2917
+ maxTokens: rawMaxTokens ?? this.config.defaultBudget?.maxTokens ?? configWithRosterDefaults.maxTokens,
2918
+ maxCostUsd: rawMaxCostUsd ?? this.config.defaultBudget?.maxCostUsd ?? configWithRosterDefaults.maxCostUsd,
2919
+ // Wall-clock cap is opt-in (explicit config / defaultBudget only); the
2920
+ // roster no longer supplies one. Idle is the default reaper.
2921
+ timeoutMs: rawTimeoutMs ?? this.config.defaultBudget?.timeoutMs ?? configWithRosterDefaults.timeoutMs,
2922
+ idleTimeoutMs: rawIdleTimeoutMs ?? this.config.defaultBudget?.idleTimeoutMs ?? configWithRosterDefaults.idleTimeoutMs
2923
+ },
2924
+ "auto",
2925
+ {
2926
+ sessionId: () => this.currentSessionId(),
2927
+ subagentId,
2928
+ // Graceful-finish runs own wall-clock enforcement to the watchdog so
2929
+ // the notify-then-bound lifecycle cannot be raced by tool.progress
2930
+ // heartbeats calling checkTimeout() (see subagent-budget.ts).
2931
+ ...resolveGracefulFinish(subagent.config) ? { wallClockWatchdogOwned: true } : {}
2932
+ }
2933
+ );
2934
+ subagent.activeBudget = budget;
2935
+ if (!this.runner) {
2936
+ return;
2937
+ }
2938
+ this.inFlight++;
2939
+ const startTime = Date.now();
2940
+ let latestPartial;
2941
+ const runCtx = {
2942
+ subagentId,
2943
+ config: subagent.config,
2944
+ budget,
2945
+ signal: subagent.abortController.signal,
2946
+ bridge: subagent.context.parentBridge || null,
2947
+ reportProgress: (partial) => {
2948
+ const text = partial.text.trim();
2949
+ if (!text) return;
2950
+ latestPartial = {
2951
+ ...partial,
2952
+ // A partial is context recovery, not a transcript replacement.
2953
+ text: text.slice(-4e3)
2954
+ };
2955
+ }
2956
+ };
2957
+ let result;
2958
+ budget.start();
2959
+ try {
2960
+ const outcome = await this.executeWithTimeout(
2961
+ this.runner,
2962
+ task,
2963
+ runCtx,
2964
+ budget,
2965
+ subagent.config.preemptFraction,
2966
+ resolveGracefulFinish(subagent.config)
2967
+ );
2968
+ result = {
2969
+ subagentId,
2970
+ taskId: task.id,
2971
+ status: "success",
2972
+ result: outcome.result,
2973
+ ...outcome.report ? { report: outcome.report } : {},
2974
+ iterations: outcome.iterations,
2975
+ toolCalls: outcome.toolCalls,
2976
+ durationMs: Date.now() - startTime
2977
+ };
2978
+ } catch (err) {
2979
+ const status = err instanceof BudgetExceededError && (err.kind === "timeout" || err.kind === "idle_timeout") ? "timeout" : subagent.abortController.signal.aborted ? "stopped" : "failed";
2980
+ const usage = budget.usage();
2981
+ result = {
2982
+ subagentId,
2983
+ taskId: task.id,
2984
+ status,
2985
+ error: classifySubagentError(err, {
2986
+ parentAborted: subagent.abortController.signal.aborted
2987
+ }),
2988
+ ...latestPartial ? { partial: latestPartial } : {},
2989
+ iterations: usage.iterations,
2990
+ toolCalls: usage.toolCalls,
2991
+ durationMs: Date.now() - startTime
2992
+ };
2993
+ }
2994
+ this.recordCompletion(result);
2995
+ }
2996
+ async executeWithTimeout(runner, task, ctx, budget, preemptFraction, gracefulFinish) {
2997
+ return executeSubagentWithTimeout({
2998
+ runner,
2999
+ task,
3000
+ ctx,
3001
+ budget,
3002
+ preemptFraction,
3003
+ gracefulFinish,
3004
+ abortSubagent: (subagentId) => this.subagents.get(subagentId)?.abortController.abort(),
3005
+ currentSessionId: () => this.currentSessionId()
3006
+ });
3007
+ }
3008
+ recordCompletion(result) {
3009
+ this.completedResults.push(result);
3010
+ this.trimCompletedResults();
3011
+ this.totalIterations += result.iterations;
3012
+ if (this.inFlight > 0) {
3013
+ this.inFlight--;
3014
+ } else if (this.runner) {
3015
+ this.emit("warning", {
3016
+ type: "inFlight_underflow",
3017
+ taskId: result.taskId,
3018
+ subagentId: result.subagentId
3019
+ });
3020
+ return;
3021
+ }
3022
+ const subagent = this.subagents.get(result.subagentId);
3023
+ if (subagent && subagent.status !== "stopped") {
3024
+ const failed = result.status === "failed" || result.status === "timeout";
3025
+ subagent.status = "idle";
3026
+ void failed;
3027
+ subagent.currentTask = void 0;
3028
+ if (subagent.abortController.signal.aborted) {
3029
+ subagent.abortController = new AbortController();
3030
+ }
3031
+ this.fleetBus?.emit({
3032
+ subagentId: result.subagentId,
3033
+ ts: Date.now(),
3034
+ type: "subagent.idle",
3035
+ payload: { subagentId: result.subagentId }
3036
+ });
3037
+ }
3038
+ this.terminating.delete(result.subagentId);
3039
+ this.emit("task.completed", {
3040
+ task: subagent?.context.tasks.find((t) => t.id === result.taskId) ?? { id: result.taskId },
3041
+ result
3042
+ });
3043
+ this.fleetBus?.emit({
3044
+ subagentId: result.subagentId,
3045
+ taskId: result.taskId,
3046
+ ts: Date.now(),
3047
+ type: "subagent.completed",
3048
+ payload: {
3049
+ subagentId: result.subagentId,
3050
+ taskId: result.taskId,
3051
+ status: result.status,
3052
+ result: result.result,
3053
+ report: result.report,
3054
+ partial: result.partial,
3055
+ iterations: result.iterations,
3056
+ toolCalls: result.toolCalls,
3057
+ durationMs: result.durationMs
3058
+ }
3059
+ });
3060
+ this.tryDispatchNext();
3061
+ this.emitCoordinatorStats();
3062
+ if (this.isDone()) {
3063
+ this.emit("done", {
3064
+ results: this.completedResults,
3065
+ totalIterations: this.totalIterations
3066
+ });
3067
+ }
3068
+ }
3069
+ /**
3070
+ * Stop a subagent and remove it from the coordinator. Releases all
3071
+ * associated resources (AbortController, context, budget state).
3072
+ * The subagent entry is deleted so the id can be reused in a future spawn.
3073
+ */
3074
+ async remove(subagentId) {
3075
+ const subagent = this.subagents.get(subagentId);
3076
+ if (!subagent) return;
3077
+ if (subagent.status === "running" || subagent.status === "idle") {
3078
+ this.terminating.add(subagentId);
3079
+ subagent.abortController.abort();
3080
+ subagent.status = "stopped";
3081
+ }
3082
+ this.subagents.delete(subagentId);
3083
+ this.terminating.delete(subagentId);
3084
+ const nicknameKey = this.subagentNicknames.get(subagentId);
3085
+ if (nicknameKey) {
3086
+ this.usedNicknames.delete(nicknameKey);
3087
+ this.subagentNicknames.delete(subagentId);
3088
+ }
3089
+ const orphaned = this.pendingTasks.filter((t) => t.subagentId === subagentId);
3090
+ this.pendingTasks = this.pendingTasks.filter((t) => t.subagentId !== subagentId);
3091
+ for (const t of orphaned) {
3092
+ this.emitPendingAborted(
3093
+ t,
3094
+ `Subagent "${subagentId}" was removed while task "${t.id}" was pending`
3095
+ );
3096
+ }
3097
+ this.fleetBus?.emit({
3098
+ subagentId,
3099
+ ts: Date.now(),
3100
+ type: "subagent.removed",
3101
+ payload: { subagentId }
3102
+ });
3103
+ this.emitCoordinatorStats();
3104
+ }
3105
+ isDone() {
3106
+ if (this.config.doneCondition.type === "all_tasks_done") {
3107
+ return this.pendingTasks.length === 0 && this.inFlight === 0;
3108
+ }
3109
+ if (this.config.doneCondition.maxIterations !== void 0 && this.totalIterations >= this.config.doneCondition.maxIterations) {
3110
+ return true;
3111
+ }
3112
+ return false;
3113
+ }
3114
+ };
3115
+
3116
+ export {
3117
+ TIMEOUT_PREEMPT_FRACTION,
3118
+ DECISION_TIMEOUT_MS,
3119
+ BudgetExceededError,
3120
+ BudgetThresholdSignal,
3121
+ SubagentBudget,
3122
+ SUBAGENT_STRUCTURED_REPORT_META_KEY,
3123
+ MAX_SUBAGENT_STRUCTURED_REPORT_CHARS,
3124
+ normalizeSubagentStructuredReport,
3125
+ readSubagentStructuredReport,
3126
+ formatSubagentStructuredReport,
3127
+ makeSubagentResultTool,
3128
+ withDisabledToolFiltering,
3129
+ makeAgentSubagentRunner,
3130
+ BLOCKED_RESOLVED_MARKERS,
3131
+ COMPETING_ALTERNATIVE,
3132
+ isBlockedResolved,
3133
+ DEFAULT_BRAIN_HEURISTICS,
3134
+ compileResolutionMarkers,
3135
+ resolveBrainHeuristics,
3136
+ DETERMINISTIC_BRAIN_TIERS,
3137
+ markDecisionTier,
3138
+ readDecisionTier,
3139
+ isDeterministicTier,
3140
+ BrainTierCounter,
3141
+ BRAIN_RISK_LEVELS,
3142
+ ObservableBrainArbiter,
3143
+ BrainDecisionQueue,
3144
+ terminalPolicyDecision,
3145
+ EscalationRoutingBrainArbiter,
3146
+ HumanEscalatingBrainArbiter,
3147
+ DefaultBrainArbiter,
3148
+ formatHumanPrompt,
3149
+ brainDecisionKey,
3150
+ createLedgerGuardBrainArbiter,
3151
+ BrainDecisionLedger,
3152
+ DEFAULT_DISPATCH_ROLE,
3153
+ scoreAgents,
3154
+ dispatchAgent,
3155
+ makeLLMClassifier,
3156
+ assignNickname,
3157
+ nicknameKeyFromDisplay,
3158
+ AUDIT_LOG_AGENT,
3159
+ BUG_HUNTER_AGENT,
3160
+ REFACTOR_PLANNER_AGENT,
3161
+ SECURITY_SCANNER_AGENT,
3162
+ GENERIC_AGENT,
3163
+ FLEET_ROSTER,
3164
+ FLEET_ROSTER_BUDGETS,
3165
+ applyRosterBudget,
3166
+ ALL_FLEET_AGENTS,
3167
+ ACP_AGENTS,
3168
+ FLEET_ROSTER_WITHACP,
3169
+ DefaultMultiAgentCoordinator
3170
+ };
3171
+ //# sourceMappingURL=chunk-T3KGZBU5.js.map