@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
@@ -410,7 +410,11 @@ Working rules:
410
410
  thrashing, terminate it rather than letting cost climb silently.
411
411
  6. Never claim a subagent's work as your own without verifying it. If a
412
412
  result looks wrong, ask_subagent for clarification before passing it
413
- to the user.`;
413
+ to the user.
414
+ 7. Wind down when satisfied. When the results are good enough, call
415
+ work_complete \u2014 no new subagents will spawn and queued tasks complete
416
+ as aborted. Running subagents finish naturally. Call terminate_subagent
417
+ only for ones you need to stop immediately.`;
414
418
  var DEFAULT_SUBAGENT_BASELINE = `You are a subagent operating under a Director. You were spawned to handle
415
419
  a specific slice of a larger plan \u2014 do that slice well and report back.
416
420
 
@@ -555,14 +559,12 @@ var FleetBus = class {
555
559
  };
556
560
  var FleetUsageAggregator = class {
557
561
  constructor(bus, priceLookup, metaLookup) {
558
- this.bus = bus;
559
562
  this.priceLookup = priceLookup;
560
563
  this.metaLookup = metaLookup;
561
564
  bus.filter("provider.response", (e) => this.onProviderResponse(e));
562
565
  bus.filter("tool.executed", (e) => this.onToolExecuted(e));
563
566
  bus.filter("iteration.started", (e) => this.onIterationStarted(e));
564
567
  }
565
- bus;
566
568
  priceLookup;
567
569
  metaLookup;
568
570
  perSubagent = /* @__PURE__ */ new Map();
@@ -704,7 +706,7 @@ var DOMAIN_PREFERENCES = {
704
706
  "e2e": ["hopper", "bell", "marconi", "tesla"],
705
707
  "performance": ["knuth", "gauss", "planck", "feynman"],
706
708
  "chaos": ["tesla", "edison", "curie", "fermi"],
707
- "cost": ["oh", "bell", "marconi", "tesla"],
709
+ "cost": ["ohm", "bell", "marconi", "tesla"],
708
710
  // default fallback
709
711
  "default": ["einstein", "newton", "curie", "tesla", "edison", "turing", "shannon", "hopper", "knuth", "stallman"]
710
712
  };
@@ -714,8 +716,9 @@ function assignNickname(role, used) {
714
716
  ...DOMAIN_PREFERENCES["default"] ?? []
715
717
  ];
716
718
  for (const key of preferences) {
717
- if (!used.has(key)) {
718
- return `${NICKNAME_POOL[key].name} (${formatRole(role)})`;
719
+ const entry = NICKNAME_POOL[key];
720
+ if (entry && !used.has(key)) {
721
+ return `${entry.name} (${formatRole(role)})`;
719
722
  }
720
723
  }
721
724
  for (const entry of ALL_NICKNAMES) {
@@ -877,19 +880,16 @@ var SubagentBudget = class _SubagentBudget {
877
880
  tokenOutput = 0;
878
881
  costUsd = 0;
879
882
  startTime = null;
880
- _onThreshold;
881
883
  /**
882
- * Tracks which budget kinds currently have an extension request
883
- * in flight. While a kind is here, further `checkLimit` calls for the
884
- * same kind are no-ops without this dedup, every `recordIteration`
885
- * after the limit is reached spawns a fresh decision Promise (until
886
- * the first one lands and patches limits), flooding the FleetBus
887
- * with redundant threshold events. Cleared in `negotiateExtension`'s
888
- * `finally`.
884
+ * Timestamp of the most recent activity (iteration / tool call / token
885
+ * usage / streamed progress). Drives the idle timeout reset by
886
+ * `markActivity()`. Initialised to `start()` time so a never-active agent
887
+ * still eventually trips its idle window.
889
888
  */
890
- pendingExtensions = /* @__PURE__ */ new Set();
889
+ lastActivityTime = null;
890
+ _onThreshold;
891
891
  /**
892
- * Hard cap on how long `negotiateExtension` waits for the coordinator to
892
+ * Hard cap on how long `_negotiateExtension` waits for the coordinator to
893
893
  * respond before defaulting to 'stop'. Without this fallback an absent
894
894
  * or hung listener (Director not built / event filter detached mid-run)
895
895
  * leaves the budget over-limit and never enforces anything.
@@ -930,6 +930,24 @@ var SubagentBudget = class _SubagentBudget {
930
930
  }
931
931
  start() {
932
932
  this.startTime = Date.now();
933
+ this.lastActivityTime = this.startTime;
934
+ }
935
+ /**
936
+ * Reset the idle clock. Called on any sign of forward progress —
937
+ * iterations, tool calls, token usage, and streamed tool/text progress —
938
+ * so a long-but-productive subagent never trips its `idleTimeoutMs`.
939
+ */
940
+ markActivity() {
941
+ this.lastActivityTime = Date.now();
942
+ }
943
+ /**
944
+ * Milliseconds since the last activity signal. Returns 0 before `start()`
945
+ * (nothing to measure yet). Used by the coordinator watchdog to decide
946
+ * whether to re-arm (still active) or reap (genuinely idle).
947
+ */
948
+ idleMs() {
949
+ const since = this.lastActivityTime ?? this.startTime;
950
+ return since === null ? 0 : Date.now() - since;
933
951
  }
934
952
  /** Returns true if we're within 10% of any limit — useful for pre-flight checks. */
935
953
  isNearLimit() {
@@ -951,38 +969,88 @@ var SubagentBudget = class _SubagentBudget {
951
969
  * - `mode === 'auto'` + no listener → throw `BudgetExceededError` (hard stop; no one to ask)
952
970
  * - `mode === 'auto'` + listener → throw `BudgetThresholdSignal` with async decision promise
953
971
  */
954
- checkLimit(kind, used, limit) {
972
+ /**
973
+ * Collects all exceeded budget kinds into a single NOOP-free negotiation.
974
+ * Called by recordIteration / recordToolCall / recordUsage — each may call
975
+ * this for its own kind. The first call starts the negotiation and stores
976
+ * the Promise in _pendingNegotiation. Subsequent calls for DIFFERENT
977
+ * kinds (while a negotiation is in flight) are NOOPs — they don't start
978
+ * new conversations with the coordinator. This prevents an EventBus flood
979
+ * when multiple budget kinds are exceeded simultaneously in one iteration.
980
+ *
981
+ * Returns the kinds that were found to be exceeded (for logging/debugging).
982
+ */
983
+ checkLimits(elapsedMs) {
984
+ const exceeded = [];
985
+ if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
986
+ exceeded.push({ kind: "iterations", used: this.iterations, limit: this.limits.maxIterations });
987
+ }
988
+ if (this.limits.maxToolCalls !== void 0 && this.toolCalls > this.limits.maxToolCalls) {
989
+ exceeded.push({ kind: "tool_calls", used: this.toolCalls, limit: this.limits.maxToolCalls });
990
+ }
991
+ const totalTokens = this.tokenInput + this.tokenOutput;
992
+ if (this.limits.maxTokens !== void 0 && totalTokens > this.limits.maxTokens) {
993
+ exceeded.push({ kind: "tokens", used: totalTokens, limit: this.limits.maxTokens });
994
+ }
995
+ if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
996
+ exceeded.push({ kind: "cost", used: this.costUsd, limit: this.limits.maxCostUsd });
997
+ }
998
+ if (elapsedMs !== void 0) {
999
+ const idle = this.idleMs();
1000
+ if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
1001
+ exceeded.push({ kind: "timeout", used: idle, limit: this.limits.idleTimeoutMs });
1002
+ } else if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs) {
1003
+ exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
1004
+ }
1005
+ }
1006
+ if (exceeded.length === 0) return [];
955
1007
  if (!this._onThreshold) {
956
- throw new BudgetExceededError(kind, limit, used);
1008
+ const first2 = exceeded[0];
1009
+ throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
957
1010
  }
958
1011
  if (this._mode === "sync") {
959
- throw new BudgetExceededError(kind, limit, used);
1012
+ const first2 = exceeded[0];
1013
+ throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
960
1014
  }
961
1015
  const bus = this._events;
962
1016
  if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
963
- throw new BudgetExceededError(kind, limit, used);
1017
+ const first2 = exceeded[0];
1018
+ throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
964
1019
  }
965
- if (this.pendingExtensions.has(kind)) return;
966
- this.pendingExtensions.add(kind);
967
- const decision = this.negotiateExtension(kind, used, limit);
968
- throw new BudgetThresholdSignal(kind, limit, used, decision);
1020
+ for (const entry of exceeded) {
1021
+ if (this._pendingNegotiations.has(entry.kind)) continue;
1022
+ const decision2 = this._negotiateExtension(entry.kind, exceeded);
1023
+ this._pendingNegotiations.set(entry.kind, decision2);
1024
+ }
1025
+ const first = exceeded[0];
1026
+ const decision = this._pendingNegotiations.get(first.kind);
1027
+ throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
969
1028
  }
1029
+ /**
1030
+ * Per-kind in-flight negotiation Promises. Each budget kind can have its
1031
+ * own concurrent negotiation — e.g. iterations and timeout can both
1032
+ * be exceeded simultaneously without blocking each other. The same kind
1033
+ * cannot start two concurrent negotiations (dedup guard).
1034
+ * Cleared in `_negotiateExtension`'s `finally` block.
1035
+ */
1036
+ _pendingNegotiations = /* @__PURE__ */ new Map();
970
1037
  /**
971
1038
  * Drive the threshold handler to a decision. Resolves with `'stop'`
972
1039
  * (signal the runner to abort) or `{ extend: ... }` (limits already
973
- * patched in-place; the runner should not abort). Always releases the
974
- * `pendingExtensions` slot in `finally`.
1040
+ * patched in-place; the runner should not abort). Clears the
1041
+ * per-kind slot in `_pendingNegotiations` in `finally`.
975
1042
  *
976
1043
  * The 'continue' return from a sync handler is treated as
977
1044
  * `{ extend: {} }` — keep going without patching; next overrun fires
978
1045
  * a fresh signal.
979
1046
  */
980
- async negotiateExtension(kind, used, limit) {
1047
+ async _negotiateExtension(kind, exceeded) {
981
1048
  try {
1049
+ const first = exceeded[0];
982
1050
  const result = this._onThreshold({
983
- kind,
984
- used,
985
- limit,
1051
+ kind: first.kind,
1052
+ used: first.used,
1053
+ limit: first.limit,
986
1054
  requestDecision: () => {
987
1055
  const bus = this._events;
988
1056
  if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
@@ -999,20 +1067,22 @@ var SubagentBudget = class _SubagentBudget {
999
1067
  () => respond("stop"),
1000
1068
  _SubagentBudget.DECISION_TIMEOUT_MS
1001
1069
  );
1002
- bus.emit("budget.threshold_reached", {
1003
- kind,
1004
- used,
1005
- limit,
1006
- timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
1007
- extend: (extra) => {
1008
- clearTimeout(fallback);
1009
- respond({ extend: extra });
1010
- },
1011
- deny: () => {
1012
- clearTimeout(fallback);
1013
- respond("stop");
1014
- }
1015
- });
1070
+ for (const { kind: kind2, used, limit } of exceeded) {
1071
+ bus.emit("budget.threshold_reached", {
1072
+ kind: kind2,
1073
+ used,
1074
+ limit,
1075
+ timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
1076
+ extend: (extra) => {
1077
+ clearTimeout(fallback);
1078
+ respond({ extend: extra });
1079
+ },
1080
+ deny: () => {
1081
+ clearTimeout(fallback);
1082
+ respond("stop");
1083
+ }
1084
+ });
1085
+ }
1016
1086
  });
1017
1087
  }
1018
1088
  });
@@ -1036,54 +1106,59 @@ var SubagentBudget = class _SubagentBudget {
1036
1106
  if (ext.timeoutMs !== void 0) {
1037
1107
  this.limits.timeoutMs = ext.timeoutMs;
1038
1108
  }
1109
+ if (ext.idleTimeoutMs !== void 0) {
1110
+ this.limits.idleTimeoutMs = ext.idleTimeoutMs;
1111
+ }
1039
1112
  return decision;
1040
1113
  } finally {
1041
- this.pendingExtensions.delete(kind);
1114
+ this._pendingNegotiations.delete(kind);
1042
1115
  }
1043
1116
  }
1044
1117
  recordIteration() {
1045
1118
  this.iterations++;
1046
- if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
1047
- void this.checkLimit("iterations", this.iterations, this.limits.maxIterations);
1048
- }
1119
+ this.markActivity();
1120
+ void this.checkLimits();
1049
1121
  }
1050
1122
  recordToolCall() {
1051
1123
  this.toolCalls++;
1052
- if (this.limits.maxToolCalls !== void 0 && this.toolCalls > this.limits.maxToolCalls) {
1053
- void this.checkLimit("tool_calls", this.toolCalls, this.limits.maxToolCalls);
1054
- }
1124
+ this.markActivity();
1125
+ void this.checkLimits();
1055
1126
  }
1056
1127
  recordUsage(usage, costUsd = 0) {
1057
1128
  this.tokenInput += usage.input;
1058
1129
  this.tokenOutput += usage.output;
1059
1130
  this.costUsd += costUsd;
1060
- const totalTokens = this.tokenInput + this.tokenOutput;
1061
- if (this.limits.maxTokens !== void 0 && totalTokens > this.limits.maxTokens) {
1062
- void this.checkLimit("tokens", totalTokens, this.limits.maxTokens);
1063
- }
1064
- if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
1065
- void this.checkLimit("cost", this.costUsd, this.limits.maxCostUsd);
1066
- }
1131
+ this.markActivity();
1132
+ void this.checkLimits();
1067
1133
  }
1068
1134
  /**
1069
- * Wall-clock budget check. Unlike other limits, timeout check passes through
1070
- * `checkLimit` and is subject to the same negotiation-mode decision table.
1071
- * In practice, `'sync'` mode (the usual test configuration) means a timeout
1072
- * immediately throws `BudgetExceededError`. In production with a coordinator,
1073
- * a timeout emits `budget.threshold_reached` so the Director can decide whether
1074
- * to extend or abort.
1135
+ * Wall-clock budget check. Unlike other limits, timeout is always a hard stop
1136
+ * wall-clock time cannot be "extended" by the coordinator, so it throws
1137
+ * synchronously rather than entering the negotiation flow.
1138
+ *
1139
+ * Decision table:
1140
+ * - no `onThreshold` handler → throw `BudgetExceededError`
1141
+ * - `mode === 'sync'` → throw `BudgetExceededError`
1142
+ * - `mode === 'auto'` + no listener → throw `BudgetExceededError`
1143
+ * - `mode === 'auto'` + listener → throw `BudgetExceededError` (timeout is not extendable)
1075
1144
  */
1076
1145
  checkTimeout() {
1077
- if (this.startTime === null || this.limits.timeoutMs === void 0) return;
1146
+ if (this.startTime === null) return;
1147
+ const { timeoutMs, idleTimeoutMs } = this.limits;
1148
+ if (timeoutMs === void 0 && idleTimeoutMs === void 0) return;
1078
1149
  const elapsed = Date.now() - this.startTime;
1079
- if (elapsed > this.limits.timeoutMs) {
1080
- void this.checkLimit("timeout", elapsed, this.limits.timeoutMs);
1081
- }
1150
+ const wallTripped = timeoutMs !== void 0 && elapsed > timeoutMs;
1151
+ const idleTripped = idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs;
1152
+ if (!wallTripped && !idleTripped) return;
1153
+ void this.checkLimits(elapsed);
1082
1154
  }
1083
- /** Returns true if a timeout has occurred without throwing. Useful for races. */
1155
+ /** Returns true if a wall-clock or idle timeout has occurred without throwing. */
1084
1156
  isTimedOut() {
1085
- if (this.startTime === null || this.limits.timeoutMs === void 0) return false;
1086
- return Date.now() - this.startTime > this.limits.timeoutMs;
1157
+ if (this.startTime === null) return false;
1158
+ const { timeoutMs, idleTimeoutMs } = this.limits;
1159
+ if (timeoutMs !== void 0 && Date.now() - this.startTime > timeoutMs) return true;
1160
+ if (idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs) return true;
1161
+ return false;
1087
1162
  }
1088
1163
  usage() {
1089
1164
  return {
@@ -3485,6 +3560,7 @@ var FLEET_ROSTER = {
3485
3560
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.config])
3486
3561
  )
3487
3562
  };
3563
+ var DEFAULT_IDLE_TIMEOUT_MS = 15 * 60 * 1e3;
3488
3564
  var FLEET_ROSTER_BUDGETS = {
3489
3565
  "audit-log": { timeoutMs: 7.5 * 60 * 60 * 1e3, maxIterations: 5e3, maxToolCalls: 15e3 },
3490
3566
  "bug-hunter": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
@@ -3496,7 +3572,7 @@ var FLEET_ROSTER_BUDGETS = {
3496
3572
  )
3497
3573
  };
3498
3574
  var GENERIC_SUBAGENT_BUDGET = {
3499
- timeoutMs: 3 * 60 * 60 * 1e3,
3575
+ idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
3500
3576
  maxIterations: 5e3,
3501
3577
  maxToolCalls: 15e3
3502
3578
  };
@@ -3506,7 +3582,13 @@ function applyRosterBudget(cfg) {
3506
3582
  if (!defaultBudget) return cfg;
3507
3583
  return {
3508
3584
  ...cfg,
3509
- timeoutMs: cfg.timeoutMs ?? defaultBudget.timeoutMs,
3585
+ // Wall-clock cap is opt-in only: forward an explicit `cfg.timeoutMs`, but
3586
+ // do NOT impose the roster's historical multi-hour wall-clock default — it
3587
+ // killed agents that were still actively working. Reaping is idle-based.
3588
+ timeoutMs: cfg.timeoutMs,
3589
+ // Idle window is the default reaper. Resets on activity, so a long-but-
3590
+ // productive run is never killed; only a genuine stall is reaped.
3591
+ idleTimeoutMs: cfg.idleTimeoutMs ?? defaultBudget.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS,
3510
3592
  maxIterations: cfg.maxIterations ?? defaultBudget.maxIterations,
3511
3593
  maxToolCalls: cfg.maxToolCalls ?? defaultBudget.maxToolCalls,
3512
3594
  maxTokens: cfg.maxTokens ?? defaultBudget.maxTokens,
@@ -3583,6 +3665,15 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3583
3665
  runner;
3584
3666
  fleetBus;
3585
3667
  subagents = /* @__PURE__ */ new Map();
3668
+ /**
3669
+ * Base nickname keys already handed out this run (e.g. `einstein`, `tesla`).
3670
+ * Prevents two workers sharing a name. Direct `coordinator.spawn()` callers
3671
+ * (parallel/eternal engine, SDD parallel run) don't go through
3672
+ * `Director.spawn()` where nicknames are normally assigned, so the
3673
+ * coordinator upgrades placeholder names ("Executor", "slot-ab12cd", role
3674
+ * names) to memorable ones here — that's what surfaces in the fleet monitor.
3675
+ */
3676
+ usedNicknames = /* @__PURE__ */ new Set();
3586
3677
  pendingTasks = [];
3587
3678
  completedResults = [];
3588
3679
  totalIterations = 0;
@@ -3631,7 +3722,26 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3631
3722
  this.config.maxConcurrent = Math.floor(n);
3632
3723
  this.tryDispatchNext();
3633
3724
  }
3725
+ /**
3726
+ * Upgrade a placeholder/role-derived name to a memorable scientist nickname
3727
+ * (e.g. "Einstein (Executor)"). A name is treated as a placeholder when it is
3728
+ * empty, equals the role (case-insensitive), is a generic default
3729
+ * ("subagent"/"adhoc"/"generic"), or is an auto-generated `slot-…` id.
3730
+ * Explicit, human-chosen names — including nicknames already assigned by
3731
+ * `Director.spawn()` — are left untouched, so this never double-assigns.
3732
+ */
3733
+ withNickname(subagent) {
3734
+ const role = subagent.role ?? "subagent";
3735
+ const name = subagent.name?.trim() ?? "";
3736
+ const isPlaceholder = name === "" || name.toLowerCase() === role.toLowerCase() || name === "subagent" || name === "adhoc" || name === "generic" || /^slot-/.test(name);
3737
+ if (!isPlaceholder) return subagent;
3738
+ const nickname = assignNickname(role, this.usedNicknames);
3739
+ const baseKey = nickname.split(" ")[0].toLowerCase().replace(/[^a-z0-9-]/g, "-");
3740
+ this.usedNicknames.add(baseKey);
3741
+ return { ...subagent, name: nickname };
3742
+ }
3634
3743
  async spawn(subagent) {
3744
+ subagent = this.withNickname(subagent);
3635
3745
  const id = subagent.id || randomUUID();
3636
3746
  if (this.subagents.has(id)) {
3637
3747
  throw new Error(`Subagent id "${id}" already exists \u2014 refusing to overwrite`);
@@ -3939,13 +4049,17 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3939
4049
  const rawMaxTokens = subagent.config.maxTokens;
3940
4050
  const rawMaxCostUsd = subagent.config.maxCostUsd;
3941
4051
  const rawTimeoutMs = subagent.config.timeoutMs;
4052
+ const rawIdleTimeoutMs = subagent.config.idleTimeoutMs;
3942
4053
  const configWithRosterDefaults = applyRosterBudget(subagent.config);
3943
4054
  const budget = new SubagentBudget({
3944
4055
  maxIterations: rawMaxIterations ?? this.config.defaultBudget?.maxIterations ?? configWithRosterDefaults.maxIterations,
3945
4056
  maxToolCalls: rawMaxToolCalls ?? this.config.defaultBudget?.maxToolCalls ?? configWithRosterDefaults.maxToolCalls,
3946
4057
  maxTokens: rawMaxTokens ?? this.config.defaultBudget?.maxTokens ?? configWithRosterDefaults.maxTokens,
3947
4058
  maxCostUsd: rawMaxCostUsd ?? this.config.defaultBudget?.maxCostUsd ?? configWithRosterDefaults.maxCostUsd,
3948
- timeoutMs: rawTimeoutMs ?? this.config.defaultBudget?.timeoutMs ?? configWithRosterDefaults.timeoutMs
4059
+ // Wall-clock cap is opt-in (explicit config / defaultBudget only); the
4060
+ // roster no longer supplies one. Idle is the default reaper.
4061
+ timeoutMs: rawTimeoutMs ?? this.config.defaultBudget?.timeoutMs ?? configWithRosterDefaults.timeoutMs,
4062
+ idleTimeoutMs: rawIdleTimeoutMs ?? this.config.defaultBudget?.idleTimeoutMs ?? configWithRosterDefaults.idleTimeoutMs
3949
4063
  });
3950
4064
  subagent.activeBudget = budget;
3951
4065
  if (!this.runner) {
@@ -3992,63 +4106,79 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3992
4106
  }
3993
4107
  async executeWithTimeout(runner, task, ctx, budget) {
3994
4108
  const initialTimeoutMs = budget.limits.timeoutMs;
3995
- if (initialTimeoutMs === void 0) return runner(task, ctx);
4109
+ const idleLimitMs = budget.limits.idleTimeoutMs;
4110
+ if (initialTimeoutMs === void 0 && idleLimitMs === void 0) {
4111
+ return runner(task, ctx);
4112
+ }
3996
4113
  const start = Date.now();
3997
4114
  let timer = null;
3998
4115
  const timeoutPromise = new Promise((_, reject) => {
3999
4116
  const armFor = (ms) => {
4000
4117
  if (timer) clearTimeout(timer);
4001
- timer = setTimeout(async () => {
4002
- const elapsed = Date.now() - start;
4003
- const limit = budget.limits.timeoutMs ?? initialTimeoutMs;
4004
- if (!budget.onThreshold) {
4005
- this.subagents.get(ctx.subagentId)?.abortController.abort();
4006
- reject(new BudgetExceededError("timeout", limit, elapsed));
4118
+ timer = setTimeout(onTick, Math.max(0, ms));
4119
+ };
4120
+ const scheduleNext = () => {
4121
+ const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.timeoutMs ?? initialTimeoutMs) - (Date.now() - start);
4122
+ const idleRemaining = idleLimitMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
4123
+ armFor(Math.max(25, Math.min(wallRemaining, idleRemaining)));
4124
+ };
4125
+ const onTick = async () => {
4126
+ const elapsed = Date.now() - start;
4127
+ const wallLimit = initialTimeoutMs === void 0 ? void 0 : budget.limits.timeoutMs ?? initialTimeoutMs;
4128
+ const idleLimit = idleLimitMs === void 0 ? void 0 : budget.limits.idleTimeoutMs ?? idleLimitMs;
4129
+ const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
4130
+ const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
4131
+ if (idleExceeded && !wallExceeded) {
4132
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
4133
+ reject(new BudgetExceededError("timeout", idleLimit, budget.idleMs()));
4134
+ return;
4135
+ }
4136
+ if (!wallExceeded) {
4137
+ scheduleNext();
4138
+ return;
4139
+ }
4140
+ const limit = wallLimit;
4141
+ if (!budget.onThreshold) {
4142
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
4143
+ reject(new BudgetExceededError("timeout", limit, elapsed));
4144
+ return;
4145
+ }
4146
+ try {
4147
+ const result = budget.onThreshold({
4148
+ kind: "timeout",
4149
+ used: elapsed,
4150
+ limit,
4151
+ requestDecision: () => new Promise((resolveDecision) => {
4152
+ budget._events?.emit("budget.threshold_reached", {
4153
+ kind: "timeout",
4154
+ used: elapsed,
4155
+ limit,
4156
+ timeoutMs: 6e4,
4157
+ extend: (extra) => resolveDecision({ extend: extra }),
4158
+ deny: () => resolveDecision("stop")
4159
+ });
4160
+ })
4161
+ });
4162
+ const decision = typeof result === "string" ? result : await result;
4163
+ if (decision === "continue" || decision === "throw" || decision === "stop") {
4164
+ armFor(Math.max(1e3, limit));
4007
4165
  return;
4008
4166
  }
4009
- try {
4010
- const result = budget.onThreshold({
4011
- kind: "timeout",
4012
- used: elapsed,
4013
- limit,
4014
- requestDecision: () => new Promise((resolveDecision) => {
4015
- budget._events?.emit("budget.threshold_reached", {
4016
- kind: "timeout",
4017
- used: elapsed,
4018
- limit,
4019
- timeoutMs: 6e4,
4020
- extend: (extra) => resolveDecision({ extend: extra }),
4021
- deny: () => resolveDecision("stop")
4022
- });
4023
- })
4024
- });
4025
- const decision = typeof result === "string" ? result : await result;
4026
- if (decision === "continue") {
4027
- armFor(Math.max(1e3, limit));
4028
- return;
4029
- }
4030
- if (decision === "throw" || decision === "stop") {
4031
- armFor(Math.max(1e3, limit));
4032
- return;
4033
- }
4034
- if (decision.extend.timeoutMs !== void 0) {
4035
- budget.limits.timeoutMs = decision.extend.timeoutMs;
4036
- const newLimit = decision.extend.timeoutMs;
4037
- const remaining = Math.max(1e3, newLimit - elapsed);
4038
- armFor(remaining);
4039
- return;
4040
- }
4041
- this.subagents.get(ctx.subagentId)?.abortController.abort();
4042
- reject(new BudgetExceededError("timeout", limit, elapsed));
4043
- } catch (err) {
4044
- this.subagents.get(ctx.subagentId)?.abortController.abort();
4045
- reject(
4046
- err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
4047
- );
4167
+ if (decision.extend.timeoutMs !== void 0) {
4168
+ budget.limits.timeoutMs = decision.extend.timeoutMs;
4169
+ scheduleNext();
4170
+ return;
4048
4171
  }
4049
- }, ms);
4172
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
4173
+ reject(new BudgetExceededError("timeout", limit, elapsed));
4174
+ } catch (err) {
4175
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
4176
+ reject(
4177
+ err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
4178
+ );
4179
+ }
4050
4180
  };
4051
- armFor(initialTimeoutMs);
4181
+ scheduleNext();
4052
4182
  });
4053
4183
  try {
4054
4184
  return await Promise.race([runner(task, ctx), timeoutPromise]);
@@ -4510,7 +4640,7 @@ function makeRollUpTool(director) {
4510
4640
  function makeTerminateTool(director) {
4511
4641
  return {
4512
4642
  name: "terminate_subagent",
4513
- description: "Forcibly abort a subagent.",
4643
+ description: 'Forcibly abort a subagent. The subagent finishes its current iteration then exits with status "stopped".',
4514
4644
  permission: "auto",
4515
4645
  mutating: true,
4516
4646
  inputSchema: { type: "object", properties: { subagentId: { type: "string", description: "Subagent to abort." } }, required: ["subagentId"] },
@@ -4521,6 +4651,19 @@ function makeTerminateTool(director) {
4521
4651
  }
4522
4652
  };
4523
4653
  }
4654
+ function makeTerminateAllTool(director) {
4655
+ return {
4656
+ name: "terminate_all",
4657
+ description: 'Forcibly stop every subagent in the fleet and drain the pending task queue. In-flight tasks are terminated mid-execution; pending tasks receive "aborted_by_parent" completion immediately. Use this when the fleet is wedged, looping, or you need a clean slate. Compare: work_complete stops spawning but lets running agents finish naturally.',
4658
+ permission: "auto",
4659
+ mutating: true,
4660
+ inputSchema: { type: "object", properties: {}, required: [] },
4661
+ async execute() {
4662
+ await director.terminateAll();
4663
+ return { ok: true, message: `Fleet shutdown complete \u2014 all subagents stopped, pending tasks drained.` };
4664
+ }
4665
+ };
4666
+ }
4524
4667
  function makeFleetStatusTool(director) {
4525
4668
  return {
4526
4669
  name: "fleet_status",
@@ -4663,7 +4806,7 @@ function makeCollabDebugTool(director) {
4663
4806
  function makeFleetEmitTool(director) {
4664
4807
  return {
4665
4808
  name: "fleet_emit",
4666
- description: "Emit a structured event on the FleetBus. Use this when you find a bug (bug.found), complete a refactor plan (refactor.plan), or finish an evaluation (critic.evaluation). Events are routed to other agents in the collab session.",
4809
+ description: "Emit a structured event on the FleetBus. Any subagent can emit any event type; the Director routes it to all listeners. Use it to stream findings, progress updates, or final results to other agents in real time.",
4667
4810
  permission: "auto",
4668
4811
  mutating: false,
4669
4812
  inputSchema: {
@@ -4671,14 +4814,14 @@ function makeFleetEmitTool(director) {
4671
4814
  properties: {
4672
4815
  type: {
4673
4816
  type: "string",
4674
- description: "Event type: bug.found | refactor.plan | critic.evaluation"
4817
+ description: "Event type string (e.g. bug.found, refactor.plan, critic.evaluation, progress, result)."
4675
4818
  },
4676
4819
  payload: {
4677
4820
  type: "object",
4678
- description: "Event payload matching the event type."
4821
+ description: "Event payload. Structure depends on event type. Use null if no payload."
4679
4822
  }
4680
4823
  },
4681
- required: ["type", "payload"]
4824
+ required: ["type"]
4682
4825
  },
4683
4826
  async execute(input) {
4684
4827
  const i = input;
@@ -4686,12 +4829,31 @@ function makeFleetEmitTool(director) {
4686
4829
  subagentId: director.id,
4687
4830
  ts: Date.now(),
4688
4831
  type: i.type,
4689
- payload: i.payload
4832
+ payload: i.payload ?? {}
4690
4833
  });
4691
4834
  return { ok: true, event: i.type };
4692
4835
  }
4693
4836
  };
4694
4837
  }
4838
+ function makeWorkCompleteTool(director) {
4839
+ return {
4840
+ name: "work_complete",
4841
+ description: "Signal that the director is satisfied with the results and the fleet should wind down. After calling this, spawn_subagent will refuse with a budget error and assign_task will instantly complete any queued tasks as aborted. Running subagents finish naturally. Call terminate_subagent separately to stop specific subagents immediately.",
4842
+ permission: "auto",
4843
+ mutating: false,
4844
+ inputSchema: { type: "object", properties: {}, required: [] },
4845
+ async execute() {
4846
+ director.workComplete();
4847
+ return { ok: true, message: "Fleet wind-down signaled. No new spawns or task dispatches." };
4848
+ }
4849
+ };
4850
+ }
4851
+ var DirectorAlertLevel = /* @__PURE__ */ ((DirectorAlertLevel2) => {
4852
+ DirectorAlertLevel2["WARNING"] = "warning";
4853
+ DirectorAlertLevel2["CRITICAL"] = "critical";
4854
+ DirectorAlertLevel2["CANCELLED"] = "cancelled";
4855
+ return DirectorAlertLevel2;
4856
+ })(DirectorAlertLevel || {});
4695
4857
  var CollabSession = class extends EventEmitter {
4696
4858
  sessionId;
4697
4859
  options;
@@ -4706,6 +4868,14 @@ var CollabSession = class extends EventEmitter {
4706
4868
  disposers = new Array();
4707
4869
  settled = false;
4708
4870
  timeoutMs;
4871
+ cancelled = false;
4872
+ alerts = [];
4873
+ /** Tracks tool call counts per subagent for progress-based timeout decisions. */
4874
+ progressBySubagent = /* @__PURE__ */ new Map();
4875
+ /** Last tool call count when a timeout warning was handled. */
4876
+ lastTimeoutProgress = /* @__PURE__ */ new Map();
4877
+ /** Session-level timeout timer handle (cleared on cancel or natural completion). */
4878
+ _timeoutTimer;
4709
4879
  constructor(director, fleetBus, options) {
4710
4880
  super();
4711
4881
  this.sessionId = randomUUID();
@@ -4723,11 +4893,23 @@ var CollabSession = class extends EventEmitter {
4723
4893
  };
4724
4894
  }
4725
4895
  }
4896
+ get id() {
4897
+ return this.sessionId;
4898
+ }
4899
+ getSessionAlerts() {
4900
+ return [...this.alerts];
4901
+ }
4902
+ isCancelled() {
4903
+ return this.cancelled;
4904
+ }
4726
4905
  /**
4727
- * Read the target files from disk and populate the snapshot.
4728
- * Call this after construction if you did not provide a prebuiltSnapshot
4729
- * and want the session to operate on real file contents.
4906
+ * Snapshot of role subagentId map. The Director calls coordinator.stop()
4907
+ * for each agent when cancelling the session, using this map to enumerate
4908
+ * all three collab agents.
4730
4909
  */
4910
+ getSubagentIds() {
4911
+ return new Map(this.subagentIds);
4912
+ }
4731
4913
  async buildSnapshot() {
4732
4914
  if (this.snapshot.files.length > 0) return this.snapshot;
4733
4915
  for (const filePath of this.options.targetPaths) {
@@ -4743,9 +4925,30 @@ var CollabSession = class extends EventEmitter {
4743
4925
  return this.snapshot;
4744
4926
  }
4745
4927
  /**
4746
- * Start the collaborative session: snapshot files, spawn all three agents,
4747
- * wire up event routing, and wait for completion.
4928
+ * Cancel the session. Emits director.cancel_collab on the FleetBus so all
4929
+ * collab agents finish early. The session-level timeout timer is also cleared.
4930
+ * Safe to call multiple times (idempotent after first call).
4748
4931
  */
4932
+ cancel(reason = "Director cancelled collab session") {
4933
+ if (this.settled) return;
4934
+ this.cancelled = true;
4935
+ if (this._timeoutTimer) {
4936
+ clearTimeout(this._timeoutTimer);
4937
+ this._timeoutTimer = void 0;
4938
+ }
4939
+ this.fleetBus.emit({
4940
+ subagentId: this.director.id,
4941
+ ts: Date.now(),
4942
+ type: "director.cancel_collab",
4943
+ payload: { sessionId: this.sessionId, reason, cancelledAt: (/* @__PURE__ */ new Date()).toISOString() }
4944
+ });
4945
+ this.fleetBus.emit({
4946
+ subagentId: this.director.id,
4947
+ ts: Date.now(),
4948
+ type: "collab.cancelled",
4949
+ payload: { sessionId: this.sessionId, reason }
4950
+ });
4951
+ }
4749
4952
  async start() {
4750
4953
  if (this.settled) throw new Error("session already settled");
4751
4954
  this.settled = true;
@@ -4760,10 +4963,10 @@ var CollabSession = class extends EventEmitter {
4760
4963
  this.subagentIds.set("refactor-planner", refactorPlannerId);
4761
4964
  this.subagentIds.set("critic", criticId);
4762
4965
  const timeout = new Promise((_, reject) => {
4763
- setTimeout(
4764
- () => reject(new Error(`CollabSession timed out after ${this.timeoutMs}ms`)),
4765
- this.timeoutMs
4766
- );
4966
+ this._timeoutTimer = setTimeout(() => {
4967
+ this.cancel("Session-level timeout reached");
4968
+ reject(new Error(`CollabSession timed out after ${this.timeoutMs}ms`));
4969
+ }, this.timeoutMs);
4767
4970
  });
4768
4971
  let results;
4769
4972
  try {
@@ -4776,6 +4979,10 @@ var CollabSession = class extends EventEmitter {
4776
4979
  timeout
4777
4980
  ]);
4778
4981
  } catch (err) {
4982
+ if (this._timeoutTimer) {
4983
+ clearTimeout(this._timeoutTimer);
4984
+ this._timeoutTimer = void 0;
4985
+ }
4779
4986
  this.cleanup();
4780
4987
  const error = err instanceof Error ? err : new Error(String(err));
4781
4988
  this.emit("session.error", error);
@@ -4789,17 +4996,6 @@ var CollabSession = class extends EventEmitter {
4789
4996
  this.emit("session.done", report);
4790
4997
  return report;
4791
4998
  }
4792
- /**
4793
- * Parse a completed agent's final text output and route the structured
4794
- * findings onto the FleetBus. Agents are instructed to emit one JSON object
4795
- * per finding / plan / evaluation; we scan the output for balanced JSON
4796
- * objects (tolerating prose, markdown, and ```json fences around them) and
4797
- * dispatch each by its discriminating key. The wired FleetBus listeners then
4798
- * collect them into the bugs / plans / evaluations maps.
4799
- *
4800
- * Failed or empty results are ignored, and a single malformed object never
4801
- * aborts the collation pass — see {@link extractJsonObjects}.
4802
- */
4803
4999
  async parseAndEmit(result) {
4804
5000
  if (result.status !== "success" || result.result == null) return;
4805
5001
  const text = typeof result.result === "string" ? result.result : JSON.stringify(result.result);
@@ -4815,12 +5011,6 @@ var CollabSession = class extends EventEmitter {
4815
5011
  });
