@wrongstack/core 0.264.0 → 0.265.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/{agent-bridge-D8sa1vtv.d.ts → agent-bridge-DrkBxszZ.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-c9DLkaas.d.ts → agent-subagent-runner-DM2pP-B6.d.ts} +113 -11
  3. package/dist/{brain-O1IdKPaK.d.ts → brain-BXd_61kQ.d.ts} +31 -2
  4. package/dist/{compactor-BBy0rCtB.d.ts → compactor-B8pOf45Y.d.ts} +1 -1
  5. package/dist/{config-Dz2F3H2K.d.ts → config-BMCj_XDs.d.ts} +80 -12
  6. package/dist/{context-BGSpZNSE.d.ts → context-MRk5PhNv.d.ts} +26 -12
  7. package/dist/coordination/index.d.ts +77 -21
  8. package/dist/coordination/index.js +557 -159
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/{default-config-CXsDvOmP.d.ts → default-config-B0cj-Hry.d.ts} +11 -1
  11. package/dist/defaults/index.d.ts +28 -28
  12. package/dist/defaults/index.js +609 -195
  13. package/dist/defaults/index.js.map +1 -1
  14. package/dist/execution/index.d.ts +16 -16
  15. package/dist/execution/index.js +394 -155
  16. package/dist/execution/index.js.map +1 -1
  17. package/dist/execution/prompt-enhancer.d.ts +2 -2
  18. package/dist/execution/prompt-enhancer.js +1 -1
  19. package/dist/execution/prompt-enhancer.js.map +1 -1
  20. package/dist/extension/index.d.ts +6 -6
  21. package/dist/{goal-preamble-DzjFuN3p.d.ts → goal-preamble-DvHDSKSe.d.ts} +14 -10
  22. package/dist/{goal-store-CxWmCGbH.d.ts → goal-store-DtLMySNb.d.ts} +1 -1
  23. package/dist/{index-CYIQrXVF.d.ts → index-B-ch8K9C.d.ts} +8 -8
  24. package/dist/{index-CbLSI66_.d.ts → index-CEDeNodM.d.ts} +5 -5
  25. package/dist/index.d.ts +183 -52
  26. package/dist/index.js +1779 -673
  27. package/dist/index.js.map +1 -1
  28. package/dist/infrastructure/index.d.ts +6 -6
  29. package/dist/infrastructure/index.js +12 -8
  30. package/dist/infrastructure/index.js.map +1 -1
  31. package/dist/kernel/index.d.ts +9 -9
  32. package/dist/kernel/index.js +1 -1
  33. package/dist/kernel/index.js.map +1 -1
  34. package/dist/{llm-selector-DzxuZnNz.d.ts → llm-selector-C0tfTCUe.d.ts} +14 -2
  35. package/dist/{mcp-servers-DC4QRPUI.d.ts → mcp-servers-2x4w6Jn9.d.ts} +3 -3
  36. package/dist/models/index.d.ts +5 -5
  37. package/dist/models/index.js +74 -30
  38. package/dist/models/index.js.map +1 -1
  39. package/dist/{models-registry-B_siPxqN.d.ts → models-registry-DmJlKuNp.d.ts} +1 -1
  40. package/dist/{multi-agent-coordinator-CK5Jdj9K.d.ts → multi-agent-coordinator-DyCkCZnU.d.ts} +1 -1
  41. package/dist/{null-fleet-bus-DgvD4SCO.d.ts → null-fleet-bus-CG9QY2aP.d.ts} +6 -6
  42. package/dist/observability/index.d.ts +2 -2
  43. package/dist/{parallel-eternal-engine-bK0JQBR_.d.ts → parallel-eternal-engine-Jw9uhEoT.d.ts} +9 -9
  44. package/dist/{path-resolver-BPEDlN38.d.ts → path-resolver-Dy2ej-gE.d.ts} +3 -3
  45. package/dist/{permission-4yvGmMRB.d.ts → permission-B9SB45lp.d.ts} +1 -1
  46. package/dist/{permission-policy-C6XpsBOy.d.ts → permission-policy-CkjSXabK.d.ts} +2 -2
  47. package/dist/{pipeline-CXCeMz8J.d.ts → pipeline-DPDxH_7m.d.ts} +3 -3
  48. package/dist/{plan-templates-BvzRBkJc.d.ts → plan-templates-CzD9GnAU.d.ts} +32 -8
  49. package/dist/{provider-runner-C5aQpDWE.d.ts → provider-runner-DMa70ODu.d.ts} +3 -3
  50. package/dist/{retry-policy-CFhdtRzz.d.ts → retry-policy-CN0khdlj.d.ts} +1 -1
  51. package/dist/sdd/index.d.ts +8 -8
  52. package/dist/sdd/index.js +274 -93
  53. package/dist/sdd/index.js.map +1 -1
  54. package/dist/{secret-vault-CxiVLbt1.d.ts → secret-vault-B2yw84VT.d.ts} +43 -4
  55. package/dist/secret-vault-BAKpgFw_.d.ts +57 -0
  56. package/dist/security/index.d.ts +5 -5
  57. package/dist/security/index.js +204 -23
  58. package/dist/security/index.js.map +1 -1
  59. package/dist/{selector-gIuhRTkN.d.ts → selector-CzHh_igB.d.ts} +1 -1
  60. package/dist/{session-event-bridge-DkvvrpDt.d.ts → session-event-bridge-BUI6Jf-4.d.ts} +1 -1
  61. package/dist/{session-reader-KdfVwkKP.d.ts → session-reader-CMgdMSRP.d.ts} +1 -1
  62. package/dist/storage/index.d.ts +112 -15
  63. package/dist/storage/index.js +419 -81
  64. package/dist/storage/index.js.map +1 -1
  65. package/dist/tools/index.d.ts +2 -2
  66. package/dist/types/index.d.ts +21 -21
  67. package/dist/types/index.js +261 -53
  68. package/dist/types/index.js.map +1 -1
  69. package/dist/utils/index.d.ts +3 -3
  70. package/dist/utils/index.js +3 -5
  71. package/dist/utils/index.js.map +1 -1
  72. package/dist/{wstack-paths-CJjEwPXn.d.ts → wstack-paths-hOpNLmvf.d.ts} +2 -0
  73. package/package.json +1 -1
  74. package/skills/api-design/SKILL.md +1 -1
  75. package/skills/audit-log/SKILL.md +6 -6
  76. package/skills/bug-hunter/SKILL.md +5 -5
  77. package/skills/chimera/SKILL.md +4 -4
  78. package/skills/docker-deploy/SKILL.md +1 -1
  79. package/skills/git-flow/SKILL.md +3 -3
  80. package/skills/multi-agent/SKILL.md +3 -3
  81. package/skills/node-modern/SKILL.md +1 -0
  82. package/skills/observability/SKILL.md +2 -2
  83. package/skills/output-standards/SKILL.md +51 -28
  84. package/skills/refactor-planner/SKILL.md +3 -3
  85. package/skills/security-scanner/SKILL.md +4 -3
  86. package/skills/tech-stack/SKILL.md +1 -2
  87. package/dist/secret-vault-BJDY28ev.d.ts +0 -25
package/dist/index.js CHANGED
@@ -59,8 +59,8 @@ async function atomicWrite(targetPath, content, opts = {}) {
59
59
  }
60
60
  let mode;
61
61
  try {
62
- const stat13 = await fsp3.stat(targetPath);
63
- mode = stat13.mode & 511;
62
+ const stat14 = await fsp3.stat(targetPath);
63
+ mode = stat14.mode & 511;
64
64
  } catch {
65
65
  mode = opts.mode;
66
66
  }
@@ -100,8 +100,8 @@ async function withFileLock(targetPath, fn, opts = {}) {
100
100
  }
101
101
  if (code !== "EEXIST") throw err;
102
102
  try {
103
- const stat13 = await fsp3.stat(lockPath);
104
- if (Date.now() - stat13.mtimeMs > staleMs) {
103
+ const stat14 = await fsp3.stat(lockPath);
104
+ if (Date.now() - stat14.mtimeMs > staleMs) {
105
105
  await fsp3.unlink(lockPath);
106
106
  continue;
107
107
  }
@@ -111,7 +111,7 @@ async function withFileLock(targetPath, fn, opts = {}) {
111
111
  if (Date.now() - started >= timeoutMs) {
112
112
  throw new Error(`Timed out waiting for file lock: ${targetPath}`);
113
113
  }
114
- await new Promise((resolve14) => setTimeout(resolve14, 25));
114
+ await new Promise((resolve15) => setTimeout(resolve15, 25));
115
115
  }
116
116
  }
117
117
  try {
@@ -144,7 +144,7 @@ async function renameWithRetry(from, to) {
144
144
  if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
145
145
  throw err;
146
146
  }
147
- await new Promise((resolve14) => setTimeout(resolve14, delays[i]));
147
+ await new Promise((resolve15) => setTimeout(resolve15, delays[i]));
148
148
  }
149
149
  }
150
150
  throw lastErr;
