@wrongstack/core 0.9.19 → 0.9.20

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 (63) hide show
  1. package/dist/{agent-bridge-DMVOX0cF.d.ts → agent-bridge-Dti3KXGk.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-C4qt9e5Y.d.ts → agent-subagent-runner-U-rs7kk7.d.ts} +3 -4
  3. package/dist/compactor-D7X96RLZ.d.ts +41 -0
  4. package/dist/{config-CWva0qoL.d.ts → config-CLXMDOSs.d.ts} +1 -1
  5. package/dist/{context-BRNbHmRM.d.ts → context-zkZeILpr.d.ts} +46 -0
  6. package/dist/coordination/index.d.ts +13 -13
  7. package/dist/coordination/index.js +660 -146
  8. package/dist/coordination/index.js.map +1 -1
  9. package/dist/defaults/index.d.ts +20 -20
  10. package/dist/defaults/index.js +918 -350
  11. package/dist/defaults/index.js.map +1 -1
  12. package/dist/{events-CiG9qUM_.d.ts → events-DH-9r-_C.d.ts} +42 -1
  13. package/dist/execution/index.d.ts +41 -30
  14. package/dist/execution/index.js +358 -112
  15. package/dist/execution/index.js.map +1 -1
  16. package/dist/extension/index.d.ts +6 -6
  17. package/dist/extension/index.js.map +1 -1
  18. package/dist/{index-aizK8olO.d.ts → index-BIHJ4uII.d.ts} +11 -8
  19. package/dist/{index-p95HQ22A.d.ts → index-CFO9QmJo.d.ts} +8 -8
  20. package/dist/index.d.ts +311 -35
  21. package/dist/index.js +1933 -512
  22. package/dist/index.js.map +1 -1
  23. package/dist/infrastructure/index.d.ts +6 -6
  24. package/dist/infrastructure/index.js +36 -0
  25. package/dist/infrastructure/index.js.map +1 -1
  26. package/dist/kernel/index.d.ts +9 -9
  27. package/dist/kernel/index.js.map +1 -1
  28. package/dist/{mcp-servers-BkVEqkRe.d.ts → mcp-servers-DkESgh0G.d.ts} +25 -3
  29. package/dist/models/index.d.ts +2 -2
  30. package/dist/models/index.js +1 -1
  31. package/dist/models/index.js.map +1 -1
  32. package/dist/{multi-agent-3ZnTB1aT.d.ts → multi-agent-DNp6lAzg.d.ts} +35 -23
  33. package/dist/{multi-agent-coordinator-bRaI_aD1.d.ts → multi-agent-coordinator-CAhsegPz.d.ts} +20 -2
  34. package/dist/{null-fleet-bus-DKM3Iy9d.d.ts → null-fleet-bus-Dnl19vmf.d.ts} +411 -110
  35. package/dist/observability/index.d.ts +2 -2
  36. package/dist/{path-resolver-TcJfc29Y.d.ts → path-resolver-CHiBL0DD.d.ts} +2 -2
  37. package/dist/{permission-bPuzAy4x.d.ts → permission-H35s9Evv.d.ts} +1 -1
  38. package/dist/{permission-policy-BUQSutpl.d.ts → permission-policy-CT-nRmTn.d.ts} +2 -2
  39. package/dist/{plan-templates-fkQTyz3U.d.ts → plan-templates-Bs8iRwi6.d.ts} +4 -4
  40. package/dist/{provider-runner-BEpikbbN.d.ts → provider-runner-BZdDrWrS.d.ts} +3 -3
  41. package/dist/{retry-policy-BYkq0ugs.d.ts → retry-policy-J9N_PM40.d.ts} +1 -1
  42. package/dist/sdd/index.d.ts +9 -10
  43. package/dist/sdd/index.js +224 -68
  44. package/dist/sdd/index.js.map +1 -1
  45. package/dist/security/index.d.ts +3 -3
  46. package/dist/{selector-pox8abg0.d.ts → selector-CFTh3Z6p.d.ts} +1 -1
  47. package/dist/{session-reader-CSWcb5Ga.d.ts → session-reader-C7JJlxOw.d.ts} +2 -2
  48. package/dist/skills/index.d.ts +1 -1
  49. package/dist/skills/index.js +1 -1
  50. package/dist/skills/index.js.map +1 -1
  51. package/dist/storage/index.d.ts +390 -6
  52. package/dist/storage/index.js +672 -35
  53. package/dist/storage/index.js.map +1 -1
  54. package/dist/{system-prompt-Bs-Wliab.d.ts → system-prompt-CneIxVip.d.ts} +1 -1
  55. package/dist/{tool-executor-Boo3dekH.d.ts → tool-executor-flTuxsqO.d.ts} +9 -4
  56. package/dist/types/index.d.ts +14 -14
  57. package/dist/types/index.js +60 -0
  58. package/dist/types/index.js.map +1 -1
  59. package/dist/utils/index.d.ts +18 -6
  60. package/dist/utils/index.js +61 -56
  61. package/dist/utils/index.js.map +1 -1
  62. package/package.json +1 -1
  63. package/dist/compactor-DVTKL7XD.d.ts +0 -23
@@ -640,7 +640,6 @@ var FileSessionWriter = class {
640
640
  }
641
641
  return event;
642
642
  }
643
- promptIndex = 0;
644
643
  pendingFileSnapshots = [];
645
644
  /** Tracks open tool_use IDs during the current run to serialize on close for resume. */
646
645
  openToolUses = /* @__PURE__ */ new Set();
@@ -731,7 +730,6 @@ var FileSessionWriter = class {
731
730
  await this.writeFileSnapshot(promptIndex, [...this.pendingFileSnapshots]);
732
731
  this.pendingFileSnapshots = [];
733
732
  }