4816
5012
  }
4817
5013
  }
4818
- /**
4819
- * Extract every top-level JSON object embedded in free-form agent text.
4820
- * Scans for balanced braces while respecting string literals and escape
4821
- * sequences, so prose, markdown headers, and code fences around the JSON
4822
- * are skipped. Malformed spans are dropped silently rather than thrown.
4823
- */
4824
5014
  extractJsonObjects(text) {
4825
5015
  const objects = [];
4826
5016
  let depth = 0;
@@ -4857,25 +5047,33 @@ var CollabSession = class extends EventEmitter {
4857
5047
  }
4858
5048
  return objects;
4859
5049
  }
5050
+ budgetForRole(role) {
5051
+ if (this.options.budgetOverrides?.[role]) {
5052
+ return this.options.budgetOverrides[role];
5053
+ }
5054
+ const defaults = {
5055
+ "bug-hunter": { maxIterations: 2e3, maxToolCalls: 5e3, timeoutMs: 10 * 60 * 1e3 },
5056
+ "refactor-planner": { maxIterations: 1500, maxToolCalls: 4e3, timeoutMs: 8 * 60 * 1e3 },
5057
+ "critic": { maxIterations: 1e3, maxToolCalls: 3e3, timeoutMs: 6 * 60 * 1e3 }
5058
+ };
5059
+ return defaults[role] ?? { maxIterations: 1500, maxToolCalls: 4e3, timeoutMs: 8 * 60 * 1e3 };
5060
+ }
4860
5061
  async spawnAgent(role, taskBrief) {
5062
+ const budget = this.budgetForRole(role);
4861
5063
  const cfg = {
4862
5064
  id: `${role}-${this.sessionId}`,
4863
5065
  name: role,
4864
- role
4865
- // No fleet_emit tool agents output structured JSON, the Director parses
4866
- // results and emits FleetBus events. This keeps the agent simple and the
4867
- // routing deterministic.
5066
+ role,
5067
+ tools: ["fleet_emit", "fleet_status", "read", "grep", "glob", "bash", "write"],
5068
+ maxIterations: budget.maxIterations,
5069
+ maxToolCalls: budget.maxToolCalls,
5070
+ timeoutMs: budget.timeoutMs
4868
5071
  };
4869
5072
  const subagentId = await this.director.spawn(cfg);
4870
- await this.director.assign({
4871
- id: randomUUID(),
4872
- subagentId,
4873
- description: taskBrief
4874
- });
5073
+ await this.director.assign({ id: randomUUID(), subagentId, description: taskBrief });
4875
5074
  return subagentId;
4876
5075
  }
