@wrongstack/core 0.264.0 → 0.267.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/{agent-bridge-D8sa1vtv.d.ts → agent-bridge-STJ3JwwK.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-c9DLkaas.d.ts → agent-subagent-runner-CzPGP3jA.d.ts} +131 -11
  3. package/dist/{brain-O1IdKPaK.d.ts → brain-Cdg77tVN.d.ts} +103 -2
  4. package/dist/{compactor-BBy0rCtB.d.ts → compactor-iMZ84CXq.d.ts} +19 -1
  5. package/dist/{config-Dz2F3H2K.d.ts → config-Du3pYYln.d.ts} +132 -13
  6. package/dist/{context-BGSpZNSE.d.ts → context-dT5Ueund.d.ts} +90 -12
  7. package/dist/coordination/index.d.ts +78 -22
  8. package/dist/coordination/index.js +695 -273
  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 +2327 -965
  13. package/dist/defaults/index.js.map +1 -1
  14. package/dist/execution/index.d.ts +16 -16
  15. package/dist/execution/index.js +1500 -371
  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-SulMTowG.d.ts} +33 -12
  22. package/dist/{goal-store-CxWmCGbH.d.ts → goal-store-CABDwdFE.d.ts} +1 -1
  23. package/dist/{index-CbLSI66_.d.ts → index-Bms0m4oy.d.ts} +5 -5
  24. package/dist/{index-CYIQrXVF.d.ts → index-DtCVWel4.d.ts} +8 -8
  25. package/dist/index-IEuxQd-E.d.ts +82 -0
  26. package/dist/index.d.ts +261 -57
  27. package/dist/index.js +4799 -2212
  28. package/dist/index.js.map +1 -1
  29. package/dist/infrastructure/index.d.ts +6 -6
  30. package/dist/infrastructure/index.js +84 -9
  31. package/dist/infrastructure/index.js.map +1 -1
  32. package/dist/kernel/index.d.ts +9 -9
  33. package/dist/kernel/index.js +1 -1
  34. package/dist/kernel/index.js.map +1 -1
  35. package/dist/{mcp-servers-DC4QRPUI.d.ts → mcp-servers-C2cBTxUR.d.ts} +3 -3
  36. package/dist/models/index.d.ts +5 -5
  37. package/dist/models/index.js +104 -31
  38. package/dist/models/index.js.map +1 -1
  39. package/dist/{models-registry-B_siPxqN.d.ts → models-registry-BqGZNJQ-.d.ts} +1 -1
  40. package/dist/{multi-agent-coordinator-CK5Jdj9K.d.ts → multi-agent-coordinator-B8R43uPz.d.ts} +1 -1
  41. package/dist/{null-fleet-bus-DgvD4SCO.d.ts → null-fleet-bus-CnXa5oTH.d.ts} +14 -9
  42. package/dist/observability/index.d.ts +2 -2
  43. package/dist/{parallel-eternal-engine-bK0JQBR_.d.ts → parallel-eternal-engine-DdNnw9BQ.d.ts} +11 -9
  44. package/dist/{path-resolver-BPEDlN38.d.ts → path-resolver-COIMLCQL.d.ts} +3 -3
  45. package/dist/{permission-4yvGmMRB.d.ts → permission-B75JAi3-.d.ts} +1 -1
  46. package/dist/{permission-policy-C6XpsBOy.d.ts → permission-policy-DlR9eJAM.d.ts} +2 -2
  47. package/dist/{pipeline-CXCeMz8J.d.ts → pipeline-BfD2k1rT.d.ts} +3 -3
  48. package/dist/{plan-templates-BvzRBkJc.d.ts → plan-templates-DSIKCXZN.d.ts} +32 -8
  49. package/dist/provider-model-resolve-BNRsNuJx.d.ts +107 -0
  50. package/dist/{provider-runner-C5aQpDWE.d.ts → provider-runner-CX7iIvox.d.ts} +3 -3
  51. package/dist/{retry-policy-CFhdtRzz.d.ts → retry-policy-BilV1ujH.d.ts} +1 -1
  52. package/dist/sdd/index.d.ts +8 -8
  53. package/dist/sdd/index.js +286 -105
  54. package/dist/sdd/index.js.map +1 -1
  55. package/dist/secret-vault-BAKpgFw_.d.ts +57 -0
  56. package/dist/{secret-vault-CxiVLbt1.d.ts → secret-vault-gkvEZZfE.d.ts} +43 -4
  57. package/dist/security/index.d.ts +6 -68
  58. package/dist/security/index.js +296 -95
  59. package/dist/security/index.js.map +1 -1
  60. package/dist/{selector-gIuhRTkN.d.ts → selector-Bc7eWtT3.d.ts} +1 -1
  61. package/dist/{session-event-bridge-DkvvrpDt.d.ts → session-event-bridge-D-araDEz.d.ts} +1 -1
  62. package/dist/{session-reader-KdfVwkKP.d.ts → session-reader-D7Dapswh.d.ts} +1 -1
  63. package/dist/storage/index.d.ts +112 -15
  64. package/dist/storage/index.js +491 -156
  65. package/dist/storage/index.js.map +1 -1
  66. package/dist/tools/index.d.ts +4 -2
  67. package/dist/tools/index.js.map +1 -1
  68. package/dist/types/index.d.ts +21 -21
  69. package/dist/types/index.js +1523 -450
  70. package/dist/types/index.js.map +1 -1
  71. package/dist/utils/index.d.ts +455 -407
  72. package/dist/utils/index.js +2191 -1203
  73. package/dist/utils/index.js.map +1 -1
  74. package/dist/{wstack-paths-CJjEwPXn.d.ts → wstack-paths-hOpNLmvf.d.ts} +2 -0
  75. package/package.json +1 -1
  76. package/skills/api-design/SKILL.md +1 -1
  77. package/skills/audit-log/SKILL.md +6 -6
  78. package/skills/bug-hunter/SKILL.md +5 -5
  79. package/skills/chimera/SKILL.md +4 -4
  80. package/skills/docker-deploy/SKILL.md +1 -1
  81. package/skills/git-flow/SKILL.md +3 -3
  82. package/skills/multi-agent/SKILL.md +3 -3
  83. package/skills/node-modern/SKILL.md +1 -0
  84. package/skills/observability/SKILL.md +2 -2
  85. package/skills/output-standards/SKILL.md +51 -28
  86. package/skills/refactor-planner/SKILL.md +3 -3
  87. package/skills/security-scanner/SKILL.md +4 -3
  88. package/skills/tech-stack/SKILL.md +1 -2
  89. package/dist/llm-selector-DzxuZnNz.d.ts +0 -58
  90. package/dist/secret-vault-BJDY28ev.d.ts +0 -25
@@ -449,11 +449,6 @@ function safeParse(input, maxBytes = 5e6) {
449
449
  }
450
450
  }
451
451
 
452
- // src/utils/string.ts
453
- function truncate(s, max) {
454
- return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
455
- }
456
-
457
452
  // src/utils/expect-defined.ts
