@wrongstack/core 0.9.19 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +37 -33
  2. package/dist/{agent-bridge-DMVOX0cF.d.ts → agent-bridge-DBBGP5O4.d.ts} +1 -1
  3. package/dist/{agent-subagent-runner-C4qt9e5Y.d.ts → agent-subagent-runner-KwufTWwn.d.ts} +3 -4
  4. package/dist/compactor-D1RHFRmF.d.ts +41 -0
  5. package/dist/{config-CWva0qoL.d.ts → config-CJXBka2r.d.ts} +38 -2
  6. package/dist/{context-BRNbHmRM.d.ts → context-7u93AcGD.d.ts} +140 -0
  7. package/dist/coordination/index.d.ts +13 -13
  8. package/dist/coordination/index.js +786 -200
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/{default-config-Uzla85H-.d.ts → default-config-DEXI4jsl.d.ts} +10 -1
  11. package/dist/defaults/index.d.ts +24 -24
  12. package/dist/defaults/index.js +1612 -528
  13. package/dist/defaults/index.js.map +1 -1
  14. package/dist/{events-CiG9qUM_.d.ts → events-Bw1K5E6K.d.ts} +49 -1
  15. package/dist/execution/index.d.ts +46 -32
  16. package/dist/execution/index.js +663 -169
  17. package/dist/execution/index.js.map +1 -1
  18. package/dist/extension/index.d.ts +6 -6
  19. package/dist/extension/index.js.map +1 -1
  20. package/dist/goal-store-BeRsj7YX.d.ts +188 -0
  21. package/dist/{index-p95HQ22A.d.ts → index-DIt5TIaF.d.ts} +8 -8
  22. package/dist/{index-aizK8olO.d.ts → index-DgQWnZxP.d.ts} +11 -8
  23. package/dist/index.d.ts +338 -41
  24. package/dist/index.js +2747 -787
  25. package/dist/index.js.map +1 -1
  26. package/dist/infrastructure/index.d.ts +6 -6
  27. package/dist/infrastructure/index.js +36 -0
  28. package/dist/infrastructure/index.js.map +1 -1
  29. package/dist/kernel/index.d.ts +9 -9
  30. package/dist/kernel/index.js +57 -8
  31. package/dist/kernel/index.js.map +1 -1
  32. package/dist/{mcp-servers-BkVEqkRe.d.ts → mcp-servers-BgINZzuo.d.ts} +25 -3
  33. package/dist/models/index.d.ts +3 -3
  34. package/dist/models/index.js +164 -11
  35. package/dist/models/index.js.map +1 -1
  36. package/dist/models-registry-gwMAo6E3.d.ts +86 -0
  37. package/dist/{multi-agent-3ZnTB1aT.d.ts → multi-agent-C1O8EFzb.d.ts} +77 -24
  38. package/dist/{multi-agent-coordinator-bRaI_aD1.d.ts → multi-agent-coordinator-DoVSBFNx.d.ts} +20 -2
  39. package/dist/{null-fleet-bus-DKM3Iy9d.d.ts → null-fleet-bus-T-s3JaOt.d.ts} +413 -110
  40. package/dist/observability/index.d.ts +2 -2
  41. package/dist/{path-resolver-TcJfc29Y.d.ts → path-resolver-BfyK-I7w.d.ts} +2 -2
  42. package/dist/{permission-bPuzAy4x.d.ts → permission-B6sldrSp.d.ts} +1 -1
  43. package/dist/{permission-policy-BUQSutpl.d.ts → permission-policy-CtNscWOA.d.ts} +10 -9
  44. package/dist/{plan-templates-fkQTyz3U.d.ts → plan-templates-BWs6Je7j.d.ts} +8 -8
  45. package/dist/{provider-runner-BEpikbbN.d.ts → provider-runner-BmPe2RuU.d.ts} +3 -3
  46. package/dist/{retry-policy-BYkq0ugs.d.ts → retry-policy-KF18W4dg.d.ts} +1 -1
  47. package/dist/sdd/index.d.ts +9 -10
  48. package/dist/sdd/index.js +350 -122
  49. package/dist/sdd/index.js.map +1 -1
  50. package/dist/security/index.d.ts +65 -4
  51. package/dist/security/index.js +67 -19
  52. package/dist/security/index.js.map +1 -1
  53. package/dist/{selector-pox8abg0.d.ts → selector-DmXxpFyM.d.ts} +1 -1
  54. package/dist/{session-reader-CSWcb5Ga.d.ts → session-reader-CCSBfQmd.d.ts} +2 -2
  55. package/dist/skills/index.d.ts +1 -1
  56. package/dist/skills/index.js +1 -1
  57. package/dist/skills/index.js.map +1 -1
  58. package/dist/storage/index.d.ts +392 -8
  59. package/dist/storage/index.js +796 -40
  60. package/dist/storage/index.js.map +1 -1
  61. package/dist/{system-prompt-Bs-Wliab.d.ts → system-prompt-CM6zOhd2.d.ts} +19 -1
  62. package/dist/{tool-executor-Boo3dekH.d.ts → tool-executor-D7RxLtg4.d.ts} +9 -4
  63. package/dist/types/index.d.ts +16 -16
  64. package/dist/types/index.js +404 -14
  65. package/dist/types/index.js.map +1 -1
  66. package/dist/utils/index.d.ts +40 -8
  67. package/dist/utils/index.js +127 -57
  68. package/dist/utils/index.js.map +1 -1
  69. package/dist/{wstack-paths-BCgmTNlG.d.ts → wstack-paths-eMXnY1_X.d.ts} +8 -0
  70. package/package.json +1 -1
  71. package/dist/compactor-DVTKL7XD.d.ts +0 -23
  72. package/dist/goal-store-C7jcumEh.d.ts +0 -96
  73. package/dist/models-registry-OG_30xqZ.d.ts +0 -46