4877
5076
  buildBugHunterTask() {
4878
- this.options.targetPaths.join(", ");
4879
5077
  const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
4880
5078
  const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
4881
5079
  ${f.content}`).join("\n\n");
@@ -4884,13 +5082,17 @@ ${f.content}`).join("\n\n");
4884
5082
  Target files:
4885
5083
  ${fileContents}
4886
5084
 
4887
- For each bug found, write ONE valid JSON line to the scratchpad with this exact structure:
4888
- { "finding": { "id": "<uuid>", "type": "<pattern>", "severity": "<critical|high|medium|low>", "location": { "file": "<path>", "line": <n> }, "description": "<explain>", "suggestedFix": "<optional>" } }.
4889
- After scanning all files, write your full markdown bug report to the shared scratchpad at:
4890
- ${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
5085
+ For each bug found, emit it using the fleet_emit tool immediately:
5086
+ { "type": "bug.found", "payload": { "finding": { "id": "<uuid>", "type": "<pattern>", "severity": "<critical|high|medium|low>", "location": { "file": "<path>", "line": <n> }, "description": "<explain>", "suggestedFix": "<optional>" } } }
5087
+
5088
+ After scanning all files, write your full markdown bug report to:
5089
+ ${scratchpad}/bug-hunter-report-${this.sessionId}.md
5090
+
5091
+ Important: emit each finding as soon as you find it. Do not batch or wait until the end.`;
4891
5092
  }
4892
5093
  buildRefactorPlannerTask() {
4893
5094
  const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
5095
+ const bugHunterReportPath = `${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
4894
5096
  const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
4895
5097
  ${f.content}`).join("\n\n");
4896
5098
  return `You are RefactorPlanner. Plan refactorings for the following files.
@@ -4898,26 +5100,146 @@ ${f.content}`).join("\n\n");
4898
5100
  Target files:
4899
5101
  ${fileContents}
4900
5102
 
4901
- Listen for bug.found events from BugHunter on the fleet bus. For each bug, create a refactor plan and write one JSON line per plan:
4902
- { "plan": { "id": "<uuid>", "basedOnBugIds": ["<bug-id>"], "phases": [...], "riskScore": "<low|medium|high>", "estimatedChangeCount": <n>, "rollbackStrategy": "<text>" } }.
4903
- After planning, write your full markdown plan to:
4904
- ${scratchpad}/refactor-plan-${this.sessionId}.md`;
5103
+ Read the BugHunter report at: ${bugHunterReportPath}
5104
+
5105
+ For each bug you can address, emit a refactor plan using fleet_emit:
5106
+ { "type": "refactor.plan", "payload": { "plan": { "id": "<uuid>", "basedOnBugIds": ["<bug-id>"], "phases": [{ "number": 1, "title": "<phase>", "tasks": ["<task>"], "risk": "<low|medium|high>" }], "riskScore": "<low|medium|high>", "estimatedChangeCount": <n>, "rollbackStrategy": "<text>" } } }
5107
+
5108
+ Also write your full markdown plan to:
5109
+ ${scratchpad}/refactor-plan-${this.sessionId}.md
5110
+
5111
+ Emit each plan immediately. Do not wait until planning is complete.`;
4905
5112
  }
4906
5113
  buildCriticTask() {
4907
5114
  const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
5115
+ const bugHunterReportPath = `${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
5116
+ const refactorPlanPath = `${scratchpad}/refactor-plan-${this.sessionId}.md`;
4908
5117
  const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
4909
5118
  ${f.content}`).join("\n\n");
4910
- return `You are Critic. Evaluate bug findings and refactor plans as they arrive via fleet bus events.
5119
+ return `You are Critic. Evaluate bug findings and refactor plans.
4911
5120
 
4912
5121
  Target files:
4913
5122
  ${fileContents}
4914
5123
 
4915
- Subscribe to bug.found events (from BugHunter) and refactor.plan events (from RefactorPlanner). For each subject, write one JSON line per evaluation:
4916
- { "evaluation": { "id": "<uuid>", "subjectType": "<bug_finding|refactor_plan>", "subjectId": "<id>", "score": <0-10>, "verdict": "<approve|needs_revision|reject>", "strengths": [...], "weaknesses": [...], "concerns": [...] } }.
4917
- After all evaluations, write your markdown critic report to:
4918
- ${scratchpad}/critic-report-${this.sessionId}.md`;
5124
+ Read the BugHunter report at: ${bugHunterReportPath}
5125
+ Read the RefactorPlanner report at: ${refactorPlanPath}
5126
+
5127
+ For each bug and refactor plan, emit your evaluation using fleet_emit:
5128
+ { "type": "critic.evaluation", "payload": { "evaluation": { "id": "<uuid>", "subjectType": "<bug_finding|refactor_plan>", "subjectId": "<id>", "score": <0-10>, "verdict": "<approve|needs_revision|reject>", "strengths": ["<strength>"], "weaknesses": ["<weakness>"], "concerns": [{ "description": "<concern>", "severity": "<blocking|advisory>" }] } } }
5129
+
5130
+ After all evaluations, write your markdown report to:
5131
+ ${scratchpad}/critic-report-${this.sessionId}.md
5132
+
5133
+ Emit each evaluation immediately. Do not wait until you have read all reports.`;
4919
5134
  }
4920
5135
  wireFleetBus() {
5136
+ const dTool = this.fleetBus.filter("tool.executed", (e) => {
5137
+ this.progressBySubagent.set(e.subagentId, (this.progressBySubagent.get(e.subagentId) ?? 0) + 1);
5138
+ });
5139
+ this.disposers.push(dTool);
5140
+ const dBudget = this.fleetBus.filter("budget.threshold_reached", (e) => {
5141
+ const payload = e.payload;
5142
+ const role = this.roleFromSubagentId(e.subagentId);
5143
+ if (!role) return;
5144
+ const btwNotes = this.director.getLeaderBtwNotes();
5145
+ const alert = {
5146
+ sessionId: this.sessionId,
5147
+ subagentId: e.subagentId,
5148
+ role,
5149
+ level: "warning" /* WARNING */,
5150
+ message: `${role} hit ${payload.kind} soft limit (${payload.used}/${payload.limit})`,
5151
+ budgetKind: payload.kind,
5152
+ elapsedMs: payload.timeoutMs,
5153
+ limit: payload.limit,
5154
+ btwNotes
5155
+ };
5156
+ this.alerts.push(alert);
5157
+ this.fleetBus.emit({
5158
+ subagentId: e.subagentId,
5159
+ ts: Date.now(),
5160
+ type: "collab.warning",
5161
+ payload: alert
5162
+ });
5163
+ const decision = this.options.onBudgetWarning?.(alert) ?? "ignore";
5164
+ if (decision === "cancel") {
5165
+ this.cancel(`Director cancelled: ${role} ${payload.kind} threshold`);
5166
+ return;
5167
+ }
5168
+ if (payload.kind === "timeout") {
5169
+ const progress = this.progressBySubagent.get(e.subagentId) ?? 0;
5170
+ const lastProgress = this.lastTimeoutProgress.get(e.subagentId) ?? -1;
5171
+ if (progress <= lastProgress) {
5172
+ payload.deny();
5173
+ return;
5174
+ }
5175
+ this.lastTimeoutProgress.set(e.subagentId, progress);
5176
+ const newLimit = Math.min(Math.ceil((payload.timeoutMs ?? payload.limit) * 2), 24 * 60 * 6e4);
5177
+ setImmediate(() => {
5178
+ payload.extend({ timeoutMs: newLimit });
5179
+ });
5180
+ return;
5181
+ }
5182
+ if (decision === "extend") {
5183
+ setImmediate(() => {
5184
+ const base = Math.max(payload.limit, payload.used);
5185
+ const extra = {};
5186
+ switch (payload.kind) {
5187
+ case "iterations":
5188
+ extra.maxIterations = Math.min(Math.ceil(base * 1.5), 5e4);
5189
+ break;
5190
+ case "tool_calls":
5191
+ extra.maxToolCalls = Math.min(Math.ceil(base * 1.5), 1e5);
5192
+ break;
5193
+ case "tokens":
5194
+ extra.maxTokens = Math.min(Math.ceil(base * 1.5), 5e6);
5195
+ break;
5196
+ case "cost":
5197
+ extra.maxCostUsd = Math.min(base * 1.5, 100);
5198
+ break;
5199
+ }
5200
+ payload.extend(extra);
5201
+ });
5202
+ return;
5203
+ }
5204
+ if (payload.kind !== "timeout") {
5205
+ setImmediate(() => {
5206
+ const base = Math.max(payload.limit, payload.used);
5207
+ const extra = {};
5208
+ switch (payload.kind) {
5209
+ case "iterations":
5210
+ extra.maxIterations = Math.min(Math.ceil(base * 1.25), 5e4);
5211
+ break;
5212
+ case "tool_calls":
5213
+ extra.maxToolCalls = Math.min(Math.ceil(base * 1.25), 1e5);
5214
+ break;
5215
+ case "tokens":
5216
+ extra.maxTokens = Math.min(Math.ceil(base * 1.25), 5e6);
5217
+ break;
5218
+ case "cost":
5219
+ extra.maxCostUsd = Math.min(base * 1.25, 100);
5220
+ break;
5221
+ }
5222
+ payload.extend(extra);
5223
+ });
5224
+ }
5225
+ });
5226
+ this.disposers.push(dBudget);
5227
+ const dCancel = this.fleetBus.filter("director.cancel_collab", (e) => {
5228
+ const payload = e.payload;
5229
+ if (payload.sessionId !== this.sessionId) return;
5230
+ this.cancelled = true;
5231
+ if (this._timeoutTimer) {
5232
+ clearTimeout(this._timeoutTimer);
5233
+ this._timeoutTimer = void 0;
5234
+ }
5235
+ this.fleetBus.emit({
5236
+ subagentId: this.director.id,
5237
+ ts: Date.now(),
5238
+ type: "collab.cancelled",
5239
+ payload: { sessionId: this.sessionId, reason: payload.reason }
5240
+ });
5241
+ });
5242
+ this.disposers.push(dCancel);
4921
5243
  const d1 = this.fleetBus.filter("bug.found", (e) => {
4922
5244
  const payload = e.payload;
4923
5245
  if (payload?.finding) {
@@ -4943,10 +5265,19 @@ ${scratchpad}/critic-report-${this.sessionId}.md`;
4943
5265
  });
4944
5266
  this.disposers.push(d3);
4945
5267
  }
