@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
package/dist/index.js CHANGED
@@ -56,8 +56,8 @@ async function atomicWrite(targetPath, content, opts = {}) {
56
56
  }
57
57
  let mode;
58
58
  try {
59
- const stat9 = await fsp2.stat(targetPath);
60
- mode = stat9.mode & 511;
59
+ const stat8 = await fsp2.stat(targetPath);
60
+ mode = stat8.mode & 511;
61
61
  } catch {
62
62
  mode = opts.mode;
63
63
  }
@@ -1676,6 +1676,7 @@ var HybridCompactor = class {
1676
1676
  }
1677
1677
  async compact(ctx, opts = {}) {
1678
1678
  const beforeTokens = this.estimateMessages(ctx.messages);
1679
+ const beforeFull = this.estimateFullRequest(ctx);
1679
1680
  const reductions = [];
1680
1681
  const policy = readContextWindowPolicy(ctx);
1681
1682
  const preserveK = policy?.preserveK ?? this.preserveK;
@@ -1691,9 +1692,12 @@ var HybridCompactor = class {
1691
1692
  ctx.state.replaceMessages(repaired.messages);
1692
1693
  }
1693
1694
  const afterTokens = this.estimateMessages(ctx.messages);
1695
+ const afterFull = this.estimateFullRequest(ctx);
1694
1696
  return {
1695
1697
  before: beforeTokens,
1696
1698
  after: afterTokens,
1699
+ fullRequestTokensBefore: beforeFull,
1700
+ fullRequestTokensAfter: afterFull,
1697
1701
  reductions,
1698
1702
  repaired: repaired.report.changed ? {
1699
1703
  removedToolUses: repaired.report.removedToolUses,
@@ -1702,6 +1706,14 @@ var HybridCompactor = class {
1702
1706
  } : void 0
1703
1707
  };
1704
1708
  }
1709
+ /**
1710
+ * Estimate the full API request token count: messages + systemPrompt + toolDefs.
1711
+ * This is the accurate figure for context-window pressure monitoring.
1712
+ */
1713
+ estimateFullRequest(ctx) {
1714
+ const breakdown = estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []);
1715
+ return breakdown.total;
1716
+ }
1705
1717
  eliseOldToolResults(ctx, preserveK = this.preserveK, eliseThreshold = this.eliseThreshold) {
1706
1718
  const messages = ctx.messages;
1707
1719
  let pairCount = 0;
@@ -4036,11 +4048,11 @@ function validateAgainstSchema(value, schema) {
4036
4048
  walk2(value, schema, "", errors);
4037
4049
  return { ok: errors.length === 0, errors };
4038
4050
  }
4039
- function walk2(value, schema, path31, errors) {
4051
+ function walk2(value, schema, path35, errors) {
4040
4052
  if (schema.enum !== void 0) {
4041
4053
  if (!schema.enum.some((e) => deepEqual(e, value))) {
4042
4054
  errors.push({
4043
- path: path31 || "<root>",
4055
+ path: path35 || "<root>",
4044
4056
  message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
4045
4057
  });
4046
4058
  return;
@@ -4049,7 +4061,7 @@ function walk2(value, schema, path31, errors) {
4049
4061
  if (typeof schema.type === "string") {
4050
4062
  if (!checkType(value, schema.type)) {
4051
4063
  errors.push({
4052
- path: path31 || "<root>",
4064
+ path: path35 || "<root>",
4053
4065
  message: `expected ${schema.type}, got ${describeType(value)}`
4054
4066
  });
4055
4067
  return;
@@ -4059,19 +4071,19 @@ function walk2(value, schema, path31, errors) {
4059
4071
  const obj = value;
4060
4072
  for (const req of schema.required ?? []) {
4061
4073
  if (!(req in obj)) {
4062
- errors.push({ path: joinPath(path31, req), message: "required property missing" });
4074
+ errors.push({ path: joinPath(path35, req), message: "required property missing" });
4063
4075
  }
4064
4076
  }
4065
4077
  if (schema.properties) {
4066
4078
  for (const [key, subSchema] of Object.entries(schema.properties)) {
4067
4079
  if (key in obj) {
4068
- walk2(obj[key], subSchema, joinPath(path31, key), errors);
4080
+ walk2(obj[key], subSchema, joinPath(path35, key), errors);
4069
4081
  }
4070
4082
  }
4071
4083
  }
4072
4084
  }
4073
4085
  if (schema.type === "array" && Array.isArray(value) && schema.items) {
4074
- value.forEach((item, i) => walk2(item, schema.items, `${path31}[${i}]`, errors));
4086
+ value.forEach((item, i) => walk2(item, schema.items, `${path35}[${i}]`, errors));
4075
4087
  }
4076
4088
  }
4077
4089
  function checkType(value, type) {
@@ -4126,80 +4138,85 @@ function deepEqual(a, b) {
4126
4138
 
4127
4139
  // src/utils/json-repair.ts
4128
4140
  function completePartialObject(s) {
4129
- let result = s;
4130
- const trimmed = result.trim();
4131
- if (!trimmed.startsWith("{")) return s;
4132
- for (let pass = 0; pass < 3; pass++) {
4133
- let braceDepth = 0;
4134
- let inString2 = false;
4135
- let escaped2 = false;
4136
- let foundClose = false;
4137
- for (const ch of result) {
4138
- if (escaped2) {
4139
- escaped2 = false;
4141
+ if (!s.trim().startsWith("{")) return s;
4142
+ if (tryParse(s).ok) return s;
4143
+ const stack = [];
4144
+ let inString = false;
4145
+ let escaped = false;
4146
+ let sawKey = false;
4147
+ let prevSig = "";
4148
+ let contentEnd = 0;
4149
+ let stringBraceDepth = 0;
4150
+ for (let i = 0; i < s.length; i++) {
4151
+ const ch = s[i];
4152
+ if (inString) {
4153
+ contentEnd = i + 1;
4154
+ if (escaped) {
4155
+ escaped = false;
4140
4156
  continue;
4141
4157
  }
4142
4158
  if (ch === "\\") {
4143
- escaped2 = true;
4159
+ escaped = true;
4144
4160
  continue;
4145
4161
  }
4146
4162
  if (ch === '"') {
4147
- inString2 = !inString2;
4163
+ inString = false;
4164
+ prevSig = '"';
4165
+ stringBraceDepth = 0;
4148
4166
  continue;
4149
4167
  }
4150
- if (inString2) continue;
4151
- if (ch === "{") {
4152
- braceDepth++;
4153
- foundClose = false;
4154
- } else if (ch === "}") {
4155
- braceDepth--;
4156
- if (braceDepth === 0) foundClose = true;
4157
- }
4158
- }
4159
- if (foundClose || braceDepth <= 0) break;
4160
- result += "}".repeat(braceDepth);
4161
- }
4162
- if (tryParse(result).ok) return result;
4163
- let inString = false;
4164
- let escaped = false;
4165
- for (let i = result.length - 1; i >= 0; i--) {
4166
- const ch = result[i];
4167
- if (escaped) {
4168
- escaped = false;
4169
- continue;
4170
- }
4171
- if (ch === "\\") {
4172
- escaped = true;
4168
+ if (ch === "{") stringBraceDepth++;
4169
+ else if (ch === "}" && stringBraceDepth > 0) stringBraceDepth--;
4173
4170
  continue;
4174
4171
  }
4172
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") continue;
4173
+ contentEnd = i + 1;
4175
4174
  if (ch === '"') {
4176
- let nextNonWs;
4177
- for (let j = i + 1; j < result.length; j++) {
4178
- const nc = result[j];
4179
- if (nc === " " || nc === " " || nc === "\n" || nc === "\r") continue;
4180
- nextNonWs = result[j];
4181
- break;
4182
- }
4183
- if (nextNonWs === ":") {
4184
- continue;
4185
- } else {
4186
- inString = !inString;
4187
- continue;
4188
- }
4175
+ inString = true;
4176
+ sawKey = true;
4177
+ stringBraceDepth = 0;
4178
+ prevSig = '"';
4179
+ } else if (ch === "{" || ch === "[") {
4180
+ stack.push(ch);
4181
+ prevSig = ch;
4182
+ } else if (ch === "}" || ch === "]") {
4183
+ stack.pop();
4184
+ prevSig = ch;
4185
+ } else {
4186
+ prevSig = ch;
4189
4187
  }
4190
4188
  }
4189
+ if (!sawKey && !inString) return s;
4190
+ let result = s.slice(0, contentEnd);
4191
4191
  if (inString) {
4192
- result = result.trimEnd();
4193
- if (result.endsWith("\\")) result = result.slice(0, -1);
4194
- let depth = 0;
4195
- for (const ch of result) {
4196
- if (ch === "{") depth++;
4197
- else if (ch === "}") depth = Math.max(0, depth - 1);
4192
+ if (escaped) {
4193
+ result = result.slice(0, -1);
4194
+ } else if (endsWithInvalidEscape(result)) {
4195
+ result = result.slice(0, -2);
4198
4196
  }
4199
- result += '"' + "}".repeat(Math.max(1, depth));
4197
+ if (stringBraceDepth > 0) result += "}".repeat(stringBraceDepth);
4198
+ result += '"';
4199
+ } else if (prevSig === ":") {
4200
+ result += "null";
4201
+ }
4202
+ for (let k = stack.length - 1; k >= 0; k--) {
4203
+ result += stack[k] === "{" ? "}" : "]";
4204
+ }
4205
+ if (!tryParse(result).ok) {
4206
+ const patched = result.replace(/:(\s*)([}\]])/g, ":null$2");
4207
+ if (tryParse(patched).ok) result = patched;
4200
4208
  }
4201
4209
  return result;
4202
4210
  }
4211
+ var VALID_ESCAPE = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t", "u"]);
4212
+ function endsWithInvalidEscape(str) {
4213
+ const last = str[str.length - 1];
4214
+ if (str[str.length - 2] !== "\\" || last === void 0) return false;
4215
+ if (VALID_ESCAPE.has(last)) return false;
4216
+ let backslashes = 0;
4217
+ for (let k = str.length - 2; k >= 0 && str[k] === "\\"; k--) backslashes++;
4218
+ return backslashes % 2 === 1;
4219
+ }
4203
4220
  function tryParse(s) {
4204
4221
  try {
4205
4222
  return { ok: true, value: JSON.parse(s) };
@@ -4340,8 +4357,8 @@ var DefaultSessionStore = class {
4340
4357
  return JSON.parse(raw);
4341
4358
  } catch {
4342
4359
  const full = this.sessionPath(id, ".jsonl");
4343
- const stat9 = await fsp2.stat(full);
4344
- const summary = await this.summarize(id, stat9.mtime.toISOString());
4360
+ const stat8 = await fsp2.stat(full);
4361
+ const summary = await this.summarize(id, stat8.mtime.toISOString());
4345
4362
  await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
4346
4363
  console.warn(
4347
4364
  `[session-store] Failed to write manifest for "${id}":`,
@@ -4534,7 +4551,6 @@ var FileSessionWriter = class {
4534
4551
  }
4535
4552
  return event;
4536
4553
  }
4537
- promptIndex = 0;
4538
4554
  pendingFileSnapshots = [];
4539
4555
  /** Tracks open tool_use IDs during the current run to serialize on close for resume. */
4540
4556
  openToolUses = /* @__PURE__ */ new Set();
@@ -4625,7 +4641,6 @@ var FileSessionWriter = class {
4625
4641
  await this.writeFileSnapshot(promptIndex, [...this.pendingFileSnapshots]);
4626
4642
  this.pendingFileSnapshots = [];
4627
4643
  }
4628
- this.promptIndex = promptIndex + 1;
4629
4644
  await this.append({
4630
4645
  type: "checkpoint",
4631
4646
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -4721,6 +4736,38 @@ var FileSessionWriter = class {
4721
4736
  `;
4722
4737
  await fsp2.writeFile(this.filePath, record, "utf8");
4723
4738
  }
4739
+ /**
4740
+ * Idea #1 — write an in-flight marker. The agent loop should call
4741
+ * this at the start of each long-running operation; a matching
4742
+ * `clearInFlightMarker` follows on clean exit. A stale marker
4743
+ * (no end) is what `SessionRecovery.detectStale` looks for.
4744
+ */
4745
+ async writeInFlightMarker(context) {
4746
+ if (!context || context.length > 500) {
4747
+ throw new Error("In-flight context must be 1..500 chars");
4748
+ }
4749
+ await this.append({
4750
+ type: "in_flight_start",
4751
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
4752
+ context
4753
+ });
4754
+ this.events?.emit("in_flight.started", { context, ts: (/* @__PURE__ */ new Date()).toISOString() });
4755
+ }
4756
+ /**
4757
+ * Idea #1 — close the in-flight marker. Idempotent in spirit
4758
+ * (you can call it after a successful iteration even if you
4759
+ * didn't open one this round) — but the session log records
4760
+ * every call so postmortem tooling can see "the agent finished
4761
+ * cleanly X times, then died without finishing Y".
4762
+ */
4763
+ async clearInFlightMarker(reason) {
4764
+ await this.append({
4765
+ type: "in_flight_end",
4766
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
4767
+ reason
4768
+ });
4769
+ this.events?.emit("in_flight.ended", { reason, ts: (/* @__PURE__ */ new Date()).toISOString() });
4770
+ }
4724
4771
  };
4725
4772
  function userInputTitle(content) {
4726
4773
  const text = typeof content === "string" ? content : content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
@@ -6976,8 +7023,9 @@ var IntelligentCompactor = class {
6976
7023
  }
6977
7024
  async compact(ctx, opts = {}) {
6978
7025
  const beforeTokens = this.estimateTokens(ctx.messages);
7026
+ const beforeFull = this.estimateFullRequest(ctx);
6979
7027
  const reductions = [];
6980
- const load = beforeTokens / this.maxContext;
7028
+ const load = beforeFull / this.maxContext;
6981
7029
  const aggressive = load >= this.hardThreshold ? true : opts.aggressive ?? load >= this.softThreshold;
6982
7030
  const saved1 = this.eliseOldToolResults(ctx);
6983
7031
  if (saved1 > 0) reductions.push({ phase: "elision", saved: saved1 });
@@ -6991,9 +7039,12 @@ var IntelligentCompactor = class {
6991
7039
  const repaired = repairToolUseAdjacency(ctx.messages);
6992
7040
  if (repaired.report.changed) ctx.state.replaceMessages(repaired.messages);
6993
7041
  const afterTokens = this.estimateTokens(ctx.messages);
7042
+ const afterFull = this.estimateFullRequest(ctx);
6994
7043
  return {
6995
7044
  before: beforeTokens,
6996
7045
  after: afterTokens,
7046
+ fullRequestTokensBefore: beforeFull,
7047
+ fullRequestTokensAfter: afterFull,
6997
7048
  reductions,
6998
7049
  repaired: repaired.report.changed ? {
6999
7050
  removedToolUses: repaired.report.removedToolUses,
@@ -7002,6 +7053,14 @@ var IntelligentCompactor = class {
7002
7053
  } : void 0
7003
7054
  };
7004
7055
  }
7056
+ /**
7057
+ * Estimate the full API request token count: messages + systemPrompt + toolDefs.
7058
+ * This is the accurate figure for context-window pressure monitoring.
7059
+ */
7060
+ estimateFullRequest(ctx) {
7061
+ const breakdown = estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []);
7062
+ return breakdown.total;
7063
+ }
7005
7064
  async summarizeAncientTurns(ctx) {
7006
7065
  const messages = ctx.messages;
7007
7066
  const cutoff = Math.max(0, messages.length - this.preserveK * 2);
@@ -7014,29 +7073,26 @@ var IntelligentCompactor = class {
7014
7073
  try {
7015
7074
  summaryText = await this.callSummarizer(toSummarize, ctx);
7016
7075
  } catch {
7017
- const toolNames = /* @__PURE__ */ new Set();
7018
- const filePaths = /* @__PURE__ */ new Set();
7019
- let userTurns = 0;
7020
- let assistantTurns = 0;
7076
+ const preservedMessages = [];
7021
7077
  for (const m of toSummarize) {
7022
- if (m.role === "user") userTurns++;
7023
- else if (m.role === "assistant") {
7024
- assistantTurns++;
7025
- if (Array.isArray(m.content)) {
7026
- for (const b of m.content) {
7027
- if (b.type === "tool_use") toolNames.add(b.name ?? "unknown");
7028
- }
7029
- }
7078
+ if (m.role === "system") continue;
7079
+ if (typeof m.content === "string") {
7080
+ preservedMessages.push(m);
7081
+ } else if (Array.isArray(m.content)) {
7082
+ const textParts = m.content.filter(isTextBlock);
7083
+ const toolParts = m.content.filter((b) => b.type === "tool_use" || b.type === "tool_result");
7084
+ const next = {
7085
+ role: m.role,
7086
+ content: [
7087
+ ...textParts,
7088
+ ...toolParts.length > 0 ? [{ type: "text", text: `[${toolParts.length} tool call(s) omitted]` }] : []
7089
+ ]
7090
+ };
7091
+ preservedMessages.push(next);
7030
7092
  }
7031
- const text = typeof m.content === "string" ? m.content : "";
7032
- const matches = text.matchAll(/(?:[\w.,\-/@]+\/)*[\w.,\-/@]+\.\w+/g);
7033
- for (const m_ of matches) filePaths.add(m_[0]);
7034
7093
  }
7035
- const parts = [`${toSummarize.length} turns (${userTurns} user, ${assistantTurns} assistant)`];
7036
- if (toolNames.size > 0) parts.push(`tools: ${[...toolNames].join(", ")}`);
7037
- if (filePaths.size > 0) parts.push(`files: ${[...filePaths].slice(0, 10).join(", ")}`);
7038
- summaryText = parts.join(" | ");
7039
- if (!summaryText) summaryText = `${toSummarize.length} earlier turns omitted`;
7094
+ summaryText = preservedMessages.map((m) => `[${m.role}]: ${typeof m.content === "string" ? m.content : m.content.filter(isTextBlock).map((b) => b.text).join(" ")}`).join("\n");
7095
+ if (!summaryText) summaryText = `${toSummarize.length} earlier turns (semantic content preserved)`;
7040
7096
  }
7041
7097
  const summaryMsg = {
7042
7098
  role: "system",
@@ -7282,7 +7338,7 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
7282
7338
  const res = await this.provider.complete(req, { signal: ac.signal });
7283
7339
  const textBlocks = res.content.filter(isTextBlock);
7284
7340
  raw = textBlocks.map((b) => b.text).join("\n").trim();
7285
- } catch (err) {
7341
+ } catch (_err) {
7286
7342
  return this.fallbackSelect(messages, effectiveBudget);
7287
7343
  }
7288
7344
  return this.parseSelectorOutput(raw, messages.length);
@@ -7374,15 +7430,24 @@ var SelectiveCompactor = class {
7374
7430
  }
7375
7431
  async compact(ctx, opts = {}) {
7376
7432
  const beforeTokens = this.estimateTokens(ctx.messages);
7433
+ const beforeFull = this.estimateFullRequest(ctx);
7377
7434
  const reductions = [];
7378
- const load = beforeTokens / this.maxContext;
7435
+ const load = beforeFull / this.maxContext;
7379
7436
  const shouldCompact = load >= this.warnThreshold || opts.aggressive;
7380
7437
  if (!shouldCompact) {
7381
7438
  const saved = this.eliseOldToolResults(ctx);
7382
7439
  if (saved > 0) reductions.push({ phase: "elision", saved });
7383
7440
  const repair2 = this.repairProtocolAdjacency(ctx);
7384
7441
  const afterTokens2 = this.estimateTokens(ctx.messages);
7385
- return { before: beforeTokens, after: afterTokens2, reductions, repaired: repair2 };
7442
+ const afterFull2 = this.estimateFullRequest(ctx);
7443
+ return {
7444
+ before: beforeTokens,
7445
+ after: afterTokens2,
7446
+ fullRequestTokensBefore: beforeFull,
7447
+ fullRequestTokensAfter: afterFull2,
7448
+ reductions,
7449
+ repaired: repair2
7450
+ };
7386
7451
  }
7387
7452
  const savedElision = this.eliseOldToolResults(ctx);
7388
7453
  if (savedElision > 0) reductions.push({ phase: "elision", saved: savedElision });
@@ -7394,7 +7459,23 @@ var SelectiveCompactor = class {
7394
7459
  }
7395
7460
  const repair = this.repairProtocolAdjacency(ctx);
7396
7461
  const afterTokens = this.estimateTokens(ctx.messages);
7397
- return { before: beforeTokens, after: afterTokens, reductions, repaired: repair };
7462
+ const afterFull = this.estimateFullRequest(ctx);
7463
+ return {
7464
+ before: beforeTokens,
7465
+ after: afterTokens,
7466
+ fullRequestTokensBefore: beforeFull,
7467
+ fullRequestTokensAfter: afterFull,
7468
+ reductions,
7469
+ repaired: repair
7470
+ };
7471
+ }
7472
+ /**
7473
+ * Estimate the full API request token count: messages + systemPrompt + toolDefs.
7474
+ * This is the accurate figure for context-window pressure monitoring.
7475
+ */
7476
+ estimateFullRequest(ctx) {
7477
+ const breakdown = estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []);
7478
+ return breakdown.total;
7398
7479
  }
7399
7480
  repairProtocolAdjacency(ctx) {
7400
7481
  const repaired = repairToolUseAdjacency(ctx.messages);
@@ -7584,7 +7665,8 @@ var LEVEL_RANK2 = { warn: 0, soft: 1, hard: 2 };
7584
7665
  var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7585
7666
  name = "AutoCompaction";
7586
7667
  compactor;
7587
- estimator;
7668
+ /** Deprecated. Kept for backward compat with tests that pass simpleEstimator. */
7669
+ _estimator;
7588
7670
  warnThreshold;
7589
7671
  softThreshold;
7590
7672
  hardThreshold;
@@ -7594,19 +7676,6 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7594
7676
  events;
7595
7677
  failureMode;
7596
7678
  policyProvider;
7597
- /**
7598
- * Calibration factor applied to the estimator output. The estimator is now
7599
- * expected to already include messages + system prompt + tool definitions
7600
- * (see `estimateRequestTokens.total`), so no overhead boost is needed. Kept
7601
- * as a constant so a future calibration against real provider tokenization
7602
- * (BPE vs the chars/3.5 rough estimator) can dial this without touching the
7603
- * threshold-check math.
7604
- *
7605
- * Historical note: was 1.3 when the estimator only counted messages — that
7606
- * double-counted system+tools once the estimator was upgraded, firing
7607
- * compaction ~30% earlier than intended (e.g. real 56% load showed as 73%).
7608
- */
7609
- static OVERHEAD_FACTOR = 1;
7610
7679
  /**
7611
7680
  * Once a compaction attempt reduces nothing (preserveK protects everything,
7612
7681
  * no oversized tool_results remain to elide), retrying on every iteration
@@ -7619,8 +7688,11 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7619
7688
  lastNoopAttempt = null;
7620
7689
  /**
7621
7690
  * @param compactor Compactor to use for compaction.
7622
- * @param maxContext Provider's max context window in tokens.
7623
- * @param estimator Token estimation function.
7691
+ * @param maxContext Provider's max context window in tokens.
7692
+ * @param _estimator Deprecated parameter kept for backward compatibility.
7693
+ * The middleware now uses `estimateRequestTokens` internally
7694
+ * for accurate full-request token counting (messages +
7695
+ * systemPrompt + toolDefs).
7624
7696
  * @param thresholds Threshold fractions (0-1) of maxContext.
7625
7697
  * @param opts Optional behavior. By default, failures at the
7626
7698
  * hard threshold throw AGENT_CONTEXT_OVERFLOW so
@@ -7628,11 +7700,11 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7628
7700
  * provider context overflow. Warn/soft failures
7629
7701
  * still emit compaction.failed and continue.
7630
7702
  */
7631
- constructor(compactor, maxContext, estimator, thresholds, optsOrAggressiveOn = {}, events) {
7703
+ constructor(compactor, maxContext, _estimator, thresholds, optsOrAggressiveOn = {}, events) {
7632
7704
  const opts = typeof optsOrAggressiveOn === "string" ? { aggressiveOn: optsOrAggressiveOn, events } : optsOrAggressiveOn;
7633
7705
  this.compactor = compactor;
7634
7706
  this._maxContext = maxContext;
7635
- this.estimator = estimator;
7707
+ this._estimator = _estimator;
7636
7708
  this.warnThreshold = thresholds.warn;
7637
7709
  this.softThreshold = thresholds.soft;
7638
7710
  this.hardThreshold = thresholds.hard;
@@ -7648,8 +7720,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7648
7720
  }
7649
7721
  handler() {
7650
7722
  return async (ctx, next) => {
7651
- const rawTokens = this.estimator(ctx);
7652
- const tokens = Math.ceil(rawTokens * _AutoCompactionMiddleware.OVERHEAD_FACTOR);
7723
+ const tokens = this._estimator ? this._estimator(ctx) : estimateRequestTokens(ctx.messages, ctx.systemPrompt, ctx.tools ?? []).total;
7653
7724
  const load = tokens / this._maxContext;
7654
7725
  const policy = this.policyProvider?.(ctx);
7655
7726
  const thresholds = policy?.thresholds ?? {
@@ -7684,7 +7755,7 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
7684
7755
  return delta < _AutoCompactionMiddleware.NOOP_RETRY_DELTA_TOKENS;
7685
7756
  }
7686
7757
  recordAttempt(level, tokens, report) {
7687
- const reduced = report.before > report.after;
7758
+ const reduced = (report.fullRequestTokensBefore ?? report.before) > (report.fullRequestTokensAfter ?? report.after);
7688
7759
  const repaired = !!report.repaired;
7689
7760
  if (reduced || repaired) {
7690
7761
  this.lastNoopAttempt = null;
@@ -8617,17 +8688,7 @@ var SubagentBudget = class _SubagentBudget {
8617
8688
  startTime = null;
8618
8689
  _onThreshold;
8619
8690
  /**
8620
- * Tracks which budget kinds currently have an extension request
8621
- * in flight. While a kind is here, further `checkLimit` calls for the
8622
- * same kind are no-ops — without this dedup, every `recordIteration`
8623
- * after the limit is reached spawns a fresh decision Promise (until
8624
- * the first one lands and patches limits), flooding the FleetBus
8625
- * with redundant threshold events. Cleared in `negotiateExtension`'s
8626
- * `finally`.
8627
- */
8628
- pendingExtensions = /* @__PURE__ */ new Set();
8629
- /**
8630
- * Hard cap on how long `negotiateExtension` waits for the coordinator to
8691
+ * Hard cap on how long `_negotiateExtension` waits for the coordinator to
8631
8692
  * respond before defaulting to 'stop'. Without this fallback an absent
8632
8693
  * or hung listener (Director not built / event filter detached mid-run)
8633
8694
  * leaves the budget over-limit and never enforces anything.
@@ -8689,38 +8750,83 @@ var SubagentBudget = class _SubagentBudget {
8689
8750
  * - `mode === 'auto'` + no listener → throw `BudgetExceededError` (hard stop; no one to ask)
8690
8751
  * - `mode === 'auto'` + listener → throw `BudgetThresholdSignal` with async decision promise
8691
8752
  */
8692
- checkLimit(kind, used, limit) {
8753
+ /**
8754
+ * Collects all exceeded budget kinds into a single NOOP-free negotiation.
8755
+ * Called by recordIteration / recordToolCall / recordUsage — each may call
8756
+ * this for its own kind. The first call starts the negotiation and stores
8757
+ * the Promise in _pendingNegotiation. Subsequent calls for DIFFERENT
8758
+ * kinds (while a negotiation is in flight) are NOOPs — they don't start
8759
+ * new conversations with the coordinator. This prevents an EventBus flood
8760
+ * when multiple budget kinds are exceeded simultaneously in one iteration.
8761
+ *
8762
+ * Returns the kinds that were found to be exceeded (for logging/debugging).
8763
+ */
8764
+ checkLimits(elapsedMs) {
8765
+ const exceeded = [];
8766
+ if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
8767
+ exceeded.push({ kind: "iterations", used: this.iterations, limit: this.limits.maxIterations });
8768
+ }
8769
+ if (this.limits.maxToolCalls !== void 0 && this.toolCalls > this.limits.maxToolCalls) {
8770
+ exceeded.push({ kind: "tool_calls", used: this.toolCalls, limit: this.limits.maxToolCalls });
8771
+ }
8772
+ const totalTokens = this.tokenInput + this.tokenOutput;
8773
+ if (this.limits.maxTokens !== void 0 && totalTokens > this.limits.maxTokens) {
8774
+ exceeded.push({ kind: "tokens", used: totalTokens, limit: this.limits.maxTokens });
8775
+ }
8776
+ if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
8777
+ exceeded.push({ kind: "cost", used: this.costUsd, limit: this.limits.maxCostUsd });
8778
+ }
8779
+ if (elapsedMs !== void 0 && this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs) {
8780
+ exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
8781
+ }
8782
+ if (exceeded.length === 0) return [];
8693
8783
  if (!this._onThreshold) {
8694
- throw new BudgetExceededError(kind, limit, used);
8784
+ const first2 = exceeded[0];
8785
+ throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
8695
8786
  }
8696
8787
  if (this._mode === "sync") {
8697
- throw new BudgetExceededError(kind, limit, used);
8788
+ const first2 = exceeded[0];
8789
+ throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
8698
8790
  }
8699
8791
  const bus = this._events;
8700
8792
  if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
8701
- throw new BudgetExceededError(kind, limit, used);
8793
+ const first2 = exceeded[0];
8794
+ throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
8795
+ }
8796
+ for (const entry of exceeded) {
8797
+ if (this._pendingNegotiations.has(entry.kind)) continue;
8798
+ const decision2 = this._negotiateExtension(entry.kind, exceeded);
8799
+ this._pendingNegotiations.set(entry.kind, decision2);
8702
8800
  }
8703
- if (this.pendingExtensions.has(kind)) return;
8704
- this.pendingExtensions.add(kind);
8705
- const decision = this.negotiateExtension(kind, used, limit);
8706
- throw new BudgetThresholdSignal(kind, limit, used, decision);
8801
+ const first = exceeded[0];
8802
+ const decision = this._pendingNegotiations.get(first.kind);
8803
+ throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
8707
8804
  }
8805
+ /**
8806
+ * Per-kind in-flight negotiation Promises. Each budget kind can have its
8807
+ * own concurrent negotiation — e.g. iterations and timeout can both
8808
+ * be exceeded simultaneously without blocking each other. The same kind
8809
+ * cannot start two concurrent negotiations (dedup guard).
8810
+ * Cleared in `_negotiateExtension`'s `finally` block.
8811
+ */
8812
+ _pendingNegotiations = /* @__PURE__ */ new Map();
8708
8813
  /**
8709
8814
  * Drive the threshold handler to a decision. Resolves with `'stop'`
8710
8815
  * (signal the runner to abort) or `{ extend: ... }` (limits already
8711
- * patched in-place; the runner should not abort). Always releases the
8712
- * `pendingExtensions` slot in `finally`.
8816
+ * patched in-place; the runner should not abort). Clears the
8817
+ * per-kind slot in `_pendingNegotiations` in `finally`.
8713
8818
  *
8714
8819
  * The 'continue' return from a sync handler is treated as
8715
8820
  * `{ extend: {} }` — keep going without patching; next overrun fires
8716
8821
  * a fresh signal.
8717
8822
  */
8718
- async negotiateExtension(kind, used, limit) {
8823
+ async _negotiateExtension(kind, exceeded) {
8719
8824
  try {
8825
+ const first = exceeded[0];
8720
8826
  const result = this._onThreshold({
8721
- kind,
8722
- used,
8723
- limit,
8827
+ kind: first.kind,
8828
+ used: first.used,
8829
+ limit: first.limit,
8724
8830
  requestDecision: () => {
8725
8831
  const bus = this._events;
8726
8832
  if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
@@ -8737,20 +8843,22 @@ var SubagentBudget = class _SubagentBudget {
8737
8843
  () => respond("stop"),
8738
8844
  _SubagentBudget.DECISION_TIMEOUT_MS
8739
8845
  );
8740
- bus.emit("budget.threshold_reached", {
8741
- kind,
8742
- used,
8743
- limit,
8744
- timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
8745
- extend: (extra) => {
8746
- clearTimeout(fallback);
8747
- respond({ extend: extra });
8748
- },
8749
- deny: () => {
8750
- clearTimeout(fallback);
8751
- respond("stop");
8752
- }
8753
- });
8846
+ for (const { kind: kind2, used, limit } of exceeded) {
8847
+ bus.emit("budget.threshold_reached", {
8848
+ kind: kind2,
8849
+ used,
8850
+ limit,
8851
+ timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
8852
+ extend: (extra) => {
8853
+ clearTimeout(fallback);
8854
+ respond({ extend: extra });
8855
+ },
8856
+ deny: () => {
8857
+ clearTimeout(fallback);
8858
+ respond("stop");
8859
+ }
8860
+ });
8861
+ }
8754
8862
  });
8755
8863
  }
8756
8864
  });
@@ -8776,47 +8884,39 @@ var SubagentBudget = class _SubagentBudget {
8776
8884
  }
8777
8885
  return decision;
8778
8886
  } finally {
8779
- this.pendingExtensions.delete(kind);
8887
+ this._pendingNegotiations.delete(kind);
8780
8888
  }
8781
8889
  }
8782
8890
  recordIteration() {
8783
8891
  this.iterations++;
8784
- if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
8785
- void this.checkLimit("iterations", this.iterations, this.limits.maxIterations);
8786
- }
8892
+ void this.checkLimits();
8787
8893
  }
8788
8894
  recordToolCall() {
8789
8895
  this.toolCalls++;
8790
- if (this.limits.maxToolCalls !== void 0 && this.toolCalls > this.limits.maxToolCalls) {
8791
- void this.checkLimit("tool_calls", this.toolCalls, this.limits.maxToolCalls);
8792
- }
8896
+ void this.checkLimits();
8793
8897
  }
8794
8898
  recordUsage(usage, costUsd = 0) {
8795
8899
  this.tokenInput += usage.input;
8796
8900
  this.tokenOutput += usage.output;
8797
8901
  this.costUsd += costUsd;
8798
- const totalTokens = this.tokenInput + this.tokenOutput;
8799
- if (this.limits.maxTokens !== void 0 && totalTokens > this.limits.maxTokens) {
8800
- void this.checkLimit("tokens", totalTokens, this.limits.maxTokens);
8801
- }
8802
- if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
8803
- void this.checkLimit("cost", this.costUsd, this.limits.maxCostUsd);
8804
- }
8902
+ void this.checkLimits();
8805
8903
  }
8806
8904
  /**
8807
- * Wall-clock budget check. Unlike other limits, timeout check passes through
8808
- * `checkLimit` and is subject to the same negotiation-mode decision table.
8809
- * In practice, `'sync'` mode (the usual test configuration) means a timeout
8810
- * immediately throws `BudgetExceededError`. In production with a coordinator,
8811
- * a timeout emits `budget.threshold_reached` so the Director can decide whether
8812
- * to extend or abort.
8905
+ * Wall-clock budget check. Unlike other limits, timeout is always a hard stop
8906
+ * wall-clock time cannot be "extended" by the coordinator, so it throws
8907
+ * synchronously rather than entering the negotiation flow.
8908
+ *
8909
+ * Decision table:
8910
+ * - no `onThreshold` handler → throw `BudgetExceededError`
8911
+ * - `mode === 'sync'` → throw `BudgetExceededError`
8912
+ * - `mode === 'auto'` + no listener → throw `BudgetExceededError`
8913
+ * - `mode === 'auto'` + listener → throw `BudgetExceededError` (timeout is not extendable)
8813
8914
  */
8814
8915
  checkTimeout() {
8815
8916
  if (this.startTime === null || this.limits.timeoutMs === void 0) return;
8816
8917
  const elapsed = Date.now() - this.startTime;
8817
- if (elapsed > this.limits.timeoutMs) {
8818
- void this.checkLimit("timeout", elapsed, this.limits.timeoutMs);
8819
- }
8918
+ if (elapsed <= this.limits.timeoutMs) return;
8919
+ void this.checkLimits(elapsed);
8820
8920
  }
8821
8921
  /** Returns true if a timeout has occurred without throwing. Useful for races. */
8822
8922
  isTimedOut() {
@@ -11604,6 +11704,107 @@ var FLEET_ROSTER_WITHACP = {
11604
11704
  ...Object.fromEntries(ACP_AGENTS.map((a) => [a.role, a]))
11605
11705
  };
11606
11706
 
11707
+ // src/coordination/subagent-nicknames.ts
11708
+ var NICKNAME_POOL = {
11709
+ // Physics & fundamental sciences
11710
+ "einstein": { name: "Einstein", domain: "physics" },
11711
+ "newton": { name: "Newton", domain: "physics" },
11712
+ "feynman": { name: "Feynman", domain: "physics" },
11713
+ "dirac": { name: "Dirac", domain: "physics" },
11714
+ "bohr": { name: "Bohr", domain: "physics" },
11715
+ "planck": { name: "Planck", domain: "physics" },
11716
+ "curie": { name: "Curie", domain: "physics" },
11717
+ "fermi": { name: "Fermi", domain: "physics" },
11718
+ "heisenberg": { name: "Heisenberg", domain: "physics" },
11719
+ "schrodinger": { name: "Schr\xF6dinger", domain: "physics" },
11720
+ // Mathematics
11721
+ "euclid": { name: "Euclid", domain: "math" },
11722
+ "gauss": { name: "Gauss", domain: "math" },
11723
+ "turing": { name: "Turing", domain: "math" },
11724
+ "poincare": { name: "Poincar\xE9", domain: "math" },
11725
+ "riemann": { name: "Riemann", domain: "math" },
11726
+ "hilbert": { name: "Hilbert", domain: "math" },
11727
+ "pythagoras": { name: "Pythagoras", domain: "math" },
11728
+ // Computing & information theory
11729
+ "von-neumann": { name: "Von Neumann", domain: "computing" },
11730
+ "shannon": { name: "Shannon", domain: "computing" },
11731
+ "hopper": { name: "Hopper", domain: "computing" },
11732
+ "backus": { name: "Backus", domain: "computing" },
11733
+ "knuth": { name: "Knuth", domain: "computing" },
11734
+ "torvalds": { name: "Torvalds", domain: "computing" },
11735
+ "stallman": { name: "Stallman", domain: "computing" },
11736
+ "berners-lee": { name: "Berners-Lee", domain: "computing" },
11737
+ "babbage": { name: "Babbage", domain: "computing" },
11738
+ "lovelace": { name: "Lovelace", domain: "computing" },
11739
+ "klein": { name: "Klein", domain: "computing" },
11740
+ // Electronics & electrical engineering
11741
+ "edison": { name: "Edison", domain: "ee" },
11742
+ "tesla": { name: "Tesla", domain: "ee" },
11743
+ "faraday": { name: "Faraday", domain: "ee" },
11744
+ "maxwell": { name: "Maxwell", domain: "ee" },
11745
+ "ohm": { name: "Ohm", domain: "ee" },
11746
+ "bell": { name: "Bell", domain: "ee" },
11747
+ "marconi": { name: "Marconi", domain: "ee" },
11748
+ "lamarr": { name: "Lamarr", domain: "ee" },
11749
+ // General science / multi-disciplinary
11750
+ "darwin": { name: "Darwin", domain: "biology" },
11751
+ "mendel": { name: "Mendel", domain: "biology" },
11752
+ "pasteur": { name: "Pasteur", domain: "biology" },
11753
+ "hawking": { name: "Hawking", domain: "cosmology" },
11754
+ "sagan": { name: "Sagan", domain: "cosmology" },
11755
+ // Chemistry / materials
11756
+ "lavoisier": { name: "Lavoisier", domain: "chemistry" },
11757
+ "mendeleev": { name: "Mendeleev", domain: "chemistry" }
11758
+ };
11759
+ var ALL_NICKNAMES = Object.values(NICKNAME_POOL);
11760
+ var DOMAIN_PREFERENCES = {
11761
+ "security": ["shannon", "turing", "lamarr", "stallman"],
11762
+ "bug-hunter": ["darwin", "curie", "feynman", "fermi"],
11763
+ "refactor": ["gauss", "hilbert", "euclid", "planck"],
11764
+ "audit-log": ["sagan", "hawking", "poincare", "newton"],
11765
+ "planner": ["hilbert", "gauss", "turing", "euclid"],
11766
+ "researcher": ["sagan", "hawking", "darwin", "pasteur"],
11767
+ "explorer": ["marconi", "bell", "columbus", "polo"],
11768
+ "testing": ["pasteur", "curie", "fermi", "bohr"],
11769
+ "frontend": ["lovelace", "hopper", "babbage", "backus"],
11770
+ "backend": ["torvalds", "stallman", "von-neumann", "backus"],
11771
+ "database": ["turing", "shannon", "backus", "knuth"],
11772
+ "devops": ["tesla", "edison", "faraday", "bell"],
11773
+ "security-scanner": ["shannon", "turing", "lamarr", "stallman"],
11774
+ "refactor-planner": ["gauss", "hilbert", "planck", "newton"],
11775
+ "architect": ["von-neumann", "turing", "gauss", "hilbert"],
11776
+ "critic": ["einstein", "feynman", "dirac", "bohr"],
11777
+ "e2e": ["hopper", "bell", "marconi", "tesla"],
11778
+ "performance": ["knuth", "gauss", "planck", "feynman"],
11779
+ "chaos": ["tesla", "edison", "curie", "fermi"],
11780
+ "cost": ["ohm", "bell", "marconi", "tesla"],
11781
+ // default fallback
11782
+ "default": ["einstein", "newton", "curie", "tesla", "edison", "turing", "shannon", "hopper", "knuth", "stallman"]
11783
+ };
11784
+ function assignNickname(role, used) {
11785
+ const preferences = [
11786
+ ...DOMAIN_PREFERENCES[role] ?? [],
11787
+ ...DOMAIN_PREFERENCES["default"] ?? []
11788
+ ];
11789
+ for (const key of preferences) {
11790
+ const entry = NICKNAME_POOL[key];
11791
+ if (entry && !used.has(key)) {
11792
+ return `${entry.name} (${formatRole(role)})`;
11793
+ }
11794
+ }
11795
+ for (const entry of ALL_NICKNAMES) {
11796
+ const key = Object.entries(NICKNAME_POOL).find(([, v]) => v.name === entry.name)?.[0];
11797
+ if (key && !used.has(key)) {
11798
+ return `${entry.name} (${formatRole(role)})`;
11799
+ }
11800
+ }
11801
+ const counter = used.size + 1;
11802
+ return `Scientist #${counter} (${formatRole(role)})`;
11803
+ }
11804
+ function formatRole(role) {
11805
+ return role.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
11806
+ }
11807
+
11607
11808
  // src/coordination/multi-agent-coordinator.ts
11608
11809
  var DefaultMultiAgentCoordinator = class extends EventEmitter {
11609
11810
  coordinatorId;
@@ -11611,6 +11812,15 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
11611
11812
  runner;
11612
11813
  fleetBus;
11613
11814
  subagents = /* @__PURE__ */ new Map();
11815
+ /**
11816
+ * Base nickname keys already handed out this run (e.g. `einstein`, `tesla`).
11817
+ * Prevents two workers sharing a name. Direct `coordinator.spawn()` callers
11818
+ * (parallel/eternal engine, SDD parallel run) don't go through
11819
+ * `Director.spawn()` where nicknames are normally assigned, so the
11820
+ * coordinator upgrades placeholder names ("Executor", "slot-ab12cd", role
11821
+ * names) to memorable ones here — that's what surfaces in the fleet monitor.
11822
+ */
11823
+ usedNicknames = /* @__PURE__ */ new Set();
11614
11824
  pendingTasks = [];
11615
11825
  completedResults = [];
11616
11826
  totalIterations = 0;
@@ -11659,7 +11869,26 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
11659
11869
  this.config.maxConcurrent = Math.floor(n);
11660
11870
  this.tryDispatchNext();
11661
11871
  }
11872
+ /**
11873
+ * Upgrade a placeholder/role-derived name to a memorable scientist nickname
11874
+ * (e.g. "Einstein (Executor)"). A name is treated as a placeholder when it is
11875
+ * empty, equals the role (case-insensitive), is a generic default
11876
+ * ("subagent"/"adhoc"/"generic"), or is an auto-generated `slot-…` id.
11877
+ * Explicit, human-chosen names — including nicknames already assigned by
11878
+ * `Director.spawn()` — are left untouched, so this never double-assigns.
11879
+ */
11880
+ withNickname(subagent) {
11881
+ const role = subagent.role ?? "subagent";
11882
+ const name = subagent.name?.trim() ?? "";
11883
+ const isPlaceholder = name === "" || name.toLowerCase() === role.toLowerCase() || name === "subagent" || name === "adhoc" || name === "generic" || /^slot-/.test(name);
11884
+ if (!isPlaceholder) return subagent;
11885
+ const nickname = assignNickname(role, this.usedNicknames);
11886
+ const baseKey = nickname.split(" ")[0].toLowerCase().replace(/[^a-z0-9-]/g, "-");
11887
+ this.usedNicknames.add(baseKey);
11888
+ return { ...subagent, name: nickname };
11889
+ }
11662
11890
  async spawn(subagent) {
11891
+ subagent = this.withNickname(subagent);
11663
11892
  const id = subagent.id || randomUUID();
11664
11893
  if (this.subagents.has(id)) {
11665
11894
  throw new Error(`Subagent id "${id}" already exists \u2014 refusing to overwrite`);
@@ -12253,12 +12482,12 @@ var GOAL_COMPLETE_MARKER2 = /^\s*\[goal[_\s-]?complete\]\s*$/im;
12253
12482
  var ParallelEternalEngine = class {
12254
12483
  constructor(opts) {
12255
12484
  this.opts = opts;
12256
- this.goalPath = goalFilePath(opts.projectRoot);
12485
+ this.goalPath = opts.goalPath ?? goalFilePath(opts.projectRoot);
12257
12486
  this.slots = Math.min(16, Math.max(1, opts.parallelSlots ?? 4));
12258
12487
  this.timeoutMs = opts.iterationTimeoutMs ?? 3e5;
12259
12488
  this.dispatchEnabled = opts.dispatch !== false;
12260
12489
  this.dispatchClassifier = opts.dispatchClassifier;
12261
- this.agentFactory = opts.subagentFactory ?? (async (config) => ({
12490
+ this.agentFactory = opts.subagentFactory ?? (async (_config) => ({
12262
12491
  agent: this.opts.agent,
12263
12492
  events: this.opts.agent.events
12264
12493
  }));
@@ -12352,7 +12581,6 @@ var ParallelEternalEngine = class {
12352
12581
  }
12353
12582
  const tasks = await this.decomposeGoal(goal);
12354
12583
  if (!tasks || tasks.length === 0) {
12355
- await sleep2(5e3);
12356
12584
  return false;
12357
12585
  }
12358
12586
  const fanOut = await this.fanOut(goal, tasks);
@@ -12373,6 +12601,7 @@ var ParallelEternalEngine = class {
12373
12601
  });
12374
12602
  if (fanOut.goalComplete) {
12375
12603
  this.stopRequested = true;
12604
+ this.state = "stopped";
12376
12605
  return true;
12377
12606
  }
12378
12607
  await this.maybeCompact();
@@ -12553,7 +12782,11 @@ ${lastFew}` : "No prior iterations.",
12553
12782
  } finally {
12554
12783
  clearTimeout(timer);
12555
12784
  }
12556
- } catch {
12785
+ } catch (err) {
12786
+ this.opts.onError?.(
12787
+ err instanceof Error ? err : new Error(String(err)),
12788
+ this.consecutiveFailures
12789
+ );
12557
12790
  return [];
12558
12791
  }
12559
12792
  }
@@ -12770,7 +13003,11 @@ Working rules:
12770
13003
  thrashing, terminate it rather than letting cost climb silently.
12771
13004
  6. Never claim a subagent's work as your own without verifying it. If a
12772
13005
  result looks wrong, ask_subagent for clarification before passing it
12773
- to the user.`;
13006
+ to the user.
13007
+ 7. Wind down when satisfied. When the results are good enough, call
13008
+ work_complete \u2014 no new subagents will spawn and queued tasks complete
13009
+ as aborted. Running subagents finish naturally. Call terminate_subagent
13010
+ only for ones you need to stop immediately.`;
12774
13011
  var DEFAULT_SUBAGENT_BASELINE = `You are a subagent operating under a Director. You were spawned to handle
12775
13012
  a specific slice of a larger plan \u2014 do that slice well and report back.
12776
13013
 
@@ -12915,14 +13152,12 @@ var FleetBus = class {
12915
13152
  };
12916
13153
  var FleetUsageAggregator = class {
12917
13154
  constructor(bus, priceLookup, metaLookup) {
12918
- this.bus = bus;
12919
13155
  this.priceLookup = priceLookup;
12920
13156
  this.metaLookup = metaLookup;
12921
13157
  bus.filter("provider.response", (e) => this.onProviderResponse(e));
12922
13158
  bus.filter("tool.executed", (e) => this.onToolExecuted(e));
12923
13159
  bus.filter("iteration.started", (e) => this.onIterationStarted(e));
12924
13160
  }
12925
- bus;
12926
13161
  priceLookup;
12927
13162
  metaLookup;
12928
13163
  perSubagent = /* @__PURE__ */ new Map();
@@ -12990,106 +13225,6 @@ var FleetUsageAggregator = class {
12990
13225
  snap.lastEventAt = e.ts;
12991
13226
  }
12992
13227
  };
12993
-
12994
- // src/coordination/subagent-nicknames.ts
12995
- var NICKNAME_POOL = {
12996
- // Physics & fundamental sciences
12997
- "einstein": { name: "Einstein", domain: "physics" },
12998
- "newton": { name: "Newton", domain: "physics" },
12999
- "feynman": { name: "Feynman", domain: "physics" },
13000
- "dirac": { name: "Dirac", domain: "physics" },
13001
- "bohr": { name: "Bohr", domain: "physics" },
13002
- "planck": { name: "Planck", domain: "physics" },
13003
- "curie": { name: "Curie", domain: "physics" },
13004
- "fermi": { name: "Fermi", domain: "physics" },
13005
- "heisenberg": { name: "Heisenberg", domain: "physics" },
13006
- "schrodinger": { name: "Schr\xF6dinger", domain: "physics" },
13007
- // Mathematics
13008
- "euclid": { name: "Euclid", domain: "math" },
13009
- "gauss": { name: "Gauss", domain: "math" },
13010
- "turing": { name: "Turing", domain: "math" },
13011
- "poincare": { name: "Poincar\xE9", domain: "math" },
13012
- "riemann": { name: "Riemann", domain: "math" },
13013
- "hilbert": { name: "Hilbert", domain: "math" },
13014
- "pythagoras": { name: "Pythagoras", domain: "math" },
13015
- // Computing & information theory
13016
- "von-neumann": { name: "Von Neumann", domain: "computing" },
13017
- "shannon": { name: "Shannon", domain: "computing" },
13018
- "hopper": { name: "Hopper", domain: "computing" },
13019
- "backus": { name: "Backus", domain: "computing" },
13020
- "knuth": { name: "Knuth", domain: "computing" },
13021
- "torvalds": { name: "Torvalds", domain: "computing" },
13022
- "stallman": { name: "Stallman", domain: "computing" },
13023
- "berners-lee": { name: "Berners-Lee", domain: "computing" },
13024
- "babbage": { name: "Babbage", domain: "computing" },
13025
- "lovelace": { name: "Lovelace", domain: "computing" },
13026
- "klein": { name: "Klein", domain: "computing" },
13027
- // Electronics & electrical engineering
13028
- "edison": { name: "Edison", domain: "ee" },
13029
- "tesla": { name: "Tesla", domain: "ee" },
13030
- "faraday": { name: "Faraday", domain: "ee" },
13031
- "maxwell": { name: "Maxwell", domain: "ee" },
13032
- "ohm": { name: "Ohm", domain: "ee" },
13033
- "bell": { name: "Bell", domain: "ee" },
13034
- "marconi": { name: "Marconi", domain: "ee" },
13035
- "lamarr": { name: "Lamarr", domain: "ee" },
13036
- // General science / multi-disciplinary
13037
- "darwin": { name: "Darwin", domain: "biology" },
13038
- "mendel": { name: "Mendel", domain: "biology" },
13039
- "pasteur": { name: "Pasteur", domain: "biology" },
13040
- "hawking": { name: "Hawking", domain: "cosmology" },
13041
- "sagan": { name: "Sagan", domain: "cosmology" },
13042
- // Chemistry / materials
13043
- "lavoisier": { name: "Lavoisier", domain: "chemistry" },
13044
- "mendeleev": { name: "Mendeleev", domain: "chemistry" }
13045
- };
13046
- var ALL_NICKNAMES = Object.values(NICKNAME_POOL);
13047
- var DOMAIN_PREFERENCES = {
13048
- "security": ["shannon", "turing", "lamarr", "stallman"],
13049
- "bug-hunter": ["darwin", "curie", "feynman", "fermi"],
13050
- "refactor": ["gauss", "hilbert", "euclid", "planck"],
13051
- "audit-log": ["sagan", "hawking", "poincare", "newton"],
13052
- "planner": ["hilbert", "gauss", "turing", "euclid"],
13053
- "researcher": ["sagan", "hawking", "darwin", "pasteur"],
13054
- "explorer": ["marconi", "bell", "columbus", "polo"],
13055
- "testing": ["pasteur", "curie", "fermi", "bohr"],
13056
- "frontend": ["lovelace", "hopper", "babbage", "backus"],
13057
- "backend": ["torvalds", "stallman", "von-neumann", "backus"],
13058
- "database": ["turing", "shannon", "backus", "knuth"],
13059
- "devops": ["tesla", "edison", "faraday", "bell"],
13060
- "security-scanner": ["shannon", "turing", "lamarr", "stallman"],
13061
- "refactor-planner": ["gauss", "hilbert", "planck", "newton"],
13062
- "architect": ["von-neumann", "turing", "gauss", "hilbert"],
13063
- "critic": ["einstein", "feynman", "dirac", "bohr"],
13064
- "e2e": ["hopper", "bell", "marconi", "tesla"],
13065
- "performance": ["knuth", "gauss", "planck", "feynman"],
13066
- "chaos": ["tesla", "edison", "curie", "fermi"],
13067
- "cost": ["oh", "bell", "marconi", "tesla"],
13068
- // default fallback
13069
- "default": ["einstein", "newton", "curie", "tesla", "edison", "turing", "shannon", "hopper", "knuth", "stallman"]
13070
- };
13071
- function assignNickname(role, used) {
13072
- const preferences = [
13073
- ...DOMAIN_PREFERENCES[role] ?? [],
13074
- ...DOMAIN_PREFERENCES["default"] ?? []
13075
- ];
13076
- for (const key of preferences) {
13077
- if (!used.has(key)) {
13078
- return `${NICKNAME_POOL[key].name} (${formatRole(role)})`;
13079
- }
13080
- }
13081
- for (const entry of ALL_NICKNAMES) {
13082
- const key = Object.entries(NICKNAME_POOL).find(([, v]) => v.name === entry.name)?.[0];
13083
- if (key && !used.has(key)) {
13084
- return `${entry.name} (${formatRole(role)})`;
13085
- }
13086
- }
13087
- const counter = used.size + 1;
13088
- return `Scientist #${counter} (${formatRole(role)})`;
13089
- }
13090
- function formatRole(role) {
13091
- return role.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
13092
- }
13093
13228
  function makeSpawnTool(director, roster) {
13094
13229
  const inputSchema = {
13095
13230
  type: "object",
@@ -13260,7 +13395,7 @@ function makeRollUpTool(director) {
13260
13395
  function makeTerminateTool(director) {
13261
13396
  return {
13262
13397
  name: "terminate_subagent",
13263
- description: "Forcibly abort a subagent.",
13398
+ description: 'Forcibly abort a subagent. The subagent finishes its current iteration then exits with status "stopped".',
13264
13399
  permission: "auto",
13265
13400
  mutating: true,
13266
13401
  inputSchema: { type: "object", properties: { subagentId: { type: "string", description: "Subagent to abort." } }, required: ["subagentId"] },
@@ -13271,6 +13406,19 @@ function makeTerminateTool(director) {
13271
13406
  }
13272
13407
  };
13273
13408
  }
13409
+ function makeTerminateAllTool(director) {
13410
+ return {
13411
+ name: "terminate_all",
13412
+ 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.',
13413
+ permission: "auto",
13414
+ mutating: true,
13415
+ inputSchema: { type: "object", properties: {}, required: [] },
13416
+ async execute() {
13417
+ await director.terminateAll();
13418
+ return { ok: true, message: `Fleet shutdown complete \u2014 all subagents stopped, pending tasks drained.` };
13419
+ }
13420
+ };
13421
+ }
13274
13422
  function makeFleetStatusTool(director) {
13275
13423
  return {
13276
13424
  name: "fleet_status",
@@ -13413,7 +13561,7 @@ function makeCollabDebugTool(director) {
13413
13561
  function makeFleetEmitTool(director) {
13414
13562
  return {
13415
13563
  name: "fleet_emit",
13416
- 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.",
13564
+ 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.",
13417
13565
  permission: "auto",
13418
13566
  mutating: false,
13419
13567
  inputSchema: {
@@ -13421,14 +13569,14 @@ function makeFleetEmitTool(director) {
13421
13569
  properties: {
13422
13570
  type: {
13423
13571
  type: "string",
13424
- description: "Event type: bug.found | refactor.plan | critic.evaluation"
13572
+ description: "Event type string (e.g. bug.found, refactor.plan, critic.evaluation, progress, result)."
13425
13573
  },
13426
13574
  payload: {
13427
13575
  type: "object",
13428
- description: "Event payload matching the event type."
13576
+ description: "Event payload. Structure depends on event type. Use null if no payload."
13429
13577
  }
13430
13578
  },
13431
- required: ["type", "payload"]
13579
+ required: ["type"]
13432
13580
  },
13433
13581
  async execute(input) {
13434
13582
  const i = input;
@@ -13436,12 +13584,25 @@ function makeFleetEmitTool(director) {
13436
13584
  subagentId: director.id,
13437
13585
  ts: Date.now(),
13438
13586
  type: i.type,
13439
- payload: i.payload
13587
+ payload: i.payload ?? {}
13440
13588
  });
13441
13589
  return { ok: true, event: i.type };
13442
13590
  }
13443
13591
  };
13444
13592
  }
13593
+ function makeWorkCompleteTool(director) {
13594
+ return {
13595
+ name: "work_complete",
13596
+ 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.",
13597
+ permission: "auto",
13598
+ mutating: false,
13599
+ inputSchema: { type: "object", properties: {}, required: [] },
13600
+ async execute() {
13601
+ director.workComplete();
13602
+ return { ok: true, message: "Fleet wind-down signaled. No new spawns or task dispatches." };
13603
+ }
13604
+ };
13605
+ }
13445
13606
  var CollabSession = class extends EventEmitter {
13446
13607
  sessionId;
13447
13608
  options;
@@ -13456,6 +13617,14 @@ var CollabSession = class extends EventEmitter {
13456
13617
  disposers = new Array();
13457
13618
  settled = false;
13458
13619
  timeoutMs;
13620
+ cancelled = false;
13621
+ alerts = [];
13622
+ /** Tracks tool call counts per subagent for progress-based timeout decisions. */
13623
+ progressBySubagent = /* @__PURE__ */ new Map();
13624
+ /** Last tool call count when a timeout warning was handled. */
13625
+ lastTimeoutProgress = /* @__PURE__ */ new Map();
13626
+ /** Session-level timeout timer handle (cleared on cancel or natural completion). */
13627
+ _timeoutTimer;
13459
13628
  constructor(director, fleetBus, options) {
13460
13629
  super();
13461
13630
  this.sessionId = randomUUID();
@@ -13473,11 +13642,23 @@ var CollabSession = class extends EventEmitter {
13473
13642
  };
13474
13643
  }
13475
13644
  }
13645
+ get id() {
13646
+ return this.sessionId;
13647
+ }
13648
+ getSessionAlerts() {
13649
+ return [...this.alerts];
13650
+ }
13651
+ isCancelled() {
13652
+ return this.cancelled;
13653
+ }
13476
13654
  /**
13477
- * Read the target files from disk and populate the snapshot.
13478
- * Call this after construction if you did not provide a prebuiltSnapshot
13479
- * and want the session to operate on real file contents.
13655
+ * Snapshot of role subagentId map. The Director calls coordinator.stop()
13656
+ * for each agent when cancelling the session, using this map to enumerate
13657
+ * all three collab agents.
13480
13658
  */
13659
+ getSubagentIds() {
13660
+ return new Map(this.subagentIds);
13661
+ }
13481
13662
  async buildSnapshot() {
13482
13663
  if (this.snapshot.files.length > 0) return this.snapshot;
13483
13664
  for (const filePath of this.options.targetPaths) {
@@ -13493,9 +13674,30 @@ var CollabSession = class extends EventEmitter {
13493
13674
  return this.snapshot;
13494
13675
  }
13495
13676
  /**
13496
- * Start the collaborative session: snapshot files, spawn all three agents,
13497
- * wire up event routing, and wait for completion.
13677
+ * Cancel the session. Emits director.cancel_collab on the FleetBus so all
13678
+ * collab agents finish early. The session-level timeout timer is also cleared.
13679
+ * Safe to call multiple times (idempotent after first call).
13498
13680
  */
13681
+ cancel(reason = "Director cancelled collab session") {
13682
+ if (this.settled) return;
13683
+ this.cancelled = true;
13684
+ if (this._timeoutTimer) {
13685
+ clearTimeout(this._timeoutTimer);
13686
+ this._timeoutTimer = void 0;
13687
+ }
13688
+ this.fleetBus.emit({
13689
+ subagentId: this.director.id,
13690
+ ts: Date.now(),
13691
+ type: "director.cancel_collab",
13692
+ payload: { sessionId: this.sessionId, reason, cancelledAt: (/* @__PURE__ */ new Date()).toISOString() }
13693
+ });
13694
+ this.fleetBus.emit({
13695
+ subagentId: this.director.id,
13696
+ ts: Date.now(),
13697
+ type: "collab.cancelled",
13698
+ payload: { sessionId: this.sessionId, reason }
13699
+ });
13700
+ }
13499
13701
  async start() {
13500
13702
  if (this.settled) throw new Error("session already settled");
13501
13703
  this.settled = true;
@@ -13510,10 +13712,10 @@ var CollabSession = class extends EventEmitter {
13510
13712
  this.subagentIds.set("refactor-planner", refactorPlannerId);
13511
13713
  this.subagentIds.set("critic", criticId);
13512
13714
  const timeout = new Promise((_, reject) => {
13513
- setTimeout(
13514
- () => reject(new Error(`CollabSession timed out after ${this.timeoutMs}ms`)),
13515
- this.timeoutMs
13516
- );
13715
+ this._timeoutTimer = setTimeout(() => {
13716
+ this.cancel("Session-level timeout reached");
13717
+ reject(new Error(`CollabSession timed out after ${this.timeoutMs}ms`));
13718
+ }, this.timeoutMs);
13517
13719
  });
13518
13720
  let results;
13519
13721
  try {
@@ -13526,6 +13728,10 @@ var CollabSession = class extends EventEmitter {
13526
13728
  timeout
13527
13729
  ]);
13528
13730
  } catch (err) {
13731
+ if (this._timeoutTimer) {
13732
+ clearTimeout(this._timeoutTimer);
13733
+ this._timeoutTimer = void 0;
13734
+ }
13529
13735
  this.cleanup();
13530
13736
  const error = err instanceof Error ? err : new Error(String(err));
13531
13737
  this.emit("session.error", error);
@@ -13539,17 +13745,6 @@ var CollabSession = class extends EventEmitter {
13539
13745
  this.emit("session.done", report);
13540
13746
  return report;
13541
13747
  }
13542
- /**
13543
- * Parse a completed agent's final text output and route the structured
13544
- * findings onto the FleetBus. Agents are instructed to emit one JSON object
13545
- * per finding / plan / evaluation; we scan the output for balanced JSON
13546
- * objects (tolerating prose, markdown, and ```json fences around them) and
13547
- * dispatch each by its discriminating key. The wired FleetBus listeners then
13548
- * collect them into the bugs / plans / evaluations maps.
13549
- *
13550
- * Failed or empty results are ignored, and a single malformed object never
13551
- * aborts the collation pass — see {@link extractJsonObjects}.
13552
- */
13553
13748
  async parseAndEmit(result) {
13554
13749
  if (result.status !== "success" || result.result == null) return;
13555
13750
  const text = typeof result.result === "string" ? result.result : JSON.stringify(result.result);
@@ -13565,12 +13760,6 @@ var CollabSession = class extends EventEmitter {
13565
13760
  });
13566
13761
  }
13567
13762
  }
13568
- /**
13569
- * Extract every top-level JSON object embedded in free-form agent text.
13570
- * Scans for balanced braces while respecting string literals and escape
13571
- * sequences, so prose, markdown headers, and code fences around the JSON
13572
- * are skipped. Malformed spans are dropped silently rather than thrown.
13573
- */
13574
13763
  extractJsonObjects(text) {
13575
13764
  const objects = [];
13576
13765
  let depth = 0;
@@ -13607,25 +13796,33 @@ var CollabSession = class extends EventEmitter {
13607
13796
  }
13608
13797
  return objects;
13609
13798
  }
13799
+ budgetForRole(role) {
13800
+ if (this.options.budgetOverrides?.[role]) {
13801
+ return this.options.budgetOverrides[role];
13802
+ }
13803
+ const defaults = {
13804
+ "bug-hunter": { maxIterations: 2e3, maxToolCalls: 5e3, timeoutMs: 10 * 60 * 1e3 },
13805
+ "refactor-planner": { maxIterations: 1500, maxToolCalls: 4e3, timeoutMs: 8 * 60 * 1e3 },
13806
+ "critic": { maxIterations: 1e3, maxToolCalls: 3e3, timeoutMs: 6 * 60 * 1e3 }
13807
+ };
13808
+ return defaults[role] ?? { maxIterations: 1500, maxToolCalls: 4e3, timeoutMs: 8 * 60 * 1e3 };
13809
+ }
13610
13810
  async spawnAgent(role, taskBrief) {
13811
+ const budget = this.budgetForRole(role);
13611
13812
  const cfg = {
13612
13813
  id: `${role}-${this.sessionId}`,
13613
13814
  name: role,
13614
- role
13615
- // No fleet_emit tool agents output structured JSON, the Director parses
13616
- // results and emits FleetBus events. This keeps the agent simple and the
13617
- // routing deterministic.
13815
+ role,
13816
+ tools: ["fleet_emit", "fleet_status", "read", "grep", "glob", "bash", "write"],
13817
+ maxIterations: budget.maxIterations,
13818
+ maxToolCalls: budget.maxToolCalls,
13819
+ timeoutMs: budget.timeoutMs
13618
13820
  };
13619
13821
  const subagentId = await this.director.spawn(cfg);
13620
- await this.director.assign({
13621
- id: randomUUID(),
13622
- subagentId,
13623
- description: taskBrief
13624
- });
13822
+ await this.director.assign({ id: randomUUID(), subagentId, description: taskBrief });
13625
13823
  return subagentId;
13626
13824
  }
13627
13825
  buildBugHunterTask() {
13628
- this.options.targetPaths.join(", ");
13629
13826
  const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
13630
13827
  const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
13631
13828
  ${f.content}`).join("\n\n");
@@ -13634,13 +13831,17 @@ ${f.content}`).join("\n\n");
13634
13831
  Target files:
13635
13832
  ${fileContents}
13636
13833
 
13637
- For each bug found, write ONE valid JSON line to the scratchpad with this exact structure:
13638
- { "finding": { "id": "<uuid>", "type": "<pattern>", "severity": "<critical|high|medium|low>", "location": { "file": "<path>", "line": <n> }, "description": "<explain>", "suggestedFix": "<optional>" } }.
13639
- After scanning all files, write your full markdown bug report to the shared scratchpad at:
13640
- ${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
13834
+ For each bug found, emit it using the fleet_emit tool immediately:
13835
+ { "type": "bug.found", "payload": { "finding": { "id": "<uuid>", "type": "<pattern>", "severity": "<critical|high|medium|low>", "location": { "file": "<path>", "line": <n> }, "description": "<explain>", "suggestedFix": "<optional>" } } }
13836
+
13837
+ After scanning all files, write your full markdown bug report to:
13838
+ ${scratchpad}/bug-hunter-report-${this.sessionId}.md
13839
+
13840
+ Important: emit each finding as soon as you find it. Do not batch or wait until the end.`;
13641
13841
  }
13642
13842
  buildRefactorPlannerTask() {
13643
13843
  const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
13844
+ const bugHunterReportPath = `${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
13644
13845
  const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
13645
13846
  ${f.content}`).join("\n\n");
13646
13847
  return `You are RefactorPlanner. Plan refactorings for the following files.
@@ -13648,26 +13849,146 @@ ${f.content}`).join("\n\n");
13648
13849
  Target files:
13649
13850
  ${fileContents}
13650
13851
 
13651
- 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:
13652
- { "plan": { "id": "<uuid>", "basedOnBugIds": ["<bug-id>"], "phases": [...], "riskScore": "<low|medium|high>", "estimatedChangeCount": <n>, "rollbackStrategy": "<text>" } }.
13653
- After planning, write your full markdown plan to:
13654
- ${scratchpad}/refactor-plan-${this.sessionId}.md`;
13852
+ Read the BugHunter report at: ${bugHunterReportPath}
13853
+
13854
+ For each bug you can address, emit a refactor plan using fleet_emit:
13855
+ { "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>" } } }
13856
+
13857
+ Also write your full markdown plan to:
13858
+ ${scratchpad}/refactor-plan-${this.sessionId}.md
13859
+
13860
+ Emit each plan immediately. Do not wait until planning is complete.`;
13655
13861
  }
13656
13862
  buildCriticTask() {
13657
13863
  const scratchpad = this.director.sharedScratchpadPath ?? "/tmp";
13864
+ const bugHunterReportPath = `${scratchpad}/bug-hunter-report-${this.sessionId}.md`;
13865
+ const refactorPlanPath = `${scratchpad}/refactor-plan-${this.sessionId}.md`;
13658
13866
  const fileContents = this.snapshot.files.map((f) => `=== ${f.path} ===
13659
13867
  ${f.content}`).join("\n\n");
13660
- return `You are Critic. Evaluate bug findings and refactor plans as they arrive via fleet bus events.
13868
+ return `You are Critic. Evaluate bug findings and refactor plans.
13661
13869
 
13662
13870
  Target files:
13663
13871
  ${fileContents}
13664
13872
 
13665
- Subscribe to bug.found events (from BugHunter) and refactor.plan events (from RefactorPlanner). For each subject, write one JSON line per evaluation:
13666
- { "evaluation": { "id": "<uuid>", "subjectType": "<bug_finding|refactor_plan>", "subjectId": "<id>", "score": <0-10>, "verdict": "<approve|needs_revision|reject>", "strengths": [...], "weaknesses": [...], "concerns": [...] } }.
13667
- After all evaluations, write your markdown critic report to:
13668
- ${scratchpad}/critic-report-${this.sessionId}.md`;
13873
+ Read the BugHunter report at: ${bugHunterReportPath}
13874
+ Read the RefactorPlanner report at: ${refactorPlanPath}
13875
+
13876
+ For each bug and refactor plan, emit your evaluation using fleet_emit:
13877
+ { "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>" }] } } }
13878
+
13879
+ After all evaluations, write your markdown report to:
13880
+ ${scratchpad}/critic-report-${this.sessionId}.md
13881
+
13882
+ Emit each evaluation immediately. Do not wait until you have read all reports.`;
13669
13883
  }
13670
13884
  wireFleetBus() {
13885
+ const dTool = this.fleetBus.filter("tool.executed", (e) => {
13886
+ this.progressBySubagent.set(e.subagentId, (this.progressBySubagent.get(e.subagentId) ?? 0) + 1);
13887
+ });
13888
+ this.disposers.push(dTool);
13889
+ const dBudget = this.fleetBus.filter("budget.threshold_reached", (e) => {
13890
+ const payload = e.payload;
13891
+ const role = this.roleFromSubagentId(e.subagentId);
13892
+ if (!role) return;
13893
+ const btwNotes = this.director.getLeaderBtwNotes();
13894
+ const alert = {
13895
+ sessionId: this.sessionId,
13896
+ subagentId: e.subagentId,
13897
+ role,
13898
+ level: "warning" /* WARNING */,
13899
+ message: `${role} hit ${payload.kind} soft limit (${payload.used}/${payload.limit})`,
13900
+ budgetKind: payload.kind,
13901
+ elapsedMs: payload.timeoutMs,
13902
+ limit: payload.limit,
13903
+ btwNotes
13904
+ };
13905
+ this.alerts.push(alert);
13906
+ this.fleetBus.emit({
13907
+ subagentId: e.subagentId,
13908
+ ts: Date.now(),
13909
+ type: "collab.warning",
13910
+ payload: alert
13911
+ });
13912
+ const decision = this.options.onBudgetWarning?.(alert) ?? "ignore";
13913
+ if (decision === "cancel") {
13914
+ this.cancel(`Director cancelled: ${role} ${payload.kind} threshold`);
13915
+ return;
13916
+ }
13917
+ if (payload.kind === "timeout") {
13918
+ const progress = this.progressBySubagent.get(e.subagentId) ?? 0;
13919
+ const lastProgress = this.lastTimeoutProgress.get(e.subagentId) ?? -1;
13920
+ if (progress <= lastProgress) {
13921
+ payload.deny();
13922
+ return;
13923
+ }
13924
+ this.lastTimeoutProgress.set(e.subagentId, progress);
13925
+ const newLimit = Math.min(Math.ceil((payload.timeoutMs ?? payload.limit) * 2), 24 * 60 * 6e4);
13926
+ setImmediate(() => {
13927
+ payload.extend({ timeoutMs: newLimit });
13928
+ });
13929
+ return;
13930
+ }
13931
+ if (decision === "extend") {
13932
+ setImmediate(() => {
13933
+ const base = Math.max(payload.limit, payload.used);
13934
+ const extra = {};
13935
+ switch (payload.kind) {
13936
+ case "iterations":
13937
+ extra.maxIterations = Math.min(Math.ceil(base * 1.5), 5e4);
13938
+ break;
13939
+ case "tool_calls":
13940
+ extra.maxToolCalls = Math.min(Math.ceil(base * 1.5), 1e5);
13941
+ break;
13942
+ case "tokens":
13943
+ extra.maxTokens = Math.min(Math.ceil(base * 1.5), 5e6);
13944
+ break;
13945
+ case "cost":
13946
+ extra.maxCostUsd = Math.min(base * 1.5, 100);
13947
+ break;
13948
+ }
13949
+ payload.extend(extra);
13950
+ });
13951
+ return;
13952
+ }
13953
+ if (payload.kind !== "timeout") {
13954
+ setImmediate(() => {
13955
+ const base = Math.max(payload.limit, payload.used);
13956
+ const extra = {};
13957
+ switch (payload.kind) {
13958
+ case "iterations":
13959
+ extra.maxIterations = Math.min(Math.ceil(base * 1.25), 5e4);
13960
+ break;
13961
+ case "tool_calls":
13962
+ extra.maxToolCalls = Math.min(Math.ceil(base * 1.25), 1e5);
13963
+ break;
13964
+ case "tokens":
13965
+ extra.maxTokens = Math.min(Math.ceil(base * 1.25), 5e6);
13966
+ break;
13967
+ case "cost":
13968
+ extra.maxCostUsd = Math.min(base * 1.25, 100);
13969
+ break;
13970
+ }
13971
+ payload.extend(extra);
13972
+ });
13973
+ }
13974
+ });
13975
+ this.disposers.push(dBudget);
13976
+ const dCancel = this.fleetBus.filter("director.cancel_collab", (e) => {
13977
+ const payload = e.payload;
13978
+ if (payload.sessionId !== this.sessionId) return;
13979
+ this.cancelled = true;
13980
+ if (this._timeoutTimer) {
13981
+ clearTimeout(this._timeoutTimer);
13982
+ this._timeoutTimer = void 0;
13983
+ }
13984
+ this.fleetBus.emit({
13985
+ subagentId: this.director.id,
13986
+ ts: Date.now(),
13987
+ type: "collab.cancelled",
13988
+ payload: { sessionId: this.sessionId, reason: payload.reason }
13989
+ });
13990
+ });
13991
+ this.disposers.push(dCancel);
13671
13992
  const d1 = this.fleetBus.filter("bug.found", (e) => {
13672
13993
  const payload = e.payload;
13673
13994
  if (payload?.finding) {
@@ -13693,10 +14014,19 @@ ${scratchpad}/critic-report-${this.sessionId}.md`;
13693
14014
  });
13694
14015
  this.disposers.push(d3);
13695
14016
  }
14017
+ roleFromSubagentId(subagentId) {
14018
+ for (const [role, id] of this.subagentIds) {
14019
+ if (id === subagentId) return role;
14020
+ }
14021
+ const match = subagentId.match(/^(bug-hunter|refactor-planner|critic)/);
14022
+ return match?.[1] ?? null;
14023
+ }
13696
14024
  assembleReport() {
13697
14025
  const bugList = Array.from(this.bugs.values());
13698
14026
  const planList = Array.from(this.plans.values());
13699
14027
  const evalList = Array.from(this.evaluations.values());
14028
+ let disposition = "completed";
14029
+ if (this.cancelled) disposition = "cancelled";
13700
14030
  const verdictOrder = {
13701
14031
  approve: 0,
13702
14032
  needs_revision: 1,
@@ -13710,33 +14040,41 @@ ${scratchpad}/critic-report-${this.sessionId}.md`;
13710
14040
  },
13711
14041
  "approve"
13712
14042
  );
13713
- const summary = this.buildMarkdownSummary(bugList, planList, evalList, overallVerdict);
14043
+ const summary = this.buildMarkdownSummary(bugList, planList, evalList, overallVerdict, disposition);
13714
14044
  return {
13715
14045
  sessionId: this.sessionId,
13716
14046
  startedAt: this.snapshot.createdAt,
13717
14047
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
13718
14048
  targetPaths: this.options.targetPaths,
14049
+ disposition,
13719
14050
  bugs: bugList,
13720
14051
  refactorPlans: planList,
13721
14052
  evaluations: evalList,
14053
+ alerts: [...this.alerts],
13722
14054
  overallVerdict,
13723
14055
  summary
13724
14056
  };
13725
14057
  }
13726
- buildMarkdownSummary(bugs, plans, evals, overallVerdict) {
14058
+ buildMarkdownSummary(bugs, plans, evals, overallVerdict, disposition) {
13727
14059
  const lines = [
13728
14060
  `## Collaborative Debugging Report \u2014 ${this.sessionId}`,
13729
14061
  "",
13730
14062
  `**Target:** ${this.options.targetPaths.join(", ")}`,
14063
+ `**Disposition:** ${disposition.toUpperCase()}`,
13731
14064
  `**Overall Verdict:** **${overallVerdict.toUpperCase()}**`,
13732
14065
  ""
13733
14066
  ];
14067
+ if (this.alerts.length > 0) {
14068
+ lines.push("### Alerts", "");
14069
+ for (const alert of this.alerts) {
14070
+ lines.push(`- **[${alert.level.toUpperCase()}]** ${alert.role}: ${alert.message}`);
14071
+ }
14072
+ lines.push("");
14073
+ }
13734
14074
  if (bugs.length > 0) {
13735
14075
  lines.push("### Bugs Found", "");
13736
14076
  for (const b of bugs) {
13737
- lines.push(
13738
- `- **[${b.severity.toUpperCase()}]** \`${b.location.file}:${b.location.line}\` \u2014 ${b.description}`
13739
- );
14077
+ lines.push(`- **[${b.severity.toUpperCase()}]** \`${b.location.file}:${b.location.line}\` \u2014 ${b.description}`);
13740
14078
  }
13741
14079
  lines.push("");
13742
14080
  }
@@ -13755,9 +14093,7 @@ ${scratchpad}/critic-report-${this.sessionId}.md`;
13755
14093
  for (const e of evals) {
13756
14094
  lines.push(`- [${e.subjectType}] score=${e.score}/10 \u2014 **${e.verdict.toUpperCase()}**`);
13757
14095
  for (const c of e.concerns) {
13758
- if (c.severity === "blocking") {
13759
- lines.push(` - ${c.description}`);
13760
- }
14096
+ if (c.severity === "blocking") lines.push(` - ${c.description}`);
13761
14097
  }
13762
14098
  }
13763
14099
  lines.push("");
@@ -13775,10 +14111,9 @@ var FleetSpawnBudgetError = class extends Error {
13775
14111
  kind;
13776
14112
  limit;
13777
14113
  observed;
13778
- constructor(kind, limit, observed) {
13779
- super(
13780
- 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}`
13781
- );
14114
+ constructor(kind, limit, observed, message) {
14115
+ 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}`;
14116
+ super(message ?? defaultMsg);
13782
14117
  this.name = "FleetSpawnBudgetError";
13783
14118
  this.kind = kind;
13784
14119
  this.limit = limit;
@@ -13799,6 +14134,20 @@ var FleetCostCapError = class extends Error {
13799
14134
  this.observed = observed;
13800
14135
  }
13801
14136
  };
14137
+ var FleetContextOverflowError = class extends Error {
14138
+ kind;
14139
+ limit;
14140
+ observed;
14141
+ constructor(limit, observed) {
14142
+ super(
14143
+ `Leader context overflow: leader has ${observed} tokens in flight (limit: ${limit}). Compact the leader context before spawning more subagents.`
14144
+ );
14145
+ this.name = "FleetContextOverflowError";
14146
+ this.kind = "max_context_load";
14147
+ this.limit = limit;
14148
+ this.observed = observed;
14149
+ }
14150
+ };
13802
14151
  var Director = class {
13803
14152
  /** Alias for the ICoordinator contract. `id` is retained for backward compatibility. */
13804
14153
  get coordinatorId() {
@@ -13815,6 +14164,25 @@ var Director = class {
13815
14164
  * injected; otherwise own FleetUsageAggregator.
13816
14165
  */
13817
14166
  usage;
14167
+ /**
14168
+ * Update the leader agent's current context pressure (full request tokens:
14169
+ * messages + systemPrompt + toolDefs). The director checks this before every
14170
+ * spawn — if the pressure exceeds `maxLeaderContextLoad * maxContext`,
14171
+ * spawning is refused with a `FleetContextOverflowError`.
14172
+ *
14173
+ * Call this after each leader agent iteration to keep the pressure current.
14174
+ * The compactor's `CompactReport.fullRequestTokensAfter` is a natural source.
14175
+ */
14176
+ setLeaderContextPressure(tokens) {
14177
+ this.leaderContextPressure = tokens;
14178
+ this.fleetManager?.setLeaderContextPressure(tokens);
14179
+ }
14180
+ /**
14181
+ * Read the leader agent's current context pressure.
14182
+ */
14183
+ getLeaderContextPressure() {
14184
+ return this.leaderContextPressure;
14185
+ }
13818
14186
  /**
13819
14187
  * Optional fleet-level policy container. When provided the Director
13820
14188
  * delegates spawn budgeting, manifest entries, and checkpointing to it
@@ -13900,6 +14268,23 @@ var Director = class {
13900
14268
  taskCompletedListener = null;
13901
14269
  /** Optional LLM classifier for smart dispatch. Passed from options. */
13902
14270
  dispatchClassifier;
14271
+ /** Leader agent's current context pressure (full request tokens). */
14272
+ leaderContextPressure = 0;
14273
+ /** Maximum context load fraction before spawn is refused. */
14274
+ maxLeaderContextLoad;
14275
+ /** Provider's max context window in tokens. */
14276
+ maxContext;
14277
+ /**
14278
+ * When set by `workComplete()`, the director stops dispatching new tasks
14279
+ * and terminates all running subagents. Used when the director's LLM decides
14280
+ * the goal is satisfied and no further spawns are needed — prevents the
14281
+ * coordinator from keeping workers alive for tasks that will never arrive.
14282
+ */
14283
+ workCompleteFlag = false;
14284
+ /** Pending /btw notes stashed by the leader agent (see setLeaderBtwNote). */
14285
+ _leaderBtwNotes = [];
14286
+ /** Active collab sessions tracked by sessionId (see spawnCollab). */
14287
+ _activeCollabSessions = /* @__PURE__ */ new Map();
13903
14288
  constructor(opts) {
13904
14289
  this.id = opts.config.coordinatorId || randomUUID();
13905
14290
  this.manifestPath = opts.manifestPath;
@@ -13915,6 +14300,8 @@ var Director = class {
13915
14300
  this.dispatchClassifier = opts.dispatchClassifier;
13916
14301
  this.maxFleetCostUsd = opts.directorBudget?.maxCostUsd ?? Number.POSITIVE_INFINITY;
13917
14302
  this.maxBudgetExtensions = opts.maxBudgetExtensions ?? 5;
14303
+ this.maxLeaderContextLoad = opts.maxLeaderContextLoad ?? 0.85;
14304
+ this.maxContext = opts.maxContext ?? 128e3;
13918
14305
  this.sessionsRoot = opts.sessionsRoot;
13919
14306
  this.directorRunId = opts.directorRunId ?? this.id;
13920
14307
  this.stateCheckpoint = opts.stateCheckpointPath ? new DirectorStateCheckpoint(opts.stateCheckpointPath, {
@@ -13942,7 +14329,7 @@ var Director = class {
13942
14329
  this.fleet = new FleetBus();
13943
14330
  this.usage = new FleetUsageAggregator(
13944
14331
  this.fleet,
13945
- (id, provider, model) => {
14332
+ (_id, provider, model) => {
13946
14333
  if (provider && model) return this.priceLookups.get(`${provider}/${model}`);
13947
14334
  return void 0;
13948
14335
  },
@@ -14004,6 +14391,9 @@ var Director = class {
14004
14391
  });
14005
14392
  this.fleet.filter("budget.threshold_reached", (e) => {
14006
14393
  const payload = e.payload;
14394
+ if (e.subagentId.startsWith("bug-hunter-") || e.subagentId.startsWith("refactor-planner-") || e.subagentId.startsWith("critic-")) {
14395
+ return;
14396
+ }
14007
14397
  if (payload.kind === "timeout") {
14008
14398
  const progress = progressBySubagent.get(e.subagentId) ?? 0;
14009
14399
  const lastProgress = lastTimeoutProgress.get(e.subagentId) ?? -1;
@@ -14082,6 +14472,107 @@ var Director = class {
14082
14472
  extensionsFor(subagentId) {
14083
14473
  return this.extendTotals.get(subagentId) ?? 0;
14084
14474
  }
14475
+ /**
14476
+ * Signal that the director's work is done. Once called:
14477
+ * - `spawn()` throws `FleetSpawnBudgetError('max_spawns', …)` — no new
14478
+ * subagents can be created
14479
+ * - Running subagents are NOT forcibly stopped — they finish naturally,
14480
+ * but no new tasks are dispatched to them
14481
+ *
14482
+ * This lets the director LLM say "I'm satisfied with the results, stop
14483
+ * spawning and wind down" — without killing in-flight work mid-execution.
14484
+ * Call `terminateAll()` separately if you need immediate teardown.
14485
+ *
14486
+ * Idempotent — calling twice is a no-op.
14487
+ */
14488
+ workComplete() {
14489
+ this.workCompleteFlag = true;
14490
+ this.fleet.emit({ subagentId: this.id, ts: Date.now(), type: "director.work_complete", payload: {} });
14491
+ }
14492
+ /** Returns true if `workComplete()` has been called on this director. */
14493
+ isWorkComplete() {
14494
+ return this.workCompleteFlag;
14495
+ }
14496
+ /**
14497
+ * Stashes a /btw note on the leader agent's context. The leader's agent loop
14498
+ * calls `consumeBtwNotes()` at each iteration boundary and folds pending notes
14499
+ * into the conversation as a visible block — no abort, no restart, just a
14500
+ * "by the way" nudge the model picks up on its next turn.
14501
+ *
14502
+ * This is the entry point for the host (CLI, TUI) to inject /btw notes
14503
+ * programmatically without going through the slash-command path.
14504
+ */
14505
+ setLeaderBtwNote(note) {
14506
+ const trimmed = note.trim();
14507
+ if (!trimmed) return this._leaderBtwNotes.length;
14508
+ this._leaderBtwNotes = [...this._leaderBtwNotes, trimmed].slice(-20);
14509
+ return this._leaderBtwNotes.length;
14510
+ }
14511
+ /**
14512
+ * Read and clear all pending /btw notes the leader has stashed.
14513
+ * Returns them in FIFO order (empty array when none).
14514
+ *
14515
+ * Called by CollabSession when a budget threshold event fires so the
14516
+ * Director can inspect accumulated /btw notes before deciding whether
14517
+ * to cancel the collab session or let it continue.
14518
+ */
14519
+ getLeaderBtwNotes() {
14520
+ const notes = this._leaderBtwNotes;
14521
+ this._leaderBtwNotes = [];
14522
+ return notes;
14523
+ }
14524
+ /**
14525
+ * Peek at pending /btw notes without consuming them.
14526
+ * Useful for UI to show "N pending notes" without clearing them.
14527
+ */
14528
+ peekLeaderBtwNotes() {
14529
+ return [...this._leaderBtwNotes];
14530
+ }
14531
+ /**
14532
+ * Drain (read + clear) all /btw notes in one call.
14533
+ * Alias for getLeaderBtwNotes() — kept for symmetry with consumeBtwNotes()
14534
+ * in the agent's btw.ts. The Director calls this at the point where it
14535
+ * makes a cancellation decision, not on every budget event.
14536
+ */
14537
+ drainLeaderBtwNotes() {
14538
+ return this.getLeaderBtwNotes();
14539
+ }
14540
+ /**
14541
+ * Cancel an active collab session by its id.
14542
+ * Emits `director.cancel_collab` on the FleetBus so the session's agents
14543
+ * finish early with a 'cancelled' disposition.
14544
+ *
14545
+ * Returns silently if the session id is not tracked or already settled.
14546
+ * The CollabDebugReport will reflect 'cancelled' disposition when awaited.
14547
+ */
14548
+ cancelCollabSession(sessionId, reason = "Director cancelled") {
14549
+ const session = this._activeCollabSessions.get(sessionId);
14550
+ if (!session || session.isCancelled()) return;
14551
+ session.cancel(reason);
14552
+ for (const [_role, subagentId] of session.getSubagentIds()) {
14553
+ this.coordinator.stop(subagentId).catch(() => {
14554
+ });
14555
+ }
14556
+ }
14557
+ /**
14558
+ * Subscribe a callback to be notified whenever a collab session raises
14559
+ * an alert (warning level). The callback receives the full DirectorAlert
14560
+ * payload so the host (CLI, TUI) can display it to the user.
14561
+ * Returns an unsubscribe function.
14562
+ */
14563
+ onCollabAlert(handler) {
14564
+ return this.fleet.filter("collab.warning", (e) => {
14565
+ handler(e.payload);
14566
+ });
14567
+ }
14568
+ /**
14569
+ * Returns all active (not yet settled) collab session ids.
14570
+ * Useful for the TUI to render a "N active sessions" badge and for
14571
+ * the host to know what can be cancelled.
14572
+ */
14573
+ activeCollabSessions() {
14574
+ return Array.from(this._activeCollabSessions.keys());
14575
+ }
14085
14576
  /** Best-effort session-writer append. Swallows failures — the director
14086
14577
  * must not break a fleet run because the session JSONL handle closed. */
14087
14578
  async appendSessionEvent(event) {
@@ -14120,12 +14611,21 @@ var Director = class {
14120
14611
  * it the `cost` column in `usage.snapshot()` stays at 0.
14121
14612
  */
14122
14613
  async spawn(config, priceLookup) {
14614
+ if (this.workCompleteFlag) {
14615
+ throw new FleetSpawnBudgetError(
14616
+ "max_spawns",
14617
+ this.maxSpawns,
14618
+ this.spawnCount + 1,
14619
+ "workComplete() has been called \u2014 director closed further spawning"
14620
+ );
14621
+ }
14123
14622
  if (this.fleetManager) {
14124
14623
  const rejection = this.fleetManager.canSpawn(config);
14125
14624
  if (rejection) {
14126
14625
  if (rejection.kind === "max_spawn_depth") throw new FleetSpawnBudgetError("max_spawn_depth", rejection.limit, rejection.observed);
14127
14626
  if (rejection.kind === "max_spawns") throw new FleetSpawnBudgetError("max_spawns", rejection.limit, rejection.observed);
14128
14627
  if (rejection.kind === "max_cost_usd") throw new FleetCostCapError(rejection.limit, rejection.observed);
14628
+ if (rejection.kind === "max_context_load") throw new FleetContextOverflowError(rejection.limit, rejection.observed);
14129
14629
  }
14130
14630
  } else {
14131
14631
  if (this.spawnDepth >= this.maxSpawnDepth) {
@@ -14140,9 +14640,15 @@ var Director = class {
14140
14640
  throw new FleetCostCapError(this.maxFleetCostUsd, totalCost);
14141
14641
  }
14142
14642
  }
14643
+ if (this.maxLeaderContextLoad < 1) {
14644
+ const threshold = this.maxContext * this.maxLeaderContextLoad;
14645
+ if (this.leaderContextPressure >= threshold) {
14646
+ throw new FleetContextOverflowError(threshold, this.leaderContextPressure);
14647
+ }
14648
+ }
14143
14649
  }
14144
14650
  let result;
14145
- const needsNickname = config.name === config.role || !config.name || config.name === "subagent";
14651
+ const needsNickname = config.name === config.role || !config.name || config.name === "subagent" || config.name === "adhoc";
14146
14652
  if (needsNickname) {
14147
14653
  const role = config.role ?? "subagent";
14148
14654
  if (this.fleetManager) {
@@ -14367,6 +14873,24 @@ var Director = class {
14367
14873
  */
14368
14874
  async assign(task) {
14369
14875
  const taskWithId = task.id ? task : { ...task, id: randomUUID() };
14876
+ if (this.workCompleteFlag) {
14877
+ const synthetic = {
14878
+ subagentId: taskWithId.subagentId ?? "unassigned",
14879
+ taskId: taskWithId.id,
14880
+ status: "stopped",
14881
+ error: { kind: "aborted_by_parent", message: "Director called workComplete() \u2014 no further tasks will run", retryable: false },
14882
+ iterations: 0,
14883
+ toolCalls: 0,
14884
+ durationMs: 0
14885
+ };
14886
+ this.completed.set(taskWithId.id, synthetic);
14887
+ const waiter = this.taskWaiters.get(taskWithId.id);
14888
+ if (waiter) {
14889
+ waiter.resolve(synthetic);
14890
+ this.taskWaiters.delete(taskWithId.id);
14891
+ }
14892
+ return taskWithId.id;
14893
+ }
14370
14894
  if (task.subagentId) {
14371
14895
  if (this.fleetManager) {
14372
14896
  this.fleetManager.addTaskToSubagent(task.subagentId, taskWithId.id);
@@ -14589,12 +15113,14 @@ var Director = class {
14589
15113
  makeAskTool(this),
14590
15114
  makeRollUpTool(this),
14591
15115
  makeTerminateTool(this),
15116
+ makeTerminateAllTool(this),
14592
15117
  makeFleetStatusTool(this),
14593
15118
  makeFleetUsageTool(this),
14594
15119
  makeFleetSessionTool(this),
14595
15120
  makeFleetHealthTool(this),
14596
15121
  makeCollabDebugTool(this),
14597
- makeFleetEmitTool(this)
15122
+ makeFleetEmitTool(this),
15123
+ makeWorkCompleteTool(this)
14598
15124
  ];
14599
15125
  return t2;
14600
15126
  }
@@ -14614,7 +15140,15 @@ var Director = class {
14614
15140
  * three agents complete or the session times out.
14615
15141
  */
14616
15142
  async spawnCollab(options) {
14617
- const session = new CollabSession(this, this.fleet, options);
15143
+ const session = new CollabSession(this, this.fleet, {
15144
+ ...options,
15145
+ onBudgetWarning: (alert) => {
15146
+ return options.onBudgetWarning?.(alert) ?? "ignore";
15147
+ }
15148
+ });
15149
+ this._activeCollabSessions.set(session.sessionId, session);
15150
+ session.on("session.done", () => this._activeCollabSessions.delete(session.sessionId));
15151
+ session.on("session.error", () => this._activeCollabSessions.delete(session.sessionId));
14618
15152
  return session.start();
14619
15153
  }
14620
15154
  /**
@@ -15104,8 +15638,8 @@ async function loadProjectModes(modesDir) {
15104
15638
  for (const entry of entries) {
15105
15639
  if (!entry.endsWith(".md") && !entry.endsWith(".txt")) continue;
15106
15640
  const filePath = path6.join(modesDir, entry);
15107
- const stat9 = await fsp2.stat(filePath);
15108
- if (!stat9.isFile()) continue;
15641
+ const stat8 = await fsp2.stat(filePath);
15642
+ if (!stat8.isFile()) continue;
15109
15643
  const content = await fsp2.readFile(filePath, "utf8");
15110
15644
  const id = path6.basename(entry, path6.extname(entry));
15111
15645
  modes.push({
@@ -16379,9 +16913,9 @@ var AISpecBuilder = class {
16379
16913
  if (!this.sessionPath) return;
16380
16914
  try {
16381
16915
  const fsp19 = await import('fs/promises');
16382
- const path31 = await import('path');
16916
+ const path35 = await import('path');
16383
16917
  const { atomicWrite: atomicWrite2 } = await Promise.resolve().then(() => (init_atomic_write(), atomic_write_exports));
16384
- await fsp19.mkdir(path31.dirname(this.sessionPath), { recursive: true });
16918
+ await fsp19.mkdir(path35.dirname(this.sessionPath), { recursive: true });
16385
16919
  await atomicWrite2(this.sessionPath, JSON.stringify(this.session, null, 2));
16386
16920
  } catch {
16387
16921
  }
@@ -17033,7 +17567,7 @@ function analyzeCriticalPath(graph) {
17033
17567
  blockedTasks
17034
17568
  };
17035
17569
  }
17036
- function getTransitiveBlocked(graph, taskId, blocksMap) {
17570
+ function getTransitiveBlocked(_graph, taskId, blocksMap) {
17037
17571
  const visited = /* @__PURE__ */ new Set();
17038
17572
  const queue = [taskId];
17039
17573
  while (queue.length > 0) {
@@ -17049,7 +17583,7 @@ function getTransitiveBlocked(graph, taskId, blocksMap) {
17049
17583
  }
17050
17584
  return visited;
17051
17585
  }
17052
- function computeCriticalPath(graph, topoOrder, blockedByMap) {
17586
+ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
17053
17587
  const allIds = Array.from(graph.nodes.keys());
17054
17588
  if (allIds.length === 0) return [];
17055
17589
  const dist = /* @__PURE__ */ new Map();
@@ -17091,15 +17625,15 @@ function computeCriticalPath(graph, topoOrder, blockedByMap) {
17091
17625
  maxId = id;
17092
17626
  }
17093
17627
  }
17094
- const path31 = [];
17628
+ const path35 = [];
17095
17629
  let current = maxId;
17096
17630
  const visited = /* @__PURE__ */ new Set();
17097
17631
  while (current && !visited.has(current)) {
17098
17632
  visited.add(current);
17099
- path31.unshift(current);
17633
+ path35.unshift(current);
17100
17634
  current = prev.get(current) ?? null;
17101
17635
  }
17102
- return path31;
17636
+ return path35;
17103
17637
  }
17104
17638
  function computeParallelGroups(graph, blockedByMap) {
17105
17639
  const groups = [];
@@ -17467,13 +18001,11 @@ function createAutoExecutor(opts) {
17467
18001
 
17468
18002
  // src/sdd/sdd-task-decomposer.ts
17469
18003
  var SddTaskDecomposer = class {
17470
- constructor(tracker, graph, opts = {}) {
18004
+ constructor(tracker, _graph, opts = {}) {
17471
18005
  this.tracker = tracker;
17472
- this.graph = graph;
17473
18006
  this.slots = Math.min(16, Math.max(1, opts.parallelSlots ?? 4));
17474
18007
  }
17475
18008
  tracker;
17476
- graph;
17477
18009
  slots;
17478
18010
  wave = 0;
17479
18011
  // -------------------------------------------------------------------
@@ -17626,7 +18158,7 @@ var SddParallelRun = class {
17626
18158
  this.coordinator.setRunner?.(runner);
17627
18159
  }
17628
18160
  defaultFactory() {
17629
- return async (config) => ({
18161
+ return async (_config) => ({
17630
18162
  agent: this.opts.agent,
17631
18163
  events: this.opts.agent.events
17632
18164
  });
@@ -18079,7 +18611,7 @@ async function startMetricsServer(opts) {
18079
18611
  const tls = opts.tls;
18080
18612
  const useHttps = !!(tls?.cert && tls?.key);
18081
18613
  const host = opts.host ?? "127.0.0.1";
18082
- const path31 = opts.path ?? "/metrics";
18614
+ const path35 = opts.path ?? "/metrics";
18083
18615
  const healthPath = opts.healthPath ?? "/healthz";
18084
18616
  const healthRegistry = opts.healthRegistry;
18085
18617
  const listener = (req, res) => {
@@ -18089,7 +18621,7 @@ async function startMetricsServer(opts) {
18089
18621
  return;
18090
18622
  }
18091
18623
  const url = req.url.split("?")[0];
18092
- if (url === path31) {
18624
+ if (url === path35) {
18093
18625
  let body;
18094
18626
  try {
18095
18627
  body = renderPrometheus(opts.sink.snapshot());
@@ -18153,7 +18685,7 @@ async function startMetricsServer(opts) {
18153
18685
  const protocol = useHttps ? "https" : "http";
18154
18686
  return {
18155
18687
  port: boundPort,
18156
- url: `${protocol}://${host}:${boundPort}${path31}`,
18688
+ url: `${protocol}://${host}:${boundPort}${path35}`,
18157
18689
  close: () => new Promise((resolve9, reject) => {
18158
18690
  server.close((err) => err ? reject(err) : resolve9());
18159
18691
  })
@@ -18440,6 +18972,12 @@ function roughEstimate(messages) {
18440
18972
  return total;
18441
18973
  }
18442
18974
  function createContextManagerTool(opts = {}) {
18975
+ const minCompactThreshold = opts.minCompactThreshold ?? 0;
18976
+ const noopRetryDeltaTokens = opts.noopRetryDeltaTokens ?? 2e3;
18977
+ const maxContext = opts.maxContext ?? 128e3;
18978
+ const compactThresholdFraction = opts.compactThresholdFraction ?? 0.5;
18979
+ const effectiveThreshold = minCompactThreshold > 0 ? minCompactThreshold : Math.floor(maxContext * compactThresholdFraction);
18980
+ let lastNoopTokens = 0;
18443
18981
  return {
18444
18982
  name: CONTEXT_MANAGER_TOOL_NAME,
18445
18983
  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.',
@@ -18530,11 +19068,41 @@ function createContextManagerTool(opts = {}) {
18530
19068
  notes: "No compactor registered. Use /compact aggressive via slash command instead."
18531
19069
  };
18532
19070
  }
19071
+ const fullEstimate = input.systemPrompt != null && Array.isArray(input.tools) ? estimateRequestTokens(messages, input.systemPrompt, input.tools) : { total: beforeTokens};
19072
+ const currentTokens = fullEstimate.total;
19073
+ if (lastNoopTokens > 0) {
19074
+ const delta = currentTokens - lastNoopTokens;
19075
+ if (delta < noopRetryDeltaTokens) {
19076
+ return {
19077
+ action: "compact",
19078
+ beforeTokens,
19079
+ afterTokens: beforeTokens,
19080
+ messageCount: messages.length,
19081
+ notes: `Compact is a NOOP retry: context grew only ${delta} tokens since the last no-op attempt (threshold: ${noopRetryDeltaTokens}). Skip until more content accumulates.`
19082
+ };
19083
+ }
19084
+ }
19085
+ if (currentTokens < effectiveThreshold) {
19086
+ return {
19087
+ action: "compact",
19088
+ beforeTokens,
19089
+ afterTokens: beforeTokens,
19090
+ messageCount: messages.length,
19091
+ notes: `Context tokens (${currentTokens}) below compact threshold (${effectiveThreshold}). Skipping.`
19092
+ };
19093
+ }
18533
19094
  const report = await opts.compactor.compact(ctx);
18534
19095
  ctx.clearFileTracking();
18535
19096
  const repair = applyMessages([...ctx.messages]);
18536
19097
  const afterTokens = repair.changed ? roughEstimate(ctx.messages) : report.after;
18537
19098
  const repaired = report.repaired ?? (repair.changed ? repair : void 0);
19099
+ const reduced = report.fullRequestTokensBefore > report.fullRequestTokensAfter;
19100
+ const repairedSomething = !!report.repaired;
19101
+ if (reduced || repairedSomething) {
19102
+ lastNoopTokens = 0;
19103
+ } else {
19104
+ lastNoopTokens = currentTokens;
19105
+ }
18538
19106
  return {
18539
19107
  action: "compact",
18540
19108
  beforeTokens,
@@ -19000,10 +19568,10 @@ var SkillInstaller = class {
19000
19568
  if (!resolved.startsWith(path6.resolve(destDir))) {
19001
19569
  throw new Error(`Path traversal detected in skill file: ${file}`);
19002
19570
  }
19003
- const stat9 = await fsp2.stat(srcPath);
19004
- if (stat9.size > MAX_SKILL_FILE_SIZE) {
19571
+ const stat8 = await fsp2.stat(srcPath);
19572
+ if (stat8.size > MAX_SKILL_FILE_SIZE) {
19005
19573
  throw new Error(
19006
- `Skill file "${file}" is too large (${(stat9.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`
19574
+ `Skill file "${file}" is too large (${(stat8.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`
19007
19575
  );
19008
19576
  }
19009
19577
  await fsp2.mkdir(path6.dirname(destPath), { recursive: true });
@@ -19042,7 +19610,7 @@ var SkillInstaller = class {
19042
19610
  * - Name: update that specific skill
19043
19611
  * - Name + newRef: update to a different ref
19044
19612
  */
19045
- async update(nameOrRef, opts) {
19613
+ async update(nameOrRef, _opts) {
19046
19614
  const result = { updated: [], unchanged: [], errors: [] };
19047
19615
  const allEntries = await this.manifest.listAll();
19048
19616
  let targets;
@@ -19112,140 +19680,756 @@ var SkillInstaller = class {
19112
19680
  return result;
19113
19681
  }
19114
19682
  /**
19115
- * Uninstall a skill by name.
19683
+ * Uninstall a skill by name.
19684
+ */
19685
+ async uninstall(name, opts) {
19686
+ const scope = opts?.global ? "user" : "project";
19687
+ const entries = await this.manifest.findByName(name);
19688
+ const entry = entries.find((e) => e.scope === scope);
19689
+ if (!entry) {
19690
+ throw new Error(`Skill "${name}" is not installed${scope === "user" ? " (global)" : ""}.`);
19691
+ }
19692
+ await this.removeSkillFiles(name, scope);
19693
+ await this.manifest.removeEntry(name, scope);
19694
+ this.invalidateLoaderCache();
19695
+ }
19696
+ /**
19697
+ * List all installed skills from the manifest.
19698
+ */
19699
+ async listInstalled() {
19700
+ return this.manifest.listAll();
19701
+ }
19702
+ // ── Private helpers ──────────────────────────────────────────────
19703
+ /**
19704
+ * Detect skills in an extracted repository.
19705
+ * Returns an array of detected skills with their files.
19706
+ */
19707
+ async detectSkills(baseDir) {
19708
+ const results = [];
19709
+ const rootSkillMd = path6.join(baseDir, "SKILL.md");
19710
+ try {
19711
+ await fsp2.access(rootSkillMd);
19712
+ const content = await fsp2.readFile(rootSkillMd, "utf8");
19713
+ const meta = parseFrontmatter2(content);
19714
+ if (meta.name && meta.description) {
19715
+ results.push({
19716
+ name: meta.name,
19717
+ baseDir,
19718
+ files: ["SKILL.md"]
19719
+ });
19720
+ return results;
19721
+ }
19722
+ } catch {
19723
+ }
19724
+ const skillsDir = path6.join(baseDir, "skills");
19725
+ try {
19726
+ const entries = await fsp2.readdir(skillsDir, { withFileTypes: true });
19727
+ for (const entry of entries) {
19728
+ if (!entry.isDirectory()) continue;
19729
+ const skillFile = path6.join(skillsDir, entry.name, "SKILL.md");
19730
+ try {
19731
+ const content = await fsp2.readFile(skillFile, "utf8");
19732
+ const meta = parseFrontmatter2(content);
19733
+ if (meta.name && meta.description) {
19734
+ const skillDir = path6.join(skillsDir, entry.name);
19735
+ const files = await collectFiles(skillDir, skillDir);
19736
+ results.push({
19737
+ name: meta.name,
19738
+ baseDir: skillDir,
19739
+ files
19740
+ });
19741
+ }
19742
+ } catch {
19743
+ }
19744
+ }
19745
+ } catch {
19746
+ }
19747
+ return results;
19748
+ }
19749
+ /**
19750
+ * Remove all files for an installed skill.
19751
+ */
19752
+ async removeSkillFiles(name, scope) {
19753
+ const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
19754
+ const skillDir = path6.join(targetDir, name);
19755
+ await fsp2.rm(skillDir, { recursive: true, force: true });
19756
+ }
19757
+ /**
19758
+ * Invalidate the skill loader's cache so newly installed skills appear.
19759
+ */
19760
+ invalidateLoaderCache() {
19761
+ const loader = this.opts.skillLoader;
19762
+ if (loader && typeof loader.invalidateCache === "function") {
19763
+ loader.invalidateCache();
19764
+ }
19765
+ }
19766
+ };
19767
+ function parseFrontmatter2(raw) {
19768
+ if (!raw.startsWith("---")) return {};
19769
+ const end = raw.indexOf("\n---", 4);
19770
+ if (end === -1) return {};
19771
+ const block = raw.slice(4, end);
19772
+ const out = {};
19773
+ let key = null;
19774
+ let value = [];
19775
+ const flush = () => {
19776
+ if (key) {
19777
+ out[key] = value.join("\n").trim();
19778
+ }
19779
+ key = null;
19780
+ value = [];
19781
+ };
19782
+ for (const line of block.split("\n")) {
19783
+ const m = /^([a-zA-Z_]+):\s*(\|?)\s*(.*)$/.exec(line);
19784
+ if (m) {
19785
+ flush();
19786
+ key = m[1] ?? "";
19787
+ const pipe = m[2];
19788
+ const rest = m[3] ?? "";
19789
+ if (pipe === "|") {
19790
+ value = [];
19791
+ } else if (rest) {
19792
+ value = [rest];
19793
+ } else {
19794
+ value = [];
19795
+ }
19796
+ } else if (key) {
19797
+ value.push(line.replace(/^\s+/, ""));
19798
+ }
19799
+ }
19800
+ flush();
19801
+ return out;
19802
+ }
19803
+ async function collectFiles(dir, baseDir) {
19804
+ const results = [];
19805
+ const entries = await fsp2.readdir(dir, { withFileTypes: true });
19806
+ for (const entry of entries) {
19807
+ const fullPath = path6.join(dir, entry.name);
19808
+ const relPath = path6.relative(baseDir, fullPath);
19809
+ if (entry.isDirectory()) {
19810
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
19811
+ results.push(...await collectFiles(fullPath, baseDir));
19812
+ } else {
19813
+ results.push(relPath);
19814
+ }
19815
+ }
19816
+ return results;
19817
+ }
19818
+
19819
+ // src/storage/annotations-store.ts
19820
+ init_atomic_write();
19821
+ var FILE_VERSION = 1;
19822
+ var MAX_TEXT_LENGTH = 2e3;
19823
+ var MAX_ANNOTATIONS = 1e3;
19824
+ var AnnotationsStore = class {
19825
+ dir;
19826
+ /** Per-session write queue. Created lazily on first add. */
19827
+ writeChains = /* @__PURE__ */ new Map();
19828
+ constructor(opts) {
19829
+ this.dir = opts.dir;
19830
+ }
19831
+ // ── Reads ──────────────────────────────────────────────────────────────
19832
+ /**
19833
+ * Return all annotations for `sessionId` in insertion order
19834
+ * (oldest first). Returns an empty array when no file exists
19835
+ * yet (the normal case for a fresh session).
19836
+ */
19837
+ async list(sessionId) {
19838
+ const file = await this.readFile(sessionId);
19839
+ return file ? file.annotations : [];
19840
+ }
19841
+ /**
19842
+ * Convenience: only unresolved annotations, newest first — the
19843
+ * common UI rendering for "what still needs attention?".
19844
+ */
19845
+ async listOpen(sessionId) {
19846
+ const all = await this.list(sessionId);
19847
+ return all.filter((a) => !a.resolved).reverse();
19848
+ }
19849
+ // ── Writes ─────────────────────────────────────────────────────────────
19850
+ /**
19851
+ * Add a new annotation. Returns the persisted record (with id
19852
+ * and timestamps filled in). Throws when `text` is empty or
19853
+ * exceeds `MAX_TEXT_LENGTH`.
19854
+ */
19855
+ async add(input) {
19856
+ const text = input.text.trim();
19857
+ if (text.length === 0) {
19858
+ throw new Error("Annotation text must be non-empty");
19859
+ }
19860
+ if (text.length > MAX_TEXT_LENGTH) {
19861
+ throw new Error(
19862
+ `Annotation text exceeds ${MAX_TEXT_LENGTH} chars (got ${text.length})`
19863
+ );
19864
+ }
19865
+ if (!Number.isInteger(input.atEventIndex) || input.atEventIndex < 0) {
19866
+ throw new Error("atEventIndex must be a non-negative integer");
19867
+ }
19868
+ const annotation = {
19869
+ id: randomUUID(),
19870
+ sessionId: input.sessionId,
19871
+ atEventIndex: input.atEventIndex,
19872
+ authorId: input.authorId,
19873
+ authorRole: "annotator",
19874
+ text,
19875
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
19876
+ resolved: false
19877
+ };
19878
+ await this.enqueue(input.sessionId, async () => {
19879
+ const all = await this.list(input.sessionId);
19880
+ all.push(annotation);
19881
+ if (all.length > MAX_ANNOTATIONS) {
19882
+ const sorted = all.map((a, i) => ({ a, i })).sort((x, y) => {
19883
+ if (x.a.resolved !== y.a.resolved) return x.a.resolved ? 1 : -1;
19884
+ return x.a.createdAt.localeCompare(y.a.createdAt);
19885
+ });
19886
+ const evictCount = all.length - MAX_ANNOTATIONS;
19887
+ const toEvict = new Set(sorted.slice(0, evictCount).map((s) => s.a.id));
19888
+ const kept = all.filter((a) => !toEvict.has(a.id));
19889
+ await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: kept });
19890
+ } else {
19891
+ await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: all });
19892
+ }
19893
+ });
19894
+ return annotation;
19895
+ }
19896
+ /**
19897
+ * Mark an annotation as resolved. Returns the updated record, or
19898
+ * `null` if no annotation with that id exists in this session.
19899
+ * Idempotent: resolving an already-resolved annotation refreshes
19900
+ * `resolvedAt` / `resolvedBy` to the latest call.
19901
+ */
19902
+ async resolve(input) {
19903
+ let updated = null;
19904
+ await this.enqueue(input.sessionId, async () => {
19905
+ const all = await this.list(input.sessionId);
19906
+ const idx = all.findIndex((a) => a.id === input.annotationId);
19907
+ if (idx === -1) {
19908
+ updated = null;
19909
+ return;
19910
+ }
19911
+ const next = {
19912
+ ...all[idx],
19913
+ resolved: true,
19914
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
19915
+ resolvedBy: input.resolvedBy
19916
+ };
19917
+ all[idx] = next;
19918
+ await this.writeFile(input.sessionId, { version: FILE_VERSION, annotations: all });
19919
+ updated = next;
19920
+ });
19921
+ return updated;
19922
+ }
19923
+ // ── Internals ──────────────────────────────────────────────────────────
19924
+ filePath(sessionId) {
19925
+ if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
19926
+ throw new Error(`Invalid sessionId: ${sessionId}`);
19927
+ }
19928
+ return path6.join(this.dir, `${sessionId}.annotations.json`);
19929
+ }
19930
+ async readFile(sessionId) {
19931
+ const fp = this.filePath(sessionId);
19932
+ try {
19933
+ const raw = await fsp2.readFile(fp, "utf8");
19934
+ const parsed = JSON.parse(raw);
19935
+ if (parsed.version !== FILE_VERSION) {
19936
+ return { version: FILE_VERSION, annotations: [] };
19937
+ }
19938
+ return parsed;
19939
+ } catch (err) {
19940
+ if (err.code === "ENOENT") return null;
19941
+ return { version: FILE_VERSION, annotations: [] };
19942
+ }
19943
+ }
19944
+ async writeFile(sessionId, file) {
19945
+ const fp = this.filePath(sessionId);
19946
+ await atomicWrite(fp, JSON.stringify(file, null, 2));
19947
+ }
19948
+ /**
19949
+ * Serialize writes per-sessionId. We chain promises instead of
19950
+ * using a Mutex class so the contract is obvious from the
19951
+ * call-site: `enqueue(sid, fn)` runs `fn` after every prior
19952
+ * enqueue for `sid` has settled.
19953
+ */
19954
+ enqueue(sessionId, fn) {
19955
+ const prev = this.writeChains.get(sessionId) ?? Promise.resolve();
19956
+ const next = prev.then(fn, fn);
19957
+ this.writeChains.set(
19958
+ sessionId,
19959
+ next.catch(() => void 0)
19960
+ );
19961
+ return next;
19962
+ }
19963
+ };
19964
+
19965
+ // src/storage/replay-log-store.ts
19966
+ init_atomic_write();
19967
+ function stableStringify(value) {
19968
+ return JSON.stringify(sortKeys(value));
19969
+ }
19970
+ function sortKeys(value) {
19971
+ if (Array.isArray(value)) return value.map(sortKeys);
19972
+ if (value && typeof value === "object") {
19973
+ const obj = value;
19974
+ const sorted = {};
19975
+ for (const key of Object.keys(obj).sort()) {
19976
+ sorted[key] = sortKeys(obj[key]);
19977
+ }
19978
+ return sorted;
19979
+ }
19980
+ return value;
19981
+ }
19982
+ function hashRequest(request) {
19983
+ const payload = {
19984
+ model: request.model,
19985
+ system: request.system,
19986
+ messages: request.messages,
19987
+ tools: request.tools,
19988
+ maxTokens: request.maxTokens,
19989
+ temperature: request.temperature,
19990
+ topP: request.topP,
19991
+ stopSequences: request.stopSequences,
19992
+ toolChoice: request.toolChoice
19993
+ };
19994
+ const json = stableStringify(payload);
19995
+ const digest = createHash("sha256").update(json, "utf8").digest("hex");
19996
+ return `sha256:${digest}`;
19997
+ }
19998
+ var DEFAULT_MAX_ENTRIES = 1e3;
19999
+ var ReplayLogStore = class {
20000
+ dir;
20001
+ writeChains = /* @__PURE__ */ new Map();
20002
+ /** Per-session hash → entry index, kept in memory after the first load. */
20003
+ cache = /* @__PURE__ */ new Map();
20004
+ maxEntries;
20005
+ constructor(opts) {
20006
+ this.dir = opts.dir;
20007
+ this.maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
20008
+ }
20009
+ // ── Writes ──────────────────────────────────────────────────────────────
20010
+ /**
20011
+ * Record a request/response pair. Idempotent on hash: a second
20012
+ * `record` for the same hash is a no-op (the existing entry wins).
20013
+ * Returns the hash.
20014
+ */
20015
+ async record(input) {
20016
+ const hash = hashRequest(input.request);
20017
+ await this.enqueue(input.sessionId, async () => {
20018
+ const cache = await this.ensureCache(input.sessionId);
20019
+ if (cache.has(hash)) return;
20020
+ const entry = {
20021
+ hash,
20022
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
20023
+ request: input.request,
20024
+ response: input.response
20025
+ };
20026
+ let all = [...cache.values(), entry];
20027
+ if (all.length > this.maxEntries) {
20028
+ const evictCount = all.length - this.maxEntries;
20029
+ all = all.slice(evictCount);
20030
+ }
20031
+ await this.writeAll(input.sessionId, all);
20032
+ cache.clear();
20033
+ for (const e of all) cache.set(e.hash, e);
20034
+ });
20035
+ return hash;
20036
+ }
20037
+ // ── Reads ───────────────────────────────────────────────────────────────
20038
+ /**
20039
+ * Look up an entry by hash. Returns `null` when the request has
20040
+ * not been recorded for this session. O(1) after the first call
20041
+ * per session (in-memory cache).
20042
+ */
20043
+ async lookup(sessionId, hash) {
20044
+ const cache = await this.ensureCache(sessionId);
20045
+ return cache.get(hash) ?? null;
20046
+ }
20047
+ /** All recorded entries for a session, in insertion order. */
20048
+ async load(sessionId) {
20049
+ const cache = await this.ensureCache(sessionId);
20050
+ return [...cache.values()];
20051
+ }
20052
+ /**
20053
+ * List every session id that has a replay log in the store dir.
20054
+ * Returns an array of `{ sessionId, entryCount, path }` sorted
20055
+ * by sessionId for stable output. Used by `wstack replay --list`.
20056
+ */
20057
+ async list() {
20058
+ let entries;
20059
+ try {
20060
+ entries = await fsp2.readdir(this.dir);
20061
+ } catch (err) {
20062
+ if (err.code === "ENOENT") return [];
20063
+ return [];
20064
+ }
20065
+ const out = [];
20066
+ for (const name of entries) {
20067
+ if (!name.endsWith(".replay.jsonl")) continue;
20068
+ const sessionId = name.slice(0, -".replay.jsonl".length);
20069
+ const all = await this.load(sessionId);
20070
+ out.push({
20071
+ sessionId,
20072
+ entryCount: all.length,
20073
+ path: path6.join(this.dir, name)
20074
+ });
20075
+ }
20076
+ return out.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
20077
+ }
20078
+ // ── Internals ───────────────────────────────────────────────────────────
20079
+ filePath(sessionId) {
20080
+ if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
20081
+ throw new Error(`Invalid sessionId: ${sessionId}`);
20082
+ }
20083
+ return path6.join(this.dir, `${sessionId}.replay.jsonl`);
20084
+ }
20085
+ async readAll(sessionId) {
20086
+ const fp = this.filePath(sessionId);
20087
+ try {
20088
+ const raw = await fsp2.readFile(fp, "utf8");
20089
+ const out = [];
20090
+ for (const line of raw.split("\n")) {
20091
+ if (!line.trim()) continue;
20092
+ try {
20093
+ const parsed = JSON.parse(line);
20094
+ if ("entry" in parsed && parsed.entry) {
20095
+ out.push(parsed.entry);
20096
+ } else {
20097
+ out.push(parsed);
20098
+ }
20099
+ } catch {
20100
+ }
20101
+ }
20102
+ return out;
20103
+ } catch (err) {
20104
+ if (err.code === "ENOENT") return [];
20105
+ return [];
20106
+ }
20107
+ }
20108
+ async writeAll(sessionId, entries) {
20109
+ const fp = this.filePath(sessionId);
20110
+ const body = entries.map((e) => JSON.stringify(e)).join("\n") + (entries.length ? "\n" : "");
20111
+ await atomicWrite(fp, body);
20112
+ }
20113
+ async ensureCache(sessionId) {
20114
+ let cache = this.cache.get(sessionId);
20115
+ if (cache) return cache;
20116
+ const all = await this.readAll(sessionId);
20117
+ cache = /* @__PURE__ */ new Map();
20118
+ for (const e of all) cache.set(e.hash, e);
20119
+ this.cache.set(sessionId, cache);
20120
+ return cache;
20121
+ }
20122
+ enqueue(sessionId, fn) {
20123
+ const prev = this.writeChains.get(sessionId) ?? Promise.resolve();
20124
+ const next = prev.then(fn, fn);
20125
+ this.writeChains.set(
20126
+ sessionId,
20127
+ next.catch(() => void 0)
20128
+ );
20129
+ return next;
20130
+ }
20131
+ };
20132
+ var SessionRecovery = class {
20133
+ constructor(dir) {
20134
+ this.dir = dir;
20135
+ }
20136
+ dir;
20137
+ /**
20138
+ * Scan a session log and return a `StaleSession` if and only
20139
+ * if the last event is an `in_flight_start` without a matching
20140
+ * `in_flight_end`. Returns `null` when:
20141
+ * - the log does not exist;
20142
+ * - the log is empty;
20143
+ * - the last event is `in_flight_end` (clean shutdown);
20144
+ * - the last event is something else (e.g. an unannotated
20145
+ * legacy log without in-flight markers).
20146
+ */
20147
+ async detectStale(sessionId) {
20148
+ const fp = this.filePath(sessionId);
20149
+ let raw;
20150
+ try {
20151
+ raw = await fsp2.readFile(fp, "utf8");
20152
+ } catch (err) {
20153
+ if (err.code === "ENOENT") return null;
20154
+ return null;
20155
+ }
20156
+ return this.parseForStale(sessionId, fp, raw);
20157
+ }
20158
+ /**
20159
+ * Generate a recovery plan for a session. The plan describes
20160
+ * "what would be re-executed" if the user chose to resume —
20161
+ * everything after the last `checkpoint` event, plus the
20162
+ * dangling in-flight marker if present.
20163
+ *
20164
+ * Returns a non-null plan for ANY session that has at least
20165
+ * one event after a checkpoint (or, for legacy sessions, at
20166
+ * least one event). Pure read; no mutation.
20167
+ */
20168
+ async recover(sessionId) {
20169
+ const fp = this.filePath(sessionId);
20170
+ let raw;
20171
+ try {
20172
+ raw = await fsp2.readFile(fp, "utf8");
20173
+ } catch (err) {
20174
+ if (err.code === "ENOENT") return null;
20175
+ return null;
20176
+ }
20177
+ const events = [];
20178
+ for (const line of raw.split("\n")) {
20179
+ if (!line.trim()) continue;
20180
+ try {
20181
+ events.push(JSON.parse(line));
20182
+ } catch {
20183
+ }
20184
+ }
20185
+ if (events.length === 0) return null;
20186
+ let lastCheckpoint = null;
20187
+ let lastCheckpointIdx = -1;
20188
+ for (let i = 0; i < events.length; i++) {
20189
+ if (events[i].type === "checkpoint") {
20190
+ lastCheckpoint = events[i];
20191
+ lastCheckpointIdx = i;
20192
+ }
20193
+ }
20194
+ const pendingEvents = lastCheckpointIdx >= 0 ? events.slice(lastCheckpointIdx + 1) : events;
20195
+ const lastEv = events[events.length - 1];
20196
+ const inFlightStart = lastEv.type === "in_flight_start" ? lastEv : null;
20197
+ const context = inFlightStart && inFlightStart.type === "in_flight_start" ? inFlightStart.context : null;
20198
+ return {
20199
+ sessionId,
20200
+ stale: inFlightStart !== null,
20201
+ lastCheckpoint,
20202
+ pendingEvents,
20203
+ inFlightStart,
20204
+ context
20205
+ };
20206
+ }
20207
+ /**
20208
+ * List every stale session in a directory. Returns an array
20209
+ * (possibly empty) sorted by `lastEventTs` descending — most
20210
+ * recent crash first.
19116
20211
  */
19117
- async uninstall(name, opts) {
19118
- const scope = opts?.global ? "user" : "project";
19119
- const entries = await this.manifest.findByName(name);
19120
- const entry = entries.find((e) => e.scope === scope);
19121
- if (!entry) {
19122
- throw new Error(`Skill "${name}" is not installed${scope === "user" ? " (global)" : ""}.`);
20212
+ async listResumable() {
20213
+ let entries;
20214
+ try {
20215
+ entries = await fsp2.readdir(this.dir);
20216
+ } catch (err) {
20217
+ if (err.code === "ENOENT") return [];
20218
+ return [];
19123
20219
  }
19124
- await this.removeSkillFiles(name, scope);
19125
- await this.manifest.removeEntry(name, scope);
19126
- this.invalidateLoaderCache();
20220
+ const out = [];
20221
+ for (const name of entries) {
20222
+ if (!name.endsWith(".jsonl")) continue;
20223
+ const sessionId = name.slice(0, -".jsonl".length);
20224
+ if (sessionId.includes(".replay") || sessionId.includes(".annotations")) continue;
20225
+ const stale = await this.detectStale(sessionId);
20226
+ if (stale) out.push(stale);
20227
+ }
20228
+ return out.sort((a, b) => b.lastEventTs.localeCompare(a.lastEventTs));
19127
20229
  }
19128
- /**
19129
- * List all installed skills from the manifest.
19130
- */
19131
- async listInstalled() {
19132
- return this.manifest.listAll();
20230
+ // ── Internals ──────────────────────────────────────────────────────────
20231
+ filePath(sessionId) {
20232
+ if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
20233
+ throw new Error(`Invalid sessionId: ${sessionId}`);
20234
+ }
20235
+ return path6.join(this.dir, `${sessionId}.jsonl`);
19133
20236
  }
19134
- // ── Private helpers ──────────────────────────────────────────────
19135
20237
  /**
19136
- * Detect skills in an extracted repository.
19137
- * Returns an array of detected skills with their files.
20238
+ * Stream-parse the last few lines of a JSONL log. We do NOT load
20239
+ * the whole file into memory for long-running sessions the log
20240
+ * can be megabytes. Instead we read tail-ward and find the last
20241
+ * `in_flight_start` / `in_flight_end` pair.
19138
20242
  */
19139
- async detectSkills(baseDir) {
19140
- const results = [];
19141
- const rootSkillMd = path6.join(baseDir, "SKILL.md");
19142
- try {
19143
- await fsp2.access(rootSkillMd);
19144
- const content = await fsp2.readFile(rootSkillMd, "utf8");
19145
- const meta = parseFrontmatter2(content);
19146
- if (meta.name && meta.description) {
19147
- results.push({
19148
- name: meta.name,
19149
- baseDir,
19150
- files: ["SKILL.md"]
19151
- });
19152
- return results;
20243
+ async parseForStale(sessionId, fp, raw) {
20244
+ const lines = raw.split("\n");
20245
+ let lastEvent = null;
20246
+ let eventCount = 0;
20247
+ for (const line of lines) {
20248
+ if (!line.trim()) continue;
20249
+ try {
20250
+ const ev = JSON.parse(line);
20251
+ lastEvent = ev;
20252
+ eventCount++;
20253
+ } catch {
19153
20254
  }
19154
- } catch {
19155
20255
  }
19156
- const skillsDir = path6.join(baseDir, "skills");
19157
- try {
19158
- const entries = await fsp2.readdir(skillsDir, { withFileTypes: true });
19159
- for (const entry of entries) {
19160
- if (!entry.isDirectory()) continue;
19161
- const skillFile = path6.join(skillsDir, entry.name, "SKILL.md");
19162
- try {
19163
- const content = await fsp2.readFile(skillFile, "utf8");
19164
- const meta = parseFrontmatter2(content);
19165
- if (meta.name && meta.description) {
19166
- const skillDir = path6.join(skillsDir, entry.name);
19167
- const files = await collectFiles(skillDir, skillDir);
19168
- results.push({
19169
- name: meta.name,
19170
- baseDir: skillDir,
19171
- files
19172
- });
19173
- }
19174
- } catch {
19175
- }
19176
- }
19177
- } catch {
20256
+ if (!lastEvent) return null;
20257
+ if (lastEvent.type === "in_flight_start") {
20258
+ return {
20259
+ sessionId,
20260
+ path: fp,
20261
+ lastEventTs: lastEvent.ts,
20262
+ context: lastEvent.context,
20263
+ eventCount
20264
+ };
19178
20265
  }
19179
- return results;
20266
+ return null;
20267
+ }
20268
+ };
20269
+
20270
+ // src/storage/tool-audit-log.ts
20271
+ init_atomic_write();
20272
+ var GENESIS_PREV = "0".repeat(64);
20273
+ var ToolAuditLog = class {
20274
+ dir;
20275
+ /** In-memory cache of the last entry's hash (per session), to compute chains efficiently. */
20276
+ tailHash = /* @__PURE__ */ new Map();
20277
+ writeChains = /* @__PURE__ */ new Map();
20278
+ constructor(opts) {
20279
+ this.dir = opts.dir;
19180
20280
  }
19181
20281
  /**
19182
- * Remove all files for an installed skill.
20282
+ * Append a tool call/result pair to the chain. Returns the
20283
+ * resulting entry. Idempotency is not guaranteed — if you
20284
+ * record the same tool_use twice you get two entries. That's
20285
+ * intentional: the audit log is a record, not a cache.
19183
20286
  */
19184
- async removeSkillFiles(name, scope) {
19185
- const targetDir = scope === "project" ? this.opts.projectSkillsDir : this.opts.globalSkillsDir;
19186
- const skillDir = path6.join(targetDir, name);
19187
- await fsp2.rm(skillDir, { recursive: true, force: true });
20287
+ async record(input) {
20288
+ let entry = null;
20289
+ await this.enqueue(input.sessionId, async () => {
20290
+ const prevHash = this.tailHash.get(input.sessionId) ?? GENESIS_PREV;
20291
+ const id = randomUUID();
20292
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
20293
+ const content = {
20294
+ id,
20295
+ ts,
20296
+ prevHash,
20297
+ toolName: input.toolName,
20298
+ toolUseId: input.toolUseId,
20299
+ input: input.input,
20300
+ output: input.output,
20301
+ isError: input.isError
20302
+ };
20303
+ const hash = createHash("sha256").update(stableStringify2(content), "utf8").digest("hex");
20304
+ entry = {
20305
+ index: this.tailHash.has(input.sessionId) ? -1 : 0,
20306
+ // placeholder; recomputed below
20307
+ id,
20308
+ ts,
20309
+ prevHash,
20310
+ hash,
20311
+ toolName: input.toolName,
20312
+ toolUseId: input.toolUseId,
20313
+ input: input.input,
20314
+ output: input.output,
20315
+ isError: input.isError
20316
+ };
20317
+ const existing = await this.readAll(input.sessionId);
20318
+ const index = existing.length;
20319
+ const finalContent = { ...content, index };
20320
+ const finalHash = createHash("sha256").update(stableStringify2(finalContent), "utf8").digest("hex");
20321
+ entry = { ...entry, index, hash: finalHash };
20322
+ await this.appendLine(input.sessionId, entry);
20323
+ this.tailHash.set(input.sessionId, finalHash);
20324
+ });
20325
+ return entry;
19188
20326
  }
19189
20327
  /**
19190
- * Invalidate the skill loader's cache so newly installed skills appear.
20328
+ * Walk the chain and verify every entry's hash and prevHash.
20329
+ * Returns a structured verdict — never throws.
19191
20330
  */
19192
- invalidateLoaderCache() {
19193
- const loader = this.opts.skillLoader;
19194
- if (loader && typeof loader.invalidateCache === "function") {
19195
- loader.invalidateCache();
20331
+ async verify(sessionId) {
20332
+ const entries = await this.readAll(sessionId);
20333
+ if (entries.length === 0) return { ok: true, entries: 0 };
20334
+ if (entries[0].prevHash !== GENESIS_PREV) {
20335
+ return {
20336
+ ok: false,
20337
+ brokenAt: 0,
20338
+ reason: "first entry is not the genesis (prevHash != 0\u20260)"
20339
+ };
20340
+ }
20341
+ let prevHash = GENESIS_PREV;
20342
+ for (let i = 0; i < entries.length; i++) {
20343
+ const e = entries[i];
20344
+ if (e.prevHash !== prevHash) {
20345
+ return {
20346
+ ok: false,
20347
+ brokenAt: i,
20348
+ reason: `prevHash mismatch at entry ${i} (expected ${prevHash.slice(0, 8)}\u2026, got ${e.prevHash.slice(0, 8)}\u2026)`
20349
+ };
20350
+ }
20351
+ const content = {
20352
+ id: e.id,
20353
+ ts: e.ts,
20354
+ prevHash: e.prevHash,
20355
+ toolName: e.toolName,
20356
+ toolUseId: e.toolUseId,
20357
+ input: e.input,
20358
+ output: e.output,
20359
+ isError: e.isError,
20360
+ index: e.index
20361
+ };
20362
+ const expectedHash = createHash("sha256").update(stableStringify2(content), "utf8").digest("hex");
20363
+ if (expectedHash !== e.hash) {
20364
+ return {
20365
+ ok: false,
20366
+ brokenAt: i,
20367
+ reason: `hash mismatch at entry ${i} (entry content was modified)`
20368
+ };
20369
+ }
20370
+ prevHash = e.hash;
19196
20371
  }
20372
+ return { ok: true, entries: entries.length };
19197
20373
  }
19198
- };
19199
- function parseFrontmatter2(raw) {
19200
- if (!raw.startsWith("---")) return {};
19201
- const end = raw.indexOf("\n---", 4);
19202
- if (end === -1) return {};
19203
- const block = raw.slice(4, end);
19204
- const out = {};
19205
- let key = null;
19206
- let value = [];
19207
- const flush = () => {
19208
- if (key) {
19209
- out[key] = value.join("\n").trim();
20374
+ /** All entries for a session, in insertion order. */
20375
+ async load(sessionId) {
20376
+ return this.readAll(sessionId);
20377
+ }
20378
+ // ── Internals ────────────────────────────────────────────────────────────
20379
+ filePath(sessionId) {
20380
+ if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
20381
+ throw new Error(`Invalid sessionId: ${sessionId}`);
19210
20382
  }
19211
- key = null;
19212
- value = [];
19213
- };
19214
- for (const line of block.split("\n")) {
19215
- const m = /^([a-zA-Z_]+):\s*(\|?)\s*(.*)$/.exec(line);
19216
- if (m) {
19217
- flush();
19218
- key = m[1] ?? "";
19219
- const pipe = m[2];
19220
- const rest = m[3] ?? "";
19221
- if (pipe === "|") {
19222
- value = [];
19223
- } else if (rest) {
19224
- value = [rest];
19225
- } else {
19226
- value = [];
20383
+ return path6.join(this.dir, `${sessionId}.audit.jsonl`);
20384
+ }
20385
+ async readAll(sessionId) {
20386
+ const fp = this.filePath(sessionId);
20387
+ try {
20388
+ const raw = await fsp2.readFile(fp, "utf8");
20389
+ const out = [];
20390
+ for (const line of raw.split("\n")) {
20391
+ if (!line.trim()) continue;
20392
+ try {
20393
+ out.push(JSON.parse(line));
20394
+ } catch {
20395
+ }
19227
20396
  }
19228
- } else if (key) {
19229
- value.push(line.replace(/^\s+/, ""));
20397
+ return out;
20398
+ } catch (err) {
20399
+ if (err.code === "ENOENT") return [];
20400
+ return [];
19230
20401
  }
19231
20402
  }
19232
- flush();
19233
- return out;
20403
+ async appendLine(sessionId, entry) {
20404
+ const fp = this.filePath(sessionId);
20405
+ const existing = await this.readAll(sessionId);
20406
+ const all = [...existing, entry];
20407
+ await atomicWrite(fp, all.map((e) => JSON.stringify(e)).join("\n") + (all.length ? "\n" : ""));
20408
+ }
20409
+ enqueue(sessionId, fn) {
20410
+ const prev = this.writeChains.get(sessionId) ?? Promise.resolve();
20411
+ const next = prev.then(fn, fn);
20412
+ this.writeChains.set(
20413
+ sessionId,
20414
+ next.catch(() => void 0)
20415
+ );
20416
+ return next;
20417
+ }
20418
+ };
20419
+ function stableStringify2(value) {
20420
+ return JSON.stringify(sortKeys2(value));
19234
20421
  }
19235
- async function collectFiles(dir, baseDir) {
19236
- const results = [];
19237
- const entries = await fsp2.readdir(dir, { withFileTypes: true });
19238
- for (const entry of entries) {
19239
- const fullPath = path6.join(dir, entry.name);
19240
- const relPath = path6.relative(baseDir, fullPath);
19241
- if (entry.isDirectory()) {
19242
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
19243
- results.push(...await collectFiles(fullPath, baseDir));
19244
- } else {
19245
- results.push(relPath);
20422
+ function sortKeys2(value) {
20423
+ if (Array.isArray(value)) return value.map(sortKeys2);
20424
+ if (value && typeof value === "object") {
20425
+ const obj = value;
20426
+ const sorted = {};
20427
+ for (const key of Object.keys(obj).sort()) {
20428
+ sorted[key] = sortKeys2(obj[key]);
19246
20429
  }
20430
+ return sorted;
19247
20431
  }
19248
- return results;
20432
+ return value;
19249
20433
  }
19250
20434
 
19251
20435
  // src/storage/session-rewinder.ts
@@ -19421,7 +20605,7 @@ var DefaultPromptStore = class {
19421
20605
  }
19422
20606
  async list() {
19423
20607
  await ensureDir(this.dir);
19424
- let entries = [];
20608
+ const entries = [];
19425
20609
  try {
19426
20610
  const files = await fsp2.readdir(this.dir);
19427
20611
  for (const file of files) {
@@ -19697,8 +20881,8 @@ var CloudSync = class {
19697
20881
  const localPath = this.categoryToPath(cat);
19698
20882
  if (!localPath) continue;
19699
20883
  try {
19700
- const stat9 = await fsp2.stat(localPath);
19701
- if (stat9.isDirectory()) {
20884
+ const stat8 = await fsp2.stat(localPath);
20885
+ if (stat8.isDirectory()) {
19702
20886
  const files = await this.walkDir(localPath, localPath);
19703
20887
  for (const file of files) {
19704
20888
  const content = await fsp2.readFile(file, "utf8");
@@ -19723,8 +20907,8 @@ var CloudSync = class {
19723
20907
  const localPath = this.categoryToPath(cat);
19724
20908
  if (!localPath) continue;
19725
20909
  try {
19726
- const stat9 = await fsp2.stat(localPath);
19727
- if (stat9.isDirectory()) {
20910
+ const stat8 = await fsp2.stat(localPath);
20911
+ if (stat8.isDirectory()) {
19728
20912
  const files = await this.walkDir(localPath, localPath);
19729
20913
  for (const file of files) {
19730
20914
  const content = await fsp2.readFile(file);
@@ -20325,7 +21509,7 @@ var SkillGenerator = class {
20325
21509
  };
20326
21510
  return [commonInjection, ...stackSpecific[stack] ?? []];
20327
21511
  }
20328
- getConfigPatterns(stack) {
21512
+ getConfigPatterns(_stack) {
20329
21513
  const commonConfig = [
20330
21514
  {
20331
21515
  id: "insecure-tls",
@@ -20907,7 +22091,6 @@ var SecurityScannerOrchestrator = class {
20907
22091
  retryPolicy;
20908
22092
  errorHandler;
20909
22093
  detector = defaultTechStackDetector;
20910
- reportGenerator = defaultReportGenerator;
20911
22094
  gitignoreUpdater = defaultGitignoreUpdater;
20912
22095
  /**
20913
22096
  * Wraps provider.complete with retry logic using the injected RetryPolicy.
@@ -21091,7 +22274,7 @@ Return ONLY the JSON object, no markdown, no explanation.`;
21091
22274
  /**
21092
22275
  * Scan a batch of files using LLM.
21093
22276
  */
21094
- async scanFileBatchLLM(provider, model, files, skill, techStack) {
22277
+ async scanFileBatchLLM(provider, model, files, skill, _techStack) {
21095
22278
  const fileContents = [];
21096
22279
  for (const file of files) {
21097
22280
  try {
@@ -21288,7 +22471,7 @@ Be specific about the vulnerabilities found and how to fix them.`;
21288
22471
  /**
21289
22472
  * Gather project info for skill generation.
21290
22473
  */
21291
- async gatherProjectInfo(projectRoot, techStack) {
22474
+ async gatherProjectInfo(projectRoot, _techStack) {
21292
22475
  const info = [];
21293
22476
  const keyFiles = [
21294
22477
  "package.json",
@@ -21320,7 +22503,7 @@ ${dirs.join(", ")}`);
21320
22503
  /**
21321
22504
  * Gather files to scan based on patterns.
21322
22505
  */
21323
- async gatherFiles(root, patterns, depth) {
22506
+ async gatherFiles(root, _patterns, depth) {
21324
22507
  const files = [];
21325
22508
  const maxDepth = depth === "quick" ? 2 : depth === "deep" ? 20 : 5;
21326
22509
  const extensions = [".ts", ".js", ".jsx", ".tsx", ".py", ".go", ".java", ".cs", ".rs"];
@@ -21579,22 +22762,22 @@ Use \`/security report <number>\` to view a specific report.` };
21579
22762
  }
21580
22763
  const index = Number.parseInt(reportId, 10) - 1;
21581
22764
  if (!Number.isNaN(index) && reports[index]) {
21582
- const { readFile: readFile35 } = await import('fs/promises');
21583
- const content = await readFile35(join(reportsDir, reports[index]), "utf-8");
22765
+ const { readFile: readFile37 } = await import('fs/promises');
22766
+ const content = await readFile37(join(reportsDir, reports[index]), "utf-8");
21584
22767
  return { message: `# Security Report
21585
22768
 
21586
22769
  ${content}` };
21587
22770
  }
21588
22771
  const match = reports.find((r) => r.includes(reportId));
21589
22772
  if (match) {
21590
- const { readFile: readFile35 } = await import('fs/promises');
21591
- const content = await readFile35(join(reportsDir, match), "utf-8");
22773
+ const { readFile: readFile37 } = await import('fs/promises');
22774
+ const content = await readFile37(join(reportsDir, match), "utf-8");
21592
22775
  return { message: `# Security Report
21593
22776
 
21594
22777
  ${content}` };
21595
22778
  }
21596
22779
  return { message: `\u274C Report "${reportId}" not found. Use \`/security report\` to see available reports.` };
21597
- } catch (error) {
22780
+ } catch (_error) {
21598
22781
  return { message: "\u{1F4ED} No security reports found. Run `/security scan` first." };
21599
22782
  }
21600
22783
  }
@@ -21647,7 +22830,6 @@ var FleetManager = class {
21647
22830
  /** Usage rollup across all subagents. */
21648
22831
  usage;
21649
22832
  manifestPath;
21650
- sessionsRoot;
21651
22833
  directorRunId;
21652
22834
  /** Spawn cap (lifetime total). Infinity means unlimited. */
21653
22835
  maxSpawns;
@@ -21675,9 +22857,14 @@ var FleetManager = class {
21675
22857
  _usedNicknames = /* @__PURE__ */ new Set();
21676
22858
  /** The coordinator (wired via setCoordinator by Director after construction). */
21677
22859
  coordinator = null;
22860
+ /** Leader agent's current context pressure (full request tokens). */
22861
+ leaderContextPressure = 0;
22862
+ /** Maximum context load fraction before spawn is refused. */
22863
+ maxLeaderContextLoad;
22864
+ /** Provider's max context window in tokens. */
22865
+ maxContext;
21678
22866
  constructor(opts = {}) {
21679
22867
  this.manifestPath = opts.manifestPath;
21680
- this.sessionsRoot = opts.sessionsRoot;
21681
22868
  this.directorRunId = opts.directorRunId ?? randomUUID();
21682
22869
  this.maxSpawns = opts.maxSpawns ?? Number.POSITIVE_INFINITY;
21683
22870
  this.maxSpawnDepth = opts.maxSpawnDepth ?? 2;
@@ -21685,6 +22872,8 @@ var FleetManager = class {
21685
22872
  this.sessionWriter = opts.sessionWriter ?? null;
21686
22873
  this.manifestDebounceMs = opts.manifestDebounceMs ?? 2e3;
21687
22874
  this.maxFleetCostUsd = opts.directorBudget?.maxCostUsd ?? Number.POSITIVE_INFINITY;
22875
+ this.maxLeaderContextLoad = opts.maxLeaderContextLoad ?? 0.85;
22876
+ this.maxContext = opts.maxContext ?? 128e3;
21688
22877
  this.stateCheckpoint = opts.stateCheckpointPath ? new DirectorStateCheckpoint(
21689
22878
  opts.stateCheckpointPath,
21690
22879
  {
@@ -21699,7 +22888,7 @@ var FleetManager = class {
21699
22888
  this.fleet = new FleetBus();
21700
22889
  this.usage = new FleetUsageAggregator(
21701
22890
  this.fleet,
21702
- (id, provider, model) => {
22891
+ (_id, provider, model) => {
21703
22892
  if (provider && model) return this.priceLookups.get(`${provider}/${model}`);
21704
22893
  return void 0;
21705
22894
  },
@@ -21738,7 +22927,7 @@ var FleetManager = class {
21738
22927
  * which cap was exceeded. Does NOT throw — the caller decides
21739
22928
  * how to surface the rejection.
21740
22929
  */
21741
- canSpawn(config) {
22930
+ canSpawn(_config) {
21742
22931
  if (this.spawnDepth >= this.maxSpawnDepth) {
21743
22932
  return { kind: "max_spawn_depth", limit: this.maxSpawnDepth, observed: this.spawnDepth };
21744
22933
  }
@@ -21751,8 +22940,21 @@ var FleetManager = class {
21751
22940
  return { kind: "max_cost_usd", limit: this.maxFleetCostUsd, observed: totalCost };
21752
22941
  }
21753
22942
  }
22943
+ if (this.maxLeaderContextLoad < 1) {
22944
+ const threshold = this.maxContext * this.maxLeaderContextLoad;
22945
+ if (this.leaderContextPressure >= threshold) {
22946
+ return {
22947
+ kind: "max_context_load",
22948
+ limit: threshold,
22949
+ observed: this.leaderContextPressure
22950
+ };
22951
+ }
22952
+ }
21754
22953
  return null;
21755
22954
  }
22955
+ setLeaderContextPressure(tokens) {
22956
+ this.leaderContextPressure = tokens;
22957
+ }
21756
22958
  /**
21757
22959
  * Assign a memorable nickname (e.g. "Einstein (Bug Hunter)") to the config,
21758
22960
  * record it so the same name is never reused, then record the spawn.
@@ -21977,7 +23179,7 @@ function createMcpControlTool(opts) {
21977
23179
  mutating: true,
21978
23180
  riskTier: "standard",
21979
23181
  inputSchema,
21980
- async execute(raw, ctx) {
23182
+ async execute(raw) {
21981
23183
  const input = raw;
21982
23184
  return mcpControlDispatch(input, { getConfig, configPath, registry });
21983
23185
  }
@@ -22525,7 +23727,6 @@ var Agent = class {
22525
23727
  pipelines;
22526
23728
  ctx;
22527
23729
  maxIterations;
22528
- iterationTimeoutMs;
22529
23730
  executionStrategy;
22530
23731
  perIterationOutputCapBytes;
22531
23732
  plugins = [];
@@ -22543,7 +23744,6 @@ var Agent = class {
22543
23744
  this.pipelines = init.pipelines;
22544
23745
  this.ctx = init.context;
22545
23746
  this.maxIterations = init.maxIterations ?? DEFAULT_MAX_ITERATIONS;
22546
- this.iterationTimeoutMs = init.iterationTimeoutMs ?? 3e5;
22547
23747
  this.executionStrategy = init.executionStrategy ?? "smart";
22548
23748
  this.perIterationOutputCapBytes = init.perIterationOutputCapBytes ?? 1e5;
22549
23749
  this.autoExtendLimit = init.autoExtendLimit ?? true;
@@ -22565,9 +23765,6 @@ var Agent = class {
22565
23765
  get permission() {
22566
23766
  return this.container.resolve(TOKENS.PermissionPolicy);
22567
23767
  }
22568
- get scrubber() {
22569
- return this.container.resolve(TOKENS.SecretScrubber);
22570
- }
22571
23768
  get renderer() {
22572
23769
  return this.container.has(TOKENS.Renderer) ? this.container.resolve(TOKENS.Renderer) : void 0;
22573
23770
  }
@@ -22697,6 +23894,11 @@ var Agent = class {
22697
23894
  if (controller.signal.aborted) {
22698
23895
  return { status: "aborted", iterations };
22699
23896
  }
23897
+ await this.ctx.session.writeInFlightMarker(`iteration ${i} / max ${this.maxIterations}`).catch((err) => {
23898
+ this.logger.debug?.(
23899
+ `in-flight marker write failed: ${err instanceof Error ? err.message : String(err)}`
23900
+ );
23901
+ });
22700
23902
  if (autonomousContinue) {
22701
23903
  consumeAutonomousContinue(this.ctx);
22702
23904
  }
@@ -22778,6 +23980,7 @@ var Agent = class {
22778
23980
  finalText = responseResult.finalText;
22779
23981
  const toolUses = res.content.filter(isToolUseBlock);
22780
23982
  if (toolUses.length === 0) {
23983
+ this.emitContextPct();
22781
23984
  this.events.emit("iteration.completed", { ctx: this.ctx, index: i });
22782
23985
  if (autonomousContinue && responseResult.directive === "continue") {
22783
23986
  await this.compactContextIfNeeded();
@@ -22791,11 +23994,13 @@ var Agent = class {
22791
23994
  }
22792
23995
  await this.executeTools(toolUses);
22793
23996
  if (autonomousContinue && consumeAutonomousContinue(this.ctx)) {
23997
+ this.emitContextPct();
22794
23998
  this.events.emit("iteration.completed", { ctx: this.ctx, index: i });
22795
23999
  await this.compactContextIfNeeded();
22796
24000
  await this.extensions.runAfterIteration(this.ctx, i);
22797
24001
  continue;
22798
24002
  }
24003
+ this.emitContextPct();
22799
24004
  this.events.emit("iteration.completed", { ctx: this.ctx, index: i });
22800
24005
  await this.compactContextIfNeeded();
22801
24006
  await this.extensions.runAfterIteration(this.ctx, i);
@@ -22808,6 +24013,12 @@ var Agent = class {
22808
24013
  }
22809
24014
  } finally {
22810
24015
  offSubagentDone();
24016
+ const reason = controller.signal.aborted ? "aborted" : "clean";
24017
+ await this.ctx.session.clearInFlightMarker(reason).catch((err) => {
24018
+ this.logger.debug?.(
24019
+ `in-flight marker clear failed: ${err instanceof Error ? err.message : String(err)}`
24020
+ );
24021
+ });
22811
24022
  }
22812
24023
  }
22813
24024
  /**
@@ -23100,6 +24311,19 @@ var Agent = class {
23100
24311
  async compactContextIfNeeded() {
23101
24312
  await this.pipelines.contextWindow.run(this.ctx);
23102
24313
  }
24314
+ /**
24315
+ * Emit the current context window load as a `ctx.pct` event so subscribers
24316
+ * (FleetBus → TUI) can render a live fill bar per agent.
24317
+ */
24318
+ emitContextPct() {
24319
+ const maxContext = this.ctx.provider.capabilities.maxContext ?? 2e5;
24320
+ const { total } = estimateRequestTokens(
24321
+ this.ctx.messages,
24322
+ this.ctx.systemPrompt,
24323
+ this.ctx.tools ?? []
24324
+ );
24325
+ this.events.emit("ctx.pct", { load: total / maxContext, tokens: total, maxContext });
24326
+ }
23103
24327
  };
23104
24328
  function toError(err) {
23105
24329
  return err instanceof Error ? err : new Error(String(err));
@@ -23776,8 +25000,8 @@ ${mem}`);
23776
25000
  }
23777
25001
  async dirExists(p) {
23778
25002
  try {
23779
- const stat9 = await fsp2.stat(p);
23780
- return stat9.isDirectory();
25003
+ const stat8 = await fsp2.stat(p);
25004
+ return stat8.isDirectory();
23781
25005
  } catch {
23782
25006
  return false;
23783
25007
  }
@@ -26113,6 +27337,203 @@ function assertSafePath(dir, projectRoot) {
26113
27337
  }
26114
27338
  }
26115
27339
 
27340
+ // src/coordination/collab-bus.ts
27341
+ var CollaborationBus = class {
27342
+ pausePromise = null;
27343
+ pauseResolve = null;
27344
+ pausedAtMs = null;
27345
+ pausedBy = null;
27346
+ // ── State queries ──────────────────────────────────────────────────────
27347
+ isPaused() {
27348
+ return this.pausePromise !== null;
27349
+ }
27350
+ getState() {
27351
+ return {
27352
+ paused: this.isPaused(),
27353
+ pausedAt: this.pausedAtMs ? new Date(this.pausedAtMs).toISOString() : null,
27354
+ pausedBy: this.pausedBy
27355
+ };
27356
+ }
27357
+ // ── Pause / resume control ─────────────────────────────────────────────
27358
+ /**
27359
+ * Pause the agent loop. Idempotent: a second `requestPause` while
27360
+ * already paused is a no-op (the original pause wins; we do not
27361
+ * overwrite `pausedBy`). Returns true when the state actually
27362
+ * transitioned, false when it was already paused.
27363
+ */
27364
+ requestPause(byParticipant) {
27365
+ if (this.isPaused()) return false;
27366
+ this.pausedAtMs = Date.now();
27367
+ this.pausedBy = byParticipant;
27368
+ this.pausePromise = new Promise((resolve9) => {
27369
+ this.pauseResolve = resolve9;
27370
+ });
27371
+ return true;
27372
+ }
27373
+ /**
27374
+ * Resume the agent loop. Returns true when the state actually
27375
+ * transitioned from paused → running, false when it was already
27376
+ * running (no-op).
27377
+ */
27378
+ resume() {
27379
+ if (!this.isPaused()) return false;
27380
+ if (this.pauseResolve) this.pauseResolve();
27381
+ this.pausePromise = null;
27382
+ this.pauseResolve = null;
27383
+ this.pausedAtMs = null;
27384
+ this.pausedBy = null;
27385
+ return true;
27386
+ }
27387
+ // ── Wait semantics (consumed by the middleware) ────────────────────────
27388
+ /**
27389
+ * Block until the bus is resumed or the timeout fires. Returns:
27390
+ * - `true` → bus was resumed in time
27391
+ * - `false` → timeout fired; bus was auto-resumed as a side effect
27392
+ *
27393
+ * When `timeoutMs` is `0` the wait is unbounded (the middleware must
27394
+ * be paired with an external AbortSignal in that case — we don't
27395
+ * expose one here to keep the API simple).
27396
+ */
27397
+ async waitForResume(timeoutMs) {
27398
+ if (!this.isPaused()) return true;
27399
+ if (!this.pausePromise) return true;
27400
+ if (timeoutMs === 0) {
27401
+ await this.pausePromise;
27402
+ return true;
27403
+ }
27404
+ let timer;
27405
+ const timeoutPromise = new Promise((resolve9) => {
27406
+ timer = setTimeout(() => resolve9("timeout"), timeoutMs);
27407
+ });
27408
+ const resumedPromise = this.pausePromise.then(() => "resumed");
27409
+ const winner = await Promise.race([resumedPromise, timeoutPromise]);
27410
+ if (timer) clearTimeout(timer);
27411
+ if (winner === "timeout") {
27412
+ this.resume();
27413
+ return false;
27414
+ }
27415
+ return true;
27416
+ }
27417
+ // ── Manual tool-call injection (Phase 4) ─────────────────────────────────
27418
+ //
27419
+ // A controller can ask the agent loop to use a specific tool_result
27420
+ // for a given tool_use_id — bypassing the real tool execution. This
27421
+ // is "I want the agent to think the read returned THIS content" or
27422
+ // "skip the bash call, just give it the answer I typed". The
27423
+ // injection is matched by tool_use_id, consumed once, and discarded.
27424
+ injectionQueue = /* @__PURE__ */ new Map();
27425
+ /**
27426
+ * Queue a manual tool result. The next time the agent's toolCall
27427
+ * pipeline sees a matching `toolUse.id`, the
27428
+ * `collabInjectMiddleware` consumes this entry and replaces the
27429
+ * real tool execution. Returns `false` if an injection for the
27430
+ * same id is already queued (idempotent — first write wins).
27431
+ */
27432
+ injectToolResult(input) {
27433
+ if (this.injectionQueue.has(input.toolUseId)) return false;
27434
+ this.injectionQueue.set(input.toolUseId, input);
27435
+ return true;
27436
+ }
27437
+ /**
27438
+ * Pop an injection from the queue. Returns the payload (and
27439
+ * removes it) if one is pending, or `null` when nothing matches.
27440
+ * Called by the middleware on every tool call.
27441
+ */
27442
+ takeInjection(toolUseId) {
27443
+ const v = this.injectionQueue.get(toolUseId);
27444
+ if (!v) return null;
27445
+ this.injectionQueue.delete(toolUseId);
27446
+ return v;
27447
+ }
27448
+ /** Inspect the queue size (for observability / tests). */
27449
+ pendingInjectionCount() {
27450
+ return this.injectionQueue.size;
27451
+ }
27452
+ };
27453
+
27454
+ // src/middleware/collab-pause.ts
27455
+ function collabPauseMiddleware(bus, opts = {}) {
27456
+ const timeoutMs = opts.defaultTimeoutMs ?? 6e4;
27457
+ const logger = opts.logger;
27458
+ return async function collabPause(payload, next) {
27459
+ if (!bus.isPaused()) {
27460
+ return next();
27461
+ }
27462
+ const state = bus.getState();
27463
+ logger?.debug?.(
27464
+ `collab-pause: tool '${payload.toolUse.name}' blocked \u2014 bus paused by ${state.pausedBy ?? "?"} at ${state.pausedAt ?? "?"}, waiting up to ${timeoutMs}ms for resume`
27465
+ );
27466
+ const resumed = await bus.waitForResume(timeoutMs);
27467
+ if (!resumed) {
27468
+ logger?.warn?.(
27469
+ `collab-pause: timeout after ${timeoutMs}ms \u2014 auto-resuming the bus to unblock the agent loop`
27470
+ );
27471
+ } else {
27472
+ logger?.debug?.(`collab-pause: resumed \u2014 proceeding with tool '${payload.toolUse.name}'`);
27473
+ }
27474
+ return next();
27475
+ };
27476
+ }
27477
+ function collabInjectMiddleware(bus, opts = {}) {
27478
+ const logger = opts.logger;
27479
+ return async function collabInject(payload, next) {
27480
+ const injected = bus.takeInjection(payload.toolUse.id);
27481
+ if (!injected) {
27482
+ return next();
27483
+ }
27484
+ logger?.debug?.(
27485
+ `collab-inject: tool '${payload.toolUse.name}' (id ${payload.toolUse.id}) \u2014 using controller-injected result (reason: ${injected.reason})`
27486
+ );
27487
+ payload.result = {
27488
+ type: "tool_result",
27489
+ tool_use_id: payload.toolUse.id,
27490
+ content: typeof injected.content === "string" ? injected.content : JSON.stringify(injected.content),
27491
+ is_error: injected.isError
27492
+ };
27493
+ };
27494
+ }
27495
+
27496
+ // src/replay/replay-provider-runner.ts
27497
+ var ReplayProviderRunner = class {
27498
+ constructor(inner, opts) {
27499
+ this.inner = inner;
27500
+ this.opts = opts;
27501
+ }
27502
+ inner;
27503
+ opts;
27504
+ async run(runOpts) {
27505
+ const hash = hashRequest(runOpts.request);
27506
+ const cached = await this.opts.log.lookup(this.opts.sessionId, hash);
27507
+ if (this.opts.mode === "replay") {
27508
+ if (!cached) {
27509
+ this.opts.logger?.warn?.(
27510
+ `replay: no recorded response for hash ${hash} (model ${runOpts.request.model})`
27511
+ );
27512
+ throw new Error(
27513
+ `ReplayProviderRunner: no recorded response for hash ${hash} in session ${this.opts.sessionId}. Either the request changed since recording, or this session has no replay log.`
27514
+ );
27515
+ }
27516
+ this.opts.logger?.debug?.(
27517
+ `replay: served cached response for hash ${hash} (recorded ${cached.ts})`
27518
+ );
27519
+ return cached.response;
27520
+ }
27521
+ if (this.opts.mode === "auto" && cached) {
27522
+ this.opts.logger?.debug?.(
27523
+ `replay: auto-hit hash ${hash}, served cached response`
27524
+ );
27525
+ return cached.response;
27526
+ }
27527
+ const response = await this.inner.run(runOpts);
27528
+ await this.opts.log.record({
27529
+ sessionId: this.opts.sessionId,
27530
+ request: runOpts.request,
27531
+ response
27532
+ });
27533
+ return response;
27534
+ }
27535
+ };
27536
+
26116
27537
  // src/plugins/prompts-plugin.ts
26117
27538
  function createPromptsPlugin(opts) {
26118
27539
  let store = null;
@@ -27170,6 +28591,6 @@ ${formatPlan(updated)}`
27170
28591
  };
27171
28592
  }
27172
28593
 
27173
- export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, ALL_SYNC_CATEGORIES, AUDIT_LOG_AGENT, Agent, AgentError, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutoPhasePlanner, AutoPhaseRunner, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, CONTEXT_WINDOW_MODES, CheckpointManager, CloudSync, ConfigError, ConfigMigrationError, Container, Context, ConversationState, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SPEC_TEMPLATE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultPromptStore, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DirectorStateCheckpoint, DoneConditionChecker, ERROR_CODES, EternalAutonomyEngine, EventBus, ExtensionRegistry, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FleetBus, FleetCostCapError, FleetManager, FleetSpawnBudgetError, FleetUsageAggregator, FsError, GitignoreUpdater, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, LAYER_1_IDENTITY, LLMSelector, MAX_JOURNAL_ENTRIES, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, PhaseGraphBuilder, PhaseOrchestrator, PhaseStore, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, ReportGenerator, RunController, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, ScopedEventBus, SddParallelRun, SddTaskDecomposer, SecurityScanner, SecurityScannerOrchestrator, SelectiveCompactor, SessionAnalyzer, SessionError, SkillGenerator, SkillInstaller, SkillManifestStore, SlashCommandRegistry, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TOKENS, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, TechStackDetector, ToolError, ToolExecutor, ToolRegistry, WorktreeManager, WrongStackError, addPlanItem, allServers, analyzeCriticalPath, appendJournal, applyRosterBudget, asBlocks, asText, assertSafePath, atomicWrite, attachAutoExtend, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildBtwBlock, buildChildEnv, buildGoalPreamble, buildOtlpMetricsRequest, buildOtlpTracesRequest, buildRecoveryStrategies, classifyFamily, clearPlan, color, compileGlob, compileUserRegex, completePartialObject, composeDirectorPrompt, composeSubagentPrompt, computeTaskProgress, consumeBtwNotes, context7Server, contextManagerTool, createAutoExecutor, createAutoPhaseFromTaskGraph, createContextManagerTool, createDefaultPipelines, createDelegateTool, createGitPlugin, createMcpControlTool, createMessage, createObservabilityPlugin, createPlanPlugin, createPromptsPlugin, createSecurityPlugin, createSecuritySlashCommand, createSkillsPlugin, createSyncPlugin, createToolOutputSerializer, decryptConfigSecrets, defaultGitignoreUpdater, defaultOrchestrator, defaultReportGenerator, defaultSecurityScanner, defaultSkillGenerator, defaultTechStackDetector, deriveTodosFromPlanItem, detectNewlineStyle, dispatchAgent, downloadGitHubTarball, emptyGoal, emptyPlan, encryptConfigSecrets, ensureDir, estimateRequestTokens, estimateTextTokens, estimateToolDefTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, extractRunEnv, filesystemServer, findCriticalPath, formatContextWindowModeList, formatGoal, formatPlan, formatPlanTemplates, formatTodosList, getAgentDefinition, getContextWindowMode, getPlanTemplate, getTemplate, githubServer, goalFilePath, googleMapsServer, isAgentError, isConfigError, isContextWindowModeId, isFsError, isImageBlock, isPluginError, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, listContextWindowModes, listPlanTemplates, listTemplates, loadDirectorState, loadGoal, loadPlan, loadPlugins, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeContinueToNextIterationTool, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeRollUpTool, makeSpawnTool, makeTerminateTool, matchAny, matchGlob, migratePlaintextSecrets, miniMaxVisionServer, normalizeToLf, parseContinueDirective, parseSkillRef, pendingBtwCount, projectHash, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, repairToolUseAdjacency, resolveContextWindowPolicy, resolveWstackPaths, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, safeParse, safeStringify, sanitizeJsonString, saveGoal, savePlan, saveTodosCheckpoint, scoreAgents, securitySlashCommand, sentinelServer, setBtwNote, setPlanItemStatus, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, stripAnsi, summarizeUsage, templateToMarkdown, toStyle, toWrongStackError, topologicalSort, unifiedDiff, unloadPlugins, validateAgainstSchema, wireMetricsToEvents, wrapAsState, zaiVisionServer };
28594
+ export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, ALL_SYNC_CATEGORIES, AUDIT_LOG_AGENT, Agent, AgentError, AnnotationsStore, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutoPhasePlanner, AutoPhaseRunner, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, CONTEXT_WINDOW_MODES, CheckpointManager, CloudSync, CollaborationBus, ConfigError, ConfigMigrationError, Container, Context, ConversationState, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SPEC_TEMPLATE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultPromptStore, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DirectorStateCheckpoint, DoneConditionChecker, ERROR_CODES, EternalAutonomyEngine, EventBus, ExtensionRegistry, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FleetBus, FleetCostCapError, FleetManager, FleetSpawnBudgetError, FleetUsageAggregator, FsError, GitignoreUpdater, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, LAYER_1_IDENTITY, LLMSelector, MAX_JOURNAL_ENTRIES, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, PhaseGraphBuilder, PhaseOrchestrator, PhaseStore, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, ReplayLogStore, ReplayProviderRunner, ReportGenerator, RunController, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, ScopedEventBus, SddParallelRun, SddTaskDecomposer, SecurityScanner, SecurityScannerOrchestrator, SelectiveCompactor, SessionAnalyzer, SessionError, SessionRecovery, SkillGenerator, SkillInstaller, SkillManifestStore, SlashCommandRegistry, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TOKENS, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, TechStackDetector, ToolAuditLog, ToolError, ToolExecutor, ToolRegistry, WorktreeManager, WrongStackError, addPlanItem, allServers, analyzeCriticalPath, appendJournal, applyRosterBudget, asBlocks, asText, assertSafePath, atomicWrite, attachAutoExtend, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildBtwBlock, buildChildEnv, buildGoalPreamble, buildOtlpMetricsRequest, buildOtlpTracesRequest, buildRecoveryStrategies, classifyFamily, clearPlan, collabInjectMiddleware, collabPauseMiddleware, color, compileGlob, compileUserRegex, completePartialObject, composeDirectorPrompt, composeSubagentPrompt, computeTaskProgress, consumeBtwNotes, context7Server, contextManagerTool, createAutoExecutor, createAutoPhaseFromTaskGraph, createContextManagerTool, createDefaultPipelines, createDelegateTool, createGitPlugin, createMcpControlTool, createMessage, createObservabilityPlugin, createPlanPlugin, createPromptsPlugin, createSecurityPlugin, createSecuritySlashCommand, createSkillsPlugin, createSyncPlugin, createToolOutputSerializer, decryptConfigSecrets, defaultGitignoreUpdater, defaultOrchestrator, defaultReportGenerator, defaultSecurityScanner, defaultSkillGenerator, defaultTechStackDetector, deriveTodosFromPlanItem, detectNewlineStyle, dispatchAgent, downloadGitHubTarball, emptyGoal, emptyPlan, encryptConfigSecrets, ensureDir, estimateRequestTokens, estimateTextTokens, estimateToolDefTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, extractRunEnv, filesystemServer, findCriticalPath, formatContextWindowModeList, formatGoal, formatPlan, formatPlanTemplates, formatTodosList, getAgentDefinition, getContextWindowMode, getPlanTemplate, getTemplate, githubServer, goalFilePath, googleMapsServer, hashRequest, isAgentError, isConfigError, isContextWindowModeId, isFsError, isImageBlock, isPluginError, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, listContextWindowModes, listPlanTemplates, listTemplates, loadDirectorState, loadGoal, loadPlan, loadPlugins, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeContinueToNextIterationTool, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeRollUpTool, makeSpawnTool, makeTerminateTool, matchAny, matchGlob, migratePlaintextSecrets, miniMaxVisionServer, normalizeToLf, parseContinueDirective, parseSkillRef, pendingBtwCount, projectHash, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, repairToolUseAdjacency, resolveContextWindowPolicy, resolveWstackPaths, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, runProviderWithRetry, safeParse, safeStringify, sanitizeJsonString, saveGoal, savePlan, saveTodosCheckpoint, scoreAgents, securitySlashCommand, sentinelServer, setBtwNote, setPlanItemStatus, slackServer, stableStringify, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, stripAnsi, summarizeUsage, templateToMarkdown, toStyle, toWrongStackError, topologicalSort, unifiedDiff, unloadPlugins, validateAgainstSchema, wireMetricsToEvents, wrapAsState, zaiVisionServer };
27174
28595
  //# sourceMappingURL=index.js.map
27175
28596
  //# sourceMappingURL=index.js.map