458
453
  function expectDefined(value, label) {
459
454
  if (value === null || value === void 0) {
@@ -463,111 +458,6 @@ function expectDefined(value, label) {
463
458
  }
464
459
  return value;
465
460
  }
466
- function projectSlug(absRoot) {
467
- const base = slugify(path5.basename(absRoot));
468
- const hash = createHash("sha256").update(path5.resolve(absRoot)).digest("hex").slice(0, 6);
469
- return `${base}-${hash}`;
470
- }
471
- function slugify(name) {
472
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
473
- }
474
- function wstackGlobalRoot() {
475
- const fromEnv = process.env["WRONGSTACK_HOME"];
476
- if (fromEnv && fromEnv.trim().length > 0) return path5.resolve(fromEnv);
477
- return path5.join(os.homedir(), ".wrongstack");
478
- }
479
-
480
- // src/utils/message-invariants.ts
481
- function repairToolUseAdjacency(messages) {
482
- const removedToolUses = [];
483
- const removedToolResults = [];
484
- let removedMessages = 0;
485
- let changed = false;
486
- const out = [];
487
- for (let i = 0; i < messages.length; i++) {
488
- const original = expectDefined(messages[i]);
489
- let msg = original;
490
- if (hasToolUse(msg)) {
491
- const nextIds = toolResultIds(messages[i + 1]);
492
- const filtered = mapContent(msg, (blocks) => {
493
- const next = [];
494
- for (const block of blocks) {
495
- if (block.type === "tool_use" && !nextIds.has(block.id)) {
496
- removedToolUses.push(block.id);
497
- changed = true;
498
- continue;
499
- }
500
- next.push(block);
501
- }
502
- return next;
503
- });
504
- msg = filtered ?? msg;
505
- }
506
- if (hasToolResult(msg)) {
507
- const allowed = toolUseIds(out[out.length - 1]);
508
- const filtered = mapContent(msg, (blocks) => {
509
- const next = [];
510
- for (const block of blocks) {
511
- if (block.type === "tool_result" && !allowed.has(block.tool_use_id)) {
512
- removedToolResults.push(block.tool_use_id);
513
- changed = true;
514
- continue;
515
- }
516
- next.push(block);
517
- }
518
- return next;
519
- });
520
- msg = filtered ?? msg;
521
- }
522
- if (isEmptyMessage(msg)) {
523
- removedMessages++;
524
- changed = true;
525
- continue;
526
- }
527
- out.push(msg);
528
- }
529
- return {
530
- messages: changed ? out : messages,
531
- report: { changed, removedToolUses, removedToolResults, removedMessages }
532
- };
533
- }
534
- function hasToolUse(msg) {
535
- return contentBlocks(msg).some((b) => b.type === "tool_use");
536
- }
537
- function hasToolResult(msg) {
538
- return contentBlocks(msg).some((b) => b.type === "tool_result");
539
- }
540
- function toolUseIds(msg) {
541
- const ids = /* @__PURE__ */ new Set();
542
- if (!msg || msg.role !== "assistant") return ids;
543
- for (const block of contentBlocks(msg)) {
544
- if (block.type === "tool_use") ids.add(block.id);
545
- }
546
- return ids;
547
- }
548
- function toolResultIds(msg) {
549
- const ids = /* @__PURE__ */ new Set();
550
- if (!msg || msg.role !== "user") return ids;
551
- for (const block of contentBlocks(msg)) {
552
- if (block.type === "tool_result") ids.add(block.tool_use_id);
553
- }
554
- return ids;
555
- }
556
- function contentBlocks(msg) {
557
- return msg && Array.isArray(msg.content) ? msg.content : [];
558
- }
559
- function mapContent(msg, fn) {
560
- if (!Array.isArray(msg.content)) return msg;
561
- const next = fn(msg.content);
562
- if (next.length === msg.content.length && next.every((b, idx) => b === msg.content[idx])) {
563
- return msg;
564
- }
565
- return { ...msg, content: next };
566
- }
567
- function isEmptyMessage(msg) {
568
- if (typeof msg.content === "string") return msg.content.trim().length === 0;
569
- return msg.content.length === 0;
570
- }
571
461
  var GLOB_CHARS = /* @__PURE__ */ new Set(["*", "?", "["]);
572
462
  var IS_WINDOWS = process.platform === "win32";
573
463
  var SEP = IS_WINDOWS ? "\\" : "/";
@@ -684,6 +574,116 @@ async function expandGlob(pattern) {
684
574
  return [...results];
685
575
  }
686
576
 
577
+ // src/utils/message-invariants.ts
578
+ function repairToolUseAdjacency(messages) {
579
+ const removedToolUses = [];
580
+ const removedToolResults = [];
581
+ let removedMessages = 0;
582
+ let changed = false;
583
+ const out = [];
584
+ for (let i = 0; i < messages.length; i++) {
585
+ const original = expectDefined(messages[i]);
586
+ let msg = original;
587
+ if (hasToolUse(msg)) {
588
+ const nextIds = toolResultIds(messages[i + 1]);
589
+ const filtered = mapContent(msg, (blocks) => {
590
+ const next = [];
591
+ for (const block of blocks) {
592
+ if (block.type === "tool_use" && !nextIds.has(block.id)) {
593
+ removedToolUses.push(block.id);
594
+ changed = true;
595
+ continue;
596
+ }
597
+ next.push(block);
598
+ }
599
+ return next;
600
+ });
601
+ msg = filtered ?? msg;
602
+ }
603
+ if (hasToolResult(msg)) {
604
+ const allowed = toolUseIds(out[out.length - 1]);
605
+ const filtered = mapContent(msg, (blocks) => {
606
+ const next = [];
607
+ for (const block of blocks) {
608
+ if (block.type === "tool_result" && !allowed.has(block.tool_use_id)) {
609
+ removedToolResults.push(block.tool_use_id);
610
+ changed = true;
611
+ continue;
612
+ }
613
+ next.push(block);
614
+ }
615
+ return next;
616
+ });
617
+ msg = filtered ?? msg;
618
+ }
619
+ if (isEmptyMessage(msg)) {
620
+ removedMessages++;
621
+ changed = true;
622
+ continue;
623
+ }
624
+ out.push(msg);
625
+ }
626
+ return {
627
+ messages: changed ? out : messages,
628
+ report: { changed, removedToolUses, removedToolResults, removedMessages }
629
+ };
630
+ }
631
+ function hasToolUse(msg) {
632
+ return contentBlocks(msg).some((b) => b.type === "tool_use");
633
+ }
634
+ function hasToolResult(msg) {
635
+ return contentBlocks(msg).some((b) => b.type === "tool_result");
636
+ }
637
+ function toolUseIds(msg) {
638
+ const ids = /* @__PURE__ */ new Set();
639
+ if (!msg || msg.role !== "assistant") return ids;
640
+ for (const block of contentBlocks(msg)) {
641
+ if (block.type === "tool_use") ids.add(block.id);
642
+ }
643
+ return ids;
644
+ }
645
+ function toolResultIds(msg) {
646
+ const ids = /* @__PURE__ */ new Set();
647
+ if (!msg || msg.role !== "user") return ids;
648
+ for (const block of contentBlocks(msg)) {
649
+ if (block.type === "tool_result") ids.add(block.tool_use_id);
650
+ }
651
+ return ids;
652
+ }
653
+ function contentBlocks(msg) {
654
+ return msg && Array.isArray(msg.content) ? msg.content : [];
655
+ }
656
+ function mapContent(msg, fn) {
657
+ if (!Array.isArray(msg.content)) return msg;
658
+ const next = fn(msg.content);
659
+ if (next.length === msg.content.length && next.every((b, idx) => b === msg.content[idx])) {
660
+ return msg;
661
+ }
662
+ return { ...msg, content: next };
663
+ }
664
+ function isEmptyMessage(msg) {
665
+ if (typeof msg.content === "string") return msg.content.trim().length === 0;
666
+ return msg.content.length === 0;
667
+ }
668
+
669
+ // src/utils/string.ts
670
+ function truncate(s, max) {
671
+ return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
672
+ }
673
+ function projectSlug(absRoot) {
674
+ const base = slugify(path5.basename(absRoot));
675
+ const hash = createHash("sha256").update(path5.resolve(absRoot)).digest("hex").slice(0, 6);
676
+ return `${base}-${hash}`;
677
+ }
678
+ function slugify(name) {
679
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
680
+ }
681
+ function wstackGlobalRoot() {
682
+ const fromEnv = process.env["WRONGSTACK_HOME"];
683
+ if (fromEnv && fromEnv.trim().length > 0) return path5.resolve(fromEnv);
684
+ return path5.join(os.homedir(), ".wrongstack");
685
+ }
686
+
687
687
  // src/types/errors.ts
688
688
  var ERROR_CODES = {
689
689
  // Provider
@@ -1539,6 +1539,24 @@ Working rules:
1539
1539
  var DEFAULT_SUBAGENT_BASELINE = `You are a subagent operating under a Director. You were spawned to handle
1540
1540
  a specific slice of a larger plan \u2014 do that slice well and report back.
1541
1541
 
1542
+ Capabilities & operating rules:
1543
+ - You have full developer tools for your task: read, write/edit, search,
1544
+ shell + build (lint, format, typecheck, test), and dependency install.
1545
+ Use them directly to finish the task end-to-end. You run non-interactively
1546
+ \u2014 there is no human to approve individual tool calls, so routine work is
1547
+ pre-authorized; do not stop to ask for permission to read, edit, or build.
1548
+ - Stay inside the project root. Do not write files outside the repository,
1549
+ and do not touch machine config, credentials, or global state \u2014 those
1550
+ require an explicit grant you do not have.
1551
+ - Prefer the least-destructive path. Do not run irreversible or destructive
1552
+ commands (e.g. \`rm -rf\`, \`git push --force\`, history rewrites, dropping
1553
+ databases, mass deletes) unless the task explicitly requires it and names
1554
+ the target.
1555
+ - When you change code, verify it: run the relevant build / typecheck / tests
1556
+ and fix what you broke before reporting done.
1557
+ - Make only the changes the task calls for \u2014 don't refactor or reformat
1558
+ unrelated code.
1559
+
1542
1560
  Bridge contract:
1543
1561
  - You have a parent (the Director). You may call \`request\` on the
1544
1562
  parent bridge to ask a clarifying question. Use this sparingly; the
@@ -5089,6 +5107,7 @@ function resolveModelMatrix(matrix, role) {
5089
5107
 
5090
5108
  // src/coordination/subagent-budget.ts
5091
5109
  var TIMEOUT_PREEMPT_FRACTION = 0.85;
5110
+ var DECISION_TIMEOUT_MS = 6e4;
5092
5111
  var BudgetExceededError = class extends Error {
5093
5112
  kind;
5094
5113
  limit;
@@ -5118,6 +5137,31 @@ var BudgetThresholdSignal = class extends Error {
5118
5137
  };
5119
5138
  var SubagentBudget = class _SubagentBudget {
5120
5139
  limits;
5140
+ /** Patch one or more budget limits in-place after construction.
5141
+ * Used by the coordinator watchdog when granting an extension.
5142
+ * All fields are optional — only provided fields are updated.
5143
+ * This is the single write path for limit mutations so that future
5144
+ * validation or side-effects live in one place (M1). */
5145
+ patchLimits(ext) {
5146
+ if (ext.maxIterations !== void 0) {
5147
+ this.limits.maxIterations = ext.maxIterations;
5148
+ }
5149
+ if (ext.maxToolCalls !== void 0) {
5150
+ this.limits.maxToolCalls = ext.maxToolCalls;
5151
+ }
5152
+ if (ext.maxTokens !== void 0) {
5153
+ this.limits.maxTokens = ext.maxTokens;
5154
+ }
5155
+ if (ext.maxCostUsd !== void 0) {
5156
+ this.limits.maxCostUsd = ext.maxCostUsd;
5157
+ }
5158
+ if (ext.timeoutMs !== void 0) {
5159
+ this.limits.timeoutMs = ext.timeoutMs;
5160
+ }
5161
+ if (ext.idleTimeoutMs !== void 0) {
5162
+ this.limits.idleTimeoutMs = ext.idleTimeoutMs;
5163
+ }
5164
+ }
5121
5165
  iterations = 0;
5122
5166
  toolCalls = 0;
5123
5167
  tokenInput = 0;
@@ -5138,12 +5182,44 @@ var SubagentBudget = class _SubagentBudget {
5138
5182
  * or hung listener (Director not built / event filter detached mid-run)
5139
5183
  * leaves the budget over-limit and never enforces anything.
5140
5184
  */
5141
- static DECISION_TIMEOUT_MS = 6e4;
5185
+ static DECISION_TIMEOUT_MS = DECISION_TIMEOUT_MS;
5142
5186
  /**
5143
5187
  * Injected by the runner when wiring the budget to its EventBus.
5144
5188
  * Used to emit `budget.threshold_reached` events in `'auto'` mode.
5145
5189
  */
5146
5190
  _events;
5191
+ /**
5192
+ * Guard against dual-path races between the coordinator watchdog
5193
+ * (`executeWithTimeout`) and the budget's own `checkTimeout()`.
5194
+ * Both paths detect `elapsed >= timeoutMs` and can emit
5195
+ * `budget.threshold_reached` for kind `'timeout'` simultaneously.
5196
+ * Set to the current `timeoutMs` ceiling by the coordinator BEFORE
5197
+ * calling `onThreshold`, and cleared after the negotiation resolves.
5198
+ * `checkTimeout()` skips its wall-clock check while this is set so
5199
+ * the coordinator's watchdog is the sole source of wall-clock timeout
5200
+ * events — `checkTimeout()` focuses exclusively on `idle_timeout`.
5201
+ */
5202
+ _watchdogActive;
5203
+ /** Returns the timeout ceiling currently being negotiated by the watchdog,
5204
+ * or `undefined` when no wall-clock negotiation is in flight.
5205
+ * Used by `executeWithTimeout` to detect a stale lock (M3). */
5206
+ get watchdogActive() {
5207
+ return this._watchdogActive;
5208
+ }
5209
+ /** Called by the coordinator watchdog BEFORE calling `onThreshold` so that
5210
+ * `checkTimeout()` skips its wall-clock check for this ceiling. Prevents
5211
+ * the budget's own `checkTimeout()` from emitting a second
5212
+ * `budget.threshold_reached` event while the watchdog is already
5213
+ * negotiating the same wall-clock deadline (C1). */
5214
+ setWatchdogNegotiation(timeoutMs) {
5215
+ this._watchdogActive = timeoutMs;
5216
+ }
5217
+ /** Clears the watchdog guard after negotiation resolves. Called in the
5218
+ * `finally` block of both the pre-empt and deadline branches so it fires
5219
+ * on every exit path: grant, deny, throw, or error. */
5220
+ clearWatchdogNegotiation() {
5221
+ this._watchdogActive = void 0;
5222
+ }
5147
5223
  /**
5148
5224
  * Negotiation mode — controls whether a threshold hit tries to emit
5149
5225
  * `budget.threshold_reached` and wait for a coordinator decision, or
@@ -5244,7 +5320,8 @@ var SubagentBudget = class _SubagentBudget {
5244
5320
  if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
5245
5321
  exceeded.push({ kind: "idle_timeout", used: idle, limit: this.limits.idleTimeoutMs });
5246
5322
  }
5247
- if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs) {
5323
+ const wallOwnedByWatchdog = this._onThreshold !== void 0 && this._watchdogActive === this.limits.timeoutMs;
5324
+ if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs && !wallOwnedByWatchdog) {
5248
5325
  exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
5249
5326
  }
5250
5327
  }
@@ -5258,19 +5335,99 @@ var SubagentBudget = class _SubagentBudget {
5258
5335
  throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
5259
5336
  }
5260
5337
  const bus = this._events;
5261
- if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
5338
+ if (!bus) {
5262
5339
  const first2 = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
5263
5340
  throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
5264
5341
  }
5342
+ const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
5343
+ if (bus.hasListenerFor("budget.threshold_reached")) {
5344
+ for (const entry of exceeded) {
5345
+ if (this._pendingNegotiations.has(entry.kind)) continue;
5346
+ this._pendingNegotiations.set(entry.kind, this._negotiateExtension(entry));
5347
+ }
5348
+ const decision = this._pendingNegotiations.get(first.kind);
5349
+ if (!decision) throw new Error(`No pending negotiation for ${first.kind}`);
5350
+ throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
5351
+ }
5352
+ let hardStop = null;
5265
5353
  for (const entry of exceeded) {
5266
5354
  if (this._pendingNegotiations.has(entry.kind)) continue;
5267
- const decision2 = this._negotiateExtension(entry.kind, exceeded);
5268
- this._pendingNegotiations.set(entry.kind, decision2);
5355
+ const marker = Promise.resolve("stop");
5356
+ this._pendingNegotiations.set(entry.kind, marker);
5357
+ void marker.finally(() => this._pendingNegotiations.delete(entry.kind));
5358
+ const sync = this._invokeHandlerSync(entry);
5359
+ if (!sync) hardStop ??= new BudgetExceededError(entry.kind, entry.limit, entry.used);
5360
+ }
5361
+ if (hardStop) throw hardStop;
5362
+ return exceeded;
5363
+ }
5364
+ /**
5365
+ * Invoke `onThreshold` once for `entry` on the NO-LISTENER path and report
5366
+ * whether it decided synchronously. Returns `true` when the handler returned
5367
+ * a synchronous decision (already honored — an `extend` patched the limits),
5368
+ * or `false` when it returned a Promise (async; the caller hard-stops, since
5369
+ * there is no listener to resolve the negotiation). The handler is given the
5370
+ * full info shape (`requestDecision` plus direct `extend`/`deny`) so both
5371
+ * recording handlers and policy handlers work without a wired listener.
5372
+ */
5373
+ _invokeHandlerSync(entry) {
5374
+ const handler = this._onThreshold;
5375
+ if (!handler) return false;
5376
+ let extendArg;
5377
+ const result = handler({
5378
+ kind: entry.kind,
5379
+ used: entry.used,
5380
+ limit: entry.limit,
5381
+ requestDecision: () => this._busRequestDecision(entry),
5382
+ // Direct hooks for synchronous policy/recording handlers.
5383
+ extend: (extra) => {
5384
+ extendArg = extra;
5385
+ },
5386
+ deny: () => {
5387
+ }
5388
+ });
5389
+ if (result && typeof result.then === "function") return false;
5390
+ if (result === "throw") return false;
5391
+ if (result && typeof result === "object" && "extend" in result) {
5392
+ extendArg = result.extend;
5269
5393
  }
5270
- const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
5271
- const decision = this._pendingNegotiations.get(first.kind);
5272
- if (!decision) throw new Error(`No pending negotiation for ${first.kind}`);
5273
- throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
5394
+ if (extendArg) this.patchLimits(extendArg);
5395
+ return true;
5396
+ }
5397
+ /**
5398
+ * Emit `budget.threshold_reached` and resolve to the listener's verdict.
5399
+ * Resolves to `'stop'` immediately when there is no listener (or no bus) so
5400
+ * no negotiation can hang and no fallback timer leaks. Mirrors the
5401
+ * coordinator watchdog's own request path so both agree on the no-listener
5402
+ * default.
5403
+ */
5404
+ _busRequestDecision(entry) {
5405
+ const bus = this._events;
5406
+ if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
5407
+ return Promise.resolve("stop");
5408
+ }
5409
+ return new Promise((resolve3) => {
5410
+ let resolved = false;
5411
+ const respond = (d) => {
5412
+ if (resolved) return;
5413
+ resolved = true;
5414
+ clearTimeout(fallback);
5415
+ resolve3(d);
5416
+ };
5417
+ const fallback = setTimeout(() => respond("stop"), _SubagentBudget.DECISION_TIMEOUT_MS);
5418
+ bus.emit("budget.threshold_reached", {
5419
+ kind: entry.kind,
5420
+ used: entry.used,
5421
+ limit: entry.limit,
5422
+ timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
5423
+ // deny() wins over a same-dispatch extend(): a listener that both grants
5424
+ // and denies (or two listeners disagreeing) is resolved as a stop. The
5425
+ // grant is deferred a microtask so a synchronous deny in the same emit
5426
+ // pre-empts it; async grants still resolve normally.
5427
+ extend: (extra) => queueMicrotask(() => respond({ extend: extra })),
5428
+ deny: () => respond("stop")
5429
+ });
5430
+ });
5274
5431
  }
5275
5432
  /**
5276
5433
  * Per-kind in-flight negotiation Promises. Each budget kind can have its
@@ -5290,77 +5447,33 @@ var SubagentBudget = class _SubagentBudget {
5290
5447
  * `{ extend: {} }` — keep going without patching; next overrun fires
5291
5448
  * a fresh signal.
5292
5449
  */
5293
- async _negotiateExtension(kind, exceeded) {
5450
+ async _negotiateExtension(entry) {
5294
5451
  if (!this._onThreshold) {
5295
5452
  return "stop";
5296
5453
  }
5297
5454
  try {
5298
- const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
5299
5455
  const result = this._onThreshold({
5300
- kind: first.kind,
5301
- used: first.used,
5302
- limit: first.limit,
5303
- requestDecision: () => {
5304
- const bus = this._events;
5305
- if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
5306
- return Promise.resolve("stop");
5307
- }
5308
- return new Promise((resolve3) => {
5309
- let resolved = false;
5310
- const respond = (d) => {
5311
- if (resolved) return;
5312
- resolved = true;
5313
- resolve3(d);
5314
- };
5315
- const fallback = setTimeout(
5316
- () => respond("stop"),
5317
- _SubagentBudget.DECISION_TIMEOUT_MS
5318
- );
5319
- for (const { kind: kind2, used, limit } of exceeded) {
5320
- bus.emit("budget.threshold_reached", {
5321
- kind: kind2,
5322
- used,
5323
- limit,
5324
- timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
5325
- extend: (extra) => {
5326
- clearTimeout(fallback);
5327
- respond({ extend: extra });
5328
- },
5329
- deny: () => {
5330
- clearTimeout(fallback);
5331
- respond("stop");
5332
- }
5333
- });
5334
- }
5335
- });
5456
+ kind: entry.kind,
5457
+ used: entry.used,
5458
+ limit: entry.limit,
5459
+ // One event for THIS kind only — each exceeded kind has its own
5460
+ // negotiation (and its own resolve), so there is no cross-kind
5461
+ // first-wins drop and no O(N^2) re-emission.
5462
+ requestDecision: () => this._busRequestDecision(entry),
5463
+ extend: (extra) => {
5464
+ this.patchLimits(extra);
5465
+ },
5466
+ deny: () => {
5336
5467
  }
5337
5468
  });
5338
5469
  if (result === "throw") return "stop";
5339
5470
  if (result === "continue") return { extend: {} };
5340
5471
  const decision = await result;
5341
5472
  if (decision === "stop") return "stop";
5342
- const ext = decision.extend;
5343
- if (ext.maxIterations !== void 0) {
5344
- this.limits.maxIterations = ext.maxIterations;
5345
- }
5346
- if (ext.maxToolCalls !== void 0) {
5347
- this.limits.maxToolCalls = ext.maxToolCalls;
5348
- }
5349
- if (ext.maxTokens !== void 0) {
5350
- this.limits.maxTokens = ext.maxTokens;
5351
- }
5352
- if (ext.maxCostUsd !== void 0) {
5353
- this.limits.maxCostUsd = ext.maxCostUsd;
5354
- }
5355
- if (ext.timeoutMs !== void 0) {
5356
- this.limits.timeoutMs = ext.timeoutMs;
5357
- }
5358
- if (ext.idleTimeoutMs !== void 0) {
5359
- this.limits.idleTimeoutMs = ext.idleTimeoutMs;
5360
- }
5473
+ this.patchLimits(decision.extend);
5361
5474
  return decision;
5362
5475
  } finally {
5363
- this._pendingNegotiations.delete(kind);
5476
+ this._pendingNegotiations.delete(entry.kind);
5364
5477
  }
5365
5478
  }
5366
5479
  recordIteration() {
@@ -5403,7 +5516,8 @@ var SubagentBudget = class _SubagentBudget {
5403
5516
  const { timeoutMs, idleTimeoutMs } = this.limits;
5404
5517
  if (timeoutMs === void 0 && idleTimeoutMs === void 0) return;
5405
5518
  const elapsed = Date.now() - this.startTime;
5406
- const wallTripped = timeoutMs !== void 0 && elapsed > timeoutMs;
5519
+ const wallSkipped = this._onThreshold !== void 0 && this._watchdogActive !== void 0 && timeoutMs !== void 0 && this._watchdogActive === timeoutMs;
5520
+ const wallTripped = wallSkipped ? false : timeoutMs !== void 0 && elapsed > timeoutMs;
5407
5521
  const idleTripped = idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs;
5408
5522
  if (!wallTripped && !idleTripped) return;
5409
5523
  void this.checkLimits(elapsed);
@@ -6025,6 +6139,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
6025
6139
  terminating = /* @__PURE__ */ new Set();
6026
6140
  constructor(config, options = {}) {
6027
6141
  super();
6142
+ this.setMaxListeners(0);
6028
6143
  this.coordinatorId = config.coordinatorId;
6029
6144
  this.config = config;
6030
6145
  this.runner = options.runner;
@@ -6419,7 +6534,13 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
6419
6534
  let result;
6420
6535
  budget.start();
6421
6536
  try {
6422
- const outcome = await this.executeWithTimeout(this.runner, task, runCtx, budget);
6537
+ const outcome = await this.executeWithTimeout(
6538
+ this.runner,
6539
+ task,
6540
+ runCtx,
6541
+ budget,
6542
+ subagent.config.preemptFraction
6543
+ );
6423
6544
  result = {
6424
6545
  subagentId,
6425
6546
  taskId: task.id,
@@ -6446,7 +6567,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
6446
6567
  }
6447
6568
  this.recordCompletion(result);
6448
6569
  }
6449
- async executeWithTimeout(runner, task, ctx, budget) {
6570
+ async executeWithTimeout(runner, task, ctx, budget, preemptFraction = TIMEOUT_PREEMPT_FRACTION) {
6450
6571
  const initialTimeoutMs = budget.limits.timeoutMs;
6451
6572
  const idleLimitMs = budget.limits.idleTimeoutMs;
6452
6573
  if (initialTimeoutMs === void 0 && idleLimitMs === void 0) {
@@ -6454,8 +6575,21 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
6454
6575
  }
6455
6576
  const start = Date.now();
6456
6577
  let timer = null;
6457
- let preemptedForLimit = null;
6578
+ let PreemptState;
6579
+ ((PreemptState2) => {
6580
+ PreemptState2["ACTIVE"] = "active";
6581
+ PreemptState2["LOCKED"] = "locked";
6582
+ })(PreemptState || (PreemptState = {}));
6583
+ let preemptedCeiling = null;
6584
+ let preemptState = "active" /* ACTIVE */;
6585
+ let lastGrantActivityTs = -1;
6458
6586
  const timeoutPromise = new Promise((_, reject) => {
6587
+ const terminate = (kind, limit, used) => {
6588
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
6589
+ reject(
6590
+ budget._events?.hasListenerFor("budget.threshold_reached") ? new Error(`subagent stopped: budget ${kind} (limit=${limit}, used=${used})`) : new BudgetExceededError(kind, limit, used)
6591
+ );
6592
+ };
6459
6593
  const armFor = (ms) => {
6460
6594
  if (timer) clearTimeout(timer);
6461
6595
  timer = setTimeout(onTick, Math.max(0, ms));
@@ -6464,7 +6598,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
6464
6598
  const wallLimit = budget.limits.timeoutMs ?? initialTimeoutMs;
6465
6599
  const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : wallLimit - (Date.now() - start);
6466
6600
  const idleRemaining = idleLimitMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
6467
- const preemptRemaining = initialTimeoutMs === void 0 || preemptedForLimit === wallLimit ? Number.POSITIVE_INFINITY : wallLimit * TIMEOUT_PREEMPT_FRACTION - (Date.now() - start);
6601
+ const preemptRemaining = initialTimeoutMs === void 0 || preemptedCeiling === wallLimit ? Number.POSITIVE_INFINITY : wallLimit * preemptFraction - (Date.now() - start);
6468
6602
  armFor(Math.max(25, Math.min(wallRemaining, idleRemaining, preemptRemaining)));
6469
6603
  };
6470
6604
  const negotiateTimeout = async (used, limit) => {
@@ -6474,16 +6608,42 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
6474
6608
  kind: "timeout",
6475
6609
  used,
6476
6610
  limit,
6477
- requestDecision: () => new Promise((resolveDecision) => {
6478
- budget._events?.emit("budget.threshold_reached", {
6479
- kind: "timeout",
6480
- used,
6481
- limit,
6482
- timeoutMs: 6e4,
6483
- extend: (extra) => resolveDecision({ extend: extra }),
6484
- deny: () => resolveDecision("stop")
6611
+ requestDecision: () => {
6612
+ if (!budget._events?.hasListenerFor("budget.threshold_reached")) {
6613
+ return Promise.resolve("stop");
6614
+ }
6615
+ return new Promise((resolveDecision) => {
6616
+ let settled = false;
6617
+ const resolve3 = (d) => {
6618
+ if (settled) return;
6619
+ settled = true;
6620
+ resolveDecision(d);
6621
+ };
6622
+ const fallback = setTimeout(() => resolve3("stop"), DECISION_TIMEOUT_MS);
6623
+ budget._events?.emit("budget.threshold_reached", {
6624
+ kind: "timeout",
6625
+ used,
6626
+ limit,
6627
+ // Informational: the budget's own decision deadline. Listeners may use
6628
+ // this to display a countdown. The coordinator does NOT enforce it —
6629
+ // it is the budget's own `setTimeout(fallback)` that races against
6630
+ // the listener's `extend()`/`deny()` call to guarantee progress.
6631
+ timeoutMs: DECISION_TIMEOUT_MS,
6632
+ // deny() wins over a same-dispatch extend(): defer the grant a
6633
+ // microtask so a synchronous deny in the same emit pre-empts it
6634
+ // (a listener that both grants and denies, or two listeners
6635
+ // disagreeing, resolves as a stop). Async grants still resolve.
6636
+ extend: (extra) => {
6637
+ clearTimeout(fallback);
6638
+ queueMicrotask(() => resolve3({ extend: extra }));
6639
+ },
6640
+ deny: () => {
6641
+ clearTimeout(fallback);
6642
+ resolve3("stop");
6643
+ }
6644
+ });
6485
6645
  });
6486
- })
6646
+ }
6487
6647
  });
6488
6648
  return typeof result === "string" ? result : await result;
6489
6649
  };
@@ -6494,21 +6654,45 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
6494
6654
  const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
6495
6655
  const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
6496
6656
  if (idleExceeded && !wallExceeded) {
6657
+ budget._events?.emit("budget.threshold_reached", {
6658
+ kind: "idle_timeout",
6659
+ used: budget.idleMs(),
6660
+ limit: idleLimit ?? 0,
6661
+ timeoutMs: DECISION_TIMEOUT_MS,
6662
+ extend: () => {
6663
+ },
6664
+ deny: () => {
6665
+ }
6666
+ });
6497
6667
  this.subagents.get(ctx.subagentId)?.abortController.abort();
6498
- reject(new BudgetExceededError("timeout", idleLimit ?? 0, budget.idleMs()));
6668
+ reject(new BudgetExceededError("idle_timeout", idleLimit ?? 0, budget.idleMs()));
6499
6669
  return;
6500
6670
  }
6501
- if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold && preemptedForLimit !== wallLimit && elapsed >= wallLimit * TIMEOUT_PREEMPT_FRACTION) {
6671
+ if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold && preemptState === "active" /* ACTIVE */ && elapsed >= wallLimit * preemptFraction) {
6672
+ const activityTs = Date.now() - budget.idleMs();
6673
+ if (activityTs <= lastGrantActivityTs) {
6674
+ preemptState = "locked" /* LOCKED */;
6675
+ preemptedCeiling = wallLimit;
6676
+ scheduleNext();
6677
+ return;
6678
+ }
6679
+ budget.setWatchdogNegotiation(wallLimit);
6502
6680
  try {
6503
6681
  const decision = await negotiateTimeout(elapsed, wallLimit);
6504
6682
  if (typeof decision !== "string" && decision.extend.timeoutMs !== void 0) {
6505
- budget.limits.timeoutMs = decision.extend.timeoutMs;
6506
- preemptedForLimit = null;
6683
+ budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
6684
+ lastGrantActivityTs = Date.now() - budget.idleMs();
6685
+ preemptState = "active" /* ACTIVE */;
6686
+ preemptedCeiling = null;
6507
6687
  } else {
6508
- preemptedForLimit = wallLimit;
6688
+ preemptState = "locked" /* LOCKED */;
6689
+ preemptedCeiling = wallLimit;
6509
6690
  }
6510
6691
  } catch {
6511
- preemptedForLimit = wallLimit;
6692
+ preemptState = "locked" /* LOCKED */;
6693
+ preemptedCeiling = wallLimit;
6694
+ } finally {
6695
+ budget.clearWatchdogNegotiation();
6512
6696
  }
6513
6697
  scheduleNext();
6514
6698
  return;
@@ -6523,26 +6707,41 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
6523
6707
  reject(new BudgetExceededError("timeout", limit, elapsed));
6524
6708
  return;
6525
6709
  }
6710
+ budget.setWatchdogNegotiation(limit);
6526
6711
  try {
6527
6712
  const decision = await negotiateTimeout(elapsed, limit);
6528
- if (decision === "continue" || decision === "throw" || decision === "stop") {
6529
- preemptedForLimit = null;
6713
+ if (decision === "throw") {
6714
+ terminate("timeout", limit, elapsed);
6715
+ return;
6716
+ }
6717
+ if (decision === "continue") {
6718
+ preemptState = "locked" /* LOCKED */;
6719
+ preemptedCeiling = wallLimit;
6530
6720
  armFor(Math.max(1e3, limit));
6531
6721
  return;
6532
6722
  }
6723
+ if (decision === "stop") {
6724
+ terminate("timeout", limit, elapsed);
6725
+ return;
6726
+ }
6533
6727
  if (decision.extend.timeoutMs !== void 0) {
6534
- budget.limits.timeoutMs = decision.extend.timeoutMs;
6535
- preemptedForLimit = null;
6728
+ budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
6729
+ lastGrantActivityTs = Date.now() - budget.idleMs();
6730
+ preemptState = "active" /* ACTIVE */;
6731
+ preemptedCeiling = null;
6536
6732
  scheduleNext();
6537
6733
  return;
6538
6734
  }
6539
- this.subagents.get(ctx.subagentId)?.abortController.abort();
6540
- reject(new BudgetExceededError("timeout", limit, elapsed));
6735
+ terminate("timeout", limit, elapsed);
6736
+ return;
6541
6737
  } catch (err) {
6542
6738
  this.subagents.get(ctx.subagentId)?.abortController.abort();
6543
6739
  reject(
6544
6740
  err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
6545
6741
  );
6742
+ return;
6743
+ } finally {
6744
+ budget.clearWatchdogNegotiation();
6546
6745
  }
6547
6746
  };
6548
6747
  scheduleNext();
@@ -6815,6 +7014,8 @@ var Director = class _Director {
6815
7014
  sessionsRoot;
6816
7015
  /** Director run id for JSONL path resolution. */
6817
7016
  directorRunId;
7017
+ /** Optional logger for structured logging. Falls back to noop when omitted. */
7018
+ logger;
6818
7019
  /** Resolves task descriptions back from `assign()` so completion events
6819
7020
  * can also carry a human-readable title. */
6820
7021
  taskDescriptions = /* @__PURE__ */ new Map();
@@ -6903,6 +7104,7 @@ var Director = class _Director {
6903
7104
  opts.checkpointDebounceMs ?? 250
6904
7105
  ) : null;
6905
7106
  this.fleetManager = opts.fleetManager;
7107
+ this.logger = opts.logger;
6906
7108
  if (this.sharedScratchpadPath) {
6907
7109
  void fsp6.mkdir(this.sharedScratchpadPath, { recursive: true }).catch((err) => this.logShutdownError("shared_scratchpad_mkdir", err));
6908
7110
  }
@@ -7202,7 +7404,10 @@ var Director = class _Director {
7202
7404
  entry.session.cancel(reason);
7203
7405
  for (const [_role, subagentId] of entry.session.getSubagentIds()) {
7204
7406
  this.coordinator.stop(subagentId).catch((err) => {
7205
- console.debug(`[director] stop subagent ${subagentId} failed (may have already completed): ${err}`);
7407
+ this.logger?.debug(`stop subagent ${subagentId} failed (may have already completed)`, {
7408
+ subagentId,
7409
+ err: err instanceof Error ? err.message : String(err)
7410
+ });
7206
7411
  });
7207
7412
  }
7208
7413
  }
@@ -7262,7 +7467,7 @@ var Director = class _Director {
7262
7467
  * Caller-supplied `priceLookup` is optional but recommended — without
7263
7468
  * it the `cost` column in `usage.snapshot()` stays at 0.
7264
7469
  */
7265
- async spawn(config, priceLookup) {
7470
+ async spawn(callerConfig, priceLookup) {
7266
7471
  if (this.workCompleteFlag) {
7267
7472
  throw new FleetSpawnBudgetError(
7268
7473
  "max_spawns",
@@ -7271,6 +7476,7 @@ var Director = class _Director {
7271
7476
  "workComplete() has been called \u2014 director closed further spawning"
7272
7477
  );
7273
7478
  }
7479
+ const config = { ...callerConfig };
7274
7480
  if (!config.model && this.modelMatrix) {
7275
7481
  const matrix = typeof this.modelMatrix === "function" ? this.modelMatrix() : this.modelMatrix;
7276
7482
  const entry = resolveModelMatrix(matrix, config.role);
@@ -8516,11 +8722,34 @@ var DefaultSessionStore = class _DefaultSessionStore {
8516
8722
  dir;
8517
8723
  events;
8518
8724
  secretScrubber;
8725
+ /**
8726
+ * In-memory cache for load() results, keyed by session ID. The cache is
8727
+ * invalidated when the file's mtimeMs or size changes (indicating the
8728
+ * file was written to). This eliminates redundant full-file reads and
8729
+ * JSON parses when the same session is loaded multiple times within the
8730
+ * store's lifetime (e.g., webui session detail views, list() fallbacks).
8731
+ *
8732
+ * Max size is capped to prevent unbounded memory growth in long-running
8733
+ * processes. When the limit is reached, the oldest entry is evicted.
8734
+ */
8735
+ _loadCache = /* @__PURE__ */ new Map();
8736
+ static LOAD_CACHE_MAX_ENTRIES = 50;
8519
8737
  constructor(opts) {
8520
8738
  this.dir = opts.dir;
8521
8739
  this.events = opts.events;
8522
8740
  this.secretScrubber = opts.secretScrubber;
8523
8741
  }
8742
+ /**
8743
+ * Clear the load() cache. Useful for testing or when the caller knows
8744
+ * the file has changed externally (e.g., another process wrote to it).
8745
+ */
8746
+ clearLoadCache(sessionId) {
8747
+ if (sessionId !== void 0) {
8748
+ this._loadCache.delete(sessionId);
8749
+ } else {
8750
+ this._loadCache.clear();
8751
+ }
8752
+ }
8524
8753
  // ── Storage event helpers ───────────────────────────────────────────────────
8525
8754
  emitRead(sessionId, filePath, operation, outcome, durationMs, error) {
8526
8755
  this.events?.emit("storage.read", {
@@ -8663,7 +8892,20 @@ var DefaultSessionStore = class _DefaultSessionStore {
8663
8892
  const t0 = Date.now();
8664
8893
  let outcome = "success";
8665
8894
  let errorMsg;
8895
+ let cacheHit = false;
8666
8896
  try {
8897
+ let stat6;
8898
+ try {
8899
+ const s = await fsp6.stat(file);
8900
+ stat6 = { mtimeMs: s.mtimeMs, size: s.size };
8901
+ } catch (err) {
8902
+ throw err;
8903
+ }
8904
+ const cached = this._loadCache.get(id);
8905
+ if (cached && cached.mtimeMs === stat6.mtimeMs && cached.size === stat6.size) {
8906
+ cacheHit = true;
8907
+ return cached.data;
8908
+ }
8667
8909
  const raw = await fsp6.readFile(file, "utf8");
8668
8910
  const lines = raw.split("\n").filter((l) => l.trim());
8669
8911
  const events = [];
@@ -8679,13 +8921,30 @@ var DefaultSessionStore = class _DefaultSessionStore {
8679
8921
  const meta = this.metaFromEvents(id, events);
8680
8922
  const { messages, usage } = this.replay(events, id);
8681
8923
  const toolCallEnds = extractToolCallEnds(events);
8682
- return { metadata: meta, events, messages, usage, toolCallEnds };
8924
+ const data = { metadata: meta, events, messages, usage, toolCallEnds };
8925
+ if (this._loadCache.size >= _DefaultSessionStore.LOAD_CACHE_MAX_ENTRIES) {
8926
+ const oldest = this._loadCache.keys().next().value;
8927
+ if (oldest !== void 0) {
8928
+ this._loadCache.delete(oldest);
8929
+ }
8930
+ }
8931
+ this._loadCache.set(id, { mtimeMs: stat6.mtimeMs, size: stat6.size, data });
8932
+ return data;
8683
8933
  } catch (err) {
8684
8934
  outcome = "failure";
8685
8935
  errorMsg = toErrorMessage(err);
8686
8936
  throw err;
8687
8937
  } finally {
8688
8938
  this.emitRead(id, file, "load", outcome, Date.now() - t0, errorMsg);
8939
+ if (cacheHit) {
8940
+ this.events?.emit("storage.cache_hit", {
8941
+ sessionId: id,
8942
+ store: "session",
8943
+ filePath: file,
8944
+ operation: "load",
8945
+ durationMs: Date.now() - t0
8946
+ });
8947
+ }
8689
8948
  }
8690
8949
  }
8691
8950
  async list(limit = 20) {
@@ -9692,6 +9951,7 @@ function attachAutoExtend(events, policy = {}) {
9692
9951
  const extendCounts = /* @__PURE__ */ new Map();
9693
9952
  let progress = 0;
9694
9953
  let lastTimeoutProgress = -1;
9954
+ let lastSeenKey = null;
9695
9955
  const unsubs = [
9696
9956
  events.on("tool.executed", () => {
9697
9957
  progress++;
@@ -9701,6 +9961,9 @@ function attachAutoExtend(events, policy = {}) {
9701
9961
  }),
9702
9962
  events.on("budget.threshold_reached", (e) => {
9703
9963
  const { kind, limit, extend, deny } = e;
9964
+ const key = `${kind}:${limit}`;
9965
+ if (key === lastSeenKey) return;
9966
+ lastSeenKey = key;
9704
9967
  if (kind === "timeout" || kind === "idle_timeout") {
9705
9968
  if (progress > lastTimeoutProgress) {
9706
9969
  lastTimeoutProgress = progress;
@@ -10410,7 +10673,8 @@ var BrainMonitor = class {
10410
10673
  id: "steer",
10411
10674
  label: "Steer the agent with corrective guidance",
10412
10675
  consequence: "A steer message is injected before its next step.",
10413
- risk: "low"
10676
+ risk: "low",
10677
+ recommended: true
10414
10678
  },
10415
10679
  {
10416
10680
  id: "continue",
@@ -10419,9 +10683,9 @@ var BrainMonitor = class {
10419
10683
  }
10420
10684
  ],
10421
10685
  risk: "medium",
10422
- // Without an LLM layer the policy brain resolves this fallback to
10423
- // "continue" the monitor observes but never interferes.
10424
- fallback: "continue"
10686
+ // 'ask_human' routes to the LLM-backed autonomous layer via
10687
+ // createTieredBrainArbiter before any human escalation.
10688
+ fallback: "ask_human"
10425
10689
  };
10426
10690
  const decision = await this.opts.brain.decide(request);
10427
10691
  const intervened = await this.maybeIntervene(kind, request, decision);
@@ -11518,6 +11782,10 @@ function makeDependencyWatcherConfig(opts) {
11518
11782
  return {
11519
11783
  watchPaths: unique,
11520
11784
  debounceMs,
11785
+ dispose() {
11786
+ for (const t of pending.values()) clearTimeout(t);
11787
+ pending.clear();
11788
+ },
11521
11789
  async onChange(entry) {
11522
11790
  if (entry.event === "delete") return;
11523
11791
  if (!matchesPattern(entry.path)) return;
@@ -11935,6 +12203,10 @@ var KnowledgeGraph = class {
11935
12203
  pendingDeliveries = /* @__PURE__ */ new Map();
11936
12204
  filePath;
11937
12205
  graphFilePath;
12206
+ /** Exposed for unit-testing only: read current index contents. */
12207
+ getIndex() {
12208
+ return this.index;
12209
+ }
11938
12210
  constructor(sessionDir) {
11939
12211
  this.filePath = path5.join(sessionDir, "_knowledge_graph");
11940
12212
  this.graphFilePath = path5.join(this.filePath, "graph.jsonl");
@@ -11947,7 +12219,7 @@ var KnowledgeGraph = class {
11947
12219
  async add(node) {
11948
12220
  const full = { id: randomUUID(), ...node };
11949
12221
  this.nodes.set(full.id, full);
11950
- this._index(full);
12222
+ this._addToIndex(full, this._indexKeys(full));
11951
12223
  await this._persist(full);
11952
12224
  this._deliver(full);
11953
12225
  return full;
@@ -11956,8 +12228,10 @@ var KnowledgeGraph = class {
11956
12228
  async update(id, patch) {
11957
12229
  const existing = this.nodes.get(id);
11958
12230
  if (!existing) return null;
12231
+ this._removeFromIndex(existing, this._indexKeys(existing));
11959
12232
  const updated = { ...existing, ...patch };
11960
12233
  this.nodes.set(id, updated);
12234
+ this._addToIndex(updated, this._indexKeys(updated));
11961
12235
  this._deliver(updated);
11962
12236
  await this._append(updated);
11963
12237
  return updated;
@@ -12043,15 +12317,10 @@ var KnowledgeGraph = class {
12043
12317
  return { passed: checks.every((c) => c.passed), checks };
12044
12318
  }
12045
12319
  // ── Private ────────────────────────────────────────────────────────────
12046
- _index(node) {
12047
- const add = (key) => {
12048
- let set = this.index.get(key);
12049
- if (!set) {
12050
- set = /* @__PURE__ */ new Set();
12051
- this.index.set(key, set);
12052
- }
12053
- set.add(node.id);
12054
- };
12320
+ /** Pure: compute the set of index keys a node would belong to. */
12321
+ _indexKeys(node) {
12322
+ const keys = /* @__PURE__ */ new Set();
12323
+ const add = (key) => keys.add(key);
12055
12324
  add(`type:${node.type}`);
12056
12325
  if (node.type === "fact") {
12057
12326
  const f = node;
@@ -12060,6 +12329,8 @@ var KnowledgeGraph = class {
12060
12329
  add(`by:${f.discoveredBy}`);
12061
12330
  for (const tag of f.tags) add(`tag:${tag}`);
12062
12331
  add(`key:${f.key}`);
12332
+ add(`subject:${f.subject}`);
12333
+ if (f.detail) add(`detail:${f.detail}`);
12063
12334
  }
12064
12335
  if (node.type === "goal") {
12065
12336
  const g = node;
@@ -12074,6 +12345,24 @@ var KnowledgeGraph = class {
12074
12345
  add(`by:${c.proposedBy}`);
12075
12346
  for (const g of c.satisfiesGoals) add(`goal:${g}`);
12076
12347
  }
12348
+ return keys;
12349
+ }
12350
+ /** Mutate the index: add a node's id to every set for the given keys. */
12351
+ _addToIndex(node, keys) {
12352
+ for (const key of keys) {
12353
+ let set = this.index.get(key);
12354
+ if (!set) {
12355
+ set = /* @__PURE__ */ new Set();
12356
+ this.index.set(key, set);
12357
+ }
12358
+ set.add(node.id);
12359
+ }
12360
+ }
12361
+ /** Remove a node's id from all index sets for the given keys. */
12362
+ _removeFromIndex(node, keys) {
12363
+ for (const key of keys) {
12364
+ this.index.get(key)?.delete(node.id);
12365
+ }
12077
12366
  }
12078
12367
  _matches(node, f) {
12079
12368
  if (f.type && node.type !== f.type) return false;
@@ -12123,11 +12412,15 @@ var KnowledgeGraph = class {
12123
12412
  try {
12124
12413
  const parsed = JSON.parse(line);
12125
12414
  if (parsed.op === "update") {
12415
+ const oldNode = this.nodes.get(parsed.node.id);
12416
+ if (oldNode) {
12417
+ this._removeFromIndex(oldNode, this._indexKeys(oldNode));
12418
+ }
12126
12419
  this.nodes.set(parsed.node.id, parsed.node);
12127
- this._index(parsed.node);
12420
+ this._addToIndex(parsed.node, this._indexKeys(parsed.node));
12128
12421
  } else {
12129
12422
  this.nodes.set(parsed.id, parsed);
12130
- this._index(parsed);
12423
+ this._addToIndex(parsed, this._indexKeys(parsed));
12131
12424
  }
12132
12425
  } catch {
12133
12426
  }
@@ -12428,7 +12721,7 @@ var TaskDAG = class {
12428
12721
  if (visited.has(current)) continue;
12429
12722
  visited.add(current);
12430
12723
  const node = this.nodes.get(current);
12431
- if (node) stack.push(...node.dependents);
12724
+ if (node) stack.push(...node.deps);
12432
12725
  }
12433
12726
  return false;
12434
12727
  }
@@ -12531,8 +12824,10 @@ var ConsensusProtocol = class {
12531
12824
  return this._resolve(changeId, change.votes, eligible);
12532
12825
  }
12533
12826
  // ── Private ───────────────────────────────────────────────────────────
12534
- _eligibleVoters(_change) {
12535
- return Array.from(this.voters.keys());
12827
+ _eligibleVoters(change) {
12828
+ return Array.from(this.voters.keys()).filter(
12829
+ (agentId) => agentId !== change.proposedBy
12830
+ );
12536
12831
  }
12537
12832
  _resolve(changeId, votes, eligible) {
12538
12833
  const totalEligible = eligible.length;
@@ -12577,7 +12872,7 @@ var ConsensusProtocol = class {
12577
12872
  if (!quorumMet) {
12578
12873
  return {
12579
12874
  changeId,
12580
- outcome: "pending",
12875
+ outcome: "quorum_not_met",
12581
12876
  votes,
12582
12877
  approveCount: approve.length,
12583
12878
  rejectCount: reject.length,
@@ -12715,8 +13010,7 @@ var ChangeManager = class {
12715
13010
  // filled after quality gate
12716
13011
  satisfiesGoals: input.satisfiesGoals
12717
13012
  });
12718
- void this._runQualityGate(node.id, input.files).then((gate) => {
12719
- void this.graph.update(node.id, { qualityGate: gate });
13013
+ void this._runQualityGate(node.id, input.files).then((gate) => this.graph.update(node.id, { qualityGate: gate })).catch(() => {
12720
13014
  });
12721
13015
  this._emit("change:proposed", { changeId: node.id, title: node.title });
12722
13016
  return node;
@@ -12954,7 +13248,7 @@ var AutonomousBrain = class {
12954
13248
  }
12955
13249
  return { type: "deny", reason: `Brain LLM failed: ${String(err)}` };
12956
13250
  }
12957
- void this._recordDecision({
13251
+ this._recordDecision({
12958
13252
  id,
12959
13253
  decisionType,
12960
13254
  question,
@@ -12963,6 +13257,7 @@ var AutonomousBrain = class {
12963
13257
  rationale: result.rationale,
12964
13258
  madeBy: "autonomous-brain",
12965
13259
  context: JSON.stringify(context)
13260
+ }).catch(() => {
12966
13261
  });
12967
13262
  if (requiresConsensus) {
12968
13263
  this._emit("brain.decision", { id, decisionType, optionId: result.optionId, rationale: result.rationale, consensusRequired: true });
@@ -13241,10 +13536,16 @@ var TaskAuctioneer = class {
13241
13536
  maxTasksPerAgent;
13242
13537
  minConfidence;
13243
13538
  // minimum dispatcher confidence to accept a bid
13539
+ maxBidRetries;
13540
+ // max republished attempts before marking task failed
13244
13541
  /** Pending bids keyed by taskId. */
13245
13542
  pendingBids = /* @__PURE__ */ new Map();
13246
13543
  /** Active bid windows keyed by taskId. */
13247
13544
  bidTimers = /* @__PURE__ */ new Map();
13545
+ /** FleetBus subscription disposers, detached in dispose(). */
13546
+ unsubs = [];
13547
+ /** How many times a task has been republished with no bids received. */
13548
+ bidRetryCounts = /* @__PURE__ */ new Map();
13248
13549
  /** Agent → current task count (from graph + in-flight). */
13249
13550
  agentTaskCounts = /* @__PURE__ */ new Map();
13250
13551
  constructor(opts) {
@@ -13255,8 +13556,26 @@ var TaskAuctioneer = class {
13255
13556
  this.bidWindowMs = opts.bidWindowMs ?? 3e4;
13256
13557
  this.maxTasksPerAgent = opts.maxTasksPerAgent ?? 3;
13257
13558
  this.minConfidence = opts.minConfidence ?? 0.3;
13258
- this.fleet?.filter("task:bid", (e) => this._onBidEvent(e));
13259
- this.fleet?.filter("task:claimed", (e) => this._onClaimedEvent(e));
13559
+ this.maxBidRetries = opts.maxBidRetries ?? 3;
13560
+ const offBid = this.fleet?.filter("task:bid", (e) => this._onBidEvent(e));
13561
+ const offClaimed = this.fleet?.filter("task:claimed", (e) => this._onClaimedEvent(e));
13562
+ if (offBid) this.unsubs.push(offBid);
13563
+ if (offClaimed) this.unsubs.push(offClaimed);
13564
+ }
13565
+ /**
13566
+ * Detach all FleetBus subscriptions and cancel any open bid-window timers.
13567
+ * Call when the owning coordinator stops/restarts so handlers and timers
13568
+ * don't accumulate across cycles.
13569
+ */
13570
+ dispose() {
13571
+ for (const off of this.unsubs.splice(0)) {
13572
+ try {
13573
+ off();
13574
+ } catch {
13575
+ }
13576
+ }
13577
+ for (const t of this.bidTimers.values()) clearTimeout(t);
13578
+ this.bidTimers.clear();
13260
13579
  }
13261
13580
  // ── Publish a task ────────────────────────────────────────────────────
13262
13581
  /**
@@ -13266,14 +13585,16 @@ var TaskAuctioneer = class {
13266
13585
  * If `targetAgent` is specified, the task is assigned directly without auction.
13267
13586
  */
13268
13587
  async publishTask(input) {
13588
+ const blockedBy = input.blockedBy ?? [];
13589
+ const hasOpenBlockers = blockedBy.length > 0 && blockedBy.some((id) => this.graph.get(id)?.status !== "done");
13269
13590
  const goal = await this.graph.add({
13270
13591
  type: "goal",
13271
13592
  title: input.title,
13272
13593
  description: input.description,
13273
- status: input.targetAgent ? "in_progress" : "pending",
13594
+ status: input.targetAgent ? "in_progress" : hasOpenBlockers ? "blocked" : "pending",
13274
13595
  priority: input.priority ?? "medium",
13275
13596
  assignee: input.targetAgent,
13276
- blockedBy: [],
13597
+ blockedBy,
13277
13598
  dependsOn: input.satisfiesGoals ?? [],
13278
13599
  createdBy: this.selfAgentId,
13279
13600
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -13290,6 +13611,12 @@ var TaskAuctioneer = class {
13290
13611
  });
13291
13612
  }
13292
13613
  }
13614
+ for (const blockerId of blockedBy) {
13615
+ const blocker = this.graph.get(blockerId);
13616
+ if (blocker && !blocker.children.includes(goal.id)) {
13617
+ await this.graph.update(blockerId, { children: [...blocker.children, goal.id] });
13618
+ }
13619
+ }
13293
13620
  if (input.targetAgent) {
13294
13621
  await this._assignDirect(goal.id, input.targetAgent);
13295
13622
  } else {
@@ -13401,9 +13728,12 @@ Priority: ${goal.priority}`,
13401
13728
  status: "done",
13402
13729
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13403
13730
  });