5268
+ roleFromSubagentId(subagentId) {
5269
+ for (const [role, id] of this.subagentIds) {
5270
+ if (id === subagentId) return role;
5271
+ }
5272
+ const match = subagentId.match(/^(bug-hunter|refactor-planner|critic)/);
5273
+ return match?.[1] ?? null;
5274
+ }
4946
5275
  assembleReport() {
4947
5276
  const bugList = Array.from(this.bugs.values());
4948
5277
  const planList = Array.from(this.plans.values());
4949
5278
  const evalList = Array.from(this.evaluations.values());
5279
+ let disposition = "completed";
5280
+ if (this.cancelled) disposition = "cancelled";
4950
5281
  const verdictOrder = {
4951
5282
  approve: 0,
4952
5283
  needs_revision: 1,
@@ -4960,33 +5291,41 @@ ${scratchpad}/critic-report-${this.sessionId}.md`;
4960
5291
  },
4961
5292
  "approve"
4962
5293
  );
4963
- const summary = this.buildMarkdownSummary(bugList, planList, evalList, overallVerdict);
5294
+ const summary = this.buildMarkdownSummary(bugList, planList, evalList, overallVerdict, disposition);
4964
5295
  return {
4965
5296
  sessionId: this.sessionId,
4966
5297
  startedAt: this.snapshot.createdAt,
4967
5298
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
4968
5299
  targetPaths: this.options.targetPaths,
5300
+ disposition,
4969
5301
  bugs: bugList,
4970
5302
  refactorPlans: planList,
4971
5303
  evaluations: evalList,
5304
+ alerts: [...this.alerts],
4972
5305
  overallVerdict,
4973
5306
  summary
4974
5307
  };
4975
5308
  }
4976
- buildMarkdownSummary(bugs, plans, evals, overallVerdict) {
5309
+ buildMarkdownSummary(bugs, plans, evals, overallVerdict, disposition) {
4977
5310
  const lines = [
4978
5311
  `## Collaborative Debugging Report \u2014 ${this.sessionId}`,
4979
5312
  "",
4980
5313
  `**Target:** ${this.options.targetPaths.join(", ")}`,
5314
+ `**Disposition:** ${disposition.toUpperCase()}`,
4981
5315
  `**Overall Verdict:** **${overallVerdict.toUpperCase()}**`,
4982
5316
  ""
4983
5317
  ];
5318
+ if (this.alerts.length > 0) {
5319
+ lines.push("### Alerts", "");
5320
+ for (const alert of this.alerts) {
5321
+ lines.push(`- **[${alert.level.toUpperCase()}]** ${alert.role}: ${alert.message}`);
5322
+ }
5323
+ lines.push("");
5324
+ }
4984
5325
  if (bugs.length > 0) {
4985
5326
  lines.push("### Bugs Found", "");
4986
5327
  for (const b of bugs) {
4987
- lines.push(
4988
- `- **[${b.severity.toUpperCase()}]** \`${b.location.file}:${b.location.line}\` \u2014 ${b.description}`
4989
- );
5328
+ lines.push(`- **[${b.severity.toUpperCase()}]** \`${b.location.file}:${b.location.line}\` \u2014 ${b.description}`);
4990
5329
  }
4991
5330
  lines.push("");
4992
5331
  }
@@ -5005,9 +5344,7 @@ ${scratchpad}/critic-report-${this.sessionId}.md`;
5005
5344
  for (const e of evals) {
5006
5345
  lines.push(`- [${e.subjectType}] score=${e.score}/10 \u2014 **${e.verdict.toUpperCase()}**`);
5007
5346
  for (const c of e.concerns) {
5008
- if (c.severity === "blocking") {
5009
- lines.push(` - ${c.description}`);
5010
- }
5347
+ if (c.severity === "blocking") lines.push(` - ${c.description}`);
5011
5348
  }
5012
5349
  }
5013
5350
  lines.push("");
@@ -5025,10 +5362,9 @@ var FleetSpawnBudgetError = class extends Error {
5025
5362
  kind;
5026
5363
  limit;
5027
5364
  observed;
5028
- constructor(kind, limit, observed) {
5029
- super(
5030
- kind === "max_spawns" ? `Director spawn budget exceeded: tried to spawn #${observed} but maxSpawns is ${limit}` : `Director spawn depth budget exceeded: this director is at depth ${observed} and maxSpawnDepth is ${limit}`
5031
- );
5365
+ constructor(kind, limit, observed, message) {
5366
+ const defaultMsg = kind === "max_spawns" ? `Director spawn budget exceeded: tried to spawn #${observed} but maxSpawns is ${limit}` : `Director spawn depth budget exceeded: this director is at depth ${observed} and maxSpawnDepth is ${limit}`;
5367
+ super(message ?? defaultMsg);
5032
5368
  this.name = "FleetSpawnBudgetError";
5033
5369
  this.kind = kind;
5034
5370
  this.limit = limit;
@@ -5049,6 +5385,20 @@ var FleetCostCapError = class extends Error {
5049
5385
  this.observed = observed;
5050
5386
  }
5051
5387
  };