@@ -39,6 +39,54 @@ function estimateToolResultTokens(content) {
39
39
  function estimateTextTokens(text) {
40
40
  return RoughTokenEstimate(text);
41
41
  }
42
+ function estimateToolDefTokens(tool) {
43
+ return RoughTokenEstimate(tool.name) + RoughTokenEstimate(tool.description ?? "") + RoughTokenEstimate(JSON.stringify(tool.inputSchema));
44
+ }
45
+ function estimateRequestTokens(messages, systemPrompt, tools) {
46
+ let messagesTokens = 0;
47
+ if (typeof messages === "string") {
48
+ messagesTokens = RoughTokenEstimate(messages);
49
+ } else if (Array.isArray(messages)) {
50
+ for (const m of messages) {
51
+ if (typeof m === "object" && m !== null && "content" in m) {
52
+ const content = m.content;
53
+ if (typeof content === "string") {
54
+ messagesTokens += RoughTokenEstimate(content);
55
+ } else if (Array.isArray(content)) {
56
+ for (const b of content) {
57
+ if (typeof b === "object" && b !== null) {
58
+ if (b.type === "text") {
59
+ messagesTokens += RoughTokenEstimate(b.text);
60
+ } else {
61
+ messagesTokens += RoughTokenEstimate(JSON.stringify(b));
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ let systemTokens = 0;
70
+ if (typeof systemPrompt === "string") {
71
+ systemTokens = RoughTokenEstimate(systemPrompt);
72
+ } else if (Array.isArray(systemPrompt)) {
73
+ for (const b of systemPrompt) {
74
+ if (typeof b === "object" && b !== null && b.type === "text") {
75
+ systemTokens += RoughTokenEstimate(b.text);
76
+ }
77
+ }
78
+ }
79
+ let toolsTokens = 0;
80
+ for (const t of tools) {
81
+ toolsTokens += estimateToolDefTokens(t);
82
+ }
83
+ return {
84
+ messages: messagesTokens,
85
+ systemPrompt: systemTokens,
86
+ tools: toolsTokens,
87
+ total: messagesTokens + systemTokens + toolsTokens
88
+ };
89
+ }
42
90
 
43
91
  // src/utils/message-invariants.ts
44
92
  function repairToolUseAdjacency(messages) {
@@ -144,6 +192,7 @@ var HybridCompactor = class {
144
192
  }
145
193
  async compact(ctx, opts = {}) {
146
194
  const beforeTokens = this.estimateMessages(ctx.messages);
195
+ const beforeFull = this.estimateFullRequest(ctx);
147
196
  const reductions = [];
148
197
  const policy = readContextWindowPolicy(ctx);
149
198
  const preserveK = policy?.preserveK ?? this.preserveK;
@@ -159,9 +208,12 @@ var HybridCompactor = class {
159
208
  ctx.state.replaceMessages(repaired.messages);
160
209
  }
161
210
  const afterTokens = this.estimateMessages(ctx.messages);
211
+ const afterFull = this.estimateFullRequest(ctx);
162
212
  return {
163
213
  before: beforeTokens,
164
214
  after: afterTokens,
215
+ fullRequestTokensBefore: beforeFull,
216
+ fullRequestTokensAfter: afterFull,
165
217
  reductions,
166
218
  repaired: repaired.report.changed ? {
167
219
  removedToolUses: repaired.report.removedToolUses,
@@ -170,6 +222,14 @@ var HybridCompactor = class {
170
222
  } : void 0
171
223
  };
172
224
  }
225
+ /**
226
+ * Estimate the full API request token count: messages + systemPrompt + toolDefs.
227
+ * This is the accurate figure for context-window pressure monitoring.
228
+ */
229
+ estimateFullRequest(ctx) {
230
+ const breakdown = estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []);
231
+ return breakdown.total;
232
+ }
173
233
  eliseOldToolResults(ctx, preserveK = this.preserveK, eliseThreshold = this.eliseThreshold) {
174
234
  const messages = ctx.messages;
175
235
  let pairCount = 0;
@@ -315,8 +375,9 @@ var IntelligentCompactor = class {
315
375
  }
316
376
  async compact(ctx, opts = {}) {
317
377
  const beforeTokens = this.estimateTokens(ctx.messages);
378
+ const beforeFull = this.estimateFullRequest(ctx);
318
379
  const reductions = [];
319
- const load = beforeTokens / this.maxContext;
380
+ const load = beforeFull / this.maxContext;
320
381
  const aggressive = load >= this.hardThreshold ? true : opts.aggressive ?? load >= this.softThreshold;
321
382
  const saved1 = this.eliseOldToolResults(ctx);
322
383
  if (saved1 > 0) reductions.push({ phase: "elision", saved: saved1 });
@@ -330,9 +391,12 @@ var IntelligentCompactor = class {
330
391
  const repaired = repairToolUseAdjacency(ctx.messages);
331
392
  if (repaired.report.changed) ctx.state.replaceMessages(repaired.messages);
332
393
  const afterTokens = this.estimateTokens(ctx.messages);
394
+ const afterFull = this.estimateFullRequest(ctx);
333
395
  return {
334
396
  before: beforeTokens,
335
397
  after: afterTokens,
398
+ fullRequestTokensBefore: beforeFull,
399
+ fullRequestTokensAfter: afterFull,
336
400
  reductions,
337
401
  repaired: repaired.report.changed ? {
338
402
  removedToolUses: repaired.report.removedToolUses,
@@ -341,6 +405,14 @@ var IntelligentCompactor = class {
341
405
  } : void 0
342
406
  };
343
407
  }
408
+ /**
409
+ * Estimate the full API request token count: messages + systemPrompt + toolDefs.
410
+ * This is the accurate figure for context-window pressure monitoring.
411
+ */
412
+ estimateFullRequest(ctx) {
413
+ const breakdown = estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []);
414
+ return breakdown.total;
415
+ }
344
416
  async summarizeAncientTurns(ctx) {
345
417
  const messages = ctx.messages;
346
418
  const cutoff = Math.max(0, messages.length - this.preserveK * 2);
@@ -353,29 +425,26 @@ var IntelligentCompactor = class {
353
425
  try {
354
426
  summaryText = await this.callSummarizer(toSummarize, ctx);
355
427
  } catch {
356
- const toolNames = /* @__PURE__ */ new Set();
357
- const filePaths = /* @__PURE__ */ new Set();
358
- let userTurns = 0;
359
- let assistantTurns = 0;
428
+ const preservedMessages = [];
360
429
  for (const m of toSummarize) {
361
- if (m.role === "user") userTurns++;
362
- else if (m.role === "assistant") {
363
- assistantTurns++;
364
- if (Array.isArray(m.content)) {
365
- for (const b of m.content) {
366
- if (b.type === "tool_use") toolNames.add(b.name ?? "unknown");
367
- }
368
- }
430
+ if (m.role === "system") continue;
431
+ if (typeof m.content === "string") {
432
+ preservedMessages.push(m);
433
+ } else if (Array.isArray(m.content)) {
434
+ const textParts = m.content.filter(isTextBlock);
435
+ const toolParts = m.content.filter((b) => b.type === "tool_use" || b.type === "tool_result");
436
+ const next = {
437
+ role: m.role,
438
+ content: [
439
+ ...textParts,
440
+ ...toolParts.length > 0 ? [{ type: "text", text: `[${toolParts.length} tool call(s) omitted]` }] : []
441
+ ]
442
+ };
443
+ preservedMessages.push(next);
369
444
  }
370
- const text = typeof m.content === "string" ? m.content : "";
371
- const matches = text.matchAll(/(?:[\w.,\-/@]+\/)*[\w.,\-/@]+\.\w+/g);
372
- for (const m_ of matches) filePaths.add(m_[0]);
373
445
  }
374
- const parts = [`${toSummarize.length} turns (${userTurns} user, ${assistantTurns} assistant)`];
375
- if (toolNames.size > 0) parts.push(`tools: ${[...toolNames].join(", ")}`);
376
- if (filePaths.size > 0) parts.push(`files: ${[...filePaths].slice(0, 10).join(", ")}`);
377
- summaryText = parts.join(" | ");
378
- if (!summaryText) summaryText = `${toSummarize.length} earlier turns omitted`;
446
+ summaryText = preservedMessages.map((m) => `[${m.role}]: ${typeof m.content === "string" ? m.content : m.content.filter(isTextBlock).map((b) => b.text).join(" ")}`).join("\n");
447
+ if (!summaryText) summaryText = `${toSummarize.length} earlier turns (semantic content preserved)`;
379
448
  }
380
449
  const summaryMsg = {
381
450
  role: "system",
@@ -621,7 +690,7 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
621
690
  const res = await this.provider.complete(req, { signal: ac.signal });
622
691
  const textBlocks = res.content.filter(isTextBlock);
623
692
  raw = textBlocks.map((b) => b.text).join("\n").trim();
624
- } catch (err) {
693
+ } catch (_err) {
625
694
  return this.fallbackSelect(messages, effectiveBudget);
626
695
  }
627
696
  return this.parseSelectorOutput(raw, messages.length);
@@ -713,15 +782,24 @@ var SelectiveCompactor = class {
713
782
  }
714
783
  async compact(ctx, opts = {}) {
715
784
  const beforeTokens = this.estimateTokens(ctx.messages);
785
+ const beforeFull = this.estimateFullRequest(ctx);
716
786
  const reductions = [];
717
- const load = beforeTokens / this.maxContext;
787
+ const load = beforeFull / this.maxContext;
718
788
  const shouldCompact = load >= this.warnThreshold || opts.aggressive;
719
789
  if (!shouldCompact) {
720
790
  const saved = this.eliseOldToolResults(ctx);
721
791
  if (saved > 0) reductions.push({ phase: "elision", saved });
722
792
  const repair2 = this.repairProtocolAdjacency(ctx);
723
793
  const afterTokens2 = this.estimateTokens(ctx.messages);
724
- return { before: beforeTokens, after: afterTokens2, reductions, repaired: repair2 };
794
+ const afterFull2 = this.estimateFullRequest(ctx);
795
+ return {
796
+ before: beforeTokens,
797
+ after: afterTokens2,
798
+ fullRequestTokensBefore: beforeFull,
799
+ fullRequestTokensAfter: afterFull2,
800
+ reductions,
801
+ repaired: repair2
802
+ };
725
803
  }
726
804
  const savedElision = this.eliseOldToolResults(ctx);
727
805
  if (savedElision > 0) reductions.push({ phase: "elision", saved: savedElision });
@@ -733,7 +811,23 @@ var SelectiveCompactor = class {
733
811
  }
734
812
  const repair = this.repairProtocolAdjacency(ctx);
735
813
  const afterTokens = this.estimateTokens(ctx.messages);
736
- return { before: beforeTokens, after: afterTokens, reductions, repaired: repair };
814
+ const afterFull = this.estimateFullRequest(ctx);
815
+ return {
816
+ before: beforeTokens,
817
+ after: afterTokens,
818
+ fullRequestTokensBefore: beforeFull,
819
+ fullRequestTokensAfter: afterFull,
820
+ reductions,
821
+ repaired: repair
822
+ };
823
+ }
824
+ /**
825
+ * Estimate the full API request token count: messages + systemPrompt + toolDefs.
826
+ * This is the accurate figure for context-window pressure monitoring.
827
+ */
828
+ estimateFullRequest(ctx) {
829
+ const breakdown = estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []);
830
+ return breakdown.total;
737
831
  }
738
832
  repairProtocolAdjacency(ctx) {
739
833
  const repaired = repairToolUseAdjacency(ctx.messages);
@@ -1008,7 +1102,8 @@ var LEVEL_RANK = { warn: 0, soft: 1, hard: 2 };
1008
1102
  var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
1009
1103
  name = "AutoCompaction";
1010
1104
  compactor;
1011
- estimator;
1105
+ /** Deprecated. Kept for backward compat with tests that pass simpleEstimator. */
1106
+ _estimator;
1012
1107
  warnThreshold;
1013
1108
  softThreshold;
1014
1109
  hardThreshold;
@@ -1018,19 +1113,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
1018
1113
  events;
1019
1114
  failureMode;
1020
1115
  policyProvider;
1021
- /**
1022
- * Calibration factor applied to the estimator output. The estimator is now
1023
- * expected to already include messages + system prompt + tool definitions
1024
- * (see `estimateRequestTokens.total`), so no overhead boost is needed. Kept
1025
- * as a constant so a future calibration against real provider tokenization
1026
- * (BPE vs the chars/3.5 rough estimator) can dial this without touching the
1027
- * threshold-check math.
1028
- *
1029
- * Historical note: was 1.3 when the estimator only counted messages — that
1030
- * double-counted system+tools once the estimator was upgraded, firing
1031
- * compaction ~30% earlier than intended (e.g. real 56% load showed as 73%).
1032
- */
1033
- static OVERHEAD_FACTOR = 1;
1116
+ sessionBridge;
1034
1117
  /**
1035
1118
  * Once a compaction attempt reduces nothing (preserveK protects everything,
1036
1119
  * no oversized tool_results remain to elide), retrying on every iteration
@@ -1043,8 +1126,11 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
1043
1126
  lastNoopAttempt = null;
1044
1127
  /**
1045
1128
  * @param compactor Compactor to use for compaction.
1046
- * @param maxContext Provider's max context window in tokens.
1047
- * @param estimator Token estimation function.
1129
+ * @param maxContext Provider's max context window in tokens.
1130
+ * @param _estimator Deprecated parameter kept for backward compatibility.
1131
+ * The middleware now uses `estimateRequestTokens` internally
1132
+ * for accurate full-request token counting (messages +
1133
+ * systemPrompt + toolDefs).
1048
1134
  * @param thresholds Threshold fractions (0-1) of maxContext.
1049
1135
  * @param opts Optional behavior. By default, failures at the
1050
1136
  * hard threshold throw AGENT_CONTEXT_OVERFLOW so
@@ -1052,11 +1138,11 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
1052
1138
  * provider context overflow. Warn/soft failures
1053
1139
  * still emit compaction.failed and continue.
1054
1140
  */
1055
- constructor(compactor, maxContext, estimator, thresholds, optsOrAggressiveOn = {}, events) {
1141
+ constructor(compactor, maxContext, _estimator, thresholds, optsOrAggressiveOn = {}, events) {
1056
1142
  const opts = typeof optsOrAggressiveOn === "string" ? { aggressiveOn: optsOrAggressiveOn, events } : optsOrAggressiveOn;
1057
1143
  this.compactor = compactor;
1058
1144
  this._maxContext = maxContext;
1059
- this.estimator = estimator;
1145
+ this._estimator = _estimator;
1060
1146
  this.warnThreshold = thresholds.warn;
1061
1147
  this.softThreshold = thresholds.soft;
1062
1148
  this.hardThreshold = thresholds.hard;
@@ -1064,6 +1150,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
1064
1150
  this.events = opts.events;
1065
1151
  this.failureMode = opts.failureMode ?? "throw_on_hard";
1066
1152
  this.policyProvider = opts.policyProvider;
1153
+ this.sessionBridge = opts.sessionBridge;
1067
1154
  }
1068
1155
  /** Allow callers (e.g. model-switch in WebUI) to update the context window
1069
1156
  * denominator when the active model changes. */
@@ -1072,8 +1159,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
1072
1159
  }
1073
1160
  handler() {
1074
1161
  return async (ctx, next) => {
1075
- const rawTokens = this.estimator(ctx);
1076
- const tokens = Math.ceil(rawTokens * _AutoCompactionMiddleware.OVERHEAD_FACTOR);
1162
+ const tokens = this._estimator ? this._estimator(ctx) : estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []).total;
1077
1163
  const load = tokens / this._maxContext;
1078
1164
  const policy = this.policyProvider?.(ctx);
1079
1165
  const thresholds = policy?.thresholds ?? {
@@ -1108,7 +1194,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
1108
1194
  return delta < _AutoCompactionMiddleware.NOOP_RETRY_DELTA_TOKENS;
1109
1195
  }
1110
1196
  recordAttempt(level, tokens, report) {
1111
- const reduced = report.before > report.after;
1197
+ const reduced = (report.fullRequestTokensBefore ?? report.before) > (report.fullRequestTokensAfter ?? report.after);
1112
1198
  const repaired = !!report.repaired;
1113
1199
  if (reduced || repaired) {
1114
1200
  this.lastNoopAttempt = null;
@@ -1128,6 +1214,15 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
1128
1214
  report,
1129
1215
  aggressive
1130
1216
  });
1217
+ await this.sessionBridge?.append({
1218
+ type: "compaction",
1219
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1220
+ before: report.before,
1221
+ after: report.after,
1222
+ level: pressure.level,
1223
+ aggressive,
1224
+ reductions: report.reductions?.map((r) => ({ phase: r.phase, saved: r.saved }))
1225
+ });
1131
1226
  ctx.clearFileTracking();
1132
1227
  } catch (err) {
1133
1228
  const error = err instanceof Error ? err : new Error(String(err));
@@ -1158,6 +1253,141 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
1158
1253
  }
1159
1254
  };
1160
1255
 
1256
+ // src/security/capabilities.ts
1257
+ var ToolCapabilities = {
1258
+ /** Can execute arbitrary commands in the user's shell (the `bash` tool). */
1259
+ SHELL_ARBITRARY: "shell.arbitrary",
1260
+ /** Can write / modify / delete files inside the project. */
1261
+ FS_WRITE: "fs.write",
1262
+ /** Can write files outside the current project root (very high risk). */
1263
+ FS_WRITE_OUTSIDE_PROJECT: "fs.write.outside-project",
1264
+ /** Proxies tools from external MCP servers (unknown capability). */
1265
+ MCP_PROXY: "mcp.proxy",
1266
+ /** Can spawn or manage subagents / multi-agent tasks. */
1267
+ SUBAGENT_SPAWN: "subagent.spawn",
1268
+ /** Can mutate global or session configuration / trust state. */
1269
+ CONFIG_MUTATE: "config.mutate",
1270
+ /** Can install packages or run package managers with side effects. */
1271
+ PACKAGE_INSTALL: "package.install"
1272
+ };
1273
+ var DANGEROUS_FOR_SUBAGENTS = [
1274
+ ToolCapabilities.SHELL_ARBITRARY,
1275
+ ToolCapabilities.FS_WRITE,
1276
+ ToolCapabilities.FS_WRITE_OUTSIDE_PROJECT,
1277
+ ToolCapabilities.MCP_PROXY,
1278
+ ToolCapabilities.SUBAGENT_SPAWN,
1279
+ ToolCapabilities.CONFIG_MUTATE,
1280
+ ToolCapabilities.PACKAGE_INSTALL
1281
+ ];
1282
+ function hasDangerousCapabilityForSubagents(toolOrCaps) {
1283
+ if (!toolOrCaps) return false;
1284
+ const input = toolOrCaps;
1285
+ const caps = Array.isArray(input) ? input : input?.capabilities ?? [];
1286
+ return caps.some((c) => DANGEROUS_FOR_SUBAGENTS.includes(c));
1287
+ }
1288
+ function getDangerousCapabilities(toolOrCaps) {
1289
+ if (!toolOrCaps) return [];
1290
+ const input = toolOrCaps;
1291
+ const caps = Array.isArray(input) ? input : input?.capabilities ?? [];
1292
+ return caps.filter(
1293
+ (c) => DANGEROUS_FOR_SUBAGENTS.includes(c)
1294
+ );
1295
+ }
1296
+
1297
+ // src/utils/json-schema-validate.ts
1298
+ function validateAgainstSchema(value, schema) {
1299
+ const errors = [];
1300
+ walk(value, schema, "", errors);
1301
+ return { ok: errors.length === 0, errors };
1302
+ }
1303
+ function walk(value, schema, path4, errors) {
1304
+ if (schema.enum !== void 0) {
1305
+ if (!schema.enum.some((e) => deepEqual(e, value))) {
1306
+ errors.push({
1307
+ path: path4 || "<root>",
1308
+ message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
1309
+ });
1310
+ return;
1311
+ }
1312
+ }
1313
+ if (typeof schema.type === "string") {
1314
+ if (!checkType(value, schema.type)) {
1315
+ errors.push({
1316
+ path: path4 || "<root>",
1317
+ message: `expected ${schema.type}, got ${describeType(value)}`
1318
+ });
1319
+ return;
1320
+ }
1321
+ }
1322
+ if (schema.type === "object" && isPlainObject(value)) {
1323
+ const obj = value;
1324
+ for (const req of schema.required ?? []) {
1325
+ if (!(req in obj)) {
1326
+ errors.push({ path: joinPath(path4, req), message: "required property missing" });
1327
+ }
1328
+ }
1329
+ if (schema.properties) {
1330
+ for (const [key, subSchema] of Object.entries(schema.properties)) {
1331
+ if (key in obj) {
1332
+ walk(obj[key], subSchema, joinPath(path4, key), errors);
1333
+ }
1334
+ }
1335
+ }
1336
+ }
1337
+ if (schema.type === "array" && Array.isArray(value) && schema.items) {
1338
+ value.forEach((item, i) => walk(item, schema.items, `${path4}[${i}]`, errors));
1339
+ }
1340
+ }
1341
+ function checkType(value, type) {
1342
+ switch (type) {
1343
+ case "string":
1344
+ return typeof value === "string";
1345
+ case "number":
1346
+ return typeof value === "number" && !Number.isNaN(value);
1347
+ case "integer":
1348
+ return typeof value === "number" && Number.isInteger(value);
1349
+ case "boolean":
1350
+ return typeof value === "boolean";
1351
+ case "null":
1352
+ return value === null;
1353
+ case "array":
1354
+ return Array.isArray(value);
1355
+ case "object":
1356
+ return isPlainObject(value);
1357
+ default:
1358
+ return true;
1359
+ }
1360
+ }
1361
+ function isPlainObject(v) {
1362
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1363
+ }
1364
+ function describeType(v) {
1365
+ if (v === null) return "null";
1366
+ if (Array.isArray(v)) return "array";
1367
+ return typeof v;
1368
+ }
1369
+ function joinPath(parent, key) {
1370
+ if (!parent) return key;
1371
+ return `${parent}.${key}`;
1372
+ }
1373
+ function deepEqual(a, b) {
1374
+ if (a === b) return true;
1375
+ if (typeof a !== typeof b) return false;
1376
+ if (a === null || b === null) return a === b;
1377
+ if (Array.isArray(a) && Array.isArray(b)) {
1378
+ return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
1379
+ }
1380
+ if (typeof a === "object" && typeof b === "object") {
1381
+ const ak = Object.keys(a);
1382
+ const bk = Object.keys(b);
1383
+ if (ak.length !== bk.length) return false;
1384
+ return ak.every(
1385
+ (k) => deepEqual(a[k], b[k])
1386
+ );
1387
+ }
1388
+ return false;
1389
+ }
1390
+
1161
1391
  // src/utils/tool-output-serializer.ts
1162
1392
  function createToolOutputSerializer(opts = {}) {
1163
1393
  const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
@@ -1239,18 +1469,45 @@ var ToolExecutor = class {
1239
1469
  budget = this.decrementBudget(result, budget);
1240
1470
  return { result, tool, durationMs: Date.now() - start };
1241
1471
  }
1472
+ const validation = validateAgainstSchema(use.input, tool.inputSchema);
1473
+ if (!validation.ok) {
1474
+ const errorDetails = validation.errors.map((e) => ` - ${e.path || "input"}: ${e.message}`).join("\n");
1475
+ const result = {
1476
+ type: "tool_result",
1477
+ tool_use_id: use.id,
1478
+ content: `Invalid arguments for tool "${tool.name}".
1479
+
1480
+ Validation errors:
1481
+ ${errorDetails}
1482
+
1483
+ Please call the tool again with arguments that match its inputSchema. You can use the "tool-help" tool with name="${tool.name}" to see the exact expected schema.`,
1484
+ is_error: true
1485
+ };
1486
+ budget = this.decrementBudget(result, budget);
1487
+ return { result, tool, durationMs: Date.now() - start };
1488
+ }
1489
+ const toolDangerousCaps = getDangerousCapabilities(tool);
1490
+ if (toolDangerousCaps.length > 0) {
1491
+ if (this.opts.events) ;
1492
+ }
1242
1493
  if (hasMalformedArguments(use.input)) {
1243
1494
  const result = this.malformedInputResult(use, extractMalformedRaw(use.input));
1244
1495
  budget = this.decrementBudget(result, budget);
1245
1496
  return { result, tool, durationMs: Date.now() - start };
1246
1497
  }
1247
1498
  const decision = await this.opts.permissionPolicy.evaluate(tool, use.input, ctx);
1248
- if (decision.permission === "deny") {
1499
+ let effectivePermission = decision.permission;
1500
+ const policy = this.opts.permissionPolicy;
1501
+ const yolo = policy.getYolo?.() === true || policy.getForceAllYolo?.() === true;
1502
+ if (toolDangerousCaps.length > 0 && effectivePermission === "auto" && !yolo) {
1503
+ effectivePermission = "confirm";
1504
+ }
1505
+ if (effectivePermission === "deny") {
1249
1506
  const result = this.deniedResult(use, decision.reason);
1250
1507
  budget = this.decrementBudget(result, budget);
1251
1508
  return { result, tool, durationMs: Date.now() - start };
1252
1509
  }
1253
- if (decision.permission === "confirm") {
1510
+ if (effectivePermission === "confirm") {
1254
1511
  if (this.opts.confirmAwaiter) {
1255
1512
  const choice = await this.opts.confirmAwaiter(tool, use.input, use.id, tool.name);
1256
1513
  if (choice !== "yes" && choice !== "always") {
@@ -1275,10 +1532,13 @@ var ToolExecutor = class {
1275
1532
  return { result: pending, tool, durationMs: Date.now() - start };
1276
1533
  }
1277
1534
  }
1535
+ const toolCapsForAudit = hasDangerousCapabilityForSubagents(tool) ? tool.capabilities ?? [] : [];
1278
1536
  const span = this.opts.tracer?.startSpan(`tool.${tool.name}`, {
1279
1537
  "tool.name": tool.name,
1280
1538
  "tool.mutating": tool.mutating,
1281
- "tool.permission": tool.permission
1539
+ "tool.permission": tool.permission,
1540
+ "tool.capabilities": JSON.stringify(tool.capabilities ?? []),
1541
+ "tool.has_dangerous_capabilities": toolCapsForAudit.length > 0
1282
1542
  });
1283
1543
  try {
1284
1544
  const result = await this.executeTool(tool, use, ctx, budget);
@@ -2447,19 +2707,16 @@ var SubagentBudget = class _SubagentBudget {
2447
2707
  tokenOutput = 0;
2448
2708
  costUsd = 0;
2449
2709
  startTime = null;
2450
- _onThreshold;
2451
2710
  /**
2452
- * Tracks which budget kinds currently have an extension request
2453
- * in flight. While a kind is here, further `checkLimit` calls for the
2454
- * same kind are no-ops without this dedup, every `recordIteration`
2455
- * after the limit is reached spawns a fresh decision Promise (until
2456
- * the first one lands and patches limits), flooding the FleetBus
2457
- * with redundant threshold events. Cleared in `negotiateExtension`'s
2458
- * `finally`.
2711
+ * Timestamp of the most recent activity (iteration / tool call / token
2712
+ * usage / streamed progress). Drives the idle timeout reset by
2713
+ * `markActivity()`. Initialised to `start()` time so a never-active agent
2714
+ * still eventually trips its idle window.
2459
2715
  */
2460
- pendingExtensions = /* @__PURE__ */ new Set();
2716
+ lastActivityTime = null;
2717
+ _onThreshold;
2461
2718
  /**
2462
- * Hard cap on how long `negotiateExtension` waits for the coordinator to
2719
+ * Hard cap on how long `_negotiateExtension` waits for the coordinator to
2463
2720
  * respond before defaulting to 'stop'. Without this fallback an absent
2464
2721
  * or hung listener (Director not built / event filter detached mid-run)
2465
2722
  * leaves the budget over-limit and never enforces anything.
@@ -2500,6 +2757,24 @@ var SubagentBudget = class _SubagentBudget {
2500
2757
  }
2501
2758
  start() {
2502
2759
  this.startTime = Date.now();
2760
+ this.lastActivityTime = this.startTime;
2761
+ }
2762
+ /**
2763
+ * Reset the idle clock. Called on any sign of forward progress —
2764
+ * iterations, tool calls, token usage, and streamed tool/text progress —
2765
+ * so a long-but-productive subagent never trips its `idleTimeoutMs`.
2766
+ */
2767
+ markActivity() {
2768
+ this.lastActivityTime = Date.now();
2769
+ }
2770
+ /**
2771
+ * Milliseconds since the last activity signal. Returns 0 before `start()`
2772
+ * (nothing to measure yet). Used by the coordinator watchdog to decide
2773
+ * whether to re-arm (still active) or reap (genuinely idle).
2774
+ */
2775
+ idleMs() {
2776
+ const since = this.lastActivityTime ?? this.startTime;
2777
+ return since === null ? 0 : Date.now() - since;
2503
2778
  }
2504
2779
  /** Returns true if we're within 10% of any limit — useful for pre-flight checks. */
2505
2780
  isNearLimit() {
@@ -2521,38 +2796,88 @@ var SubagentBudget = class _SubagentBudget {
2521
2796
  * - `mode === 'auto'` + no listener → throw `BudgetExceededError` (hard stop; no one to ask)
2522
2797
  * - `mode === 'auto'` + listener → throw `BudgetThresholdSignal` with async decision promise
2523
2798
  */
2524
- checkLimit(kind, used, limit) {
2799
+ /**
2800
+ * Collects all exceeded budget kinds into a single NOOP-free negotiation.
2801
+ * Called by recordIteration / recordToolCall / recordUsage — each may call
2802
+ * this for its own kind. The first call starts the negotiation and stores
2803
+ * the Promise in _pendingNegotiation. Subsequent calls for DIFFERENT
2804
+ * kinds (while a negotiation is in flight) are NOOPs — they don't start
2805
+ * new conversations with the coordinator. This prevents an EventBus flood
2806
+ * when multiple budget kinds are exceeded simultaneously in one iteration.
2807
+ *
2808
+ * Returns the kinds that were found to be exceeded (for logging/debugging).
2809
+ */
2810
+ checkLimits(elapsedMs) {
2811
+ const exceeded = [];
2812
+ if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
2813
+ exceeded.push({ kind: "iterations", used: this.iterations, limit: this.limits.maxIterations });
2814
+ }
2815
+ if (this.limits.maxToolCalls !== void 0 && this.toolCalls > this.limits.maxToolCalls) {
2816
+ exceeded.push({ kind: "tool_calls", used: this.toolCalls, limit: this.limits.maxToolCalls });
2817
+ }
2818
+ const totalTokens = this.tokenInput + this.tokenOutput;
2819
+ if (this.limits.maxTokens !== void 0 && totalTokens > this.limits.maxTokens) {
2820
+ exceeded.push({ kind: "tokens", used: totalTokens, limit: this.limits.maxTokens });
2821
+ }
2822
+ if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
2823
+ exceeded.push({ kind: "cost", used: this.costUsd, limit: this.limits.maxCostUsd });
2824
+ }
2825
+ if (elapsedMs !== void 0) {
2826
+ const idle = this.idleMs();
2827
+ if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
2828
+ exceeded.push({ kind: "timeout", used: idle, limit: this.limits.idleTimeoutMs });
2829
+ } else if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs) {
2830
+ exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
2831
+ }
2832
+ }
2833
+ if (exceeded.length === 0) return [];
2525
2834
  if (!this._onThreshold) {
2526
- throw new BudgetExceededError(kind, limit, used);
2835
+ const first2 = exceeded[0];
2836
+ throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
2527
2837
  }
2528
2838
  if (this._mode === "sync") {
2529
- throw new BudgetExceededError(kind, limit, used);
2839
+ const first2 = exceeded[0];
2840
+ throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
2530
2841
  }
2531
2842
  const bus = this._events;
2532
2843
  if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
2533
- throw new BudgetExceededError(kind, limit, used);
2844
+ const first2 = exceeded[0];
2845
+ throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
2846
+ }
2847
+ for (const entry of exceeded) {
2848
+ if (this._pendingNegotiations.has(entry.kind)) continue;
2849
+ const decision2 = this._negotiateExtension(entry.kind, exceeded);
2850
+ this._pendingNegotiations.set(entry.kind, decision2);
2534
2851
  }
2535
- if (this.pendingExtensions.has(kind)) return;
2536
- this.pendingExtensions.add(kind);
2537
- const decision = this.negotiateExtension(kind, used, limit);
2538
- throw new BudgetThresholdSignal(kind, limit, used, decision);
2852
+ const first = exceeded[0];
2853
+ const decision = this._pendingNegotiations.get(first.kind);
2854
+ throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
2539
2855
  }
2856
+ /**
2857
+ * Per-kind in-flight negotiation Promises. Each budget kind can have its
2858
+ * own concurrent negotiation — e.g. iterations and timeout can both
2859
+ * be exceeded simultaneously without blocking each other. The same kind
2860
+ * cannot start two concurrent negotiations (dedup guard).
2861
+ * Cleared in `_negotiateExtension`'s `finally` block.
2862
+ */
2863
+ _pendingNegotiations = /* @__PURE__ */ new Map();
2540
2864
  /**
2541
2865
  * Drive the threshold handler to a decision. Resolves with `'stop'`
2542
2866
  * (signal the runner to abort) or `{ extend: ... }` (limits already
2543
- * patched in-place; the runner should not abort). Always releases the
2544
- * `pendingExtensions` slot in `finally`.
2867
+ * patched in-place; the runner should not abort). Clears the
2868
+ * per-kind slot in `_pendingNegotiations` in `finally`.
2545
2869
  *
2546
2870
  * The 'continue' return from a sync handler is treated as
2547
2871
  * `{ extend: {} }` — keep going without patching; next overrun fires
2548
2872
  * a fresh signal.
2549
2873
  */
2550
- async negotiateExtension(kind, used, limit) {
2874
+ async _negotiateExtension(kind, exceeded) {
2551
2875
  try {
2876
+ const first = exceeded[0];
2552
2877
  const result = this._onThreshold({
2553
- kind,
2554
- used,
2555
- limit,
2878
+ kind: first.kind,
2879
+ used: first.used,
2880
+ limit: first.limit,
2556
2881
  requestDecision: () => {
2557
2882
  const bus = this._events;
2558
2883
  if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
@@ -2569,20 +2894,22 @@ var SubagentBudget = class _SubagentBudget {
2569
2894
  () => respond("stop"),
2570
2895
  _SubagentBudget.DECISION_TIMEOUT_MS
2571
2896
  );
2572
- bus.emit("budget.threshold_reached", {
2573
- kind,
2574
- used,
2575
- limit,
2576
- timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
2577
- extend: (extra) => {
2578
- clearTimeout(fallback);
2579
- respond({ extend: extra });
2580
- },
2581
- deny: () => {
2582
- clearTimeout(fallback);
2583
- respond("stop");
2584
- }
2585
- });
2897
+ for (const { kind: kind2, used, limit } of exceeded) {
2898
+ bus.emit("budget.threshold_reached", {
2899
+ kind: kind2,
2900
+ used,
2901
+ limit,
2902
+ timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
2903
+ extend: (extra) => {
2904
+ clearTimeout(fallback);
2905
+ respond({ extend: extra });
2906
+ },
2907
+ deny: () => {
2908
+ clearTimeout(fallback);
2909
+ respond("stop");
2910
+ }
2911
+ });
2912
+ }
2586
2913
  });
2587
2914
  }
2588
2915
  });
@@ -2606,54 +2933,59 @@ var SubagentBudget = class _SubagentBudget {
2606
2933
  if (ext.timeoutMs !== void 0) {
2607
2934
  this.limits.timeoutMs = ext.timeoutMs;
2608
2935
  }
2936
+ if (ext.idleTimeoutMs !== void 0) {
2937
+ this.limits.idleTimeoutMs = ext.idleTimeoutMs;
2938
+ }
2609
2939
  return decision;
2610
2940
  } finally {
2611
- this.pendingExtensions.delete(kind);
2941
+ this._pendingNegotiations.delete(kind);
2612
2942
  }
2613
2943
  }
2614
2944
  recordIteration() {
2615
2945
  this.iterations++;
2616
- if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
2617
- void this.checkLimit("iterations", this.iterations, this.limits.maxIterations);
2618
- }
2946
+ this.markActivity();
2947
+ void this.checkLimits();
2619
2948
  }
2620
2949
  recordToolCall() {
2621
2950
  this.toolCalls++;
2622
- if (this.limits.maxToolCalls !== void 0 && this.toolCalls > this.limits.maxToolCalls) {
2623
- void this.checkLimit("tool_calls", this.toolCalls, this.limits.maxToolCalls);
2624
- }
2951
+ this.markActivity();
2952
+ void this.checkLimits();
2625
2953
  }
2626
2954
  recordUsage(usage, costUsd = 0) {
2627
2955
  this.tokenInput += usage.input;
2628
2956
  this.tokenOutput += usage.output;
2629
2957
  this.costUsd += costUsd;
2630
- const totalTokens = this.tokenInput + this.tokenOutput;
2631
- if (this.limits.maxTokens !== void 0 && totalTokens > this.limits.maxTokens) {
2632
- void this.checkLimit("tokens", totalTokens, this.limits.maxTokens);
2633
- }
2634
- if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
2635
- void this.checkLimit("cost", this.costUsd, this.limits.maxCostUsd);
2636
- }
2958
+ this.markActivity();
2959
+ void this.checkLimits();
2637
2960
  }
2638
2961
  /**
2639
- * Wall-clock budget check. Unlike other limits, timeout check passes through
2640
- * `checkLimit` and is subject to the same negotiation-mode decision table.
2641
- * In practice, `'sync'` mode (the usual test configuration) means a timeout
2642
- * immediately throws `BudgetExceededError`. In production with a coordinator,
2643
- * a timeout emits `budget.threshold_reached` so the Director can decide whether
2644
- * to extend or abort.
2962
+ * Wall-clock budget check. Unlike other limits, timeout is always a hard stop
2963
+ * wall-clock time cannot be "extended" by the coordinator, so it throws
2964
+ * synchronously rather than entering the negotiation flow.
2965
+ *
2966
+ * Decision table:
2967
+ * - no `onThreshold` handler → throw `BudgetExceededError`
2968
+ * - `mode === 'sync'` → throw `BudgetExceededError`
2969
+ * - `mode === 'auto'` + no listener → throw `BudgetExceededError`
2970
+ * - `mode === 'auto'` + listener → throw `BudgetExceededError` (timeout is not extendable)
2645
2971
  */
2646
2972
  checkTimeout() {
2647
- if (this.startTime === null || this.limits.timeoutMs === void 0) return;
2973
+ if (this.startTime === null) return;
2974
+ const { timeoutMs, idleTimeoutMs } = this.limits;
2975
+ if (timeoutMs === void 0 && idleTimeoutMs === void 0) return;
2648
2976
  const elapsed = Date.now() - this.startTime;
2649
- if (elapsed > this.limits.timeoutMs) {
2650
- void this.checkLimit("timeout", elapsed, this.limits.timeoutMs);
2651
- }
2977
+ const wallTripped = timeoutMs !== void 0 && elapsed > timeoutMs;
2978
+ const idleTripped = idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs;
2979
+ if (!wallTripped && !idleTripped) return;
2980
+ void this.checkLimits(elapsed);
2652
2981
  }
2653
- /** Returns true if a timeout has occurred without throwing. Useful for races. */
2982
+ /** Returns true if a wall-clock or idle timeout has occurred without throwing. */
2654
2983
  isTimedOut() {
2655
- if (this.startTime === null || this.limits.timeoutMs === void 0) return false;
2656
- return Date.now() - this.startTime > this.limits.timeoutMs;
2984
+ if (this.startTime === null) return false;
2985
+ const { timeoutMs, idleTimeoutMs } = this.limits;
2986
+ if (timeoutMs !== void 0 && Date.now() - this.startTime > timeoutMs) return true;
2987
+ if (idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs) return true;
2988
+ return false;
2657
2989
  }
2658
2990
  usage() {
2659
2991
  return {
@@ -2760,6 +3092,7 @@ function makeAgentSubagentRunner(opts) {
2760
3092
  // Cheap: O(1) per progress event, and the budget short-circuits
2761
3093
  // when timeoutMs is unset (most subagents have one set anyway).
2762
3094
  events.on("tool.progress", () => {
3095
+ ctx.budget.markActivity();
2763
3096
  try {
2764
3097
  ctx.budget.checkTimeout();
2765
3098
  } catch (e) {
@@ -2776,6 +3109,7 @@ function makeAgentSubagentRunner(opts) {
2776
3109
  currentToolName = e.name;
2777
3110
  }),
2778
3111
  events.on("provider.text_delta", (e) => {
3112
+ ctx.budget.markActivity();
2779
3113
  streamingTextAcc = (streamingTextAcc + e.text).slice(-200);
2780
3114
  })
2781
3115
  );
@@ -5169,6 +5503,7 @@ function providerStatusToCode(status, type) {
5169
5503
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.config])
5170
5504
  )
5171
5505
  });
5506
+ var DEFAULT_IDLE_TIMEOUT_MS = 15 * 60 * 1e3;
5172
5507
  var FLEET_ROSTER_BUDGETS = {
5173
5508
  "audit-log": { timeoutMs: 7.5 * 60 * 60 * 1e3, maxIterations: 5e3, maxToolCalls: 15e3 },
5174
5509
  "bug-hunter": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
@@ -5180,7 +5515,7 @@ var FLEET_ROSTER_BUDGETS = {
5180
5515
  )
5181
5516
  };
5182
5517
  var GENERIC_SUBAGENT_BUDGET = {
5183
- timeoutMs: 3 * 60 * 60 * 1e3,
5518
+ idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
5184
5519
  maxIterations: 5e3,
5185
5520
  maxToolCalls: 15e3
5186
5521
  };
@@ -5190,7 +5525,13 @@ function applyRosterBudget(cfg) {
5190
5525
  if (!defaultBudget) return cfg;
5191
5526
  return {
5192
5527
  ...cfg,
5193
- timeoutMs: cfg.timeoutMs ?? defaultBudget.timeoutMs,
5528
+ // Wall-clock cap is opt-in only: forward an explicit `cfg.timeoutMs`, but
5529
+ // do NOT impose the roster's historical multi-hour wall-clock default — it
5530
+ // killed agents that were still actively working. Reaping is idle-based.
5531
+ timeoutMs: cfg.timeoutMs,
5532
+ // Idle window is the default reaper. Resets on activity, so a long-but-
5533
+ // productive run is never killed; only a genuine stall is reaped.
5534
+ idleTimeoutMs: cfg.idleTimeoutMs ?? defaultBudget.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS,
5194
5535
  maxIterations: cfg.maxIterations ?? defaultBudget.maxIterations,
5195
5536
  maxToolCalls: cfg.maxToolCalls ?? defaultBudget.maxToolCalls,
5196
5537
  maxTokens: cfg.maxTokens ?? defaultBudget.maxTokens,
@@ -5258,6 +5599,107 @@ FLEET_ROSTER_BUDGETS["goose"] = { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations:
5258
5599
  ...Object.fromEntries(ACP_AGENTS.map((a) => [a.role, a]))
5259
5600
  });
5260
5601
 
5602
+ // src/coordination/subagent-nicknames.ts
5603
+ var NICKNAME_POOL = {
5604
+ // Physics & fundamental sciences
5605
+ "einstein": { name: "Einstein", domain: "physics" },
5606
+ "newton": { name: "Newton", domain: "physics" },
5607
+ "feynman": { name: "Feynman", domain: "physics" },
5608
+ "dirac": { name: "Dirac", domain: "physics" },
5609
+ "bohr": { name: "Bohr", domain: "physics" },
5610
+ "planck": { name: "Planck", domain: "physics" },
5611
+ "curie": { name: "Curie", domain: "physics" },
5612
+ "fermi": { name: "Fermi", domain: "physics" },
5613
+ "heisenberg": { name: "Heisenberg", domain: "physics" },
5614
+ "schrodinger": { name: "Schr\xF6dinger", domain: "physics" },
5615
+ // Mathematics
5616
+ "euclid": { name: "Euclid", domain: "math" },
5617
+ "gauss": { name: "Gauss", domain: "math" },
5618
+ "turing": { name: "Turing", domain: "math" },
5619
+ "poincare": { name: "Poincar\xE9", domain: "math" },
5620
+ "riemann": { name: "Riemann", domain: "math" },
5621
+ "hilbert": { name: "Hilbert", domain: "math" },
5622
+ "pythagoras": { name: "Pythagoras", domain: "math" },
5623
+ // Computing & information theory
5624
+ "von-neumann": { name: "Von Neumann", domain: "computing" },
5625
+ "shannon": { name: "Shannon", domain: "computing" },
5626
+ "hopper": { name: "Hopper", domain: "computing" },
5627
+ "backus": { name: "Backus", domain: "computing" },
5628
+ "knuth": { name: "Knuth", domain: "computing" },
5629
+ "torvalds": { name: "Torvalds", domain: "computing" },
5630
+ "stallman": { name: "Stallman", domain: "computing" },
5631
+ "berners-lee": { name: "Berners-Lee", domain: "computing" },
5632
+ "babbage": { name: "Babbage", domain: "computing" },
5633
+ "lovelace": { name: "Lovelace", domain: "computing" },
5634
+ "klein": { name: "Klein", domain: "computing" },
5635
+ // Electronics & electrical engineering
5636
+ "edison": { name: "Edison", domain: "ee" },
5637
+ "tesla": { name: "Tesla", domain: "ee" },
5638
+ "faraday": { name: "Faraday", domain: "ee" },
5639
+ "maxwell": { name: "Maxwell", domain: "ee" },
5640
+ "ohm": { name: "Ohm", domain: "ee" },
5641
+ "bell": { name: "Bell", domain: "ee" },
5642
+ "marconi": { name: "Marconi", domain: "ee" },
5643
+ "lamarr": { name: "Lamarr", domain: "ee" },
5644
+ // General science / multi-disciplinary
5645
+ "darwin": { name: "Darwin", domain: "biology" },
5646
+ "mendel": { name: "Mendel", domain: "biology" },
5647
+ "pasteur": { name: "Pasteur", domain: "biology" },
5648
+ "hawking": { name: "Hawking", domain: "cosmology" },
5649
+ "sagan": { name: "Sagan", domain: "cosmology" },
5650
+ // Chemistry / materials
5651
+ "lavoisier": { name: "Lavoisier", domain: "chemistry" },
5652
+ "mendeleev": { name: "Mendeleev", domain: "chemistry" }
5653
+ };
5654
+ var ALL_NICKNAMES = Object.values(NICKNAME_POOL);
5655
+ var DOMAIN_PREFERENCES = {
5656
+ "security": ["shannon", "turing", "lamarr", "stallman"],
5657
+ "bug-hunter": ["darwin", "curie", "feynman", "fermi"],
5658
+ "refactor": ["gauss", "hilbert", "euclid", "planck"],
5659
+ "audit-log": ["sagan", "hawking", "poincare", "newton"],
5660
+ "planner": ["hilbert", "gauss", "turing", "euclid"],
5661
+ "researcher": ["sagan", "hawking", "darwin", "pasteur"],
5662
+ "explorer": ["marconi", "bell", "columbus", "polo"],
5663
+ "testing": ["pasteur", "curie", "fermi", "bohr"],
5664
+ "frontend": ["lovelace", "hopper", "babbage", "backus"],
5665
+ "backend": ["torvalds", "stallman", "von-neumann", "backus"],
5666
+ "database": ["turing", "shannon", "backus", "knuth"],
5667
+ "devops": ["tesla", "edison", "faraday", "bell"],
5668
+ "security-scanner": ["shannon", "turing", "lamarr", "stallman"],
5669
+ "refactor-planner": ["gauss", "hilbert", "planck", "newton"],
5670
+ "architect": ["von-neumann", "turing", "gauss", "hilbert"],
5671
+ "critic": ["einstein", "feynman", "dirac", "bohr"],
5672
+ "e2e": ["hopper", "bell", "marconi", "tesla"],
5673
+ "performance": ["knuth", "gauss", "planck", "feynman"],
5674
+ "chaos": ["tesla", "edison", "curie", "fermi"],
5675
+ "cost": ["ohm", "bell", "marconi", "tesla"],
5676
+ // default fallback
5677
+ "default": ["einstein", "newton", "curie", "tesla", "edison", "turing", "shannon", "hopper", "knuth", "stallman"]
5678
+ };
5679
+ function assignNickname(role, used) {
5680
+ const preferences = [
5681
+ ...DOMAIN_PREFERENCES[role] ?? [],
5682
+ ...DOMAIN_PREFERENCES["default"] ?? []
5683
+ ];
5684
+ for (const key of preferences) {
5685
+ const entry = NICKNAME_POOL[key];
5686
+ if (entry && !used.has(key)) {
5687
+ return `${entry.name} (${formatRole(role)})`;
5688
+ }
5689
+ }
5690
+ for (const entry of ALL_NICKNAMES) {
5691
+ const key = Object.entries(NICKNAME_POOL).find(([, v]) => v.name === entry.name)?.[0];
5692
+ if (key && !used.has(key)) {
5693
+ return `${entry.name} (${formatRole(role)})`;
5694
+ }
5695
+ }
5696
+ const counter = used.size + 1;
5697
+ return `Scientist #${counter} (${formatRole(role)})`;
5698
+ }
5699
+ function formatRole(role) {
5700
+ return role.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
5701
+ }
5702
+
5261
5703
  // src/coordination/multi-agent-coordinator.ts
5262
5704
  var DefaultMultiAgentCoordinator = class extends EventEmitter {
5263
5705
  coordinatorId;
@@ -5265,6 +5707,15 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
5265
5707
  runner;
5266
5708
  fleetBus;
5267
5709
  subagents = /* @__PURE__ */ new Map();
5710
+ /**
5711
+ * Base nickname keys already handed out this run (e.g. `einstein`, `tesla`).
5712
+ * Prevents two workers sharing a name. Direct `coordinator.spawn()` callers
5713
+ * (parallel/eternal engine, SDD parallel run) don't go through
5714
+ * `Director.spawn()` where nicknames are normally assigned, so the
5715
+ * coordinator upgrades placeholder names ("Executor", "slot-ab12cd", role
5716
+ * names) to memorable ones here — that's what surfaces in the fleet monitor.
5717
+ */
5718
+ usedNicknames = /* @__PURE__ */ new Set();
5268
5719
  pendingTasks = [];
5269
5720
  completedResults = [];
5270
5721
  totalIterations = 0;
@@ -5313,7 +5764,26 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
5313
5764
  this.config.maxConcurrent = Math.floor(n);
5314
5765
  this.tryDispatchNext();
5315
5766
  }
5767
+ /**
5768
+ * Upgrade a placeholder/role-derived name to a memorable scientist nickname
5769
+ * (e.g. "Einstein (Executor)"). A name is treated as a placeholder when it is
5770
+ * empty, equals the role (case-insensitive), is a generic default
5771
+ * ("subagent"/"adhoc"/"generic"), or is an auto-generated `slot-…` id.
5772
+ * Explicit, human-chosen names — including nicknames already assigned by
5773
+ * `Director.spawn()` — are left untouched, so this never double-assigns.
5774
+ */
5775
+ withNickname(subagent) {
5776
+ const role = subagent.role ?? "subagent";
5777
+ const name = subagent.name?.trim() ?? "";
5778
+ const isPlaceholder = name === "" || name.toLowerCase() === role.toLowerCase() || name === "subagent" || name === "adhoc" || name === "generic" || /^slot-/.test(name);
5779
+ if (!isPlaceholder) return subagent;
5780
+ const nickname = assignNickname(role, this.usedNicknames);
5781
+ const baseKey = nickname.split(" ")[0].toLowerCase().replace(/[^a-z0-9-]/g, "-");
5782
+ this.usedNicknames.add(baseKey);
5783
+ return { ...subagent, name: nickname };
5784
+ }
5316
5785
  async spawn(subagent) {
5786
+ subagent = this.withNickname(subagent);
5317
5787
  const id = subagent.id || randomUUID();
5318
5788
  if (this.subagents.has(id)) {
5319
5789
  throw new Error(`Subagent id "${id}" already exists \u2014 refusing to overwrite`);
@@ -5621,13 +6091,17 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
5621
6091
  const rawMaxTokens = subagent.config.maxTokens;
5622
6092
  const rawMaxCostUsd = subagent.config.maxCostUsd;
5623
6093
  const rawTimeoutMs = subagent.config.timeoutMs;
6094
+ const rawIdleTimeoutMs = subagent.config.idleTimeoutMs;
5624
6095
  const configWithRosterDefaults = applyRosterBudget(subagent.config);
5625
6096
  const budget = new SubagentBudget({
5626
6097
  maxIterations: rawMaxIterations ?? this.config.defaultBudget?.maxIterations ?? configWithRosterDefaults.maxIterations,
5627
6098
  maxToolCalls: rawMaxToolCalls ?? this.config.defaultBudget?.maxToolCalls ?? configWithRosterDefaults.maxToolCalls,
5628
6099
  maxTokens: rawMaxTokens ?? this.config.defaultBudget?.maxTokens ?? configWithRosterDefaults.maxTokens,
5629
6100
  maxCostUsd: rawMaxCostUsd ?? this.config.defaultBudget?.maxCostUsd ?? configWithRosterDefaults.maxCostUsd,
5630
- timeoutMs: rawTimeoutMs ?? this.config.defaultBudget?.timeoutMs ?? configWithRosterDefaults.timeoutMs
6101
+ // Wall-clock cap is opt-in (explicit config / defaultBudget only); the
6102
+ // roster no longer supplies one. Idle is the default reaper.
6103
+ timeoutMs: rawTimeoutMs ?? this.config.defaultBudget?.timeoutMs ?? configWithRosterDefaults.timeoutMs,
6104
+ idleTimeoutMs: rawIdleTimeoutMs ?? this.config.defaultBudget?.idleTimeoutMs ?? configWithRosterDefaults.idleTimeoutMs
5631
6105
  });
5632
6106
  subagent.activeBudget = budget;
5633
6107
  if (!this.runner) {
@@ -5674,63 +6148,79 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
5674
6148
  }
5675
6149
  async executeWithTimeout(runner, task, ctx, budget) {
5676
6150
  const initialTimeoutMs = budget.limits.timeoutMs;
5677
- if (initialTimeoutMs === void 0) return runner(task, ctx);
6151
+ const idleLimitMs = budget.limits.idleTimeoutMs;
6152
+ if (initialTimeoutMs === void 0 && idleLimitMs === void 0) {
6153
+ return runner(task, ctx);
6154
+ }
5678
6155
  const start = Date.now();
5679
6156
  let timer = null;
5680
6157
  const timeoutPromise = new Promise((_, reject) => {
5681
6158
  const armFor = (ms) => {
5682
6159
  if (timer) clearTimeout(timer);
5683
- timer = setTimeout(async () => {
5684
- const elapsed = Date.now() - start;
5685
- const limit = budget.limits.timeoutMs ?? initialTimeoutMs;
5686
- if (!budget.onThreshold) {
5687
- this.subagents.get(ctx.subagentId)?.abortController.abort();
5688
- reject(new BudgetExceededError("timeout", limit, elapsed));
6160
+ timer = setTimeout(onTick, Math.max(0, ms));
6161
+ };
6162
+ const scheduleNext = () => {
6163
+ const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.timeoutMs ?? initialTimeoutMs) - (Date.now() - start);
6164
+ const idleRemaining = idleLimitMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
6165
+ armFor(Math.max(25, Math.min(wallRemaining, idleRemaining)));
6166
+ };
6167
+ const onTick = async () => {
6168
+ const elapsed = Date.now() - start;
6169
+ const wallLimit = initialTimeoutMs === void 0 ? void 0 : budget.limits.timeoutMs ?? initialTimeoutMs;
6170
+ const idleLimit = idleLimitMs === void 0 ? void 0 : budget.limits.idleTimeoutMs ?? idleLimitMs;
6171
+ const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
6172
+ const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
6173
+ if (idleExceeded && !wallExceeded) {
6174
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
6175
+ reject(new BudgetExceededError("timeout", idleLimit, budget.idleMs()));
6176
+ return;
6177
+ }
6178
+ if (!wallExceeded) {
6179
+ scheduleNext();
6180
+ return;
6181
+ }
6182
+ const limit = wallLimit;
6183
+ if (!budget.onThreshold) {
6184
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
6185
+ reject(new BudgetExceededError("timeout", limit, elapsed));
6186
+ return;
6187
+ }
6188
+ try {
6189
+ const result = budget.onThreshold({
6190
+ kind: "timeout",
6191
+ used: elapsed,
6192
+ limit,
6193
+ requestDecision: () => new Promise((resolveDecision) => {
6194
+ budget._events?.emit("budget.threshold_reached", {
6195
+ kind: "timeout",
6196
+ used: elapsed,
6197
+ limit,
6198
+ timeoutMs: 6e4,
6199
+ extend: (extra) => resolveDecision({ extend: extra }),
6200
+ deny: () => resolveDecision("stop")
6201
+ });
6202
+ })
6203
+ });
6204
+ const decision = typeof result === "string" ? result : await result;
6205
+ if (decision === "continue" || decision === "throw" || decision === "stop") {
6206
+ armFor(Math.max(1e3, limit));
5689
6207
  return;
5690
6208
  }
5691
- try {
5692
- const result = budget.onThreshold({
5693
- kind: "timeout",
5694
- used: elapsed,
5695
- limit,
5696
- requestDecision: () => new Promise((resolveDecision) => {
5697
- budget._events?.emit("budget.threshold_reached", {
5698
- kind: "timeout",
5699
- used: elapsed,
5700
- limit,
5701
- timeoutMs: 6e4,
5702
- extend: (extra) => resolveDecision({ extend: extra }),
5703
- deny: () => resolveDecision("stop")
5704
- });
5705
- })
5706
- });
5707
- const decision = typeof result === "string" ? result : await result;
5708
- if (decision === "continue") {
5709
- armFor(Math.max(1e3, limit));
5710
- return;
5711
- }
5712
- if (decision === "throw" || decision === "stop") {
5713
- armFor(Math.max(1e3, limit));
5714
- return;
5715
- }
5716
- if (decision.extend.timeoutMs !== void 0) {
5717
- budget.limits.timeoutMs = decision.extend.timeoutMs;
5718
- const newLimit = decision.extend.timeoutMs;
5719
- const remaining = Math.max(1e3, newLimit - elapsed);
5720
- armFor(remaining);
5721
- return;
5722
- }
5723
- this.subagents.get(ctx.subagentId)?.abortController.abort();
5724
- reject(new BudgetExceededError("timeout", limit, elapsed));
5725
- } catch (err) {
5726
- this.subagents.get(ctx.subagentId)?.abortController.abort();
5727
- reject(
5728
- err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
5729
- );
6209
+ if (decision.extend.timeoutMs !== void 0) {
6210
+ budget.limits.timeoutMs = decision.extend.timeoutMs;
6211
+ scheduleNext();
6212
+ return;
5730
6213
  }
5731
- }, ms);
6214
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
6215
+ reject(new BudgetExceededError("timeout", limit, elapsed));
6216
+ } catch (err) {
6217
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
6218
+ reject(
6219
+ err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
6220
+ );
6221
+ }
5732
6222
  };
5733
- armFor(initialTimeoutMs);
6223
+ scheduleNext();
5734
6224
  });
5735
6225
  try {
5736
6226
  return await Promise.race([runner(task, ctx), timeoutPromise]);
@@ -5907,12 +6397,12 @@ var GOAL_COMPLETE_MARKER2 = /^\s*\[goal[_\s-]?complete\]\s*$/im;
5907
6397
  var ParallelEternalEngine = class {
5908
6398
  constructor(opts) {
5909
6399
  this.opts = opts;
5910
- this.goalPath = goalFilePath(opts.projectRoot);
6400
+ this.goalPath = opts.goalPath ?? goalFilePath(opts.projectRoot);
5911
6401
  this.slots = Math.min(16, Math.max(1, opts.parallelSlots ?? 4));
5912
6402
  this.timeoutMs = opts.iterationTimeoutMs ?? 3e5;
5913
6403
  this.dispatchEnabled = opts.dispatch !== false;
5914
6404
  this.dispatchClassifier = opts.dispatchClassifier;
5915
- this.agentFactory = opts.subagentFactory ?? (async (config) => ({
6405
+ this.agentFactory = opts.subagentFactory ?? (async (_config) => ({
5916
6406
  agent: this.opts.agent,
5917
6407
  events: this.opts.agent.events
5918
6408
  }));
@@ -6006,7 +6496,6 @@ var ParallelEternalEngine = class {
6006
6496
  }
6007
6497
  const tasks = await this.decomposeGoal(goal);
6008
6498
  if (!tasks || tasks.length === 0) {
6009
- await sleep2(5e3);
6010
6499
  return false;
6011
6500
  }
6012
6501
  const fanOut = await this.fanOut(goal, tasks);
@@ -6027,6 +6516,7 @@ var ParallelEternalEngine = class {
6027
6516
  });
6028
6517
  if (fanOut.goalComplete) {
6029
6518
  this.stopRequested = true;
6519
+ this.state = "stopped";
6030
6520
  return true;
6031
6521
  }
6032
6522
  await this.maybeCompact();
@@ -6207,7 +6697,11 @@ ${lastFew}` : "No prior iterations.",
6207
6697
  } finally {
6208
6698
  clearTimeout(timer);
6209
6699
  }
6210
- } catch {
6700
+ } catch (err) {
6701
+ this.opts.onError?.(
6702
+ err instanceof Error ? err : new Error(String(err)),
6703
+ this.consecutiveFailures
6704
+ );
6211
6705
  return [];
6212
6706
  }
6213
6707
  }