@@ -183,16 +183,24 @@ function getSessionRegistry(globalRoot) {
183
183
  function hasSessionRegistry() {
184
184
  return _instance !== null;
185
185
  }
186
- var REGISTRY_FILE, HEARTBEAT_INTERVAL_MS, STALE_TIMEOUT_MS, SessionRegistry, _instance;
186
+ var REGISTRY_FILE, HEARTBEAT_INTERVAL_MS, STALE_TIMEOUT_MS, CLOSING_GRACE_MS, STALE_LOCK_MS, SessionRegistry, _instance;
187
187
  var init_session_registry = __esm({
188
188
  "src/session-registry.ts"() {
189
189
  REGISTRY_FILE = "session-registry.json";
190
190
  HEARTBEAT_INTERVAL_MS = 5e3;
191
191
  STALE_TIMEOUT_MS = 3e4;
192
+ CLOSING_GRACE_MS = 15e3;
193
+ STALE_LOCK_MS = 1e4;
192
194
  SessionRegistry = class {
193
195
  filePath;
194
196
  heartbeatTimer = null;
195
197
  currentSessionId = null;
198
+ /**
199
+ * Last full entry this process registered. Kept so the heartbeat can
200
+ * re-create our entry if it ever goes missing — e.g. our initial register()
201
+ * write was dropped (a wedged lock), the file was reset, or we were pruned.
202
+ */
203
+ lastEntry = null;
196
204
  constructor(globalRoot) {
197
205
  this.filePath = path2.join(globalRoot, REGISTRY_FILE);
198
206
  }
@@ -214,6 +222,7 @@ var init_session_registry = __esm({
214
222
  agentCount: entry.agents?.length ?? 0,
215
223
  agents: entry.agents ?? []
216
224
  };
225
+ this.lastEntry = full;
217
226
  await this.atomicUpdate((registry) => {
218
227
  const now = Date.now();
219
228
  for (const [id, existing] of Object.entries(registry)) {
@@ -239,16 +248,28 @@ var init_session_registry = __esm({
239
248
  */
240
249
  async updateAgents(agents) {
241
250
  if (!this.currentSessionId) return;
251
+ const hasRunning = agents.some((a) => a.status === "running" || a.status === "streaming");
252
+ const hasWaiting = agents.some((a) => a.status === "waiting_user");
253
+ const hasError = agents.some((a) => a.status === "error");
254
+ const status = hasRunning || hasWaiting || hasError ? "active" : "idle";
255
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
256
+ if (this.lastEntry) {
257
+ this.lastEntry.agents = agents;
258
+ this.lastEntry.agentCount = agents.length;
259
+ this.lastEntry.status = status;
260
+ this.lastEntry.lastHeartbeatAt = nowIso;
261
+ }
242
262
  await this.atomicUpdate((registry) => {
243
- const entry = registry[this.currentSessionId];
244
- if (!entry) return;
263
+ let entry = registry[this.currentSessionId];
264
+ if (!entry) {
265
+ if (!this.lastEntry) return;
266
+ entry = { ...this.lastEntry };
267
+ registry[this.currentSessionId] = entry;
268
+ }
245
269
  entry.agents = agents;
246
270
  entry.agentCount = agents.length;
247
- const hasRunning = agents.some((a) => a.status === "running" || a.status === "streaming");
248
- const hasWaiting = agents.some((a) => a.status === "waiting_user");
249
- const hasError = agents.some((a) => a.status === "error");
250
- entry.status = hasRunning ? "active" : hasWaiting ? "active" : hasError ? "active" : "idle";
251
- entry.lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
271
+ entry.status = status;
272
+ entry.lastHeartbeatAt = nowIso;
252
273
  });
253
274
  }
254
275
  /**
@@ -326,6 +347,12 @@ var init_session_registry = __esm({
326
347
  entry.status = hasRunning ? "active" : "idle";
327
348
  }
328
349
  await this.writeAtomic(registry);
350
+ } else if (this.lastEntry) {
351
+ await this.atomicUpdate((reg) => {
352
+ if (!reg[this.currentSessionId] && this.lastEntry) {
353
+ reg[this.currentSessionId] = { ...this.lastEntry, lastHeartbeatAt: (/* @__PURE__ */ new Date()).toISOString() };
354
+ }
355
+ });
329
356
  }
330
357
  } catch {
331
358
  }
@@ -338,6 +365,11 @@ var init_session_registry = __esm({
338
365
  let pruned = false;
339
366
  for (const [id, entry] of Object.entries(registry)) {
340
367
  const heartbeatAge = now - new Date(entry.lastHeartbeatAt).getTime();
368
+ if (entry.status === "closing" && heartbeatAge > CLOSING_GRACE_MS) {
369
+ delete registry[id];
370
+ pruned = true;
371
+ continue;
372
+ }
341
373
  if (heartbeatAge > STALE_TIMEOUT_MS && !pidAlive(entry.pid)) {
342
374
  entry.status = "stale";
343
375
  const startedAge = now - new Date(entry.startedAt).getTime();
@@ -357,17 +389,23 @@ var init_session_registry = __esm({
357
389
  }
358
390
  async atomicUpdate(fn) {
359
391
  const lockPath = `${this.filePath}.lock`;
360
- const maxRetries = 5;
392
+ const maxRetries = 8;
361
393
  const retryDelayMs = 20;
362
394
  for (let attempt = 0; attempt < maxRetries; attempt++) {
363
395
  try {
364
396
  await fsp3.mkdir(path2.dirname(this.filePath), { recursive: true });
365
- const lockHandle = await fsp3.open(lockPath, "wx").catch(() => null);
397
+ let lockHandle = await fsp3.open(lockPath, "wx").catch(() => null);
366
398
  if (!lockHandle) {
367
- await new Promise((r) => setTimeout(r, retryDelayMs * (attempt + 1)));
368
- continue;
399
+ if (await this.breakStaleLock(lockPath)) {
400
+ lockHandle = await fsp3.open(lockPath, "wx").catch(() => null);
401
+ }
402
+ if (!lockHandle) {
403
+ await new Promise((r) => setTimeout(r, retryDelayMs * (attempt + 1)));
404
+ continue;
405
+ }
369
406
  }
370
407
  try {
408
+ await lockHandle.writeFile(String(process.pid)).catch(() => void 0);
371
409
  const raw = await fsp3.readFile(this.filePath, "utf8").catch(() => "{}");
372
410
  const registry = JSON.parse(raw);
373
411
  fn(registry);
@@ -382,6 +420,31 @@ var init_session_registry = __esm({
382
420
  }
383
421
  }
384
422
  }
423
+ /**
424
+ * Break a contended lock if it is stale: the recorded owner pid is no longer
425
+ * alive, or the lock is older than {@link STALE_LOCK_MS}. Returns true when the
426
+ * lock was removed (caller should retry acquisition). Best-effort and
427
+ * race-tolerant — a fresh lock (age ~0, live owner) is never broken, so the
428
+ * common concurrent case self-heals on the next heartbeat.
429
+ */
430
+ async breakStaleLock(lockPath) {
431
+ try {
432
+ const [stat14, content] = await Promise.all([
433
+ fsp3.stat(lockPath),
434
+ fsp3.readFile(lockPath, "utf8").catch(() => "")
435
+ ]);
436
+ const ageMs = Date.now() - stat14.mtimeMs;
437
+ const ownerPid = Number.parseInt(content.trim(), 10);
438
+ const ownerDead = Number.isInteger(ownerPid) && ownerPid > 0 && ownerPid !== process.pid && !pidAlive(ownerPid);
439
+ if (ownerDead || ageMs > STALE_LOCK_MS) {
440
+ await fsp3.unlink(lockPath).catch(() => void 0);
441
+ return true;
442
+ }
443
+ return false;
444
+ } catch {
445
+ return true;
446
+ }
447
+ }
385
448
  async writeAtomicLocked(registry) {
386
449
  const tmp = `${this.filePath}.${randomUUID().slice(0, 8)}.tmp`;
387
450
  await fsp3.writeFile(tmp, JSON.stringify(registry, null, 2), "utf8");
@@ -1061,7 +1124,8 @@ function resolveWstackPaths(opts) {
1061
1124
  projectSddSession: path2.join(projectDir, "sdd-session.json"),
1062
1125
  projectPlan: path2.join(projectDir, "plan.json"),
1063
1126
  projectAutophase: path2.join(projectDir, "autophase"),
1064
- syncConfig: path2.join(globalRoot, "sync.json")
1127
+ syncConfig: path2.join(globalRoot, "sync.json"),
1128
+ projectStatus: (projectHash2) => path2.join(globalRoot, "projects", projectHash2, "status.json")
1065
1129
  };
1066
1130
  }
1067
1131
 
@@ -1162,7 +1226,7 @@ function buildChildEnv(optsOrSessionId) {
1162
1226
 
1163
1227
  // src/utils/sleep.ts
1164
1228
  function sleep(ms) {
1165
- return new Promise((resolve14) => setTimeout(resolve14, ms));
1229
+ return new Promise((resolve15) => setTimeout(resolve15, ms));
1166
1230
  }
1167
1231
 
1168
1232
  // src/utils/assert-never.ts
@@ -1320,12 +1384,9 @@ function getCachedEstimate(key, compute) {
1320
1384
  const existing = ESTIMATE_CACHE.get(key);
1321
1385
  if (existing !== void 0) return existing;
1322
1386
  if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
1323
- let evicted = 0;
1324
- const maxEvict = Math.floor(ESTIMATE_CACHE_MAX_SIZE / 4);
1325
1387
  for (const k of ESTIMATE_CACHE.keys()) {
1326
- if (evicted >= maxEvict) break;
1388
+ if (ESTIMATE_CACHE.size <= Math.floor(ESTIMATE_CACHE_MAX_SIZE / 2)) break;
1327
1389
  ESTIMATE_CACHE.delete(k);
1328
- evicted++;
1329
1390
  }
1330
1391
  }
1331
1392
  const estimate = compute(key);
@@ -1567,11 +1628,11 @@ function validateAgainstSchema(value, schema) {
1567
1628
  walk(value, schema, "", errors);
1568
1629
  return { ok: errors.length === 0, errors };
1569
1630
  }
1570
- function walk(value, schema, path43, errors) {
1631
+ function walk(value, schema, path44, errors) {
1571
1632
  if (schema.enum !== void 0) {
1572
1633
  if (!schema.enum.some((e) => deepEqual(e, value))) {
1573
1634
  errors.push({
1574
- path: path43 || "<root>",
1635
+ path: path44 || "<root>",
1575
1636
  message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
1576
1637
  });
1577
1638
  return;
@@ -1580,7 +1641,7 @@ function walk(value, schema, path43, errors) {
1580
1641
  if (typeof schema.type === "string") {
1581
1642
  if (!checkType(value, schema.type)) {
1582
1643
  errors.push({
1583
- path: path43 || "<root>",
1644
+ path: path44 || "<root>",
1584
1645
  message: `expected ${schema.type}, got ${describeType(value)}`
1585
1646
  });
1586
1647
  return;
@@ -1590,20 +1651,20 @@ function walk(value, schema, path43, errors) {
1590
1651
  const obj = value;
1591
1652
  for (const req of schema.required ?? []) {
1592
1653
  if (!(req in obj)) {
1593
- errors.push({ path: joinPath(path43, req), message: "required property missing" });
1654
+ errors.push({ path: joinPath(path44, req), message: "required property missing" });
1594
1655
  }
1595
1656
  }
1596
1657
  if (schema.properties) {
1597
1658
  for (const [key, subSchema] of Object.entries(schema.properties)) {
1598
1659
  if (key in obj) {
1599
- walk(obj[key], subSchema, joinPath(path43, key), errors);
1660
+ walk(obj[key], subSchema, joinPath(path44, key), errors);
1600
1661
  }
1601
1662
  }
1602
1663
  }
1603
1664
  }
1604
1665
  if (schema.type === "array" && Array.isArray(value) && schema.items) {
1605
1666
  for (let i = 0; i < value.length; i++) {
1606
- walk(value[i], schema.items, `${path43}[${i}]`, errors);
1667
+ walk(value[i], schema.items, `${path44}[${i}]`, errors);
1607
1668
  }
1608
1669
  }
1609
1670
  }
@@ -1779,8 +1840,8 @@ async function expandGlob(pattern) {
1779
1840
  for (const e of entries) {
1780
1841
  const full = `${dir}${SEP}${e}`;
1781
1842
  try {
1782
- const stat13 = await fsp3.stat(full);
1783
- if (stat13.isDirectory()) await walk3(full, rest);
1843
+ const stat14 = await fsp3.stat(full);
1844
+ if (stat14.isDirectory()) await walk3(full, rest);
1784
1845
  } catch {
1785
1846
  }
1786
1847
  }
@@ -1797,8 +1858,8 @@ async function expandGlob(pattern) {
1797
1858
  if (entries.includes(seg)) {
1798
1859
  const full = `${dir}${SEP}${seg}`;
1799
1860
  try {
1800
- const stat13 = await fsp3.stat(full);
1801
- if (stat13.isDirectory()) await walk3(full, rest);
1861
+ const stat14 = await fsp3.stat(full);
1862
+ if (stat14.isDirectory()) await walk3(full, rest);
1802
1863
  } catch {
1803
1864
  }
1804
1865
  }
@@ -2870,7 +2931,7 @@ function makePatternMatcher(pattern) {
2870
2931
  }
2871
2932
 
2872
2933
  // src/kernel/tokens.ts
2873
- var t = (name) => Symbol(name);
2934
+ var t = (name) => /* @__PURE__ */ Symbol.for(`@wrongstack/core/kernel#${name}`);
2874
2935
  var TOKENS = {
2875
2936
  Logger: t("Logger"),
2876
2937
  TokenCounter: t("TokenCounter"),
@@ -3109,6 +3170,20 @@ function providerStatusToCode(status, type) {
3109
3170
  return ERROR_CODES.PROVIDER_INVALID_REQUEST;
3110
3171
  }
3111
3172
 
3173
+ // src/types/config.ts
3174
+ function normalizeTokenSavingTier(val) {
3175
+ if (val === void 0) return "off";
3176
+ if (typeof val === "boolean") return val ? "medium" : "off";
3177
+ const validTiers = /* @__PURE__ */ new Set([
3178
+ "off",
3179
+ "minimal",
3180
+ "light",
3181
+ "medium",
3182
+ "aggressive"
3183
+ ]);
3184
+ return validTiers.has(val) ? val : "off";
3185
+ }
3186
+
3112
3187
  // src/types/default-config.ts
3113
3188
  var DEFAULT_TOOLS_CONFIG = Object.freeze({
3114
3189
  defaultExecutionStrategy: "smart",
@@ -3116,7 +3191,8 @@ var DEFAULT_TOOLS_CONFIG = Object.freeze({
3116
3191
  iterationTimeoutMs: 3e5,
3117
3192
  sessionTimeoutMs: 18e5,
3118
3193
  perIterationOutputCapBytes: 1e5,
3119
- autoExtendLimit: true
3194
+ autoExtendLimit: true,
3195
+ restrictToProjectRoot: false
3120
3196
  });
3121
3197
  var DEFAULT_CONTEXT_CONFIG = Object.freeze({
3122
3198
  preserveK: 10,
@@ -3125,6 +3201,10 @@ var DEFAULT_CONTEXT_CONFIG = Object.freeze({
3125
3201
  var DEFAULT_AUTONOMY_CONFIG = Object.freeze({
3126
3202
  autoProceedDelayMs: 45e3
3127
3203
  });
3204
+ var DEFAULT_CIRCUIT_BREAKER_CONFIG = Object.freeze({
3205
+ enabled: false,
3206
+ autoKillResetMs: 6e4
3207
+ });
3128
3208
  var DEFAULT_SESSION_LOGGING_CONFIG = Object.freeze({
3129
3209
  auditLevel: "standard",
3130
3210
  sampling: {
@@ -3136,11 +3216,19 @@ var DEFAULT_SESSION_LOGGING_CONFIG = Object.freeze({
3136
3216
  var DEFAULT_SESSION_PRUNE_DAYS = 30;
3137
3217
 
3138
3218
  // src/types/secret-vault.ts
3139
- var ENCRYPTED_PREFIX = "enc:v1:";
3219
+ var ENCRYPTED_PREFIX_PATTERN = /^enc:v(\d+):/;
3220
+ function encryptedPrefixForVersion(version) {
3221
+ return `enc:v${version}:`;
3222
+ }
3223
+ function parseEncryptedVersion(value) {
3224
+ const match = value.match(ENCRYPTED_PREFIX_PATTERN);
3225
+ return match ? Number.parseInt(match[1], 10) : void 0;
3226
+ }
3140
3227
  var noOpVault = {
3141
3228
  encrypt: (v) => v,
3142
3229
  decrypt: (v) => v,
3143
- isEncrypted: () => false
3230
+ isEncrypted: () => false,
3231
+ keyVersion: 1
3144
3232
  };
3145
3233
 
3146
3234
  // src/security/secret-vault.ts
@@ -3150,11 +3238,13 @@ var IV_BYTES = 12;
3150
3238
  var TAG_BYTES = 16;
3151
3239
  var ALGO = "aes-256-gcm";
3152
3240
  var KEY_FILE_MODE = 384;
3241
+ var KEY_FILE_MAGIC = Buffer.from("WSKV", "ascii");
3242
+ var VERSIONED_KEY_FILE_SIZE = KEY_FILE_MAGIC.length + 1 + KEY_BYTES;
3153
3243
  function checkKeyFilePermissions(keyFile) {
3154
3244
  if (process.platform === "win32") return;
3155
3245
  try {
3156
- const stat13 = fs2.statSync(keyFile);
3157
- const actualMode = stat13.mode & 511;
3246
+ const stat14 = fs2.statSync(keyFile);
3247
+ const actualMode = stat14.mode & 511;
3158
3248
  if (actualMode !== KEY_FILE_MODE) {
3159
3249
  console.warn(JSON.stringify({
3160
3250
  level: "warn",
@@ -3172,11 +3262,17 @@ function checkKeyFilePermissions(keyFile) {
3172
3262
  var DefaultSecretVault = class {
3173
3263
  keyFile;
3174
3264
  key;
3265
+ _keyVersion = 1;
3175
3266
  constructor(opts) {
3176
3267
  this.keyFile = opts.keyFile;
3177
3268
  }
3269
+ /** Current key version. Starts at 1; incremented by rotateKey(). */
3270
+ get keyVersion() {
3271
+ if (!this.key) this.loadOrCreateKey();
3272
+ return this._keyVersion;
3273
+ }
3178
3274
  isEncrypted(value) {
3179
- return typeof value === "string" && value.startsWith(ENCRYPTED_PREFIX);
3275
+ return typeof value === "string" && ENCRYPTED_PREFIX_PATTERN.test(value);
3180
3276
  }
3181
3277
  encrypt(plaintext) {
3182
3278
  if (this.isEncrypted(plaintext)) return plaintext;
@@ -3185,11 +3281,20 @@ var DefaultSecretVault = class {
3185
3281
  const cipher = createCipheriv(ALGO, key, iv);
3186
3282
  const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
3187
3283
  const tag = cipher.getAuthTag();
3188
- return `${ENCRYPTED_PREFIX}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
3284
+ const prefix = encryptedPrefixForVersion(this._keyVersion);
3285
+ return `${prefix}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
3189
3286
  }
3190
3287
  decrypt(value) {
3191
3288
  if (!this.isEncrypted(value)) return value;
3192
- const rest = value.slice(ENCRYPTED_PREFIX.length);
3289
+ const prefixMatch = value.match(ENCRYPTED_PREFIX_PATTERN);
3290
+ if (!prefixMatch) {
3291
+ throw new ConfigError({
3292
+ message: "SecretVault: malformed encrypted value",
3293
+ code: ERROR_CODES.CONFIG_PARSE_FAILED,
3294
+ context: { field: "encrypted_value" }
3295
+ });
3296
+ }
3297
+ const rest = value.slice(prefixMatch[0].length);
3193
3298
  const parts = rest.split(":");
3194
3299
  if (parts.length !== 3) {
3195
3300
  throw new ConfigError({
@@ -3218,20 +3323,64 @@ var DefaultSecretVault = class {
3218
3323
  const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
3219
3324
  return pt.toString("utf8");
3220
3325
  }
3326
+ /**
3327
+ * Generate a new encryption key, write it to disk, and increment the key version.
3328
+ * After rotation, encrypt() emits the new version prefix (e.g. enc:v2:).
3329
+ * The caller must re-encrypt existing config values (see rotateConfigKeys()).
3330
+ */
3331
+ rotateKey() {
3332
+ const oldVersion = this._keyVersion;
3333
+ const newKey = randomBytes(KEY_BYTES);
3334
+ const newVersion = oldVersion + 1;
3335
+ const keyFileBuf = Buffer.alloc(VERSIONED_KEY_FILE_SIZE);
3336
+ KEY_FILE_MAGIC.copy(keyFileBuf, 0);
3337
+ keyFileBuf[KEY_FILE_MAGIC.length] = newVersion;
3338
+ newKey.copy(keyFileBuf, KEY_FILE_MAGIC.length + 1);
3339
+ fs2.mkdirSync(path2.dirname(this.keyFile), { recursive: true });
3340
+ fs2.writeFileSync(this.keyFile, keyFileBuf, { mode: 384 });
3341
+ checkKeyFilePermissions(this.keyFile);
3342
+ this.key = newKey;
3343
+ this._keyVersion = newVersion;
3344
+ return { oldVersion, newVersion };
3345
+ }
3221
3346
  loadOrCreateKey() {
3222
3347
  if (this.key) return this.key;
3223
3348
  try {
3224
3349
  const buf = fs2.readFileSync(this.keyFile);
3225
- if (buf.length !== KEY_BYTES) {
3226
- throw new ConfigError({
3227
- message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`,
3228
- code: ERROR_CODES.CONFIG_INVALID,
3229
- context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
3230
- });
3350
+ if (buf.length === KEY_BYTES) {
3351
+ this.key = buf;
3352
+ this._keyVersion = 1;
3353
+ checkKeyFilePermissions(this.keyFile);
3354
+ return this.key;
3355
+ }
3356
+ if (buf.length === VERSIONED_KEY_FILE_SIZE) {
3357
+ const magic = buf.subarray(0, KEY_FILE_MAGIC.length);
3358
+ if (!magic.equals(KEY_FILE_MAGIC)) {
3359
+ throw new ConfigError({
3360
+ message: `SecretVault: key file ${this.keyFile} has invalid magic header`,
3361
+ code: ERROR_CODES.CONFIG_INVALID,
3362
+ context: { keyFile: this.keyFile }
3363
+ });
3364
+ }
3365
+ const version = buf[KEY_FILE_MAGIC.length];
3366
+ const key2 = buf.subarray(KEY_FILE_MAGIC.length + 1);
3367
+ if (key2.length !== KEY_BYTES) {
3368
+ throw new ConfigError({
3369
+ message: `SecretVault: key file ${this.keyFile} has wrong key size (${key2.length} bytes, expected ${KEY_BYTES})`,
3370
+ code: ERROR_CODES.CONFIG_INVALID,
3371
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: key2.length }
3372
+ });
3373
+ }
3374
+ this.key = Buffer.from(key2);
3375
+ this._keyVersion = version;
3376
+ checkKeyFilePermissions(this.keyFile);
3377
+ return this.key;
3231
3378
  }
3232
- this.key = buf;
3233
- checkKeyFilePermissions(this.keyFile);
3234
- return this.key;
3379
+ throw new ConfigError({
3380
+ message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES} for v1 or ${VERSIONED_KEY_FILE_SIZE} for v2+). Remove it manually to generate a new key.`,
3381
+ code: ERROR_CODES.CONFIG_INVALID,
3382
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
3383
+ });
3235
3384
  } catch (err) {
3236
3385
  if (err.code !== "ENOENT") throw err;
3237
3386
  }
@@ -3242,18 +3391,36 @@ var DefaultSecretVault = class {
3242
3391
  } catch (err) {
3243
3392
  if (err.code !== "EEXIST") throw err;
3244
3393
  const buf = fs2.readFileSync(this.keyFile);
3245
- if (buf.length !== KEY_BYTES) {
3246
- throw new ConfigError({
3247
- message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`,
3248
- code: ERROR_CODES.CONFIG_INVALID,
3249
- context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
3250
- });
3394
+ if (buf.length === KEY_BYTES) {
3395
+ this.key = buf;
3396
+ this._keyVersion = 1;
3397
+ checkKeyFilePermissions(this.keyFile);
3398
+ return this.key;
3399
+ }
3400
+ if (buf.length === VERSIONED_KEY_FILE_SIZE) {
3401
+ const magic = buf.subarray(0, KEY_FILE_MAGIC.length);
3402
+ if (!magic.equals(KEY_FILE_MAGIC)) {
3403
+ throw new ConfigError({
3404
+ message: `SecretVault: key file ${this.keyFile} has invalid magic header`,
3405
+ code: ERROR_CODES.CONFIG_INVALID,
3406
+ context: { keyFile: this.keyFile }
3407
+ });
3408
+ }
3409
+ const version = buf[KEY_FILE_MAGIC.length];
3410
+ const winnerKey = buf.subarray(KEY_FILE_MAGIC.length + 1);
3411
+ this.key = Buffer.from(winnerKey);
3412
+ this._keyVersion = version;
3413
+ checkKeyFilePermissions(this.keyFile);
3414
+ return this.key;
3251
3415
  }
3252
- this.key = buf;
3253
- checkKeyFilePermissions(this.keyFile);
3254
- return this.key;
3416
+ throw new ConfigError({
3417
+ message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES} for v1 or ${VERSIONED_KEY_FILE_SIZE} for v2+). Remove it manually to generate a new key.`,
3418
+ code: ERROR_CODES.CONFIG_INVALID,
3419
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
3420
+ });
3255
3421
  }
3256
3422
  this.key = key;
3423
+ this._keyVersion = 1;
3257
3424
  return key;
3258
3425
  }
3259
3426
  };
@@ -3334,6 +3501,80 @@ async function migratePlaintextSecrets(configPath, vault, logger) {
3334
3501
  );
3335
3502
  return { migrated: counter.n, file: configPath };
3336
3503
  }
3504
+ async function rotateConfigKeys(configPath, vault, logger) {
3505
+ const log = logger?.info ?? (() => {
3506
+ });
3507
+ const warn = logger?.warn ?? ((msg) => console.warn(msg));
3508
+ let raw;
3509
+ try {
3510
+ raw = await fsp3.readFile(configPath, "utf8");
3511
+ } catch {
3512
+ const { oldVersion: oldVersion2, newVersion: newVersion2 } = vault.rotateKey();
3513
+ log(`[secret-vault] Key rotated (v${oldVersion2} \u2192 v${newVersion2}) \u2014 no config file to re-encrypt`);
3514
+ return { rotated: 0, oldVersion: oldVersion2, newVersion: newVersion2, file: configPath };
3515
+ }
3516
+ let parsed;
3517
+ try {
3518
+ parsed = JSON.parse(raw);
3519
+ } catch {
3520
+ warn(`[secret-vault] Config file ${configPath} is not valid JSON \u2014 skipping rotation`);
3521
+ return { rotated: 0, oldVersion: vault.keyVersion, newVersion: vault.keyVersion, file: configPath };
3522
+ }
3523
+ const counter = { n: 0 };
3524
+ const decrypted = walkDecryptCount(parsed, vault, counter);
3525
+ if (counter.n === 0) {
3526
+ const { oldVersion: oldVersion2, newVersion: newVersion2 } = vault.rotateKey();
3527
+ log(`[secret-vault] Key rotated (v${oldVersion2} \u2192 v${newVersion2}) \u2014 no encrypted fields to re-encrypt`);
3528
+ return { rotated: 0, oldVersion: oldVersion2, newVersion: newVersion2, file: configPath };
3529
+ }
3530
+ const { oldVersion, newVersion } = vault.rotateKey();
3531
+ const reencrypted = walkReencrypt(decrypted, vault);
3532
+ await atomicWrite(configPath, JSON.stringify(reencrypted, null, 2), { mode: 384 });
3533
+ await restrictFilePermissions(configPath, { warn });
3534
+ log(`[secret-vault] Key rotated (v${oldVersion} \u2192 v${newVersion}) \u2014 re-encrypted ${counter.n} field(s)`);
3535
+ return { rotated: counter.n, oldVersion, newVersion, file: configPath };
3536
+ }
3537
+ function walkDecryptCount(node, vault, counter) {
3538
+ if (node === null || node === void 0) return node;
3539
+ if (typeof node !== "object") return node;
3540
+ if (Array.isArray(node)) {
3541
+ return node.map((item) => walkDecryptCount(item, vault, counter));
3542
+ }
3543
+ const out = /* @__PURE__ */ Object.create(null);
3544
+ for (const [k, v] of Object.entries(node)) {
3545
+ if (typeof v === "string" && vault.isEncrypted(v)) {
3546
+ try {
3547
+ out[k] = vault.decrypt(v);
3548
+ counter.n++;
3549
+ } catch {
3550
+ out[k] = v;
3551
+ }
3552
+ } else if (typeof v === "object" && v !== null) {
3553
+ out[k] = walkDecryptCount(v, vault, counter);
3554
+ } else {
3555
+ out[k] = v;
3556
+ }
3557
+ }
3558
+ return out;
3559
+ }
3560
+ function walkReencrypt(node, vault) {
3561
+ if (node === null || node === void 0) return node;
3562
+ if (typeof node !== "object") return node;
3563
+ if (Array.isArray(node)) {
3564
+ return node.map((item) => walkReencrypt(item, vault));
3565
+ }
3566
+ const out = /* @__PURE__ */ Object.create(null);
3567
+ for (const [k, v] of Object.entries(node)) {
3568
+ if (typeof v === "string" && isSecretField(k) && v.length > 0 && !vault.isEncrypted(v)) {
3569
+ out[k] = vault.encrypt(v);
3570
+ } else if (typeof v === "object" && v !== null) {
3571
+ out[k] = walkReencrypt(v, vault);
3572
+ } else {
3573
+ out[k] = v;
3574
+ }
3575
+ }
3576
+ return out;
3577
+ }
3337
3578
  async function restrictFilePermissions(filePath, opts) {
3338
3579
  const warn = opts?.warn ?? ((msg) => console.warn(msg));
3339
3580
  if (process.platform === "win32") {
@@ -3684,7 +3925,11 @@ var MEMORY_TYPE_LABELS = {
3684
3925
  };
3685
3926
 
3686
3927
  // src/execution/compaction-core.ts
3928
+ function compactionDebugEnabled() {
3929
+ return process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1";
3930
+ }
3687
3931
  function emitCompactionMetrics(event, metrics) {
3932
+ if (!compactionDebugEnabled()) return;
3688
3933
  console.log(
3689
3934
  JSON.stringify({
3690
3935
  level: "debug",
@@ -3739,18 +3984,20 @@ function findPreserveStart(messages, preserveK) {
3739
3984
  }
3740
3985
  }
3741
3986
  }
3742
- console.log(
3743
- JSON.stringify({
3744
- level: "debug",
3745
- event: "compaction.find_preserve_start.ended",
3746
- messageCount: messages.length,
3747
- preserveK,
3748
- preserveStart,
3749
- forwardWalkIterations,
3750
- forwardWalkInnerIterations,
3751
- forwardWalkInnerPerOuter: forwardWalkIterations > 0 ? forwardWalkInnerIterations / forwardWalkIterations : 0
3752
- })
3753
- );
3987
+ if (compactionDebugEnabled()) {
3988
+ console.log(
3989
+ JSON.stringify({
3990
+ level: "debug",
3991
+ event: "compaction.find_preserve_start.ended",
3992
+ messageCount: messages.length,
3993
+ preserveK,
3994
+ preserveStart,
3995
+ forwardWalkIterations,
3996
+ forwardWalkInnerIterations,
3997
+ forwardWalkInnerPerOuter: forwardWalkIterations > 0 ? forwardWalkInnerIterations / forwardWalkIterations : 0
3998
+ })
3999
+ );
4000
+ }
3754
4001
  return preserveStart;
3755
4002
  }
3756
4003
  function eliseOldToolResults(messages, opts) {
@@ -3817,7 +4064,7 @@ function eliseOldToolResults(messages, opts) {
3817
4064
  changed = true;
3818
4065
  }
3819
4066
  fullPassInnerIterations += original.length;
3820
- if (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1") {
4067
+ if (compactionDebugEnabled()) {
3821
4068
  const ratio = fullPassInnerIterations / fullPassIterations;
3822
4069
  if (ratio > 10) {
3823
4070
  console.error(
@@ -4374,6 +4621,27 @@ var PATTERNS = [
4374
4621
  { type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g },
4375
4622
  { type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g },
4376
4623
  { type: "redis_uri", regex: /redis:\/\/[^\s"'`]+/g },
4624
+ // AI/ML provider keys — modern LLM services with well-known prefixes
4625
+ {
4626
+ type: "huggingface_token",
4627
+ // HuggingFace tokens: hf_ followed by 34 alphanumeric chars
4628
+ regex: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{34}(?![A-Za-z0-9])/g
4629
+ },
4630
+ {
4631
+ type: "replicate_token",
4632
+ // Replicate tokens: r8_ followed by 40+ alphanumeric chars
4633
+ regex: /(?<![A-Za-z0-9])r8_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
4634
+ },
4635
+ {
4636
+ type: "perplexity_key",
4637
+ // Perplexity API keys: pplx- followed by 40+ alphanumeric chars
4638
+ regex: /(?<![A-Za-z0-9])pplx-[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
4639
+ },
4640
+ {
4641
+ type: "groq_key",
4642
+ // Groq API keys: gsk_ followed by 40+ alphanumeric chars
4643
+ regex: /(?<![A-Za-z0-9])gsk_[A-Za-z0-9]{40,}(?![A-Za-z0-9])/g
4644
+ },
4377
4645
  {
4378
4646
  type: "bearer_token",
4379
4647
  // Anchored with alternation instead of negative lookahead — avoids V8
@@ -4407,6 +4675,10 @@ function hasCredentialAnchors(text) {
4407
4675
  text.includes("xox") || // Slack token (xoxa/xoxb/xoxp/xoxo/xoxs)
4408
4676
  text.includes("Bearer ") || // Bearer token (space suffix reduces false positives)
4409
4677
  text.includes("/bot") || // Telegram bot token (URL path pattern)
4678
+ text.includes("hf_") || // HuggingFace token
4679
+ text.includes("r8_") || // Replicate token
4680
+ text.includes("pplx-") || // Perplexity API key
4681
+ text.includes("gsk_") || // Groq API key
4410
4682
  text.includes("_KEY=") || // High-entropy env vars: API_KEY=, SECRET_KEY=, ...
4411
4683
  text.includes("_TOKEN=") || // ACCESS_TOKEN=, AUTH_TOKEN=, ...
4412
4684
  text.includes("_SECRET=") || // API_SECRET=, CLIENT_SECRET=, ...
@@ -5243,7 +5515,7 @@ var InMemoryAgentBridge = class {
5243
5515
  });
5244
5516
  }
5245
5517
  this.inflightGuards.add(correlationId);
5246
- return new Promise((resolve14, reject) => {
5518
+ return new Promise((resolve15, reject) => {
5247
5519
  const timer = setTimeout(() => {
5248
5520
  this.inflightGuards.delete(correlationId);
5249
5521
  this.pendingRequests.delete(correlationId);
@@ -5262,7 +5534,7 @@ var InMemoryAgentBridge = class {
5262
5534
  return;
5263
5535
  }
5264
5536
  this.pendingRequests.set(correlationId, {
5265
- resolve: resolve14,
5537
+ resolve: resolve15,
5266
5538
  reject,
5267
5539
  timer
5268
5540
  });
@@ -6062,6 +6334,13 @@ var Context = class {
6062
6334
  projectRoot;
6063
6335
  /** Mutable working directory — starts as `cwd`. Change via `setWorkingDir()`. */
6064
6336
  workingDir;
6337
+ /**
6338
+ * When true, file tools (via `_util.ts`) and `setWorkingDir()` reject paths
6339
+ * outside `projectRoot`. When false, those boundary checks are bypassed so
6340
+ * tools may reach paths outside the project (still gated by permission
6341
+ * tiers). Mutable so `/settings` can toggle it live on the running session.
6342
+ */
6343
+ allowOutsideProjectRoot;
6065
6344
  model;
6066
6345
  tools = [];
6067
6346
  meta = {};
@@ -6075,11 +6354,6 @@ var Context = class {
6075
6354
  * so storage operations can include it in `storage.*` events.
6076
6355
  */
6077
6356
  traceId;
6078
- /**
6079
- * When true, tools can access any path on the filesystem.
6080
- * When false or undefined, tools are restricted to the project root.
6081
- */
6082
- allowOutsideProjectRoot;
6083
6357
  /** Callbacks fired when `setWorkingDir()` changes the working directory. */
6084
6358
  _onWorkingDirChanged = [];
6085
6359
  /**
@@ -6111,12 +6385,13 @@ var Context = class {
6111
6385
  this.cwd = init.cwd;
6112
6386
  this.projectRoot = init.projectRoot;
6113
6387
  this.workingDir = init.workingDir ?? init.cwd;
6388
+ this.allowOutsideProjectRoot = init.allowOutsideProjectRoot ?? false;
6114
6389
  this.model = init.model;
6115
6390
  this.tools = init.tools ?? [];
6116
6391
  this.agentId = init.agentId ?? "unknown";
6117
6392
  this.agentName = init.agentName ?? "Unknown Agent";
6118
6393
  this.traceId = init.traceId;
6119
- this.allowOutsideProjectRoot = init.allowOutsideProjectRoot ?? true;
6394
+ this.allowOutsideProjectRoot = init.allowOutsideProjectRoot ?? false;
6120
6395
  this.session.traceId = init.traceId;
6121
6396
  }
6122
6397
  /**
@@ -6182,12 +6457,14 @@ var Context = class {
6182
6457
  */
6183
6458
  setWorkingDir(dir) {
6184
6459
  const resolved = path2.isAbsolute(dir) ? path2.resolve(dir) : path2.resolve(this.projectRoot, dir);
6185
- const root = path2.resolve(this.projectRoot);
6186
- const rel = path2.relative(root, resolved);
6187
- if (rel.startsWith("..") || path2.isAbsolute(rel)) {
6188
- throw new Error(
6189
- `Working directory "${resolved}" is outside project root "${root}"`
6190
- );
6460
+ if (!this.allowOutsideProjectRoot) {
6461
+ const root = path2.resolve(this.projectRoot);
6462
+ const rel = path2.relative(root, resolved);
6463
+ if (rel.startsWith("..") || path2.isAbsolute(rel)) {
6464
+ throw new Error(
6465
+ `Working directory "${resolved}" is outside project root "${root}"`
6466
+ );
6467
+ }
6191
6468
  }
6192
6469
  const old = this.workingDir;
6193
6470
  this.workingDir = resolved;
@@ -6509,11 +6786,34 @@ var DefaultSessionStore = class _DefaultSessionStore {
6509
6786
  dir;
6510
6787
  events;
6511
6788
  secretScrubber;
6789
+ /**
6790
+ * In-memory cache for load() results, keyed by session ID. The cache is
6791
+ * invalidated when the file's mtimeMs or size changes (indicating the
6792
+ * file was written to). This eliminates redundant full-file reads and
6793
+ * JSON parses when the same session is loaded multiple times within the
6794
+ * store's lifetime (e.g., webui session detail views, list() fallbacks).
6795
+ *
6796
+ * Max size is capped to prevent unbounded memory growth in long-running
6797
+ * processes. When the limit is reached, the oldest entry is evicted.
6798
+ */
6799
+ _loadCache = /* @__PURE__ */ new Map();
6800
+ static LOAD_CACHE_MAX_ENTRIES = 50;
6512
6801
  constructor(opts) {
6513
6802
  this.dir = opts.dir;
6514
6803
  this.events = opts.events;
6515
6804
  this.secretScrubber = opts.secretScrubber;
6516
6805
  }
6806
+ /**
6807
+ * Clear the load() cache. Useful for testing or when the caller knows
6808
+ * the file has changed externally (e.g., another process wrote to it).
6809
+ */
6810
+ clearLoadCache(sessionId) {
6811
+ if (sessionId !== void 0) {
6812
+ this._loadCache.delete(sessionId);
6813
+ } else {
6814
+ this._loadCache.clear();
6815
+ }
6816
+ }
6517
6817
  // ── Storage event helpers ───────────────────────────────────────────────────
6518
6818
  emitRead(sessionId, filePath, operation, outcome, durationMs, error) {
6519
6819
  this.events?.emit("storage.read", {
@@ -6656,7 +6956,20 @@ var DefaultSessionStore = class _DefaultSessionStore {
6656
6956
  const t0 = Date.now();
6657
6957
  let outcome = "success";
6658
6958
  let errorMsg;
6959
+ let cacheHit = false;
6659
6960
  try {
6961
+ let stat14;
6962
+ try {
6963
+ const s = await fsp3.stat(file);
6964
+ stat14 = { mtimeMs: s.mtimeMs, size: s.size };
6965
+ } catch (err) {
6966
+ throw err;
6967
+ }
6968
+ const cached = this._loadCache.get(id);
6969
+ if (cached && cached.mtimeMs === stat14.mtimeMs && cached.size === stat14.size) {
6970
+ cacheHit = true;
6971
+ return cached.data;
6972
+ }
6660
6973
  const raw = await fsp3.readFile(file, "utf8");
6661
6974
  const lines = raw.split("\n").filter((l) => l.trim());
6662
6975
  const events = [];
@@ -6672,13 +6985,30 @@ var DefaultSessionStore = class _DefaultSessionStore {
6672
6985
  const meta = this.metaFromEvents(id, events);
6673
6986
  const { messages, usage } = this.replay(events, id);
6674
6987
  const toolCallEnds = extractToolCallEnds(events);
6675
- return { metadata: meta, events, messages, usage, toolCallEnds };
6988
+ const data = { metadata: meta, events, messages, usage, toolCallEnds };
6989
+ if (this._loadCache.size >= _DefaultSessionStore.LOAD_CACHE_MAX_ENTRIES) {
6990
+ const oldest = this._loadCache.keys().next().value;
6991
+ if (oldest !== void 0) {
6992
+ this._loadCache.delete(oldest);
6993
+ }
6994
+ }
6995
+ this._loadCache.set(id, { mtimeMs: stat14.mtimeMs, size: stat14.size, data });
6996
+ return data;
6676
6997
  } catch (err) {
6677
6998
  outcome = "failure";
6678
6999
  errorMsg = toErrorMessage(err);
6679
7000
  throw err;
6680
7001
  } finally {
6681
7002
  this.emitRead(id, file, "load", outcome, Date.now() - t0, errorMsg);
7003
+ if (cacheHit) {
7004
+ this.events?.emit("storage.cache_hit", {
7005
+ sessionId: id,
7006
+ store: "session",
7007
+ filePath: file,
7008
+ operation: "load",
7009
+ durationMs: Date.now() - t0
7010
+ });
7011
+ }
6682
7012
  }
6683
7013
  }
6684
7014
  async list(limit = 20) {
@@ -6846,8 +7176,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
6846
7176
  return JSON.parse(raw);
6847
7177
  } catch {
6848
7178
  const full = this.sessionPath(id, ".jsonl");
6849
- const stat13 = await fsp3.stat(full);
6850
- const summary = await this.summarize(id, stat13.mtime.toISOString());
7179
+ const stat14 = await fsp3.stat(full);
7180
+ const summary = await this.summarize(id, stat14.mtime.toISOString());
6851
7181
  await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
6852
7182
  const msg = toErrorMessage(err);
6853
7183
  this.emitError(id, manifest, "summary_fallback", msg, true);
@@ -6929,8 +7259,8 @@ var DefaultSessionStore = class _DefaultSessionStore {
6929
7259
  const pruneFile = async (dir, name, prefix) => {
6930
7260
  const jsonlPath = path2.join(dir, name);
6931
7261
  try {
6932
- const stat13 = await fsp3.stat(jsonlPath);
6933
- if (stat13.mtimeMs >= cutoff) return;
7262
+ const stat14 = await fsp3.stat(jsonlPath);
7263
+ if (stat14.mtimeMs >= cutoff) return;
6934
7264
  } catch {
6935
7265
  return;
6936
7266
  }
@@ -8754,7 +9084,8 @@ var BEHAVIOR_DEFAULTS = {
8754
9084
  iterationTimeoutMs: DEFAULT_TOOLS_CONFIG.iterationTimeoutMs,
8755
9085
  sessionTimeoutMs: DEFAULT_TOOLS_CONFIG.sessionTimeoutMs,
8756
9086
  perIterationOutputCapBytes: DEFAULT_TOOLS_CONFIG.perIterationOutputCapBytes,
8757
- autoExtendLimit: DEFAULT_TOOLS_CONFIG.autoExtendLimit
9087
+ autoExtendLimit: DEFAULT_TOOLS_CONFIG.autoExtendLimit,
9088
+ restrictToProjectRoot: DEFAULT_TOOLS_CONFIG.restrictToProjectRoot
8758
9089
  },
8759
9090
  log: { level: "info" },
8760
9091
  features: {
@@ -9747,6 +10078,7 @@ async function savePlan(filePath, plan, events) {
9747
10078
  outcome: "success",
9748
10079
  durationMs: Date.now() - t0
9749
10080
  });
10081
+ return true;
9750
10082
  } catch (err) {
9751
10083
  events?.emit("storage.error", {
9752
10084
  sessionId: "~boot~",
@@ -9760,6 +10092,7 @@ async function savePlan(filePath, plan, events) {
9760
10092
  "[plan-store] save failed:",
9761
10093
  toErrorMessage(err)
9762
10094
  );
10095
+ return false;
9763
10096
  }
9764
10097
  }
9765
10098
  function emptyPlan(sessionId, title) {
@@ -9861,7 +10194,10 @@ async function mutatePlan(filePath, sessionId, fn) {
9861
10194
  return withFileLock(filePath, async () => {
9862
10195
  const plan = await loadPlan(filePath) ?? emptyPlan(sessionId);
9863
10196
  const updated = await fn(plan);
9864
- await savePlan(filePath, updated);
10197
+ const persisted = await savePlan(filePath, updated);
10198
+ if (!persisted) {
10199
+ throw new Error(`Failed to persist plan to ${filePath} \u2014 the change was NOT saved.`);
10200
+ }
9865
10201
  return updated;
9866
10202
  });
9867
10203
  }
@@ -10691,12 +11027,12 @@ var DefaultSkillLoader = class {
10691
11027
  }
10692
11028
  async find(name) {
10693
11029
  const all = await this.list();
10694
- return all.find((s) => s.name === name);
11030
+ const lower = name.toLowerCase();
11031
+ return all.find((s) => s.name.toLowerCase() === lower);
10695
11032
  }
10696
11033
  async manifestText() {
10697
- const skills = await this.list();
10698
- if (skills.length === 0) return "";
10699
11034
  const entries = await this.listEntries();
11035
+ if (entries.length === 0) return "";
10700
11036
  const lines = ["## Available skills"];
10701
11037
  for (const e of entries) {
10702
11038
  const scopeTag = e.scope.length > 0 ? ` \u2014 ${e.scope.slice(0, 3).join(", ")}` : "";
@@ -10710,12 +11046,8 @@ var DefaultSkillLoader = class {
10710
11046
  const skills = await this.list();
10711
11047
  const entries = [];
10712
11048
  for (const s of skills) {
10713
- try {
10714
- const raw = await fsp3.readFile(s.path, "utf8");
10715
- const { trigger, scope } = parseDescription(raw);
10716
- entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
10717
- } catch {
10718
- }
11049
+ const { trigger, scope } = parseDescriptionFromText(s.description ?? "");
11050
+ entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
10719
11051
  }
10720
11052
  this.entriesCache = entries;
10721
11053
  return entries;
@@ -10726,16 +11058,17 @@ var DefaultSkillLoader = class {
10726
11058
  this.bodyCache.clear();
10727
11059
  }
10728
11060
  async readBody(name) {
10729
- const cached = this.bodyCache.get(name);
11061
+ const key = name.toLowerCase();
11062
+ const cached = this.bodyCache.get(key);
10730
11063
  if (cached !== void 0) return cached;
10731
11064
  const m = await this.find(name);
10732
11065
  if (!m) throw new Error(`Skill "${name}" not found`);
10733
11066
  const body = await fsp3.readFile(m.path, "utf8");
10734
- this.bodyCache.set(name, body);
11067
+ this.bodyCache.set(key, body);
10735
11068
  return body;
10736
11069
  }
10737
11070
  async readSaveBody(name) {
10738
- const key = `save:${name}`;
11071
+ const key = `save:${name.toLowerCase()}`;
10739
11072
  const cached = this.bodyCache.get(key);
10740
11073
  if (cached !== void 0) return cached;
10741
11074
  const m = await this.find(name);
@@ -10796,9 +11129,7 @@ function parseFrontmatter(raw) {
10796
11129
  flush();
10797
11130
  return out;
10798
11131
  }
10799
- function parseDescription(raw) {
10800
- const fm = parseFrontmatter(raw);
10801
- const desc = fm.description ?? "";
11132
+ function parseDescriptionFromText(desc) {
10802
11133
  const firstSentenceEnd = desc.indexOf(". ");
10803
11134
  const trigger = firstSentenceEnd !== -1 ? desc.slice(0, firstSentenceEnd + 1).trim() : desc.trim().split("\n")[0] ?? "";
10804
11135
  const scope = [];
@@ -11056,8 +11387,8 @@ async function streamProviderToResponse(provider, req, signal, ctx, events, logg
11056
11387
  });
11057
11388
  await Promise.race([
11058
11389
  drainPromise,
11059
- new Promise((resolve14) => {
11060
- drainTimer = setTimeout(resolve14, STREAM_DRAIN_TIMEOUT_MS);
11390
+ new Promise((resolve15) => {
11391
+ drainTimer = setTimeout(resolve15, STREAM_DRAIN_TIMEOUT_MS);
11061
11392
  })
11062
11393
  ]);
11063
11394
  } finally {
@@ -11163,7 +11494,7 @@ async function runProviderWithRetry(opts) {
11163
11494
  description
11164
11495
  });
11165
11496
  }
11166
- await new Promise((resolve14, reject) => {
11497
+ await new Promise((resolve15, reject) => {
11167
11498
  let settled = false;
11168
11499
  const onAbort = () => {
11169
11500
  if (settled) return;
@@ -11176,7 +11507,7 @@ async function runProviderWithRetry(opts) {
11176
11507
  settled = true;
11177
11508
  clearTimeout(t2);
11178
11509
  signal.removeEventListener("abort", onAbort);
11179
- resolve14();
11510
+ resolve15();
11180
11511
  }, delay);
11181
11512
  if (signal.aborted) {
11182
11513
  onAbort();
@@ -11306,7 +11637,12 @@ var IntelligentCompactor = class {
11306
11637
  };
11307
11638
  const ac = ctx.signal ? void 0 : new AbortController();
11308
11639
  const signal = ctx.signal ?? ac?.signal;
11309
- const res = await this.provider.complete(req, { signal });
11640
+ let res;
11641
+ try {
11642
+ res = await this.provider.complete(req, { signal });
11643
+ } finally {
11644
+ ac?.abort();
11645
+ }
11310
11646
  const textBlocks = res.content.filter(isTextBlock);
11311
11647
  return textBlocks.map((b) => b.text).join("\n").trim() || "(empty summary)";
11312
11648
  }
@@ -11350,9 +11686,9 @@ Rules:
11350
11686
  - If unsure, keep rather than collapse (errors are more costly than waste)
11351
11687
 
11352
11688
  Return ONLY the JSON object, no markdown, no explanation outside the JSON.`;
11353
- function formatMessages(messages, maxChars = 8e3) {
11689
+ function formatMessages(messages, maxTokens = 2048) {
11354
11690
  const lines = [];
11355
- let used = 0;
11691
+ let usedTokens = 0;
11356
11692
  for (let i = 0; i < messages.length; i++) {
11357
11693
  const m = expectDefined(messages[i]);
11358
11694
  const role = m.role.padEnd(10, " ");
@@ -11364,13 +11700,14 @@ function formatMessages(messages, maxChars = 8e3) {
11364
11700
  text = content.filter(isTextBlock).map((b) => b.text).join(" ");
11365
11701
  const toolUses = content.filter((b) => b.type === "tool_use");
11366
11702
  if (toolUses.length > 0) {
11367
- text += ` [tools: ${toolUses.map((b) => b.name).join(", ")}]`;
11703
+ text += ` [tools: ${toolUses.map((b) => b.name).filter(Boolean).join(", ")}]`;
11368
11704
  }
11369
11705
  }
11370
11706
  const line = `[${i}][${role}]: ${text}`;
11371
- if (used + line.length > maxChars) break;
11707
+ const lineTokens = estimateTextTokens(line);
11708
+ if (usedTokens + lineTokens > maxTokens) break;
11372
11709
  lines.push(line);
11373
- used += line.length;
11710
+ usedTokens += lineTokens;
11374
11711
  }
11375
11712
  return lines.join("\n");
11376
11713
  }
@@ -11379,20 +11716,29 @@ var LLMSelector = class {
11379
11716
  model;
11380
11717
  maxContextTokens;
11381
11718
  systemPrompt;
11719
+ maxOutputTokens;
11382
11720
  constructor(opts) {
11383
11721
  this.provider = opts.provider;
11384
11722
  this.model = opts.model ?? "unknown";
11723
+ if (this.model === "unknown" && (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1")) {
11724
+ console.warn(
11725
+ "[LLMSelector] model not set \u2014 selector will use the provider default. Set `model` explicitly in LLMSelectorOptions to silence this warning."
11726
+ );
11727
+ }
11385
11728
  this.maxContextTokens = opts.maxContextTokens ?? 4e4;
11386
11729
  this.systemPrompt = opts.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
11730
+ this.maxOutputTokens = opts.maxOutputTokens ?? 1024;
11387
11731
  }
11388
11732
  async select(messages, maxToKeep) {
11389
11733
  const effectiveBudget = Math.min(maxToKeep, this.maxContextTokens);
11390
- const historyText = formatMessages(messages);
11391
11734
  const totalTokens = estimateMessageTokens(messages);
11392
11735
  const systemText = `${this.systemPrompt}
11393
11736
 
11394
11737
  Conversation (${messages.length} messages, ~${totalTokens} tokens, budget: ${effectiveBudget}):
11395
11738
  `;
11739
+ const systemTokens = estimateTextTokens(systemText);
11740
+ const historyBudget = Math.max(512, effectiveBudget - systemTokens - this.maxOutputTokens);
11741
+ const historyText = formatMessages(messages, historyBudget);
11396
11742
  const budgetInstruction = totalTokens > effectiveBudget ? `
11397
11743
 
11398
11744
  IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiveBudget}). You MUST collapse enough to fit. Prefer collapsing older/lower-importance ranges.` : "";
@@ -11400,18 +11746,26 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
11400
11746
  model: this.model,
11401
11747
  system: [{ type: "text", text: systemText + budgetInstruction }],
11402
11748
  messages: [{ role: "user", content: historyText }],
11403
- maxTokens: 1024
11749
+ maxTokens: this.maxOutputTokens
11404
11750
  };
11405
11751
  let raw;
11752
+ const ac = new AbortController();
11406
11753
  try {
11407
- const ac = new AbortController();
11408
- const res = await this.provider.complete(req, { signal: ac.signal });
11754
+ const timeoutSignal = AbortSignal.timeout(3e4);
11755
+ const res = await this.provider.complete(req, {
11756
+ signal: AbortSignal.any([ac.signal, timeoutSignal])
11757
+ });
11409
11758
  const textBlocks = res.content.filter(isTextBlock);
11410
11759
  raw = textBlocks.map((b) => b.text).join("\n").trim();
11411
- } catch (_err) {
11760
+ } catch (err) {
11761
+ if (err instanceof Error) {
11762
+ console.warn("[LLMSelector] selector call failed, using recency fallback:", err.message);
11763
+ }
11412
11764
  return this.fallbackSelect(messages, effectiveBudget);
11765
+ } finally {
11766
+ ac.abort();
11413
11767
  }
11414
- return this.parseSelectorOutput(raw, messages.length);
11768
+ return this.parseSelectorOutput(raw, messages);
11415
11769
  }
11416
11770
  fallbackSelect(messages, budget) {
11417
11771
  const toKeep = [];
@@ -11438,34 +11792,63 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
11438
11792
  reasoning: `Fallback: kept last ${messages.length - startIdx} messages within ${budget} token budget`
11439
11793
  };
11440
11794
  }
11441
- parseSelectorOutput(raw, messageCount) {
11795
+ /**
11796
+ * Parse and validate the raw LLM output into a SelectorResult.
11797
+ * Falls back to recency-based selection if the LLM output is malformed,
11798
+ * out-of-bounds, or internally inconsistent.
11799
+ */
11800
+ parseSelectorOutput(raw, messages) {
11801
+ const messageCount = messages.length;
11802
+ if (messageCount === 0) {
11803
+ return { kept: [], collapsed: [], reasoning: "empty session" };
11804
+ }
11442
11805
  const jsonStart = raw.indexOf("{");
11443
11806
  const jsonEnd = raw.lastIndexOf("}");
11444
11807
  if (jsonStart === -1 || jsonEnd === -1) {
11445
- return this.fallbackSelect(
11446
- Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
11447
- this.maxContextTokens
11448
- );
11808
+ return this.fallbackSelect(messages, this.maxContextTokens);
11449
11809
  }
11450
11810
  let parsed;
11451
11811
  try {
11452
11812
  parsed = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
11453
11813
  } catch {
11454
- return this.fallbackSelect(
11455
- Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
11456
- this.maxContextTokens
11457
- );
11814
+ return this.fallbackSelect(messages, this.maxContextTokens);
11458
11815
  }
11459
11816
  const obj = parsed;
11460
- const kept = obj.kept ?? [];
11461
- const collapsed = obj.collapsed ?? [];
11462
- return {
11463
- kept: kept.map((k) => ({
11817
+ const keptRaw = obj.kept ?? [];
11818
+ const collapsedRaw = obj.collapsed ?? [];
11819
+ const kept = [];
11820
+ for (const k of keptRaw) {
11821
+ if (typeof k.from !== "number" || typeof k.to !== "number" || k.from < 0 || k.to >= messageCount || k.from > k.to) {
11822
+ return this.fallbackSelect(messages, this.maxContextTokens);
11823
+ }
11824
+ kept.push({
11464
11825
  from: k.from,
11465
11826
  to: k.to,
11466
11827
  importance: k.importance ?? "medium"
11467
- })),
11468
- collapsed: collapsed.map((c) => ({ from: c.from, to: c.to, summary: c.summary })),
11828
+ });
11829
+ }
11830
+ const collapsed = [];
11831
+ for (const c of collapsedRaw) {
11832
+ if (typeof c.from !== "number" || typeof c.to !== "number" || c.from < 0 || c.to >= messageCount || c.from > c.to) {
11833
+ return this.fallbackSelect(messages, this.maxContextTokens);
11834
+ }
11835
+ collapsed.push({ from: c.from, to: c.to, summary: c.summary });
11836
+ }
11837
+ const allRanges = [...kept, ...collapsed];
11838
+ for (let i = 0; i < allRanges.length; i++) {
11839
+ const a = allRanges[i];
11840
+ if (!a) continue;
11841
+ for (let j = i + 1; j < allRanges.length; j++) {
11842
+ const b = allRanges[j];
11843
+ if (!b) continue;
11844
+ if (a.from <= b.to && a.to >= b.from) {
11845
+ return this.fallbackSelect(messages, this.maxContextTokens);
11846
+ }
11847
+ }
11848
+ }
11849
+ return {
11850
+ kept,
11851
+ collapsed,
11469
11852
  reasoning: typeof obj.reasoning === "string" ? obj.reasoning : ""
11470
11853
  };
11471
11854
  }
@@ -11485,7 +11868,7 @@ var SelectiveCompactor = class {
11485
11868
  summarizerPrompt;
11486
11869
  constructor(opts) {
11487
11870
  this.provider = opts.provider;
11488
- this.selector = opts.selector ?? new LLMSelector({ provider: opts.provider, model: opts.selectorModel });
11871
+ this.selector = opts.selector ?? new LLMSelector({ provider: opts.provider, model: opts.selectorModel, maxOutputTokens: opts.selectorMaxOutputTokens });
11489
11872
  this.warnThreshold = opts.warnThreshold ?? 0.6;
11490
11873
  this.softThreshold = opts.softThreshold ?? 0.75;
11491
11874
  this.hardThreshold = opts.hardThreshold ?? 0.9;
@@ -11493,6 +11876,11 @@ var SelectiveCompactor = class {
11493
11876
  this.preserveK = opts.preserveK ?? 4;
11494
11877
  this.eliseThreshold = opts.eliseThreshold ?? 500;
11495
11878
  this.summarizerModel = opts.summarizerModel ?? opts.selectorModel ?? "unknown";
11879
+ if (this.summarizerModel === "unknown" && (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1")) {
11880
+ console.warn(
11881
+ "[SelectiveCompactor] summarizerModel not set \u2014 will use provider default. Set `summarizerModel` explicitly to silence this warning."
11882
+ );
11883
+ }
11496
11884
  this.summarizerPrompt = opts.summarizerPrompt ?? "You are a context summarizer. Given a list of messages, produce a concise summary that preserves all factual information, decisions, file changes, and state changes. Do not add commentary or opinions.";
11497
11885
  }
11498
11886
  async compact(ctx, opts = {}) {
@@ -11604,8 +11992,9 @@ Summarize the following message range:`;
11604
11992
  maxTokens: 512
11605
11993
  };
11606
11994
  try {
11995
+ const timeoutSignal = AbortSignal.timeout(3e4);
11607
11996
  const res = await this.provider.complete(req, {
11608
- signal: ctx.signal ?? new AbortController().signal
11997
+ signal: AbortSignal.any([ctx.signal, timeoutSignal])
11609
11998
  });
11610
11999
  return res.content.filter(isTextBlock).map((b) => b.text).join("\n").trim() || "(empty)";
11611
12000
  } catch {
@@ -11739,6 +12128,7 @@ var ProviderBackedCompactor = class {
11739
12128
  return new SelectiveCompactor({
11740
12129
  ...common,
11741
12130
  selectorModel: this.opts.summarizerModel,
12131
+ selectorMaxOutputTokens: this.opts.selectorMaxOutputTokens,
11742
12132
  summarizerModel: this.opts.summarizerModel
11743
12133
  });
11744
12134
  }
@@ -13282,6 +13672,7 @@ ${recentJournal}` : ""
13282
13672
 
13283
13673
  // src/coordination/subagent-budget.ts
13284
13674
  var TIMEOUT_PREEMPT_FRACTION = 0.85;
13675
+ var DECISION_TIMEOUT_MS = 6e4;
13285
13676
  var BudgetExceededError = class extends Error {
13286
13677
  kind;
13287
13678
  limit;
@@ -13311,6 +13702,31 @@ var BudgetThresholdSignal = class extends Error {
13311
13702
  };
13312
13703
  var SubagentBudget = class _SubagentBudget {
13313
13704
  limits;
13705
+ /** Patch one or more budget limits in-place after construction.
13706
+ * Used by the coordinator watchdog when granting an extension.
13707
+ * All fields are optional — only provided fields are updated.
13708
+ * This is the single write path for limit mutations so that future
13709
+ * validation or side-effects live in one place (M1). */
13710
+ patchLimits(ext) {
13711
+ if (ext.maxIterations !== void 0) {
13712
+ this.limits.maxIterations = ext.maxIterations;
13713
+ }
13714
+ if (ext.maxToolCalls !== void 0) {
13715
+ this.limits.maxToolCalls = ext.maxToolCalls;
13716
+ }
13717
+ if (ext.maxTokens !== void 0) {
13718
+ this.limits.maxTokens = ext.maxTokens;
13719
+ }
13720
+ if (ext.maxCostUsd !== void 0) {
13721
+ this.limits.maxCostUsd = ext.maxCostUsd;
13722
+ }
13723
+ if (ext.timeoutMs !== void 0) {
13724
+ this.limits.timeoutMs = ext.timeoutMs;
13725
+ }
13726
+ if (ext.idleTimeoutMs !== void 0) {
13727
+ this.limits.idleTimeoutMs = ext.idleTimeoutMs;
13728
+ }
13729
+ }
13314
13730
  iterations = 0;
13315
13731
  toolCalls = 0;
13316
13732
  tokenInput = 0;
@@ -13331,12 +13747,44 @@ var SubagentBudget = class _SubagentBudget {
13331
13747
  * or hung listener (Director not built / event filter detached mid-run)
13332
13748
  * leaves the budget over-limit and never enforces anything.
13333
13749
  */
13334
- static DECISION_TIMEOUT_MS = 6e4;
13750
+ static DECISION_TIMEOUT_MS = DECISION_TIMEOUT_MS;
13335
13751
  /**
13336
13752
  * Injected by the runner when wiring the budget to its EventBus.
13337
13753
  * Used to emit `budget.threshold_reached` events in `'auto'` mode.
13338
13754
  */
13339
13755
  _events;
13756
+ /**
13757
+ * Guard against dual-path races between the coordinator watchdog
13758
+ * (`executeWithTimeout`) and the budget's own `checkTimeout()`.
13759
+ * Both paths detect `elapsed >= timeoutMs` and can emit
13760
+ * `budget.threshold_reached` for kind `'timeout'` simultaneously.
13761
+ * Set to the current `timeoutMs` ceiling by the coordinator BEFORE
13762
+ * calling `onThreshold`, and cleared after the negotiation resolves.
13763
+ * `checkTimeout()` skips its wall-clock check while this is set so
13764
+ * the coordinator's watchdog is the sole source of wall-clock timeout
13765
+ * events — `checkTimeout()` focuses exclusively on `idle_timeout`.
13766
+ */
13767
+ _watchdogActive;
13768
+ /** Returns the timeout ceiling currently being negotiated by the watchdog,
13769
+ * or `undefined` when no wall-clock negotiation is in flight.
13770
+ * Used by `executeWithTimeout` to detect a stale lock (M3). */
13771
+ get watchdogActive() {
13772
+ return this._watchdogActive;
13773
+ }
13774
+ /** Called by the coordinator watchdog BEFORE calling `onThreshold` so that
13775
+ * `checkTimeout()` skips its wall-clock check for this ceiling. Prevents
13776
+ * the budget's own `checkTimeout()` from emitting a second
13777
+ * `budget.threshold_reached` event while the watchdog is already
13778
+ * negotiating the same wall-clock deadline (C1). */
13779
+ setWatchdogNegotiation(timeoutMs) {
13780
+ this._watchdogActive = timeoutMs;
13781
+ }
13782
+ /** Clears the watchdog guard after negotiation resolves. Called in the
13783
+ * `finally` block of both the pre-empt and deadline branches so it fires
13784
+ * on every exit path: grant, deny, throw, or error. */
13785
+ clearWatchdogNegotiation() {
13786
+ this._watchdogActive = void 0;
13787
+ }
13340
13788
  /**
13341
13789
  * Negotiation mode — controls whether a threshold hit tries to emit
13342
13790
  * `budget.threshold_reached` and wait for a coordinator decision, or
@@ -13437,7 +13885,8 @@ var SubagentBudget = class _SubagentBudget {
13437
13885
  if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
13438
13886
  exceeded.push({ kind: "idle_timeout", used: idle, limit: this.limits.idleTimeoutMs });
13439
13887
  }
13440
- if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs) {
13888
+ const wallOwnedByWatchdog = this._onThreshold !== void 0 && this._watchdogActive === this.limits.timeoutMs;
13889
+ if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs && !wallOwnedByWatchdog) {
13441
13890
  exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
13442
13891
  }
13443
13892
  }
@@ -13451,19 +13900,99 @@ var SubagentBudget = class _SubagentBudget {
13451
13900
  throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
13452
13901
  }
13453
13902
  const bus = this._events;
13454
- if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
13903
+ if (!bus) {
13455
13904
  const first2 = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
13456
13905
  throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
13457
13906
  }
13907
+ const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
13908
+ if (bus.hasListenerFor("budget.threshold_reached")) {
13909
+ for (const entry of exceeded) {
13910
+ if (this._pendingNegotiations.has(entry.kind)) continue;
13911
+ this._pendingNegotiations.set(entry.kind, this._negotiateExtension(entry));
13912
+ }
13913
+ const decision = this._pendingNegotiations.get(first.kind);
13914
+ if (!decision) throw new Error(`No pending negotiation for ${first.kind}`);
13915
+ throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
13916
+ }
13917
+ let hardStop = null;
13458
13918
  for (const entry of exceeded) {
13459
13919
  if (this._pendingNegotiations.has(entry.kind)) continue;
13460
- const decision2 = this._negotiateExtension(entry.kind, exceeded);
13461
- this._pendingNegotiations.set(entry.kind, decision2);
13920
+ const marker = Promise.resolve("stop");
13921
+ this._pendingNegotiations.set(entry.kind, marker);
13922
+ void marker.finally(() => this._pendingNegotiations.delete(entry.kind));
13923
+ const sync = this._invokeHandlerSync(entry);
13924
+ if (!sync) hardStop ??= new BudgetExceededError(entry.kind, entry.limit, entry.used);
13925
+ }
13926
+ if (hardStop) throw hardStop;
13927
+ return exceeded;
13928
+ }
13929
+ /**
13930
+ * Invoke `onThreshold` once for `entry` on the NO-LISTENER path and report
13931
+ * whether it decided synchronously. Returns `true` when the handler returned
13932
+ * a synchronous decision (already honored — an `extend` patched the limits),
13933
+ * or `false` when it returned a Promise (async; the caller hard-stops, since
13934
+ * there is no listener to resolve the negotiation). The handler is given the
13935
+ * full info shape (`requestDecision` plus direct `extend`/`deny`) so both
13936
+ * recording handlers and policy handlers work without a wired listener.
13937
+ */
13938
+ _invokeHandlerSync(entry) {
13939
+ const handler = this._onThreshold;
13940
+ if (!handler) return false;
13941
+ let extendArg;
13942
+ const result = handler({
13943
+ kind: entry.kind,
13944
+ used: entry.used,
13945
+ limit: entry.limit,
13946
+ requestDecision: () => this._busRequestDecision(entry),
13947
+ // Direct hooks for synchronous policy/recording handlers.
13948
+ extend: (extra) => {
13949
+ extendArg = extra;
13950
+ },
13951
+ deny: () => {
13952
+ }
13953
+ });
13954
+ if (result && typeof result.then === "function") return false;
13955
+ if (result === "throw") return false;
13956
+ if (result && typeof result === "object" && "extend" in result) {
13957
+ extendArg = result.extend;
13462
13958
  }
13463
- const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
13464
- const decision = this._pendingNegotiations.get(first.kind);
13465
- if (!decision) throw new Error(`No pending negotiation for ${first.kind}`);
13466
- throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
13959
+ if (extendArg) this.patchLimits(extendArg);
13960
+ return true;
13961
+ }
13962
+ /**
13963
+ * Emit `budget.threshold_reached` and resolve to the listener's verdict.
13964
+ * Resolves to `'stop'` immediately when there is no listener (or no bus) so
13965
+ * no negotiation can hang and no fallback timer leaks. Mirrors the
13966
+ * coordinator watchdog's own request path so both agree on the no-listener
13967
+ * default.
13968
+ */
13969
+ _busRequestDecision(entry) {
13970
+ const bus = this._events;
13971
+ if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
13972
+ return Promise.resolve("stop");
13973
+ }
13974
+ return new Promise((resolve15) => {
13975
+ let resolved = false;
13976
+ const respond = (d) => {
13977
+ if (resolved) return;
13978
+ resolved = true;
13979
+ clearTimeout(fallback);
13980
+ resolve15(d);
13981
+ };
13982
+ const fallback = setTimeout(() => respond("stop"), _SubagentBudget.DECISION_TIMEOUT_MS);
13983
+ bus.emit("budget.threshold_reached", {
13984
+ kind: entry.kind,
13985
+ used: entry.used,
13986
+ limit: entry.limit,
13987
+ timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
13988
+ // deny() wins over a same-dispatch extend(): a listener that both grants
13989
+ // and denies (or two listeners disagreeing) is resolved as a stop. The
13990
+ // grant is deferred a microtask so a synchronous deny in the same emit
13991
+ // pre-empts it; async grants still resolve normally.
13992
+ extend: (extra) => queueMicrotask(() => respond({ extend: extra })),
13993
+ deny: () => respond("stop")
13994
+ });
13995
+ });
13467
13996
  }
13468
13997
  /**
13469
13998
  * Per-kind in-flight negotiation Promises. Each budget kind can have its
@@ -13483,77 +14012,33 @@ var SubagentBudget = class _SubagentBudget {
13483
14012
  * `{ extend: {} }` — keep going without patching; next overrun fires
13484
14013
  * a fresh signal.
13485
14014
  */
13486
- async _negotiateExtension(kind, exceeded) {
14015
+ async _negotiateExtension(entry) {
13487
14016
  if (!this._onThreshold) {
13488
14017
  return "stop";
13489
14018
  }
13490
14019
  try {
13491
- const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
13492
14020
  const result = this._onThreshold({
13493
- kind: first.kind,
13494
- used: first.used,
13495
- limit: first.limit,
13496
- requestDecision: () => {
13497
- const bus = this._events;
13498
- if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
13499
- return Promise.resolve("stop");
13500
- }
13501
- return new Promise((resolve14) => {
13502
- let resolved = false;
13503
- const respond = (d) => {
13504
- if (resolved) return;
13505
- resolved = true;
13506
- resolve14(d);
13507
- };
13508
- const fallback = setTimeout(
13509
- () => respond("stop"),
13510
- _SubagentBudget.DECISION_TIMEOUT_MS
13511
- );
13512
- for (const { kind: kind2, used, limit } of exceeded) {
13513
- bus.emit("budget.threshold_reached", {
13514
- kind: kind2,
13515
- used,
13516
- limit,
13517
- timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
13518
- extend: (extra) => {
13519
- clearTimeout(fallback);
13520
- respond({ extend: extra });
13521
- },
13522
- deny: () => {
13523
- clearTimeout(fallback);
13524
- respond("stop");
13525
- }
13526
- });
13527
- }
13528
- });
14021
+ kind: entry.kind,
14022
+ used: entry.used,
14023
+ limit: entry.limit,
14024
+ // One event for THIS kind only — each exceeded kind has its own
14025
+ // negotiation (and its own resolve), so there is no cross-kind
14026
+ // first-wins drop and no O(N^2) re-emission.
14027
+ requestDecision: () => this._busRequestDecision(entry),
14028
+ extend: (extra) => {
14029
+ this.patchLimits(extra);
14030
+ },
14031
+ deny: () => {
13529
14032
  }
13530
14033
  });
13531
14034
  if (result === "throw") return "stop";
13532
14035
  if (result === "continue") return { extend: {} };
13533
14036
  const decision = await result;
13534
14037
  if (decision === "stop") return "stop";
13535
- const ext = decision.extend;
13536
- if (ext.maxIterations !== void 0) {
13537
- this.limits.maxIterations = ext.maxIterations;
13538
- }
13539
- if (ext.maxToolCalls !== void 0) {
13540
- this.limits.maxToolCalls = ext.maxToolCalls;
13541
- }
13542
- if (ext.maxTokens !== void 0) {
13543
- this.limits.maxTokens = ext.maxTokens;
13544
- }
13545
- if (ext.maxCostUsd !== void 0) {
13546
- this.limits.maxCostUsd = ext.maxCostUsd;
13547
- }
13548
- if (ext.timeoutMs !== void 0) {
13549
- this.limits.timeoutMs = ext.timeoutMs;
13550
- }
13551
- if (ext.idleTimeoutMs !== void 0) {
13552
- this.limits.idleTimeoutMs = ext.idleTimeoutMs;
13553
- }
14038
+ this.patchLimits(decision.extend);
13554
14039
  return decision;
13555
14040
  } finally {
13556
- this._pendingNegotiations.delete(kind);
14041
+ this._pendingNegotiations.delete(entry.kind);
13557
14042
  }
13558
14043
  }
13559
14044
  recordIteration() {
@@ -13596,7 +14081,8 @@ var SubagentBudget = class _SubagentBudget {
13596
14081
  const { timeoutMs, idleTimeoutMs } = this.limits;
13597
14082
  if (timeoutMs === void 0 && idleTimeoutMs === void 0) return;
13598
14083
  const elapsed = Date.now() - this.startTime;
13599
- const wallTripped = timeoutMs !== void 0 && elapsed > timeoutMs;
14084
+ const wallSkipped = this._onThreshold !== void 0 && this._watchdogActive !== void 0 && timeoutMs !== void 0 && this._watchdogActive === timeoutMs;
14085
+ const wallTripped = wallSkipped ? false : timeoutMs !== void 0 && elapsed > timeoutMs;
13600
14086
  const idleTripped = idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs;
13601
14087
  if (!wallTripped && !idleTripped) return;
13602
14088
  void this.checkLimits(elapsed);
@@ -17104,6 +17590,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
17104
17590
  terminating = /* @__PURE__ */ new Set();
17105
17591
  constructor(config, options = {}) {
17106
17592
  super();
17593
+ this.setMaxListeners(0);
17107
17594
  this.coordinatorId = config.coordinatorId;
17108
17595
  this.config = config;
17109
17596
  this.runner = options.runner;
@@ -17301,7 +17788,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
17301
17788
  taskIds.map((id) => {
17302
17789
  const cached = this.completedResults.find((r) => r.taskId === id);
17303
17790
  if (cached) return cached;
17304
- return new Promise((resolve14, reject) => {
17791
+ return new Promise((resolve15, reject) => {
17305
17792
  const timeout = setTimeout(() => {
17306
17793
  this.off("task.completed", handler);
17307
17794
  reject(new Error(`awaitTasks timed out waiting for task "${id}"`));
@@ -17310,7 +17797,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
17310
17797
  if (result.taskId === id) {
17311
17798
  clearTimeout(timeout);
17312
17799
  this.off("task.completed", handler);
17313
- resolve14(result);
17800
+ resolve15(result);
17314
17801
  }
17315
17802
  };
17316
17803
  this.on("task.completed", handler);
@@ -17498,7 +17985,13 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
17498
17985
  let result;
17499
17986
  budget.start();
17500
17987
  try {
17501
- const outcome = await this.executeWithTimeout(this.runner, task, runCtx, budget);
17988
+ const outcome = await this.executeWithTimeout(
17989
+ this.runner,
17990
+ task,
17991
+ runCtx,
17992
+ budget,
17993
+ subagent.config.preemptFraction
17994
+ );
17502
17995
  result = {
17503
17996
  subagentId,
17504
17997
  taskId: task.id,
@@ -17525,7 +18018,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
17525
18018
  }
17526
18019
  this.recordCompletion(result);
17527
18020
  }
17528
- async executeWithTimeout(runner, task, ctx, budget) {
18021
+ async executeWithTimeout(runner, task, ctx, budget, preemptFraction = TIMEOUT_PREEMPT_FRACTION) {
17529
18022
  const initialTimeoutMs = budget.limits.timeoutMs;
17530
18023
  const idleLimitMs = budget.limits.idleTimeoutMs;
17531
18024
  if (initialTimeoutMs === void 0 && idleLimitMs === void 0) {
@@ -17533,8 +18026,21 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
17533
18026
  }
17534
18027
  const start = Date.now();
17535
18028
  let timer = null;
17536
- let preemptedForLimit = null;
18029
+ let PreemptState;
18030
+ ((PreemptState2) => {
18031
+ PreemptState2["ACTIVE"] = "active";
18032
+ PreemptState2["LOCKED"] = "locked";
18033
+ })(PreemptState || (PreemptState = {}));
18034
+ let preemptedCeiling = null;
18035
+ let preemptState = "active" /* ACTIVE */;
18036
+ let lastGrantActivityTs = -1;
17537
18037
  const timeoutPromise = new Promise((_, reject) => {
18038
+ const terminate = (kind, limit, used) => {
18039
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
18040
+ reject(
18041
+ budget._events?.hasListenerFor("budget.threshold_reached") ? new Error(`subagent stopped: budget ${kind} (limit=${limit}, used=${used})`) : new BudgetExceededError(kind, limit, used)
18042
+ );
18043
+ };
17538
18044
  const armFor = (ms) => {
17539
18045
  if (timer) clearTimeout(timer);
17540
18046
  timer = setTimeout(onTick, Math.max(0, ms));
@@ -17543,7 +18049,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
17543
18049
  const wallLimit = budget.limits.timeoutMs ?? initialTimeoutMs;
17544
18050
  const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : wallLimit - (Date.now() - start);
17545
18051
  const idleRemaining = idleLimitMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
17546
- const preemptRemaining = initialTimeoutMs === void 0 || preemptedForLimit === wallLimit ? Number.POSITIVE_INFINITY : wallLimit * TIMEOUT_PREEMPT_FRACTION - (Date.now() - start);
18052
+ const preemptRemaining = initialTimeoutMs === void 0 || preemptedCeiling === wallLimit ? Number.POSITIVE_INFINITY : wallLimit * preemptFraction - (Date.now() - start);
17547
18053
  armFor(Math.max(25, Math.min(wallRemaining, idleRemaining, preemptRemaining)));
17548
18054
  };
17549
18055
  const negotiateTimeout = async (used, limit) => {
@@ -17553,16 +18059,42 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
17553
18059
  kind: "timeout",
17554
18060
  used,
17555
18061
  limit,
17556
- requestDecision: () => new Promise((resolveDecision) => {
17557
- budget._events?.emit("budget.threshold_reached", {
17558
- kind: "timeout",
17559
- used,
17560
- limit,
17561
- timeoutMs: 6e4,
17562
- extend: (extra) => resolveDecision({ extend: extra }),
17563
- deny: () => resolveDecision("stop")
18062
+ requestDecision: () => {
18063
+ if (!budget._events?.hasListenerFor("budget.threshold_reached")) {
18064
+ return Promise.resolve("stop");
18065
+ }
18066
+ return new Promise((resolveDecision) => {
18067
+ let settled = false;
18068
+ const resolve15 = (d) => {
18069
+ if (settled) return;
18070
+ settled = true;
18071
+ resolveDecision(d);
18072
+ };
18073
+ const fallback = setTimeout(() => resolve15("stop"), DECISION_TIMEOUT_MS);
18074
+ budget._events?.emit("budget.threshold_reached", {
18075
+ kind: "timeout",
18076
+ used,
18077
+ limit,
18078
+ // Informational: the budget's own decision deadline. Listeners may use
18079
+ // this to display a countdown. The coordinator does NOT enforce it —
18080
+ // it is the budget's own `setTimeout(fallback)` that races against
18081
+ // the listener's `extend()`/`deny()` call to guarantee progress.
18082
+ timeoutMs: DECISION_TIMEOUT_MS,
18083
+ // deny() wins over a same-dispatch extend(): defer the grant a
18084
+ // microtask so a synchronous deny in the same emit pre-empts it
18085
+ // (a listener that both grants and denies, or two listeners
18086
+ // disagreeing, resolves as a stop). Async grants still resolve.
18087
+ extend: (extra) => {
18088
+ clearTimeout(fallback);
18089
+ queueMicrotask(() => resolve15({ extend: extra }));
18090
+ },
18091
+ deny: () => {
18092
+ clearTimeout(fallback);
18093
+ resolve15("stop");
18094
+ }
18095
+ });
17564
18096
  });
17565
- })
18097
+ }
17566
18098
  });
17567
18099
  return typeof result === "string" ? result : await result;
17568
18100
  };
@@ -17573,21 +18105,45 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
17573
18105
  const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
17574
18106
  const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
17575
18107
  if (idleExceeded && !wallExceeded) {
18108
+ budget._events?.emit("budget.threshold_reached", {
18109
+ kind: "idle_timeout",
18110
+ used: budget.idleMs(),
18111
+ limit: idleLimit ?? 0,
18112
+ timeoutMs: DECISION_TIMEOUT_MS,
18113
+ extend: () => {
18114
+ },
18115
+ deny: () => {
18116
+ }
18117
+ });
17576
18118
  this.subagents.get(ctx.subagentId)?.abortController.abort();
17577
- reject(new BudgetExceededError("timeout", idleLimit ?? 0, budget.idleMs()));
18119
+ reject(new BudgetExceededError("idle_timeout", idleLimit ?? 0, budget.idleMs()));
17578
18120
  return;
17579
18121
  }
17580
- if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold && preemptedForLimit !== wallLimit && elapsed >= wallLimit * TIMEOUT_PREEMPT_FRACTION) {
18122
+ if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold && preemptState === "active" /* ACTIVE */ && elapsed >= wallLimit * preemptFraction) {
18123
+ const activityTs = Date.now() - budget.idleMs();
18124
+ if (activityTs <= lastGrantActivityTs) {
18125
+ preemptState = "locked" /* LOCKED */;
18126
+ preemptedCeiling = wallLimit;
18127
+ scheduleNext();
18128
+ return;
18129
+ }
18130
+ budget.setWatchdogNegotiation(wallLimit);
17581
18131
  try {
17582
18132
  const decision = await negotiateTimeout(elapsed, wallLimit);
17583
18133
  if (typeof decision !== "string" && decision.extend.timeoutMs !== void 0) {
17584
- budget.limits.timeoutMs = decision.extend.timeoutMs;
17585
- preemptedForLimit = null;
18134
+ budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
18135
+ lastGrantActivityTs = Date.now() - budget.idleMs();
18136
+ preemptState = "active" /* ACTIVE */;
18137
+ preemptedCeiling = null;
17586
18138
  } else {
17587
- preemptedForLimit = wallLimit;
18139
+ preemptState = "locked" /* LOCKED */;
18140
+ preemptedCeiling = wallLimit;
17588
18141
  }
17589
18142
  } catch {
17590
- preemptedForLimit = wallLimit;
18143
+ preemptState = "locked" /* LOCKED */;
18144
+ preemptedCeiling = wallLimit;
18145
+ } finally {
18146
+ budget.clearWatchdogNegotiation();
17591
18147
  }
17592
18148
  scheduleNext();
17593
18149
  return;
@@ -17602,26 +18158,41 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
17602
18158
  reject(new BudgetExceededError("timeout", limit, elapsed));
17603
18159
  return;
17604
18160
  }
18161
+ budget.setWatchdogNegotiation(limit);
17605
18162
  try {
17606
18163
  const decision = await negotiateTimeout(elapsed, limit);
17607
- if (decision === "continue" || decision === "throw" || decision === "stop") {
17608
- preemptedForLimit = null;
18164
+ if (decision === "throw") {
18165
+ terminate("timeout", limit, elapsed);
18166
+ return;
18167
+ }
18168
+ if (decision === "continue") {
18169
+ preemptState = "locked" /* LOCKED */;
18170
+ preemptedCeiling = wallLimit;
17609
18171
  armFor(Math.max(1e3, limit));
17610
18172
  return;
17611
18173
  }
18174
+ if (decision === "stop") {
18175
+ terminate("timeout", limit, elapsed);
18176
+ return;
18177
+ }
17612
18178
  if (decision.extend.timeoutMs !== void 0) {
17613
- budget.limits.timeoutMs = decision.extend.timeoutMs;
17614
- preemptedForLimit = null;
18179
+ budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
18180
+ lastGrantActivityTs = Date.now() - budget.idleMs();
18181
+ preemptState = "active" /* ACTIVE */;
18182
+ preemptedCeiling = null;
17615
18183
  scheduleNext();
17616
18184
  return;
17617
18185
  }
17618
- this.subagents.get(ctx.subagentId)?.abortController.abort();
17619
- reject(new BudgetExceededError("timeout", limit, elapsed));
18186
+ terminate("timeout", limit, elapsed);
18187
+ return;
17620
18188
  } catch (err) {
17621
18189
  this.subagents.get(ctx.subagentId)?.abortController.abort();
17622
18190
  reject(
17623
18191
  err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
17624
18192
  );
18193
+ return;
18194
+ } finally {
18195
+ budget.clearWatchdogNegotiation();
17625
18196
  }
17626
18197
  };
17627
18198
  scheduleNext();
@@ -18398,7 +18969,7 @@ var CollabSession = class extends EventEmitter {
18398
18969
  }
18399
18970
  for (const filePath of allFiles) {
18400
18971
  try {
18401
- const [content, stat13] = await Promise.all([
18972
+ const [content, stat14] = await Promise.all([
18402
18973
  fsp3.readFile(filePath, "utf8"),
18403
18974
  fsp3.stat(filePath)
18404
18975
  ]);
@@ -18408,8 +18979,8 @@ var CollabSession = class extends EventEmitter {
18408
18979
  path: filePath,
18409
18980
  content,
18410
18981
  language,
18411
- snapshotMtimeMs: stat13.mtimeMs,
18412
- snapshotSizeBytes: stat13.size
18982
+ snapshotMtimeMs: stat14.mtimeMs,
18983
+ snapshotSizeBytes: stat14.size
18413
18984
  });
18414
18985
  } catch {
18415
18986
  this.snapshot.files.push({ path: filePath, content: "", language: void 0 });
@@ -18822,9 +19393,9 @@ Emit each evaluation immediately. Do not wait until you have read all reports.`;
18822
19393
  for (const file of this.snapshot.files) {
18823
19394
  if (file.snapshotMtimeMs === void 0 && file.snapshotSizeBytes === void 0) continue;
18824
19395
  try {
18825
- const stat13 = await fsp3.stat(file.path);
18826
- const mtimeChanged = file.snapshotMtimeMs !== void 0 && stat13.mtimeMs > file.snapshotMtimeMs + 1;
18827
- const sizeChanged = file.snapshotSizeBytes !== void 0 && stat13.size !== file.snapshotSizeBytes;
19396
+ const stat14 = await fsp3.stat(file.path);
19397
+ const mtimeChanged = file.snapshotMtimeMs !== void 0 && stat14.mtimeMs > file.snapshotMtimeMs + 1;
19398
+ const sizeChanged = file.snapshotSizeBytes !== void 0 && stat14.size !== file.snapshotSizeBytes;
18828
19399
  if (mtimeChanged || sizeChanged) {
18829
19400
  warnings.push(`${file.path} changed after the collab snapshot was captured.`);
18830
19401
  }
@@ -20718,11 +21289,11 @@ var Director = class _Director {
20718
21289
  if (cached) return cached;
20719
21290
  const existing = this.taskWaiters.get(id);
20720
21291
  if (existing) return existing.promise;
20721
- let resolve14;
21292
+ let resolve15;
20722
21293
  const promise = new Promise((res) => {
20723
- resolve14 = res;
21294
+ resolve15 = res;
20724
21295
  });
20725
- this.taskWaiters.set(id, { promise, resolve: resolve14 });
21296
+ this.taskWaiters.set(id, { promise, resolve: resolve15 });
20726
21297
  return promise;
20727
21298
  })
20728
21299
  );
@@ -21118,7 +21689,7 @@ function createDelegateTool(opts) {
21118
21689
  subagentId
21119
21690
  });
21120
21691
  const dir = director;
21121
- const result = await new Promise((resolve14) => {
21692
+ const result = await new Promise((resolve15) => {
21122
21693
  let settled = false;
21123
21694
  let timer;
21124
21695
  const finish = (value) => {
@@ -21128,7 +21699,7 @@ function createDelegateTool(opts) {
21128
21699
  offTool();
21129
21700
  offIter();
21130
21701
  offProgress();
21131
- resolve14(value);
21702
+ resolve15(value);
21132
21703
  };
21133
21704
  const arm = () => {
21134
21705
  if (timer) clearTimeout(timer);
@@ -21439,6 +22010,7 @@ function attachAutoExtend(events, policy = {}) {
21439
22010
  const extendCounts = /* @__PURE__ */ new Map();
21440
22011
  let progress = 0;
21441
22012
  let lastTimeoutProgress = -1;
22013
+ let lastSeenKey = null;
21442
22014
  const unsubs = [
21443
22015
  events.on("tool.executed", () => {
21444
22016
  progress++;
@@ -21448,6 +22020,9 @@ function attachAutoExtend(events, policy = {}) {
21448
22020
  }),
21449
22021
  events.on("budget.threshold_reached", (e) => {
21450
22022
  const { kind, limit, extend, deny } = e;
22023
+ const key = `${kind}:${limit}`;
22024
+ if (key === lastSeenKey) return;
22025
+ lastSeenKey = key;
21451
22026
  if (kind === "timeout" || kind === "idle_timeout") {
21452
22027
  if (progress > lastTimeoutProgress) {
21453
22028
  lastTimeoutProgress = progress;
@@ -21556,8 +22131,8 @@ async function loadProjectModes(modesDir) {
21556
22131
  for (const entry of entries) {
21557
22132
  if (!entry.endsWith(".md") && !entry.endsWith(".txt")) continue;
21558
22133
  const filePath = path2.join(modesDir, entry);
21559
- const stat13 = await fsp3.stat(filePath);
21560
- if (!stat13.isFile()) continue;
22134
+ const stat14 = await fsp3.stat(filePath);
22135
+ if (!stat14.isFile()) continue;
21561
22136
  const content = await fsp3.readFile(filePath, "utf8");
21562
22137
  const id = path2.basename(entry, path2.extname(entry));
21563
22138
  modes.push({
@@ -22866,9 +23441,9 @@ var AISpecBuilder = class {
22866
23441
  if (!this.sessionPath) return;
22867
23442
  try {
22868
23443
  const fsp25 = await import('fs/promises');
22869
- const path43 = await import('path');
23444
+ const path44 = await import('path');
22870
23445
  const { atomicWrite: atomicWrite2 } = await Promise.resolve().then(() => (init_atomic_write(), atomic_write_exports));
22871
- await fsp25.mkdir(path43.dirname(this.sessionPath), { recursive: true });
23446
+ await fsp25.mkdir(path44.dirname(this.sessionPath), { recursive: true });
22872
23447
  await atomicWrite2(this.sessionPath, JSON.stringify(this.session, null, 2));
22873
23448
  } catch {
22874
23449
  }
@@ -23595,15 +24170,15 @@ function computeCriticalPath(graph, _topoOrder, blockedByMap) {
23595
24170
  maxId = id;
23596
24171
  }
23597
24172
  }
23598
- const path43 = [];
24173
+ const path44 = [];
23599
24174
  let current = maxId;
23600
24175
  const visited = /* @__PURE__ */ new Set();
23601
24176
  while (current && !visited.has(current)) {
23602
24177
  visited.add(current);
23603
- path43.unshift(current);
24178
+ path44.unshift(current);
23604
24179
  current = prev.get(current) ?? null;
23605
24180
  }
23606
- return path43;
24181
+ return path44;
23607
24182
  }
23608
24183
  function computeParallelGroups(graph, blockedByMap) {
23609
24184
  const groups = [];
@@ -24426,9 +25001,9 @@ var DefaultHealthRegistry = class {
24426
25001
  }
24427
25002
  async runOne(check) {
24428
25003
  let timer = null;
24429
- const timeout = new Promise((resolve14) => {
25004
+ const timeout = new Promise((resolve15) => {
24430
25005
  timer = setTimeout(
24431
- () => resolve14({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
25006
+ () => resolve15({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
24432
25007
  this.timeoutMs
24433
25008
  );
24434
25009
  });
@@ -24611,7 +25186,7 @@ async function startMetricsServer(opts) {
24611
25186
  const tls = opts.tls;
24612
25187
  const useHttps = !!(tls?.cert && tls?.key);
24613
25188
  const host = opts.host ?? "127.0.0.1";
24614
- const path43 = opts.path ?? "/metrics";
25189
+ const path44 = opts.path ?? "/metrics";
24615
25190
  const healthPath = opts.healthPath ?? "/healthz";
24616
25191
  const healthRegistry = opts.healthRegistry;
24617
25192
  const listener = (req, res) => {
@@ -24621,7 +25196,7 @@ async function startMetricsServer(opts) {
24621
25196
  return;
24622
25197
  }
24623
25198
  const url = req.url.split("?")[0];
24624
- if (url === path43) {
25199
+ if (url === path44) {
24625
25200
  let body;
24626
25201
  try {
24627
25202
  body = renderPrometheus(opts.sink.snapshot());
@@ -24667,14 +25242,14 @@ async function startMetricsServer(opts) {
24667
25242
  const { createServer } = await import('http');
24668
25243
  server = createServer(listener);
24669
25244
  }
24670
- await new Promise((resolve14, reject) => {
25245
+ await new Promise((resolve15, reject) => {
24671
25246
  const onError = (err) => {
24672
25247
  server.off("listening", onListening);
24673
25248
  reject(err);
24674
25249
  };
24675
25250
  const onListening = () => {
24676
25251
  server.off("error", onError);
24677
- resolve14();
25252
+ resolve15();
24678
25253
  };
24679
25254
  server.once("error", onError);
24680
25255
  server.once("listening", onListening);
@@ -24685,9 +25260,9 @@ async function startMetricsServer(opts) {
24685
25260
  const protocol = useHttps ? "https" : "http";
24686
25261
  return {
24687
25262
  port: boundPort,
24688
- url: `${protocol}://${host}:${boundPort}${path43}`,
24689
- close: () => new Promise((resolve14, reject) => {
24690
- server.close((err) => err ? reject(err) : resolve14());
25263
+ url: `${protocol}://${host}:${boundPort}${path44}`,
25264
+ close: () => new Promise((resolve15, reject) => {
25265
+ server.close((err) => err ? reject(err) : resolve15());
24691
25266
  })
24692
25267
  };
24693
25268
  }
@@ -25002,11 +25577,12 @@ function createContextManagerTool(opts = {}) {
25002
25577
  const applyMessages = (next) => {
25003
25578
  const repaired = repairToolUseAdjacency(next);
25004
25579
  const finalMessages = repaired.messages;
25580
+ if (finalMessages === messages) return repaired.report;
25005
25581
  if (ctx.state) {
25006
25582
  ctx.state.replaceMessages(finalMessages);
25007
25583
  } else {
25008
25584
  messages.length = 0;
25009
- messages.splice(0, 0, ...finalMessages);
25585
+ messages.push(...finalMessages);
25010
25586
  }
25011
25587
  return repaired.report;
25012
25588
  };
@@ -25081,9 +25657,15 @@ function createContextManagerTool(opts = {}) {
25081
25657
  }
25082
25658
  const report = await opts.compactor.compact(ctx);
25083
25659
  ctx.clearFileTracking();
25084
- const repair = applyMessages([...ctx.messages]);
25085
- const afterTokens = repair.changed ? roughEstimate(ctx.messages) : report.after;
25086
- const repaired = report.repaired ?? (repair.changed ? repair : void 0);
25660
+ let repaired = report.repaired;
25661
+ let afterTokens;
25662
+ if (!ctx.state) {
25663
+ const repair = applyMessages([...ctx.messages]);
25664
+ repaired = report.repaired ?? (repair.changed ? repair : void 0);
25665
+ afterTokens = repair.changed ? roughEstimate(ctx.messages) : report.after;
25666
+ } else {
25667
+ afterTokens = report.after;
25668
+ }
25087
25669
  const reduced = report.fullRequestTokensBefore > report.fullRequestTokensAfter;
25088
25670
  const repairedSomething = !!report.repaired;
25089
25671
  if (reduced || repairedSomething) {
@@ -25603,13 +26185,13 @@ var SkillInstaller = class {
25603
26185
  context: { reason: "path_traversal", skillName: skill.name }
25604
26186
  });
25605
26187
  }
25606
- const stat13 = await fsp3.stat(srcPath);
25607
- if (stat13.size > MAX_SKILL_FILE_SIZE) {
26188
+ const stat14 = await fsp3.stat(srcPath);
26189
+ if (stat14.size > MAX_SKILL_FILE_SIZE) {
25608
26190
  throw new FsError({
25609
- message: `Skill file "${file}" is too large (${(stat13.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`,
26191
+ message: `Skill file "${file}" is too large (${(stat14.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`,
25610
26192
  code: ERROR_CODES.FS_WRITE_FAILED,
25611
26193
  path: srcPath,
25612
- context: { skillName: skill.name, fileSize: stat13.size, maxSize: MAX_SKILL_FILE_SIZE }
26194
+ context: { skillName: skill.name, fileSize: stat14.size, maxSize: MAX_SKILL_FILE_SIZE }
25613
26195
  });
25614
26196
  }
25615
26197
  await fsp3.mkdir(path2.dirname(destPath), { recursive: true });
@@ -26833,15 +27415,15 @@ var SessionRecovery = class {
26833
27415
  async detectStale(sessionId) {
26834
27416
  const fp = this.filePath(sessionId);
26835
27417
  const TAIL_SIZE = 8192;
26836
- let stat13;
27418
+ let stat14;
26837
27419
  try {
26838
- stat13 = await fsp3.stat(fp);
27420
+ stat14 = await fsp3.stat(fp);
26839
27421
  } catch (err) {
26840
27422
  if (err.code === "ENOENT") return null;
26841
27423
  return null;
26842
27424
  }
26843
- if (stat13.size === 0) return null;
26844
- const position = Math.max(0, stat13.size - TAIL_SIZE);
27425
+ if (stat14.size === 0) return null;
27426
+ const position = Math.max(0, stat14.size - TAIL_SIZE);
26845
27427
  const buf = Buffer.alloc(TAIL_SIZE);
26846
27428
  let fh;
26847
27429
  try {
@@ -27309,6 +27891,10 @@ function sortKeys2(value) {
27309
27891
  init_session_registry();
27310
27892
 
27311
27893
  // src/agent-status-tracker.ts
27894
+ var AGENT_REAP_MS = 3e4;
27895
+ var AGENT_SWEEP_INTERVAL_MS = 1e4;
27896
+ var PARTIAL_TEXT_CAP = 1200;
27897
+ var PARTIAL_FLUSH_THROTTLE_MS = 300;
27312
27898
  var AgentStatusTracker = class {
27313
27899
  events;
27314
27900
  registry;
@@ -27320,11 +27906,21 @@ var AgentStatusTracker = class {
27320
27906
  leaderCurrentTool;
27321
27907
  leaderIterations = 0;
27322
27908
  leaderToolCalls = 0;
27909
+ leaderCostUsd = 0;
27910
+ leaderTokensIn = 0;
27911
+ leaderTokensOut = 0;
27912
+ leaderCtxPct;
27913
+ leaderModel;
27914
+ leaderPartialText = "";
27323
27915
  unsubscribers = [];
27916
+ onUpdate;
27917
+ sweepTimer = null;
27918
+ partialTimer = null;
27324
27919
  constructor(opts) {
27325
27920
  this.events = opts.events;
27326
27921
  this.registry = opts.registry;
27327
27922
  this.leaderName = opts.leaderName ?? "leader";
27923
+ this.onUpdate = opts.onUpdate;
27328
27924
  }
27329
27925
  start() {
27330
27926
  this.unsubscribers.push(
@@ -27334,10 +27930,22 @@ var AgentStatusTracker = class {
27334
27930
  this.flush();
27335
27931
  })
27336
27932
  );
27933
+ this.unsubscribers.push(
27934
+ this.events.onPattern("iteration.started", (_e, payload) => {
27935
+ const ctx = payload?.ctx;
27936
+ if (!ctx) return;
27937
+ if (ctx.model) this.leaderModel = ctx.model;
27938
+ if (typeof ctx.tokenCount === "number" && typeof ctx.maxContext === "number" && ctx.maxContext > 0) {
27939
+ this.leaderCtxPct = Math.round(ctx.tokenCount / ctx.maxContext * 100);
27940
+ }
27941
+ this.flush();
27942
+ })
27943
+ );
27337
27944
  this.unsubscribers.push(
27338
27945
  this.events.onPattern("agent.run.completed", () => {
27339
27946
  this.leaderStatus = "idle";
27340
27947
  this.leaderCurrentTool = void 0;
27948
+ this.leaderPartialText = "";
27341
27949
  this.flush();
27342
27950
  })
27343
27951
  );
@@ -27345,6 +27953,7 @@ var AgentStatusTracker = class {
27345
27953
  this.events.onPattern("agent.run.error", () => {
27346
27954
  this.leaderStatus = "error";
27347
27955
  this.leaderCurrentTool = void 0;
27956
+ this.leaderPartialText = "";
27348
27957
  this.flush();
27349
27958
  })
27350
27959
  );
@@ -27374,74 +27983,120 @@ var AgentStatusTracker = class {
27374
27983
  this.unsubscribers.push(
27375
27984
  this.events.onPattern("llm.stream_started", () => {
27376
27985
  this.leaderStatus = "streaming";
27986
+ this.leaderPartialText = "";
27377
27987
  this.flush();
27378
27988
  })
27379
27989
  );
27380
27990
  this.unsubscribers.push(
27381
- this.events.onPattern("fleet.subagent.spawned", (_event, payload) => {
27991
+ this.events.onPattern("provider.text_delta", (_e, payload) => {
27992
+ const text = payload?.text;
27993
+ if (!text) return;
27994
+ this.leaderStatus = "streaming";
27995
+ const next = this.leaderPartialText + text;
27996
+ this.leaderPartialText = next.length > PARTIAL_TEXT_CAP ? next.slice(next.length - PARTIAL_TEXT_CAP) : next;
27997
+ this.schedulePartialFlush();
27998
+ })
27999
+ );
28000
+ this.unsubscribers.push(
28001
+ this.events.onPattern("token.accounted", (_e, payload) => {
27382
28002
  const p = payload;
27383
- if (p?.subagentId) {
27384
- this.agents.set(p.subagentId, {
27385
- id: p.subagentId,
27386
- name: p.name ?? p.subagentId,
27387
- status: "idle",
27388
- iterations: 0,
27389
- toolCalls: 0,
27390
- lastActivityAt: (/* @__PURE__ */ new Date()).toISOString()
27391
- });
27392
- this.flush();
27393
- }
28003
+ if (!p) return;
28004
+ this.leaderTokensIn += p.usage?.input ?? 0;
28005
+ this.leaderTokensOut += p.usage?.output ?? 0;
28006
+ this.leaderCostUsd += p.cost?.total ?? 0;
28007
+ this.flush();
27394
28008
  })
27395
28009
  );
28010
+ const touch = (id) => {
28011
+ let entry = this.agents.get(id);
28012
+ if (!entry) {
28013
+ entry = { id, name: id, status: "idle", iterations: 0, toolCalls: 0, lastActivityAt: (/* @__PURE__ */ new Date()).toISOString() };
28014
+ this.agents.set(id, entry);
28015
+ }
28016
+ entry.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
28017
+ return entry;
28018
+ };
27396
28019
  this.unsubscribers.push(
27397
- this.events.onPattern("fleet.subagent.task_started", (_event, payload) => {
28020
+ this.events.onPattern("subagent.spawned", (_e, payload) => {
27398
28021
  const p = payload;
27399
- if (p?.subagentId) {
27400
- const entry = this.agents.get(p.subagentId);
27401
- if (entry) {
27402
- entry.status = "running";
27403
- entry.iterations++;
27404
- entry.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
27405
- this.flush();
27406
- }
27407
- }
28022
+ if (!p?.subagentId) return;
28023
+ const entry = touch(p.subagentId);
28024
+ entry.name = p.name?.trim() || entry.name;
28025
+ if (p.model) entry.model = p.model;
28026
+ entry.status = "running";
28027
+ this.flush();
27408
28028
  })
27409
28029
  );
27410
28030
  this.unsubscribers.push(
27411
- this.events.onPattern("fleet.subagent.task_completed", (_event, payload) => {
28031
+ this.events.onPattern("subagent.ctx_pct", (_e, payload) => {
27412
28032
  const p = payload;
27413
- if (p?.subagentId) {
27414
- const entry = this.agents.get(p.subagentId);
27415
- if (entry) {
27416
- entry.status = "idle";
27417
- entry.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
27418
- this.flush();
27419
- }
27420
- }
28033
+ if (!p?.subagentId) return;
28034
+ const entry = touch(p.subagentId);
28035
+ if (typeof p.load === "number") entry.ctxPct = Math.round(p.load * 100);
28036
+ this.flush();
27421
28037
  })
27422
28038
  );
27423
28039
  this.unsubscribers.push(
27424
- this.events.onPattern("fleet.subagent.error", (_event, payload) => {
28040
+ this.events.onPattern("subagent.task_started", (_e, payload) => {
27425
28041
  const p = payload;
27426
- if (p?.subagentId) {
27427
- const entry = this.agents.get(p.subagentId);
27428
- if (entry) {
27429
- entry.status = "error";
27430
- entry.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
27431
- this.flush();
27432
- }
27433
- }
28042
+ if (!p?.subagentId) return;
28043
+ const entry = touch(p.subagentId);
28044
+ entry.status = "running";
28045
+ entry.iterations++;
28046
+ this.flush();
27434
28047
  })
27435
28048
  );
27436
28049
  this.unsubscribers.push(
27437
- this.events.onPattern("fleet.subagent.stopped", (_event, payload) => {
28050
+ this.events.onPattern("subagent.tool_executed", (_e, payload) => {
27438
28051
  const p = payload;
27439
- if (p?.subagentId) {
27440
- this.agents.delete(p.subagentId);
27441
- this.flush();
28052
+ if (!p?.subagentId) return;
28053
+ const entry = touch(p.subagentId);
28054
+ entry.status = "running";
28055
+ entry.currentTool = p.name;
28056
+ entry.toolCalls++;
28057
+ this.flush();
28058
+ })
28059
+ );
28060
+ this.unsubscribers.push(
28061
+ this.events.onPattern("subagent.iteration_summary", (_e, payload) => {
28062
+ const p = payload;
28063
+ if (!p?.subagentId) return;
28064
+ const entry = touch(p.subagentId);
28065
+ entry.status = "running";
28066
+ if (typeof p.iteration === "number") entry.iterations = p.iteration;
28067
+ if (typeof p.toolCalls === "number") entry.toolCalls = p.toolCalls;
28068
+ if (typeof p.costUsd === "number") entry.costUsd = p.costUsd;
28069
+ if (p.currentTool) entry.currentTool = p.currentTool;
28070
+ if (typeof p.partialText === "string") {
28071
+ entry.partialText = p.partialText.length > PARTIAL_TEXT_CAP ? p.partialText.slice(p.partialText.length - PARTIAL_TEXT_CAP) : p.partialText;
27442
28072
  }
28073
+ this.flush();
28074
+ })
28075
+ );
28076
+ this.unsubscribers.push(
28077
+ this.events.onPattern("subagent.task_completed", (_e, payload) => {
28078
+ const p = payload;
28079
+ if (!p?.subagentId) return;
28080
+ const entry = this.agents.get(p.subagentId);
28081
+ if (!entry) return;
28082
+ entry.status = p.status === "failed" || p.status === "timeout" ? "error" : "idle";
28083
+ entry.currentTool = void 0;
28084
+ entry.partialText = void 0;
28085
+ if (typeof p.iterations === "number") entry.iterations = p.iterations;
28086
+ if (typeof p.toolCalls === "number") entry.toolCalls = p.toolCalls;
28087
+ entry.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
28088
+ this.flush();
27443
28089
  })
27444
28090
  );
28091
+ this.unsubscribers.push(
28092
+ this.events.onPattern("subagent.stopped", (_e, payload) => {
28093
+ const p = payload;
28094
+ if (!p?.subagentId) return;
28095
+ if (this.agents.delete(p.subagentId)) this.flush();
28096
+ })
28097
+ );
28098
+ this.sweepTimer = setInterval(() => this.sweep(), AGENT_SWEEP_INTERVAL_MS);
28099
+ if (typeof this.sweepTimer.unref === "function") this.sweepTimer.unref();
27445
28100
  }
27446
28101
  stop() {
27447
28102
  for (const unsub of this.unsubscribers) {
@@ -27451,6 +28106,45 @@ var AgentStatusTracker = class {
27451
28106
  }
27452
28107
  }
27453
28108
  this.unsubscribers = [];
28109
+ if (this.sweepTimer) {
28110
+ clearInterval(this.sweepTimer);
28111
+ this.sweepTimer = null;
28112
+ }
28113
+ if (this.partialTimer) {
28114
+ clearTimeout(this.partialTimer);
28115
+ this.partialTimer = null;
28116
+ }
28117
+ }
28118
+ /**
28119
+ * Coalesce streamed-text flushes: at most one registry write per
28120
+ * {@link PARTIAL_FLUSH_THROTTLE_MS} while text streams in, so per-token
28121
+ * deltas never thrash the cross-process registry file.
28122
+ */
28123
+ schedulePartialFlush() {
28124
+ if (this.partialTimer) return;
28125
+ this.partialTimer = setTimeout(() => {
28126
+ this.partialTimer = null;
28127
+ this.flush();
28128
+ }, PARTIAL_FLUSH_THROTTLE_MS);
28129
+ if (typeof this.partialTimer.unref === "function") this.partialTimer.unref();
28130
+ }
28131
+ /**
28132
+ * Remove subagents that have been finished (idle/error) for longer than
28133
+ * {@link AGENT_REAP_MS}. Running / streaming / waiting_user agents are kept
28134
+ * regardless of age — only *not-working* agents are reaped.
28135
+ */
28136
+ sweep() {
28137
+ const now = Date.now();
28138
+ let removed = false;
28139
+ for (const [id, a] of this.agents) {
28140
+ const finished = a.status !== "running" && a.status !== "streaming" && a.status !== "waiting_user";
28141
+ const age = now - Date.parse(a.lastActivityAt);
28142
+ if (finished && Number.isFinite(age) && age > AGENT_REAP_MS) {
28143
+ this.agents.delete(id);
28144
+ removed = true;
28145
+ }
28146
+ }
28147
+ if (removed) this.flush();
27454
28148
  }
27455
28149
  flush() {
27456
28150
  const leaderEntry = {
@@ -27460,12 +28154,105 @@ var AgentStatusTracker = class {
27460
28154
  currentTool: this.leaderCurrentTool,
27461
28155
  iterations: this.leaderIterations,
27462
28156
  toolCalls: this.leaderToolCalls,
28157
+ costUsd: this.leaderCostUsd,
28158
+ tokensIn: this.leaderTokensIn,
28159
+ tokensOut: this.leaderTokensOut,
28160
+ ctxPct: this.leaderCtxPct,
28161
+ model: this.leaderModel,
28162
+ partialText: this.leaderPartialText || void 0,
27463
28163
  lastActivityAt: (/* @__PURE__ */ new Date()).toISOString()
27464
28164
  };
27465
28165
  const allAgents = [leaderEntry, ...this.agents.values()];
27466
- this.registry.updateAgents(allAgents).catch(() => void 0);
28166
+ this.registry.updateAgents(allAgents).then(() => {
28167
+ try {
28168
+ this.onUpdate?.();
28169
+ } catch {
28170
+ }
28171
+ }).catch(() => void 0);
28172
+ }
28173
+ };
28174
+ var INSTANCES_FILE = "webui-instances.json";
28175
+ var DISCOVERY_TTL_MS = 2500;
28176
+ var COALESCE_MS = 50;
28177
+ var POST_TIMEOUT_MS = 500;
28178
+ function pidAlive2(pid) {
28179
+ if (!Number.isInteger(pid) || pid <= 0) return false;
28180
+ try {
28181
+ process.kill(pid, 0);
28182
+ return true;
28183
+ } catch (err) {
28184
+ return err.code !== "ESRCH";
28185
+ }
28186
+ }
28187
+ function normRoot(root) {
28188
+ const resolved = path2.resolve(root);
28189
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
28190
+ }
28191
+ var FleetNotifier = class {
28192
+ baseDir;
28193
+ projectRoot;
28194
+ selfPid;
28195
+ doPost;
28196
+ cache = null;
28197
+ timer = null;
28198
+ disposed = false;
28199
+ constructor(opts) {
28200
+ this.baseDir = opts.baseDir;
28201
+ this.projectRoot = normRoot(opts.projectRoot);
28202
+ this.selfPid = opts.selfPid ?? process.pid;
28203
+ this.doPost = opts.post ?? defaultPost;
28204
+ }
28205
+ /** Coalesced, best-effort nudge. Safe to call on every status change. */
28206
+ notify() {
28207
+ if (this.disposed || this.timer) return;
28208
+ this.timer = setTimeout(() => {
28209
+ this.timer = null;
28210
+ void this.flush();
28211
+ }, COALESCE_MS);
28212
+ if (typeof this.timer.unref === "function") this.timer.unref();
28213
+ }
28214
+ /** Resolve same-project WebUI ping URLs (cached briefly). Exposed for tests. */
28215
+ async endpoints() {
28216
+ const now = Date.now();
28217
+ if (this.cache && now - this.cache.at < DISCOVERY_TTL_MS) return this.cache.urls;
28218
+ const urls = await this.discover();
28219
+ this.cache = { at: now, urls };
28220
+ return urls;
28221
+ }
28222
+ dispose() {
28223
+ this.disposed = true;
28224
+ if (this.timer) {
28225
+ clearTimeout(this.timer);
28226
+ this.timer = null;
28227
+ }
28228
+ }
28229
+ async flush() {
28230
+ const urls = await this.endpoints();
28231
+ await Promise.all(urls.map((u) => this.doPost(u).catch(() => void 0)));
28232
+ }
28233
+ async discover() {
28234
+ try {
28235
+ const raw = await fsp3.readFile(path2.join(this.baseDir, INSTANCES_FILE), "utf8");
28236
+ const data = JSON.parse(raw);
28237
+ const list = Array.isArray(data?.instances) ? data.instances : [];
28238
+ return list.filter((i) => i && typeof i.httpPort === "number").filter((i) => i.pid !== this.selfPid).filter((i) => normRoot(i.projectRoot) === this.projectRoot).filter((i) => pidAlive2(i.pid)).map((i) => {
28239
+ const host = i.host === "0.0.0.0" || i.host === "::" || !i.host ? "127.0.0.1" : i.host;
28240
+ return `http://${host}:${i.httpPort}/api/fleet/ping`;
28241
+ });
28242
+ } catch {
28243
+ return [];
28244
+ }
27467
28245
  }
27468
28246
  };
28247
+ async function defaultPost(url) {
28248
+ const ac = new AbortController();
28249
+ const t2 = setTimeout(() => ac.abort(), POST_TIMEOUT_MS);
28250
+ try {
28251
+ await fetch(url, { method: "POST", signal: ac.signal });
28252
+ } finally {
28253
+ clearTimeout(t2);
28254
+ }
28255
+ }
27469
28256
 
27470
28257
  // src/storage/session-rewinder.ts
27471
28258
  init_atomic_write();
@@ -27715,6 +28502,7 @@ async function saveTasks(filePath, tasks, events, traceId) {
27715
28502
  durationMs: Date.now() - t0,
27716
28503
  ...traceId !== void 0 && { traceId }
27717
28504
  });
28505
+ return true;
27718
28506
  } catch (err) {
27719
28507
  events?.emit("storage.error", {
27720
28508
  sessionId: traceId ?? "~boot~",
@@ -27730,13 +28518,17 @@ async function saveTasks(filePath, tasks, events, traceId) {
27730
28518
  "[task-store] save failed:",
27731
28519
  toErrorMessage(err)
27732
28520
  );
28521
+ return false;
27733
28522
  }
27734
28523
  }
27735
28524
  async function mutateTasks(filePath, sessionId, fn, events, traceId) {
27736
28525
  return withFileLock(filePath, async () => {
27737
28526
  const file = await loadTasks(filePath, events, traceId) ?? emptyTaskFile(sessionId);
27738
28527
  const updated = await fn(file);
27739
- await saveTasks(filePath, updated, events, traceId);
28528
+ const persisted = await saveTasks(filePath, updated, events, traceId);
28529
+ if (!persisted) {
28530
+ throw new Error(`Failed to persist tasks to ${filePath} \u2014 the change was NOT saved.`);
28531
+ }
27740
28532
  return updated;
27741
28533
  });
27742
28534
  }
@@ -28033,8 +28825,8 @@ var CloudSync = class {
28033
28825
  const localPath = this.categoryToPath(cat);
28034
28826
  if (!localPath) continue;
28035
28827
  try {
28036
- const stat13 = await fsp3.stat(localPath);
28037
- if (stat13.isDirectory()) {
28828
+ const stat14 = await fsp3.stat(localPath);
28829
+ if (stat14.isDirectory()) {
28038
28830
  const files = await this.walkDir(localPath, localPath);
28039
28831
  for (const file of files) {
28040
28832
  const content = await fsp3.readFile(file, "utf8");
@@ -28059,8 +28851,8 @@ var CloudSync = class {
28059
28851
  const localPath = this.categoryToPath(cat);
28060
28852
  if (!localPath) continue;
28061
28853
  try {
28062
- const stat13 = await fsp3.stat(localPath);
28063
- if (stat13.isDirectory()) {
28854
+ const stat14 = await fsp3.stat(localPath);
28855
+ if (stat14.isDirectory()) {
28064
28856
  const files = await this.walkDir(localPath, localPath);
28065
28857
  for (const file of files) {
28066
28858
  const content = await fsp3.readFile(file);
@@ -28087,6 +28879,7 @@ var CloudSync = class {
28087
28879
  return this.paths.globalMemory;
28088
28880
  case "history":
28089
28881
  return this.paths.historyFile;
28882
+ /* v8 ignore next -- unreachable: SyncCategory is exhaustively matched above */
28090
28883
  default:
28091
28884
  return null;
28092
28885
  }
@@ -29316,7 +30109,7 @@ var SecurityScannerOrchestrator = class {
29316
30109
  message: errAsErr.message,
29317
30110
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
29318
30111
  }));
29319
- await new Promise((resolve14) => setTimeout(resolve14, delay));
30112
+ await new Promise((resolve15) => setTimeout(resolve15, delay));
29320
30113
  return this.completeWithRetry(provider, request, abortController, attempt + 1);
29321
30114
  }
29322
30115
  }
@@ -29980,16 +30773,16 @@ Use \`/security report <number>\` to view a specific report.` };
29980
30773
  }
29981
30774
  const index = Number.parseInt(reportId, 10) - 1;
29982
30775
  if (!Number.isNaN(index) && reports[index]) {
29983
- const { readFile: readFile47 } = await import('fs/promises');
29984
- const content = await readFile47(join(reportsDir, reports[index]), "utf-8");
30776
+ const { readFile: readFile48 } = await import('fs/promises');
30777
+ const content = await readFile48(join(reportsDir, reports[index]), "utf-8");
29985
30778
  return { message: `# Security Report
29986
30779
 
29987
30780
  ${content}` };
29988
30781
  }
29989
30782
  const match = reports.find((r) => r.includes(reportId));
29990
30783
  if (match) {
29991
- const { readFile: readFile47 } = await import('fs/promises');
29992
- const content = await readFile47(join(reportsDir, match), "utf-8");
30784
+ const { readFile: readFile48 } = await import('fs/promises');
30785
+ const content = await readFile48(join(reportsDir, match), "utf-8");
29993
30786
  return { message: `# Security Report
29994
30787
 
29995
30788
  ${content}` };
@@ -30449,12 +31242,12 @@ var BrainDecisionQueue = class {
30449
31242
  options: request.options,
30450
31243
  rationale: "Decision escalated to human authority."
30451
31244
  };
30452
- const pending = new Promise((resolve14) => {
30453
- const entry = { request, resolve: resolve14 };
31245
+ const pending = new Promise((resolve15) => {
31246
+ const entry = { request, resolve: resolve15 };
30454
31247
  if (this.opts.timeoutMs && this.opts.timeoutMs > 0) {
30455
31248
  entry.timer = setTimeout(() => {
30456
31249
  this.pending.delete(request.id);
30457
- resolve14({ type: "deny", reason: "Brain human decision timed out." });
31250
+ resolve15({ type: "deny", reason: "Brain human decision timed out." });
30458
31251
  }, this.opts.timeoutMs);
30459
31252
  }
30460
31253
  this.pending.set(request.id, entry);
@@ -30868,7 +31661,8 @@ var BrainMonitor = class {
30868
31661
  id: "steer",
30869
31662
  label: "Steer the agent with corrective guidance",
30870
31663
  consequence: "A steer message is injected before its next step.",
30871
- risk: "low"
31664
+ risk: "low",
31665
+ recommended: true
30872
31666
  },
30873
31667
  {
30874
31668
  id: "continue",
@@ -30877,9 +31671,9 @@ var BrainMonitor = class {
30877
31671
  }
30878
31672
  ],
30879
31673
  risk: "medium",
30880
- // Without an LLM layer the policy brain resolves this fallback to
30881
- // "continue" the monitor observes but never interferes.
30882
- fallback: "continue"
31674
+ // 'ask_human' routes to the LLM-backed autonomous layer via
31675
+ // createTieredBrainArbiter before any human escalation.
31676
+ fallback: "ask_human"
30883
31677
  };
30884
31678
  const decision = await this.opts.brain.decide(request);
30885
31679
  const intervened = await this.maybeIntervene(kind, request, decision);
@@ -31979,6 +32773,10 @@ function makeDependencyWatcherConfig(opts) {
31979
32773
  return {
31980
32774
  watchPaths: unique,
31981
32775
  debounceMs,
32776
+ dispose() {
32777
+ for (const t2 of pending.values()) clearTimeout(t2);
32778
+ pending.clear();
32779
+ },
31982
32780
  async onChange(entry) {
31983
32781
  if (entry.event === "delete") return;
31984
32782
  if (!matchesPattern(entry.path)) return;
@@ -32399,6 +33197,10 @@ var KnowledgeGraph = class {
32399
33197
  pendingDeliveries = /* @__PURE__ */ new Map();
32400
33198
  filePath;
32401
33199
  graphFilePath;
33200
+ /** Exposed for unit-testing only: read current index contents. */
33201
+ getIndex() {
33202
+ return this.index;
33203
+ }
32402
33204
  constructor(sessionDir) {
32403
33205
  this.filePath = path2.join(sessionDir, "_knowledge_graph");
32404
33206
  this.graphFilePath = path2.join(this.filePath, "graph.jsonl");
@@ -32411,7 +33213,7 @@ var KnowledgeGraph = class {
32411
33213
  async add(node) {
32412
33214
  const full = { id: randomUUID(), ...node };
32413
33215
  this.nodes.set(full.id, full);
32414
- this._index(full);
33216
+ this._addToIndex(full, this._indexKeys(full));
32415
33217
  await this._persist(full);
32416
33218
  this._deliver(full);
32417
33219
  return full;
@@ -32420,8 +33222,10 @@ var KnowledgeGraph = class {
32420
33222
  async update(id, patch) {
32421
33223
  const existing = this.nodes.get(id);
32422
33224
  if (!existing) return null;
33225
+ this._removeFromIndex(existing, this._indexKeys(existing));
32423
33226
  const updated = { ...existing, ...patch };
32424
33227
  this.nodes.set(id, updated);
33228
+ this._addToIndex(updated, this._indexKeys(updated));
32425
33229
  this._deliver(updated);
32426
33230
  await this._append(updated);
32427
33231
  return updated;
@@ -32507,15 +33311,10 @@ var KnowledgeGraph = class {
32507
33311
  return { passed: checks.every((c) => c.passed), checks };
32508
33312
  }
32509
33313
  // ── Private ────────────────────────────────────────────────────────────
32510
- _index(node) {
32511
- const add = (key) => {
32512
- let set = this.index.get(key);
32513
- if (!set) {
32514
- set = /* @__PURE__ */ new Set();
32515
- this.index.set(key, set);
32516
- }
32517
- set.add(node.id);
32518
- };
33314
+ /** Pure: compute the set of index keys a node would belong to. */
33315
+ _indexKeys(node) {
33316
+ const keys = /* @__PURE__ */ new Set();
33317
+ const add = (key) => keys.add(key);
32519
33318
  add(`type:${node.type}`);
32520
33319
  if (node.type === "fact") {
32521
33320
  const f = node;
@@ -32524,6 +33323,8 @@ var KnowledgeGraph = class {
32524
33323
  add(`by:${f.discoveredBy}`);
32525
33324
  for (const tag of f.tags) add(`tag:${tag}`);
32526
33325
  add(`key:${f.key}`);
33326
+ add(`subject:${f.subject}`);
33327
+ if (f.detail) add(`detail:${f.detail}`);
32527
33328
  }
32528
33329
  if (node.type === "goal") {
32529
33330
  const g = node;
@@ -32538,6 +33339,24 @@ var KnowledgeGraph = class {
32538
33339
  add(`by:${c.proposedBy}`);
32539
33340
  for (const g of c.satisfiesGoals) add(`goal:${g}`);
32540
33341
  }
33342
+ return keys;
33343
+ }
33344
+ /** Mutate the index: add a node's id to every set for the given keys. */
33345
+ _addToIndex(node, keys) {
33346
+ for (const key of keys) {
33347
+ let set = this.index.get(key);
33348
+ if (!set) {
33349
+ set = /* @__PURE__ */ new Set();
33350
+ this.index.set(key, set);
33351
+ }
33352
+ set.add(node.id);
33353
+ }
33354
+ }
33355
+ /** Remove a node's id from all index sets for the given keys. */
33356
+ _removeFromIndex(node, keys) {
33357
+ for (const key of keys) {
33358
+ this.index.get(key)?.delete(node.id);
33359
+ }
32541
33360
  }
32542
33361
  _matches(node, f) {
32543
33362
  if (f.type && node.type !== f.type) return false;
@@ -32587,11 +33406,15 @@ var KnowledgeGraph = class {
32587
33406
  try {
32588
33407
  const parsed = JSON.parse(line);
32589
33408
  if (parsed.op === "update") {
33409
+ const oldNode = this.nodes.get(parsed.node.id);
33410
+ if (oldNode) {
33411
+ this._removeFromIndex(oldNode, this._indexKeys(oldNode));
33412
+ }
32590
33413
  this.nodes.set(parsed.node.id, parsed.node);
32591
- this._index(parsed.node);
33414
+ this._addToIndex(parsed.node, this._indexKeys(parsed.node));
32592
33415
  } else {
32593
33416
  this.nodes.set(parsed.id, parsed);
32594
- this._index(parsed);
33417
+ this._addToIndex(parsed, this._indexKeys(parsed));
32595
33418
  }
32596
33419
  } catch {
32597
33420
  }
@@ -32892,7 +33715,7 @@ var TaskDAG = class {
32892
33715
  if (visited.has(current)) continue;
32893
33716
  visited.add(current);
32894
33717
  const node = this.nodes.get(current);
32895
- if (node) stack.push(...node.dependents);
33718
+ if (node) stack.push(...node.deps);
32896
33719
  }
32897
33720
  return false;
32898
33721
  }
@@ -32995,8 +33818,10 @@ var ConsensusProtocol = class {
32995
33818
  return this._resolve(changeId, change.votes, eligible);
32996
33819
  }
32997
33820
  // ── Private ───────────────────────────────────────────────────────────
32998
- _eligibleVoters(_change) {
32999
- return Array.from(this.voters.keys());
33821
+ _eligibleVoters(change) {
33822
+ return Array.from(this.voters.keys()).filter(
33823
+ (agentId) => agentId !== change.proposedBy
33824
+ );
33000
33825
  }
33001
33826
  _resolve(changeId, votes, eligible) {
33002
33827
  const totalEligible = eligible.length;
@@ -33041,7 +33866,7 @@ var ConsensusProtocol = class {
33041
33866
  if (!quorumMet) {
33042
33867
  return {
33043
33868
  changeId,
33044
- outcome: "pending",
33869
+ outcome: "quorum_not_met",
33045
33870
  votes,
33046
33871
  approveCount: approve.length,
33047
33872
  rejectCount: reject.length,
@@ -33418,7 +34243,7 @@ var AutonomousBrain = class {
33418
34243
  }
33419
34244
  return { type: "deny", reason: `Brain LLM failed: ${String(err)}` };
33420
34245
  }
33421
- void this._recordDecision({
34246
+ this._recordDecision({
33422
34247
  id,
33423
34248
  decisionType,
33424
34249
  question,
@@ -33427,6 +34252,7 @@ var AutonomousBrain = class {
33427
34252
  rationale: result.rationale,
33428
34253
  madeBy: "autonomous-brain",
33429
34254
  context: JSON.stringify(context)
34255
+ }).catch(() => {
33430
34256
  });
33431
34257
  if (requiresConsensus) {
33432
34258
  this._emit("brain.decision", { id, decisionType, optionId: result.optionId, rationale: result.rationale, consensusRequired: true });
@@ -33705,10 +34531,16 @@ var TaskAuctioneer = class {
33705
34531
  maxTasksPerAgent;
33706
34532
  minConfidence;
33707
34533
  // minimum dispatcher confidence to accept a bid
34534
+ maxBidRetries;
34535
+ // max republished attempts before marking task failed
33708
34536
  /** Pending bids keyed by taskId. */
33709
34537
  pendingBids = /* @__PURE__ */ new Map();
33710
34538
  /** Active bid windows keyed by taskId. */
33711
34539
  bidTimers = /* @__PURE__ */ new Map();
34540
+ /** FleetBus subscription disposers, detached in dispose(). */
34541
+ unsubs = [];
34542
+ /** How many times a task has been republished with no bids received. */
34543
+ bidRetryCounts = /* @__PURE__ */ new Map();
33712
34544
  /** Agent → current task count (from graph + in-flight). */
33713
34545
  agentTaskCounts = /* @__PURE__ */ new Map();
33714
34546
  constructor(opts) {
@@ -33719,8 +34551,26 @@ var TaskAuctioneer = class {
33719
34551
  this.bidWindowMs = opts.bidWindowMs ?? 3e4;
33720
34552
  this.maxTasksPerAgent = opts.maxTasksPerAgent ?? 3;
33721
34553
  this.minConfidence = opts.minConfidence ?? 0.3;
33722
- this.fleet?.filter("task:bid", (e) => this._onBidEvent(e));
33723
- this.fleet?.filter("task:claimed", (e) => this._onClaimedEvent(e));
34554
+ this.maxBidRetries = opts.maxBidRetries ?? 3;
34555
+ const offBid = this.fleet?.filter("task:bid", (e) => this._onBidEvent(e));
34556
+ const offClaimed = this.fleet?.filter("task:claimed", (e) => this._onClaimedEvent(e));
34557
+ if (offBid) this.unsubs.push(offBid);
34558
+ if (offClaimed) this.unsubs.push(offClaimed);
34559
+ }
34560
+ /**
34561
+ * Detach all FleetBus subscriptions and cancel any open bid-window timers.
34562
+ * Call when the owning coordinator stops/restarts so handlers and timers
34563
+ * don't accumulate across cycles.
34564
+ */
34565
+ dispose() {
34566
+ for (const off of this.unsubs.splice(0)) {
34567
+ try {
34568
+ off();
34569
+ } catch {
34570
+ }
34571
+ }
34572
+ for (const t2 of this.bidTimers.values()) clearTimeout(t2);
34573
+ this.bidTimers.clear();
33724
34574
  }
33725
34575
  // ── Publish a task ────────────────────────────────────────────────────
33726
34576
  /**
@@ -33730,14 +34580,16 @@ var TaskAuctioneer = class {
33730
34580
  * If `targetAgent` is specified, the task is assigned directly without auction.
33731
34581
  */
33732
34582
  async publishTask(input) {
34583
+ const blockedBy = input.blockedBy ?? [];
34584
+ const hasOpenBlockers = blockedBy.length > 0 && blockedBy.some((id) => this.graph.get(id)?.status !== "done");
33733
34585
  const goal = await this.graph.add({
33734
34586
  type: "goal",
33735
34587
  title: input.title,
33736
34588
  description: input.description,
33737
- status: input.targetAgent ? "in_progress" : "pending",
34589
+ status: input.targetAgent ? "in_progress" : hasOpenBlockers ? "blocked" : "pending",
33738
34590
  priority: input.priority ?? "medium",
33739
34591
  assignee: input.targetAgent,
33740
- blockedBy: [],
34592
+ blockedBy,
33741
34593
  dependsOn: input.satisfiesGoals ?? [],
33742
34594
  createdBy: this.selfAgentId,
33743
34595
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -33754,6 +34606,12 @@ var TaskAuctioneer = class {
33754
34606
  });
33755
34607
  }
33756
34608
  }
34609
+ for (const blockerId of blockedBy) {
34610
+ const blocker = this.graph.get(blockerId);
34611
+ if (blocker && !blocker.children.includes(goal.id)) {
34612
+ await this.graph.update(blockerId, { children: [...blocker.children, goal.id] });
34613
+ }
34614
+ }
33757
34615
  if (input.targetAgent) {
33758
34616
  await this._assignDirect(goal.id, input.targetAgent);
33759
34617
  } else {
@@ -33865,9 +34723,12 @@ Priority: ${goal.priority}`,
33865
34723
  status: "done",
33866
34724
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
33867
34725
  });
34726
+ this.bidRetryCounts.delete(taskId);
34727
+ this.pendingBids.delete(taskId);
34728
+ this._cancelBidWindow(taskId);
33868
34729
  for (const childId of goal.children) {
33869
34730
  const child = this.graph.get(childId);
33870
- if (child) {
34731
+ if (child && child.status === "blocked") {
33871
34732
  const allUnblocked = child.blockedBy.every((blockedId) => {
33872
34733
  const blocked = this.graph.get(blockedId);
33873
34734
  return blocked?.status === "done";
@@ -33876,6 +34737,7 @@ Priority: ${goal.priority}`,
33876
34737
  await this.graph.update(childId, { status: "pending", updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
33877
34738
  const unblockedGoal = this.graph.get(childId);
33878
34739
  await this._broadcastTask(unblockedGoal);
34740
+ this._startBidWindow(childId);
33879
34741
  }
33880
34742
  }
33881
34743
  }
@@ -33901,6 +34763,9 @@ ${_result ?? "No result provided."}`
33901
34763
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
33902
34764
  result: error
33903
34765
  });
34766
+ this.bidRetryCounts.delete(taskId);
34767
+ this.pendingBids.delete(taskId);
34768
+ this._cancelBidWindow(taskId);
33904
34769
  this._emit("task:failed", { taskId, agentId, error });
33905
34770
  await this._mailboxPublish({
33906
34771
  type: "note",
@@ -33979,7 +34844,7 @@ Assignee: ${agentId}`
33979
34844
  priority: goal.priority,
33980
34845
  tags: goal.tags
33981
34846
  });
33982
- void this._mailboxPublish({
34847
+ this._mailboxPublish({
33983
34848
  type: "broadcast",
33984
34849
  subject: `[task] ${goal.title} (${goal.priority})`,
33985
34850
  body: `New task available: "${goal.title}"
@@ -33990,6 +34855,7 @@ Task ID: ${goal.id}
33990
34855
  Tags: ${goal.tags.join(", ") || "none"}
33991
34856
 
33992
34857
  Bid by calling taskAuctioneer.bid("${goal.id}", ...)`
34858
+ }).catch(() => {
33993
34859
  });
33994
34860
  }
33995
34861
  async _mailboxPublish(msg) {
@@ -34023,9 +34889,10 @@ Bid by calling taskAuctioneer.bid("${goal.id}", ...)`
34023
34889
  }
34024
34890
  _startBidWindow(taskId) {
34025
34891
  this._cancelBidWindow(taskId);
34026
- const timer = setTimeout(async () => {
34892
+ const timer = setTimeout(() => {
34027
34893
  this.bidTimers.delete(taskId);
34028
- await this._evaluateBids(taskId);
34894
+ void this._evaluateBids(taskId).catch(() => {
34895
+ });
34029
34896
  }, this.bidWindowMs);
34030
34897
  this.bidTimers.set(taskId, timer);
34031
34898
  }
@@ -34039,6 +34906,13 @@ Bid by calling taskAuctioneer.bid("${goal.id}", ...)`
34039
34906
  async _evaluateBids(taskId) {
34040
34907
  const bids = this.pendingBids.get(taskId);
34041
34908
  if (!bids || bids.length === 0) {
34909
+ const retryCount = (this.bidRetryCounts.get(taskId) ?? 0) + 1;
34910
+ this.bidRetryCounts.set(taskId, retryCount);
34911
+ if (retryCount >= this.maxBidRetries) {
34912
+ await this.fail(taskId, `No bids received after ${this.maxBidRetries} attempts`);
34913
+ this.bidRetryCounts.delete(taskId);
34914
+ return;
34915
+ }
34042
34916
  const goal = this.graph.get(taskId);
34043
34917
  if (goal) {
34044
34918
  await this._broadcastTask(goal);
@@ -34047,7 +34921,15 @@ Bid by calling taskAuctioneer.bid("${goal.id}", ...)`
34047
34921
  return;
34048
34922
  }
34049
34923
  bids.sort((a, b) => b.score - a.score);
34050
- const winner = bids[0];
34924
+ const winner = bids.find((b) => this._getAgentTaskCount(b.agentId) < this.maxTasksPerAgent);
34925
+ if (!winner) {
34926
+ const goal = this.graph.get(taskId);
34927
+ if (goal) {
34928
+ await this._broadcastTask(goal);
34929
+ this._startBidWindow(taskId);
34930
+ }
34931
+ return;
34932
+ }
34051
34933
  await this.claim(taskId, winner.agentId, winner.agentName);
34052
34934
  }
34053
34935
  async _assignDirect(taskId, agentId) {
@@ -34094,16 +34976,24 @@ var AutonomousCoordinator = class {
34094
34976
  selfAgentId;
34095
34977
  fleet;
34096
34978
  fleetManager;
34979
+ director;
34097
34980
  mailbox;
34098
34981
  events;
34982
+ onCoordinatorEvent;
34099
34983
  running = false;
34100
34984
  iterationCount = 0;
34985
+ /** Tasks already handled by _onSubagentTerminated (to avoid double goal:failed on fleet event). */
34986
+ _handledBySubagent = /* @__PURE__ */ new Set();
34987
+ /** FleetBus subscription disposers, detached in dispose(). */
34988
+ unsubs = [];
34101
34989
  constructor(opts) {
34102
34990
  this.selfAgentId = opts.selfAgentId;
34103
34991
  this.fleet = opts.fleet ?? void 0;
34104
34992
  this.fleetManager = opts.fleetManager ?? void 0;
34993
+ this.director = opts.director ?? void 0;
34105
34994
  this.mailbox = opts.mailbox ?? void 0;
34106
34995
  this.events = opts.events ?? void 0;
34996
+ this.onCoordinatorEvent = opts.onCoordinatorEvent;
34107
34997
  this.graph = new KnowledgeGraph(opts.sessionDir);
34108
34998
  this.dag = new TaskDAG();
34109
34999
  this.auction = new TaskAuctioneer({
@@ -34139,9 +35029,19 @@ var AutonomousCoordinator = class {
34139
35029
  this.dag.onEvent((event) => {
34140
35030
  this._onDagEvent(event);
34141
35031
  });
34142
- this.fleet?.filter("subagent.terminated", (e) => {
35032
+ const offCompleted = this.fleet?.filter("subagent.completed", (e) => {
34143
35033
  this._onSubagentTerminated(e);
34144
35034
  });
35035
+ if (offCompleted) this.unsubs.push(offCompleted);
35036
+ const offFailed = this.fleet?.filter("task:failed", (e) => {
35037
+ const payload = e.payload;
35038
+ const taskId = payload?.taskId;
35039
+ if (!taskId || this._handledBySubagent.has(taskId)) return;
35040
+ this._handledBySubagent.add(taskId);
35041
+ this._emit({ type: "goal:failed", goalId: taskId, text: payload?.error ?? "Task failed" });
35042
+ });
35043
+ if (offFailed) this.unsubs.push(offFailed);
35044
+ this._emit({ type: "coordinator:mode", mode: this.fleet ? "fleet" : "standalone" });
34145
35045
  }
34146
35046
  // ── Public API ───────────────────────────────────────────────────────
34147
35047
  /**
@@ -34155,11 +35055,13 @@ var AutonomousCoordinator = class {
34155
35055
  const maxIterations = opts.maxIterations ?? 100;
34156
35056
  const goal = opts.goal ?? "Improve the codebase";
34157
35057
  const maxCost = opts.maxCostUsd;
34158
- await this.graph.load();
34159
35058
  try {
35059
+ await this.graph.load();
34160
35060
  const goalConfigs = await this._decomposeGoal(goal);
34161
35061
  for (const g of goalConfigs) {
34162
- await this.auction.publishTask(g);
35062
+ const goalId = await this.auction.publishTask(g);
35063
+ this.dag.addNode(goalId, g.description, []);
35064
+ this._emit({ type: "goal:added", goalId, title: g.title, text: g.description });
34163
35065
  }
34164
35066
  while (this.running) {
34165
35067
  this.iterationCount++;
@@ -34186,6 +35088,7 @@ var AutonomousCoordinator = class {
34186
35088
  const blocked = this.dag.getBlocked();
34187
35089
  if (blocked.length > 0 && this.dag.hasDeadlock()) {
34188
35090
  (this.events?.emit)("autonomous:deadlock", { blocked });
35091
+ this._emit({ type: "deadlock:detected", goalId: blocked[0]?.id ?? "", text: `Deadlock detected: ${blocked.map((n) => n.id).join(", ")}` });
34189
35092
  this.running = false;
34190
35093
  }
34191
35094
  break;
@@ -34202,7 +35105,15 @@ var AutonomousCoordinator = class {
34202
35105
  }
34203
35106
  const pendingChanges = this.changes.getPendingReviews();
34204
35107
  for (const change of pendingChanges) {
34205
- await this._handlePendingChange(change);
35108
+ try {
35109
+ await this._handlePendingChange(change);
35110
+ } catch (err) {
35111
+ this._emit({
35112
+ type: "goal:failed",
35113
+ goalId: change.id,
35114
+ text: `Consensus handling failed: ${err instanceof Error ? err.message : String(err)}`
35115
+ });
35116
+ }
34206
35117
  }
34207
35118
  }
34208
35119
  } finally {
@@ -34212,7 +35123,26 @@ var AutonomousCoordinator = class {
34212
35123
  }
34213
35124
  /** Stop the autonomous loop. */
34214
35125
  stop() {
35126
+ if (!this.running) return;
34215
35127
  this.running = false;
35128
+ console.error(`[AutonomousCoordinator] stop signal received \u2014 shutting down (iteration ${this.iterationCount})`);
35129
+ }
35130
+ /**
35131
+ * Tear down the coordinator for good: stop the loop and detach all FleetBus
35132
+ * subscriptions (this coordinator's + the auctioneer's) plus any open bid
35133
+ * timers. Call this when discarding the instance (e.g. `/coordinator stop`
35134
+ * that recreates a fresh coordinator on the next start) so handlers and
35135
+ * timers don't accumulate across cycles. `stop()` only pauses the loop.
35136
+ */
35137
+ dispose() {
35138
+ this.stop();
35139
+ for (const off of this.unsubs.splice(0)) {
35140
+ try {
35141
+ off();
35142
+ } catch {
35143
+ }
35144
+ }
35145
+ this.auction.dispose();
34216
35146
  }
34217
35147
  /** Get a stats snapshot. */
34218
35148
  getStats() {
@@ -34267,6 +35197,7 @@ var AutonomousCoordinator = class {
34267
35197
  body: `**${input.category}**${input.file ? ` in ${input.file}${input.line ? `:${input.line}` : ""}` : ""}
34268
35198
  ${input.detail}`
34269
35199
  });
35200
+ this._emit({ type: "knowledge:added", knowledgeId: fact.id, title: input.subject, text: input.detail });
34270
35201
  return fact;
34271
35202
  }
34272
35203
  // ── Goal creation helpers ────────────────────────────────────────────
@@ -34279,7 +35210,10 @@ ${input.detail}`
34279
35210
  title: input.title,
34280
35211
  description: input.description,
34281
35212
  priority: resolvedPriority,
34282
- ...input.tags ? { tags: input.tags } : {}
35213
+ ...input.tags ? { tags: input.tags } : {},
35214
+ // Mirror the dependency edges into the auction so blocked goals aren't
35215
+ // biddable until their deps complete (the DAG tracks the same edges).
35216
+ ...input.deps && input.deps.length > 0 ? { blockedBy: input.deps } : {}
34283
35217
  });
34284
35218
  const goal = this.graph.get(goalId);
34285
35219
  for (const depId of input.deps ?? []) {
@@ -34320,30 +35254,34 @@ ${input.detail}`
34320
35254
  async _processGoal(goalId) {
34321
35255
  const ready = this.dag.getReady();
34322
35256
  if (ready.length === 0) return;
34323
- const next = ready.find((n) => n.id === goalId) ?? ready[0];
34324
- this.dag.start(next.id, "auctioneer");
34325
- const existingAgent = this.auction.getTasksForAgent(this.selfAgentId).find((g) => g.id === next.id);
34326
- if (!existingAgent) {
34327
- await this.auction.publishTask({
34328
- title: next.description,
34329
- description: next.description,
34330
- priority: this._dagPriorityToGoal(next.priority),
34331
- tags: next.tags
35257
+ const dagNode = ready.find((n) => n.id === goalId) ?? ready[0];
35258
+ this.dag.start(dagNode.id, "auctioneer");
35259
+ const goalNode = this.graph.get(goalId);
35260
+ if (!goalNode) return;
35261
+ const title = goalNode.title || dagNode.description;
35262
+ const taskId = await this.auction.publishTask({
35263
+ title,
35264
+ description: goalNode.description,
35265
+ priority: this._dagPriorityToGoal(dagNode.priority),
35266
+ tags: dagNode.tags
35267
+ });
35268
+ this._emit({ type: "task:ready", goalId, taskId, title });
35269
+ if (this.director) {
35270
+ const config = {
35271
+ name: `worker-${goalId.slice(0, 8)}`,
35272
+ role: "general",
35273
+ maxIterations: 100,
35274
+ timeoutMs: 6e5
35275
+ // 10 minutes per goal
35276
+ };
35277
+ const subagentId = await this.director.spawn(config);
35278
+ await this.auction.claim(taskId, subagentId, config.name);
35279
+ await this.director.assign({
35280
+ id: goalId,
35281
+ subagentId,
35282
+ description: goalNode.description
34332
35283
  });
34333
35284
  }
34334
- await this._waitForClaim(next.id);
34335
- }
34336
- async _waitForClaim(taskId) {
34337
- const maxWait = 6e4;
34338
- const pollInterval = 2e3;
34339
- const start = Date.now();
34340
- while (Date.now() - start < maxWait) {
34341
- const goal = this.graph.get(taskId);
34342
- if (goal?.status === "in_progress" || goal?.status === "done") {
34343
- return;
34344
- }
34345
- await this._sleep(pollInterval);
34346
- }
34347
35285
  }
34348
35286
  async _handlePendingChange(change) {
34349
35287
  const result = this.consensus.getStatus(change.id);
@@ -34357,6 +35295,17 @@ ${input.detail}`
34357
35295
  );
34358
35296
  if (voteResult.outcome === "approved") {
34359
35297
  await this.changes.markApplied(change.id, (/* @__PURE__ */ new Date()).toISOString());
35298
+ this._emit({ type: "consensus:reached", goalId: change.id, text: "Change approved and applied" });
35299
+ }
35300
+ } else {
35301
+ const voteResult = await this.consensus.castVote(
35302
+ change.id,
35303
+ this.selfAgentId,
35304
+ "reject",
35305
+ `Quality gate failed: ${change.qualityGate.checks.map((c) => `${c.name}=${c.passed}`).join(", ")}`
35306
+ );
35307
+ if (voteResult.outcome === "rejected" || voteResult.outcome === "vetoed") {
35308
+ this._emit({ type: "consensus:reached", goalId: change.id, text: "Change rejected by quality gate" });
34360
35309
  }
34361
35310
  }
34362
35311
  }
@@ -34367,10 +35316,6 @@ ${input.detail}`
34367
35316
  (this.events?.emit)("autonomous:task_ready", { taskId: event.nodeId, description: node.description });
34368
35317
  }
34369
35318
  }
34370
- if (event.type === "deadlock") {
34371
- (this.events?.emit)("autonomous:deadlock", { blocked: event.blocked });
34372
- this.running = false;
34373
- }
34374
35319
  if (event.type === "graph:done") {
34375
35320
  (this.events?.emit)("autonomous:all_done", this.getStats());
34376
35321
  }
@@ -34378,13 +35323,16 @@ ${input.detail}`
34378
35323
  _onSubagentTerminated(e) {
34379
35324
  const payload = e.payload;
34380
35325
  const subagentId = payload?.subagentId ?? e.subagentId;
34381
- const stopReason = payload?.stopReason ?? "unknown";
35326
+ const stopReason = payload?.stopReason ?? (payload?.status === "ok" ? "end_turn" : payload?.status ?? "unknown");
34382
35327
  const tasks = this.auction.getTasksForAgent(subagentId);
34383
35328
  for (const task of tasks) {
35329
+ this._handledBySubagent.add(task.id);
34384
35330
  if (stopReason === "end_turn") {
34385
35331
  void this.auction.complete(task.id, "Subagent completed successfully");
35332
+ this._emit({ type: "task:completed", goalId: task.id, taskId: task.id, text: "Subagent completed successfully" });
34386
35333
  } else {
34387
35334
  void this.auction.fail(task.id, `Subagent terminated: ${stopReason}`);
35335
+ this._emit({ type: "goal:failed", goalId: task.id, text: `Subagent terminated: ${stopReason}` });
34388
35336
  }
34389
35337
  }
34390
35338
  }
@@ -34398,6 +35346,10 @@ ${input.detail}`
34398
35346
  }
34399
35347
  _buildVoters() {
34400
35348
  return [
35349
+ // The coordinator itself casts the quality-gate auto-vote in
35350
+ // _handlePendingChange — it MUST be a registered, eligible voter or
35351
+ // castVote throws "unknown voter" and tears down the run() loop.
35352
+ { agentId: this.selfAgentId, agentName: "Coordinator", role: "coordinator", weight: 1 },
34401
35353
  { agentId: "critic", agentName: "Critic", role: "critic", weight: 2, veto: true },
34402
35354
  { agentId: "bug-hunter", agentName: "Bug Hunter", role: "bug-hunter", weight: 1.5 },
34403
35355
  { agentId: "security-scanner", agentName: "Security Scanner", role: "security-scanner", weight: 1.5 },
@@ -34435,8 +35387,9 @@ ${input.detail}`
34435
35387
  } catch {
34436
35388
  }
34437
35389
  }
34438
- _sleep(ms) {
34439
- return new Promise((r) => setTimeout(r, ms));
35390
+ /** Emit a CoordinatorEvent to the subscriber (e.g. TUI panel timeline). */
35391
+ _emit(event) {
35392
+ this.onCoordinatorEvent?.(event);
34440
35393
  }
34441
35394
  };
34442
35395
  function createMcpControlTool(opts) {
@@ -34999,13 +35952,13 @@ function createAgentToolHandler(a) {
34999
35952
  }
35000
35953
  }
35001
35954
  function waitForConfirm(info) {
35002
- return new Promise((resolve14) => {
35955
+ return new Promise((resolve15) => {
35003
35956
  a.events.emit("tool.confirm_needed", {
35004
35957
  tool: info.tool,
35005
35958
  input: info.input,
35006
35959
  toolUseId: info.toolUseId,
35007
35960
  suggestedPattern: info.suggestedPattern,
35008
- resolve: resolve14
35961
+ resolve: resolve15
35009
35962
  });
35010
35963
  });
35011
35964
  }
@@ -35433,7 +36386,7 @@ async function injectPendingMailboxMessages(checkMailbox, foldFn, a) {
35433
36386
  try {
35434
36387
  messages = await checkMailbox();
35435
36388
  } catch {
35436
- return;
36389
+ return { interrupt: false };
35437
36390
  }
35438
36391
  for (const m of messages) {
35439
36392
  a.events.emit("mailbox.received", {
@@ -35443,14 +36396,22 @@ async function injectPendingMailboxMessages(checkMailbox, foldFn, a) {
35443
36396
  subject: m.subject
35444
36397
  });
35445
36398
  }
35446
- if (messages.length === 0) return;
35447
- try {
35448
- foldFn(buildMailboxBlock(messages));
35449
- } catch (err) {
35450
- (a.logger.debug ?? console.debug)?.(
35451
- `mailbox: failed to fold messages: ${toErrorMessage(err)}`
35452
- );
36399
+ if (messages.length === 0) return { interrupt: false };
36400
+ const control = messages.filter((m) => m.type === "control");
36401
+ const content = messages.filter((m) => m.type !== "control");
36402
+ if (content.length > 0) {
36403
+ try {
36404
+ foldFn(buildMailboxBlock(content));
36405
+ } catch (err) {
36406
+ (a.logger.debug ?? console.debug)?.(
36407
+ `mailbox: failed to fold messages: ${toErrorMessage(err)}`
36408
+ );
36409
+ }
35453
36410
  }
36411
+ const interruptMsg = control.find(
36412
+ (m) => /\b(interrupt|stop|halt|abort|cancel)\b/i.test(`${m.subject} ${m.body}`)
36413
+ );
36414
+ return interruptMsg ? { interrupt: true, interruptReason: interruptMsg.body || interruptMsg.subject || "operator interrupt" } : { interrupt: false };
35454
36415
  }
35455
36416
 
35456
36417
  // src/mailbox-attach.ts
@@ -35516,12 +36477,12 @@ function attachMailboxCheckerInner(a, source) {
35516
36477
  // src/core/iteration-limit.ts
35517
36478
  function requestLimitExtension(opts) {
35518
36479
  const { events, currentIterations, currentLimit, autoExtend, timeoutMs = 3e4 } = opts;
35519
- return new Promise((resolve14) => {
36480
+ return new Promise((resolve15) => {
35520
36481
  let resolved = false;
35521
36482
  const timerFired = () => {
35522
36483
  if (!resolved) {
35523
36484
  resolved = true;
35524
- resolve14(0);
36485
+ resolve15(0);
35525
36486
  }
35526
36487
  };
35527
36488
  const timer = setTimeout(timerFired, timeoutMs);
@@ -35530,14 +36491,14 @@ function requestLimitExtension(opts) {
35530
36491
  if (!resolved) {
35531
36492
  resolved = true;
35532
36493
  clearTimeout(timer);
35533
- resolve14(0);
36494
+ resolve15(0);
35534
36495
  }
35535
36496
  };
35536
36497
  const grant = (extra) => {
35537
36498
  if (!resolved) {
35538
36499
  resolved = true;
35539
36500
  clearTimeout(timer);
35540
- resolve14(Math.max(0, extra));
36501
+ resolve15(Math.max(0, extra));
35541
36502
  }
35542
36503
  };
35543
36504
  events.emit("iteration.limit_reached", {
@@ -35551,7 +36512,7 @@ function requestLimitExtension(opts) {
35551
36512
  if (!resolved) {
35552
36513
  resolved = true;
35553
36514
  clearTimeout(timer);
35554
- resolve14(100);
36515
+ resolve15(100);
35555
36516
  }
35556
36517
  });
35557
36518
  }
@@ -35736,12 +36697,20 @@ function createAgentLoopHandler(a, handlers) {
35736
36697
  a.events.emit("iteration.started", { ctx: a.ctx, index: i });
35737
36698
  injectPendingBtwNotes();
35738
36699
  injectQueueAwareness();
35739
- await injectPendingMailboxMessages(checkMailbox, foldBlockIntoConversation, {
35740
- // Cast to the broad parameter type — injectPendingMailboxMessages only
35741
- // calls emit('mailbox.received', ...) and uses logger.debug optionally.
35742
- events: a.events,
35743
- logger: a.logger
35744
- });
36700
+ const mailboxResult = await injectPendingMailboxMessages(
36701
+ checkMailbox,
36702
+ foldBlockIntoConversation,
36703
+ {
36704
+ // Cast to the broad parameter type — injectPendingMailboxMessages only
36705
+ // calls emit('mailbox.received', ...) and uses logger.debug optionally.
36706
+ events: a.events,
36707
+ logger: a.logger
36708
+ }
36709
+ );
36710
+ if (mailboxResult.interrupt) {
36711
+ const reason = `interrupted: ${mailboxResult.interruptReason ?? "operator request"}`;
36712
+ return { status: "aborted", iterations, abortReason: reason, finalText };
36713
+ }
35745
36714
  const req = await handlers.response.buildAndRunRequestPipeline(opts);
35746
36715
  const preFlight = estimateRequestTokens(
35747
36716
  req.messages,
@@ -36182,13 +37151,13 @@ async function runShellHook(spec, input, logger) {
36182
37151
  logger?.warn?.(`hook rejected: command not in allowlist: ${spec.command}`);
36183
37152
  return null;
36184
37153
  }
36185
- return await new Promise((resolve14) => {
37154
+ return await new Promise((resolve15) => {
36186
37155
  let settled = false;
36187
37156
  const done = (v) => {
36188
37157
  if (settled) return;
36189
37158
  settled = true;
36190
37159
  clearTimeout(timer);
36191
- resolve14(v);
37160
+ resolve15(v);
36192
37161
  };
36193
37162
  let child;
36194
37163
  try {
@@ -36201,7 +37170,7 @@ async function runShellHook(spec, input, logger) {
36201
37170
  });
36202
37171
  } catch (err2) {
36203
37172
  logger?.warn?.(`hook spawn failed: ${toErrorMessage(err2)}`);
36204
- return resolve14(null);
37173
+ return resolve15(null);
36205
37174
  }
36206
37175
  const timer = setTimeout(() => {
36207
37176
  logger?.warn?.(`hook command timed out after ${timeoutMs}ms: ${spec.command}`);
@@ -36456,6 +37425,12 @@ function flagsToConfigPatch(flags) {
36456
37425
  patch.features ??= {};
36457
37426
  patch.features.tokenSavingMode = true;
36458
37427
  }
37428
+ if (typeof flags["token-saving-tier"] === "string") {
37429
+ patch.features ??= {};
37430
+ patch.features.tokenSavingMode = normalizeTokenSavingTier(
37431
+ flags["token-saving-tier"]
37432
+ );
37433
+ }
36459
37434
  return patch;
36460
37435
  }
36461
37436
  async function writeProjectMeta(paths, projectRoot) {
@@ -36687,8 +37662,8 @@ var InputBuilder = class {
36687
37662
  async registerFile(input) {
36688
37663
  const ref = await this.store.add({ ...input, kind: "file" });
36689
37664
  this.refs.push(ref);
36690
- const path43 = ref.meta.filename ?? ref.meta.label ?? String(ref.seq);
36691
- return `[file:${path43}]`;
37665
+ const path44 = ref.meta.filename ?? ref.meta.label ?? String(ref.seq);
37666
+ return `[file:${path44}]`;
36692
37667
  }
36693
37668
  /**
36694
37669
  * Whether `appendPaste(text)` would collapse the text to a placeholder
@@ -36782,26 +37757,30 @@ Never silently skip a failure \u2014 always report it, even when you choose not
36782
37757
 
36783
37758
  ## After-task suggestions
36784
37759
 
36785
- **You are the leader agent.** After completing a significant task, end your
36786
- response with 2\u20134 suggested next actions in a \`<next_steps>\` block. Use this
36787
- exact format so the user can select them with \`/next 1\`, \`/next 2\`, or
36788
- \`/next 1 2 3\`:
37760
+ **You are the leader agent.** After completing a significant task, you MAY end your
37761
+ response with 2\u20134 suggested next prompt options in a \`<next_steps>\` block.
37762
+ The \`/next 1\`, \`/next 2\`, \`/next 1 2 3\` shortcuts let the user select one
37763
+ and continue in a new agent session.
37764
+
37765
+ Format:
36789
37766
 
36790
37767
  \`\`\`
36791
37768
  <next_steps>
36792
- 1. First suggestion \u2014 imperative, specific, actionable
36793
- 2. Second suggestion
36794
- 3. Third suggestion
37769
+ 1. Prompt option 1 \u2014 a concrete next action phrased as what to type
37770
+ 2. Prompt option 2
37771
+ 3. Prompt option 3 (optional)
36795
37772
  </next_steps>
36796
37773
  \`\`\`
36797
37774
 
36798
37775
  Rules:
36799
- - Each line is a single imperative sentence the user can act on immediately.
36800
- - Be specific: mention file names, tool names, or commands.
36801
- - **Concrete actions only** \u2014 never write declarations of intent ("we should fix X", "consider refactoring Y") or manual suggestions ("manually check Z"). Write exactly what should be done: "Fix null deref in auth/session.ts:42", "Run pnpm typecheck".
37776
+ - Each item is a **prompt the user can type** \u2014 not an instruction to a human.
37777
+ Write "pnpm test" not "Run the test suite."
37778
+ - Human-only actions (e.g., "open DevTools") go outside the tag as plain text,
37779
+ not inside \`<next_steps>\`.
37780
+ - Items marked \`auto="true"\` must include the exact input content for copy-paste.
36802
37781
  - Order by priority. Keep each suggestion to one line.
36803
37782
  - Skip during multi-step operations \u2014 only show after completion.
36804
- - If nothing is pending, say "No pending actions."
37783
+ - If nothing is pending, omit the tag entirely.
36805
37784
 
36806
37785
  **After a significant task**, also post a status update to the inter-agent
36807
37786
  mailbox so other agents in the fleet can discover what you finished and
@@ -36835,6 +37814,43 @@ var DefaultSystemPromptBuilder = class {
36835
37814
  _lastOnlineAgents;
36836
37815
  /** Cached full buildToolUsage output — keyed by tools array + online agents refs. */
36837
37816
  _toolsUsageCache;
37817
+ /**
37818
+ * Normalizes `tokenSavingMode` to a boolean for backward-compatible boolean checks.
37819
+ * - `undefined` / `false` / `'off'` → false
37820
+ * - `true` / any tier string other than `'off'` → true
37821
+ */
37822
+ get isCompact() {
37823
+ const val = this.opts.tokenSavingMode;
37824
+ if (!val) return false;
37825
+ if (typeof val === "boolean") return val;
37826
+ return val !== "off";
37827
+ }
37828
+ /** Exposes the normalized `TokenSavingTier` for tier-aware guidance decisions. */
37829
+ get tier() {
37830
+ const val = this.opts.tokenSavingMode;
37831
+ if (typeof val === "string") return val;
37832
+ if (val === true) return "medium";
37833
+ return "off";
37834
+ }
37835
+ /**
37836
+ * Returns the max tool description length for the current tier.
37837
+ * Per the design doc: off=80, minimal=40, light=50, medium=60, aggressive=70.
37838
+ */
37839
+ toolDescLimit() {
37840
+ switch (this.tier) {
37841
+ case "minimal":
37842
+ return 40;
37843
+ case "light":
37844
+ return 50;
37845
+ case "medium":
37846
+ return 60;
37847
+ case "aggressive":
37848
+ return 70;
37849
+ case "off":
37850
+ default:
37851
+ return 80;
37852
+ }
37853
+ }
36838
37854
  async build(ctx) {
36839
37855
  this._lastBuildTools = ctx.tools;
36840
37856
  if (this.opts.skillLoader && !this.skillCache) {
@@ -36933,13 +37949,13 @@ var DefaultSystemPromptBuilder = class {
36933
37949
  if (!planPath) return "";
36934
37950
  let raw;
36935
37951
  try {
36936
- const stat13 = await fsp3.stat(planPath);
36937
- if (this._planCache && this._planCache.path === planPath && this._planCache.mtimeMs === stat13.mtimeMs) {
37952
+ const stat14 = await fsp3.stat(planPath);
37953
+ if (this._planCache && this._planCache.path === planPath && this._planCache.mtimeMs === stat14.mtimeMs) {
36938
37954
  return this._planCache.text;
36939
37955
  }
36940
37956
  raw = await fsp3.readFile(planPath, "utf8");
36941
37957
  const text = this._formatPlan(raw);
36942
- this._planCache = { path: planPath, mtimeMs: stat13.mtimeMs, text };
37958
+ this._planCache = { path: planPath, mtimeMs: stat14.mtimeMs, text };
36943
37959
  return text;
36944
37960
  } catch {
36945
37961
  this._planCache = void 0;
@@ -36988,12 +38004,13 @@ var DefaultSystemPromptBuilder = class {
36988
38004
  }
36989
38005
  }
36990
38006
  const lines = ["## Tool usage"];
38007
+ const descLimit = this.toolDescLimit();
36991
38008
  for (const [cat, catTools] of byCat) {
36992
38009
  lines.push(`
36993
38010
  ### ${cat}`);
36994
38011
  for (const t2 of catTools) {
36995
38012
  const hint = t2.usageHint ?? t2.description;
36996
- const desc = this.opts.tokenSavingMode ? hint.length > 60 ? hint.slice(0, hint.indexOf(".", 20) + 1 || 60) + (hint.length > 60 ? "\u2026" : "") : hint.trim() : hint.length > 80 ? `${hint.slice(0, 77)}...` : hint.trim();
38013
+ const desc = hint.length > descLimit ? hint.slice(0, hint.indexOf(".", 20) + 1 || descLimit) + (hint.length > descLimit ? "\u2026" : "") : hint.trim();
36997
38014
  lines.push(`- **${t2.name}** \u2014 ${desc}`);
36998
38015
  }
36999
38016
  }
@@ -37006,7 +38023,7 @@ var DefaultSystemPromptBuilder = class {
37006
38023
  ${hint.trim()}`);
37007
38024
  }
37008
38025
  }
37009
- if (!this.opts.tokenSavingMode) {
38026
+ if (this.tier !== "minimal") {
37010
38027
  lines.push(`
37011
38028
  ## Common patterns
37012
38029
 
@@ -37026,7 +38043,7 @@ When unsure about a file's current state, read it first rather than assuming.`);
37026
38043
  return Array.isArray(role) ? role.filter((r) => typeof r === "string") : [];
37027
38044
  })();
37028
38045
  const roleList = enumValues.length > 0 ? enumValues.join(", ") : "(no roster configured)";
37029
- if (this.opts.tokenSavingMode) {
38046
+ if (this.tier === "minimal") ; else if (this.tier === "light" || this.tier === "medium") {
37030
38047
  lines.push(`## Delegation
37031
38048
 
37032
38049
  Use \`delegate\` to hand work to a subagent (roles: ${roleList}).`);
@@ -37099,9 +38116,9 @@ one by one, roll up results), use \`spawn_subagent\` + \`assign_task\` +
37099
38116
  const hasMailbox = tools.some(
37100
38117
  (t2) => t2.name === "mailbox" || t2.name === "mail_send" || t2.name === "mail_inbox"
37101
38118
  );
37102
- if (hasMailbox) {
38119
+ if (hasMailbox && this.tier !== "minimal") {
37103
38120
  const onlineAgentsInfo = this.renderOnlineAgents(ctx.onlineAgents);
37104
- if (this.opts.tokenSavingMode) {
38121
+ if (this.tier === "light" || this.tier === "medium") {
37105
38122
  lines.push(`
37106
38123
  ## Inter-agent mailbox${onlineAgentsInfo}
37107
38124
 
@@ -37179,9 +38196,19 @@ To catch up explicitly:
37179
38196
  }
37180
38197
  const hasMcpControl = tools.some((t2) => t2.name === "mcp_control");
37181
38198
  const hasMcpUse = tools.some((t2) => t2.name === "mcp_use");
37182
- if (hasMcpControl && this.opts.tokenSavingMode) {
37183
- if (hasMcpUse) {
37184
- lines.push(`
38199
+ if (hasMcpControl) {
38200
+ if (this.tier === "minimal" || this.tier === "light") {
38201
+ lines.push(
38202
+ hasMcpUse ? `
38203
+ ## MCP tools (lazy-loaded)
38204
+
38205
+ Use \`mcp_use({ server: "<name>", tool: "<bare-tool>", input: { ... } })\` to activate and call MCP tools.` : `
38206
+ ## MCP tools (lazy-loaded)
38207
+
38208
+ Use \`mcp_control({ action: "list" })\` to see available servers, \`mcp_control({ action: "activate", server: "<name>" })\` to register tools.`
38209
+ );
38210
+ } else {
38211
+ lines.push(hasMcpUse ? `
37185
38212
  ## MCP tools (lazy-loaded)
37186
38213
 
37187
38214
  MCP server tools are NOT registered by default in token-saving mode to keep
@@ -37200,9 +38227,7 @@ deactivates \u2014 all in one call. No need to track activate/deactivate state.
37200
38227
  4. \`mcp_control({ action: "deactivate", server: "<name>" })\` \u2014 clean up
37201
38228
 
37202
38229
  Activation/deactivation is ephemeral (no config writes) and does NOT affect
37203
- the server connection \u2014 only tool visibility changes.`);
37204
- } else {
37205
- lines.push(`
38230
+ the server connection \u2014 only tool visibility changes.` : `
37206
38231
  ## MCP tools (lazy-loaded)
37207
38232
 
37208
38233
  MCP server tools are NOT registered by default in token-saving mode to keep
@@ -37220,10 +38245,16 @@ the server connection \u2014 only tool visibility changes.`);
37220
38245
  }
37221
38246
  }
37222
38247
  const hasContextManager = tools.some((t2) => t2.name === "context_manager");
37223
- if (hasContextManager && !this.opts.tokenSavingMode) {
37224
- const maxCtx = this.opts.modelCapabilities?.maxContextTokens ?? 128e3;
37225
- const threshold = maxCtx <= 32e3 ? "50" : "70";
37226
- lines.push(`
38248
+ if (hasContextManager) {
38249
+ if (this.tier === "minimal" || this.tier === "light") ; else if (this.tier === "medium") {
38250
+ lines.push(`
38251
+ ## Context management
38252
+
38253
+ Use \`context_manager\` to manage context. Call \`{"action":"check"}\` to see token budget.`);
38254
+ } else {
38255
+ const maxCtx = this.opts.modelCapabilities?.maxContextTokens ?? 128e3;
38256
+ const threshold = maxCtx <= 32e3 ? "50" : "70";
38257
+ lines.push(`
37227
38258
  ## Context management
37228
38259
 
37229
38260
  When the conversation grows long and context window usage exceeds what you can track,
@@ -37236,6 +38267,7 @@ use the context_manager tool proactively \u2014 do NOT wait to be told:
37236
38267
 
37237
38268
  **Never** stuff redundant information into a tool result. If you summarize a file, do not paste its full content \u2014
37238
38269
  summarize it, and let the tool result hold only the summary.`);
38270
+ }
37239
38271
  }
37240
38272
  const text = lines.join("\n");
37241
38273
  this._toolsUsageCache = { toolsRef: tools, agentsRef: ctx.onlineAgents, text };
@@ -37247,6 +38279,10 @@ summarize it, and let the tool result hold only the summary.`);
37247
38279
  * build turn (hundreds of ms). Reference equality avoids re-stringifying
37248
38280
  * the same array on every iteration while still being correct when the
37249
38281
  * caller passes a fresh array.
38282
+ *
38283
+ * Tier behaviour:
38284
+ * - 'off' / 'medium' / 'aggressive' → full list with names, sessions, sources
38285
+ * - 'minimal' / 'light' → count only (no list)
37250
38286
  */
37251
38287
  renderOnlineAgents(agents) {
37252
38288
  if (!agents || agents.length === 0) return "";
@@ -37254,6 +38290,11 @@ summarize it, and let the tool result hold only the summary.`);
37254
38290
  return this._lastOnlineAgents.text;
37255
38291
  }
37256
38292
  const totalCount = agents.length;
38293
+ if (this.tier === "minimal" || this.tier === "light") {
38294
+ const text2 = ` (${totalCount} agent${totalCount !== 1 ? "s" : ""} online)`;
38295
+ this._lastOnlineAgents = { ref: agents, text: text2 };
38296
+ return text2;
38297
+ }
37257
38298
  const agentList = agents.map(
37258
38299
  (a) => `- **${a.name}** (${a.source ?? "unknown"}${a.sessionId ? `, session: ${a.sessionId.slice(0, 8)}` : ""})`
37259
38300
  ).join("\n");
@@ -37276,28 +38317,46 @@ ${agentList}`;
37276
38317
  isGit ? this.gitStatus(ctx.projectRoot) : Promise.resolve("not a git repo"),
37277
38318
  this.detectLanguages(ctx.projectRoot)
37278
38319
  ]);
37279
- const lines = [
37280
- "## Environment",
37281
- "",
37282
- `- Operating system: ${platform2}`,
37283
- `- Shell: ${shell}`,
37284
- `- Node.js: ${node}`,
37285
- `- Detected languages: ${langs}`,
37286
- `- Git status: ${git}`,
37287
- `- Today's date: ${today}`
37288
- ];
37289
- if (ctx.provider || ctx.model) {
37290
- lines.push(
37291
- `- Running on: ${ctx.provider ?? "<unknown provider>"}/${ctx.model ?? "<unknown model>"}`
37292
- );
37293
- }
37294
- if (this.opts.modeId && this.opts.modeId !== "default") {
37295
- lines.push(`- Mode: ${this.opts.modeId}`);
37296
- }
37297
- if (this.opts.modelCapabilities) {
37298
- lines.push(
37299
- `- Context window: ${this.opts.modelCapabilities.maxContextTokens.toLocaleString()} tokens max`
37300
- );
38320
+ const tier = this.tier;
38321
+ const lines = ["## Environment"];
38322
+ if (tier === "minimal") {
38323
+ lines.push(`- Git: ${git} | Date: ${today}`);
38324
+ } else {
38325
+ lines.push(`- Operating system: ${platform2}`);
38326
+ if (tier !== "light") {
38327
+ lines.push(`- Shell: ${shell}`);
38328
+ lines.push(`- Node.js: ${node}`);
38329
+ }
38330
+ if (tier === "off" || tier === "medium" || tier === "aggressive") {
38331
+ lines.push(`- Detected languages: ${langs}`);
38332
+ }
38333
+ lines.push(`- Git status: ${git}`);
38334
+ lines.push(`- Today's date: ${today}`);
38335
+ if (tier === "aggressive") {
38336
+ if (ctx.provider || ctx.model) {
38337
+ lines.push(
38338
+ `- Running on: ${ctx.provider ?? "<unknown provider>"}/${ctx.model ?? "<unknown model>"}`
38339
+ );
38340
+ }
38341
+ if (this.opts.modelCapabilities) {
38342
+ lines.push(
38343
+ `- Context window: ${this.opts.modelCapabilities.maxContextTokens.toLocaleString()} tokens max`
38344
+ );
38345
+ }
38346
+ }
38347
+ if (tier !== "aggressive" && this.opts.modelCapabilities) {
38348
+ lines.push(
38349
+ `- Context window: ${this.opts.modelCapabilities.maxContextTokens.toLocaleString()} tokens max`
38350
+ );
38351
+ }
38352
+ if (tier !== "aggressive" && (ctx.provider || ctx.model)) {
38353
+ lines.push(
38354
+ `- Running on: ${ctx.provider ?? "<unknown provider>"}/${ctx.model ?? "<unknown model>"}`
38355
+ );
38356
+ }
38357
+ if (tier !== "aggressive" && this.opts.modeId && this.opts.modeId !== "default") {
38358
+ lines.push(`- Mode: ${this.opts.modeId}`);
38359
+ }
37301
38360
  }
37302
38361
  if (this.skillCache) {
37303
38362
  lines.push(
@@ -37305,7 +38364,7 @@ ${agentList}`;
37305
38364
  "## Skills in scope for this session",
37306
38365
  this.skillCache,
37307
38366
  "",
37308
- this.opts.tokenSavingMode ? "Compact skill instructions are injected in the Active Skills block below (Overview + Rules only)." : "Full skill instructions are injected in the Active Skills block below."
38367
+ this.isCompact ? "Compact skill instructions are injected in the Active Skills block below (Overview + Rules only)." : "Full skill instructions are injected in the Active Skills block below."
37309
38368
  );
37310
38369
  }
37311
38370
  const text = lines.join("\n");
@@ -37314,6 +38373,8 @@ ${agentList}`;
37314
38373
  }
37315
38374
  async buildMemoryAndSkills() {
37316
38375
  const parts = [];
38376
+ const memoryCount = this.tier === "minimal" ? 3 : this.tier === "light" ? 5 : 8;
38377
+ const compactMemory = this.tier === "minimal";
37317
38378
  if (this.opts.memoryStore) {
37318
38379
  try {
37319
38380
  if (this.opts.memoryStore.scoreRelevant) {
@@ -37324,14 +38385,18 @@ ${agentList}`;
37324
38385
  toolNames
37325
38386
  },
37326
38387
  "project-memory",
37327
- 8
38388
+ memoryCount
37328
38389
  );
37329
38390
  if (scored.length > 0) {
37330
38391
  const lines = ["# Relevant Memory"];
37331
38392
  for (const e of scored) {
37332
- const badge2 = e.type ? `[\`${e.type.replace("_", "-")}\`] ` : "";
37333
- const priorityMark = e.priority === "critical" ? "\u26A1" : e.priority === "high" ? "\u25B2" : "";
37334
- lines.push(`- ${priorityMark}${badge2}${e.text}${e.tags ? ` \`#${e.tags.join(" #")}\`` : ""}`);
38393
+ if (compactMemory) {
38394
+ lines.push(`- ${e.text}`);
38395
+ } else {
38396
+ const badge2 = e.type ? `[\`${e.type.replace("_", "-")}\`] ` : "";
38397
+ const priorityMark = e.priority === "critical" ? "\u26A1" : e.priority === "high" ? "\u25B2" : "";
38398
+ lines.push(`- ${priorityMark}${badge2}${e.text}${e.tags ? ` \`#${e.tags.join(" #")}\`` : ""}`);
38399
+ }
37335
38400
  }
37336
38401
  parts.push(lines.join("\n"));
37337
38402
  }
@@ -37345,7 +38410,7 @@ ${mem}`);
37345
38410
  }
37346
38411
  }
37347
38412
  if (this.opts.skillLoader) {
37348
- if (this.opts.tokenSavingMode) {
38413
+ if (this.isCompact) {
37349
38414
  if (this.skillBodyCache === void 0) {
37350
38415
  await this.buildCompactSkillBodies();
37351
38416
  }
@@ -37431,19 +38496,19 @@ ${clean.trim()}`);
37431
38496
  }
37432
38497
  async dirExists(p) {
37433
38498
  try {
37434
- const stat13 = await fsp3.stat(p);
37435
- return stat13.isDirectory();
38499
+ const stat14 = await fsp3.stat(p);
38500
+ return stat14.isDirectory();
37436
38501
  } catch {
37437
38502
  return false;
37438
38503
  }
37439
38504
  }
37440
38505
  async gitStatus(root) {
37441
- return new Promise((resolve14) => {
38506
+ return new Promise((resolve15) => {
37442
38507
  let settled = false;
37443
38508
  const finish = (s) => {
37444
38509
  if (settled) return;
37445
38510
  settled = true;
37446
- resolve14(s);
38511
+ resolve15(s);
37447
38512
  };
37448
38513
  let proc;
37449
38514
  const timer = setTimeout(() => {
@@ -37871,173 +38936,6 @@ var SlashCommandRegistry = class {
37871
38936
  }
37872
38937
  };
37873
38938
 
37874
- // src/plugin/api.ts
37875
- var DefaultPluginAPI = class {
37876
- container;
37877
- events;
37878
- pipelines;
37879
- tools;
37880
- providers;
37881
- mcp;
37882
- slashCommands;
37883
- extensions;
37884
- session;
37885
- metrics;
37886
- config;
37887
- log;
37888
- configStore;
37889
- hookRegistry;
37890
- ownerName;
37891
- pluginCleanupFns = [];
37892
- constructor(init) {
37893
- const owner = init.ownerName;
37894
- this.ownerName = owner;
37895
- this.hookRegistry = init.hookRegistry;
37896
- this.container = init.container;
37897
- this.events = init.events;
37898
- this.config = init.config;
37899
- this.configStore = init.configStore;
37900
- this.log = init.log.child({ plugin: owner });
37901
- this.extensions = init.extensions ?? new ExtensionRegistry();
37902
- this.session = init.sessionWriter ?? noopSession;
37903
- this.metrics = init.metricsSink ? scopedMetrics(init.metricsSink, owner) : noopMetrics;
37904
- const pipelines = init.pipelines;
37905
- const readonlyPipelines = {};
37906
- for (const [key, pipeline2] of Object.entries(pipelines)) {
37907
- readonlyPipelines[key] = pipeline2.asReadonly();
37908
- }
37909
- this.pipelines = readonlyPipelines;
37910
- const tr = init.toolRegistry;
37911
- const isOfficial = init.official === true;
37912
- const capabilities = init.capabilities;
37913
- const assertCanMutateTool = (name, op) => {
37914
- if (isOfficial) return;
37915
- const currentOwner = tr.ownerOf(name);
37916
- if (currentOwner === void 0) return;
37917
- const ownedSolelyByMe = currentOwner.split("+").every((seg) => seg === owner);
37918
- if (ownedSolelyByMe) return;
37919
- const toolCaps = tr.get(name)?.capabilities ?? [];
37920
- const pluginMutateCaps = capabilities?.toolMutateCapabilities ?? [];
37921
- const hasRequiredCap = toolCaps.some((c) => pluginMutateCaps.includes(c));
37922
- if (!hasRequiredCap) {
37923
- throw new Error(
37924
- `Plugin "${owner}" may not ${op} tool "${name}" \u2014 it is owned by "${currentOwner}". Tool capabilities: [${toolCaps.join(", ") || "none"}]. Plugin toolMutateCapabilities: [${pluginMutateCaps.join(", ") || "none"}]. Missing required capability to mutate this tool.`
37925
- );
37926
- }
37927
- };
37928
- this.tools = {
37929
- register: (t2) => tr.register(t2, owner),
37930
- unregister: (name) => {
37931
- assertCanMutateTool(name, "unregister");
37932
- return tr.unregister(name);
37933
- },
37934
- wrap: (name, wrapper) => {
37935
- assertCanMutateTool(name, "wrap");
37936
- tr.wrap(name, wrapper, owner);
37937
- },
37938
- get: (name) => tr.get(name),
37939
- list: () => tr.list()
37940
- };
37941
- const pr = init.providerRegistry;
37942
- this.providers = {
37943
- register: (f) => pr.register(f),
37944
- create: (cfg) => pr.create(cfg),
37945
- list: () => pr.list()
37946
- };
37947
- this.mcp = init.mcpRegistry ?? noopMcp;
37948
- const scr = init.slashCommandRegistry;
37949
- const official = init.official === true;
37950
- this.slashCommands = scr ? {
37951
- register: (cmd) => scr.register(cmd, owner, { official }),
37952
- unregister: (name) => scr.unregister(name),
37953
- get: (name) => scr.get(name),
37954
- list: () => scr.list()
37955
- } : noopSlashCommands;
37956
- }
37957
- onEvent(event, handler) {
37958
- const off = this.events.on(event, handler);
37959
- this.pluginCleanupFns.push(off);
37960
- return off;
37961
- }
37962
- onPattern(pattern, handler) {
37963
- const off = this.events.onPattern(pattern, handler);
37964
- this.pluginCleanupFns.push(off);
37965
- return off;
37966
- }
37967
- emitCustom(event, payload) {
37968
- this.events.emitCustom(event, payload);
37969
- }
37970
- onConfigChange(handler) {
37971
- if (!this.configStore) return () => {
37972
- };
37973
- return this.configStore.watch(handler);
37974
- }
37975
- /** Called by the plugin loader when uninstalling the plugin. */
37976
- drainCleanup() {
37977
- for (const fn of this.pluginCleanupFns.splice(0)) {
37978
- try {
37979
- fn();
37980
- } catch {
37981
- }
37982
- }
37983
- }
37984
- registerSystemPromptContributor(c) {
37985
- return this.extensions.registerSystemPromptContributor(c);
37986
- }
37987
- registerHook(event, matcher, hook) {
37988
- if (!this.hookRegistry) return () => {
37989
- };
37990
- const off = this.hookRegistry.registerInProcess(event, matcher, hook, this.ownerName);
37991
- this.pluginCleanupFns.push(off);
37992
- return off;
37993
- }
37994
- };
37995
- var noopMcp = {
37996
- start: async () => void 0,
37997
- stop: async () => void 0,
37998
- restart: async () => void 0,
37999
- list: () => []
38000
- };
38001
- var noopSlashCommands = {
38002
- register() {
38003
- },
38004
- unregister() {
38005
- return false;
38006
- },
38007
- get() {
38008
- return void 0;
38009
- },
38010
- list() {
38011
- return [];
38012
- }
38013
- };
38014
- var noopSession = {
38015
- append: async () => {
38016
- }
38017
- };
38018
- var noopMetrics = {
38019
- counter() {
38020
- },
38021
- histogram() {
38022
- },
38023
- gauge() {
38024
- }
38025
- };
38026
- function scopedMetrics(sink, pluginName) {
38027
- const prefix = `plugin.${pluginName}.`;
38028
- return {
38029
- counter(name, value, labels) {
38030
- sink.counter(`${prefix}${name}`, value, labels);
38031
- },
38032
- histogram(name, value, labels) {
38033
- sink.histogram(`${prefix}${name}`, value, labels);
38034
- },
38035
- gauge(name, value, labels) {
38036
- sink.gauge(`${prefix}${name}`, value, labels);
38037
- }
38038
- };
38039
- }
38040
-
38041
38939
  // src/plugin/loader.ts
38042
38940
  var pluginApiMap = /* @__PURE__ */ new WeakMap();
38043
38941
  var KERNEL_API_VERSION = "0.1.10";
@@ -38202,12 +39100,23 @@ async function loadPlugins(plugins, opts) {
38202
39100
  try {
38203
39101
  const rawApi = opts.apiFactory(plugin);
38204
39102
  const api = plugin.capabilities ? wrapApiForCapabilityCheck(plugin, rawApi, opts.log, opts.enforceCapabilities) : rawApi;
38205
- await plugin.setup(api);
39103
+ const setupTimeoutMs = opts.setupTimeoutMs ?? 3e4;
39104
+ const setupController = new AbortController();
39105
+ const setupTimeout = setTimeout(() => setupController.abort(), setupTimeoutMs);
39106
+ try {
39107
+ await plugin.setup(api, { signal: setupController.signal });
39108
+ } finally {
39109
+ clearTimeout(setupTimeout);
39110
+ }
38206
39111
  pluginApiMap.set(plugin, api);
38207
39112
  loaded.push(plugin);
38208
39113
  opts.log.info(`Plugin "${plugin.name}" loaded`);
38209
39114
  } catch (err) {
38210
- opts.log.error(`Plugin "${plugin.name}" setup failed`, err);
39115
+ const isAbort = err instanceof DOMException && err.name === "AbortError";
39116
+ opts.log.error(
39117
+ `Plugin "${plugin.name}" setup failed`,
39118
+ isAbort ? { err, reason: "setup timed out or aborted" } : { err }
39119
+ );
38211
39120
  failed.push({ plugin, err });
38212
39121
  }
38213
39122
  }
@@ -38224,11 +39133,22 @@ async function unloadPlugins(loadedPlugins, opts) {
38224
39133
  `Plugin "${plugin.name}" API not found in pluginApiMap \u2014 was setup() called?`
38225
39134
  );
38226
39135
  }
38227
- await plugin.teardown(api);
39136
+ const teardownTimeoutMs = opts.teardownTimeoutMs ?? 1e4;
39137
+ const teardownController = new AbortController();
39138
+ const teardownTimeout = setTimeout(() => teardownController.abort(), teardownTimeoutMs);
39139
+ try {
39140
+ await plugin.teardown(api, { signal: teardownController.signal });
39141
+ } finally {
39142
+ clearTimeout(teardownTimeout);
39143
+ }
38228
39144
  pluginApiMap.delete(plugin);
38229
39145
  opts.log.info(`Plugin "${plugin.name}" torn down`);
38230
39146
  } catch (err) {
38231
- opts.log.error(`Plugin "${plugin.name}" teardown failed`, err);
39147
+ const isAbort = err instanceof DOMException && err.name === "AbortError";
39148
+ opts.log.error(
39149
+ `Plugin "${plugin.name}" teardown failed`,
39150
+ isAbort ? { err, reason: "teardown timed out or aborted" } : { err }
39151
+ );
38232
39152
  }
38233
39153
  }
38234
39154
  }
@@ -38312,9 +39232,194 @@ function wrapApiForCapabilityCheck(plugin, api, log, enforce = false) {
38312
39232
  });
38313
39233
  }
38314
39234
 
39235
+ // src/plugin/api.ts
39236
+ var DefaultPluginAPI = class {
39237
+ container;
39238
+ events;
39239
+ pipelines;
39240
+ tools;
39241
+ providers;
39242
+ mcp;
39243
+ slashCommands;
39244
+ extensions;
39245
+ session;
39246
+ metrics;
39247
+ config;
39248
+ log;
39249
+ configStore;
39250
+ hookRegistry;
39251
+ ownerName;
39252
+ pluginCleanupFns = [];
39253
+ constructor(init) {
39254
+ const owner = init.ownerName;
39255
+ this.ownerName = owner;
39256
+ this.hookRegistry = init.hookRegistry;
39257
+ this.container = init.container;
39258
+ this.events = init.events;
39259
+ this.config = init.config;
39260
+ this.configStore = init.configStore;
39261
+ this.log = init.log.child({ plugin: owner });
39262
+ this.extensions = init.extensions ?? new ExtensionRegistry();
39263
+ this.session = init.sessionWriter ?? noopSession;
39264
+ this.metrics = init.metricsSink ? scopedMetrics(init.metricsSink, owner) : noopMetrics;
39265
+ const pipelines = init.pipelines;
39266
+ const readonlyPipelines = {};
39267
+ for (const [key, pipeline2] of Object.entries(pipelines)) {
39268
+ readonlyPipelines[key] = pipeline2.asReadonly();
39269
+ }
39270
+ this.pipelines = readonlyPipelines;
39271
+ const tr = init.toolRegistry;
39272
+ const isOfficial = init.official === true;
39273
+ const capabilities = init.capabilities;
39274
+ const assertCanMutateTool = (name, op) => {
39275
+ if (isOfficial) return;
39276
+ const currentOwner = tr.ownerOf(name);
39277
+ if (currentOwner === void 0) return;
39278
+ const ownedSolelyByMe = currentOwner.split("+").every((seg) => seg === owner);
39279
+ if (ownedSolelyByMe) return;
39280
+ const toolCaps = tr.get(name)?.capabilities ?? [];
39281
+ const pluginMutateCaps = capabilities?.toolMutateCapabilities ?? [];
39282
+ const hasRequiredCap = toolCaps.some((c) => pluginMutateCaps.includes(c));
39283
+ if (!hasRequiredCap) {
39284
+ throw new Error(
39285
+ `Plugin "${owner}" may not ${op} tool "${name}" \u2014 it is owned by "${currentOwner}". Tool capabilities: [${toolCaps.join(", ") || "none"}]. Plugin toolMutateCapabilities: [${pluginMutateCaps.join(", ") || "none"}]. Missing required capability to mutate this tool.`
39286
+ );
39287
+ }
39288
+ };
39289
+ this.tools = {
39290
+ register: (t2) => tr.register(t2, owner),
39291
+ unregister: (name) => {
39292
+ assertCanMutateTool(name, "unregister");
39293
+ return tr.unregister(name);
39294
+ },
39295
+ wrap: (name, wrapper) => {
39296
+ assertCanMutateTool(name, "wrap");
39297
+ tr.wrap(name, wrapper, owner);
39298
+ },
39299
+ get: (name) => tr.get(name),
39300
+ list: () => tr.list()
39301
+ };
39302
+ const pr = init.providerRegistry;
39303
+ this.providers = {
39304
+ register: (f) => pr.register(f),
39305
+ create: (cfg) => pr.create(cfg),
39306
+ list: () => pr.list()
39307
+ };
39308
+ this.mcp = init.mcpRegistry ?? noopMcp;
39309
+ const scr = init.slashCommandRegistry;
39310
+ const official = init.official === true;
39311
+ this.slashCommands = scr ? {
39312
+ register: (cmd) => scr.register(cmd, owner, { official }),
39313
+ unregister: (name) => scr.unregister(name),
39314
+ get: (name) => scr.get(name),
39315
+ list: () => scr.list()
39316
+ } : noopSlashCommands;
39317
+ }
39318
+ onEvent(event, handler) {
39319
+ const off = this.events.on(event, handler);
39320
+ this.pluginCleanupFns.push(off);
39321
+ return off;
39322
+ }
39323
+ onPattern(pattern, handler) {
39324
+ const off = this.events.onPattern(pattern, handler);
39325
+ this.pluginCleanupFns.push(off);
39326
+ return off;
39327
+ }
39328
+ emitCustom(event, payload) {
39329
+ this.events.emitCustom(event, payload);
39330
+ }
39331
+ onConfigChange(handler) {
39332
+ if (!this.configStore) return () => {
39333
+ };
39334
+ return this.configStore.watch(handler);
39335
+ }
39336
+ /** Called by the plugin loader when uninstalling the plugin. */
39337
+ drainCleanup() {
39338
+ for (const fn of this.pluginCleanupFns.splice(0)) {
39339
+ try {
39340
+ fn();
39341
+ } catch {
39342
+ }
39343
+ }
39344
+ }
39345
+ registerSystemPromptContributor(c) {
39346
+ return this.extensions.registerSystemPromptContributor(c);
39347
+ }
39348
+ registerHook(event, matcher, hook) {
39349
+ if (!this.hookRegistry) return () => {
39350
+ };
39351
+ const off = this.hookRegistry.registerInProcess(event, matcher, hook, this.ownerName);
39352
+ this.pluginCleanupFns.push(off);
39353
+ return off;
39354
+ }
39355
+ };
39356
+ var noopMcp = {
39357
+ start: async () => void 0,
39358
+ stop: async () => void 0,
39359
+ restart: async () => void 0,
39360
+ list: () => []
39361
+ };
39362
+ var noopSlashCommands = {
39363
+ register() {
39364
+ },
39365
+ unregister() {
39366
+ return false;
39367
+ },
39368
+ get() {
39369
+ return void 0;
39370
+ },
39371
+ list() {
39372
+ return [];
39373
+ }
39374
+ };
39375
+ var noopSession = {
39376
+ append: async () => {
39377
+ }
39378
+ };
39379
+ var noopMetrics = {
39380
+ counter() {
39381
+ },
39382
+ histogram() {
39383
+ },
39384
+ gauge() {
39385
+ }
39386
+ };
39387
+ function scopedMetrics(sink, pluginName) {
39388
+ const prefix = `plugin.${pluginName}.`;
39389
+ return {
39390
+ counter(name, value, labels) {
39391
+ sink.counter(`${prefix}${name}`, value, labels);
39392
+ },
39393
+ histogram(name, value, labels) {
39394
+ sink.histogram(`${prefix}${name}`, value, labels);
39395
+ },
39396
+ gauge(name, value, labels) {
39397
+ sink.gauge(`${prefix}${name}`, value, labels);
39398
+ }
39399
+ };
39400
+ }
39401
+ function definePlugin(metadata, factory) {
39402
+ return {
39403
+ name: metadata.name,
39404
+ version: metadata.version,
39405
+ description: metadata.description,
39406
+ capabilities: metadata.capabilities,
39407
+ configSchema: metadata.configSchema,
39408
+ defaultConfig: metadata.defaultConfig,
39409
+ dependsOn: metadata.dependsOn,
39410
+ optionalDeps: metadata.optionalDeps,
39411
+ conflictsWith: metadata.conflictsWith,
39412
+ apiVersion: KERNEL_API_VERSION,
39413
+ // Cast the factory to match the Plugin.setup signature.
39414
+ // The opts parameter ({ signal }) is accepted by Plugin.setup but the
39415
+ // definePlugin factory doesn't need to declare it — the host always passes
39416
+ // it at the call site; the factory just doesn't have to care about it.
39417
+ setup: (api) => factory(api, metadata.defaultConfig)
39418
+ };
39419
+ }
39420
+
38315
39421
  // src/execution/prompt-enhancer.ts
38316
39422
  var ENHANCER_SYSTEM_PROMPT = `You are a request refiner embedded in a coding agent. Your ONLY job is to rewrite the user's message into clearer, unambiguous instructions that the coding agent can act on confidently.
38317
- import { toErrorMessage } from '../utils/error.js';
38318
39423
 
38319
39424
  Rules:
38320
39425
  - Preserve the user's intent and scope EXACTLY. Do not add new requirements, features, constraints, or steps the user did not ask for. Do not remove anything they did ask for.
@@ -38408,6 +39513,7 @@ async function enhanceUserPrompt(opts) {
38408
39513
  opts.onError?.(toErrorMessage(err));
38409
39514
  return null;
38410
39515
  } finally {
39516
+ timer.abort();
38411
39517
  clearTimeout(to);
38412
39518
  }
38413
39519
  }
@@ -38859,7 +39965,7 @@ var PhaseOrchestrator = class {
38859
39965
  async mergeOne(phase, handle) {
38860
39966
  if (!this.worktrees) return;
38861
39967
  try {
38862
- const resolve14 = this.ctx.resolveConflict ? async (info) => {
39968
+ const resolve15 = this.ctx.resolveConflict ? async (info) => {
38863
39969
  const shouldResolve = await this.shouldAttemptConflictResolution(phase, info);
38864
39970
  if (!shouldResolve) return false;
38865
39971
  this.emit("phase.conflictResolving", {
@@ -38873,7 +39979,7 @@ var PhaseOrchestrator = class {
38873
39979
  const mergeOpts = {
38874
39980
  squash: true
38875
39981
  };
38876
- if (resolve14 !== void 0) mergeOpts.resolve = resolve14;
39982
+ if (resolve15 !== void 0) mergeOpts.resolve = resolve15;
38877
39983
  const result = await this.worktrees.merge(handle, mergeOpts);
38878
39984
  if (result.resolved) {
38879
39985
  this.emit("phase.conflictResolved", { phaseId: phase.id, name: phase.name });
@@ -39224,7 +40330,7 @@ var PhaseOrchestrator = class {
39224
40330
  }
39225
40331
  }
39226
40332
  delay(ms) {
39227
- return new Promise((resolve14) => setTimeout(resolve14, ms));
40333
+ return new Promise((resolve15) => setTimeout(resolve15, ms));
39228
40334
  }
39229
40335
  };
39230
40336
 
@@ -40277,8 +41383,8 @@ var CollaborationBus = class {
40277
41383
  if (this.isPaused()) return false;
40278
41384
  this.pausedAtMs = Date.now();
40279
41385
  this.pausedBy = byParticipant;
40280
- this.pausePromise = new Promise((resolve14) => {
40281
- this.pauseResolve = resolve14;
41386
+ this.pausePromise = new Promise((resolve15) => {
41387
+ this.pauseResolve = resolve15;
40282
41388
  });
40283
41389
  return true;
40284
41390
  }
@@ -40314,8 +41420,8 @@ var CollaborationBus = class {
40314
41420
  return true;
40315
41421
  }
40316
41422
  let timer;
40317
- const timeoutPromise = new Promise((resolve14) => {
40318
- timer = setTimeout(() => resolve14("timeout"), timeoutMs);
41423
+ const timeoutPromise = new Promise((resolve15) => {
41424
+ timer = setTimeout(() => resolve15("timeout"), timeoutMs);
40319
41425
  });
40320
41426
  const resumedPromise = this.pausePromise.then(() => "resumed").catch(() => "resumed");
40321
41427
  const winner = await Promise.race([resumedPromise, timeoutPromise]);
@@ -40558,8 +41664,8 @@ function extractManifestPath(msg) {
40558
41664
  }
40559
41665
  return void 0;
40560
41666
  }
40561
- function isManifestFile(path43) {
40562
- const name = pathBasename(path43).toLowerCase();
41667
+ function isManifestFile(path44) {
41668
+ const name = pathBasename(path44).toLowerCase();
40563
41669
  const manifests = [
40564
41670
  "package.json",
40565
41671
  "package-lock.json",
@@ -41063,7 +42169,7 @@ function createGitPlugin() {
41063
42169
  }
41064
42170
  async function runGit(args, cwd) {
41065
42171
  try {
41066
- return await new Promise((resolve14, reject) => {
42172
+ return await new Promise((resolve15, reject) => {
41067
42173
  const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], signal: AbortSignal.timeout(3e4), windowsHide: true });
41068
42174
  let stdout = "";
41069
42175
  let stderr = "";
@@ -41084,7 +42190,7 @@ async function runGit(args, cwd) {
41084
42190
  })
41085
42191
  );
41086
42192
  });
41087
- child.on("close", (code) => resolve14({ stdout, stderr, code: code ?? 0 }));
42193
+ child.on("close", (code) => resolve15({ stdout, stderr, code: code ?? 0 }));
41088
42194
  });
41089
42195
  } catch (err) {
41090
42196
  if (err instanceof WrongStackError) throw err;
@@ -41887,7 +42993,7 @@ If NOTHING worth flagging:
41887
42993
  ## \u{1F982} Chimera Review \u2014 all clear \u2705
41888
42994
  No issues found in N changed files.`;
41889
42995
  async function runGit2(args, cwd) {
41890
- return new Promise((resolve14, reject) => {
42996
+ return new Promise((resolve15, reject) => {
41891
42997
  let child;
41892
42998
  try {
41893
42999
  child = spawn("git", args, {
@@ -41908,8 +43014,8 @@ async function runGit2(args, cwd) {
41908
43014
  child.stderr?.on("data", (d) => {
41909
43015
  stderr += d;
41910
43016
  });
41911
- child.on("error", () => resolve14({ stdout, stderr, code: 1 }));
41912
- child.on("close", (code) => resolve14({ stdout, stderr, code: code ?? 0 }));
43017
+ child.on("error", () => resolve15({ stdout, stderr, code: 1 }));
43018
+ child.on("close", (code) => resolve15({ stdout, stderr, code: code ?? 0 }));
41913
43019
  });
41914
43020
  }
41915
43021
  async function isGitRepo2(cwd) {
@@ -42063,6 +43169,6 @@ function createChimeraPlugin() {
42063
43169
  };
42064
43170
  }
42065
43171
 
42066
- export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, TOOLS as AGENT_TOOL_PRESETS, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, ALL_SYNC_CATEGORIES, AUDIT_LOG_AGENT, Agent, AgentError, AgentStatusTracker, AnnotationsStore, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutoPhasePlanner, AutoPhaseRunner, AutonomousBrain, AutonomousCoordinator, AutonomousRunner, BUG_HUNTER_AGENT, BUILD_AGENTS, BrainDecisionQueue, BrainMonitor, BudgetExceededError, BudgetThresholdSignal, CHIMERA_REVIEW_PROMPT, CONTEXT_WINDOW_MODES, CORE_RECONSTRUCT_EVENTS, ChangeManager, CheckpointManager, CloudSync, CollabSession, CollaborationBus, ConfigError, ConfigMigrationError, ConsensusProtocol, 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_QUALITY_CHECKS, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SESSION_LOGGING_CONFIG, DEFAULT_SESSION_PRUNE_DAYS, DEFAULT_SPEC_TEMPLATE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DELIVERY_AGENTS, DEPENDENCY_FILE_PATTERNS, DISCOVERY_AGENTS, DOMAIN_AGENTS, DefaultAttachmentStore, DefaultBrainArbiter, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMailbox, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultPromptStore, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DirectorAlertLevel, DirectorStateCheckpoint, DoneConditionChecker, ENHANCER_SYSTEM_PROMPT, ERROR_CODES, EternalAutonomyEngine, EventBus, ExtensionRegistry, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FORBIDDEN_PROTO_KEYS, FileMemoryBackend, FleetBus, FleetCostCapError, FleetManager, FleetSpawnBudgetError, FleetUsageAggregator, FsError, GitignoreUpdater, GlobalMailbox, GraphMemoryBackend, HEAVY_BUDGET, HookRegistry, HookRunner, HumanEscalatingBrainArbiter, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, KNOWLEDGE_AGENTS, KnowledgeGraph, LAYER_1_IDENTITY, LIGHT_BUDGET, LLMSelector, LargeAnswerStore, MATRIX_PHASE_KEYS, MAX_JOURNAL_ENTRIES, MAX_PROGRESS_HISTORY, MEDIUM_BUDGET, MEMORY_TYPE_LABELS, META_AGENTS, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, ObservableBrainArbiter, PLANNING_AGENTS, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, PhaseGraphBuilder, PhaseOrchestrator, PhaseStore, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, REFACTOR_PLANNER_AGENT, REVIEW_AGENTS, RecoveryLock, ReplayLogStore, ReplayProviderRunner, ReportGenerator, RunController, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, STANDARD_AUDIT_EVENTS, ScopedEventBus, SddError, SddParallelRun, SddTaskDecomposer, SecurityScanner, SecurityScannerOrchestrator, SelectiveCompactor, SessionAnalyzer, SessionError, SessionMemoryConsolidator, SessionRecovery, SessionRegistry, SkillGenerator, SkillInstaller, SkillManifestStore, SlashCommandRegistry, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, StreamHangError, SubagentBudget, TOKENS, TaskAuctioneer, TaskDAG, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, TechStackDetector, ToolAuditLog, ToolError, ToolExecutor, ToolRegistry, VERIFY_AGENTS, WorktreeManager, WrongStackError, addPlanItem, allServers, analyzeCriticalPath, appendJournal, applyRosterBudget, asBlocks, asText, assertNever, assertNotPrivateHost, assertSafePath, atomicWrite, attachAutoExtend, attachDepWatcherBridge, attachMailboxChecker, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, bootConfig, braveSearchServer, buildBtwBlock, buildChildEnv, buildGoalPreamble, buildLosslessDigest, buildMailboxBlock, buildOtlpMetricsRequest, buildOtlpTracesRequest, buildQueuedMessagesBlock, buildRecoveryStrategies, buildSmartDigest, classifyFamily, clearPlan, collabInjectMiddleware, collabPauseMiddleware, color, compactLog, compileGlob, compileUserRegex, completePartialObject, composeDirectorPrompt, composeSubagentPrompt, computeMessageTokens, computeTaskItemProgress, computeTaskProgress, consumeBtwNotes, consumeQueuedMessagesUpdate, context7Server, contextManagerTool, createAutoExecutor, createAutoPhaseFromTaskGraph, createAutonomyBrain, createChimeraPlugin, createContextManagerTool, createDefaultPipelines, createDelegateTool, createGitPlugin, createMailboxChecker, createMailboxHooks, createMcpControlTool, createMcpUseTool, createMessage, createObservabilityPlugin, createPlanPlugin, createPromptsPlugin, createSecurityPlugin, createSecuritySlashCommand, createSessionEventBridge, createSkillsPlugin, createStrategyCompactor, createSyncPlugin, createTieredBrainArbiter, createToolOutputSerializer, decryptConfigSecrets, deepMerge, defaultGitignoreUpdater, defaultOrchestrator, defaultReportGenerator, defaultSecurityScanner, defaultSkillGenerator, defaultTechStackDetector, deriveTodosFromPlanItem, detectEcosystem, detectNewlineStyle, detectEcosystem as detectPackageEcosystem, dispatchAgent, downloadGitHubTarball, eliseOldToolResults, emptyGoal, emptyPlan, emptyTaskFile, encryptConfigSecrets, enhanceUserPrompt, ensureDir, estimateMessageTokens, estimateMessages, estimateRequestTokens, estimateRequestTokensCalibrated, estimateTextTokens, estimateToolDefTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, expandGlob, expandIPv6, expectDefined, extractRunEnv, extractText, filesystemServer, findCriticalPath, findPreserveStart, flagsToConfigPatch, formatContextWindowModeList, formatDecisionSummary, formatGoal, formatHumanPrompt, formatPlan, formatPlanTemplates, formatTaskList, formatTaskProgress, formatTodosList, getAgentDefinition, getCalibrationState, getContextWindowMode, getFileHistory, getFilesByAgent, getFullLog, getFullPackageLog, getLastAuthor, getManifestPackages, getPackageAuthor, getPackagesByAgent, getPlanTemplate, getSessionRegistry, getTemplate, getTermSize, githubServer, goalFilePath, googleMapsServer, hasSessionRegistry, hasTextContent, hashRequest, hookMatcherMatches, injectPendingMailboxMessages, isAgentError, isConfigError, isContextWindowModeId, isFsError, isImageBlock, isInteractive, isPluginError, isPrimitiveArray, isPrivateIPv4, isPrivateIPv6, isSddError, isSecretField, isSessionError, isStdinTTY, isStdoutTTY, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isValidMatrixKey, isWrongStackError, listContextWindowModes, listPlanTemplates, listTemplates, loadDirectorState, loadGoal, loadPlan, loadPlugins, loadProjectModes, loadTasks, loadTodosCheckpoint, loadUserModes, mailboxSessionTag, makeAgentSubagentRunner, makeAskResultTool, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeContinueToNextIterationTool, makeDependencyWatcherConfig, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeMailInboxTool, makeMailSendTool, makeMailboxTool, makeRollUpTool, makeSpawnTool, makeTerminateTool, makeWorkCompleteTool, matchAny, matchGlob, matrixKeyKind, mergeCustomModelDefs, mergeModelsPayload, migratePlaintextSecrets, miniMaxVisionServer, mutatePlan, mutateTasks, noOpLogger, noOpVault, normalizeRecipient, normalizeToLf, normalizedEqual, onResize, parseContinueDirective, parseEntries, parseProgressFromText, parseSkillRef, peekQueuedMessages, pendingBtwCount, phaseForRole, projectHash, projectSlug, recentTextTurns, recordActualUsage, recordFileAction, recordPackageAction, recordProgress, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, repairToolUseAdjacency, resetCalibration, resolveAuditLevel, resolveChimeraConfig, resolveContextWindowPolicy, resolveMailboxIdentity, resolveModelMatrix, resolveProjectDir, resolveSessionLoggingConfig, resolveWstackPaths, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, runProviderWithRetry, runShellHook, safeParse, safeStringify, sanitizeJsonString, saveGoal, savePlan, saveTasks, saveTodosCheckpoint, scoreAgents, scoreMessage, securitySlashCommand, sentinelServer, setBtwNote, setOutputLineGuard, setPlanItemStatus, setProgress, setQueuedMessagesSnapshot, setRawMode, shouldEnhance, slackServer, sleep, stableStringify, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, startPackageOutdatedWatcher, startTechStackConsumer, stripAnsi, summarizeUsage, templateToMarkdown, toErrorMessage, toStyle, toWrongStackError, topologicalSort, truncate, unifiedDiff, unloadPlugins, updatePackageOutdatedStatus, validateAgainstSchema, wireMetricsToEvents, withDisabledToolFiltering, withFileLock, wrapAsState, writeErr, writeOut, wstackGlobalRoot, zaiVisionServer };
43172
+ export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, TOOLS as AGENT_TOOL_PRESETS, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, ALL_SYNC_CATEGORIES, AUDIT_LOG_AGENT, Agent, AgentError, AgentStatusTracker, AnnotationsStore, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutoPhasePlanner, AutoPhaseRunner, AutonomousBrain, AutonomousCoordinator, AutonomousRunner, BUG_HUNTER_AGENT, BUILD_AGENTS, BrainDecisionQueue, BrainMonitor, BudgetExceededError, BudgetThresholdSignal, CHIMERA_REVIEW_PROMPT, CONTEXT_WINDOW_MODES, CORE_RECONSTRUCT_EVENTS, ChangeManager, CheckpointManager, CloudSync, CollabSession, CollaborationBus, ConfigError, ConfigMigrationError, ConsensusProtocol, Container, Context, ConversationState, DECISION_TIMEOUT_MS, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CIRCUIT_BREAKER_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_QUALITY_CHECKS, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SESSION_LOGGING_CONFIG, DEFAULT_SESSION_PRUNE_DAYS, DEFAULT_SPEC_TEMPLATE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DELIVERY_AGENTS, DEPENDENCY_FILE_PATTERNS, DISCOVERY_AGENTS, DOMAIN_AGENTS, DefaultAttachmentStore, DefaultBrainArbiter, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMailbox, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultPromptStore, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DirectorAlertLevel, DirectorStateCheckpoint, DoneConditionChecker, ENHANCER_SYSTEM_PROMPT, ERROR_CODES, EternalAutonomyEngine, EventBus, ExtensionRegistry, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FORBIDDEN_PROTO_KEYS, FileMemoryBackend, FleetBus, FleetCostCapError, FleetManager, FleetNotifier, FleetSpawnBudgetError, FleetUsageAggregator, FsError, GitignoreUpdater, GlobalMailbox, GraphMemoryBackend, HEAVY_BUDGET, HookRegistry, HookRunner, HumanEscalatingBrainArbiter, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, KNOWLEDGE_AGENTS, KnowledgeGraph, LAYER_1_IDENTITY, LIGHT_BUDGET, LLMSelector, LargeAnswerStore, MATRIX_PHASE_KEYS, MAX_JOURNAL_ENTRIES, MAX_PROGRESS_HISTORY, MEDIUM_BUDGET, MEMORY_TYPE_LABELS, META_AGENTS, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, ObservableBrainArbiter, PLANNING_AGENTS, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, PhaseGraphBuilder, PhaseOrchestrator, PhaseStore, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, REFACTOR_PLANNER_AGENT, REVIEW_AGENTS, RecoveryLock, ReplayLogStore, ReplayProviderRunner, ReportGenerator, RunController, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, STANDARD_AUDIT_EVENTS, ScopedEventBus, SddError, SddParallelRun, SddTaskDecomposer, SecurityScanner, SecurityScannerOrchestrator, SelectiveCompactor, SessionAnalyzer, SessionError, SessionMemoryConsolidator, SessionRecovery, SessionRegistry, SkillGenerator, SkillInstaller, SkillManifestStore, SlashCommandRegistry, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, StreamHangError, SubagentBudget, TIMEOUT_PREEMPT_FRACTION, TOKENS, TaskAuctioneer, TaskDAG, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, TechStackDetector, ToolAuditLog, ToolError, ToolExecutor, ToolRegistry, VERIFY_AGENTS, WorktreeManager, WrongStackError, addPlanItem, allServers, analyzeCriticalPath, appendJournal, applyRosterBudget, asBlocks, asText, assertNever, assertNotPrivateHost, assertSafePath, atomicWrite, attachAutoExtend, attachDepWatcherBridge, attachMailboxChecker, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, bootConfig, braveSearchServer, buildBtwBlock, buildChildEnv, buildGoalPreamble, buildLosslessDigest, buildMailboxBlock, buildOtlpMetricsRequest, buildOtlpTracesRequest, buildQueuedMessagesBlock, buildRecoveryStrategies, buildSmartDigest, classifyFamily, clearPlan, collabInjectMiddleware, collabPauseMiddleware, color, compactLog, compileGlob, compileUserRegex, completePartialObject, composeDirectorPrompt, composeSubagentPrompt, computeMessageTokens, computeTaskItemProgress, computeTaskProgress, consumeBtwNotes, consumeQueuedMessagesUpdate, context7Server, contextManagerTool, createAutoExecutor, createAutoPhaseFromTaskGraph, createAutonomyBrain, createChimeraPlugin, createContextManagerTool, createDefaultPipelines, createDelegateTool, createGitPlugin, createMailboxChecker, createMailboxHooks, createMcpControlTool, createMcpUseTool, createMessage, createObservabilityPlugin, createPlanPlugin, createPromptsPlugin, createSecurityPlugin, createSecuritySlashCommand, createSessionEventBridge, createSkillsPlugin, createStrategyCompactor, createSyncPlugin, createTieredBrainArbiter, createToolOutputSerializer, decryptConfigSecrets, deepMerge, defaultGitignoreUpdater, defaultOrchestrator, defaultReportGenerator, defaultSecurityScanner, defaultSkillGenerator, defaultTechStackDetector, definePlugin, deriveTodosFromPlanItem, detectEcosystem, detectNewlineStyle, detectEcosystem as detectPackageEcosystem, dispatchAgent, downloadGitHubTarball, eliseOldToolResults, emptyGoal, emptyPlan, emptyTaskFile, encryptConfigSecrets, encryptedPrefixForVersion, enhanceUserPrompt, ensureDir, estimateMessageTokens, estimateMessages, estimateRequestTokens, estimateRequestTokensCalibrated, estimateTextTokens, estimateToolDefTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, expandGlob, expandIPv6, expectDefined, extractRunEnv, extractText, filesystemServer, findCriticalPath, findPreserveStart, flagsToConfigPatch, formatContextWindowModeList, formatDecisionSummary, formatGoal, formatHumanPrompt, formatPlan, formatPlanTemplates, formatTaskList, formatTaskProgress, formatTodosList, getAgentDefinition, getCalibrationState, getContextWindowMode, getFileHistory, getFilesByAgent, getFullLog, getFullPackageLog, getLastAuthor, getManifestPackages, getPackageAuthor, getPackagesByAgent, getPlanTemplate, getSessionRegistry, getTemplate, getTermSize, githubServer, goalFilePath, googleMapsServer, hasSessionRegistry, hasTextContent, hashRequest, hookMatcherMatches, injectPendingMailboxMessages, isAgentError, isConfigError, isContextWindowModeId, isFsError, isImageBlock, isInteractive, isPluginError, isPrimitiveArray, isPrivateIPv4, isPrivateIPv6, isSddError, isSecretField, isSessionError, isStdinTTY, isStdoutTTY, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isValidMatrixKey, isWrongStackError, listContextWindowModes, listPlanTemplates, listTemplates, loadDirectorState, loadGoal, loadPlan, loadPlugins, loadProjectModes, loadTasks, loadTodosCheckpoint, loadUserModes, mailboxSessionTag, makeAgentSubagentRunner, makeAskResultTool, makeAskTool, makeAssignTool, makeAutonomyPromptContributor, makeAwaitTasksTool, makeCollabDebugTool, makeContinueToNextIterationTool, makeDependencyWatcherConfig, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeMailInboxTool, makeMailSendTool, makeMailboxTool, makeRollUpTool, makeSpawnTool, makeTerminateTool, makeWorkCompleteTool, matchAny, matchGlob, matrixKeyKind, mergeCustomModelDefs, mergeModelsPayload, migratePlaintextSecrets, miniMaxVisionServer, mutatePlan, mutateTasks, noOpLogger, noOpVault, normalizeRecipient, normalizeToLf, normalizeTokenSavingTier, normalizedEqual, onResize, parseContinueDirective, parseEncryptedVersion, parseEntries, parseProgressFromText, parseSkillRef, peekQueuedMessages, pendingBtwCount, phaseForRole, projectHash, projectSlug, recentTextTurns, recordActualUsage, recordFileAction, recordPackageAction, recordProgress, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, repairToolUseAdjacency, resetCalibration, resolveAuditLevel, resolveChimeraConfig, resolveContextWindowPolicy, resolveMailboxIdentity, resolveModelMatrix, resolveProjectDir, resolveSessionLoggingConfig, resolveWstackPaths, rewriteConfigEncrypted, rosterSummaryFromConfigs, rotateConfigKeys, runConfigMigrations, runProviderWithRetry, runShellHook, safeParse, safeStringify, sanitizeJsonString, saveGoal, savePlan, saveTasks, saveTodosCheckpoint, scoreAgents, scoreMessage, securitySlashCommand, sentinelServer, setBtwNote, setOutputLineGuard, setPlanItemStatus, setProgress, setQueuedMessagesSnapshot, setRawMode, shouldEnhance, slackServer, sleep, stableStringify, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, startPackageOutdatedWatcher, startTechStackConsumer, stripAnsi, summarizeUsage, templateToMarkdown, toErrorMessage, toStyle, toWrongStackError, topologicalSort, truncate, unifiedDiff, unloadPlugins, updatePackageOutdatedStatus, validateAgainstSchema, wireMetricsToEvents, withDisabledToolFiltering, withFileLock, wrapAsState, writeErr, writeOut, wstackGlobalRoot, zaiVisionServer };
42067
43173
  //# sourceMappingURL=index.js.map
42068
43174
  //# sourceMappingURL=index.js.map