13731
+ this.bidRetryCounts.delete(taskId);
13732
+ this.pendingBids.delete(taskId);
13733
+ this._cancelBidWindow(taskId);
13404
13734
  for (const childId of goal.children) {
13405
13735
  const child = this.graph.get(childId);
13406
- if (child) {
13736
+ if (child && child.status === "blocked") {
13407
13737
  const allUnblocked = child.blockedBy.every((blockedId) => {
13408
13738
  const blocked = this.graph.get(blockedId);
13409
13739
  return blocked?.status === "done";
@@ -13412,6 +13742,7 @@ Priority: ${goal.priority}`,
13412
13742
  await this.graph.update(childId, { status: "pending", updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
13413
13743
  const unblockedGoal = this.graph.get(childId);
13414
13744
  await this._broadcastTask(unblockedGoal);
13745
+ this._startBidWindow(childId);
13415
13746
  }
13416
13747
  }
13417
13748
  }
@@ -13437,6 +13768,9 @@ ${_result ?? "No result provided."}`
13437
13768
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
13438
13769
  result: error
13439
13770
  });
13771
+ this.bidRetryCounts.delete(taskId);
13772
+ this.pendingBids.delete(taskId);
13773
+ this._cancelBidWindow(taskId);
13440
13774
  this._emit("task:failed", { taskId, agentId, error });
13441
13775
  await this._mailboxPublish({
13442
13776
  type: "note",
@@ -13515,7 +13849,7 @@ Assignee: ${agentId}`
13515
13849
  priority: goal.priority,
13516
13850
  tags: goal.tags
13517
13851
  });
13518
- void this._mailboxPublish({
13852
+ this._mailboxPublish({
13519
13853
  type: "broadcast",
13520
13854
  subject: `[task] ${goal.title} (${goal.priority})`,
13521
13855
  body: `New task available: "${goal.title}"
@@ -13526,6 +13860,7 @@ Task ID: ${goal.id}
13526
13860
  Tags: ${goal.tags.join(", ") || "none"}
13527
13861
 
13528
13862
  Bid by calling taskAuctioneer.bid("${goal.id}", ...)`
13863
+ }).catch(() => {
13529
13864
  });
13530
13865
  }
13531
13866
  async _mailboxPublish(msg) {
@@ -13559,9 +13894,10 @@ Bid by calling taskAuctioneer.bid("${goal.id}", ...)`
13559
13894
  }
13560
13895
  _startBidWindow(taskId) {
13561
13896
  this._cancelBidWindow(taskId);
13562
- const timer = setTimeout(async () => {
13897
+ const timer = setTimeout(() => {
13563
13898
  this.bidTimers.delete(taskId);
13564
- await this._evaluateBids(taskId);
13899
+ void this._evaluateBids(taskId).catch(() => {
13900
+ });
13565
13901
  }, this.bidWindowMs);
13566
13902
  this.bidTimers.set(taskId, timer);
13567
13903
  }
@@ -13575,6 +13911,13 @@ Bid by calling taskAuctioneer.bid("${goal.id}", ...)`
13575
13911
  async _evaluateBids(taskId) {
13576
13912
  const bids = this.pendingBids.get(taskId);
13577
13913
  if (!bids || bids.length === 0) {
13914
+ const retryCount = (this.bidRetryCounts.get(taskId) ?? 0) + 1;
13915
+ this.bidRetryCounts.set(taskId, retryCount);
13916
+ if (retryCount >= this.maxBidRetries) {
13917
+ await this.fail(taskId, `No bids received after ${this.maxBidRetries} attempts`);
13918
+ this.bidRetryCounts.delete(taskId);
13919
+ return;
13920
+ }
13578
13921
  const goal = this.graph.get(taskId);
13579
13922
  if (goal) {
13580
13923
  await this._broadcastTask(goal);
@@ -13583,7 +13926,15 @@ Bid by calling taskAuctioneer.bid("${goal.id}", ...)`
13583
13926
  return;
13584
13927
  }
13585
13928
  bids.sort((a, b) => b.score - a.score);
13586
- const winner = bids[0];
13929
+ const winner = bids.find((b) => this._getAgentTaskCount(b.agentId) < this.maxTasksPerAgent);
13930
+ if (!winner) {
13931
+ const goal = this.graph.get(taskId);
13932
+ if (goal) {
13933
+ await this._broadcastTask(goal);
13934
+ this._startBidWindow(taskId);
13935
+ }
13936
+ return;
13937
+ }
13587
13938
  await this.claim(taskId, winner.agentId, winner.agentName);
13588
13939
  }
13589
13940
  async _assignDirect(taskId, agentId) {
@@ -13630,16 +13981,24 @@ var AutonomousCoordinator = class {
13630
13981
  selfAgentId;
13631
13982
  fleet;
13632
13983
  fleetManager;
13984
+ director;
13633
13985
  mailbox;
13634
13986
  events;
13987
+ onCoordinatorEvent;
13635
13988
  running = false;
13636
13989
  iterationCount = 0;
13990
+ /** Tasks already handled by _onSubagentTerminated (to avoid double goal:failed on fleet event). */
13991
+ _handledBySubagent = /* @__PURE__ */ new Set();
13992
+ /** FleetBus subscription disposers, detached in dispose(). */
13993
+ unsubs = [];
13637
13994
  constructor(opts) {
13638
13995
  this.selfAgentId = opts.selfAgentId;
13639
13996
  this.fleet = opts.fleet ?? void 0;
13640
13997
  this.fleetManager = opts.fleetManager ?? void 0;
13998
+ this.director = opts.director ?? void 0;
13641
13999
  this.mailbox = opts.mailbox ?? void 0;
13642
14000
  this.events = opts.events ?? void 0;
14001
+ this.onCoordinatorEvent = opts.onCoordinatorEvent;
13643
14002
  this.graph = new KnowledgeGraph(opts.sessionDir);
13644
14003
  this.dag = new TaskDAG();
13645
14004
  this.auction = new TaskAuctioneer({
@@ -13675,9 +14034,19 @@ var AutonomousCoordinator = class {
13675
14034
  this.dag.onEvent((event) => {
13676
14035
  this._onDagEvent(event);
13677
14036
  });
13678
- this.fleet?.filter("subagent.terminated", (e) => {
14037
+ const offCompleted = this.fleet?.filter("subagent.completed", (e) => {
13679
14038
  this._onSubagentTerminated(e);
13680
14039
  });
14040
+ if (offCompleted) this.unsubs.push(offCompleted);
14041
+ const offFailed = this.fleet?.filter("task:failed", (e) => {
14042
+ const payload = e.payload;
14043
+ const taskId = payload?.taskId;
14044
+ if (!taskId || this._handledBySubagent.has(taskId)) return;
14045
+ this._handledBySubagent.add(taskId);
14046
+ this._emit({ type: "goal:failed", goalId: taskId, text: payload?.error ?? "Task failed" });
14047
+ });
14048
+ if (offFailed) this.unsubs.push(offFailed);
14049
+ this._emit({ type: "coordinator:mode", mode: this.fleet ? "fleet" : "standalone" });
13681
14050
  }
13682
14051
  // ── Public API ───────────────────────────────────────────────────────
13683
14052
  /**
@@ -13691,11 +14060,13 @@ var AutonomousCoordinator = class {
13691
14060
  const maxIterations = opts.maxIterations ?? 100;
13692
14061
  const goal = opts.goal ?? "Improve the codebase";
13693
14062
  const maxCost = opts.maxCostUsd;
13694
- await this.graph.load();
13695
14063
  try {
14064
+ await this.graph.load();
13696
14065
  const goalConfigs = await this._decomposeGoal(goal);
13697
14066
  for (const g of goalConfigs) {
13698
- await this.auction.publishTask(g);
14067
+ const goalId = await this.auction.publishTask(g);
14068
+ this.dag.addNode(goalId, g.description, []);
14069
+ this._emit({ type: "goal:added", goalId, title: g.title, text: g.description });
13699
14070
  }
13700
14071
  while (this.running) {
13701
14072
  this.iterationCount++;
@@ -13722,6 +14093,7 @@ var AutonomousCoordinator = class {
13722
14093
  const blocked = this.dag.getBlocked();
13723
14094
  if (blocked.length > 0 && this.dag.hasDeadlock()) {
13724
14095
  (this.events?.emit)("autonomous:deadlock", { blocked });
14096
+ this._emit({ type: "deadlock:detected", goalId: blocked[0]?.id ?? "", text: `Deadlock detected: ${blocked.map((n) => n.id).join(", ")}` });
13725
14097
  this.running = false;
13726
14098
  }
13727
14099
  break;
@@ -13738,7 +14110,15 @@ var AutonomousCoordinator = class {
13738
14110
  }
13739
14111
  const pendingChanges = this.changes.getPendingReviews();
13740
14112
  for (const change of pendingChanges) {
13741
- await this._handlePendingChange(change);
14113
+ try {
14114
+ await this._handlePendingChange(change);
14115
+ } catch (err) {
14116
+ this._emit({
14117
+ type: "goal:failed",
14118
+ goalId: change.id,
14119
+ text: `Consensus handling failed: ${err instanceof Error ? err.message : String(err)}`
14120
+ });
14121
+ }
13742
14122
  }
13743
14123
  }
13744
14124
  } finally {
@@ -13748,7 +14128,26 @@ var AutonomousCoordinator = class {
13748
14128
  }
13749
14129
  /** Stop the autonomous loop. */
13750
14130
  stop() {
14131
+ if (!this.running) return;
13751
14132
  this.running = false;
14133
+ console.error(`[AutonomousCoordinator] stop signal received \u2014 shutting down (iteration ${this.iterationCount})`);
14134
+ }
14135
+ /**
14136
+ * Tear down the coordinator for good: stop the loop and detach all FleetBus
14137
+ * subscriptions (this coordinator's + the auctioneer's) plus any open bid
14138
+ * timers. Call this when discarding the instance (e.g. `/coordinator stop`
14139
+ * that recreates a fresh coordinator on the next start) so handlers and
14140
+ * timers don't accumulate across cycles. `stop()` only pauses the loop.
14141
+ */
14142
+ dispose() {
14143
+ this.stop();
14144
+ for (const off of this.unsubs.splice(0)) {
14145
+ try {
14146
+ off();
14147
+ } catch {
14148
+ }
14149
+ }
14150
+ this.auction.dispose();
13752
14151
  }
13753
14152
  /** Get a stats snapshot. */
13754
14153
  getStats() {
@@ -13803,6 +14202,7 @@ var AutonomousCoordinator = class {
13803
14202
  body: `**${input.category}**${input.file ? ` in ${input.file}${input.line ? `:${input.line}` : ""}` : ""}
13804
14203
  ${input.detail}`
13805
14204
  });
14205
+ this._emit({ type: "knowledge:added", knowledgeId: fact.id, title: input.subject, text: input.detail });
13806
14206
  return fact;
13807
14207
  }
13808
14208
  // ── Goal creation helpers ────────────────────────────────────────────
@@ -13815,7 +14215,10 @@ ${input.detail}`
13815
14215
  title: input.title,
13816
14216
  description: input.description,
13817
14217
  priority: resolvedPriority,
13818
- ...input.tags ? { tags: input.tags } : {}
14218
+ ...input.tags ? { tags: input.tags } : {},
14219
+ // Mirror the dependency edges into the auction so blocked goals aren't
14220
+ // biddable until their deps complete (the DAG tracks the same edges).
14221
+ ...input.deps && input.deps.length > 0 ? { blockedBy: input.deps } : {}
13819
14222
  });
13820
14223
  const goal = this.graph.get(goalId);
13821
14224
  for (const depId of input.deps ?? []) {
@@ -13856,30 +14259,34 @@ ${input.detail}`
13856
14259
  async _processGoal(goalId) {
13857
14260
  const ready = this.dag.getReady();
13858
14261
  if (ready.length === 0) return;
13859
- const next = ready.find((n) => n.id === goalId) ?? ready[0];
13860
- this.dag.start(next.id, "auctioneer");
13861
- const existingAgent = this.auction.getTasksForAgent(this.selfAgentId).find((g) => g.id === next.id);
13862
- if (!existingAgent) {
13863
- await this.auction.publishTask({
13864
- title: next.description,
13865
- description: next.description,
13866
- priority: this._dagPriorityToGoal(next.priority),
13867
- tags: next.tags
14262
+ const dagNode = ready.find((n) => n.id === goalId) ?? ready[0];
14263
+ this.dag.start(dagNode.id, "auctioneer");
14264
+ const goalNode = this.graph.get(goalId);
14265
+ if (!goalNode) return;
14266
+ const title = goalNode.title || dagNode.description;
14267
+ const taskId = await this.auction.publishTask({
14268
+ title,
14269
+ description: goalNode.description,
14270
+ priority: this._dagPriorityToGoal(dagNode.priority),
14271
+ tags: dagNode.tags
14272
+ });
14273
+ this._emit({ type: "task:ready", goalId, taskId, title });
14274
+ if (this.director) {
14275
+ const config = {
14276
+ name: `worker-${goalId.slice(0, 8)}`,
14277
+ role: "general",
14278
+ maxIterations: 100,
14279
+ timeoutMs: 6e5
14280
+ // 10 minutes per goal
14281
+ };
14282
+ const subagentId = await this.director.spawn(config);
14283
+ await this.auction.claim(taskId, subagentId, config.name);
14284
+ await this.director.assign({
14285
+ id: goalId,
14286
+ subagentId,
14287
+ description: goalNode.description
13868
14288
  });
13869
14289
  }
13870
- await this._waitForClaim(next.id);
13871
- }
13872
- async _waitForClaim(taskId) {
13873
- const maxWait = 6e4;
13874
- const pollInterval = 2e3;
13875
- const start = Date.now();
13876
- while (Date.now() - start < maxWait) {
13877
- const goal = this.graph.get(taskId);
13878
- if (goal?.status === "in_progress" || goal?.status === "done") {
13879
- return;
13880
- }
13881
- await this._sleep(pollInterval);
13882
- }
13883
14290
  }
13884
14291
  async _handlePendingChange(change) {
13885
14292
  const result = this.consensus.getStatus(change.id);
@@ -13893,6 +14300,17 @@ ${input.detail}`
13893
14300
  );
13894
14301
  if (voteResult.outcome === "approved") {
13895
14302
  await this.changes.markApplied(change.id, (/* @__PURE__ */ new Date()).toISOString());
14303
+ this._emit({ type: "consensus:reached", goalId: change.id, text: "Change approved and applied" });
14304
+ }
14305
+ } else {
14306
+ const voteResult = await this.consensus.castVote(
14307
+ change.id,
14308
+ this.selfAgentId,
14309
+ "reject",
14310
+ `Quality gate failed: ${change.qualityGate.checks.map((c) => `${c.name}=${c.passed}`).join(", ")}`
14311
+ );
14312
+ if (voteResult.outcome === "rejected" || voteResult.outcome === "vetoed") {
14313
+ this._emit({ type: "consensus:reached", goalId: change.id, text: "Change rejected by quality gate" });
13896
14314
  }
13897
14315
  }
13898
14316
  }
@@ -13903,10 +14321,6 @@ ${input.detail}`
13903
14321
  (this.events?.emit)("autonomous:task_ready", { taskId: event.nodeId, description: node.description });
13904
14322
  }
13905
14323
  }
13906
- if (event.type === "deadlock") {
13907
- (this.events?.emit)("autonomous:deadlock", { blocked: event.blocked });
13908
- this.running = false;
13909
- }
13910
14324
  if (event.type === "graph:done") {
13911
14325
  (this.events?.emit)("autonomous:all_done", this.getStats());
13912
14326
  }
@@ -13914,13 +14328,16 @@ ${input.detail}`
13914
14328
  _onSubagentTerminated(e) {
13915
14329
  const payload = e.payload;
13916
14330
  const subagentId = payload?.subagentId ?? e.subagentId;
13917
- const stopReason = payload?.stopReason ?? "unknown";
14331
+ const stopReason = payload?.stopReason ?? (payload?.status === "ok" ? "end_turn" : payload?.status ?? "unknown");
13918
14332
  const tasks = this.auction.getTasksForAgent(subagentId);
13919
14333
  for (const task of tasks) {
14334
+ this._handledBySubagent.add(task.id);
13920
14335
  if (stopReason === "end_turn") {
13921
14336
  void this.auction.complete(task.id, "Subagent completed successfully");
14337
+ this._emit({ type: "task:completed", goalId: task.id, taskId: task.id, text: "Subagent completed successfully" });
13922
14338
  } else {
13923
14339
  void this.auction.fail(task.id, `Subagent terminated: ${stopReason}`);
14340
+ this._emit({ type: "goal:failed", goalId: task.id, text: `Subagent terminated: ${stopReason}` });
13924
14341
  }
13925
14342
  }
13926
14343
  }
@@ -13934,6 +14351,10 @@ ${input.detail}`
13934
14351
  }
13935
14352
  _buildVoters() {
13936
14353
  return [
14354
+ // The coordinator itself casts the quality-gate auto-vote in
14355
+ // _handlePendingChange — it MUST be a registered, eligible voter or
14356
+ // castVote throws "unknown voter" and tears down the run() loop.
14357
+ { agentId: this.selfAgentId, agentName: "Coordinator", role: "coordinator", weight: 1 },
13937
14358
  { agentId: "critic", agentName: "Critic", role: "critic", weight: 2, veto: true },
13938
14359
  { agentId: "bug-hunter", agentName: "Bug Hunter", role: "bug-hunter", weight: 1.5 },
13939
14360
  { agentId: "security-scanner", agentName: "Security Scanner", role: "security-scanner", weight: 1.5 },
@@ -13971,11 +14392,12 @@ ${input.detail}`
13971
14392
  } catch {
13972
14393
  }
13973
14394
  }
13974
- _sleep(ms) {
13975
- return new Promise((r) => setTimeout(r, ms));
14395
+ /** Emit a CoordinatorEvent to the subscriber (e.g. TUI panel timeline). */
14396
+ _emit(event) {
14397
+ this.onCoordinatorEvent?.(event);
13976
14398
  }
13977
14399
  };
13978
14400
 
13979
- export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, TOOLS as AGENT_TOOL_PRESETS, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, AutonomousBrain, AutonomousCoordinator, BUG_HUNTER_AGENT, BUILD_AGENTS, BrainDecisionQueue, BrainMonitor, BudgetExceededError, BudgetThresholdSignal, ChangeManager, CollabSession, ConsensusProtocol, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_QUALITY_CHECKS, DEFAULT_SUBAGENT_BASELINE, DELIVERY_AGENTS, DEPENDENCY_FILE_PATTERNS, DISCOVERY_AGENTS, DOMAIN_AGENTS, DefaultBrainArbiter, DefaultMailbox, DefaultMultiAgentCoordinator, Director, DirectorAlertLevel, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FleetBus, FleetCostCapError, FleetManager, FleetSpawnBudgetError, FleetUsageAggregator, GlobalMailbox, HEAVY_BUDGET, HumanEscalatingBrainArbiter, InMemoryAgentBridge, InMemoryBridgeTransport, KNOWLEDGE_AGENTS, KnowledgeGraph, LIGHT_BUDGET, LargeAnswerStore, MEDIUM_BUDGET, META_AGENTS, NULL_FLEET_BUS, ObservableBrainArbiter, PLANNING_AGENTS, REFACTOR_PLANNER_AGENT, REVIEW_AGENTS, SECURITY_SCANNER_AGENT, SubagentBudget, TaskAuctioneer, TaskDAG, VERIFY_AGENTS, applyRosterBudget, attachAutoExtend, attachDepWatcherBridge, composeDirectorPrompt, composeSubagentPrompt, createDelegateTool, createMailboxHooks, createMessage, detectEcosystem, dispatchAgent, formatHumanPrompt, getAgentDefinition, getFullPackageLog, getManifestPackages, getPackageAuthor, getPackagesByAgent, mailboxSessionTag, makeAgentSubagentRunner, makeAskResultTool, makeAskTool, makeAssignTool, makeAwaitTasksTool, makeCollabDebugTool, makeDependencyWatcherConfig, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeMailInboxTool, makeMailSendTool, makeMailboxTool, makeRollUpTool, makeSpawnTool, makeTerminateTool, makeWorkCompleteTool, normalizeRecipient, recordPackageAction, resolveMailboxIdentity, resolveProjectDir, rosterSummaryFromConfigs, scoreAgents, startPackageOutdatedWatcher, updatePackageOutdatedStatus, withDisabledToolFiltering };
14401
+ export { ACP_AGENTS, AGENTS_BY_PHASE, AGENT_CATALOG, TOOLS as AGENT_TOOL_PRESETS, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, AutonomousBrain, AutonomousCoordinator, BUG_HUNTER_AGENT, BUILD_AGENTS, BrainDecisionQueue, BrainMonitor, BudgetExceededError, BudgetThresholdSignal, ChangeManager, CollabSession, ConsensusProtocol, DECISION_TIMEOUT_MS, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_QUALITY_CHECKS, DEFAULT_SUBAGENT_BASELINE, DELIVERY_AGENTS, DEPENDENCY_FILE_PATTERNS, DISCOVERY_AGENTS, DOMAIN_AGENTS, DefaultBrainArbiter, DefaultMailbox, DefaultMultiAgentCoordinator, Director, DirectorAlertLevel, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FLEET_ROSTER_WITHACP, FleetBus, FleetCostCapError, FleetManager, FleetSpawnBudgetError, FleetUsageAggregator, GlobalMailbox, HEAVY_BUDGET, HumanEscalatingBrainArbiter, InMemoryAgentBridge, InMemoryBridgeTransport, KNOWLEDGE_AGENTS, KnowledgeGraph, LIGHT_BUDGET, LargeAnswerStore, MEDIUM_BUDGET, META_AGENTS, NULL_FLEET_BUS, ObservableBrainArbiter, PLANNING_AGENTS, REFACTOR_PLANNER_AGENT, REVIEW_AGENTS, SECURITY_SCANNER_AGENT, SubagentBudget, TIMEOUT_PREEMPT_FRACTION, TaskAuctioneer, TaskDAG, VERIFY_AGENTS, applyRosterBudget, attachAutoExtend, attachDepWatcherBridge, composeDirectorPrompt, composeSubagentPrompt, createDelegateTool, createMailboxHooks, createMessage, detectEcosystem, dispatchAgent, formatHumanPrompt, getAgentDefinition, getFullPackageLog, getManifestPackages, getPackageAuthor, getPackagesByAgent, mailboxSessionTag, makeAgentSubagentRunner, makeAskResultTool, makeAskTool, makeAssignTool, makeAwaitTasksTool, makeCollabDebugTool, makeDependencyWatcherConfig, makeDirectorSessionFactory, makeFleetEmitTool, makeFleetHealthTool, makeFleetSessionTool, makeFleetStatusTool, makeFleetUsageTool, makeLLMClassifier, makeMailInboxTool, makeMailSendTool, makeMailboxTool, makeRollUpTool, makeSpawnTool, makeTerminateTool, makeWorkCompleteTool, normalizeRecipient, recordPackageAction, resolveMailboxIdentity, resolveProjectDir, rosterSummaryFromConfigs, scoreAgents, startPackageOutdatedWatcher, updatePackageOutdatedStatus, withDisabledToolFiltering };
13980
14402
  //# sourceMappingURL=index.js.map
13981
14403
  //# sourceMappingURL=index.js.map