5388
+ var FleetContextOverflowError = class extends Error {
5389
+ kind;
5390
+ limit;
5391
+ observed;
5392
+ constructor(limit, observed) {
5393
+ super(
5394
+ `Leader context overflow: leader has ${observed} tokens in flight (limit: ${limit}). Compact the leader context before spawning more subagents.`
5395
+ );
5396
+ this.name = "FleetContextOverflowError";
5397
+ this.kind = "max_context_load";
5398
+ this.limit = limit;
5399
+ this.observed = observed;
5400
+ }
5401
+ };
5052
5402
  var Director = class {
5053
5403
  /** Alias for the ICoordinator contract. `id` is retained for backward compatibility. */
5054
5404
  get coordinatorId() {
@@ -5065,6 +5415,25 @@ var Director = class {
5065
5415
  * injected; otherwise own FleetUsageAggregator.
5066
5416
  */
5067
5417
  usage;
5418
+ /**
5419
+ * Update the leader agent's current context pressure (full request tokens:
5420
+ * messages + systemPrompt + toolDefs). The director checks this before every
5421
+ * spawn — if the pressure exceeds `maxLeaderContextLoad * maxContext`,
5422
+ * spawning is refused with a `FleetContextOverflowError`.
5423
+ *
5424
+ * Call this after each leader agent iteration to keep the pressure current.
5425
+ * The compactor's `CompactReport.fullRequestTokensAfter` is a natural source.
5426
+ */
5427
+ setLeaderContextPressure(tokens) {
5428
+ this.leaderContextPressure = tokens;
5429
+ this.fleetManager?.setLeaderContextPressure(tokens);
5430
+ }
5431
+ /**
5432
+ * Read the leader agent's current context pressure.
5433
+ */
5434
+ getLeaderContextPressure() {
5435
+ return this.leaderContextPressure;
5436
+ }
5068
5437
  /**
5069
5438
  * Optional fleet-level policy container. When provided the Director
5070
5439
  * delegates spawn budgeting, manifest entries, and checkpointing to it
@@ -5150,6 +5519,23 @@ var Director = class {
5150
5519
  taskCompletedListener = null;
5151
5520
  /** Optional LLM classifier for smart dispatch. Passed from options. */
5152
5521
  dispatchClassifier;
5522
+ /** Leader agent's current context pressure (full request tokens). */
5523
+ leaderContextPressure = 0;
5524
+ /** Maximum context load fraction before spawn is refused. */
5525
+ maxLeaderContextLoad;
5526
+ /** Provider's max context window in tokens. */
5527
+ maxContext;
5528
+ /**
5529
+ * When set by `workComplete()`, the director stops dispatching new tasks
5530
+ * and terminates all running subagents. Used when the director's LLM decides
5531
+ * the goal is satisfied and no further spawns are needed — prevents the
5532
+ * coordinator from keeping workers alive for tasks that will never arrive.
5533
+ */
5534
+ workCompleteFlag = false;
5535
+ /** Pending /btw notes stashed by the leader agent (see setLeaderBtwNote). */
5536
+ _leaderBtwNotes = [];
5537
+ /** Active collab sessions tracked by sessionId (see spawnCollab). */
5538
+ _activeCollabSessions = /* @__PURE__ */ new Map();
5153
5539
  constructor(opts) {
5154
5540
  this.id = opts.config.coordinatorId || randomUUID();
5155
5541
  this.manifestPath = opts.manifestPath;
@@ -5165,6 +5551,8 @@ var Director = class {
5165
5551
  this.dispatchClassifier = opts.dispatchClassifier;
5166
5552
  this.maxFleetCostUsd = opts.directorBudget?.maxCostUsd ?? Number.POSITIVE_INFINITY;
5167
5553
  this.maxBudgetExtensions = opts.maxBudgetExtensions ?? 5;
5554
+ this.maxLeaderContextLoad = opts.maxLeaderContextLoad ?? 0.85;
5555
+ this.maxContext = opts.maxContext ?? 128e3;
5168
5556
  this.sessionsRoot = opts.sessionsRoot;
5169
5557
  this.directorRunId = opts.directorRunId ?? this.id;
5170
5558
  this.stateCheckpoint = opts.stateCheckpointPath ? new DirectorStateCheckpoint(opts.stateCheckpointPath, {
@@ -5192,7 +5580,7 @@ var Director = class {
5192
5580
  this.fleet = new FleetBus();
5193
5581
  this.usage = new FleetUsageAggregator(
5194
5582
  this.fleet,
5195
- (id, provider, model) => {
5583
+ (_id, provider, model) => {
5196
5584
  if (provider && model) return this.priceLookups.get(`${provider}/${model}`);
5197
5585
  return void 0;
5198
5586
  },
@@ -5254,6 +5642,9 @@ var Director = class {
5254
5642
  });
5255
5643
  this.fleet.filter("budget.threshold_reached", (e) => {
5256
5644
  const payload = e.payload;
5645
+ if (e.subagentId.startsWith("bug-hunter-") || e.subagentId.startsWith("refactor-planner-") || e.subagentId.startsWith("critic-")) {
5646
+ return;
5647
+ }
5257
5648
  if (payload.kind === "timeout") {
5258
5649
  const progress = progressBySubagent.get(e.subagentId) ?? 0;
5259
5650
  const lastProgress = lastTimeoutProgress.get(e.subagentId) ?? -1;
@@ -5332,6 +5723,107 @@ var Director = class {
5332
5723
  extensionsFor(subagentId) {
5333
5724
  return this.extendTotals.get(subagentId) ?? 0;
5334
5725
  }
5726
+ /**
5727
+ * Signal that the director's work is done. Once called:
5728
+ * - `spawn()` throws `FleetSpawnBudgetError('max_spawns', …)` — no new
5729
+ * subagents can be created
5730
+ * - Running subagents are NOT forcibly stopped — they finish naturally,
5731
+ * but no new tasks are dispatched to them
5732
+ *
5733
+ * This lets the director LLM say "I'm satisfied with the results, stop
5734
+ * spawning and wind down" — without killing in-flight work mid-execution.
5735
+ * Call `terminateAll()` separately if you need immediate teardown.
5736
+ *
5737
+ * Idempotent — calling twice is a no-op.
5738
+ */
5739
+ workComplete() {
5740
+ this.workCompleteFlag = true;
5741
+ this.fleet.emit({ subagentId: this.id, ts: Date.now(), type: "director.work_complete", payload: {} });
5742
+ }
5743
+ /** Returns true if `workComplete()` has been called on this director. */
5744
+ isWorkComplete() {
5745
+ return this.workCompleteFlag;
5746
+ }
5747
+ /**
5748
+ * Stashes a /btw note on the leader agent's context. The leader's agent loop
5749
+ * calls `consumeBtwNotes()` at each iteration boundary and folds pending notes
5750
+ * into the conversation as a visible block — no abort, no restart, just a
5751
+ * "by the way" nudge the model picks up on its next turn.
5752
+ *
5753
+ * This is the entry point for the host (CLI, TUI) to inject /btw notes
5754
+ * programmatically without going through the slash-command path.
5755
+ */
5756
+ setLeaderBtwNote(note) {
5757
+ const trimmed = note.trim();
5758
+ if (!trimmed) return this._leaderBtwNotes.length;
5759
+ this._leaderBtwNotes = [...this._leaderBtwNotes, trimmed].slice(-20);
5760
+ return this._leaderBtwNotes.length;
5761
+ }
5762
+ /**
5763
+ * Read and clear all pending /btw notes the leader has stashed.
5764
+ * Returns them in FIFO order (empty array when none).
5765
+ *
5766
+ * Called by CollabSession when a budget threshold event fires so the
5767
+ * Director can inspect accumulated /btw notes before deciding whether
5768
+ * to cancel the collab session or let it continue.
5769
+ */
5770
+ getLeaderBtwNotes() {
5771
+ const notes = this._leaderBtwNotes;
5772
+ this._leaderBtwNotes = [];
5773
+ return notes;
5774
+ }
5775
+ /**
5776
+ * Peek at pending /btw notes without consuming them.
5777
+ * Useful for UI to show "N pending notes" without clearing them.
5778
+ */
5779
+ peekLeaderBtwNotes() {
5780
+ return [...this._leaderBtwNotes];
5781
+ }
5782
+ /**
5783
+ * Drain (read + clear) all /btw notes in one call.
5784
+ * Alias for getLeaderBtwNotes() — kept for symmetry with consumeBtwNotes()
5785
+ * in the agent's btw.ts. The Director calls this at the point where it
5786
+ * makes a cancellation decision, not on every budget event.
5787
+ */
5788
+ drainLeaderBtwNotes() {
5789
+ return this.getLeaderBtwNotes();
5790
+ }
5791
+ /**
5792
+ * Cancel an active collab session by its id.
5793
+ * Emits `director.cancel_collab` on the FleetBus so the session's agents
5794
+ * finish early with a 'cancelled' disposition.
5795
+ *
5796
+ * Returns silently if the session id is not tracked or already settled.
5797
+ * The CollabDebugReport will reflect 'cancelled' disposition when awaited.
5798
+ */
5799
+ cancelCollabSession(sessionId, reason = "Director cancelled") {
5800
+ const session = this._activeCollabSessions.get(sessionId);
5801
+ if (!session || session.isCancelled()) return;
5802
+ session.cancel(reason);
5803
+ for (const [_role, subagentId] of session.getSubagentIds()) {
5804
+ this.coordinator.stop(subagentId).catch(() => {
5805
+ });
5806
+ }
5807
+ }
5808
+ /**
5809
+ * Subscribe a callback to be notified whenever a collab session raises
5810
+ * an alert (warning level). The callback receives the full DirectorAlert
5811
+ * payload so the host (CLI, TUI) can display it to the user.
5812
+ * Returns an unsubscribe function.
5813
+ */
5814
+ onCollabAlert(handler) {
5815
+ return this.fleet.filter("collab.warning", (e) => {
5816
+ handler(e.payload);
5817
+ });
5818
+ }
5819
+ /**
5820
+ * Returns all active (not yet settled) collab session ids.
5821
+ * Useful for the TUI to render a "N active sessions" badge and for
5822
+ * the host to know what can be cancelled.
5823
+ */
5824
+ activeCollabSessions() {
5825
+ return Array.from(this._activeCollabSessions.keys());
5826
+ }
5335
5827
  /** Best-effort session-writer append. Swallows failures — the director
5336
5828
  * must not break a fleet run because the session JSONL handle closed. */
5337
5829
  async appendSessionEvent(event) {
@@ -5370,12 +5862,21 @@ var Director = class {
5370
5862
  * it the `cost` column in `usage.snapshot()` stays at 0.
5371
5863
  */
5372
5864
  async spawn(config, priceLookup) {
5865
+ if (this.workCompleteFlag) {
5866
+ throw new FleetSpawnBudgetError(
5867
+ "max_spawns",
5868
+ this.maxSpawns,
5869
+ this.spawnCount + 1,
5870
+ "workComplete() has been called \u2014 director closed further spawning"
5871
+ );
5872
+ }
5373
5873
  if (this.fleetManager) {
5374
5874
  const rejection = this.fleetManager.canSpawn(config);
5375
5875
  if (rejection) {
5376
5876
  if (rejection.kind === "max_spawn_depth") throw new FleetSpawnBudgetError("max_spawn_depth", rejection.limit, rejection.observed);
5377
5877
  if (rejection.kind === "max_spawns") throw new FleetSpawnBudgetError("max_spawns", rejection.limit, rejection.observed);
5378
5878
  if (rejection.kind === "max_cost_usd") throw new FleetCostCapError(rejection.limit, rejection.observed);
5879
+ if (rejection.kind === "max_context_load") throw new FleetContextOverflowError(rejection.limit, rejection.observed);
5379
5880
  }
5380
5881
  } else {
5381
5882
  if (this.spawnDepth >= this.maxSpawnDepth) {
@@ -5390,9 +5891,15 @@ var Director = class {
5390
5891
  throw new FleetCostCapError(this.maxFleetCostUsd, totalCost);
5391
5892
  }
5392
5893
  }
5894
+ if (this.maxLeaderContextLoad < 1) {
5895
+ const threshold = this.maxContext * this.maxLeaderContextLoad;
5896
+ if (this.leaderContextPressure >= threshold) {
5897
+ throw new FleetContextOverflowError(threshold, this.leaderContextPressure);
5898
+ }
5899
+ }
5393
5900
  }
5394
5901
  let result;
5395
- const needsNickname = config.name === config.role || !config.name || config.name === "subagent";
5902
+ const needsNickname = config.name === config.role || !config.name || config.name === "subagent" || config.name === "adhoc";
5396
5903
  if (needsNickname) {
5397
5904
  const role = config.role ?? "subagent";
5398
5905
  if (this.fleetManager) {
@@ -5617,6 +6124,24 @@ var Director = class {
5617
6124
  */
5618
6125
  async assign(task) {
5619
6126
  const taskWithId = task.id ? task : { ...task, id: randomUUID() };
6127
+ if (this.workCompleteFlag) {
6128
+ const synthetic = {
6129
+ subagentId: taskWithId.subagentId ?? "unassigned",
6130
+ taskId: taskWithId.id,
6131
+ status: "stopped",
6132
+ error: { kind: "aborted_by_parent", message: "Director called workComplete() \u2014 no further tasks will run", retryable: false },
6133
+ iterations: 0,
6134
+ toolCalls: 0,
6135
+ durationMs: 0
6136
+ };
6137
+ this.completed.set(taskWithId.id, synthetic);
6138
+ const waiter = this.taskWaiters.get(taskWithId.id);
6139
+ if (waiter) {
6140
+ waiter.resolve(synthetic);
6141
+ this.taskWaiters.delete(taskWithId.id);
6142
+ }
6143
+ return taskWithId.id;
6144
+ }
5620
6145
  if (task.subagentId) {
5621
6146
  if (this.fleetManager) {
5622
6147
  this.fleetManager.addTaskToSubagent(task.subagentId, taskWithId.id);
@@ -5839,12 +6364,14 @@ var Director = class {
5839
6364
  makeAskTool(this),
5840
6365
  makeRollUpTool(this),
5841
6366
  makeTerminateTool(this),
6367
+ makeTerminateAllTool(this),
5842
6368
  makeFleetStatusTool(this),
5843
6369
  makeFleetUsageTool(this),
5844
6370
  makeFleetSessionTool(this),
5845
6371
  makeFleetHealthTool(this),
5846
6372
  makeCollabDebugTool(this),
5847
- makeFleetEmitTool(this)
6373
+ makeFleetEmitTool(this),
6374
+ makeWorkCompleteTool(this)
5848
6375
  ];
5849
6376
  return t;
5850
6377
  }
@@ -5864,7 +6391,15 @@ var Director = class {
5864
6391
  * three agents complete or the session times out.
5865
6392
  */
5866
6393
  async spawnCollab(options) {
5867
- const session = new CollabSession(this, this.fleet, options);
6394
+ const session = new CollabSession(this, this.fleet, {
6395
+ ...options,
6396
+ onBudgetWarning: (alert) => {
6397
+ return options.onBudgetWarning?.(alert) ?? "ignore";
6398
+ }
6399
+ });
6400
+ this._activeCollabSessions.set(session.sessionId, session);
6401
+ session.on("session.done", () => this._activeCollabSessions.delete(session.sessionId));
6402
+ session.on("session.error", () => this._activeCollabSessions.delete(session.sessionId));
5868
6403
  return session.start();
5869
6404
  }
5870
6405
  /**
@@ -6282,6 +6817,7 @@ function makeAgentSubagentRunner(opts) {
6282
6817
  // Cheap: O(1) per progress event, and the budget short-circuits
6283
6818
  // when timeoutMs is unset (most subagents have one set anyway).
6284
6819
  events.on("tool.progress", () => {
6820
+ ctx.budget.markActivity();
6285
6821
  try {
6286
6822
  ctx.budget.checkTimeout();
6287
6823
  } catch (e) {
@@ -6298,6 +6834,7 @@ function makeAgentSubagentRunner(opts) {
6298
6834
  currentToolName = e.name;
6299
6835
  }),
6300
6836
  events.on("provider.text_delta", (e) => {
6837
+ ctx.budget.markActivity();
6301
6838
  streamingTextAcc = (streamingTextAcc + e.text).slice(-200);
6302
6839
  })
6303
6840
  );
@@ -6777,7 +7314,6 @@ var FileSessionWriter = class {
6777
7314
  }
6778
7315
  return event;
6779
7316
  }
6780
- promptIndex = 0;
6781
7317
  pendingFileSnapshots = [];
6782
7318
  /** Tracks open tool_use IDs during the current run to serialize on close for resume. */
6783
7319
  openToolUses = /* @__PURE__ */ new Set();
@@ -6868,7 +7404,6 @@ var FileSessionWriter = class {
6868
7404
  await this.writeFileSnapshot(promptIndex, [...this.pendingFileSnapshots]);
6869
7405
  this.pendingFileSnapshots = [];
6870
7406
  }
6871
- this.promptIndex = promptIndex + 1;
6872
7407
  await this.append({
6873
7408
  type: "checkpoint",
6874
7409
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -6964,6 +7499,38 @@ var FileSessionWriter = class {
6964
7499
  `;
6965
7500
  await fsp5.writeFile(this.filePath, record, "utf8");
6966
7501
  }
7502
+ /**
7503
+ * Idea #1 — write an in-flight marker. The agent loop should call
7504
+ * this at the start of each long-running operation; a matching
7505
+ * `clearInFlightMarker` follows on clean exit. A stale marker
7506
+ * (no end) is what `SessionRecovery.detectStale` looks for.
7507
+ */
7508
+ async writeInFlightMarker(context) {
7509
+ if (!context || context.length > 500) {
7510
+ throw new Error("In-flight context must be 1..500 chars");
7511
+ }
7512
+ await this.append({
7513
+ type: "in_flight_start",
7514
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
7515
+ context
7516
+ });
7517
+ this.events?.emit("in_flight.started", { context, ts: (/* @__PURE__ */ new Date()).toISOString() });
7518
+ }
7519
+ /**
7520
+ * Idea #1 — close the in-flight marker. Idempotent in spirit
7521
+ * (you can call it after a successful iteration even if you
7522
+ * didn't open one this round) — but the session log records
7523
+ * every call so postmortem tooling can see "the agent finished
7524
+ * cleanly X times, then died without finishing Y".
7525
+ */
7526
+ async clearInFlightMarker(reason) {
7527
+ await this.append({
7528
+ type: "in_flight_end",
7529
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
7530
+ reason
7531
+ });
7532
+ this.events?.emit("in_flight.ended", { reason, ts: (/* @__PURE__ */ new Date()).toISOString() });
7533
+ }
6967
7534
  };
6968
7535
  function userInputTitle(content) {
6969
7536
  const text = typeof content === "string" ? content : content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
@@ -7064,7 +7631,6 @@ var FleetManager = class {
7064
7631
  /** Usage rollup across all subagents. */
7065
7632
  usage;
7066
7633
  manifestPath;
7067
- sessionsRoot;
7068
7634
  directorRunId;
7069
7635
  /** Spawn cap (lifetime total). Infinity means unlimited. */
7070
7636
  maxSpawns;
@@ -7092,9 +7658,14 @@ var FleetManager = class {
7092
7658
  _usedNicknames = /* @__PURE__ */ new Set();
7093
7659
  /** The coordinator (wired via setCoordinator by Director after construction). */
7094
7660
  coordinator = null;
7661
+ /** Leader agent's current context pressure (full request tokens). */
7662
+ leaderContextPressure = 0;
7663
+ /** Maximum context load fraction before spawn is refused. */
7664
+ maxLeaderContextLoad;
7665
+ /** Provider's max context window in tokens. */
7666
+ maxContext;
7095
7667
  constructor(opts = {}) {
7096
7668
  this.manifestPath = opts.manifestPath;
7097
- this.sessionsRoot = opts.sessionsRoot;
7098
7669
  this.directorRunId = opts.directorRunId ?? randomUUID();
7099
7670
  this.maxSpawns = opts.maxSpawns ?? Number.POSITIVE_INFINITY;
7100
7671
  this.maxSpawnDepth = opts.maxSpawnDepth ?? 2;
@@ -7102,6 +7673,8 @@ var FleetManager = class {
7102
7673
  this.sessionWriter = opts.sessionWriter ?? null;
7103
7674
  this.manifestDebounceMs = opts.manifestDebounceMs ?? 2e3;
7104
7675
  this.maxFleetCostUsd = opts.directorBudget?.maxCostUsd ?? Number.POSITIVE_INFINITY;
7676
+ this.maxLeaderContextLoad = opts.maxLeaderContextLoad ?? 0.85;
7677
+ this.maxContext = opts.maxContext ?? 128e3;
7105
7678
  this.stateCheckpoint = opts.stateCheckpointPath ? new DirectorStateCheckpoint(
7106
7679
  opts.stateCheckpointPath,
7107
7680
  {
@@ -7116,7 +7689,7 @@ var FleetManager = class {
7116
7689
  this.fleet = new FleetBus();
7117
7690
  this.usage = new FleetUsageAggregator(
7118
7691
  this.fleet,
7119
- (id, provider, model) => {
7692
+ (_id, provider, model) => {
7120
7693
  if (provider && model) return this.priceLookups.get(`${provider}/${model}`);
7121
7694
  return void 0;
7122
7695
  },
@@ -7155,7 +7728,7 @@ var FleetManager = class {
7155
7728
  * which cap was exceeded. Does NOT throw — the caller decides
7156
7729
  * how to surface the rejection.
7157
7730
  */
7158
- canSpawn(config) {
7731
+ canSpawn(_config) {
7159
7732
  if (this.spawnDepth >= this.maxSpawnDepth) {
7160
7733
  return { kind: "max_spawn_depth", limit: this.maxSpawnDepth, observed: this.spawnDepth };
7161
7734
  }
@@ -7168,8 +7741,21 @@ var FleetManager = class {
7168
7741
  return { kind: "max_cost_usd", limit: this.maxFleetCostUsd, observed: totalCost };
7169
7742
  }
7170
7743
  }
7744
+ if (this.maxLeaderContextLoad < 1) {
7745
+ const threshold = this.maxContext * this.maxLeaderContextLoad;
7746
+ if (this.leaderContextPressure >= threshold) {
7747
+ return {
7748
+ kind: "max_context_load",
7749
+ limit: threshold,
7750
+ observed: this.leaderContextPressure
7751
+ };
7752
+ }
7753
+ }
7171
7754
  return null;
7172
7755
  }
7756
+ setLeaderContextPressure(tokens) {
7757
+ this.leaderContextPressure = tokens;
7758
+ }
7173
7759
  /**
7174
7760
  * Assign a memorable nickname (e.g. "Einstein (Bug Hunter)") to the config,
7175
7761
  * record it so the same name is never reused, then record the spawn.
@@ -7364,6 +7950,6 @@ var FleetManager = class {
7364
7950
  }
7365
7951
  };
7366
7952
 
7367
- export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, TOOLS as AGENT_TOOL_PRESETS, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, BUG_HUNTER_AGENT, BUILD_AGENTS, BudgetExceededError, BudgetThresholdSignal, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_SUBAGENT_BASELINE, DELIVERY_AGENTS, DISCOVERY_AGENTS, DOMAIN_AGENTS, DefaultMultiAgentCoordinator, Director, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FleetBus, FleetCostCapError, FleetManager, FleetSpawnBudgetError, FleetUsageAggregator, HEAVY_BUDGET, InMemoryAgentBridge, InMemoryBridgeTransport, KNOWLEDGE_AGENTS, LIGHT_BUDGET, MEDIUM_BUDGET, META_AGENTS, NULL_FLEET_BUS, PLANNING_AGENTS, REFACTOR_PLANNER_AGENT, REVIEW_AGENTS, SECURITY_SCANNER_AGENT, SubagentBudget, VERIFY_AGENTS, applyRosterBudget, attachAutoExtend, composeDirectorPrompt, composeSubagentPrompt, createDelegateTool, createMessage, dispatchAgent, getAgentDefinition, makeAgentSubagentRunner, makeAskTool, makeAssignTool, makeAwaitTasksTool, makeCollabDebugTool, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeRollUpTool, makeSpawnTool, makeTerminateTool, rosterSummaryFromConfigs, scoreAgents };
7953
+ export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, TOOLS as AGENT_TOOL_PRESETS, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, BUG_HUNTER_AGENT, BUILD_AGENTS, BudgetExceededError, BudgetThresholdSignal, CollabSession, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_SUBAGENT_BASELINE, DELIVERY_AGENTS, DISCOVERY_AGENTS, DOMAIN_AGENTS, DefaultMultiAgentCoordinator, Director, DirectorAlertLevel, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FleetBus, FleetCostCapError, FleetManager, FleetSpawnBudgetError, FleetUsageAggregator, HEAVY_BUDGET, InMemoryAgentBridge, InMemoryBridgeTransport, KNOWLEDGE_AGENTS, LIGHT_BUDGET, MEDIUM_BUDGET, META_AGENTS, NULL_FLEET_BUS, PLANNING_AGENTS, REFACTOR_PLANNER_AGENT, REVIEW_AGENTS, SECURITY_SCANNER_AGENT, SubagentBudget, VERIFY_AGENTS, applyRosterBudget, attachAutoExtend, composeDirectorPrompt, composeSubagentPrompt, createDelegateTool, createMessage, dispatchAgent, getAgentDefinition, makeAgentSubagentRunner, makeAskTool, makeAssignTool, makeAwaitTasksTool, makeCollabDebugTool, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeRollUpTool, makeSpawnTool, makeTerminateTool, makeWorkCompleteTool, rosterSummaryFromConfigs, scoreAgents };
7368
7954
  //# sourceMappingURL=index.js.map
7369
7955
  //# sourceMappingURL=index.js.map