734
- this.promptIndex = promptIndex + 1;
735
733
  await this.append({
736
734
  type: "checkpoint",
737
735
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -827,6 +825,38 @@ var FileSessionWriter = class {
827
825
  `;
828
826
  await fsp.writeFile(this.filePath, record, "utf8");
829
827
  }
828
+ /**
829
+ * Idea #1 — write an in-flight marker. The agent loop should call
830
+ * this at the start of each long-running operation; a matching
831
+ * `clearInFlightMarker` follows on clean exit. A stale marker
832
+ * (no end) is what `SessionRecovery.detectStale` looks for.
833
+ */
834
+ async writeInFlightMarker(context) {
835
+ if (!context || context.length > 500) {
836
+ throw new Error("In-flight context must be 1..500 chars");
837
+ }
838
+ await this.append({
839
+ type: "in_flight_start",
840
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
841
+ context
842
+ });
843
+ this.events?.emit("in_flight.started", { context, ts: (/* @__PURE__ */ new Date()).toISOString() });
844
+ }
845
+ /**
846
+ * Idea #1 — close the in-flight marker. Idempotent in spirit
847
+ * (you can call it after a successful iteration even if you
848
+ * didn't open one this round) — but the session log records
849
+ * every call so postmortem tooling can see "the agent finished
850
+ * cleanly X times, then died without finishing Y".
851
+ */
852
+ async clearInFlightMarker(reason) {
853
+ await this.append({
854
+ type: "in_flight_end",
855
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
856
+ reason
857
+ });
858
+ this.events?.emit("in_flight.ended", { reason, ts: (/* @__PURE__ */ new Date()).toISOString() });
859
+ }
830
860
  };
831
861
  function userInputTitle(content) {
832
862
  const text = typeof content === "string" ? content : content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
@@ -3798,80 +3828,85 @@ function parseDescription(raw) {
3798
3828
 
3799
3829
  // src/utils/json-repair.ts
3800
3830
  function completePartialObject(s) {
3801
- let result = s;
3802
- const trimmed = result.trim();
3803
- if (!trimmed.startsWith("{")) return s;
3804
- for (let pass = 0; pass < 3; pass++) {
3805
- let braceDepth = 0;
3806
- let inString2 = false;
3807
- let escaped2 = false;
3808
- let foundClose = false;
3809
- for (const ch of result) {
3810
- if (escaped2) {
3811
- escaped2 = false;
3831
+ if (!s.trim().startsWith("{")) return s;
3832
+ if (tryParse(s).ok) return s;
3833
+ const stack = [];
3834
+ let inString = false;
3835
+ let escaped = false;
3836
+ let sawKey = false;
3837
+ let prevSig = "";
3838
+ let contentEnd = 0;
3839
+ let stringBraceDepth = 0;
3840
+ for (let i = 0; i < s.length; i++) {
3841
+ const ch = s[i];
3842
+ if (inString) {
3843
+ contentEnd = i + 1;
3844
+ if (escaped) {
3845
+ escaped = false;
3812
3846
  continue;
3813
3847
  }
3814
3848
  if (ch === "\\") {
3815
- escaped2 = true;
3849
+ escaped = true;
3816
3850
  continue;
3817
3851
  }
3818
3852
  if (ch === '"') {
3819
- inString2 = !inString2;
3853
+ inString = false;
3854
+ prevSig = '"';
3855
+ stringBraceDepth = 0;
3820
3856
  continue;
3821
3857
  }
3822
- if (inString2) continue;
3823
- if (ch === "{") {
3824
- braceDepth++;
3825
- foundClose = false;
3826
- } else if (ch === "}") {
3827
- braceDepth--;
3828
- if (braceDepth === 0) foundClose = true;
3829
- }
3830
- }
3831
- if (foundClose || braceDepth <= 0) break;
3832
- result += "}".repeat(braceDepth);
3833
- }
3834
- if (tryParse(result).ok) return result;
3835
- let inString = false;
3836
- let escaped = false;
3837
- for (let i = result.length - 1; i >= 0; i--) {
3838
- const ch = result[i];
3839
- if (escaped) {
3840
- escaped = false;
3841
- continue;
3842
- }
3843
- if (ch === "\\") {
3844
- escaped = true;
3858
+ if (ch === "{") stringBraceDepth++;
3859
+ else if (ch === "}" && stringBraceDepth > 0) stringBraceDepth--;
3845
3860
  continue;
3846
3861
  }
3862
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") continue;
3863
+ contentEnd = i + 1;
3847
3864
  if (ch === '"') {
3848
- let nextNonWs;
3849
- for (let j = i + 1; j < result.length; j++) {
3850
- const nc = result[j];
3851
- if (nc === " " || nc === " " || nc === "\n" || nc === "\r") continue;
3852
- nextNonWs = result[j];
3853
- break;
3854
- }
3855
- if (nextNonWs === ":") {
3856
- continue;
3857
- } else {
3858
- inString = !inString;
3859
- continue;
3860
- }
3865
+ inString = true;
3866
+ sawKey = true;
3867
+ stringBraceDepth = 0;
3868
+ prevSig = '"';
3869
+ } else if (ch === "{" || ch === "[") {
3870
+ stack.push(ch);
3871
+ prevSig = ch;
3872
+ } else if (ch === "}" || ch === "]") {
3873
+ stack.pop();
3874
+ prevSig = ch;
3875
+ } else {
3876
+ prevSig = ch;
3861
3877
  }
3862
3878
  }
3879
+ if (!sawKey && !inString) return s;
3880
+ let result = s.slice(0, contentEnd);
3863
3881
  if (inString) {
3864
- result = result.trimEnd();
3865
- if (result.endsWith("\\")) result = result.slice(0, -1);
3866
- let depth = 0;
3867
- for (const ch of result) {
3868
- if (ch === "{") depth++;
3869
- else if (ch === "}") depth = Math.max(0, depth - 1);
3882
+ if (escaped) {
3883
+ result = result.slice(0, -1);
3884
+ } else if (endsWithInvalidEscape(result)) {
3885
+ result = result.slice(0, -2);
3870
3886
  }
3871
- result += '"' + "}".repeat(Math.max(1, depth));
3887
+ if (stringBraceDepth > 0) result += "}".repeat(stringBraceDepth);
3888
+ result += '"';
3889
+ } else if (prevSig === ":") {
3890
+ result += "null";
3891
+ }
3892
+ for (let k = stack.length - 1; k >= 0; k--) {
3893
+ result += stack[k] === "{" ? "}" : "]";
3894
+ }
3895
+ if (!tryParse(result).ok) {
3896
+ const patched = result.replace(/:(\s*)([}\]])/g, ":null$2");
3897
+ if (tryParse(patched).ok) result = patched;
3872
3898
  }
3873
3899
  return result;
3874
3900
  }
3901
+ var VALID_ESCAPE = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
3902
+ function endsWithInvalidEscape(str) {
3903
+ const last = str[str.length - 1];
3904
+ if (str[str.length - 2] !== "\\" || last === void 0) return false;
3905
+ if (VALID_ESCAPE.has(last)) return false;
3906
+ let backslashes = 0;
3907
+ for (let k = str.length - 2; k >= 0 && str[k] === "\\"; k--) backslashes++;
3908
+ return backslashes % 2 === 1;
3909
+ }
3875
3910
  function tryParse(s) {
3876
3911
  try {
3877
3912
  return { ok: true, value: JSON.parse(s) };
@@ -4281,6 +4316,7 @@ var HybridCompactor = class {
4281
4316
  }
4282
4317
  async compact(ctx, opts = {}) {
4283
4318
  const beforeTokens = this.estimateMessages(ctx.messages);
4319
+ const beforeFull = this.estimateFullRequest(ctx);
4284
4320
  const reductions = [];
4285
4321
  const policy = readContextWindowPolicy(ctx);
4286
4322
  const preserveK = policy?.preserveK ?? this.preserveK;
@@ -4296,9 +4332,12 @@ var HybridCompactor = class {
4296
4332
  ctx.state.replaceMessages(repaired.messages);
4297
4333
  }
4298
4334
  const afterTokens = this.estimateMessages(ctx.messages);
4335
+ const afterFull = this.estimateFullRequest(ctx);
4299
4336
  return {
4300
4337
  before: beforeTokens,
4301
4338
  after: afterTokens,
4339
+ fullRequestTokensBefore: beforeFull,
4340
+ fullRequestTokensAfter: afterFull,
4302
4341
  reductions,
4303
4342
  repaired: repaired.report.changed ? {
4304
4343
  removedToolUses: repaired.report.removedToolUses,
@@ -4307,6 +4346,14 @@ var HybridCompactor = class {
4307
4346
  } : void 0
4308
4347
  };
4309
4348
  }
4349
+ /**
4350
+ * Estimate the full API request token count: messages + systemPrompt + toolDefs.
4351
+ * This is the accurate figure for context-window pressure monitoring.
4352
+ */
4353
+ estimateFullRequest(ctx) {
4354
+ const breakdown = estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []);
4355
+ return breakdown.total;
4356
+ }
4310
4357
  eliseOldToolResults(ctx, preserveK = this.preserveK, eliseThreshold = this.eliseThreshold) {
4311
4358
  const messages = ctx.messages;
4312
4359
  let pairCount = 0;
@@ -4452,8 +4499,9 @@ var IntelligentCompactor = class {
4452
4499
  }
4453
4500
  async compact(ctx, opts = {}) {
4454
4501
  const beforeTokens = this.estimateTokens(ctx.messages);
4502
+ const beforeFull = this.estimateFullRequest(ctx);
4455
4503
  const reductions = [];
4456
- const load = beforeTokens / this.maxContext;
4504
+ const load = beforeFull / this.maxContext;
4457
4505
  const aggressive = load >= this.hardThreshold ? true : opts.aggressive ?? load >= this.softThreshold;
4458
4506
  const saved1 = this.eliseOldToolResults(ctx);
4459
4507
  if (saved1 > 0) reductions.push({ phase: "elision", saved: saved1 });
@@ -4467,9 +4515,12 @@ var IntelligentCompactor = class {
4467
4515
  const repaired = repairToolUseAdjacency(ctx.messages);
4468
4516
  if (repaired.report.changed) ctx.state.replaceMessages(repaired.messages);
4469
4517
  const afterTokens = this.estimateTokens(ctx.messages);
4518
+ const afterFull = this.estimateFullRequest(ctx);
4470
4519
  return {
4471
4520
  before: beforeTokens,
4472
4521
  after: afterTokens,
4522
+ fullRequestTokensBefore: beforeFull,
4523
+ fullRequestTokensAfter: afterFull,
4473
4524
  reductions,
4474
4525
  repaired: repaired.report.changed ? {
4475
4526
  removedToolUses: repaired.report.removedToolUses,
@@ -4478,6 +4529,14 @@ var IntelligentCompactor = class {
4478
4529
  } : void 0
4479
4530
  };
4480
4531
  }
4532
+ /**
4533
+ * Estimate the full API request token count: messages + systemPrompt + toolDefs.
4534
+ * This is the accurate figure for context-window pressure monitoring.
4535
+ */
4536
+ estimateFullRequest(ctx) {
4537
+ const breakdown = estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []);
4538
+ return breakdown.total;
4539
+ }
4481
4540
  async summarizeAncientTurns(ctx) {
4482
4541
  const messages = ctx.messages;
4483
4542
  const cutoff = Math.max(0, messages.length - this.preserveK * 2);
@@ -4490,29 +4549,26 @@ var IntelligentCompactor = class {
4490
4549
  try {
4491
4550
  summaryText = await this.callSummarizer(toSummarize, ctx);
4492
4551
  } catch {
4493
- const toolNames = /* @__PURE__ */ new Set();
4494
- const filePaths = /* @__PURE__ */ new Set();
4495
- let userTurns = 0;
4496
- let assistantTurns = 0;
4552
+ const preservedMessages = [];
4497
4553
  for (const m of toSummarize) {
4498
- if (m.role === "user") userTurns++;
4499
- else if (m.role === "assistant") {
4500
- assistantTurns++;
4501
- if (Array.isArray(m.content)) {
4502
- for (const b of m.content) {
4503
- if (b.type === "tool_use") toolNames.add(b.name ?? "unknown");
4504
- }
4505
- }
4554
+ if (m.role === "system") continue;
4555
+ if (typeof m.content === "string") {
4556
+ preservedMessages.push(m);
4557
+ } else if (Array.isArray(m.content)) {
4558
+ const textParts = m.content.filter(isTextBlock);
4559
+ const toolParts = m.content.filter((b) => b.type === "tool_use" || b.type === "tool_result");
4560
+ const next = {
4561
+ role: m.role,
4562
+ content: [
4563
+ ...textParts,
4564
+ ...toolParts.length > 0 ? [{ type: "text", text: `[${toolParts.length} tool call(s) omitted]` }] : []
4565
+ ]
4566
+ };
4567
+ preservedMessages.push(next);
4506
4568
  }
4507
- const text = typeof m.content === "string" ? m.content : "";
4508
- const matches = text.matchAll(/(?:[\w.,\-/@]+\/)*[\w.,\-/@]+\.\w+/g);
4509
- for (const m_ of matches) filePaths.add(m_[0]);
4510
4569
  }
4511
- const parts = [`${toSummarize.length} turns (${userTurns} user, ${assistantTurns} assistant)`];
4512
- if (toolNames.size > 0) parts.push(`tools: ${[...toolNames].join(", ")}`);
4513
- if (filePaths.size > 0) parts.push(`files: ${[...filePaths].slice(0, 10).join(", ")}`);
4514
- summaryText = parts.join(" | ");
4515
- if (!summaryText) summaryText = `${toSummarize.length} earlier turns omitted`;
4570
+ summaryText = preservedMessages.map((m) => `[${m.role}]: ${typeof m.content === "string" ? m.content : m.content.filter(isTextBlock).map((b) => b.text).join(" ")}`).join("\n");
4571
+ if (!summaryText) summaryText = `${toSummarize.length} earlier turns (semantic content preserved)`;
4516
4572
  }
4517
4573
  const summaryMsg = {
4518
4574
  role: "system",
@@ -4758,7 +4814,7 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
4758
4814
  const res = await this.provider.complete(req, { signal: ac.signal });
4759
4815
  const textBlocks = res.content.filter(isTextBlock);
4760
4816
  raw = textBlocks.map((b) => b.text).join("\n").trim();
4761
- } catch (err) {
4817
+ } catch (_err) {
4762
4818
  return this.fallbackSelect(messages, effectiveBudget);
4763
4819
  }
4764
4820
  return this.parseSelectorOutput(raw, messages.length);
@@ -4850,15 +4906,24 @@ var SelectiveCompactor = class {
4850
4906
  }
4851
4907
  async compact(ctx, opts = {}) {
4852
4908
  const beforeTokens = this.estimateTokens(ctx.messages);
4909
+ const beforeFull = this.estimateFullRequest(ctx);
4853
4910
  const reductions = [];
4854
- const load = beforeTokens / this.maxContext;
4911
+ const load = beforeFull / this.maxContext;
4855
4912
  const shouldCompact = load >= this.warnThreshold || opts.aggressive;
4856
4913
  if (!shouldCompact) {
4857
4914
  const saved = this.eliseOldToolResults(ctx);
4858
4915
  if (saved > 0) reductions.push({ phase: "elision", saved });
4859
4916
  const repair2 = this.repairProtocolAdjacency(ctx);
4860
4917
  const afterTokens2 = this.estimateTokens(ctx.messages);
4861
- return { before: beforeTokens, after: afterTokens2, reductions, repaired: repair2 };
4918
+ const afterFull2 = this.estimateFullRequest(ctx);
4919
+ return {
4920
+ before: beforeTokens,
4921
+ after: afterTokens2,
4922
+ fullRequestTokensBefore: beforeFull,
4923
+ fullRequestTokensAfter: afterFull2,
4924
+ reductions,
4925
+ repaired: repair2
4926
+ };
4862
4927
  }
4863
4928
  const savedElision = this.eliseOldToolResults(ctx);
4864
4929
  if (savedElision > 0) reductions.push({ phase: "elision", saved: savedElision });
@@ -4870,7 +4935,23 @@ var SelectiveCompactor = class {
4870
4935
  }
4871
4936
  const repair = this.repairProtocolAdjacency(ctx);
4872
4937
  const afterTokens = this.estimateTokens(ctx.messages);
4873
- return { before: beforeTokens, after: afterTokens, reductions, repaired: repair };
4938
+ const afterFull = this.estimateFullRequest(ctx);
4939
+ return {
4940
+ before: beforeTokens,
4941
+ after: afterTokens,
4942
+ fullRequestTokensBefore: beforeFull,
4943
+ fullRequestTokensAfter: afterFull,
4944
+ reductions,
4945
+ repaired: repair
4946
+ };
4947
+ }
4948
+ /**
4949
+ * Estimate the full API request token count: messages + systemPrompt + toolDefs.
4950
+ * This is the accurate figure for context-window pressure monitoring.
4951
+ */
4952
+ estimateFullRequest(ctx) {
4953
+ const breakdown = estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []);
4954
+ return breakdown.total;
4874
4955
  }
4875
4956
  repairProtocolAdjacency(ctx) {
4876
4957
  const repaired = repairToolUseAdjacency(ctx.messages);
@@ -5060,7 +5141,8 @@ var LEVEL_RANK2 = { warn: 0, soft: 1, hard: 2 };
5060
5141
  var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
5061
5142
  name = "AutoCompaction";
5062
5143
  compactor;
5063
- estimator;
5144
+ /** Deprecated. Kept for backward compat with tests that pass simpleEstimator. */
5145
+ _estimator;
5064
5146
  warnThreshold;
5065
5147
  softThreshold;
5066
5148
  hardThreshold;
@@ -5070,19 +5152,6 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
5070
5152
  events;
5071
5153
  failureMode;
5072
5154
  policyProvider;
5073
- /**
5074
- * Calibration factor applied to the estimator output. The estimator is now
5075
- * expected to already include messages + system prompt + tool definitions
5076
- * (see `estimateRequestTokens.total`), so no overhead boost is needed. Kept
5077
- * as a constant so a future calibration against real provider tokenization
5078
- * (BPE vs the chars/3.5 rough estimator) can dial this without touching the
5079
- * threshold-check math.
5080
- *
5081
- * Historical note: was 1.3 when the estimator only counted messages — that
5082
- * double-counted system+tools once the estimator was upgraded, firing
5083
- * compaction ~30% earlier than intended (e.g. real 56% load showed as 73%).
5084
- */
5085
- static OVERHEAD_FACTOR = 1;
5086
5155
  /**
5087
5156
  * Once a compaction attempt reduces nothing (preserveK protects everything,
5088
5157
  * no oversized tool_results remain to elide), retrying on every iteration
@@ -5095,8 +5164,11 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
5095
5164
  lastNoopAttempt = null;
5096
5165
  /**
5097
5166
  * @param compactor Compactor to use for compaction.
5098
- * @param maxContext Provider's max context window in tokens.
5099
- * @param estimator Token estimation function.
5167
+ * @param maxContext Provider's max context window in tokens.
5168
+ * @param _estimator Deprecated parameter kept for backward compatibility.
5169
+ * The middleware now uses `estimateRequestTokens` internally
5170
+ * for accurate full-request token counting (messages +
5171
+ * systemPrompt + toolDefs).
5100
5172
  * @param thresholds Threshold fractions (0-1) of maxContext.
5101
5173
  * @param opts Optional behavior. By default, failures at the
5102
5174
  * hard threshold throw AGENT_CONTEXT_OVERFLOW so
@@ -5104,11 +5176,11 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
5104
5176
  * provider context overflow. Warn/soft failures
5105
5177
  * still emit compaction.failed and continue.
5106
5178
  */
5107
- constructor(compactor, maxContext, estimator, thresholds, optsOrAggressiveOn = {}, events) {
5179
+ constructor(compactor, maxContext, _estimator, thresholds, optsOrAggressiveOn = {}, events) {
5108
5180
  const opts = typeof optsOrAggressiveOn === "string" ? { aggressiveOn: optsOrAggressiveOn, events } : optsOrAggressiveOn;
5109
5181
  this.compactor = compactor;
5110
5182
  this._maxContext = maxContext;
5111
- this.estimator = estimator;
5183
+ this._estimator = _estimator;
5112
5184
  this.warnThreshold = thresholds.warn;
5113
5185
  this.softThreshold = thresholds.soft;
5114
5186
  this.hardThreshold = thresholds.hard;
@@ -5124,8 +5196,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
5124
5196
  }
5125
5197
  handler() {
5126
5198
  return async (ctx, next) => {
5127
- const rawTokens = this.estimator(ctx);
5128
- const tokens = Math.ceil(rawTokens * _AutoCompactionMiddleware.OVERHEAD_FACTOR);
5199
+ const tokens = this._estimator ? this._estimator(ctx) : estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []).total;
5129
5200
  const load = tokens / this._maxContext;
5130
5201
  const policy = this.policyProvider?.(ctx);
5131
5202
  const thresholds = policy?.thresholds ?? {
@@ -5160,7 +5231,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
5160
5231
  return delta < _AutoCompactionMiddleware.NOOP_RETRY_DELTA_TOKENS;
5161
5232
  }
5162
5233
  recordAttempt(level, tokens, report) {
5163
- const reduced = report.before > report.after;
5234
+ const reduced = (report.fullRequestTokensBefore ?? report.before) > (report.fullRequestTokensAfter ?? report.after);
5164
5235
  const repaired = !!report.repaired;
5165
5236
  if (reduced || repaired) {
5166
5237
  this.lastNoopAttempt = null;
@@ -6405,17 +6476,7 @@ var SubagentBudget = class _SubagentBudget {
6405
6476
  startTime = null;
6406
6477
  _onThreshold;
6407
6478
  /**
6408
- * Tracks which budget kinds currently have an extension request
6409
- * in flight. While a kind is here, further `checkLimit` calls for the
6410
- * same kind are no-ops — without this dedup, every `recordIteration`
6411
- * after the limit is reached spawns a fresh decision Promise (until
6412
- * the first one lands and patches limits), flooding the FleetBus
6413
- * with redundant threshold events. Cleared in `negotiateExtension`'s
6414
- * `finally`.
6415
- */
6416
- pendingExtensions = /* @__PURE__ */ new Set();
6417
- /**
6418
- * Hard cap on how long `negotiateExtension` waits for the coordinator to
6479
+ * Hard cap on how long `_negotiateExtension` waits for the coordinator to
6419
6480
  * respond before defaulting to 'stop'. Without this fallback an absent
6420
6481
  * or hung listener (Director not built / event filter detached mid-run)
6421
6482
  * leaves the budget over-limit and never enforces anything.
@@ -6477,38 +6538,83 @@ var SubagentBudget = class _SubagentBudget {
6477
6538
  * - `mode === 'auto'` + no listener → throw `BudgetExceededError` (hard stop; no one to ask)
6478
6539
  * - `mode === 'auto'` + listener → throw `BudgetThresholdSignal` with async decision promise
6479
6540
  */
6480
- checkLimit(kind, used, limit) {
6541
+ /**
6542
+ * Collects all exceeded budget kinds into a single NOOP-free negotiation.
6543
+ * Called by recordIteration / recordToolCall / recordUsage — each may call
6544
+ * this for its own kind. The first call starts the negotiation and stores
6545
+ * the Promise in _pendingNegotiation. Subsequent calls for DIFFERENT
6546
+ * kinds (while a negotiation is in flight) are NOOPs — they don't start
6547
+ * new conversations with the coordinator. This prevents an EventBus flood
6548
+ * when multiple budget kinds are exceeded simultaneously in one iteration.
6549
+ *
6550
+ * Returns the kinds that were found to be exceeded (for logging/debugging).
6551
+ */
6552
+ checkLimits(elapsedMs) {
6553
+ const exceeded = [];
6554
+ if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
6555
+ exceeded.push({ kind: "iterations", used: this.iterations, limit: this.limits.maxIterations });
6556
+ }
6557
+ if (this.limits.maxToolCalls !== void 0 && this.toolCalls > this.limits.maxToolCalls) {
6558
+ exceeded.push({ kind: "tool_calls", used: this.toolCalls, limit: this.limits.maxToolCalls });
6559
+ }
6560
+ const totalTokens = this.tokenInput + this.tokenOutput;
6561
+ if (this.limits.maxTokens !== void 0 && totalTokens > this.limits.maxTokens) {
6562
+ exceeded.push({ kind: "tokens", used: totalTokens, limit: this.limits.maxTokens });
6563
+ }
6564
+ if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
6565
+ exceeded.push({ kind: "cost", used: this.costUsd, limit: this.limits.maxCostUsd });
6566
+ }
6567
+ if (elapsedMs !== void 0 && this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs) {
6568
+ exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
6569
+ }
6570
+ if (exceeded.length === 0) return [];
6481
6571
  if (!this._onThreshold) {
6482
- throw new BudgetExceededError(kind, limit, used);
6572
+ const first2 = exceeded[0];
6573
+ throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
6483
6574
  }
6484
6575
  if (this._mode === "sync") {
6485
- throw new BudgetExceededError(kind, limit, used);
6576
+ const first2 = exceeded[0];
6577
+ throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
6486
6578
  }
6487
6579
  const bus = this._events;
6488
6580
  if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
6489
- throw new BudgetExceededError(kind, limit, used);
6581
+ const first2 = exceeded[0];
6582
+ throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
6583
+ }
6584
+ for (const entry of exceeded) {
6585
+ if (this._pendingNegotiations.has(entry.kind)) continue;
6586
+ const decision2 = this._negotiateExtension(entry.kind, exceeded);
6587
+ this._pendingNegotiations.set(entry.kind, decision2);
6490
6588
  }
6491
- if (this.pendingExtensions.has(kind)) return;
6492
- this.pendingExtensions.add(kind);
6493
- const decision = this.negotiateExtension(kind, used, limit);
6494
- throw new BudgetThresholdSignal(kind, limit, used, decision);
6589
+ const first = exceeded[0];
6590
+ const decision = this._pendingNegotiations.get(first.kind);
6591
+ throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
6495
6592
  }
6593
+ /**
6594
+ * Per-kind in-flight negotiation Promises. Each budget kind can have its
6595
+ * own concurrent negotiation — e.g. iterations and timeout can both
6596
+ * be exceeded simultaneously without blocking each other. The same kind
6597
+ * cannot start two concurrent negotiations (dedup guard).
6598
+ * Cleared in `_negotiateExtension`'s `finally` block.
6599
+ */
6600
+ _pendingNegotiations = /* @__PURE__ */ new Map();
6496
6601
  /**
6497
6602
  * Drive the threshold handler to a decision. Resolves with `'stop'`
6498
6603
  * (signal the runner to abort) or `{ extend: ... }` (limits already
6499
- * patched in-place; the runner should not abort). Always releases the
6500
- * `pendingExtensions` slot in `finally`.
6604
+ * patched in-place; the runner should not abort). Clears the
6605
+ * per-kind slot in `_pendingNegotiations` in `finally`.
6501
6606
  *
6502
6607
  * The 'continue' return from a sync handler is treated as
6503
6608
  * `{ extend: {} }` — keep going without patching; next overrun fires
6504
6609
  * a fresh signal.
6505
6610
  */
6506
- async negotiateExtension(kind, used, limit) {
6611
+ async _negotiateExtension(kind, exceeded) {
6507
6612
  try {
6613
+ const first = exceeded[0];
6508
6614
  const result = this._onThreshold({
6509
- kind,
6510
- used,
6511
- limit,
6615
+ kind: first.kind,
6616
+ used: first.used,
6617
+ limit: first.limit,
6512
6618
  requestDecision: () => {
6513
6619
  const bus = this._events;
6514
6620
  if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
@@ -6525,20 +6631,22 @@ var SubagentBudget = class _SubagentBudget {
6525
6631
  () => respond("stop"),
6526
6632
  _SubagentBudget.DECISION_TIMEOUT_MS
6527
6633
  );
6528
- bus.emit("budget.threshold_reached", {
6529
- kind,
6530
- used,
6531
- limit,
6532
- timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
6533
- extend: (extra) => {
6534
- clearTimeout(fallback);
6535
- respond({ extend: extra });
6536
- },
6537
- deny: () => {
6538
- clearTimeout(fallback);
6539
- respond("stop");
6540
- }
6541
- });
6634
+ for (const { kind: kind2, used, limit } of exceeded) {
6635
+ bus.emit("budget.threshold_reached", {
6636
+ kind: kind2,
6637
+ used,
6638
+ limit,
6639
+ timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
6640
+ extend: (extra) => {
6641
+ clearTimeout(fallback);
6642
+ respond({ extend: extra });
6643
+ },
6644
+ deny: () => {
6645
+ clearTimeout(fallback);
6646
+ respond("stop");
6647
+ }
6648
+ });
6649
+ }
6542
6650
  });
6543
6651
  }
6544
6652
  });
@@ -6564,47 +6672,39 @@ var SubagentBudget = class _SubagentBudget {
6564
6672
  }
6565
6673
  return decision;
6566
6674
  } finally {
6567
- this.pendingExtensions.delete(kind);
6675
+ this._pendingNegotiations.delete(kind);
6568
6676
  }
6569
6677
  }
6570
6678
  recordIteration() {
6571
6679
  this.iterations++;
6572
- if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
6573
- void this.checkLimit("iterations", this.iterations, this.limits.maxIterations);
6574
- }
6680
+ void this.checkLimits();
6575
6681
  }
6576
6682
  recordToolCall() {
6577
6683
  this.toolCalls++;
6578
- if (this.limits.maxToolCalls !== void 0 && this.toolCalls > this.limits.maxToolCalls) {
6579
- void this.checkLimit("tool_calls", this.toolCalls, this.limits.maxToolCalls);
6580
- }
6684
+ void this.checkLimits();
6581
6685
  }
6582
6686
  recordUsage(usage, costUsd = 0) {
6583
6687
  this.tokenInput += usage.input;
6584
6688
  this.tokenOutput += usage.output;
6585
6689
  this.costUsd += costUsd;
6586
- const totalTokens = this.tokenInput + this.tokenOutput;
6587
- if (this.limits.maxTokens !== void 0 && totalTokens > this.limits.maxTokens) {
6588
- void this.checkLimit("tokens", totalTokens, this.limits.maxTokens);
6589
- }
6590
- if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
6591
- void this.checkLimit("cost", this.costUsd, this.limits.maxCostUsd);
6592
- }
6690
+ void this.checkLimits();
6593
6691
  }
6594
6692
  /**
6595
- * Wall-clock budget check. Unlike other limits, timeout check passes through
6596
- * `checkLimit` and is subject to the same negotiation-mode decision table.
6597
- * In practice, `'sync'` mode (the usual test configuration) means a timeout
6598
- * immediately throws `BudgetExceededError`. In production with a coordinator,
6599
- * a timeout emits `budget.threshold_reached` so the Director can decide whether
6600
- * to extend or abort.
6693
+ * Wall-clock budget check. Unlike other limits, timeout is always a hard stop
6694
+ * wall-clock time cannot be "extended" by the coordinator, so it throws
6695
+ * synchronously rather than entering the negotiation flow.
6696
+ *
6697
+ * Decision table:
6698
+ * - no `onThreshold` handler → throw `BudgetExceededError`
6699
+ * - `mode === 'sync'` → throw `BudgetExceededError`
6700
+ * - `mode === 'auto'` + no listener → throw `BudgetExceededError`
6701
+ * - `mode === 'auto'` + listener → throw `BudgetExceededError` (timeout is not extendable)
6601
6702
  */
6602
6703
  checkTimeout() {
6603
6704
  if (this.startTime === null || this.limits.timeoutMs === void 0) return;
6604
6705
  const elapsed = Date.now() - this.startTime;
6605
- if (elapsed > this.limits.timeoutMs) {
6606
- void this.checkLimit("timeout", elapsed, this.limits.timeoutMs);
6607
- }
6706
+ if (elapsed <= this.limits.timeoutMs) return;
6707
+ void this.checkLimits(elapsed);
6608
6708
  }
6609
6709
  /** Returns true if a timeout has occurred without throwing. Useful for races. */
6610
6710
  isTimedOut() {
@@ -9392,6 +9492,107 @@ FLEET_ROSTER_BUDGETS["goose"] = { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations:
9392
9492
  ...Object.fromEntries(ACP_AGENTS.map((a) => [a.role, a]))
9393
9493
  });
9394
9494
 
9495
+ // src/coordination/subagent-nicknames.ts
9496
+ var NICKNAME_POOL = {
9497
+ // Physics & fundamental sciences
9498
+ "einstein": { name: "Einstein", domain: "physics" },
9499
+ "newton": { name: "Newton", domain: "physics" },
9500
+ "feynman": { name: "Feynman", domain: "physics" },
9501
+ "dirac": { name: "Dirac", domain: "physics" },
9502
+ "bohr": { name: "Bohr", domain: "physics" },
9503
+ "planck": { name: "Planck", domain: "physics" },
9504
+ "curie": { name: "Curie", domain: "physics" },
9505
+ "fermi": { name: "Fermi", domain: "physics" },
9506
+ "heisenberg": { name: "Heisenberg", domain: "physics" },
9507
+ "schrodinger": { name: "Schr\xF6dinger", domain: "physics" },
9508
+ // Mathematics
9509
+ "euclid": { name: "Euclid", domain: "math" },
9510
+ "gauss": { name: "Gauss", domain: "math" },
9511
+ "turing": { name: "Turing", domain: "math" },
9512
+ "poincare": { name: "Poincar\xE9", domain: "math" },
9513
+ "riemann": { name: "Riemann", domain: "math" },
9514
+ "hilbert": { name: "Hilbert", domain: "math" },
9515
+ "pythagoras": { name: "Pythagoras", domain: "math" },
9516
+ // Computing & information theory
9517
+ "von-neumann": { name: "Von Neumann", domain: "computing" },
9518
+ "shannon": { name: "Shannon", domain: "computing" },
9519
+ "hopper": { name: "Hopper", domain: "computing" },
9520
+ "backus": { name: "Backus", domain: "computing" },
9521
+ "knuth": { name: "Knuth", domain: "computing" },
9522
+ "torvalds": { name: "Torvalds", domain: "computing" },
9523
+ "stallman": { name: "Stallman", domain: "computing" },
9524
+ "berners-lee": { name: "Berners-Lee", domain: "computing" },
9525
+ "babbage": { name: "Babbage", domain: "computing" },
9526
+ "lovelace": { name: "Lovelace", domain: "computing" },
9527
+ "klein": { name: "Klein", domain: "computing" },
9528
+ // Electronics & electrical engineering
9529
+ "edison": { name: "Edison", domain: "ee" },
9530
+ "tesla": { name: "Tesla", domain: "ee" },
9531
+ "faraday": { name: "Faraday", domain: "ee" },
9532
+ "maxwell": { name: "Maxwell", domain: "ee" },
9533
+ "ohm": { name: "Ohm", domain: "ee" },
9534
+ "bell": { name: "Bell", domain: "ee" },
9535
+ "marconi": { name: "Marconi", domain: "ee" },
9536
+ "lamarr": { name: "Lamarr", domain: "ee" },
9537
+ // General science / multi-disciplinary
9538
+ "darwin": { name: "Darwin", domain: "biology" },
9539
+ "mendel": { name: "Mendel", domain: "biology" },
9540
+ "pasteur": { name: "Pasteur", domain: "biology" },
9541
+ "hawking": { name: "Hawking", domain: "cosmology" },
9542
+ "sagan": { name: "Sagan", domain: "cosmology" },
9543
+ // Chemistry / materials
9544
+ "lavoisier": { name: "Lavoisier", domain: "chemistry" },
9545
+ "mendeleev": { name: "Mendeleev", domain: "chemistry" }
9546
+ };
9547
+ var ALL_NICKNAMES = Object.values(NICKNAME_POOL);
9548
+ var DOMAIN_PREFERENCES = {
9549
+ "security": ["shannon", "turing", "lamarr", "stallman"],
9550
+ "bug-hunter": ["darwin", "curie", "feynman", "fermi"],
9551
+ "refactor": ["gauss", "hilbert", "euclid", "planck"],
9552
+ "audit-log": ["sagan", "hawking", "poincare", "newton"],
9553
+ "planner": ["hilbert", "gauss", "turing", "euclid"],
9554
+ "researcher": ["sagan", "hawking", "darwin", "pasteur"],
9555
+ "explorer": ["marconi", "bell", "columbus", "polo"],
9556
+ "testing": ["pasteur", "curie", "fermi", "bohr"],
9557
+ "frontend": ["lovelace", "hopper", "babbage", "backus"],
9558
+ "backend": ["torvalds", "stallman", "von-neumann", "backus"],
9559
+ "database": ["turing", "shannon", "backus", "knuth"],
9560
+ "devops": ["tesla", "edison", "faraday", "bell"],
9561
+ "security-scanner": ["shannon", "turing", "lamarr", "stallman"],
9562
+ "refactor-planner": ["gauss", "hilbert", "planck", "newton"],
9563
+ "architect": ["von-neumann", "turing", "gauss", "hilbert"],
9564
+ "critic": ["einstein", "feynman", "dirac", "bohr"],
9565
+ "e2e": ["hopper", "bell", "marconi", "tesla"],
9566
+ "performance": ["knuth", "gauss", "planck", "feynman"],
9567
+ "chaos": ["tesla", "edison", "curie", "fermi"],
9568
+ "cost": ["ohm", "bell", "marconi", "tesla"],
9569
+ // default fallback
9570
+ "default": ["einstein", "newton", "curie", "tesla", "edison", "turing", "shannon", "hopper", "knuth", "stallman"]
9571
+ };
9572
+ function assignNickname(role, used) {
9573
+ const preferences = [
9574
+ ...DOMAIN_PREFERENCES[role] ?? [],
9575
+ ...DOMAIN_PREFERENCES["default"] ?? []
9576
+ ];
9577
+ for (const key of preferences) {
9578
+ const entry = NICKNAME_POOL[key];
9579
+ if (entry && !used.has(key)) {
9580
+ return `${entry.name} (${formatRole(role)})`;
9581
+ }
9582
+ }
9583
+ for (const entry of ALL_NICKNAMES) {
9584
+ const key = Object.entries(NICKNAME_POOL).find(([, v]) => v.name === entry.name)?.[0];
9585
+ if (key && !used.has(key)) {
9586
+ return `${entry.name} (${formatRole(role)})`;
9587
+ }
9588
+ }
9589
+ const counter = used.size + 1;
9590
+ return `Scientist #${counter} (${formatRole(role)})`;
9591
+ }
9592
+ function formatRole(role) {
9593
+ return role.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
9594
+ }
9595
+
9395
9596
  // src/coordination/multi-agent-coordinator.ts
9396
9597
  var DefaultMultiAgentCoordinator = class extends EventEmitter {
9397
9598
  coordinatorId;
@@ -9399,6 +9600,15 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
9399
9600
  runner;
9400
9601
  fleetBus;
9401
9602
  subagents = /* @__PURE__ */ new Map();
9603
+ /**
9604
+ * Base nickname keys already handed out this run (e.g. `einstein`, `tesla`).
9605
+ * Prevents two workers sharing a name. Direct `coordinator.spawn()` callers
9606
+ * (parallel/eternal engine, SDD parallel run) don't go through
9607
+ * `Director.spawn()` where nicknames are normally assigned, so the
9608
+ * coordinator upgrades placeholder names ("Executor", "slot-ab12cd", role
9609
+ * names) to memorable ones here — that's what surfaces in the fleet monitor.
9610
+ */
9611
+ usedNicknames = /* @__PURE__ */ new Set();
9402
9612
  pendingTasks = [];
9403
9613
  completedResults = [];
9404
9614
  totalIterations = 0;
@@ -9447,7 +9657,26 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
9447
9657
  this.config.maxConcurrent = Math.floor(n);
9448
9658
  this.tryDispatchNext();
9449
9659
  }
9660
+ /**
9661
+ * Upgrade a placeholder/role-derived name to a memorable scientist nickname
9662
+ * (e.g. "Einstein (Executor)"). A name is treated as a placeholder when it is
9663
+ * empty, equals the role (case-insensitive), is a generic default
9664
+ * ("subagent"/"adhoc"/"generic"), or is an auto-generated `slot-…` id.
9665
+ * Explicit, human-chosen names — including nicknames already assigned by
9666
+ * `Director.spawn()` — are left untouched, so this never double-assigns.
9667
+ */
9668
+ withNickname(subagent) {
9669
+ const role = subagent.role ?? "subagent";
9670
+ const name = subagent.name?.trim() ?? "";
9671
+ const isPlaceholder = name === "" || name.toLowerCase() === role.toLowerCase() || name === "subagent" || name === "adhoc" || name === "generic" || /^slot-/.test(name);
9672
+ if (!isPlaceholder) return subagent;
9673
+ const nickname = assignNickname(role, this.usedNicknames);
9674
+ const baseKey = nickname.split(" ")[0].toLowerCase().replace(/[^a-z0-9-]/g, "-");
9675
+ this.usedNicknames.add(baseKey);
9676
+ return { ...subagent, name: nickname };
9677
+ }
9450
9678
  async spawn(subagent) {
9679
+ subagent = this.withNickname(subagent);
9451
9680
  const id = subagent.id || randomUUID();
9452
9681
  if (this.subagents.has(id)) {
9453
9682
  throw new Error(`Subagent id "${id}" already exists \u2014 refusing to overwrite`);
@@ -10041,12 +10270,12 @@ var GOAL_COMPLETE_MARKER2 = /^\s*\[goal[_\s-]?complete\]\s*$/im;
10041
10270
  var ParallelEternalEngine = class {
10042
10271
  constructor(opts) {
10043
10272
  this.opts = opts;
10044
- this.goalPath = goalFilePath(opts.projectRoot);
10273
+ this.goalPath = opts.goalPath ?? goalFilePath(opts.projectRoot);
10045
10274
  this.slots = Math.min(16, Math.max(1, opts.parallelSlots ?? 4));
10046
10275
  this.timeoutMs = opts.iterationTimeoutMs ?? 3e5;
10047
10276
  this.dispatchEnabled = opts.dispatch !== false;
10048
10277
  this.dispatchClassifier = opts.dispatchClassifier;
10049
- this.agentFactory = opts.subagentFactory ?? (async (config) => ({
10278
+ this.agentFactory = opts.subagentFactory ?? (async (_config) => ({
10050
10279
  agent: this.opts.agent,
10051
10280
  events: this.opts.agent.events
10052
10281
  }));
@@ -10140,7 +10369,6 @@ var ParallelEternalEngine = class {
10140
10369
  }
10141
10370
  const tasks = await this.decomposeGoal(goal);
10142
10371
  if (!tasks || tasks.length === 0) {
10143
- await sleep2(5e3);
10144
10372
  return false;
10145
10373
  }
10146
10374
  const fanOut = await this.fanOut(goal, tasks);
@@ -10161,6 +10389,7 @@ var ParallelEternalEngine = class {
10161
10389
  });
10162
10390
  if (fanOut.goalComplete) {
10163
10391
  this.stopRequested = true;
10392
+ this.state = "stopped";
10164
10393
  return true;
10165
10394
  }
10166
10395
  await this.maybeCompact();
@@ -10341,7 +10570,11 @@ ${lastFew}` : "No prior iterations.",
10341
10570
  } finally {
10342
10571
  clearTimeout(timer);
10343
10572
  }
10344
- } catch {
10573
+ } catch (err) {
10574
+ this.opts.onError?.(
10575
+ err instanceof Error ? err : new Error(String(err)),
10576
+ this.consecutiveFailures
10577
+ );
10345
10578
  return [];
10346
10579
  }
10347
10580
  }
@@ -10702,7 +10935,11 @@ Working rules:
10702
10935
  thrashing, terminate it rather than letting cost climb silently.
10703
10936
  6. Never claim a subagent's work as your own without verifying it. If a
10704
10937
  result looks wrong, ask_subagent for clarification before passing it
10705
- to the user.`;
10938
+ to the user.
10939
+ 7. Wind down when satisfied. When the results are good enough, call
10940
+ work_complete \u2014 no new subagents will spawn and queued tasks complete
10941
+ as aborted. Running subagents finish naturally. Call terminate_subagent
10942
+ only for ones you need to stop immediately.`;
10706
10943
  var DEFAULT_SUBAGENT_BASELINE = `You are a subagent operating under a Director. You were spawned to handle
10707
10944
  a specific slice of a larger plan \u2014 do that slice well and report back.
10708
10945
 
@@ -10847,14 +11084,12 @@ var FleetBus = class {
10847
11084
  };
10848
11085
  var FleetUsageAggregator = class {
10849
11086
  constructor(bus, priceLookup, metaLookup) {
10850
- this.bus = bus;
10851
11087
  this.priceLookup = priceLookup;
10852
11088
  this.metaLookup = metaLookup;
10853
11089
  bus.filter("provider.response", (e) => this.onProviderResponse(e));
10854
11090
  bus.filter("tool.executed", (e) => this.onToolExecuted(e));
10855
11091
  bus.filter("iteration.started", (e) => this.onIterationStarted(e));
10856
11092
  }
10857
- bus;
10858
11093
  priceLookup;
10859
11094
  metaLookup;
10860
11095
  perSubagent = /* @__PURE__ */ new Map();
@@ -10922,106 +11157,6 @@ var FleetUsageAggregator = class {
10922
11157
  snap.lastEventAt = e.ts;
10923
11158
  }
10924
11159
  };
10925
-
10926
- // src/coordination/subagent-nicknames.ts
10927
- var NICKNAME_POOL = {
10928
- // Physics & fundamental sciences
10929
- "einstein": { name: "Einstein", domain: "physics" },
10930
- "newton": { name: "Newton", domain: "physics" },
10931
- "feynman": { name: "Feynman", domain: "physics" },
10932
- "dirac": { name: "Dirac", domain: "physics" },
10933
- "bohr": { name: "Bohr", domain: "physics" },
10934
- "planck": { name: "Planck", domain: "physics" },
10935
- "curie": { name: "Curie", domain: "physics" },
10936
- "fermi": { name: "Fermi", domain: "physics" },
10937
- "heisenberg": { name: "Heisenberg", domain: "physics" },
10938
- "schrodinger": { name: "Schr\xF6dinger", domain: "physics" },
10939
- // Mathematics
10940
- "euclid": { name: "Euclid", domain: "math" },
10941
- "gauss": { name: "Gauss", domain: "math" },
10942
- "turing": { name: "Turing", domain: "math" },
10943
- "poincare": { name: "Poincar\xE9", domain: "math" },
10944
- "riemann": { name: "Riemann", domain: "math" },
10945
- "hilbert": { name: "Hilbert", domain: "math" },
10946
- "pythagoras": { name: "Pythagoras", domain: "math" },
10947
- // Computing & information theory
10948
- "von-neumann": { name: "Von Neumann", domain: "computing" },
10949
- "shannon": { name: "Shannon", domain: "computing" },
10950
- "hopper": { name: "Hopper", domain: "computing" },
10951
- "backus": { name: "Backus", domain: "computing" },
10952
- "knuth": { name: "Knuth", domain: "computing" },
10953
- "torvalds": { name: "Torvalds", domain: "computing" },
10954
- "stallman": { name: "Stallman", domain: "computing" },
10955
- "berners-lee": { name: "Berners-Lee", domain: "computing" },
10956
- "babbage": { name: "Babbage", domain: "computing" },
10957
- "lovelace": { name: "Lovelace", domain: "computing" },
10958
- "klein": { name: "Klein", domain: "computing" },
10959
- // Electronics & electrical engineering
10960
- "edison": { name: "Edison", domain: "ee" },
10961
- "tesla": { name: "Tesla", domain: "ee" },
10962
- "faraday": { name: "Faraday", domain: "ee" },
10963
- "maxwell": { name: "Maxwell", domain: "ee" },
10964
- "ohm": { name: "Ohm", domain: "ee" },
10965
- "bell": { name: "Bell", domain: "ee" },
10966
- "marconi": { name: "Marconi", domain: "ee" },
10967
- "lamarr": { name: "Lamarr", domain: "ee" },
10968
- // General science / multi-disciplinary
10969
- "darwin": { name: "Darwin", domain: "biology" },
10970
- "mendel": { name: "Mendel", domain: "biology" },
10971
- "pasteur": { name: "Pasteur", domain: "biology" },
10972
- "hawking": { name: "Hawking", domain: "cosmology" },
10973
- "sagan": { name: "Sagan", domain: "cosmology" },
10974
- // Chemistry / materials
10975
- "lavoisier": { name: "Lavoisier", domain: "chemistry" },
10976
- "mendeleev": { name: "Mendeleev", domain: "chemistry" }
10977
- };
10978
- var ALL_NICKNAMES = Object.values(NICKNAME_POOL);
10979
- var DOMAIN_PREFERENCES = {
10980
- "security": ["shannon", "turing", "lamarr", "stallman"],
10981
- "bug-hunter": ["darwin", "curie", "feynman", "fermi"],
10982
- "refactor": ["gauss", "hilbert", "euclid", "planck"],
10983
- "audit-log": ["sagan", "hawking", "poincare", "newton"],
10984
- "planner": ["hilbert", "gauss", "turing", "euclid"],
10985
- "researcher": ["sagan", "hawking", "darwin", "pasteur"],
10986
- "explorer": ["marconi", "bell", "columbus", "polo"],
10987
- "testing": ["pasteur", "curie", "fermi", "bohr"],
10988
- "frontend": ["lovelace", "hopper", "babbage", "backus"],
10989
- "backend": ["torvalds", "stallman", "von-neumann", "backus"],
10990
- "database": ["turing", "shannon", "backus", "knuth"],
10991
- "devops": ["tesla", "edison", "faraday", "bell"],
10992
- "security-scanner": ["shannon", "turing", "lamarr", "stallman"],
10993
- "refactor-planner": ["gauss", "hilbert", "planck", "newton"],
10994
- "architect": ["von-neumann", "turing", "gauss", "hilbert"],
10995
- "critic": ["einstein", "feynman", "dirac", "bohr"],
10996
- "e2e": ["hopper", "bell", "marconi", "tesla"],
10997
- "performance": ["knuth", "gauss", "planck", "feynman"],
10998
- "chaos": ["tesla", "edison", "curie", "fermi"],
10999
- "cost": ["oh", "bell", "marconi", "tesla"],
11000
- // default fallback
11001
- "default": ["einstein", "newton", "curie", "tesla", "edison", "turing", "shannon", "hopper", "knuth", "stallman"]
11002
- };
11003
- function assignNickname(role, used) {
11004
- const preferences = [
11005
- ...DOMAIN_PREFERENCES[role] ?? [],
11006
- ...DOMAIN_PREFERENCES["default"] ?? []
11007
- ];
11008
- for (const key of preferences) {
11009
- if (!used.has(key)) {
11010
- return `${NICKNAME_POOL[key].name} (${formatRole(role)})`;
11011
- }
11012
- }
11013
- for (const entry of ALL_NICKNAMES) {
11014
- const key = Object.entries(NICKNAME_POOL).find(([, v]) => v.name === entry.name)?.[0];
11015
- if (key && !used.has(key)) {
11016
- return `${entry.name} (${formatRole(role)})`;
11017
- }
11018
- }
11019
- const counter = used.size + 1;
11020
- return `Scientist #${counter} (${formatRole(role)})`;
11021
- }
11022
- function formatRole(role) {
11023
- return role.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
11024
- }
11025
11160
  function makeSpawnTool(director, roster) {
11026
11161
  const inputSchema = {
11027
11162
  type: "object",
@@ -11192,7 +11327,7 @@ function makeRollUpTool(director) {
11192
11327
  function makeTerminateTool(director) {
11193
11328
  return {
11194
11329
  name: "terminate_subagent",
11195
- description: "Forcibly abort a subagent.",
11330
+ description: 'Forcibly abort a subagent. The subagent finishes its current iteration then exits with status "stopped".',
11196
11331
  permission: "auto",
11197
11332
  mutating: true,
11198
11333
  inputSchema: { type: "object", properties: { subagentId: { type: "string", description: "Subagent to abort." } }, required: ["subagentId"] },
@@ -11203,6 +11338,19 @@ function makeTerminateTool(director) {
11203
11338
  }
11204
11339
  };
11205
11340
  }
11341
+ function makeTerminateAllTool(director) {
11342
+ return {
11343
+ name: "terminate_all",
11344
+ 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.',
11345
+ permission: "auto",
11346
+ mutating: true,
11347
+ inputSchema: { type: "object", properties: {}, required: [] },
11348
+ async execute() {
11349
+ await director.terminateAll();
11350
+ return { ok: true, message: `Fleet shutdown complete \u2014 all subagents stopped, pending tasks drained.` };
11351
+ }
11352
+ };
11353
+ }
11206
11354
  function makeFleetStatusTool(director) {
11207
11355
  return {
11208
11356
  name: "fleet_status",
@@ -11345,7 +11493,7 @@ function makeCollabDebugTool(director) {
11345
11493
  function makeFleetEmitTool(director) {
11346
11494
  return {
11347
11495
  name: "fleet_emit",
11348
- 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.",
11496
+ 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.",
11349
11497
  permission: "auto",
11350
11498
  mutating: false,
11351
11499
  inputSchema: {
@@ -11353,14 +11501,14 @@ function makeFleetEmitTool(director) {
11353
11501
  properties: {
11354
11502
  type: {
11355
11503
  type: "string",
11356
- description: "Event type: bug.found | refactor.plan | critic.evaluation"
11504
+ description: "Event type string (e.g. bug.found, refactor.plan, critic.evaluation, progress, result)."
11357
11505
  },
11358
11506
  payload: {
11359
11507
  type: "object",
11360
- description: "Event payload matching the event type."
11508
+ description: "Event payload. Structure depends on event type. Use null if no payload."
11361
11509
  }
11362
11510
  },
11363
- required: ["type", "payload"]
11511
+ required: ["type"]
11364
11512
  },
11365
11513
  async execute(input) {
11366
11514
  const i = input;
@@ -11368,12 +11516,25 @@ function makeFleetEmitTool(director) {
11368
11516
  subagentId: director.id,
11369
11517
  ts: Date.now(),
11370
11518
  type: i.type,
11371
- payload: i.payload
11519
+ payload: i.payload ?? {}
11372
11520
  });
11373
11521
  return { ok: true, event: i.type };
11374
11522
  }
11375
11523
  };
11376
11524
  }
11525
+ function makeWorkCompleteTool(director) {
11526
+ return {
11527
+ name: "work_complete",
11528
+ 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.",
11529
+ permission: "auto",
11530
+ mutating: false,
11531
+ inputSchema: { type: "object", properties: {}, required: [] },
11532
+ async execute() {
11533
+ director.workComplete();
11534
+ return { ok: true, message: "Fleet wind-down signaled. No new spawns or task dispatches." };
11535
+ }
11536
+ };
11537
+ }
11377
11538
  var CollabSession = class extends EventEmitter {
11378
11539
  sessionId;
11379
11540
  options;
@@ -11388,6 +11549,14 @@ var CollabSession = class extends EventEmitter {
11388
11549
  disposers = new Array();
11389
11550
  settled = false;
11390
11551
  timeoutMs;
11552
+ cancelled = false;
11553
+ alerts = [];
11554
+ /** Tracks tool call counts per subagent for progress-based timeout decisions. */
11555
+ progressBySubagent = /* @__PURE__ */ new Map();
11556
+ /** Last tool call count when a timeout warning was handled. */
11557
+ lastTimeoutProgress = /* @__PURE__ */ new Map();
11558
+ /** Session-level timeout timer handle (cleared on cancel or natural completion). */
11559
+ _timeoutTimer;
11391
11560
  constructor(director, fleetBus, options) {
11392
11561
  super();
11393
11562
  this.sessionId = randomUUID();
@@ -11405,11 +11574,23 @@ var CollabSession = class extends EventEmitter {
11405
11574
  };
11406
11575
  }
11407
11576
  }
11577
+ get id() {
11578
+ return this.sessionId;
11579
+ }
11580
+ getSessionAlerts() {
11581
+ return [...this.alerts];
11582
+ }
11583
+ isCancelled() {
11584
+ return this.cancelled;
11585
+ }
11408
11586
  /**
11409
- * Read the target files from disk and populate the snapshot.
11410
- * Call this after construction if you did not provide a prebuiltSnapshot
11411
- * and want the session to operate on real file contents.
11587
+ * Snapshot of role subagentId map. The Director calls coordinator.stop()
11588
+ * for each agent when cancelling the session, using this map to enumerate
11589
+ * all three collab agents.
11412
11590
  */
11591
+ getSubagentIds() {
11592
+ return new Map(this.subagentIds);
11593
+ }
11413
11594
  async buildSnapshot() {
11414
11595
  if (this.snapshot.files.length > 0) return this.snapshot;
11415
11596
  for (const filePath of this.options.targetPaths) {
@@ -11425,9 +11606,30 @@ var CollabSession = class extends EventEmitter {
11425
11606
  return this.snapshot;
11426
11607
  }
11427
11608
  /**
11428
- * Start the collaborative session: snapshot files, spawn all three agents,
11429
- * wire up event routing, and wait for completion.
11609
+ * Cancel the session. Emits director.cancel_collab on the FleetBus so all
11610
+ * collab agents finish early. The session-level timeout timer is also cleared.
11611
+ * Safe to call multiple times (idempotent after first call).
11430
11612
  */
11613
+ cancel(reason = "Director cancelled collab session") {
11614
+ if (this.settled) return;
11615
+ this.cancelled = true;
11616
+ if (this._timeoutTimer) {
11617
+ clearTimeout(this._timeoutTimer);
11618
+ this._timeoutTimer = void 0;
11619
+ }
11620
+ this.fleetBus.emit({
11621
+ subagentId: this.director.id,
11622
+ ts: Date.now(),
11623
+ type: "director.cancel_collab",
11624
+ payload: { sessionId: this.sessionId, reason, cancelledAt: (/* @__PURE__ */ new Date()).toISOString() }
11625
+ });
11626
+ this.fleetBus.emit({
11627
+ subagentId: this.director.id,
11628
+ ts: Date.now(),
11629
+ type: "collab.cancelled",
11630
+ payload: { sessionId: this.sessionId, reason }
11631
+ });
11632
+ }
11431
11633
  async start() {
11432
11634
  if (this.settled) throw new Error("session already settled");
11433
11635
  this.settled = true;
@@ -11442,10 +11644,10 @@ var CollabSession = class extends EventEmitter {
11442
11644
  this.subagentIds.set("refactor-planner", refactorPlannerId);
11443
11645
  this.subagentIds.set("critic", criticId);
11444
11646
  const timeout = new Promise((_, reject) => {
11445
- setTimeout(
11446
- () => reject(new Error(`CollabSession timed out after ${this.timeoutMs}ms`)),
11447
- this.timeoutMs
11448
- );
11647
+ this._timeoutTimer = setTimeout(() => {
11648
+ this.cancel("Session-level timeout reached");
11649
+ reject(new Error(`CollabSession timed out after ${this.timeoutMs}ms`));
11650
+ }, this.timeoutMs);
11449
11651
  });
11450
11652
  let results;
11451
11653
  try {
@@ -11458,6 +11660,10 @@ var CollabSession = class extends EventEmitter {
11458
11660
  timeout
11459
11661
  ]);
11460
11662
  } catch (err) {
11663
+ if (this._timeoutTimer) {
11664
+ clearTimeout(this._timeoutTimer);
11665
+ this._timeoutTimer = void 0;
11666
+ }
11461
11667
  this.cleanup();
11462
11668
  const error = err instanceof Error ? err : new Error(String(err));
11463
11669
  this.emit("session.error", error);
@@ -11471,17 +11677,6 @@ var CollabSession = class extends EventEmitter {
11471
11677
  this.emit("session.done", report);
11472
11678
  return report;
11473
11679
  }
11474
- /**
11475
- * Parse a completed agent's final text output and route the structured
11476
- * findings onto the FleetBus. Agents are instructed to emit one JSON object
11477
- * per finding / plan / evaluation; we scan the output for balanced JSON
11478
- * objects (tolerating prose, markdown, and ```json fences around them) and
11479
- * dispatch each by its discriminating key. The wired FleetBus listeners then
11480
- * collect them into the bugs / plans / evaluations maps.
11481
- *
11482
- * Failed or empty results are ignored, and a single malformed object never
11483
- * aborts the collation pass — see {@link extractJsonObjects}.
11484
- */
11485
11680
  async parseAndEmit(result) {
11486
11681
  if (result.status !== "success" || result.result == null) return;
11487
11682
  const text = typeof result.result === "string" ? result.result : JSON.stringify(result.result);
@@ -11497,12 +11692,6 @@ var CollabSession = class extends EventEmitter {
11497
11692
  });
11498
11693
  }
11499
11694
  }
11500
- /**
11501
- * Extract every top-level JSON object embedded in free-form agent text.
11502
- * Scans for balanced braces while respecting string literals and escape
11503
- * sequences, so prose, markdown headers, and code fences around the JSON
11504
- * are skipped. Malformed spans are dropped silently rather than thrown.
11505
- */
11506
11695
  extractJsonObjects(text) {
11507
11696
  const objects = [];
11508
11697
  let depth = 0;
@@ -11539,25 +11728,33 @@ var CollabSession = class extends EventEmitter {
11539
11728
  }
11540
11729
  return objects;
11541
11730
  }
11731
+ budgetForRole(role) {
11732
+ if (this.options.budgetOverrides?.[role]) {
11733
+ return this.options.budgetOverrides[role];
11734
+ }
11735
+ const defaults = {
11736
+ "bug-hunter": { maxIterations: 2e3, maxToolCalls: 5e3, timeoutMs: 10 * 60 * 1e3 },
11737
+ "refactor-planner": { maxIterations: 1500, maxToolCalls: 4e3, timeoutMs: 8 * 60 * 1e3 },
11738
+ "critic": { maxIterations: 1e3, maxToolCalls: 3e3, timeoutMs: 6 * 60 * 1e3 }
11739
+ };
11740
+ return defaults[role] ?? { maxIterations: 1500, maxToolCalls: 4e3, timeoutMs: 8 * 60 * 1e3 };
11741
+ }
11542
11742
  async spawnAgent(role, taskBrief) {
11743
+ const budget = this.budgetForRole(role);
11543
11744
  const cfg = {
11544
11745
  id: `${role}-${this.sessionId}`,
11545
11746
  name: role,
11546
- role
11547
- // No fleet_emit tool agents output structured JSON, the Director parses
11548
- // results and emits FleetBus events. This keeps the agent simple and the
11549
- // routing deterministic.
11747
+ role,
11748
+ tools: ["fleet_emit", "fleet_status", "read", "grep", "glob", "bash", "write"],
11749
+ maxIterations: budget.maxIterations,
11750
+ maxToolCalls: budget.maxToolCalls,
11751
+ timeoutMs: budget.timeoutMs
11550
11752
  };
11551
11753
  const subagentId = await this.director.spawn(cfg);
11552
- await this.director.assign({
11553
- id: randomUUID(),
11554
- subagentId,
11555
- description: taskBrief
11556
- });
11754
+ await this.director.assign({ id: randomUUID(), subagentId, description: taskBrief });
11557
11755
  return subagentId;
11558
11756
  }
11559
11757
  buildBugHunterTask() {
11560
- this.options.targetPaths.join(", ");
11561
11758
  const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
11562
11759
  const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
11563
11760
  ${f.content}`).join("\n\n");
@@ -11566,13 +11763,17 @@ ${f.content}`).join("\n\n");
11566
11763
  Target files:
11567
11764
  ${fileContents}
11568
11765
 
11569
- For each bug found, write ONE valid JSON line to the scratchpad with this exact structure:
11570
- { "finding": { "id": "<uuid>", "type": "<pattern>", "severity": "<critical|high|medium|low>", "location": { "file": "<path>", "line": <n> }, "description": "<explain>", "suggestedFix": "<optional>" } }.
11571
- After scanning all files, write your full markdown bug report to the shared scratchpad at:
11572
- ${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
11766
+ For each bug found, emit it using the fleet_emit tool immediately:
11767
+ { "type": "bug.found", "payload": { "finding": { "id": "<uuid>", "type": "<pattern>", "severity": "<critical|high|medium|low>", "location": { "file": "<path>", "line": <n> }, "description": "<explain>", "suggestedFix": "<optional>" } } }
11768
+
11769
+ After scanning all files, write your full markdown bug report to:
11770
+ ${scratchpad}/bug-hunter-report-${this.sessionId}.md
11771
+
11772
+ Important: emit each finding as soon as you find it. Do not batch or wait until the end.`;
11573
11773
  }
11574
11774
  buildRefactorPlannerTask() {
11575
11775
  const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
11776
+ const bugHunterReportPath = `${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
11576
11777
  const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
11577
11778
  ${f.content}`).join("\n\n");
11578
11779
  return `You are RefactorPlanner. Plan refactorings for the following files.
@@ -11580,26 +11781,146 @@ ${f.content}`).join("\n\n");
11580
11781
  Target files:
11581
11782
  ${fileContents}
11582
11783
 
11583
- 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:
11584
- { "plan": { "id": "<uuid>", "basedOnBugIds": ["<bug-id>"], "phases": [...], "riskScore": "<low|medium|high>", "estimatedChangeCount": <n>, "rollbackStrategy": "<text>" } }.
11585
- After planning, write your full markdown plan to:
11586
- ${scratchpad}/refactor-plan-${this.sessionId}.md`;
11784
+ Read the BugHunter report at: ${bugHunterReportPath}
11785
+
11786
+ For each bug you can address, emit a refactor plan using fleet_emit:
11787
+ { "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>" } } }
11788
+
11789
+ Also write your full markdown plan to:
11790
+ ${scratchpad}/refactor-plan-${this.sessionId}.md
11791
+
11792
+ Emit each plan immediately. Do not wait until planning is complete.`;
11587
11793
  }
11588
11794
  buildCriticTask() {
11589
11795
  const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
11796
+ const bugHunterReportPath = `${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
11797
+ const refactorPlanPath = `${scratchpad}/refactor-plan-${this.sessionId}.md`;
11590
11798
  const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
11591
11799
  ${f.content}`).join("\n\n");
11592
- return `You are Critic. Evaluate bug findings and refactor plans as they arrive via fleet bus events.
11800
+ return `You are Critic. Evaluate bug findings and refactor plans.
11593
11801
 
11594
11802
  Target files:
11595
11803
  ${fileContents}
11596
11804
 
11597
- Subscribe to bug.found events (from BugHunter) and refactor.plan events (from RefactorPlanner). For each subject, write one JSON line per evaluation:
11598
- { "evaluation": { "id": "<uuid>", "subjectType": "<bug_finding|refactor_plan>", "subjectId": "<id>", "score": <0-10>, "verdict": "<approve|needs_revision|reject>", "strengths": [...], "weaknesses": [...], "concerns": [...] } }.
11599
- After all evaluations, write your markdown critic report to:
11600
- ${scratchpad}/critic-report-${this.sessionId}.md`;
11805
+ Read the BugHunter report at: ${bugHunterReportPath}
11806
+ Read the RefactorPlanner report at: ${refactorPlanPath}
11807
+
11808
+ For each bug and refactor plan, emit your evaluation using fleet_emit:
11809
+ { "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>" }] } } }
11810
+
11811
+ After all evaluations, write your markdown report to:
11812
+ ${scratchpad}/critic-report-${this.sessionId}.md
11813
+
11814
+ Emit each evaluation immediately. Do not wait until you have read all reports.`;
11601
11815
  }
11602
11816
  wireFleetBus() {
11817
+ const dTool = this.fleetBus.filter("tool.executed", (e) => {
11818
+ this.progressBySubagent.set(e.subagentId, (this.progressBySubagent.get(e.subagentId) ?? 0) + 1);
11819
+ });
11820
+ this.disposers.push(dTool);
11821
+ const dBudget = this.fleetBus.filter("budget.threshold_reached", (e) => {
11822
+ const payload = e.payload;
11823
+ const role = this.roleFromSubagentId(e.subagentId);
11824
+ if (!role) return;
11825
+ const btwNotes = this.director.getLeaderBtwNotes();
11826
+ const alert = {
11827
+ sessionId: this.sessionId,
11828
+ subagentId: e.subagentId,
11829
+ role,
11830
+ level: "warning" /* WARNING */,
11831
+ message: `${role} hit ${payload.kind} soft limit (${payload.used}/${payload.limit})`,
11832
+ budgetKind: payload.kind,
11833
+ elapsedMs: payload.timeoutMs,
11834
+ limit: payload.limit,
11835
+ btwNotes
11836
+ };
11837
+ this.alerts.push(alert);
11838
+ this.fleetBus.emit({
11839
+ subagentId: e.subagentId,
11840
+ ts: Date.now(),
11841
+ type: "collab.warning",
11842
+ payload: alert
11843
+ });
11844
+ const decision = this.options.onBudgetWarning?.(alert) ?? "ignore";
11845
+ if (decision === "cancel") {
11846
+ this.cancel(`Director cancelled: ${role} ${payload.kind} threshold`);
11847
+ return;
11848
+ }
11849
+ if (payload.kind === "timeout") {
11850
+ const progress = this.progressBySubagent.get(e.subagentId) ?? 0;
11851
+ const lastProgress = this.lastTimeoutProgress.get(e.subagentId) ?? -1;
11852
+ if (progress <= lastProgress) {
11853
+ payload.deny();
11854
+ return;
11855
+ }
11856
+ this.lastTimeoutProgress.set(e.subagentId, progress);
11857
+ const newLimit = Math.min(Math.ceil((payload.timeoutMs ?? payload.limit) * 2), 24 * 60 * 6e4);
11858
+ setImmediate(() => {
11859
+ payload.extend({ timeoutMs: newLimit });
11860
+ });
11861
+ return;
11862
+ }
11863
+ if (decision === "extend") {
11864
+ setImmediate(() => {
11865
+ const base = Math.max(payload.limit, payload.used);
11866
+ const extra = {};
11867
+ switch (payload.kind) {
11868
+ case "iterations":
11869
+ extra.maxIterations = Math.min(Math.ceil(base * 1.5), 5e4);
11870
+ break;
11871
+ case "tool_calls":
11872
+ extra.maxToolCalls = Math.min(Math.ceil(base * 1.5), 1e5);
11873
+ break;
11874
+ case "tokens":
11875
+ extra.maxTokens = Math.min(Math.ceil(base * 1.5), 5e6);
11876
+ break;
11877
+ case "cost":
11878
+ extra.maxCostUsd = Math.min(base * 1.5, 100);
11879
+ break;
11880
+ }
11881
+ payload.extend(extra);
11882
+ });
11883
+ return;
11884
+ }
11885
+ if (payload.kind !== "timeout") {
11886
+ setImmediate(() => {
11887
+ const base = Math.max(payload.limit, payload.used);
11888
+ const extra = {};
11889
+ switch (payload.kind) {
11890
+ case "iterations":
11891
+ extra.maxIterations = Math.min(Math.ceil(base * 1.25), 5e4);
11892
+ break;
11893
+ case "tool_calls":
11894
+ extra.maxToolCalls = Math.min(Math.ceil(base * 1.25), 1e5);
11895
+ break;
11896
+ case "tokens":
11897
+ extra.maxTokens = Math.min(Math.ceil(base * 1.25), 5e6);
11898
+ break;
11899
+ case "cost":
11900
+ extra.maxCostUsd = Math.min(base * 1.25, 100);
11901
+ break;
11902
+ }
11903
+ payload.extend(extra);
11904
+ });
11905
+ }
11906
+ });
11907
+ this.disposers.push(dBudget);
11908
+ const dCancel = this.fleetBus.filter("director.cancel_collab", (e) => {
11909
+ const payload = e.payload;
11910
+ if (payload.sessionId !== this.sessionId) return;
11911
+ this.cancelled = true;
11912
+ if (this._timeoutTimer) {
11913
+ clearTimeout(this._timeoutTimer);
11914
+ this._timeoutTimer = void 0;
11915
+ }
11916
+ this.fleetBus.emit({
11917
+ subagentId: this.director.id,
11918
+ ts: Date.now(),
11919
+ type: "collab.cancelled",
11920
+ payload: { sessionId: this.sessionId, reason: payload.reason }
11921
+ });
11922
+ });
11923
+ this.disposers.push(dCancel);
11603
11924
  const d1 = this.fleetBus.filter("bug.found", (e) => {
11604
11925
  const payload = e.payload;
11605
11926
  if (payload?.finding) {
@@ -11625,10 +11946,19 @@ ${scratchpad}/critic-report-${this.sessionId}.md`;
11625
11946
  });
11626
11947
  this.disposers.push(d3);
11627
11948
  }
11949
+ roleFromSubagentId(subagentId) {
11950
+ for (const [role, id] of this.subagentIds) {
11951
+ if (id === subagentId) return role;
11952
+ }
11953
+ const match = subagentId.match(/^(bug-hunter|refactor-planner|critic)/);
11954
+ return match?.[1] ?? null;
11955
+ }
11628
11956
  assembleReport() {
11629
11957
  const bugList = Array.from(this.bugs.values());
11630
11958
  const planList = Array.from(this.plans.values());
11631
11959
  const evalList = Array.from(this.evaluations.values());
11960
+ let disposition = "completed";
11961
+ if (this.cancelled) disposition = "cancelled";
11632
11962
  const verdictOrder = {
11633
11963
  approve: 0,
11634
11964
  needs_revision: 1,
@@ -11642,33 +11972,41 @@ ${scratchpad}/critic-report-${this.sessionId}.md`;
11642
11972
  },
11643
11973
  "approve"
11644
11974
  );
11645
- const summary = this.buildMarkdownSummary(bugList, planList, evalList, overallVerdict);
11975
+ const summary = this.buildMarkdownSummary(bugList, planList, evalList, overallVerdict, disposition);
11646
11976
  return {
11647
11977
  sessionId: this.sessionId,
11648
11978
  startedAt: this.snapshot.createdAt,
11649
11979
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
11650
11980
  targetPaths: this.options.targetPaths,
11981
+ disposition,
11651
11982
  bugs: bugList,
11652
11983
  refactorPlans: planList,
11653
11984
  evaluations: evalList,
11985
+ alerts: [...this.alerts],
11654
11986
  overallVerdict,
11655
11987
  summary
11656
11988
  };
11657
11989
  }
11658
- buildMarkdownSummary(bugs, plans, evals, overallVerdict) {
11990
+ buildMarkdownSummary(bugs, plans, evals, overallVerdict, disposition) {
11659
11991
  const lines = [
11660
11992
  `## Collaborative Debugging Report \u2014 ${this.sessionId}`,
11661
11993
  "",
11662
11994
  `**Target:** ${this.options.targetPaths.join(", ")}`,
11995
+ `**Disposition:** ${disposition.toUpperCase()}`,
11663
11996
  `**Overall Verdict:** **${overallVerdict.toUpperCase()}**`,
11664
11997
  ""
11665
11998
  ];
11999
+ if (this.alerts.length > 0) {
12000
+ lines.push("### Alerts", "");
12001
+ for (const alert of this.alerts) {
12002
+ lines.push(`- **[${alert.level.toUpperCase()}]** ${alert.role}: ${alert.message}`);
12003
+ }
12004
+ lines.push("");
12005
+ }
11666
12006
  if (bugs.length > 0) {
11667
12007
  lines.push("### Bugs Found", "");
11668
12008
  for (const b of bugs) {
11669
- lines.push(
11670
- `- **[${b.severity.toUpperCase()}]** \`${b.location.file}:${b.location.line}\` \u2014 ${b.description}`
11671
- );
12009
+ lines.push(`- **[${b.severity.toUpperCase()}]** \`${b.location.file}:${b.location.line}\` \u2014 ${b.description}`);
11672
12010
  }
11673
12011
  lines.push("");
11674
12012
  }
@@ -11687,9 +12025,7 @@ ${scratchpad}/critic-report-${this.sessionId}.md`;
11687
12025
  for (const e of evals) {
11688
12026
  lines.push(`- [${e.subjectType}] score=${e.score}/10 \u2014 **${e.verdict.toUpperCase()}**`);
11689
12027
  for (const c of e.concerns) {
11690
- if (c.severity === "blocking") {
11691
- lines.push(` - ${c.description}`);
11692
- }
12028
+ if (c.severity === "blocking") lines.push(` - ${c.description}`);
11693
12029
  }
11694
12030
  }
11695
12031
  lines.push("");
@@ -11707,10 +12043,9 @@ var FleetSpawnBudgetError = class extends Error {
11707
12043
  kind;
11708
12044
  limit;
11709
12045
  observed;
11710
- constructor(kind, limit, observed) {
11711
- super(
11712
- 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}`
11713
- );
12046
+ constructor(kind, limit, observed, message) {
12047
+ 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}`;
12048
+ super(message ?? defaultMsg);
11714
12049
  this.name = "FleetSpawnBudgetError";
11715
12050
  this.kind = kind;
11716
12051
  this.limit = limit;
@@ -11731,6 +12066,20 @@ var FleetCostCapError = class extends Error {
11731
12066
  this.observed = observed;
11732
12067
  }
11733
12068
  };
12069
+ var FleetContextOverflowError = class extends Error {
12070
+ kind;
12071
+ limit;
12072
+ observed;
12073
+ constructor(limit, observed) {
12074
+ super(
12075
+ `Leader context overflow: leader has ${observed} tokens in flight (limit: ${limit}). Compact the leader context before spawning more subagents.`
12076
+ );
12077
+ this.name = "FleetContextOverflowError";
12078
+ this.kind = "max_context_load";
12079
+ this.limit = limit;
12080
+ this.observed = observed;
12081
+ }
12082
+ };
11734
12083
  var Director = class {
11735
12084
  /** Alias for the ICoordinator contract. `id` is retained for backward compatibility. */
11736
12085
  get coordinatorId() {
@@ -11747,6 +12096,25 @@ var Director = class {
11747
12096
  * injected; otherwise own FleetUsageAggregator.
11748
12097
  */
11749
12098
  usage;
12099
+ /**
12100
+ * Update the leader agent's current context pressure (full request tokens:
12101
+ * messages + systemPrompt + toolDefs). The director checks this before every
12102
+ * spawn — if the pressure exceeds `maxLeaderContextLoad * maxContext`,
12103
+ * spawning is refused with a `FleetContextOverflowError`.
12104
+ *
12105
+ * Call this after each leader agent iteration to keep the pressure current.
12106
+ * The compactor's `CompactReport.fullRequestTokensAfter` is a natural source.
12107
+ */
12108
+ setLeaderContextPressure(tokens) {
12109
+ this.leaderContextPressure = tokens;
12110
+ this.fleetManager?.setLeaderContextPressure(tokens);
12111
+ }
12112
+ /**
12113
+ * Read the leader agent's current context pressure.
12114
+ */
12115
+ getLeaderContextPressure() {
12116
+ return this.leaderContextPressure;
12117
+ }
11750
12118
  /**
11751
12119
  * Optional fleet-level policy container. When provided the Director
11752
12120
  * delegates spawn budgeting, manifest entries, and checkpointing to it
@@ -11832,6 +12200,23 @@ var Director = class {
11832
12200
  taskCompletedListener = null;
11833
12201
  /** Optional LLM classifier for smart dispatch. Passed from options. */
11834
12202
  dispatchClassifier;
12203
+ /** Leader agent's current context pressure (full request tokens). */
12204
+ leaderContextPressure = 0;
12205
+ /** Maximum context load fraction before spawn is refused. */
12206
+ maxLeaderContextLoad;
12207
+ /** Provider's max context window in tokens. */
12208
+ maxContext;
12209
+ /**
12210
+ * When set by `workComplete()`, the director stops dispatching new tasks
12211
+ * and terminates all running subagents. Used when the director's LLM decides
12212
+ * the goal is satisfied and no further spawns are needed — prevents the
12213
+ * coordinator from keeping workers alive for tasks that will never arrive.
12214
+ */
12215
+ workCompleteFlag = false;
12216
+ /** Pending /btw notes stashed by the leader agent (see setLeaderBtwNote). */
12217
+ _leaderBtwNotes = [];
12218
+ /** Active collab sessions tracked by sessionId (see spawnCollab). */
12219
+ _activeCollabSessions = /* @__PURE__ */ new Map();
11835
12220
  constructor(opts) {
11836
12221
  this.id = opts.config.coordinatorId || randomUUID();
11837
12222
  this.manifestPath = opts.manifestPath;
@@ -11847,6 +12232,8 @@ var Director = class {
11847
12232
  this.dispatchClassifier = opts.dispatchClassifier;
11848
12233
  this.maxFleetCostUsd = opts.directorBudget?.maxCostUsd ?? Number.POSITIVE_INFINITY;
11849
12234
  this.maxBudgetExtensions = opts.maxBudgetExtensions ?? 5;
12235
+ this.maxLeaderContextLoad = opts.maxLeaderContextLoad ?? 0.85;
12236
+ this.maxContext = opts.maxContext ?? 128e3;
11850
12237
  this.sessionsRoot = opts.sessionsRoot;
11851
12238
  this.directorRunId = opts.directorRunId ?? this.id;
11852
12239
  this.stateCheckpoint = opts.stateCheckpointPath ? new DirectorStateCheckpoint(opts.stateCheckpointPath, {
@@ -11874,7 +12261,7 @@ var Director = class {
11874
12261
  this.fleet = new FleetBus();
11875
12262
  this.usage = new FleetUsageAggregator(
11876
12263
  this.fleet,
11877
- (id, provider, model) => {
12264
+ (_id, provider, model) => {
11878
12265
  if (provider && model) return this.priceLookups.get(`${provider}/${model}`);
11879
12266
  return void 0;
11880
12267
  },
@@ -11936,6 +12323,9 @@ var Director = class {
11936
12323
  });
11937
12324
  this.fleet.filter("budget.threshold_reached", (e) => {
11938
12325
  const payload = e.payload;
12326
+ if (e.subagentId.startsWith("bug-hunter-") || e.subagentId.startsWith("refactor-planner-") || e.subagentId.startsWith("critic-")) {
12327
+ return;
12328
+ }
11939
12329
  if (payload.kind === "timeout") {
11940
12330
  const progress = progressBySubagent.get(e.subagentId) ?? 0;
11941
12331
  const lastProgress = lastTimeoutProgress.get(e.subagentId) ?? -1;
@@ -12014,6 +12404,107 @@ var Director = class {
12014
12404
  extensionsFor(subagentId) {
12015
12405
  return this.extendTotals.get(subagentId) ?? 0;
12016
12406
  }
12407
+ /**
12408
+ * Signal that the director's work is done. Once called:
12409
+ * - `spawn()` throws `FleetSpawnBudgetError('max_spawns', …)` — no new
12410
+ * subagents can be created
12411
+ * - Running subagents are NOT forcibly stopped — they finish naturally,
12412
+ * but no new tasks are dispatched to them
12413
+ *
12414
+ * This lets the director LLM say "I'm satisfied with the results, stop
12415
+ * spawning and wind down" — without killing in-flight work mid-execution.
12416
+ * Call `terminateAll()` separately if you need immediate teardown.
12417
+ *
12418
+ * Idempotent — calling twice is a no-op.
12419
+ */
12420
+ workComplete() {
12421
+ this.workCompleteFlag = true;
12422
+ this.fleet.emit({ subagentId: this.id, ts: Date.now(), type: "director.work_complete", payload: {} });
12423
+ }
12424
+ /** Returns true if `workComplete()` has been called on this director. */
12425
+ isWorkComplete() {
12426
+ return this.workCompleteFlag;
12427
+ }
12428
+ /**
12429
+ * Stashes a /btw note on the leader agent's context. The leader's agent loop
12430
+ * calls `consumeBtwNotes()` at each iteration boundary and folds pending notes
12431
+ * into the conversation as a visible block — no abort, no restart, just a
12432
+ * "by the way" nudge the model picks up on its next turn.
12433
+ *
12434
+ * This is the entry point for the host (CLI, TUI) to inject /btw notes
12435
+ * programmatically without going through the slash-command path.
12436
+ */
12437
+ setLeaderBtwNote(note) {
12438
+ const trimmed = note.trim();
12439
+ if (!trimmed) return this._leaderBtwNotes.length;
12440
+ this._leaderBtwNotes = [...this._leaderBtwNotes, trimmed].slice(-20);
12441
+ return this._leaderBtwNotes.length;
12442
+ }
12443
+ /**
12444
+ * Read and clear all pending /btw notes the leader has stashed.
12445
+ * Returns them in FIFO order (empty array when none).
12446
+ *
12447
+ * Called by CollabSession when a budget threshold event fires so the
12448
+ * Director can inspect accumulated /btw notes before deciding whether
12449
+ * to cancel the collab session or let it continue.
12450
+ */
12451
+ getLeaderBtwNotes() {
12452
+ const notes = this._leaderBtwNotes;
12453
+ this._leaderBtwNotes = [];
12454
+ return notes;
12455
+ }
12456
+ /**
12457
+ * Peek at pending /btw notes without consuming them.
12458
+ * Useful for UI to show "N pending notes" without clearing them.
12459
+ */
12460
+ peekLeaderBtwNotes() {
12461
+ return [...this._leaderBtwNotes];
12462
+ }
12463
+ /**
12464
+ * Drain (read + clear) all /btw notes in one call.
12465
+ * Alias for getLeaderBtwNotes() — kept for symmetry with consumeBtwNotes()
12466
+ * in the agent's btw.ts. The Director calls this at the point where it
12467
+ * makes a cancellation decision, not on every budget event.
12468
+ */
12469
+ drainLeaderBtwNotes() {
12470
+ return this.getLeaderBtwNotes();
12471
+ }
12472
+ /**
12473
+ * Cancel an active collab session by its id.
12474
+ * Emits `director.cancel_collab` on the FleetBus so the session's agents
12475
+ * finish early with a 'cancelled' disposition.
12476
+ *
12477
+ * Returns silently if the session id is not tracked or already settled.
12478
+ * The CollabDebugReport will reflect 'cancelled' disposition when awaited.
12479
+ */
12480
+ cancelCollabSession(sessionId, reason = "Director cancelled") {
12481
+ const session = this._activeCollabSessions.get(sessionId);
12482
+ if (!session || session.isCancelled()) return;
12483
+ session.cancel(reason);
12484
+ for (const [_role, subagentId] of session.getSubagentIds()) {
12485
+ this.coordinator.stop(subagentId).catch(() => {
12486
+ });
12487
+ }
12488
+ }
12489
+ /**
12490
+ * Subscribe a callback to be notified whenever a collab session raises
12491
+ * an alert (warning level). The callback receives the full DirectorAlert
12492
+ * payload so the host (CLI, TUI) can display it to the user.
12493
+ * Returns an unsubscribe function.
12494
+ */
12495
+ onCollabAlert(handler) {
12496
+ return this.fleet.filter("collab.warning", (e) => {
12497
+ handler(e.payload);
12498
+ });
12499
+ }
12500
+ /**
12501
+ * Returns all active (not yet settled) collab session ids.
12502
+ * Useful for the TUI to render a "N active sessions" badge and for
12503
+ * the host to know what can be cancelled.
12504
+ */
12505
+ activeCollabSessions() {
12506
+ return Array.from(this._activeCollabSessions.keys());
12507
+ }
12017
12508
  /** Best-effort session-writer append. Swallows failures — the director
12018
12509
  * must not break a fleet run because the session JSONL handle closed. */
12019
12510
  async appendSessionEvent(event) {
@@ -12052,12 +12543,21 @@ var Director = class {
12052
12543
  * it the `cost` column in `usage.snapshot()` stays at 0.
12053
12544
  */
12054
12545
  async spawn(config, priceLookup) {
12546
+ if (this.workCompleteFlag) {
12547
+ throw new FleetSpawnBudgetError(
12548
+ "max_spawns",
12549
+ this.maxSpawns,
12550
+ this.spawnCount + 1,
12551
+ "workComplete() has been called \u2014 director closed further spawning"
12552
+ );
12553
+ }
12055
12554
  if (this.fleetManager) {
12056
12555
  const rejection = this.fleetManager.canSpawn(config);
12057
12556
  if (rejection) {
12058
12557
  if (rejection.kind === "max_spawn_depth") throw new FleetSpawnBudgetError("max_spawn_depth", rejection.limit, rejection.observed);
12059
12558
  if (rejection.kind === "max_spawns") throw new FleetSpawnBudgetError("max_spawns", rejection.limit, rejection.observed);
12060
12559
  if (rejection.kind === "max_cost_usd") throw new FleetCostCapError(rejection.limit, rejection.observed);
12560
+ if (rejection.kind === "max_context_load") throw new FleetContextOverflowError(rejection.limit, rejection.observed);
12061
12561
  }
12062
12562
  } else {
12063
12563
  if (this.spawnDepth >= this.maxSpawnDepth) {
@@ -12072,9 +12572,15 @@ var Director = class {
12072
12572
  throw new FleetCostCapError(this.maxFleetCostUsd, totalCost);
12073
12573
  }
12074
12574
  }
12575
+ if (this.maxLeaderContextLoad < 1) {
12576
+ const threshold = this.maxContext * this.maxLeaderContextLoad;
12577
+ if (this.leaderContextPressure >= threshold) {
12578
+ throw new FleetContextOverflowError(threshold, this.leaderContextPressure);
12579
+ }
12580
+ }
12075
12581
  }
12076
12582
  let result;
12077
- const needsNickname = config.name === config.role || !config.name || config.name === "subagent";
12583
+ const needsNickname = config.name === config.role || !config.name || config.name === "subagent" || config.name === "adhoc";
12078
12584
  if (needsNickname) {
12079
12585
  const role = config.role ?? "subagent";
12080
12586
  if (this.fleetManager) {
@@ -12299,6 +12805,24 @@ var Director = class {
12299
12805
  */
12300
12806
  async assign(task) {
12301
12807
  const taskWithId = task.id ? task : { ...task, id: randomUUID() };
12808
+ if (this.workCompleteFlag) {
12809
+ const synthetic = {
12810
+ subagentId: taskWithId.subagentId ?? "unassigned",
12811
+ taskId: taskWithId.id,
12812
+ status: "stopped",
12813
+ error: { kind: "aborted_by_parent", message: "Director called workComplete() \u2014 no further tasks will run", retryable: false },
12814
+ iterations: 0,
12815
+ toolCalls: 0,
12816
+ durationMs: 0
12817
+ };
12818
+ this.completed.set(taskWithId.id, synthetic);
12819
+ const waiter = this.taskWaiters.get(taskWithId.id);
12820
+ if (waiter) {
12821
+ waiter.resolve(synthetic);
12822
+ this.taskWaiters.delete(taskWithId.id);
12823
+ }
12824
+ return taskWithId.id;
12825
+ }
12302
12826
  if (task.subagentId) {
12303
12827
  if (this.fleetManager) {
12304
12828
  this.fleetManager.addTaskToSubagent(task.subagentId, taskWithId.id);
@@ -12521,12 +13045,14 @@ var Director = class {
12521
13045
  makeAskTool(this),
12522
13046
  makeRollUpTool(this),
12523
13047
  makeTerminateTool(this),
13048
+ makeTerminateAllTool(this),
12524
13049
  makeFleetStatusTool(this),
12525
13050
  makeFleetUsageTool(this),
12526
13051
  makeFleetSessionTool(this),
12527
13052
  makeFleetHealthTool(this),
12528
13053
  makeCollabDebugTool(this),
12529
- makeFleetEmitTool(this)
13054
+ makeFleetEmitTool(this),
13055
+ makeWorkCompleteTool(this)
12530
13056
  ];
12531
13057
  return t;
12532
13058
  }
@@ -12546,7 +13072,15 @@ var Director = class {
12546
13072
  * three agents complete or the session times out.
12547
13073
  */
12548
13074
  async spawnCollab(options) {
12549
- const session = new CollabSession(this, this.fleet, options);
13075
+ const session = new CollabSession(this, this.fleet, {
13076
+ ...options,
13077
+ onBudgetWarning: (alert) => {
13078
+ return options.onBudgetWarning?.(alert) ?? "ignore";
13079
+ }
13080
+ });
13081
+ this._activeCollabSessions.set(session.sessionId, session);
13082
+ session.on("session.done", () => this._activeCollabSessions.delete(session.sessionId));
13083
+ session.on("session.error", () => this._activeCollabSessions.delete(session.sessionId));
12550
13084
  return session.start();
12551
13085
  }
12552
13086
  /**
@@ -15437,7 +15971,7 @@ function analyzeCriticalPath(graph) {
15437
15971
  blockedTasks
15438
15972
  };
15439
15973
  }
15440
- function getTransitiveBlocked(graph, taskId, blocksMap) {
15974
+ function getTransitiveBlocked(_graph, taskId, blocksMap) {
15441
15975
  const visited = /* @__PURE__ */ new Set();
15442
15976
  const queue = [taskId];
15443
15977
  while (queue.length > 0) {
@@ -15453,7 +15987,7 @@ function getTransitiveBlocked(graph, taskId, blocksMap) {
15453
15987
  }
15454
15988
  return visited;
15455
15989
  }
15456
- function computeCriticalPath(graph, topoOrder, blockedByMap) {
15990
+ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
15457
15991
  const allIds = Array.from(graph.nodes.keys());
15458
15992
  if (allIds.length === 0) return [];
15459
15993
  const dist = /* @__PURE__ */ new Map();
@@ -15871,13 +16405,11 @@ function createAutoExecutor(opts) {
15871
16405
 
15872
16406
  // src/sdd/sdd-task-decomposer.ts
15873
16407
  var SddTaskDecomposer = class {
15874
- constructor(tracker, graph, opts = {}) {
16408
+ constructor(tracker, _graph, opts = {}) {
15875
16409
  this.tracker = tracker;
15876
- this.graph = graph;
15877
16410
  this.slots = Math.min(16, Math.max(1, opts.parallelSlots ?? 4));
15878
16411
  }
15879
16412
  tracker;
15880
- graph;
15881
16413
  slots;
15882
16414
  wave = 0;
15883
16415
  // -------------------------------------------------------------------
@@ -16030,7 +16562,7 @@ var SddParallelRun = class {
16030
16562
  this.coordinator.setRunner?.(runner);
16031
16563
  }
16032
16564
  defaultFactory() {
16033
- return async (config) => ({
16565
+ return async (_config) => ({
16034
16566
  agent: this.opts.agent,
16035
16567
  events: this.opts.agent.events
16036
16568
  });
@@ -16844,6 +17376,12 @@ function roughEstimate(messages) {
16844
17376
  return total;
16845
17377
  }
16846
17378
  function createContextManagerTool(opts = {}) {
17379
+ const minCompactThreshold = opts.minCompactThreshold ?? 0;
17380
+ const noopRetryDeltaTokens = opts.noopRetryDeltaTokens ?? 2e3;
17381
+ const maxContext = opts.maxContext ?? 128e3;
17382
+ const compactThresholdFraction = opts.compactThresholdFraction ?? 0.5;
17383
+ const effectiveThreshold = minCompactThreshold > 0 ? minCompactThreshold : Math.floor(maxContext * compactThresholdFraction);
17384
+ let lastNoopTokens = 0;
16847
17385
  return {
16848
17386
  name: CONTEXT_MANAGER_TOOL_NAME,
16849
17387
  description: 'Inspect or reorganize the conversation context window. Use "check" to see token budget. Use "summary" to collapse a message range into a concise note (provide "text" for custom summary). Use "prune" to remove specific messages by index. Use "add_note" to inject a summary note. Use "compact" to run aggressive compaction. Use "repair" to remove orphan tool_use/tool_result blocks after manual context surgery.',
@@ -16934,11 +17472,41 @@ function createContextManagerTool(opts = {}) {
16934
17472
  notes: "No compactor registered. Use /compact aggressive via slash command instead."
16935
17473
  };
16936
17474
  }
17475
+ const fullEstimate = input.systemPrompt != null && Array.isArray(input.tools) ? estimateRequestTokens(messages, input.systemPrompt, input.tools) : { total: beforeTokens};
17476
+ const currentTokens = fullEstimate.total;
17477
+ if (lastNoopTokens > 0) {
17478
+ const delta = currentTokens - lastNoopTokens;
17479
+ if (delta < noopRetryDeltaTokens) {
17480
+ return {
17481
+ action: "compact",
17482
+ beforeTokens,
17483
+ afterTokens: beforeTokens,
17484
+ messageCount: messages.length,
17485
+ notes: `Compact is a NOOP retry: context grew only ${delta} tokens since the last no-op attempt (threshold: ${noopRetryDeltaTokens}). Skip until more content accumulates.`
17486
+ };
17487
+ }
17488
+ }
17489
+ if (currentTokens < effectiveThreshold) {
17490
+ return {
17491
+ action: "compact",
17492
+ beforeTokens,
17493
+ afterTokens: beforeTokens,
17494
+ messageCount: messages.length,
17495
+ notes: `Context tokens (${currentTokens}) below compact threshold (${effectiveThreshold}). Skipping.`
17496
+ };
17497
+ }
16937
17498
  const report = await opts.compactor.compact(ctx);
16938
17499
  ctx.clearFileTracking();
16939
17500
  const repair = applyMessages([...ctx.messages]);
16940
17501
  const afterTokens = repair.changed ? roughEstimate(ctx.messages) : report.after;
16941
17502
  const repaired = report.repaired ?? (repair.changed ? repair : void 0);
17503
+ const reduced = report.fullRequestTokensBefore > report.fullRequestTokensAfter;
17504
+ const repairedSomething = !!report.repaired;
17505
+ if (reduced || repairedSomething) {
17506
+ lastNoopTokens = 0;
17507
+ } else {
17508
+ lastNoopTokens = currentTokens;
17509
+ }
16942
17510
  return {
16943
17511
  action: "compact",
16944
17512
  beforeTokens,