@wrongstack/core 0.285.0 → 0.286.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 (145) hide show
  1. package/dist/autophase/phase-orchestrator.d.ts.map +1 -1
  2. package/dist/boot.d.ts.map +1 -1
  3. package/dist/coordination/agent-monitor.d.ts +29 -0
  4. package/dist/coordination/agent-monitor.d.ts.map +1 -1
  5. package/dist/coordination/autonomous-brain.d.ts +0 -2
  6. package/dist/coordination/autonomous-brain.d.ts.map +1 -1
  7. package/dist/coordination/autonomous-coordinator.d.ts +4 -0
  8. package/dist/coordination/autonomous-coordinator.d.ts.map +1 -1
  9. package/dist/coordination/brain-ledger.d.ts +142 -0
  10. package/dist/coordination/brain-ledger.d.ts.map +1 -0
  11. package/dist/coordination/brain-monitor.d.ts +31 -2
  12. package/dist/coordination/brain-monitor.d.ts.map +1 -1
  13. package/dist/coordination/brain.d.ts +43 -0
  14. package/dist/coordination/brain.d.ts.map +1 -1
  15. package/dist/coordination/delegate-tool.d.ts.map +1 -1
  16. package/dist/coordination/director-construction.d.ts.map +1 -1
  17. package/dist/coordination/director.d.ts +1 -1
  18. package/dist/coordination/director.d.ts.map +1 -1
  19. package/dist/coordination/fleet.d.ts +4 -4
  20. package/dist/coordination/fleet.d.ts.map +1 -1
  21. package/dist/coordination/global-mailbox.d.ts +63 -2
  22. package/dist/coordination/global-mailbox.d.ts.map +1 -1
  23. package/dist/coordination/index.d.ts +2 -1
  24. package/dist/coordination/index.d.ts.map +1 -1
  25. package/dist/coordination/index.js +1566 -462
  26. package/dist/coordination/index.js.map +4 -4
  27. package/dist/coordination/knowledge-graph.d.ts +13 -1
  28. package/dist/coordination/knowledge-graph.d.ts.map +1 -1
  29. package/dist/coordination/mailbox-actions.d.ts +12 -13
  30. package/dist/coordination/mailbox-actions.d.ts.map +1 -1
  31. package/dist/coordination/mailbox-constants.d.ts +64 -0
  32. package/dist/coordination/mailbox-constants.d.ts.map +1 -0
  33. package/dist/coordination/mailbox-events.d.ts +32 -0
  34. package/dist/coordination/mailbox-events.d.ts.map +1 -0
  35. package/dist/coordination/mailbox-message-codec.d.ts +8 -0
  36. package/dist/coordination/mailbox-message-codec.d.ts.map +1 -0
  37. package/dist/coordination/mailbox-types.d.ts +88 -4
  38. package/dist/coordination/mailbox-types.d.ts.map +1 -1
  39. package/dist/coordination/mailbox.d.ts +21 -2
  40. package/dist/coordination/mailbox.d.ts.map +1 -1
  41. package/dist/coordination/package-author-tracker.d.ts.map +1 -1
  42. package/dist/coordination/subagent-result-tool.d.ts.map +1 -1
  43. package/dist/defaults/index.js +846 -523
  44. package/dist/defaults/index.js.map +4 -4
  45. package/dist/execution/autonomous-runner.d.ts +7 -1
  46. package/dist/execution/autonomous-runner.d.ts.map +1 -1
  47. package/dist/execution/autonomy-brain.d.ts +72 -11
  48. package/dist/execution/autonomy-brain.d.ts.map +1 -1
  49. package/dist/execution/brain-chain.d.ts +53 -0
  50. package/dist/execution/brain-chain.d.ts.map +1 -0
  51. package/dist/execution/brain-evaluation.d.ts +66 -0
  52. package/dist/execution/brain-evaluation.d.ts.map +1 -0
  53. package/dist/execution/brain-runtime.d.ts +171 -0
  54. package/dist/execution/brain-runtime.d.ts.map +1 -0
  55. package/dist/execution/council-brain.d.ts +79 -0
  56. package/dist/execution/council-brain.d.ts.map +1 -0
  57. package/dist/execution/eternal-autonomy.d.ts +12 -0
  58. package/dist/execution/eternal-autonomy.d.ts.map +1 -1
  59. package/dist/execution/index.d.ts +5 -1
  60. package/dist/execution/index.d.ts.map +1 -1
  61. package/dist/execution/index.js +1350 -377
  62. package/dist/execution/index.js.map +4 -4
  63. package/dist/execution/parallel-eternal-engine.d.ts +12 -0
  64. package/dist/execution/parallel-eternal-engine.d.ts.map +1 -1
  65. package/dist/execution/tool-executor.d.ts.map +1 -1
  66. package/dist/hq/auth-store.d.ts +21 -3
  67. package/dist/hq/auth-store.d.ts.map +1 -1
  68. package/dist/hq/factory.d.ts +3 -0
  69. package/dist/hq/factory.d.ts.map +1 -1
  70. package/dist/hq/index.js +628 -72
  71. package/dist/hq/index.js.map +4 -4
  72. package/dist/hq/protocol.d.ts +54 -2
  73. package/dist/hq/protocol.d.ts.map +1 -1
  74. package/dist/hq/publisher.d.ts +4 -0
  75. package/dist/hq/publisher.d.ts.map +1 -1
  76. package/dist/index.d.ts +12 -6
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +3482 -1495
  79. package/dist/index.js.map +4 -4
  80. package/dist/kernel/events.d.ts +22 -4
  81. package/dist/kernel/events.d.ts.map +1 -1
  82. package/dist/kernel/index.js.map +2 -2
  83. package/dist/mailbox-attach.d.ts.map +1 -1
  84. package/dist/observability/index.js +16 -15
  85. package/dist/observability/index.js.map +2 -2
  86. package/dist/security/index.d.ts +1 -0
  87. package/dist/security/index.d.ts.map +1 -1
  88. package/dist/security/index.js +253 -21
  89. package/dist/security/index.js.map +4 -4
  90. package/dist/security/permission-policy-schema.d.ts +77 -0
  91. package/dist/security/permission-policy-schema.d.ts.map +1 -0
  92. package/dist/security/permission-policy.d.ts +5 -0
  93. package/dist/security/permission-policy.d.ts.map +1 -1
  94. package/dist/security/secret-vault.d.ts +9 -0
  95. package/dist/security/secret-vault.d.ts.map +1 -1
  96. package/dist/session-registry.d.ts.map +1 -1
  97. package/dist/storage/config-loader.d.ts +10 -0
  98. package/dist/storage/config-loader.d.ts.map +1 -1
  99. package/dist/storage/goal-store.d.ts +1 -1
  100. package/dist/storage/goal-store.d.ts.map +1 -1
  101. package/dist/storage/index.js +147 -162
  102. package/dist/storage/index.js.map +2 -2
  103. package/dist/storage/plan-store.d.ts +1 -1
  104. package/dist/storage/plan-store.d.ts.map +1 -1
  105. package/dist/storage/queue-store.d.ts +4 -0
  106. package/dist/storage/queue-store.d.ts.map +1 -1
  107. package/dist/storage/session-store.d.ts +9 -0
  108. package/dist/storage/session-store.d.ts.map +1 -1
  109. package/dist/storage/task-store.d.ts +1 -1
  110. package/dist/storage/task-store.d.ts.map +1 -1
  111. package/dist/storage/todos-checkpoint.d.ts +2 -2
  112. package/dist/storage/todos-checkpoint.d.ts.map +1 -1
  113. package/dist/types/config.d.ts +209 -1
  114. package/dist/types/config.d.ts.map +1 -1
  115. package/dist/types/index.js +11 -0
  116. package/dist/types/index.js.map +2 -2
  117. package/dist/types/multi-agent.d.ts +8 -0
  118. package/dist/types/multi-agent.d.ts.map +1 -1
  119. package/dist/types/observability.d.ts +5 -0
  120. package/dist/types/observability.d.ts.map +1 -1
  121. package/dist/types/tool.d.ts +7 -0
  122. package/dist/types/tool.d.ts.map +1 -1
  123. package/dist/utils/glob-expand.d.ts.map +1 -1
  124. package/dist/utils/incoming-images.d.ts +39 -0
  125. package/dist/utils/incoming-images.d.ts.map +1 -0
  126. package/dist/utils/index.d.ts +1 -0
  127. package/dist/utils/index.d.ts.map +1 -1
  128. package/dist/utils/index.js +81 -3
  129. package/dist/utils/index.js.map +3 -3
  130. package/dist/worktree/worktree-manager.d.ts +13 -4
  131. package/dist/worktree/worktree-manager.d.ts.map +1 -1
  132. package/instructions/agents/browser.md +25 -16
  133. package/instructions/coordination/subagent-baseline.md +16 -2
  134. package/instructions/leader-after-task.md +3 -1
  135. package/instructions/sections/tool/common-patterns.md +7 -1
  136. package/instructions/system.md +47 -0
  137. package/package.json +2 -2
  138. package/skills/docker-deploy/SKILL.md +1 -1
  139. package/skills/wrongstack-mailbox/SKILL.md +51 -12
  140. package/dist/plugins/git-plugin.d.ts +0 -15
  141. package/dist/plugins/git-plugin.d.ts.map +0 -1
  142. package/dist/plugins/observability-plugin.d.ts +0 -19
  143. package/dist/plugins/observability-plugin.d.ts.map +0 -1
  144. package/dist/plugins/plan-plugin.d.ts +0 -16
  145. package/dist/plugins/plan-plugin.d.ts.map +0 -1
@@ -329,9 +329,17 @@ function globToRegex(pat) {
329
329
  return new RegExp(re + "$");
330
330
  }
331
331
  function baseDir(pat) {
332
- let i = pat.length - 1;
333
- while (i >= 0 && !GLOB_CHARS.has(expectDefined(pat[i])) && pat[i] !== SEP && pat[i] !== "/") i--;
334
- const cut = i >= 0 ? pat.lastIndexOf(SEP, i) : pat.lastIndexOf("/", i);
332
+ let firstGlob = pat.length;
333
+ for (let i = 0; i < pat.length; i++) {
334
+ if (GLOB_CHARS.has(expectDefined(pat[i]))) {
335
+ firstGlob = i;
336
+ break;
337
+ }
338
+ }
339
+ const cut = Math.max(
340
+ pat.lastIndexOf(SEP, firstGlob - 1),
341
+ pat.lastIndexOf("/", firstGlob - 1)
342
+ );
335
343
  return cut < 0 ? "." : pat.slice(0, cut);
336
344
  }
337
345
  async function expandGlob(pattern) {
@@ -1417,9 +1425,11 @@ function normalizeSubagentStructuredReport(value) {
1417
1425
  const filesExamined = stringArray(value["files_examined"]);
1418
1426
  const nextSteps = stringArray(value["suggested_next_steps"]);
1419
1427
  const confidence = value["confidence"];
1428
+ const completion = value["completion"];
1429
+ const remainingWork = optionalString(value["remaining_work"]);
1420
1430
  if (!summary || summary.length > MAX_SUMMARY_CHARS || !findings || findings.length > MAX_FINDINGS || !filesExamined || filesExamined.length > MAX_FILES || !nextSteps || nextSteps.length > MAX_NEXT_STEPS || [...findings, ...filesExamined, ...nextSteps].some(
1421
1431
  (item) => item.trim().length > MAX_ITEM_CHARS
1422
- ) || typeof confidence !== "number" || !Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
1432
+ ) || typeof confidence !== "number" || !Number.isFinite(confidence) || confidence < 0 || confidence > 1 || completion !== void 0 && completion !== "complete" && completion !== "partial" || completion === "partial" && !remainingWork || remainingWork !== void 0 && remainingWork.length > MAX_SUMMARY_CHARS) {
1423
1433
  return void 0;
1424
1434
  }
1425
1435
  const report = {
@@ -1427,7 +1437,9 @@ function normalizeSubagentStructuredReport(value) {
1427
1437
  findings: findings.map((item) => item.trim()).filter(Boolean),
1428
1438
  files_examined: filesExamined.map((item) => item.trim()).filter(Boolean),
1429
1439
  confidence,
1430
- suggested_next_steps: nextSteps.map((item) => item.trim()).filter(Boolean)
1440
+ suggested_next_steps: nextSteps.map((item) => item.trim()).filter(Boolean),
1441
+ ...completion === "complete" || completion === "partial" ? { completion } : {},
1442
+ ...remainingWork ? { remaining_work: remainingWork } : {}
1431
1443
  };
1432
1444
  return JSON.stringify(report).length <= MAX_SUBAGENT_STRUCTURED_REPORT_CHARS ? report : void 0;
1433
1445
  }
@@ -1440,6 +1452,8 @@ function formatSubagentStructuredReport(report) {
1440
1452
  const next = report.suggested_next_steps.length ? report.suggested_next_steps.map((item) => `- ${item}`).join("\n") : "- (none)";
1441
1453
  return [
1442
1454
  report.summary,
1455
+ report.completion ? `Completion: ${report.completion}` : void 0,
1456
+ report.remaining_work ? `Remaining work: ${report.remaining_work}` : void 0,
1443
1457
  `
1444
1458
  Findings:
1445
1459
  ${findings}`,
@@ -1449,7 +1463,7 @@ Files examined: ${files}`,
1449
1463
  `
1450
1464
  Suggested next steps:
1451
1465
  ${next}`
1452
- ].join("\n");
1466
+ ].filter((line) => typeof line === "string").join("\n");
1453
1467
  }
1454
1468
  function makeSubagentResultTool() {
1455
1469
  return {
@@ -1489,6 +1503,16 @@ function makeSubagentResultTool() {
1489
1503
  maxItems: MAX_NEXT_STEPS,
1490
1504
  items: { type: "string" },
1491
1505
  description: "Concrete follow-ups; use an empty array when none remain."
1506
+ },
1507
+ completion: {
1508
+ type: "string",
1509
+ enum: ["complete", "partial"],
1510
+ description: "Use `partial` only when the task is intentionally stopping at a clean checkpoint and another worker should continue."
1511
+ },
1512
+ remaining_work: {
1513
+ type: "string",
1514
+ maxLength: MAX_SUMMARY_CHARS,
1515
+ description: "Concrete work left for a successor. Required by runtime validation when completion is `partial`."
1492
1516
  }
1493
1517
  },
1494
1518
  required: ["summary", "findings", "files_examined", "confidence", "suggested_next_steps"],
@@ -1518,6 +1542,9 @@ function isRecord(value) {
1518
1542
  function nonEmptyString(value) {
1519
1543
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
1520
1544
  }
1545
+ function optionalString(value) {
1546
+ return value === void 0 ? void 0 : nonEmptyString(value);
1547
+ }
1521
1548
  function stringArray(value) {
1522
1549
  return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
1523
1550
  }
@@ -3458,7 +3485,12 @@ var BrainDecisionQueue = class {
3458
3485
  if (this.opts.timeoutMs && this.opts.timeoutMs > 0) {
3459
3486
  entry.timer = setTimeout(() => {
3460
3487
  this.pending.delete(request.id);
3461
- resolve9({ type: "deny", reason: "Brain human decision timed out." });
3488
+ resolve9(
3489
+ this.opts.onTimeout?.(request) ?? {
3490
+ type: "deny",
3491
+ reason: "Brain human decision timed out."
3492
+ }
3493
+ );
3462
3494
  }, this.opts.timeoutMs);
3463
3495
  }
3464
3496
  this.pending.set(request.id, entry);
@@ -3475,6 +3507,46 @@ var BrainDecisionQueue = class {
3475
3507
  }
3476
3508
  }
3477
3509
  };
3510
+ function terminalPolicyDecision(request) {
3511
+ const recommended = request.options?.find((option) => option.recommended);
3512
+ if (recommended && (request.risk === "low" || request.risk === "medium")) {
3513
+ return {
3514
+ type: "answer",
3515
+ optionId: recommended.id,
3516
+ text: recommended.label,
3517
+ rationale: `Headless terminal policy: caller-recommended option accepted at ${request.risk} risk (no human available by configuration).`
3518
+ };
3519
+ }
3520
+ if (request.fallback === "continue") {
3521
+ return {
3522
+ type: "answer",
3523
+ text: "Continue with the caller default.",
3524
+ rationale: "Headless terminal policy: request declares continue as its safe fallback."
3525
+ };
3526
+ }
3527
+ return {
3528
+ type: "deny",
3529
+ reason: `Headless terminal policy: no safe automatic option for "${request.question}" (risk: ${request.risk}). The proposed action was not taken.`
3530
+ };
3531
+ }
3532
+ var EscalationRoutingBrainArbiter = class {
3533
+ constructor(inner, queue, getMode) {
3534
+ this.inner = inner;
3535
+ this.queue = queue;
3536
+ this.getMode = getMode;
3537
+ }
3538
+ inner;
3539
+ queue;
3540
+ getMode;
3541
+ async decide(request) {
3542
+ const decision = await this.inner.decide(request);
3543
+ if (decision.type !== "ask_human") return decision;
3544
+ if (this.getMode() === "interactive" && this.queue) {
3545
+ return this.queue.requestHumanDecision(request);
3546
+ }
3547
+ return terminalPolicyDecision(request);
3548
+ }
3549
+ };
3478
3550
  var HumanEscalatingBrainArbiter = class {
3479
3551
  constructor(inner, queue) {
3480
3552
  this.inner = inner;
@@ -3550,29 +3622,341 @@ function formatHumanPrompt(request) {
3550
3622
  return lines.join("\n");
3551
3623
  }
3552
3624
 
3625
+ // src/coordination/brain-ledger.ts
3626
+ import { appendFile, mkdir as mkdir2, readFile } from "node:fs/promises";
3627
+ import { dirname as dirname3 } from "node:path";
3628
+ var QUESTION_MAX = 200;
3629
+ var DETAIL_MAX = 240;
3630
+ var truncate2 = (s, max) => s === void 0 ? void 0 : s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
3631
+ function brainDecisionKey(source, question) {
3632
+ const normalized = question.toLowerCase().replace(/[^\s]*\d[^\s]*/g, "#").replace(/\s+/g, " ").trim().slice(0, 100);
3633
+ return `${source ?? "?"}:${normalized}`;
3634
+ }
3635
+ function fmtAge(ms) {
3636
+ const s = Math.max(0, Math.round(ms / 1e3));
3637
+ if (s < 60) return `${s}s ago`;
3638
+ if (s < 3600) return `${Math.round(s / 60)}m ago`;
3639
+ return `${Math.round(s / 3600)}h ago`;
3640
+ }
3641
+ function createLedgerGuardBrainArbiter(opts) {
3642
+ const denyAfter = opts.denyAfter ?? 3;
3643
+ return {
3644
+ async decide(request) {
3645
+ if (denyAfter > 0) {
3646
+ const streak = opts.failureStreakFor(request);
3647
+ if (streak >= denyAfter) {
3648
+ return {
3649
+ type: "deny",
3650
+ reason: `Ledger guard: the last ${streak} approved decisions of this kind all ended in observed failures \u2014 denying deterministically until a success is recorded. (${request.question.slice(0, 120)})`
3651
+ };
3652
+ }
3653
+ }
3654
+ return opts.inner.decide(request);
3655
+ }
3656
+ };
3657
+ }
3658
+ var BrainDecisionLedger = class {
3659
+ constructor(opts) {
3660
+ this.opts = opts;
3661
+ this.maxMemoryEntries = opts.maxMemoryEntries ?? 500;
3662
+ this.retryWindowMs = opts.interventionRetryWindowMs ?? 10 * 6e4;
3663
+ this.now = opts.now ?? Date.now;
3664
+ }
3665
+ opts;
3666
+ entries = [];
3667
+ outcomeByRequest = /* @__PURE__ */ new Map();
3668
+ /** subagentId → decision request ids awaiting that subagent's task result. */
3669
+ pendingBudgetOutcomes = /* @__PURE__ */ new Map();
3670
+ lastInterventionByKind = /* @__PURE__ */ new Map();
3671
+ unsubscribers = [];
3672
+ /** Serialized fire-and-forget writes — awaited by `stop()` (drain). */
3673
+ writeChain = Promise.resolve();
3674
+ dirReady = false;
3675
+ maxMemoryEntries;
3676
+ retryWindowMs;
3677
+ now;
3678
+ /**
3679
+ * Subscribe to the EventBus and seed the in-memory ring from the JSONL
3680
+ * tail. The returned promise resolves once the seed load finished
3681
+ * (best-effort — a missing/corrupt file is an empty history, not an error).
3682
+ */
3683
+ async start() {
3684
+ const { events } = this.opts;
3685
+ this.unsubscribers.push(
3686
+ events.on("brain.decision_answered", (e) => {
3687
+ this.record({
3688
+ at: e.at,
3689
+ sessionId: e.sessionId,
3690
+ requestId: e.request.id,
3691
+ kind: "answered",
3692
+ source: e.request.source,
3693
+ risk: e.request.risk,
3694
+ question: truncate2(e.request.question, QUESTION_MAX),
3695
+ optionId: e.decision.type === "answer" ? e.decision.optionId : void 0,
3696
+ detail: e.decision.type === "answer" ? truncate2(e.decision.rationale ?? e.decision.text, DETAIL_MAX) : void 0
3697
+ });
3698
+ }),
3699
+ events.on("brain.decision_denied", (e) => {
3700
+ this.record({
3701
+ at: e.at,
3702
+ sessionId: e.sessionId,
3703
+ requestId: e.request.id,
3704
+ kind: "denied",
3705
+ source: e.request.source,
3706
+ risk: e.request.risk,
3707
+ question: truncate2(e.request.question, QUESTION_MAX),
3708
+ detail: e.decision.type === "deny" ? truncate2(e.decision.reason, DETAIL_MAX) : void 0
3709
+ });
3710
+ }),
3711
+ events.on("brain.decision_ask_human", (e) => {
3712
+ this.record({
3713
+ at: e.at,
3714
+ sessionId: e.sessionId,
3715
+ requestId: e.request.id,
3716
+ kind: "ask_human",
3717
+ source: e.request.source,
3718
+ risk: e.request.risk,
3719
+ question: truncate2(e.request.question, QUESTION_MAX)
3720
+ });
3721
+ }),
3722
+ events.on("brain.intervention", (e) => {
3723
+ this.record({
3724
+ at: e.at,
3725
+ sessionId: e.sessionId,
3726
+ requestId: e.request.id,
3727
+ kind: "intervention",
3728
+ source: e.request.source,
3729
+ risk: e.request.risk,
3730
+ question: truncate2(`[${e.kind}] ${e.request.question}`, QUESTION_MAX),
3731
+ detail: e.intervened ? "steered" : "observed only"
3732
+ });
3733
+ if (!e.intervened) return;
3734
+ const prev = this.lastInterventionByKind.get(e.kind);
3735
+ if (prev && e.at - prev.at <= this.retryWindowMs) {
3736
+ this.recordOutcome(
3737
+ prev.requestId,
3738
+ "failure",
3739
+ `same signal (${e.kind}) re-triggered ${fmtAge(e.at - prev.at).replace(" ago", " later")}`,
3740
+ e.sessionId
3741
+ );
3742
+ }
3743
+ this.lastInterventionByKind.set(e.kind, { requestId: e.request.id, at: e.at });
3744
+ }),
3745
+ // ── Outcome correlation: budget extensions ────────────────────────
3746
+ events.on("subagent.budget_extended", (e) => {
3747
+ const requestId = `director-budget-${e.subagentId}-${e.kind}`;
3748
+ if (!this.entries.some((entry) => entry.requestId === requestId)) return;
3749
+ let set = this.pendingBudgetOutcomes.get(e.subagentId);
3750
+ if (!set) {
3751
+ set = /* @__PURE__ */ new Set();
3752
+ this.pendingBudgetOutcomes.set(e.subagentId, set);
3753
+ }
3754
+ set.add(requestId);
3755
+ }),
3756
+ events.on("subagent.task_completed", (e) => {
3757
+ const pending = this.pendingBudgetOutcomes.get(e.subagentId);
3758
+ if (!pending) return;
3759
+ this.pendingBudgetOutcomes.delete(e.subagentId);
3760
+ const outcome = e.status === "success" ? "success" : "failure";
3761
+ for (const requestId of pending) {
3762
+ this.recordOutcome(
3763
+ requestId,
3764
+ outcome,
3765
+ `budget-extended subagent's task ${e.status}`,
3766
+ e.sessionId
3767
+ );
3768
+ }
3769
+ })
3770
+ );
3771
+ await this.load();
3772
+ }
3773
+ /** Unsubscribe and drain pending writes. Safe to call twice. */
3774
+ async stop() {
3775
+ for (const off of this.unsubscribers) off();
3776
+ this.unsubscribers.length = 0;
3777
+ await this.writeChain.catch(() => {
3778
+ });
3779
+ }
3780
+ /**
3781
+ * Attach a real-world outcome to an earlier decision. Appended to the
3782
+ * JSONL, folded into future digests, and re-emitted as `brain.outcome`.
3783
+ */
3784
+ recordOutcome(requestId, outcome, detail, sessionId) {
3785
+ this.outcomeByRequest.set(requestId, outcome);
3786
+ this.record({
3787
+ at: this.now(),
3788
+ sessionId,
3789
+ requestId,
3790
+ kind: "outcome",
3791
+ outcome,
3792
+ detail: truncate2(detail, DETAIL_MAX)
3793
+ });
3794
+ this.opts.events.emit("brain.outcome", {
3795
+ sessionId,
3796
+ requestId,
3797
+ outcome,
3798
+ detail,
3799
+ at: this.now()
3800
+ });
3801
+ }
3802
+ /** Last `n` ledger rows, oldest first. */
3803
+ tail(n) {
3804
+ return this.entries.slice(-n);
3805
+ }
3806
+ /**
3807
+ * Consecutive most-recent FAILURE outcomes among answered decisions of
3808
+ * this request's group. Decisions whose outcome is still unknown are
3809
+ * skipped (they neither extend nor break the streak); the first known
3810
+ * SUCCESS ends it. Powers the deterministic ledger guard: "the last K
3811
+ * times we approved this, it went wrong — stop approving".
3812
+ */
3813
+ failureStreakFor(request) {
3814
+ const key = brainDecisionKey(request.source, request.question);
3815
+ let streak = 0;
3816
+ for (let i = this.entries.length - 1; i >= 0; i--) {
3817
+ const e = this.entries[i];
3818
+ if (e?.kind !== "answered" || e.requestId === request.id || e.question === void 0 || brainDecisionKey(e.source, e.question) !== key) {
3819
+ continue;
3820
+ }
3821
+ const outcome = this.outcomeByRequest.get(e.requestId);
3822
+ if (outcome === void 0) continue;
3823
+ if (outcome === "success") break;
3824
+ streak += 1;
3825
+ }
3826
+ return streak;
3827
+ }
3828
+ /**
3829
+ * Short decision-history block for the LLM/council prompt: how similar
3830
+ * past decisions went and — when known — how they turned OUT. Returns
3831
+ * undefined when there is no relevant history (no prompt noise).
3832
+ */
3833
+ digestFor(request) {
3834
+ const key = brainDecisionKey(request.source, request.question);
3835
+ const related = this.entries.filter(
3836
+ (e) => (e.kind === "answered" || e.kind === "denied") && e.requestId !== request.id && e.question !== void 0 && brainDecisionKey(e.source, e.question) === key
3837
+ );
3838
+ if (related.length === 0) return void 0;
3839
+ const recent = related.slice(-5).reverse();
3840
+ const now = this.now();
3841
+ const lines = recent.map((e) => {
3842
+ const what = e.kind === "denied" ? "denied" : e.optionId ? `chose [${e.optionId}]` : "answered";
3843
+ const outcome = this.outcomeByRequest.get(e.requestId);
3844
+ const suffix = outcome === "failure" ? " \u2192 later FAILED" : outcome === "success" ? " \u2192 later succeeded" : "";
3845
+ return `- ${fmtAge(now - e.at)}: ${what}${suffix}`;
3846
+ });
3847
+ const approved = related.filter((e) => e.kind === "answered");
3848
+ const failedAfter = approved.filter(
3849
+ (e) => this.outcomeByRequest.get(e.requestId) === "failure"
3850
+ ).length;
3851
+ const summary = `Summary: ${related.length} similar decision(s); ${approved.length} answered${failedAfter > 0 ? ` (${failedAfter} later failed)` : ""}, ${related.length - approved.length} denied.`;
3852
+ return [...lines, summary].join("\n");
3853
+ }
3854
+ // ── Private ───────────────────────────────────────────────────────────
3855
+ record(entry) {
3856
+ this.entries.push(entry);
3857
+ if (this.entries.length > this.maxMemoryEntries) {
3858
+ const evictedRequestIds = /* @__PURE__ */ new Set();
3859
+ const evictCount = this.entries.length - this.maxMemoryEntries;
3860
+ const evicted = this.entries.splice(0, evictCount);
3861
+ for (const e of evicted) {
3862
+ if (e.requestId) evictedRequestIds.add(e.requestId);
3863
+ }
3864
+ if (evictedRequestIds.size > 0) {
3865
+ const remainingRequestIds = /* @__PURE__ */ new Set();
3866
+ for (const e of this.entries) {
3867
+ if (e.requestId) remainingRequestIds.add(e.requestId);
3868
+ }
3869
+ for (const id of evictedRequestIds) {
3870
+ if (!remainingRequestIds.has(id)) this.outcomeByRequest.delete(id);
3871
+ }
3872
+ }
3873
+ }
3874
+ this.append(entry);
3875
+ }
3876
+ append(entry) {
3877
+ this.writeChain = this.writeChain.then(async () => {
3878
+ if (!this.dirReady) {
3879
+ await mkdir2(dirname3(this.opts.filePath), { recursive: true });
3880
+ this.dirReady = true;
3881
+ }
3882
+ await appendFile(this.opts.filePath, `${JSON.stringify(entry)}
3883
+ `, "utf8");
3884
+ }).catch(() => {
3885
+ });
3886
+ }
3887
+ /** Seed the in-memory ring from the JSONL tail (cross-session learning). */
3888
+ async load() {
3889
+ let raw;
3890
+ try {
3891
+ raw = await readFile(this.opts.filePath, "utf8");
3892
+ } catch {
3893
+ return;
3894
+ }
3895
+ const lines = raw.split("\n").filter((l) => l.trim().length > 0);
3896
+ const seed = [];
3897
+ for (const line of lines.slice(-this.maxMemoryEntries)) {
3898
+ try {
3899
+ const parsed = JSON.parse(line);
3900
+ if (typeof parsed?.requestId === "string" && typeof parsed.at === "number") {
3901
+ seed.push(parsed);
3902
+ }
3903
+ } catch {
3904
+ }
3905
+ }
3906
+ this.entries.unshift(...seed);
3907
+ if (this.entries.length > this.maxMemoryEntries) {
3908
+ this.entries.splice(0, this.entries.length - this.maxMemoryEntries);
3909
+ }
3910
+ for (const e of this.entries) {
3911
+ if (e.kind === "outcome" && e.outcome) this.outcomeByRequest.set(e.requestId, e.outcome);
3912
+ }
3913
+ }
3914
+ };
3915
+
3553
3916
  // src/coordination/brain-monitor.ts
3554
3917
  import { randomUUID as randomUUID2 } from "node:crypto";
3918
+ var FILE_EDIT_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "patch", "multi_edit", "multiedit", "str_replace"]);
3919
+ function editedPath(input) {
3920
+ if (!input || typeof input !== "object") return void 0;
3921
+ const r = input;
3922
+ const candidate = r["file_path"] ?? r["path"] ?? r["filePath"] ?? r["file"];
3923
+ return typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
3924
+ }
3555
3925
  var BrainMonitor = class {
3556
3926
  constructor(opts) {
3557
3927
  this.opts = opts;
3558
3928
  this.toolFailureStreak = opts.toolFailureStreak ?? 3;
3559
3929
  this.errorStormCount = opts.errorStormCount ?? 4;
3560
3930
  this.errorStormWindowMs = opts.errorStormWindowMs ?? 6e4;
3931
+ this.stallMs = opts.stallMs ?? 3e5;
3932
+ this.stallCheckIntervalMs = opts.stallCheckIntervalMs ?? 3e4;
3933
+ this.fileChurnThreshold = opts.fileChurnThreshold ?? 5;
3934
+ this.fileChurnWindowMs = opts.fileChurnWindowMs ?? 6e5;
3561
3935
  this.cooldownMs = opts.cooldownMs ?? 12e4;
3562
3936
  }
3563
3937
  opts;
3564
3938
  failStreaks = /* @__PURE__ */ new Map();
3565
3939
  errorTimestamps = [];
3940
+ editTimestamps = /* @__PURE__ */ new Map();
3566
3941
  lastEngagedAt = /* @__PURE__ */ new Map();
3567
3942
  unsubscribers = [];
3568
3943
  engaging = false;
3944
+ activeRuns = 0;
3945
+ lastProgressAt = 0;
3946
+ stallTimer;
3569
3947
  toolFailureStreak;
3570
3948
  errorStormCount;
3571
3949
  errorStormWindowMs;
3950
+ stallMs;
3951
+ stallCheckIntervalMs;
3952
+ fileChurnThreshold;
3953
+ fileChurnWindowMs;
3572
3954
  cooldownMs;
3573
3955
  start() {
3574
3956
  this.unsubscribers.push(
3575
3957
  this.opts.events.on("tool.executed", (e) => {
3958
+ this.lastProgressAt = Date.now();
3959
+ this.trackFileChurn(e.name, e.ok, e.input);
3576
3960
  if (e.ok) {
3577
3961
  this.failStreaks.delete(e.name);
3578
3962
  return;
@@ -3592,6 +3976,38 @@ var BrainMonitor = class {
3592
3976
  }
3593
3977
  })
3594
3978
  );
3979
+ if (this.stallMs > 0) {
3980
+ this.unsubscribers.push(
3981
+ this.opts.events.on("agent.run.started", () => {
3982
+ this.activeRuns += 1;
3983
+ this.lastProgressAt = Date.now();
3984
+ }),
3985
+ this.opts.events.on("agent.run.completed", () => {
3986
+ this.activeRuns = Math.max(0, this.activeRuns - 1);
3987
+ }),
3988
+ this.opts.events.on("agent.run.error", () => {
3989
+ this.activeRuns = Math.max(0, this.activeRuns - 1);
3990
+ }),
3991
+ this.opts.events.on("iteration.started", () => {
3992
+ this.lastProgressAt = Date.now();
3993
+ })
3994
+ );
3995
+ this.stallTimer = setInterval(() => {
3996
+ if (this.activeRuns === 0 || this.lastProgressAt === 0) return;
3997
+ const idleMs = Date.now() - this.lastProgressAt;
3998
+ if (idleMs < this.stallMs) return;
3999
+ this.lastProgressAt = Date.now();
4000
+ void this.engage("agent_stall", {
4001
+ question: `An active run has made no observable progress (no tool call or iteration) for ${Math.round(idleMs / 6e4)} minute(s). Should the agent be steered before more time is wasted?`,
4002
+ context: [
4003
+ `Active runs: ${this.activeRuns}`,
4004
+ `Idle for: ${Math.round(idleMs / 1e3)}s`,
4005
+ `Stall threshold: ${Math.round(this.stallMs / 1e3)}s`
4006
+ ].join("\n")
4007
+ });
4008
+ }, this.stallCheckIntervalMs);
4009
+ this.stallTimer.unref?.();
4010
+ }
3595
4011
  this.unsubscribers.push(
3596
4012
  this.opts.events.on("error", (e) => {
3597
4013
  const now = Date.now();
@@ -3616,6 +4032,37 @@ var BrainMonitor = class {
3616
4032
  this.unsubscribers.length = 0;
3617
4033
  this.failStreaks.clear();
3618
4034
  this.errorTimestamps = [];
4035
+ this.editTimestamps.clear();
4036
+ if (this.stallTimer) {
4037
+ clearInterval(this.stallTimer);
4038
+ this.stallTimer = void 0;
4039
+ }
4040
+ this.activeRuns = 0;
4041
+ this.lastProgressAt = 0;
4042
+ }
4043
+ /** Sliding-window count of successful edits per file → churn signal. */
4044
+ trackFileChurn(toolName, ok, input) {
4045
+ if (!ok || !FILE_EDIT_TOOLS.has(toolName.toLowerCase())) return;
4046
+ const path20 = editedPath(input);
4047
+ if (!path20) return;
4048
+ const now = Date.now();
4049
+ const stamps = (this.editTimestamps.get(path20) ?? []).filter(
4050
+ (t) => now - t <= this.fileChurnWindowMs
4051
+ );
4052
+ stamps.push(now);
4053
+ if (stamps.length >= this.fileChurnThreshold) {
4054
+ this.editTimestamps.delete(path20);
4055
+ void this.engage("file_churn", {
4056
+ question: `The file "${path20}" has been edited ${stamps.length} times within ${Math.round(this.fileChurnWindowMs / 6e4)} minutes \u2014 the agent may be oscillating (edit/revert loop) instead of converging. Should it be steered?`,
4057
+ context: [
4058
+ `File: ${path20}`,
4059
+ `Edits in window: ${stamps.length}`,
4060
+ `Window: ${Math.round(this.fileChurnWindowMs / 1e3)}s`
4061
+ ].join("\n")
4062
+ });
4063
+ return;
4064
+ }
4065
+ this.editTimestamps.set(path20, stamps);
3619
4066
  }
3620
4067
  async engage(kind, input) {
3621
4068
  const last = this.lastEngagedAt.get(kind) ?? 0;
@@ -4422,58 +4869,27 @@ import * as path5 from "node:path";
4422
4869
  import { randomUUID as randomUUID4 } from "node:crypto";
4423
4870
 
4424
4871
  // src/coordination/fleet.ts
4425
- var AUDIT_LOG_AGENT = {
4426
- id: "audit-log",
4427
- name: "Audit Log",
4428
- role: "audit-log",
4429
- prompt: agentPrompt("audit-log")
4430
- // No hardcoded budgets — the orchestrator (delegate tool or
4431
- // spawn_subagent) decides per-task how much room a subagent gets.
4432
- // A monorepo audit needs hours; a single-file lint check needs
4433
- // seconds. Pinning a number here forces the orchestrator to fight
4434
- // the role's default instead of just asking for what it needs.
4435
- };
4436
- var BUG_HUNTER_AGENT = {
4437
- id: "bug-hunter",
4438
- name: "Bug Hunter",
4439
- role: "bug-hunter",
4440
- prompt: agentPrompt("bug-hunter")
4441
- // Budgets are set by the orchestrator per task — see fleet.ts header.
4442
- };
4443
- var REFACTOR_PLANNER_AGENT = {
4444
- id: "refactor-planner",
4445
- name: "Refactor Planner",
4446
- role: "refactor-planner",
4447
- prompt: agentPrompt("refactor-planner")
4448
- // Budgets are set by the orchestrator per task — see fleet.ts header.
4449
- };
4450
- var SECURITY_SCANNER_AGENT = {
4451
- id: "security-scanner",
4452
- name: "Security Scanner",
4453
- role: "security-scanner",
4454
- prompt: agentPrompt("security-scanner")
4455
- // Budgets are set by the orchestrator per task — see fleet.ts header.
4456
- };
4457
- var SHADOW_AGENT = {
4458
- id: "shadow-agent",
4459
- name: "Shadow",
4460
- role: "shadow-agent",
4461
- prompt: agentPrompt("shadow-agent")
4462
- // Budgets are set by the orchestrator per task — see fleet.ts header.
4463
- };
4464
- var CRITIC_AGENT = {
4465
- id: "critic",
4466
- name: "Critic",
4467
- role: "critic",
4468
- prompt: agentPrompt("critic")
4469
- // Budgets are set by the orchestrator per task — see fleet.ts header.
4470
- };
4872
+ function defineAgent(id, name, promptRole = id, provider) {
4873
+ return {
4874
+ id,
4875
+ name,
4876
+ role: id,
4877
+ prompt: agentPrompt(promptRole),
4878
+ ...provider === void 0 ? {} : { provider }
4879
+ };
4880
+ }
4881
+ var AUDIT_LOG_AGENT = defineAgent("audit-log", "Audit Log");
4882
+ var BUG_HUNTER_AGENT = defineAgent("bug-hunter", "Bug Hunter");
4883
+ var REFACTOR_PLANNER_AGENT = defineAgent("refactor-planner", "Refactor Planner");
4884
+ var SECURITY_SCANNER_AGENT = defineAgent("security-scanner", "Security Scanner");
4885
+ var SHADOW_AGENT = defineAgent("shadow-agent", "Shadow");
4886
+ var CRITIC_AGENT = defineAgent("critic", "Critic");
4471
4887
  var FLEET_ROSTER = {
4472
4888
  "audit-log": AUDIT_LOG_AGENT,
4473
4889
  "bug-hunter": BUG_HUNTER_AGENT,
4474
4890
  "refactor-planner": REFACTOR_PLANNER_AGENT,
4475
4891
  "security-scanner": SECURITY_SCANNER_AGENT,
4476
- "critic": CRITIC_AGENT,
4892
+ critic: CRITIC_AGENT,
4477
4893
  "shadow-agent": SHADOW_AGENT,
4478
4894
  ...Object.fromEntries(
4479
4895
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.config])
@@ -4485,7 +4901,7 @@ var FLEET_ROSTER_BUDGETS = {
4485
4901
  "bug-hunter": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
4486
4902
  "refactor-planner": { timeoutMs: 7.5 * 60 * 60 * 1e3, maxIterations: 6e3, maxToolCalls: 18e3 },
4487
4903
  "security-scanner": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
4488
- "critic": { timeoutMs: 5 * 60 * 60 * 1e3, maxIterations: 4e3, maxToolCalls: 12e3 },
4904
+ critic: { timeoutMs: 5 * 60 * 60 * 1e3, maxIterations: 4e3, maxToolCalls: 12e3 },
4489
4905
  "shadow-agent": {
4490
4906
  idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
4491
4907
  maxIterations: 2e3,
@@ -4522,41 +4938,11 @@ function applyRosterBudget(cfg) {
4522
4938
  };
4523
4939
  }
4524
4940
  var ALL_FLEET_AGENTS = Object.values(FLEET_ROSTER);
4525
- var CLINE_AGENT = {
4526
- id: "cline",
4527
- name: "Cline",
4528
- role: "cline",
4529
- prompt: agentPrompt("acp-cline"),
4530
- provider: "acp"
4531
- };
4532
- var GEMINI_CLI_AGENT = {
4533
- id: "gemini-cli",
4534
- name: "Gemini CLI",
4535
- role: "gemini-cli",
4536
- prompt: agentPrompt("acp-gemini-cli"),
4537
- provider: "acp"
4538
- };
4539
- var COPILOT_AGENT = {
4540
- id: "copilot",
4541
- name: "GitHub Copilot",
4542
- role: "copilot",
4543
- prompt: agentPrompt("acp-copilot"),
4544
- provider: "acp"
4545
- };
4546
- var OPENHANDS_AGENT = {
4547
- id: "openhands",
4548
- name: "OpenHands",
4549
- role: "openhands",
4550
- prompt: agentPrompt("acp-openhands"),
4551
- provider: "acp"
4552
- };
4553
- var GOOSE_AGENT = {
4554
- id: "goose",
4555
- name: "Goose",
4556
- role: "goose",
4557
- prompt: agentPrompt("acp-goose"),
4558
- provider: "acp"
4559
- };
4941
+ var CLINE_AGENT = defineAgent("cline", "Cline", "acp-cline", "acp");
4942
+ var GEMINI_CLI_AGENT = defineAgent("gemini-cli", "Gemini CLI", "acp-gemini-cli", "acp");
4943
+ var COPILOT_AGENT = defineAgent("copilot", "GitHub Copilot", "acp-copilot", "acp");
4944
+ var OPENHANDS_AGENT = defineAgent("openhands", "OpenHands", "acp-openhands", "acp");
4945
+ var GOOSE_AGENT = defineAgent("goose", "Goose", "acp-goose", "acp");
4560
4946
  var ACP_AGENTS = [
4561
4947
  CLINE_AGENT,
4562
4948
  GEMINI_CLI_AGENT,
@@ -4564,11 +4950,14 @@ var ACP_AGENTS = [
4564
4950
  OPENHANDS_AGENT,
4565
4951
  GOOSE_AGENT
4566
4952
  ];
4567
- FLEET_ROSTER_BUDGETS["cline"] = { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 };
4568
- FLEET_ROSTER_BUDGETS["gemini-cli"] = { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 };
4569
- FLEET_ROSTER_BUDGETS["copilot"] = { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 };
4570
- FLEET_ROSTER_BUDGETS["openhands"] = { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 };
4571
- FLEET_ROSTER_BUDGETS["goose"] = { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 };
4953
+ var ACP_AGENT_BUDGET = {
4954
+ timeoutMs: 10 * 60 * 60 * 1e3,
4955
+ maxIterations: 8e3,
4956
+ maxToolCalls: 2e4
4957
+ };
4958
+ for (const agent of ACP_AGENTS) {
4959
+ FLEET_ROSTER_BUDGETS[agent.role] = { ...ACP_AGENT_BUDGET };
4960
+ }
4572
4961
  var FLEET_ROSTER_WITHACP = {
4573
4962
  ...FLEET_ROSTER,
4574
4963
  ...Object.fromEntries(ACP_AGENTS.map((a) => [a.role, a]))
@@ -4576,7 +4965,7 @@ var FLEET_ROSTER_WITHACP = {
4576
4965
 
4577
4966
  // src/coordination/delegate-tool.ts
4578
4967
  function createDelegateTool(opts) {
4579
- const defaultTimeoutMs = opts.defaultTimeoutMs ?? 30 * 60 * 1e3;
4968
+ const defaultTimeoutMs = opts.defaultTimeoutMs ?? 4 * 60 * 60 * 1e3;
4580
4969
  const rosterIds = opts.roster ? Object.keys(opts.roster) : [];
4581
4970
  const inputSchema = {
4582
4971
  type: "object",
@@ -4635,16 +5024,23 @@ function createDelegateTool(opts) {
4635
5024
  type: "number",
4636
5025
  minimum: 0,
4637
5026
  description: "Maximum estimated USD cost the subagent may incur. Unset = use the role/coordinator default."
5027
+ },
5028
+ maxHandoffs: {
5029
+ type: "number",
5030
+ minimum: 0,
5031
+ maximum: 8,
5032
+ description: "Maximum fresh-worker continuations after partial completion or budget exhaustion. Default 1; set 0 to disable. Each successor receives the prior structured/partial report and is told to continue only the remaining work."
4638
5033
  }
4639
5034
  },
4640
5035
  required: ["task"]
4641
5036
  };
4642
5037
  return {
4643
5038
  name: "delegate",
4644
- description: "Hand a discrete piece of work to a dedicated subagent and wait for its result. The subagent has its own context, its own LLM call, and its own budget \u2014 use this when a task is self-contained, would otherwise blow up your context, or benefits from a specialized role (bug-hunter, security-scanner, refactor-planner, audit-log). For free-form coding tasks (not tied to a pre-defined role), pass `name` + `task` \u2014 the subagent runs as a general-purpose coding agent with a large default budget. YOU decide how big the budget is: pass `timeoutMs`, `maxIterations`, and `maxToolCalls` sized to the actual work. There is no hidden cap forcing a 3-minute / 80-iteration limit \u2014 if a monorepo audit needs 2 hours and 500 tool calls, ask for that. Call multiple delegates in parallel through the provider's parallel-tool-call surface to fan work out across roles.",
4645
- usageHint: "Set `task` to a complete instruction. Either pick `role` from the roster (audit-log, bug-hunter, refactor-planner, security-scanner) or pass `name` to run a free-form coding agent. For non-trivial work, also pass `timeoutMs`, `maxIterations`, and `maxToolCalls`. Returns the subagent's `TaskResult` \u2014 including the textual `result`, iteration count, tool count, and duration. Requires Director mode to be active in normal CLI sessions.",
5039
+ description: "Hand a discrete piece of work to a dedicated subagent and wait for its result. The subagent has its own context, its own LLM call, and a generous, auto-extending budget. If it reports explicit partial completion or still exhausts a recoverable budget, delegate can continue the remaining work with a fresh subagent (bounded by `maxHandoffs`, default 1). Workers may ask the leader to spawn a parallel helper through the mailbox, but cannot recursively spawn agents themselves. Use specialized roster roles when possible; otherwise pass `name` + `task`. Call multiple delegates in parallel through the provider's parallel-tool-call surface for independent work.",
5040
+ usageHint: "Set `task` to a complete instruction. Either pick `role` from the roster or pass `name` for a free-form coding agent. Defaults are intentionally generous; override `timeoutMs`, `maxIterations`, `maxToolCalls`, or `maxHandoffs` only when the task warrants it. Returns the final TaskResult plus a `handoffs` history when fresh workers continued partial work. Requires Director mode in normal CLI sessions.",
4646
5041
  permission: "auto",
4647
5042
  mutating: false,
5043
+ managesOwnTimeout: true,
4648
5044
  capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
4649
5045
  inputSchema,
4650
5046
  async execute(input, _ctx, execOpts) {
@@ -4684,7 +5080,7 @@ function createDelegateTool(opts) {
4684
5080
  error: `Unknown role "${i.role}". Available: ${rosterIds.join(", ") || "(no roster configured)"}.`
4685
5081
  };
4686
5082
  }
4687
- cfg = instantiateRosterConfig(i.role, base);
5083
+ cfg = instantiateRosterConfig(i.role, base, i.timeoutMs, defaultTimeoutMs);
4688
5084
  if (i.systemPromptOverride) cfg.systemPromptOverride = i.systemPromptOverride;
4689
5085
  if (i.provider) cfg.provider = i.provider;
4690
5086
  if (i.model) cfg.model = i.model;
@@ -4723,172 +5119,175 @@ function createDelegateTool(opts) {
4723
5119
  cfg.timeoutMs = Math.max(3e4, timeoutMs - SUBAGENT_TIMEOUT_BUFFER_MS);
4724
5120
  }
4725
5121
  opts.events?.emit("delegate.started", { sessionId, target, task: i.task });
4726
- const subagentId = await director.spawn(cfg);
4727
- const taskId = await director.assign({
4728
- id: `${randomUUID4()}`,
4729
- description: i.task,
4730
- subagentId
4731
- });
4732
5122
  const dir = director;
4733
- const result = await new Promise((resolve9) => {
4734
- let settled = false;
4735
- let timer;
4736
- let offAbort = () => {
4737
- };
4738
- const finish = (value) => {
4739
- if (settled) return;
4740
- settled = true;
4741
- if (timer) clearTimeout(timer);
4742
- offTool();
4743
- offIter();
4744
- offProgress();
4745
- offAbort();
4746
- resolve9(value);
4747
- };
4748
- const arm = () => {
4749
- if (timer) clearTimeout(timer);
4750
- timer = setTimeout(() => finish({ __timeout: true }), timeoutMs);
4751
- };
4752
- const bump = (e) => {
4753
- if (e.subagentId === subagentId) arm();
4754
- };
4755
- const offTool = dir.fleet.filter("tool.executed", bump);
4756
- const offIter = dir.fleet.filter("iteration.started", bump);
4757
- const offProgress = dir.fleet.filter("tool.progress", bump);
4758
- if (abortSignal) {
4759
- const onAbortSignal = () => finish({ __aborted: true });
4760
- abortSignal.addEventListener("abort", onAbortSignal, { once: true });
4761
- offAbort = () => abortSignal.removeEventListener("abort", onAbortSignal);
4762
- if (abortSignal.aborted) onAbortSignal();
4763
- }
4764
- arm();
4765
- dir.awaitTasks([taskId]).then((r) => finish(r[0] ?? { __emptyResult: true })).catch(() => finish({ __timeout: true }));
4766
- });
4767
- if ("__aborted" in result) {
4768
- try {
4769
- await dir.terminate(subagentId);
4770
- } catch {
4771
- }
4772
- const partial2 = await readSubagentPartial(opts, subagentId);
4773
- opts.events?.emit("delegate.completed", {
4774
- sessionId,
4775
- target,
4776
- task: i.task,
4777
- ok: false,
4778
- status: "aborted",
4779
- summary: `[${target}] aborted \u2014 the run was interrupted`,
4780
- durationMs: 0,
4781
- iterations: partial2?.events ?? 0,
4782
- toolCalls: partial2?.toolUsesObserved ?? 0,
5123
+ const maxHandoffs = Math.min(8, Math.max(0, Math.floor(i.maxHandoffs ?? 1)));
5124
+ const handoffs = [];
5125
+ let delegatedTask = i.task;
5126
+ let handoffCount = 0;
5127
+ for (; ; ) {
5128
+ const attemptConfig = handoffCount === 0 ? cfg : freshHandoffConfig(cfg, i.role, handoffCount);
5129
+ const subagentId = await dir.spawn(attemptConfig);
5130
+ const taskId = await dir.assign({
5131
+ id: randomUUID4(),
5132
+ description: delegatedTask,
4783
5133
  subagentId
4784
5134
  });
4785
- return {
4786
- ok: false,
4787
- stopReason: "aborted",
4788
- error: "Delegated task aborted \u2014 the run was interrupted.",
5135
+ const result = await awaitDelegateAttempt(
5136
+ dir,
4789
5137
  subagentId,
4790
5138
  taskId,
4791
- partial: partial2
4792
- };
4793
- }
4794
- if ("__timeout" in result) {
4795
- const partial2 = await readSubagentPartial(opts, subagentId);
4796
- opts.events?.emit("delegate.completed", {
4797
- sessionId,
4798
- target,
4799
- task: i.task,
4800
- ok: false,
4801
- status: "host_timeout",
4802
- summary: `[${target}] timed out \u2014 no result within ${Math.round(timeoutMs / 1e3)}s`,
4803
- durationMs: timeoutMs,
4804
- iterations: partial2?.events ?? 0,
4805
- toolCalls: partial2?.toolUsesObserved ?? 0,
4806
- subagentId
4807
- });
4808
- return {
4809
- ok: false,
4810
- stopReason: "host_timeout",
4811
- error: `Subagent did not finish within ${timeoutMs}ms.`,
4812
- hint: "Reduce scope of the next delegate, raise timeoutMs, or use spawn_subagent + await_tasks for long-running work.",
4813
- subagentId,
4814
- taskId,
4815
- partial: partial2
4816
- };
4817
- }
4818
- if ("__emptyResult" in result) {
4819
- const partial2 = await readSubagentPartial(opts, subagentId);
5139
+ timeoutMs,
5140
+ abortSignal
5141
+ );
5142
+ if ("__aborted" in result) {
5143
+ try {
5144
+ await dir.terminate(subagentId);
5145
+ } catch {
5146
+ }
5147
+ const partial2 = await readSubagentPartial(opts, subagentId);
5148
+ opts.events?.emit("delegate.completed", {
5149
+ sessionId,
5150
+ target,
5151
+ task: i.task,
5152
+ ok: false,
5153
+ status: "aborted",
5154
+ summary: `[${target}] aborted \u2014 the run was interrupted`,
5155
+ durationMs: 0,
5156
+ iterations: partial2?.events ?? 0,
5157
+ toolCalls: partial2?.toolUsesObserved ?? 0,
5158
+ subagentId
5159
+ });
5160
+ return {
5161
+ ok: false,
5162
+ stopReason: "aborted",
5163
+ error: "Delegated task aborted \u2014 the run was interrupted.",
5164
+ subagentId,
5165
+ taskId,
5166
+ partial: partial2,
5167
+ ...handoffs.length > 0 ? { handoffs } : {}
5168
+ };
5169
+ }
5170
+ if ("__timeout" in result) {
5171
+ try {
5172
+ await dir.terminate(subagentId);
5173
+ } catch {
5174
+ }
5175
+ const partial2 = await readSubagentPartial(opts, subagentId);
5176
+ opts.events?.emit("delegate.completed", {
5177
+ sessionId,
5178
+ target,
5179
+ task: i.task,
5180
+ ok: false,
5181
+ status: "host_timeout",
5182
+ summary: `[${target}] timed out \u2014 no progress within ${Math.round(timeoutMs / 1e3)}s`,
5183
+ durationMs: timeoutMs,
5184
+ iterations: partial2?.events ?? 0,
5185
+ toolCalls: partial2?.toolUsesObserved ?? 0,
5186
+ subagentId
5187
+ });
5188
+ return {
5189
+ ok: false,
5190
+ stopReason: "host_timeout",
5191
+ error: `Subagent timed out: it did not finish or report progress within ${timeoutMs}ms.`,
5192
+ hint: "Raise timeoutMs for unusually long single operations, or split the remaining work.",
5193
+ subagentId,
5194
+ taskId,
5195
+ partial: partial2,
5196
+ ...handoffs.length > 0 ? { handoffs } : {}
5197
+ };
5198
+ }
5199
+ if ("__emptyResult" in result) {
5200
+ const partial2 = await readSubagentPartial(opts, subagentId);
5201
+ opts.events?.emit("delegate.completed", {
5202
+ sessionId,
5203
+ target,
5204
+ task: i.task,
5205
+ ok: false,
5206
+ status: "empty_result",
5207
+ summary: `[${target}] completed without a task result`,
5208
+ durationMs: 0,
5209
+ iterations: partial2?.events ?? 0,
5210
+ toolCalls: partial2?.toolUsesObserved ?? 0,
5211
+ subagentId
5212
+ });
5213
+ return {
5214
+ ok: false,
5215
+ stopReason: "error",
5216
+ error: "Director returned no task result for the delegated task.",
5217
+ hint: "Check fleet state, then retry or reassign the task.",
5218
+ subagentId,
5219
+ taskId,
5220
+ partial: partial2,
5221
+ ...handoffs.length > 0 ? { handoffs } : {}
5222
+ };
5223
+ }
5224
+ const partial = result.status === "success" ? void 0 : result.partial ? {
5225
+ lastAssistantText: result.partial.text,
5226
+ toolUsesObserved: result.toolCalls,
5227
+ events: result.iterations
5228
+ } : await readSubagentPartial(opts, subagentId);
5229
+ const continuation = continuationFor(result, partial, attemptConfig);
5230
+ if (continuation && handoffCount < maxHandoffs) {
5231
+ handoffs.push({
5232
+ fromSubagentId: result.subagentId,
5233
+ fromTaskId: result.taskId,
5234
+ status: result.status,
5235
+ errorKind: result.error?.kind,
5236
+ summary: continuation.summary,
5237
+ remainingWork: continuation.remainingWork
5238
+ });
5239
+ handoffCount += 1;
5240
+ delegatedTask = buildHandoffTask(i.task, continuation, handoffCount, maxHandoffs);
5241
+ continue;
5242
+ }
5243
+ const incomplete = result.report?.completion === "partial";
5244
+ const baseStopReason = incomplete ? "handoff_limit" : result.status === "success" ? "end_turn" : result.status === "timeout" ? "subagent_timeout" : result.status === "stopped" ? "aborted" : "budget_exhausted";
5245
+ const errorKind = result.error?.kind;
5246
+ const retryable = result.error?.retryable;
5247
+ const backoffMs = result.error?.backoffMs;
5248
+ const summary = incomplete ? `[${target}] partial checkpoint \u2014 handoff limit reached after ${handoffCount} continuation(s)` : buildDelegateSummary(i.role, result);
5249
+ let costUsd;
5250
+ try {
5251
+ costUsd = dir.snapshot().perSubagent[result.subagentId]?.cost;
5252
+ } catch {
5253
+ costUsd = void 0;
5254
+ }
4820
5255
  opts.events?.emit("delegate.completed", {
4821
5256
  sessionId,
4822
5257
  target,
4823
5258
  task: i.task,
4824
- ok: false,
4825
- status: "empty_result",
4826
- summary: `[${target}] completed without a task result`,
4827
- durationMs: 0,
4828
- iterations: partial2?.events ?? 0,
4829
- toolCalls: partial2?.toolUsesObserved ?? 0,
4830
- subagentId
5259
+ ok: result.status === "success" && !incomplete,
5260
+ status: incomplete ? "partial" : result.status,
5261
+ summary,
5262
+ durationMs: result.durationMs,
5263
+ iterations: result.iterations,
5264
+ toolCalls: result.toolCalls,
5265
+ costUsd,
5266
+ subagentId: result.subagentId
4831
5267
  });
4832
5268
  return {
4833
- ok: false,
4834
- stopReason: "error",
4835
- error: "Director returned no task result for the delegated task.",
4836
- hint: "Check fleet state with /fleet status, then retry or reassign the task.",
4837
- subagentId,
4838
- taskId,
4839
- partial: partial2
5269
+ ok: result.status === "success" && !incomplete,
5270
+ status: incomplete ? "partial" : result.status,
5271
+ stopReason: baseStopReason,
5272
+ errorKind,
5273
+ retryable,
5274
+ backoffMs,
5275
+ subagentId: result.subagentId,
5276
+ taskId: result.taskId,
5277
+ result: result.result,
5278
+ report: result.report,
5279
+ error: result.error,
5280
+ iterations: result.iterations,
5281
+ toolCalls: result.toolCalls,
5282
+ durationMs: result.durationMs,
5283
+ ...partial ? { partial } : {},
5284
+ ...handoffs.length > 0 ? { handoffs } : {},
5285
+ ...incomplete ? {
5286
+ hint: "A clean partial checkpoint remains. Reinvoke delegate with a larger maxHandoffs or assign report.remaining_work explicitly."
5287
+ } : hintForKind(errorKind, retryable, backoffMs, partial) ? { hint: hintForKind(errorKind, retryable, backoffMs, partial) } : {},
5288
+ summary
4840
5289
  };
4841
5290
  }
4842
- const baseStopReason = result.status === "success" ? "end_turn" : result.status === "timeout" ? "subagent_timeout" : result.status === "stopped" ? "aborted" : "budget_exhausted";
4843
- const partial = result.status === "success" ? void 0 : result.partial ? {
4844
- lastAssistantText: result.partial.text,
4845
- toolUsesObserved: result.toolCalls,
4846
- events: result.iterations
4847
- } : await readSubagentPartial(opts, subagentId);
4848
- const errorKind = result.error?.kind;
4849
- const retryable = result.error?.retryable;
4850
- const backoffMs = result.error?.backoffMs;
4851
- const summary = buildDelegateSummary(i.role, result);
4852
- let costUsd;
4853
- try {
4854
- costUsd = dir.snapshot().perSubagent[result.subagentId]?.cost;
4855
- } catch {
4856
- costUsd = void 0;
4857
- }
4858
- opts.events?.emit("delegate.completed", {
4859
- sessionId,
4860
- target,
4861
- task: i.task,
4862
- ok: result.status === "success",
4863
- status: result.status,
4864
- summary,
4865
- durationMs: result.durationMs,
4866
- iterations: result.iterations,
4867
- toolCalls: result.toolCalls,
4868
- costUsd,
4869
- subagentId: result.subagentId
4870
- });
4871
- return {
4872
- ok: result.status === "success",
4873
- status: result.status,
4874
- stopReason: baseStopReason,
4875
- errorKind,
4876
- retryable,
4877
- backoffMs,
4878
- subagentId: result.subagentId,
4879
- taskId: result.taskId,
4880
- result: result.result,
4881
- report: result.report,
4882
- error: result.error,
4883
- iterations: result.iterations,
4884
- toolCalls: result.toolCalls,
4885
- durationMs: result.durationMs,
4886
- ...partial ? { partial } : {},
4887
- ...hintForKind(errorKind, retryable, backoffMs, partial) ? { hint: hintForKind(errorKind, retryable, backoffMs, partial) } : {},
4888
- // Summary is included so callers (TUI, CLI renderer) can surface
4889
- // it as a chat history line — the LLM also sees it for continuity.
4890
- summary
4891
- };
4892
5291
  } catch (err) {
4893
5292
  const message = toErrorMessage(err);
4894
5293
  opts.events?.emit("delegate.completed", {
@@ -4911,10 +5310,103 @@ function createDelegateTool(opts) {
4911
5310
  }
4912
5311
  };
4913
5312
  }
4914
- function instantiateRosterConfig(role, base) {
5313
+ async function awaitDelegateAttempt(director, subagentId, taskId, timeoutMs, abortSignal) {
5314
+ return new Promise((resolve9) => {
5315
+ let settled = false;
5316
+ let timer;
5317
+ let offAbort = () => {
5318
+ };
5319
+ const finish = (value) => {
5320
+ if (settled) return;
5321
+ settled = true;
5322
+ if (timer) clearTimeout(timer);
5323
+ offTool();
5324
+ offIter();
5325
+ offProgress();
5326
+ offAbort();
5327
+ resolve9(value);
5328
+ };
5329
+ const arm = () => {
5330
+ if (timer) clearTimeout(timer);
5331
+ timer = setTimeout(() => finish({ __timeout: true }), timeoutMs);
5332
+ };
5333
+ const bump = (event) => {
5334
+ if (event.subagentId === subagentId) arm();
5335
+ };
5336
+ const offTool = director.fleet.filter("tool.executed", bump);
5337
+ const offIter = director.fleet.filter("iteration.started", bump);
5338
+ const offProgress = director.fleet.filter("tool.progress", bump);
5339
+ if (abortSignal) {
5340
+ const onAbort = () => finish({ __aborted: true });
5341
+ abortSignal.addEventListener("abort", onAbort, { once: true });
5342
+ offAbort = () => abortSignal.removeEventListener("abort", onAbort);
5343
+ if (abortSignal.aborted) onAbort();
5344
+ }
5345
+ arm();
5346
+ director.awaitTasks([taskId]).then((results) => finish(results[0] ?? { __emptyResult: true })).catch(() => finish({ __timeout: true }));
5347
+ });
5348
+ }
5349
+ function freshHandoffConfig(cfg, role, handoffCount) {
5350
+ return {
5351
+ ...cfg,
5352
+ id: role ? `${role}-${randomUUID4().slice(0, 8)}` : `${cfg.name.toLowerCase().replace(/[^a-z0-9]+/g, "-") || "subagent"}-handoff-${handoffCount}-${randomUUID4().slice(0, 6)}`
5353
+ };
5354
+ }
5355
+ function continuationFor(result, partial, config) {
5356
+ if (result.report?.completion === "partial" && result.report.remaining_work) {
5357
+ return {
5358
+ summary: result.report.summary,
5359
+ remainingWork: result.report.remaining_work,
5360
+ partialText: typeof result.result === "string" ? result.result : partial?.lastAssistantText
5361
+ };
5362
+ }
5363
+ const budgetKinds = /* @__PURE__ */ new Set([
5364
+ "budget_iterations",
5365
+ "budget_tool_calls",
5366
+ "budget_tokens",
5367
+ "budget_cost",
5368
+ "budget_timeout"
5369
+ ]);
5370
+ const partialText = result.partial?.text ?? partial?.lastAssistantText;
5371
+ const mayHaveIsolatedWrites = config.worktree !== false && config.worktree !== "off" && (!config.tools || config.tools.some(
5372
+ (tool) => ["write", "edit", "replace", "patch", "bash", "exec", "install", "format"].includes(tool)
5373
+ ));
5374
+ if (!mayHaveIsolatedWrites && result.status !== "success" && result.error?.kind && budgetKinds.has(result.error.kind) && partialText) {
5375
+ return {
5376
+ summary: `Prior worker stopped at ${result.error.kind} after ${result.iterations} iterations and ${result.toolCalls} tool calls.`,
5377
+ remainingWork: "Inspect the existing workspace and finish only the work that remains from the original task.",
5378
+ partialText
5379
+ };
5380
+ }
5381
+ return void 0;
5382
+ }
5383
+ function buildHandoffTask(originalTask, continuation, handoffCount, maxHandoffs) {
5384
+ const partial = continuation.partialText?.trim().slice(-6e3);
5385
+ return [
5386
+ `Continue an oversized delegated task as fresh worker ${handoffCount} of ${maxHandoffs}.`,
5387
+ "Do not blindly repeat completed actions. Inspect the current workspace, git diff, tests, and any files named below before changing anything.",
5388
+ 'If the remaining work is still too large, stop at a clean checkpoint and call submit_result with completion="partial" plus concrete remaining_work.',
5389
+ "If parallel help would materially improve the outcome and mail_send is available, send the leader an `ask` that names the exact helper task; do not spawn agents yourself.",
5390
+ `Original task:
5391
+ ${originalTask}`,
5392
+ `Prior checkpoint summary:
5393
+ ${continuation.summary}`,
5394
+ `Remaining work:
5395
+ ${continuation.remainingWork}`,
5396
+ partial ? `Prior worker's last useful output:
5397
+ ${partial}` : ""
5398
+ ].filter(Boolean).join("\n\n");
5399
+ }
5400
+ function instantiateRosterConfig(role, base, requestedTimeoutMs, defaultTimeoutMs) {
4915
5401
  const withBudget = applyRosterBudget({ ...base, role });
5402
+ const rosterTimeoutMs = FLEET_ROSTER_BUDGETS[role]?.timeoutMs;
4916
5403
  return {
4917
5404
  ...withBudget,
5405
+ // Without an explicit host wait, apply the role's tuned multi-hour
5406
+ // wall-clock budget. With an explicit wait, leave this unset so the buffer
5407
+ // logic above derives a slightly shorter child budget without clamping the
5408
+ // role defaults used by ordinary calls.
5409
+ timeoutMs: requestedTimeoutMs === void 0 ? rosterTimeoutMs ?? defaultTimeoutMs : void 0,
4918
5410
  // Give each spawn a fresh id so parallel or repeated delegates
4919
5411
  // can use the same role safely.
4920
5412
  id: `${role}-${randomUUID4().slice(0, 8)}`
@@ -5100,13 +5592,13 @@ function makeDependencyWatcherConfig(opts) {
5100
5592
  const globPatterns = patterns.filter((p) => p.includes("*"));
5101
5593
  const plainPatterns = patterns.filter((p) => !p.includes("*"));
5102
5594
  function matchesPattern(filePath) {
5103
- const basename8 = filePath.split("/").pop()?.split("\\").pop() ?? "";
5104
- if (plainPatterns.includes(basename8)) return true;
5595
+ const basename7 = filePath.split("/").pop()?.split("\\").pop() ?? "";
5596
+ if (plainPatterns.includes(basename7)) return true;
5105
5597
  for (const gp of globPatterns) {
5106
5598
  const regex = new RegExp(
5107
5599
  "^" + gp.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"
5108
5600
  );
5109
- if (regex.test(basename8)) return true;
5601
+ if (regex.test(basename7)) return true;
5110
5602
  }
5111
5603
  return false;
5112
5604
  }
@@ -9011,7 +9503,7 @@ var Director = class _Director {
9011
9503
  this.subagentBaseline = opts.subagentBaseline ?? DEFAULT_SUBAGENT_BASELINE;
9012
9504
  this.taskResultNotifier = opts.taskResultNotifier;
9013
9505
  this.subagentIdleTimeoutMs = typeof opts.subagentIdleTimeoutMs === "number" && Number.isFinite(opts.subagentIdleTimeoutMs) && opts.subagentIdleTimeoutMs >= 0 ? opts.subagentIdleTimeoutMs : void 0;
9014
- this.retireSubagentOnTaskComplete = opts.retireSubagentOnTaskComplete ?? false;
9506
+ this.retireSubagentOnTaskComplete = opts.retireSubagentOnTaskComplete ?? true;
9015
9507
  this.sharedScratchpadPath = opts.sharedScratchpadPath ?? null;
9016
9508
  this.maxSpawns = opts.maxSpawns ?? Number.POSITIVE_INFINITY;
9017
9509
  this.maxSpawnDepth = opts.fleetManager?.maxSpawnDepth ?? resolveMaxSpawnDepth(opts.maxSpawnDepth);
@@ -9022,7 +9514,7 @@ var Director = class _Director {
9022
9514
  this.dispatchClassifier = opts.dispatchClassifier;
9023
9515
  this.maxFleetCostUsd = opts.directorBudget?.maxCostUsd ?? Number.POSITIVE_INFINITY;
9024
9516
  this.maxFleetTokens = opts.directorBudget?.maxTokens ?? Number.POSITIVE_INFINITY;
9025
- this.maxBudgetExtensions = opts.maxBudgetExtensions ?? 5;
9517
+ this.maxBudgetExtensions = opts.maxBudgetExtensions ?? 12;
9026
9518
  this.maxLeaderContextLoad = opts.maxLeaderContextLoad ?? 0.85;
9027
9519
  this.maxContext = opts.maxContext ?? 128e3;
9028
9520
  this.modelMatrix = opts.modelMatrix;
@@ -9271,7 +9763,7 @@ var Director = class _Director {
9271
9763
  payload.deny();
9272
9764
  return;
9273
9765
  }
9274
- if (decision.optionId === "stop" || /\bstop\b/i.test(decision.text)) {
9766
+ if (decision.optionId === "stop") {
9275
9767
  payload.deny();
9276
9768
  return;
9277
9769
  }
@@ -9799,6 +10291,7 @@ var Director = class _Director {
9799
10291
  await this.stateCheckpoint.flush().catch((err) => this.logShutdownError("state_checkpoint_flush", err));
9800
10292
  await this.stateCheckpoint.releaseLock().catch((err) => this.logShutdownError("state_checkpoint_lock_release", err));
9801
10293
  }
10294
+ this.largeAnswerStore.clear();
9802
10295
  }
9803
10296
  /**
9804
10297
  * Funnel for shutdown-phase errors. We can't throw — `shutdown()` is
@@ -10000,9 +10493,18 @@ var Director = class _Director {
10000
10493
  }
10001
10494
  async terminate(subagentId) {
10002
10495
  await this.coordinator.stop(subagentId);
10496
+ void this.remove(subagentId).catch(
10497
+ (err) => this.logShutdownError("terminate_remove", err)
10498
+ );
10003
10499
  }
10004
10500
  async terminateAll() {
10501
+ const ids = this.status().subagents.map((s) => s.id);
10005
10502
  await this.coordinator.stopAll();
10503
+ for (const id of ids) {
10504
+ void this.remove(id).catch(
10505
+ (err) => this.logShutdownError("terminate_all_remove", err)
10506
+ );
10507
+ }
10006
10508
  }
10007
10509
  async remove(subagentId) {
10008
10510
  this.clearSubagentIdleRetirement(subagentId);
@@ -10042,7 +10544,7 @@ var Director = class _Director {
10042
10544
  const timer = setTimeout(() => {
10043
10545
  this.subagentIdleTimers.delete(subagentId);
10044
10546
  const entry = this.coordinator.getStatus().subagents.find((a) => a.id === subagentId);
10045
- if (!entry || entry.status !== "idle") return;
10547
+ if (entry?.status !== "idle") return;
10046
10548
  if (this.coordinator.listPendingTasks().some((task) => task.subagentId === subagentId)) {
10047
10549
  this.armSubagentIdleRetirement(subagentId, this.subagentIdleTimeoutMs);
10048
10550
  return;
@@ -11634,6 +12136,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
11634
12136
  projectRoot;
11635
12137
  checkpointCas;
11636
12138
  isSessionInUse;
12139
+ logger;
11637
12140
  /**
11638
12141
  * In-memory cache for load() results, keyed by session ID. The cache is
11639
12142
  * invalidated when the file's mtimeMs or size changes (indicating the
@@ -11659,6 +12162,18 @@ var DefaultSessionStore = class _DefaultSessionStore {
11659
12162
  this.events = opts.events;
11660
12163
  this.secretScrubber = opts.secretScrubber;
11661
12164
  this.isSessionInUse = opts.isSessionInUse;
12165
+ this.logger = opts.logger;
12166
+ }
12167
+ /**
12168
+ * Emit a structured warning. Uses the configured Logger when available;
12169
+ * falls back to console.warn(JSON) so warnings are never silently dropped.
12170
+ */
12171
+ logWarn(msg, ctx) {
12172
+ if (this.logger) {
12173
+ this.logger.warn(msg, ctx);
12174
+ } else {
12175
+ console.warn(JSON.stringify({ ...ctx, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
12176
+ }
11662
12177
  }
11663
12178
  /**
11664
12179
  * Clear the load() cache. Useful for testing or when the caller knows
@@ -11760,12 +12275,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
11760
12275
  this.emitWrite(id, file, "create", "success", Date.now() - t0);
11761
12276
  return writer;
11762
12277
  } catch (err) {
11763
- await handle.close().catch((e) => console.warn(JSON.stringify({
11764
- level: "warn",
11765
- event: "session_store.handle_close_failed",
11766
- message: e instanceof Error ? e.message : String(e),
11767
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
11768
- })));
12278
+ await handle.close().catch((e) => this.logWarn(
12279
+ "Session handle close failed",
12280
+ { event: "session_store.handle_close_failed", message: e instanceof Error ? e.message : String(e) }
12281
+ ));
11769
12282
  this.emitError(id, file, "create", toErrorMessage(err), true);
11770
12283
  throw err;
11771
12284
  }
@@ -11896,12 +12409,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
11896
12409
  this.emitWrite(id, file, "resume", "success", Date.now() - t0);
11897
12410
  return { writer, data: resumedData };
11898
12411
  } catch (err) {
11899
- await handle.close().catch((e) => console.warn(JSON.stringify({
11900
- level: "warn",
11901
- event: "session_store.handle_close_failed",
11902
- message: e instanceof Error ? e.message : String(e),
11903
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
11904
- })));
12412
+ await handle.close().catch((e) => this.logWarn(
12413
+ "Session handle close failed",
12414
+ { event: "session_store.handle_close_failed", message: e instanceof Error ? e.message : String(e) }
12415
+ ));
11905
12416
  this.emitError(id, file, "resume", toErrorMessage(err), true);
11906
12417
  throw err;
11907
12418
  }
@@ -12522,13 +13033,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
12522
13033
  await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
12523
13034
  const msg = toErrorMessage(err);
12524
13035
  this.emitError(id, manifest, "summary_fallback", msg, true);
12525
- console.warn(JSON.stringify({
12526
- level: "warn",
12527
- event: "session_store.manifest_write_failed",
12528
- sessionId: id,
12529
- message: msg,
12530
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
12531
- }));
13036
+ this.logWarn(
13037
+ "Session manifest write failed",
13038
+ { event: "session_store.manifest_write_failed", sessionId: id, message: msg }
13039
+ );
12532
13040
  });
12533
13041
  outcome = "failure";
12534
13042
  errorMsg = "summary fallback \xE2\u20AC\u201D manifest rebuilt";
@@ -13320,6 +13828,148 @@ import { randomUUID as randomUUID10 } from "node:crypto";
13320
13828
  import * as fsp11 from "node:fs/promises";
13321
13829
  import * as path14 from "node:path";
13322
13830
 
13831
+ // src/coordination/mailbox-constants.ts
13832
+ var AGENT_STALE_MS = 6e4;
13833
+ var CLIENT_STALE_MS = 6e4;
13834
+ var HEARTBEAT_THROTTLE_MS = 5e3;
13835
+ var REGISTRY_CACHE_TTL_MS = 2e3;
13836
+ var LINE_SEPARATOR = "\n";
13837
+ var MESSAGE_CACHE_MAX_ENTRIES = 1e4;
13838
+ var AUTO_COMPACT_INTERVAL_MS = 3e5;
13839
+ var AUTO_COMPACT_READ_MAX_AGE_MS = 6e5;
13840
+ var AUTO_COMPACT_DEFAULT_TTL_MS = 864e5;
13841
+
13842
+ // src/coordination/mailbox-message-codec.ts
13843
+ var MESSAGE_TYPES = /* @__PURE__ */ new Set([
13844
+ "note",
13845
+ "ask",
13846
+ "assign",
13847
+ "steer",
13848
+ "btw",
13849
+ "broadcast",
13850
+ "status",
13851
+ "result",
13852
+ "review",
13853
+ "control"
13854
+ ]);
13855
+ var PRIORITIES = /* @__PURE__ */ new Set(["low", "normal", "high"]);
13856
+ var TASK_STATUSES = /* @__PURE__ */ new Set([
13857
+ "pending",
13858
+ "in_progress",
13859
+ "completed",
13860
+ "failed",
13861
+ "idle",
13862
+ "running",
13863
+ "streaming",
13864
+ "waiting_user",
13865
+ "error",
13866
+ "offline",
13867
+ "busy"
13868
+ ]);
13869
+ function isRecord3(value) {
13870
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13871
+ }
13872
+ function requiredString(record, key) {
13873
+ const value = record[key];
13874
+ if (typeof value !== "string") {
13875
+ throw new TypeError(`mailbox message field "${key}" must be a string`);
13876
+ }
13877
+ return value;
13878
+ }
13879
+ function optionalString2(record, key) {
13880
+ const value = record[key];
13881
+ if (value === void 0) return {};
13882
+ if (typeof value !== "string") {
13883
+ throw new TypeError(`mailbox message field "${key}" must be a string when present`);
13884
+ }
13885
+ return { [key]: value };
13886
+ }
13887
+ function parseMessageType(value) {
13888
+ if (value === "info") return "note";
13889
+ if (value === "task") return "assign";
13890
+ if (typeof value === "string" && MESSAGE_TYPES.has(value)) {
13891
+ return value;
13892
+ }
13893
+ throw new TypeError('mailbox message field "type" is invalid');
13894
+ }
13895
+ function normalizeMailboxMessageType(value) {
13896
+ return parseMessageType(value);
13897
+ }
13898
+ function parsePriority(value) {
13899
+ if (typeof value === "string" && PRIORITIES.has(value)) {
13900
+ return value;
13901
+ }
13902
+ if (typeof value === "string") return "normal";
13903
+ throw new TypeError('mailbox message field "priority" must be a string');
13904
+ }
13905
+ function parseReadReceipts(record, to) {
13906
+ const value = record["readBy"];
13907
+ if (value === void 0) {
13908
+ const legacyReadAt = record["readAt"];
13909
+ return record["read"] === true && typeof legacyReadAt === "string" ? { [to || "unknown"]: legacyReadAt } : {};
13910
+ }
13911
+ if (!isRecord3(value)) {
13912
+ throw new TypeError('mailbox message field "readBy" must be an object');
13913
+ }
13914
+ const receipts = {};
13915
+ for (const [agentId, timestamp] of Object.entries(value)) {
13916
+ if (typeof timestamp !== "string") {
13917
+ throw new TypeError("mailbox message read receipt timestamps must be strings");
13918
+ }
13919
+ receipts[agentId] = timestamp;
13920
+ }
13921
+ return receipts;
13922
+ }
13923
+ function parseTaskContext(value) {
13924
+ if (value === void 0) return void 0;
13925
+ if (!isRecord3(value)) {
13926
+ throw new TypeError('mailbox message field "taskContext" must be an object');
13927
+ }
13928
+ const status = value["status"];
13929
+ if (status !== void 0 && (typeof status !== "string" || !TASK_STATUSES.has(status))) {
13930
+ throw new TypeError("mailbox message taskContext status is invalid");
13931
+ }
13932
+ return {
13933
+ ...optionalString2(value, "agentRole"),
13934
+ ...optionalString2(value, "agentName"),
13935
+ ...optionalString2(value, "taskId"),
13936
+ ...status === void 0 ? {} : { status }
13937
+ };
13938
+ }
13939
+ function parseMailboxMessage(value) {
13940
+ if (!isRecord3(value)) throw new TypeError("mailbox message must be an object");
13941
+ const to = value["to"] === void 0 ? "" : requiredString(value, "to");
13942
+ const completed = value["completed"];
13943
+ if (typeof completed !== "boolean") {
13944
+ throw new TypeError('mailbox message field "completed" must be a boolean');
13945
+ }
13946
+ const taskContext = parseTaskContext(value["taskContext"]);
13947
+ return {
13948
+ id: requiredString(value, "id"),
13949
+ from: requiredString(value, "from"),
13950
+ to,
13951
+ type: parseMessageType(value["type"]),
13952
+ subject: requiredString(value, "subject"),
13953
+ body: requiredString(value, "body"),
13954
+ priority: parsePriority(value["priority"]),
13955
+ readBy: parseReadReceipts(value, to),
13956
+ completed,
13957
+ timestamp: requiredString(value, "timestamp"),
13958
+ ...optionalString2(value, "completedBy"),
13959
+ ...optionalString2(value, "outcome"),
13960
+ ...optionalString2(value, "completedAt"),
13961
+ ...optionalString2(value, "deletedAt"),
13962
+ ...optionalString2(value, "deletedBy"),
13963
+ ...optionalString2(value, "replyTo"),
13964
+ ...optionalString2(value, "senderSessionId"),
13965
+ ...optionalString2(value, "expiresAt"),
13966
+ ...taskContext === void 0 ? {} : { taskContext }
13967
+ };
13968
+ }
13969
+ function parseMailboxMessageLine(line) {
13970
+ return parseMailboxMessage(JSON.parse(line));
13971
+ }
13972
+
13323
13973
  // src/coordination/mailbox-types.ts
13324
13974
  function normalizeRecipient(to) {
13325
13975
  return to.trim().toLowerCase() === "all" ? "*" : to.trim();
@@ -13328,13 +13978,6 @@ function normalizeRecipient(to) {
13328
13978
  // src/coordination/global-mailbox.ts
13329
13979
  var MAILBOX_FILE = "_mailbox.jsonl";
13330
13980
  var CLIENT_REGISTRY_FILE = "_mailbox.clients.json";
13331
- var AGENT_STALE_MS = 6e4;
13332
- var AGENT_PURGE_MS = 864e5;
13333
- var CLIENT_STALE_MS = 6e4;
13334
- var HEARTBEAT_THROTTLE_MS = 5e3;
13335
- var REGISTRY_CACHE_TTL_MS = 2e3;
13336
- var LINE_SEPARATOR = "\n";
13337
- var MESSAGE_CACHE_MAX_ENTRIES = 1e4;
13338
13981
  function resolveProjectDir(projectRoot, globalRoot) {
13339
13982
  return path14.join(globalRoot, "projects", projectSlug(projectRoot));
13340
13983
  }
@@ -13349,6 +13992,12 @@ var GlobalMailbox = class {
13349
13992
  _events;
13350
13993
  /** Optional HQ publisher for cross-project command-center telemetry. */
13351
13994
  _hqPublisher;
13995
+ /**
13996
+ * Optional SSE event emitter for real-time push to HTTP bridge clients.
13997
+ * When present, `send`/`ack`/`softDelete`/`restore` emit events that
13998
+ * connected SSE subscribers receive instantly.
13999
+ */
14000
+ eventEmitter;
13352
14001
  /**
13353
14002
  * Local cache of the agent registry to avoid re-reading on every call.
13354
14003
  * Time-bounded: the registry file is shared ACROSS PROCESSES (that's the
@@ -13394,17 +14043,44 @@ var GlobalMailbox = class {
13394
14043
  * that follows the read. Pattern mirrors `DefaultMemoryStore.runSerialized`.
13395
14044
  */
13396
14045
  _readChain = Promise.resolve([]);
14046
+ /**
14047
+ * Recipient → Set of indices into `_messageCache`. Maintained alongside
14048
+ * the cache so `query({ to })` and `unreadCount(agentId)` can skip the
14049
+ * O(N) full scan and iterate only the messages addressed to the target
14050
+ * recipient (plus broadcasts `*`).
14051
+ *
14052
+ * The index is rebuilt whenever the cache array is replaced
14053
+ * (`_setMessageCache`), extended when new messages are pushed
14054
+ * (`_pushToCache`, `_readNewMessagesOnly`), and cleared on `close()`.
14055
+ * It is `null` when the cache itself is null (> MESSAGE_CACHE_MAX_ENTRIES
14056
+ * or never populated).
14057
+ *
14058
+ * Safety: array indices are valid only as long as the cache array
14059
+ * reference hasn't changed. Since we rebuild the index in the same
14060
+ * synchronous step that replaces the cache, a reader that calls
14061
+ * `_readMessagesCached()` either gets the old array + old index, or
14062
+ * the new array + new index — never a mismatch.
14063
+ */
14064
+ _recipientIndex = null;
14065
+ /**
14066
+ * Sender → Set of indices into `_messageCache`. Same lifecycle and
14067
+ * safety properties as `_recipientIndex`, but keyed on `msg.from`
14068
+ * so `query({ from })` can skip the full scan.
14069
+ */
14070
+ _senderIndex = null;
13397
14071
  /**
13398
14072
  * @param projectDir — `~/.wrongstack/projects/<slug>/`
13399
14073
  * @param events — optional EventBus for real-time TUI/WebUI notifications
13400
14074
  * @param hqPublisher — optional HQ publisher, or getter, for cross-project telemetry
14075
+ * @param eventEmitter — optional SSE event emitter for HTTP bridge push
13401
14076
  */
13402
- constructor(projectDir, events, hqPublisher) {
14077
+ constructor(projectDir, events, hqPublisher, eventEmitter) {
13403
14078
  this.messagePath = path14.join(projectDir, MAILBOX_FILE);
13404
14079
  this.registryPath = path14.join(projectDir, "_mailbox.registry.json");
13405
14080
  this.clientRegistryPath = path14.join(projectDir, CLIENT_REGISTRY_FILE);
13406
14081
  this._events = events;
13407
14082
  this._hqPublisher = hqPublisher;
14083
+ this.eventEmitter = eventEmitter;
13408
14084
  }
13409
14085
  get hqMailboxId() {
13410
14086
  return `${path14.basename(path14.dirname(this.messagePath))}:mailbox`;
@@ -13433,7 +14109,7 @@ var GlobalMailbox = class {
13433
14109
  // "all" is an accepted spelling of the broadcast address — canonical
13434
14110
  // form on disk is '*' so every query/checker matches it.
13435
14111
  to: normalizeRecipient(input.to),
13436
- type: input.type,
14112
+ type: normalizeMailboxMessageType(input.type),
13437
14113
  subject: input.subject,
13438
14114
  body: input.body,
13439
14115
  priority: input.priority ?? "normal",
@@ -13441,7 +14117,8 @@ var GlobalMailbox = class {
13441
14117
  completed: false,
13442
14118
  timestamp: now,
13443
14119
  replyTo: input.replyTo,
13444
- taskContext: input.taskContext
14120
+ taskContext: input.taskContext,
14121
+ expiresAt: input.ttlMs !== void 0 ? new Date(Date.now() + input.ttlMs).toISOString() : void 0
13445
14122
  };
13446
14123
  const line = JSON.stringify(msg) + LINE_SEPARATOR;
13447
14124
  await fsp11.mkdir(path14.dirname(this.messagePath), { recursive: true });
@@ -13456,21 +14133,49 @@ var GlobalMailbox = class {
13456
14133
  message: msg
13457
14134
  });
13458
14135
  this.publishHqMailboxSnapshot();
14136
+ this._events?.emitCustom("mailbox.message_sent", {
14137
+ messageId: msg.id,
14138
+ from: msg.from,
14139
+ to: msg.to,
14140
+ type: msg.type,
14141
+ subject: msg.subject
14142
+ });
14143
+ this.eventEmitter?.emit({
14144
+ type: "message.sent",
14145
+ messageId: msg.id,
14146
+ from: msg.from,
14147
+ to: msg.to,
14148
+ timestamp: now
14149
+ });
13459
14150
  return msg;
13460
14151
  }
13461
14152
  async query(q) {
14153
+ const queryType = q.type === void 0 ? void 0 : normalizeMailboxMessageType(q.type);
13462
14154
  const all = await this._readMessagesCached();
13463
14155
  const limit = q.limit ?? 50;
13464
14156
  const order = q.minPriority !== void 0 ? { low: 0, normal: 1, high: 2 } : null;
13465
14157
  const minPriorityRank = order && q.minPriority !== void 0 ? order[q.minPriority] : 0;
13466
14158
  const out = [];
13467
- for (let i = 0; i < all.length; i++) {
13468
- const m = all[i];
14159
+ let candidates;
14160
+ if (q.to !== void 0 && this._recipientIndex !== null && q.from === void 0 && q.type === void 0 && q.minPriority === void 0) {
14161
+ const indices = /* @__PURE__ */ new Set();
14162
+ const direct = this._recipientIndex.get(q.to);
14163
+ if (direct !== void 0) for (const i of direct) indices.add(i);
14164
+ const broadcasts = this._recipientIndex.get("*");
14165
+ if (broadcasts !== void 0) for (const i of broadcasts) indices.add(i);
14166
+ candidates = [...indices].sort((a, b) => a - b).map((i) => all[i]);
14167
+ } else if (q.from !== void 0 && q.to === void 0 && this._senderIndex !== null && q.type === void 0 && q.minPriority === void 0) {
14168
+ const indices = this._senderIndex.get(q.from);
14169
+ candidates = indices !== void 0 ? [...indices].sort((a, b) => a - b).map((i) => all[i]) : [];
14170
+ } else {
14171
+ candidates = all;
14172
+ }
14173
+ for (const m of candidates) {
13469
14174
  if (q.to !== void 0 && m.to !== q.to && m.to !== "*") continue;
13470
14175
  if (q.from !== void 0 && m.from !== q.from) continue;
13471
14176
  if (q.unreadBy !== void 0 && q.unreadBy in m.readBy) continue;
13472
14177
  if (q.incompleteOnly && m.completed) continue;
13473
- if (q.type !== void 0 && m.type !== q.type) continue;
14178
+ if (queryType !== void 0 && m.type !== queryType) continue;
13474
14179
  if (order !== null && (order[m.priority] ?? 1) < minPriorityRank) {
13475
14180
  continue;
13476
14181
  }
@@ -13528,6 +14233,13 @@ var GlobalMailbox = class {
13528
14233
  action: message.completed ? "message.completed" : "message.read",
13529
14234
  message
13530
14235
  });
14236
+ this.eventEmitter?.emit({
14237
+ type: "message.acked",
14238
+ messageId: message.id,
14239
+ from: message.from,
14240
+ to: message.to,
14241
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
14242
+ });
13531
14243
  }
13532
14244
  if (updated.length > 0) this.publishHqMailboxSnapshot();
13533
14245
  return updated;
@@ -13535,10 +14247,22 @@ var GlobalMailbox = class {
13535
14247
  async unreadCount(forAgentId) {
13536
14248
  const all = await this._readMessagesCached();
13537
14249
  let count = 0;
13538
- for (let i = 0; i < all.length; i++) {
13539
- const m = all[i];
13540
- if ((m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed) {
13541
- count++;
14250
+ if (this._recipientIndex !== null) {
14251
+ const indices = /* @__PURE__ */ new Set();
14252
+ const direct = this._recipientIndex.get(forAgentId);
14253
+ if (direct !== void 0) for (const i of direct) indices.add(i);
14254
+ const broadcasts = this._recipientIndex.get("*");
14255
+ if (broadcasts !== void 0) for (const i of broadcasts) indices.add(i);
14256
+ for (const i of indices) {
14257
+ const m = all[i];
14258
+ if (!(forAgentId in m.readBy) && !m.completed) count++;
14259
+ }
14260
+ } else {
14261
+ for (let i = 0; i < all.length; i++) {
14262
+ const m = all[i];
14263
+ if ((m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed) {
14264
+ count++;
14265
+ }
13542
14266
  }
13543
14267
  }
13544
14268
  return count;
@@ -13548,6 +14272,7 @@ var GlobalMailbox = class {
13548
14272
  await withFileLock(this.messagePath, async () => {
13549
14273
  const all = await this._readMessagesFresh();
13550
14274
  const now = (/* @__PURE__ */ new Date()).toISOString();
14275
+ let changed = false;
13551
14276
  for (const m of all) {
13552
14277
  if (m.id !== mailId) continue;
13553
14278
  if (m.deletedAt !== void 0) {
@@ -13557,19 +14282,30 @@ var GlobalMailbox = class {
13557
14282
  m.deletedAt = now;
13558
14283
  m.deletedBy = by;
13559
14284
  updated = m;
14285
+ changed = true;
14286
+ }
14287
+ if (changed) {
14288
+ const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
14289
+ await fsp11.writeFile(this.messagePath, serialized, "utf8");
13560
14290
  }
13561
- const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
13562
- await fsp11.writeFile(this.messagePath, serialized, "utf8");
13563
14291
  const { mtimeMs, size } = await this._statMessageFile();
13564
14292
  this._setMessageCache(all, mtimeMs, size);
13565
14293
  });
13566
14294
  if (updated !== null) {
14295
+ const msg = updated;
13567
14296
  this.publishHqMailboxEvent({
13568
14297
  mailboxId: this.hqMailboxId,
13569
14298
  action: "message.updated",
13570
- message: updated
14299
+ message: msg
13571
14300
  });
13572
14301
  this.publishHqMailboxSnapshot();
14302
+ this.eventEmitter?.emit({
14303
+ type: "message.deleted",
14304
+ messageId: msg.id,
14305
+ from: msg.from,
14306
+ to: msg.to,
14307
+ timestamp: msg.deletedAt ?? (/* @__PURE__ */ new Date()).toISOString()
14308
+ });
13573
14309
  }
13574
14310
  return updated;
13575
14311
  }
@@ -13577,24 +14313,40 @@ var GlobalMailbox = class {
13577
14313
  let updated = null;
13578
14314
  await withFileLock(this.messagePath, async () => {
13579
14315
  const all = await this._readMessagesFresh();
14316
+ let changed = false;
13580
14317
  for (const m of all) {
13581
14318
  if (m.id !== mailId) continue;
14319
+ if (m.deletedAt === void 0 && m.deletedBy === void 0) {
14320
+ updated = m;
14321
+ continue;
14322
+ }
13582
14323
  delete m.deletedAt;
13583
14324
  delete m.deletedBy;
13584
14325
  updated = m;
14326
+ changed = true;
14327
+ }
14328
+ if (changed) {
14329
+ const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
14330
+ await fsp11.writeFile(this.messagePath, serialized, "utf8");
13585
14331
  }
13586
- const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
13587
- await fsp11.writeFile(this.messagePath, serialized, "utf8");
13588
14332
  const { mtimeMs, size } = await this._statMessageFile();
13589
14333
  this._setMessageCache(all, mtimeMs, size);
13590
14334
  });
13591
14335
  if (updated !== null) {
14336
+ const msg = updated;
13592
14337
  this.publishHqMailboxEvent({
13593
14338
  mailboxId: this.hqMailboxId,
13594
14339
  action: "message.updated",
13595
- message: updated
14340
+ message: msg
13596
14341
  });
13597
14342
  this.publishHqMailboxSnapshot();
14343
+ this.eventEmitter?.emit({
14344
+ type: "message.restored",
14345
+ messageId: msg.id,
14346
+ from: msg.from,
14347
+ to: msg.to,
14348
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
14349
+ });
13598
14350
  }
13599
14351
  return updated;
13600
14352
  }
@@ -13727,8 +14479,19 @@ var GlobalMailbox = class {
13727
14479
  }
13728
14480
  async getAgentStatuses() {
13729
14481
  await this._ensureRegistry();
13730
- const registry = await this._readRegistry();
14482
+ let registry = await this._readRegistry();
14483
+ const before = registry.size;
13731
14484
  this._pruneStaleInPlace(registry);
14485
+ if (registry.size < before) {
14486
+ await withFileLock(this.registryPath, async () => {
14487
+ const fresh = await this._readRegistry({ fresh: true });
14488
+ this._pruneStaleInPlace(fresh);
14489
+ this._registryCache = fresh;
14490
+ this._registryCacheAt = Date.now();
14491
+ await this._writeRegistry(fresh);
14492
+ registry = fresh;
14493
+ });
14494
+ }
13732
14495
  const now = Date.now();
13733
14496
  return Array.from(registry.values()).map((a) => ({
13734
14497
  agentId: a.agentId,
@@ -13780,6 +14543,20 @@ var GlobalMailbox = class {
13780
14543
  });
13781
14544
  this.publishHqMailboxSnapshot();
13782
14545
  }
14546
+ async deregisterClient(clientId) {
14547
+ await this._ensureClientRegistry();
14548
+ await withFileLock(this.clientRegistryPath, async () => {
14549
+ const registry = await this._readClientRegistry({ fresh: true });
14550
+ this._pruneStaleClientsInPlace(registry);
14551
+ registry.delete(clientId);
14552
+ this._clientRegistryCache = registry;
14553
+ this._clientRegistryCacheAt = Date.now();
14554
+ await this._writeClientRegistry(registry);
14555
+ });
14556
+ this._lastClientHeartbeat.delete(clientId);
14557
+ this._events?.emitCustom("mailbox.client_deregistered", { clientId });
14558
+ this.publishHqMailboxSnapshot();
14559
+ }
13783
14560
  async clientHeartbeat(input) {
13784
14561
  const last = this._lastClientHeartbeat.get(input.clientId) ?? 0;
13785
14562
  const now = Date.now();
@@ -13809,7 +14586,11 @@ var GlobalMailbox = class {
13809
14586
  async getClientStatuses() {
13810
14587
  await this._ensureClientRegistry();
13811
14588
  const registry = await this._readClientRegistry();
14589
+ const before = registry.size;
13812
14590
  this._pruneStaleClientsInPlace(registry);
14591
+ if (registry.size < before) {
14592
+ await this._writeClientRegistry(registry);
14593
+ }
13813
14594
  const now = Date.now();
13814
14595
  return Array.from(registry.values()).map((c) => ({
13815
14596
  clientId: c.clientId,
@@ -13821,6 +14602,32 @@ var GlobalMailbox = class {
13821
14602
  pid: c.pid
13822
14603
  })).sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt));
13823
14604
  }
14605
+ /**
14606
+ * Explicitly purge stale clients from the registry and write back to disk.
14607
+ * Removes client entries whose lastSeenAt is older than CLIENT_STALE_MS.
14608
+ * Returns the number of entries purged.
14609
+ *
14610
+ * Idempotent — safe to call regularly. The same pruning runs implicitly
14611
+ * inside registerClient, clientHeartbeat, and getClientStatuses, but if
14612
+ * no client registers or heartbeats for a long time, this public method
14613
+ * ensures the file gets cleaned up on demand.
14614
+ */
14615
+ async purgeClients() {
14616
+ await this._ensureClientRegistry();
14617
+ let purged = 0;
14618
+ await withFileLock(this.clientRegistryPath, async () => {
14619
+ const registry = await this._readClientRegistry({ fresh: true });
14620
+ const before = registry.size;
14621
+ this._pruneStaleClientsInPlace(registry);
14622
+ purged = before - registry.size;
14623
+ if (purged > 0) {
14624
+ this._clientRegistryCache = registry;
14625
+ this._clientRegistryCacheAt = Date.now();
14626
+ await this._writeClientRegistry(registry);
14627
+ }
14628
+ });
14629
+ return purged;
14630
+ }
13824
14631
  // ── Lifecycle ───────────────────────────────────────────────────────────
13825
14632
  async close() {
13826
14633
  this._registryCache = null;
@@ -13828,6 +14635,8 @@ var GlobalMailbox = class {
13828
14635
  this._messageCache = null;
13829
14636
  this._messageCacheMtime = -1;
13830
14637
  this._messageCacheSize = -1;
14638
+ this._recipientIndex = null;
14639
+ this._senderIndex = null;
13831
14640
  }
13832
14641
  async clearAll() {
13833
14642
  await withFileLock(this.messagePath, async () => {
@@ -13876,6 +14685,100 @@ var GlobalMailbox = class {
13876
14685
  remaining
13877
14686
  };
13878
14687
  }
14688
+ // ── Auto-compaction ─────────────────────────────────────────────────────
14689
+ _autoCompactTimer = null;
14690
+ async autoCompact(opts) {
14691
+ const readMaxAgeMs = opts?.readMaxAgeMs ?? AUTO_COMPACT_READ_MAX_AGE_MS;
14692
+ const defaultTtlMs = opts?.defaultTtlMs ?? AUTO_COMPACT_DEFAULT_TTL_MS;
14693
+ const completedMaxAgeMs = opts?.completedMaxAgeMs ?? 864e5;
14694
+ const incompleteMaxAgeMs = opts?.incompleteMaxAgeMs ?? 6048e5;
14695
+ let expiredPurged = 0;
14696
+ let readByAllPurged = 0;
14697
+ let completedPurged = 0;
14698
+ let incompletePurged = 0;
14699
+ let remaining = 0;
14700
+ let onlineAgentIds = null;
14701
+ try {
14702
+ const statuses = await this.getAgentStatuses();
14703
+ const online = statuses.filter((s) => s.online);
14704
+ onlineAgentIds = online.length > 0 ? new Set(online.map((s) => s.agentId)) : null;
14705
+ } catch {
14706
+ onlineAgentIds = null;
14707
+ }
14708
+ await withFileLock(this.messagePath, async () => {
14709
+ const all = await this._readMessagesFresh();
14710
+ const now = Date.now();
14711
+ const cutoffReadAge = now - readMaxAgeMs;
14712
+ const cutoffCompleted = now - completedMaxAgeMs;
14713
+ const cutoffIncomplete = now - incompleteMaxAgeMs;
14714
+ const cutoffDefaultTtl = now - defaultTtlMs;
14715
+ const kept = [];
14716
+ for (const msg of all) {
14717
+ const msgTime = new Date(msg.timestamp).getTime();
14718
+ if (msg.expiresAt !== void 0) {
14719
+ if (new Date(msg.expiresAt).getTime() < now) {
14720
+ expiredPurged++;
14721
+ continue;
14722
+ }
14723
+ } else if (msgTime < cutoffDefaultTtl) {
14724
+ expiredPurged++;
14725
+ continue;
14726
+ }
14727
+ if (onlineAgentIds !== null && !msg.completed) {
14728
+ const readByAll = onlineAgentIds.size > 0 && [...onlineAgentIds].every((id) => id in msg.readBy);
14729
+ if (readByAll) {
14730
+ const readTimes = Object.values(msg.readBy).map((t) => new Date(t).getTime());
14731
+ const latestRead = Math.max(...readTimes);
14732
+ if (latestRead < cutoffReadAge) {
14733
+ readByAllPurged++;
14734
+ continue;
14735
+ }
14736
+ }
14737
+ }
14738
+ const completedTime = msg.completedAt ? new Date(msg.completedAt).getTime() : 0;
14739
+ if (msg.completed && completedTime < cutoffCompleted) {
14740
+ completedPurged++;
14741
+ continue;
14742
+ }
14743
+ if (!msg.completed && msgTime < cutoffIncomplete) {
14744
+ incompletePurged++;
14745
+ continue;
14746
+ }
14747
+ kept.push(msg);
14748
+ }
14749
+ remaining = kept.length;
14750
+ if (kept.length < all.length) {
14751
+ const content = kept.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
14752
+ await fsp11.writeFile(this.messagePath, content, "utf8");
14753
+ }
14754
+ const { mtimeMs, size } = await this._statMessageFile();
14755
+ this._setMessageCache(kept, mtimeMs, size);
14756
+ });
14757
+ const totalRemoved = expiredPurged + readByAllPurged + completedPurged + incompletePurged;
14758
+ return {
14759
+ expiredRemoved: expiredPurged,
14760
+ readByAllRemoved: readByAllPurged,
14761
+ stalePurged: completedPurged + incompletePurged,
14762
+ totalRemoved,
14763
+ remaining
14764
+ };
14765
+ }
14766
+ startAutoCompactTimer(opts) {
14767
+ if (this._autoCompactTimer !== null) {
14768
+ clearInterval(this._autoCompactTimer);
14769
+ }
14770
+ const intervalMs = opts?.intervalMs ?? AUTO_COMPACT_INTERVAL_MS;
14771
+ const timer = setInterval(() => {
14772
+ this.autoCompact(opts).catch(() => {
14773
+ });
14774
+ }, intervalMs);
14775
+ timer.unref?.();
14776
+ this._autoCompactTimer = timer;
14777
+ return () => {
14778
+ clearInterval(timer);
14779
+ this._autoCompactTimer = null;
14780
+ };
14781
+ }
13879
14782
  // ── Internal ────────────────────────────────────────────────────────────
13880
14783
  /**
13881
14784
  * Read all messages from the JSONL file. Always reads + parses the file.
@@ -13908,44 +14811,59 @@ var GlobalMailbox = class {
13908
14811
  const buf = Buffer.alloc(tailLen);
13909
14812
  await fd.read(buf, 0, tailLen, oldSize);
13910
14813
  const tail = buf.toString("utf8");
13911
- for (const line of tail.split(LINE_SEPARATOR)) {
14814
+ if (!tail.endsWith(LINE_SEPARATOR)) {
14815
+ const lastSeparatorIdx = tail.lastIndexOf(LINE_SEPARATOR);
14816
+ if (lastSeparatorIdx === -1) {
14817
+ return this._messageCache;
14818
+ }
14819
+ const completeTail = tail.slice(0, lastSeparatorIdx + 1);
14820
+ this._appendIncrementalLines(completeTail);
14821
+ this._messageCacheSize = oldSize + lastSeparatorIdx + 1;
14822
+ return this._messageCache;
14823
+ }
14824
+ this._appendIncrementalLines(tail);
14825
+ this._messageCacheSize = newSize;
14826
+ return this._messageCache;
14827
+ }
14828
+ _appendIncrementalLines(raw) {
14829
+ for (const line of raw.split(LINE_SEPARATOR)) {
13912
14830
  if (!line.trim()) continue;
13913
- try {
13914
- const parsed = JSON.parse(line);
13915
- if (!parsed["readBy"]) {
13916
- const readBy = {};
13917
- if (parsed["read"] && parsed["readAt"]) {
13918
- readBy[parsed["to"]] = parsed["readAt"];
13919
- }
13920
- parsed["readBy"] = readBy;
13921
- delete parsed["read"];
13922
- delete parsed["readAt"];
14831
+ const message = this._parseLine(line);
14832
+ if (message === null) continue;
14833
+ this._messageCache.push(message);
14834
+ const idx = this._messageCache.length - 1;
14835
+ if (this._recipientIndex !== null) {
14836
+ let set = this._recipientIndex.get(message.to);
14837
+ if (set === void 0) {
14838
+ set = /* @__PURE__ */ new Set();
14839
+ this._recipientIndex.set(message.to, set);
13923
14840
  }
13924
- this._messageCache.push(parsed);
13925
- } catch {
14841
+ set.add(idx);
14842
+ }
14843
+ if (this._senderIndex !== null) {
14844
+ let set = this._senderIndex.get(message.from);
14845
+ if (set === void 0) {
14846
+ set = /* @__PURE__ */ new Set();
14847
+ this._senderIndex.set(message.from, set);
14848
+ }
14849
+ set.add(idx);
13926
14850
  }
13927
14851
  }
13928
- return this._messageCache;
14852
+ }
14853
+ _parseLine(line) {
14854
+ try {
14855
+ return parseMailboxMessageLine(line);
14856
+ } catch {
14857
+ return null;
14858
+ }
13929
14859
  }
13930
14860
  /** Parse a JSONL string into MailboxMessage[], including migration. */
13931
14861
  _parseLines(raw) {
13932
14862
  const lines = raw.split(LINE_SEPARATOR).filter((l) => l.trim().length > 0);
13933
14863
  const messages = [];
13934
14864
  for (const line of lines) {
13935
- try {
13936
- const parsed = JSON.parse(line);
13937
- if (!parsed["readBy"]) {
13938
- const readBy = {};
13939
- if (parsed["read"] && parsed["readAt"]) {
13940
- readBy[parsed["to"]] = parsed["readAt"];
13941
- }
13942
- parsed["readBy"] = readBy;
13943
- delete parsed["read"];
13944
- delete parsed["readAt"];
13945
- }
13946
- messages.push(parsed);
13947
- } catch {
13948
- }
14865
+ const message = this._parseLine(line);
14866
+ if (message !== null) messages.push(message);
13949
14867
  }
13950
14868
  return messages;
13951
14869
  }
@@ -14033,7 +14951,6 @@ var GlobalMailbox = class {
14033
14951
  try {
14034
14952
  const updated = await this._readNewMessagesOnly(fd, this._messageCacheSize, st.size);
14035
14953
  this._messageCacheMtime = st.mtimeMs;
14036
- this._messageCacheSize = st.size;
14037
14954
  return updated;
14038
14955
  } finally {
14039
14956
  await fd.close();
@@ -14070,11 +14987,46 @@ var GlobalMailbox = class {
14070
14987
  this._messageCache = null;
14071
14988
  this._messageCacheMtime = -1;
14072
14989
  this._messageCacheSize = -1;
14990
+ this._recipientIndex = null;
14991
+ this._senderIndex = null;
14073
14992
  return;
14074
14993
  }
14075
14994
  this._messageCache = messages;
14076
14995
  this._messageCacheMtime = mtime;
14077
14996
  this._messageCacheSize = size;
14997
+ this._rebuildRecipientIndex();
14998
+ }
14999
+ /**
15000
+ * Build the recipient AND sender indexes from the current `_messageCache`.
15001
+ * Called only from `_setMessageCache` so both indexes are always in sync
15002
+ * with a freshly-replaced cache array.
15003
+ */
15004
+ _rebuildRecipientIndex() {
15005
+ const cache = this._messageCache;
15006
+ if (cache === null) {
15007
+ this._recipientIndex = null;
15008
+ this._senderIndex = null;
15009
+ return;
15010
+ }
15011
+ const recIdx = /* @__PURE__ */ new Map();
15012
+ const sndIdx = /* @__PURE__ */ new Map();
15013
+ for (let i = 0; i < cache.length; i++) {
15014
+ const msg = cache[i];
15015
+ let rSet = recIdx.get(msg.to);
15016
+ if (rSet === void 0) {
15017
+ rSet = /* @__PURE__ */ new Set();
15018
+ recIdx.set(msg.to, rSet);
15019
+ }
15020
+ rSet.add(i);
15021
+ let sSet = sndIdx.get(msg.from);
15022
+ if (sSet === void 0) {
15023
+ sSet = /* @__PURE__ */ new Set();
15024
+ sndIdx.set(msg.from, sSet);
15025
+ }
15026
+ sSet.add(i);
15027
+ }
15028
+ this._recipientIndex = recIdx;
15029
+ this._senderIndex = sndIdx;
14078
15030
  }
14079
15031
  /**
14080
15032
  * Append a single just-sent message to the in-memory cache without
@@ -14101,6 +15053,23 @@ var GlobalMailbox = class {
14101
15053
  return;
14102
15054
  }
14103
15055
  this._messageCache.push(msg);
15056
+ const newIdx = this._messageCache.length - 1;
15057
+ if (this._recipientIndex !== null) {
15058
+ let set = this._recipientIndex.get(msg.to);
15059
+ if (set === void 0) {
15060
+ set = /* @__PURE__ */ new Set();
15061
+ this._recipientIndex.set(msg.to, set);
15062
+ }
15063
+ set.add(newIdx);
15064
+ }
15065
+ if (this._senderIndex !== null) {
15066
+ let set = this._senderIndex.get(msg.from);
15067
+ if (set === void 0) {
15068
+ set = /* @__PURE__ */ new Set();
15069
+ this._senderIndex.set(msg.from, set);
15070
+ }
15071
+ set.add(newIdx);
15072
+ }
14104
15073
  this._messageCacheMtime = mtime;
14105
15074
  this._messageCacheSize = size;
14106
15075
  }
@@ -14132,21 +15101,12 @@ var GlobalMailbox = class {
14132
15101
  }
14133
15102
  }
14134
15103
  _pruneStaleInPlace(registry) {
14135
- const staleCutoff = Date.now() - AGENT_STALE_MS;
14136
- const purgeCutoff = Date.now() - AGENT_PURGE_MS;
14137
- const toDelete = [];
15104
+ const cutoff = Date.now() - AGENT_STALE_MS;
14138
15105
  for (const [id, agent] of registry) {
14139
- const lastSeen = new Date(agent.lastSeenAt).getTime();
14140
- if (lastSeen < purgeCutoff) {
14141
- toDelete.push(id);
14142
- continue;
15106
+ const lastSeen = Date.parse(agent.lastSeenAt);
15107
+ if (!Number.isFinite(lastSeen) || lastSeen < cutoff) {
15108
+ registry.delete(id);
14143
15109
  }
14144
- if (lastSeen < staleCutoff) {
14145
- agent.status = "idle";
14146
- }
14147
- }
14148
- for (const id of toDelete) {
14149
- registry.delete(id);
14150
15110
  }
14151
15111
  }
14152
15112
  async _writeRegistry(registry) {
@@ -14188,9 +15148,10 @@ var GlobalMailbox = class {
14188
15148
  }
14189
15149
  _pruneStaleClientsInPlace(registry) {
14190
15150
  const cutoff = Date.now() - CLIENT_STALE_MS;
14191
- for (const client of registry.values()) {
14192
- if (new Date(client.lastSeenAt).getTime() < cutoff) {
14193
- client.lastSeenAt = new Date(cutoff).toISOString();
15151
+ for (const [id, client] of registry) {
15152
+ const lastSeen = Date.parse(client.lastSeenAt);
15153
+ if (!Number.isFinite(lastSeen) || lastSeen < cutoff) {
15154
+ registry.delete(id);
14194
15155
  }
14195
15156
  }
14196
15157
  }
@@ -14248,7 +15209,7 @@ function makeMailboxTool(opts = {}) {
14248
15209
  description: "Which mailbox operation to perform."
14249
15210
  },
14250
15211
  to: { type: "string", description: "Recipient agent id, or '*' / 'all' for broadcast." },
14251
- type: { type: "string", enum: ["note", "ask", "assign", "steer", "btw", "broadcast", "status", "result", "review"], description: "Message type." },
15212
+ type: { type: "string", enum: ["note", "ask", "assign", "steer", "btw", "broadcast", "status", "result", "review", "control"], description: "Message type." },
14252
15213
  subject: { type: "string", description: "Short subject line." },
14253
15214
  body: { type: "string", description: "Full message content." },
14254
15215
  priority: { type: "string", enum: ["low", "normal", "high"] },
@@ -15302,14 +16263,18 @@ var DefaultMailbox = class {
15302
16263
  _messageCache = null;
15303
16264
  _messageCacheMtime = -1;
15304
16265
  _messageCacheSize = -1;
16266
+ /** True when the cache is only the newest MESSAGE_CACHE_MAX_ENTRIES records. */
16267
+ _messageCacheTruncated = false;
15305
16268
  /** Primary index: recipient → Set of messages (points into _messageCache). */
15306
16269
  _byTo = /* @__PURE__ */ new Map();
15307
16270
  /** Secondary index: sender → Set of messages (points into _messageCache). */
15308
16271
  _byFrom = /* @__PURE__ */ new Map();
15309
16272
  /** Counts malformed JSONL lines skipped during parsing for observability. */
15310
16273
  _corruptionCount = 0;
15311
- constructor(sessionDir) {
16274
+ logger;
16275
+ constructor(sessionDir, opts) {
15312
16276
  this.filePath = path15.join(sessionDir, MAILBOX_FILE2);
16277
+ this.logger = opts?.logger;
15313
16278
  }
15314
16279
  get mailboxPath() {
15315
16280
  return this.filePath;
@@ -15327,7 +16292,7 @@ var DefaultMailbox = class {
15327
16292
  // "all" is an accepted spelling of the broadcast address — canonical
15328
16293
  // form on disk is '*' so every query/checker matches it.
15329
16294
  to: normalizeRecipient(input.to),
15330
- type: input.type,
16295
+ type: normalizeMailboxMessageType(input.type),
15331
16296
  subject: input.subject,
15332
16297
  body: input.body,
15333
16298
  priority: input.priority ?? "normal",
@@ -15350,12 +16315,14 @@ var DefaultMailbox = class {
15350
16315
  }
15351
16316
  // ── Query ─────────────────────────────────────────────────────────────
15352
16317
  async query(q) {
16318
+ const queryType = q.type === void 0 ? void 0 : normalizeMailboxMessageType(q.type);
15353
16319
  const needFullScan = q.unreadBy !== void 0 || q.since !== void 0;
16320
+ const cached = await this._readAllCached(false);
15354
16321
  let candidates;
16322
+ let candidatesComplete = !this._messageCacheTruncated || cached !== this._messageCache;
15355
16323
  if (needFullScan) {
15356
- candidates = await this._readAllCached();
16324
+ candidates = cached;
15357
16325
  } else {
15358
- await this._readAllCached();
15359
16326
  if (q.to !== void 0) {
15360
16327
  const direct = this._byTo.get(q.to);
15361
16328
  const broadcast = this._byTo.get("*");
@@ -15363,10 +16330,12 @@ var DefaultMailbox = class {
15363
16330
  if (direct) for (const m of direct) combined.set(m.id, m);
15364
16331
  if (broadcast) for (const m of broadcast) combined.set(m.id, m);
15365
16332
  candidates = Array.from(combined.values());
16333
+ candidatesComplete = !this._messageCacheTruncated;
15366
16334
  } else if (q.from !== void 0) {
15367
16335
  candidates = Array.from(this._byFrom.get(q.from) ?? []);
16336
+ candidatesComplete = !this._messageCacheTruncated;
15368
16337
  } else {
15369
- candidates = await this._readAllCached();
16338
+ candidates = cached;
15370
16339
  }
15371
16340
  }
15372
16341
  const limit = q.limit ?? 50;
@@ -15377,27 +16346,37 @@ var DefaultMailbox = class {
15377
16346
  if (q.from !== void 0 && msg.from !== q.from) return false;
15378
16347
  if (q.unreadBy !== void 0 && q.unreadBy in msg.readBy) return false;
15379
16348
  if (q.incompleteOnly && msg.completed) return false;
15380
- if (q.type !== void 0 && msg.type !== q.type) return false;
16349
+ if (queryType !== void 0 && msg.type !== queryType) return false;
15381
16350
  if (order !== null && (order[msg.priority] ?? 1) < minPriorityRank)
15382
16351
  return false;
15383
16352
  if (q.since !== void 0 && msg.timestamp <= q.since) return false;
15384
16353
  return true;
15385
16354
  };
15386
- if (order === null) {
16355
+ const selectNewest = (source) => {
15387
16356
  const out = [];
15388
- for (let i = candidates.length - 1; i >= 0 && out.length < limit; i--) {
15389
- const msg = candidates[i];
16357
+ for (let i = source.length - 1; i >= 0 && out.length < limit; i--) {
16358
+ const msg = source[i];
15390
16359
  if (passes(msg)) out.push(msg);
15391
16360
  }
15392
16361
  return out;
15393
- }
15394
- const filtered = [];
15395
- for (let i = candidates.length - 1; i >= 0; i--) {
15396
- const msg = candidates[i];
15397
- if (passes(msg)) filtered.push(msg);
15398
- }
15399
- filtered.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
15400
- return filtered.slice(0, limit);
16362
+ };
16363
+ if (order === null) {
16364
+ const out = selectNewest(candidates);
16365
+ if (out.length >= limit || candidatesComplete) return out;
16366
+ return selectNewest(await this._readAll());
16367
+ }
16368
+ const selectByPriority = (source) => {
16369
+ const filtered2 = [];
16370
+ for (let i = source.length - 1; i >= 0; i--) {
16371
+ const msg = source[i];
16372
+ if (passes(msg)) filtered2.push(msg);
16373
+ }
16374
+ filtered2.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
16375
+ return filtered2.slice(0, limit);
16376
+ };
16377
+ const filtered = selectByPriority(candidates);
16378
+ if (filtered.length >= limit || candidatesComplete) return filtered;
16379
+ return selectByPriority(await this._readAll());
15401
16380
  }
15402
16381
  // ── Ack ───────────────────────────────────────────────────────────────
15403
16382
  async ack(input) {
@@ -15454,7 +16433,7 @@ var DefaultMailbox = class {
15454
16433
  for (const m of all) {
15455
16434
  if (m.type !== "status") continue;
15456
16435
  const existing = latest.get(m.from);
15457
- if (existing && m.timestamp <= existing.lastActivityAt) continue;
16436
+ if (existing && m.timestamp < existing.lastActivityAt) continue;
15458
16437
  latest.set(m.from, {
15459
16438
  agentId: m.from,
15460
16439
  name: m.taskContext?.agentName ?? m.from,
@@ -15496,6 +16475,7 @@ var DefaultMailbox = class {
15496
16475
  this._messageCache = null;
15497
16476
  this._messageCacheMtime = -1;
15498
16477
  this._messageCacheSize = -1;
16478
+ this._messageCacheTruncated = false;
15499
16479
  this._byTo.clear();
15500
16480
  this._byFrom.clear();
15501
16481
  }
@@ -15547,13 +16527,32 @@ var DefaultMailbox = class {
15547
16527
  };
15548
16528
  }
15549
16529
  // ── Client registry stubs (not applicable per-session) ─────────────────
16530
+ async autoCompact(_opts) {
16531
+ const purged = await this.purgeStale(_opts);
16532
+ return {
16533
+ readByAllRemoved: 0,
16534
+ expiredRemoved: 0,
16535
+ stalePurged: purged.totalPurged,
16536
+ totalRemoved: purged.totalPurged,
16537
+ remaining: purged.remaining
16538
+ };
16539
+ }
16540
+ startAutoCompactTimer(_opts) {
16541
+ return () => {
16542
+ };
16543
+ }
15550
16544
  async registerClient(_input) {
15551
16545
  }
15552
16546
  async clientHeartbeat(_input) {
15553
16547
  }
16548
+ async deregisterClient(_clientId) {
16549
+ }
15554
16550
  async getClientStatuses() {
15555
16551
  return [];
15556
16552
  }
16553
+ async purgeClients() {
16554
+ return 0;
16555
+ }
15557
16556
  // ── Internal ──────────────────────────────────────────────────────────
15558
16557
  async _readAll() {
15559
16558
  try {
@@ -15567,60 +16566,62 @@ var DefaultMailbox = class {
15567
16566
  /**
15568
16567
  * Read only newly-appended bytes from the file and append them to the
15569
16568
  * in-memory cache, avoiding a full re-read when the file only grew.
16569
+ *
16570
+ * Guards against a half-written tail: when the tail doesn't end with
16571
+ * LINE_SEPARATOR, another process may be mid-append. Parsing a partial
16572
+ * JSON line would throw (caught below), but worse: the NEXT incremental
16573
+ * read would skip that same line because the size tracker already advanced.
15570
16574
  */
15571
16575
  async _readNewMessagesOnly(fd, oldSize, newSize) {
15572
16576
  const tailLen = newSize - oldSize;
15573
16577
  const buf = Buffer.alloc(tailLen);
15574
16578
  await fd.read(buf, 0, tailLen, oldSize);
15575
16579
  const tail = buf.toString("utf8");
15576
- for (const line of tail.split(LINE_SEPARATOR2)) {
16580
+ if (!tail.endsWith(LINE_SEPARATOR2)) {
16581
+ const lastSeparatorIdx = tail.lastIndexOf(LINE_SEPARATOR2);
16582
+ if (lastSeparatorIdx === -1) {
16583
+ return this._messageCache;
16584
+ }
16585
+ const completeTail = tail.slice(0, lastSeparatorIdx + 1);
16586
+ this._appendIncrementalLines(completeTail);
16587
+ this._messageCacheSize = oldSize + lastSeparatorIdx + 1;
16588
+ return this._messageCache;
16589
+ }
16590
+ this._appendIncrementalLines(tail);
16591
+ this._messageCacheSize = newSize;
16592
+ return this._messageCache;
16593
+ }
16594
+ _appendIncrementalLines(raw) {
16595
+ for (const line of raw.split(LINE_SEPARATOR2)) {
15577
16596
  if (!line.trim()) continue;
15578
- try {
15579
- const parsed = JSON.parse(line);
15580
- if (!parsed["readBy"]) {
15581
- const readBy = {};
15582
- if (parsed["read"] && parsed["readAt"]) {
15583
- readBy[parsed["to"] ?? "unknown"] = parsed["readAt"];
15584
- }
15585
- parsed["readBy"] = readBy;
15586
- delete parsed["read"];
15587
- delete parsed["readAt"];
15588
- }
15589
- const msg = parsed;
15590
- this._messageCache.push(msg);
15591
- this._indexMsg(msg);
15592
- } catch (err) {
15593
- this._corruptionCount++;
15594
- console.debug(
15595
- `[mailbox] skipped malformed line during incremental read: ${err.message}`
16597
+ const msg = this._parseLine(line, "incremental read");
16598
+ if (msg === null) continue;
16599
+ this._pushToCache(msg);
16600
+ }
16601
+ }
16602
+ _parseLine(line, source) {
16603
+ try {
16604
+ return parseMailboxMessageLine(line);
16605
+ } catch (err) {
16606
+ this._corruptionCount++;
16607
+ if (this.logger) {
16608
+ this.logger.debug(
16609
+ "Skipped malformed mailbox line",
16610
+ { source, error: err.message, corruptionCount: this._corruptionCount }
15596
16611
  );
16612
+ } else {
16613
+ console.debug(`[mailbox] skipped malformed line during ${source}: ${err.message}`);
15597
16614
  }
16615
+ return null;
15598
16616
  }
15599
- return this._messageCache;
15600
16617
  }
15601
16618
  /** Parse a JSONL string into MailboxMessage[], including migration. */
15602
16619
  _parseLines(raw) {
15603
16620
  const lines = raw.split(LINE_SEPARATOR2).filter((l) => l.trim().length > 0);
15604
16621
  const messages = [];
15605
16622
  for (const line of lines) {
15606
- try {
15607
- const parsed = JSON.parse(line);
15608
- if (!parsed["readBy"]) {
15609
- const readBy = {};
15610
- if (parsed["read"] && parsed["readAt"]) {
15611
- readBy[parsed["to"] ?? "unknown"] = parsed["readAt"];
15612
- }
15613
- parsed["readBy"] = readBy;
15614
- delete parsed["read"];
15615
- delete parsed["readAt"];
15616
- }
15617
- messages.push(parsed);
15618
- } catch (err) {
15619
- this._corruptionCount++;
15620
- console.debug(
15621
- `[mailbox] skipped malformed line during full parse: ${err.message}`
15622
- );
15623
- }
16623
+ const message = this._parseLine(line, "full parse");
16624
+ if (message !== null) messages.push(message);
15624
16625
  }
15625
16626
  return messages;
15626
16627
  }
@@ -15641,10 +16642,11 @@ var DefaultMailbox = class {
15641
16642
  throw err;
15642
16643
  }
15643
16644
  }
15644
- async _readAllCached() {
16645
+ async _readAllCached(requireComplete = true) {
15645
16646
  try {
15646
16647
  const st = await fsp12.stat(this.filePath);
15647
16648
  if (this._messageCache !== null && this._messageCacheMtime === st.mtimeMs && this._messageCacheSize === st.size) {
16649
+ if (requireComplete && this._messageCacheTruncated) return this._readAll();
15648
16650
  return this._messageCache;
15649
16651
  }
15650
16652
  if (this._messageCache !== null && this._messageCacheSize >= 0 && st.size > this._messageCacheSize) {
@@ -15652,7 +16654,7 @@ var DefaultMailbox = class {
15652
16654
  try {
15653
16655
  const updated = await this._readNewMessagesOnly(fd, this._messageCacheSize, st.size);
15654
16656
  this._messageCacheMtime = st.mtimeMs;
15655
- this._messageCacheSize = st.size;
16657
+ if (requireComplete && this._messageCacheTruncated) return this._readAll();
15656
16658
  return updated;
15657
16659
  } finally {
15658
16660
  await fd.close();
@@ -15670,28 +16672,18 @@ var DefaultMailbox = class {
15670
16672
  }
15671
16673
  }
15672
16674
  _setMessageCache(messages, mtime, size) {
15673
- if (messages.length > MESSAGE_CACHE_MAX_ENTRIES2) {
15674
- this._messageCache = null;
15675
- this._messageCacheMtime = -1;
15676
- this._messageCacheSize = -1;
15677
- this._byTo.clear();
15678
- this._byFrom.clear();
15679
- return;
15680
- }
15681
- this._messageCache = messages;
15682
- this._buildIndexes(messages);
16675
+ this._messageCacheTruncated = messages.length > MESSAGE_CACHE_MAX_ENTRIES2;
16676
+ this._messageCache = this._messageCacheTruncated ? messages.slice(-MESSAGE_CACHE_MAX_ENTRIES2) : messages;
16677
+ this._buildIndexes(this._messageCache);
15683
16678
  this._messageCacheMtime = mtime;
15684
16679
  this._messageCacheSize = size;
15685
16680
  }
15686
16681
  _pushToCache(msg) {
15687
16682
  if (this._messageCache === null) return;
15688
16683
  if (this._messageCache.length >= MESSAGE_CACHE_MAX_ENTRIES2) {
15689
- this._messageCache = null;
15690
- this._messageCacheMtime = -1;
15691
- this._messageCacheSize = -1;
15692
- this._byTo.clear();
15693
- this._byFrom.clear();
15694
- return;
16684
+ const evicted = this._messageCache.shift();
16685
+ if (evicted !== void 0) this._unindexMsg(evicted);
16686
+ this._messageCacheTruncated = true;
15695
16687
  }
15696
16688
  this._messageCache.push(msg);
15697
16689
  this._indexMsg(msg);
@@ -15719,6 +16711,15 @@ var DefaultMailbox = class {
15719
16711
  this._byFrom.set(msg.from, /* @__PURE__ */ new Set([msg]));
15720
16712
  }
15721
16713
  }
16714
+ /** Remove an evicted message from both indexes without rebuilding them. */
16715
+ _unindexMsg(msg) {
16716
+ const toSet = this._byTo.get(msg.to);
16717
+ toSet?.delete(msg);
16718
+ if (toSet?.size === 0) this._byTo.delete(msg.to);
16719
+ const fromSet = this._byFrom.get(msg.from);
16720
+ fromSet?.delete(msg);
16721
+ if (fromSet?.size === 0) this._byFrom.delete(msg.from);
16722
+ }
15722
16723
  };
15723
16724
 
15724
16725
  // src/coordination/mailbox-actions.ts
@@ -16018,7 +17019,7 @@ async function saveLog(storageDir, log) {
16018
17019
  await fs2.rename(tmp, logPath(storageDir));
16019
17020
  }
16020
17021
  function detectEcosystem(manifestPath) {
16021
- const name = path16.basename(manifestPath).toLowerCase();
17022
+ const name = path16.win32.basename(manifestPath).toLowerCase();
16022
17023
  if (name === "package.json") return "npm";
16023
17024
  if (name === "go.mod") return "go";
16024
17025
  if (name === "cargo.toml") return "cargo";
@@ -16381,7 +17382,8 @@ function isProcessAlive(pid) {
16381
17382
  encoding: "utf-8",
16382
17383
  stdio: ["pipe", "pipe", "pipe"]
16383
17384
  });
16384
- return /\b"?,?\d+,?"?/.test(out) || out.includes(String(pid));
17385
+ const match = /(?:^|\n)"[^"]*","(\d+)"/.exec(out);
17386
+ return match !== null && match[1] === String(pid);
16385
17387
  } catch {
16386
17388
  return false;
16387
17389
  }
@@ -16586,7 +17588,7 @@ var AdaptiveConcurrencyController = class {
16586
17588
  // src/coordination/agent-monitor.ts
16587
17589
  import * as fs3 from "node:fs/promises";
16588
17590
  import * as path18 from "node:path";
16589
- var AgentMonitorService = class {
17591
+ var AgentMonitorService = class _AgentMonitorService {
16590
17592
  _fleetBus;
16591
17593
  _events;
16592
17594
  _sessionId;
@@ -16596,6 +17598,15 @@ var AgentMonitorService = class {
16596
17598
  _onEntry;
16597
17599
  /** Per-subagent virtual sessions. */
16598
17600
  _sessions = /* @__PURE__ */ new Map();
17601
+ /**
17602
+ * Per-subagent OPEN streaming segment. Provider text/thinking deltas
17603
+ * arrive word-by-word; writing one transcript entry per delta fragments
17604
+ * every surface (ring, JSONL, HQ) into unreadable single-word lines.
17605
+ * Instead, consecutive same-kind/same-iteration deltas append into ONE
17606
+ * entry that is already in the ring (live views see it grow in place);
17607
+ * the JSONL line is written once, when the segment closes.
17608
+ */
17609
+ _openStreams = /* @__PURE__ */ new Map();
16599
17610
  /** Disposers for FleetBus subscriptions, keyed by subagentId. */
16600
17611
  _subscriptions = /* @__PURE__ */ new Map();
16601
17612
  /** Generic fleet-wide subscription disposer. */
@@ -16658,6 +17669,9 @@ var AgentMonitorService = class {
16658
17669
  stop() {
16659
17670
  if (!this._started) return;
16660
17671
  this._started = false;
17672
+ for (const subagentId of [...this._openStreams.keys()]) {
17673
+ this._closeStream(subagentId);
17674
+ }
16661
17675
  if (this._fleetDisposer) {
16662
17676
  this._fleetDisposer();
16663
17677
  this._fleetDisposer = void 0;
@@ -16726,6 +17740,7 @@ var AgentMonitorService = class {
16726
17740
  }
16727
17741
  /** Drop a retired subagent from every live in-memory monitor surface. */
16728
17742
  removeSubagent(subagentId) {
17743
+ this._closeStream(subagentId);
16729
17744
  this._sessions.delete(subagentId);
16730
17745
  const dispose = this._subscriptions.get(subagentId);
16731
17746
  if (dispose) dispose();
@@ -16739,31 +17754,13 @@ var AgentMonitorService = class {
16739
17754
  case "provider.text_delta": {
16740
17755
  const text = payload.text;
16741
17756
  if (!text || text.length === 0) return;
16742
- const iteration = payload.iteration ?? 0;
16743
- this._addEntry(subagentId, {
16744
- id: this._uid(),
16745
- subagentId,
16746
- agentName: session.agentName,
16747
- ts: (/* @__PURE__ */ new Date()).toISOString(),
16748
- kind: "text",
16749
- content: text,
16750
- iteration
16751
- });
17757
+ this._appendStreamDelta(subagentId, session, "text", text, payload.iteration ?? 0);
16752
17758
  break;
16753
17759
  }
16754
17760
  case "provider.thinking_delta": {
16755
17761
  const text = payload.text;
16756
17762
  if (!text || text.length === 0) return;
16757
- const iteration = payload.iteration ?? 0;
16758
- this._addEntry(subagentId, {
16759
- id: this._uid(),
16760
- subagentId,
16761
- agentName: session.agentName,
16762
- ts: (/* @__PURE__ */ new Date()).toISOString(),
16763
- kind: "thinking",
16764
- content: text,
16765
- iteration
16766
- });
17763
+ this._appendStreamDelta(subagentId, session, "thinking", text, payload.iteration ?? 0);
16767
17764
  break;
16768
17765
  }
16769
17766
  case "tool.started": {
@@ -16820,15 +17817,66 @@ var AgentMonitorService = class {
16820
17817
  }
16821
17818
  }
16822
17819
  }
17820
+ /** Max characters accumulated into one streaming segment before rolling over. */
17821
+ static _MAX_SEGMENT_CHARS = 2e4;
17822
+ /**
17823
+ * Fold a provider text/thinking delta into the subagent's open streaming
17824
+ * segment, or open a new one. A segment breaks on kind change, iteration
17825
+ * change, size cap, or any non-delta entry (see `_addEntry`). The entry
17826
+ * is pushed to the ring when the segment OPENS (live views poll the ring
17827
+ * and watch it grow in place); the JSONL line AND the timeline/HQ
17828
+ * announcement happen once, on close, with the COMPLETE segment — so
17829
+ * downstream consumers get one whole message instead of a word-per-event
17830
+ * firehose (or, worse, only the first word).
17831
+ */
17832
+ _appendStreamDelta(subagentId, session, kind, text, iteration) {
17833
+ const open7 = this._openStreams.get(subagentId);
17834
+ if (open7 && open7.entry.kind === kind && open7.entry.iteration === iteration && open7.entry.content.length + text.length <= _AgentMonitorService._MAX_SEGMENT_CHARS) {
17835
+ open7.entry.content += text;
17836
+ return;
17837
+ }
17838
+ this._closeStream(subagentId);
17839
+ const entry = {
17840
+ id: this._uid(),
17841
+ subagentId,
17842
+ agentName: session.agentName,
17843
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
17844
+ kind,
17845
+ content: text,
17846
+ iteration
17847
+ };
17848
+ this._openStreams.set(subagentId, { entry });
17849
+ session.transcript.push(entry);
17850
+ if (session.transcript.length > this._maxEntries) {
17851
+ session.transcript.splice(0, session.transcript.length - this._maxEntries);
17852
+ }
17853
+ }
17854
+ /**
17855
+ * Close the subagent's open streaming segment: persist it to JSONL and
17856
+ * announce the completed entry on the local bus + HQ callback.
17857
+ */
17858
+ _closeStream(subagentId) {
17859
+ const open7 = this._openStreams.get(subagentId);
17860
+ if (!open7) return;
17861
+ this._openStreams.delete(subagentId);
17862
+ this._appendToFile(subagentId, open7.entry).catch(() => {
17863
+ });
17864
+ this._emitEntry(open7.entry);
17865
+ }
16823
17866
  _addEntry(subagentId, entry) {
16824
17867
  const session = this._sessions.get(subagentId);
16825
17868
  if (!session) return;
17869
+ this._closeStream(subagentId);
16826
17870
  session.transcript.push(entry);
16827
17871
  if (session.transcript.length > this._maxEntries) {
16828
17872
  session.transcript.splice(0, session.transcript.length - this._maxEntries);
16829
17873
  }
16830
17874
  this._appendToFile(subagentId, entry).catch(() => {
16831
17875
  });
17876
+ this._emitEntry(entry);
17877
+ }
17878
+ /** Announce a transcript entry on the local bus + HQ callback. */
17879
+ _emitEntry(entry) {
16832
17880
  this._events.emit("agent.timeline.message", {
16833
17881
  sessionId: this._sessionId,
16834
17882
  subagentId: entry.subagentId,
@@ -16855,8 +17903,11 @@ var AgentMonitorService = class {
16855
17903
  _formatToolUse(name, input) {
16856
17904
  if (input === void 0) return `${name}()`;
16857
17905
  const body = this._stringifyForTimeline(input);
16858
- return body ? `${name}
16859
- ${body}` : `${name}()`;
17906
+ if (!body) return `${name}()`;
17907
+ const inline = body.replace(/\s+/g, " ").trim();
17908
+ const head = inline.length <= 160 ? `${name}(${inline})` : `${name}(${inline.slice(0, 159)}\u2026)`;
17909
+ return `${head}
17910
+ ${body}`;
16860
17911
  }
16861
17912
  _formatToolResult(summary, output, outputBytes) {
16862
17913
  if (!output) return summary;
@@ -16893,8 +17944,6 @@ var AutonomousBrain = class {
16893
17944
  maxRetries;
16894
17945
  consensusRiskThreshold;
16895
17946
  selfImprove;
16896
- /** Decision history for self-improvement and audit. */
16897
- decisionHistory = [];
16898
17947
  /** Tracks failure patterns for self-improvement. */
16899
17948
  failurePatterns = /* @__PURE__ */ new Map();
16900
17949
  RISK_ORDER = ["low", "medium", "high", "critical"];
@@ -17206,7 +18255,6 @@ ${ctx.error}`);
17206
18255
  madeAt: (/* @__PURE__ */ new Date()).toISOString(),
17207
18256
  context: input.context
17208
18257
  });
17209
- this.decisionHistory.push(node);
17210
18258
  return node;
17211
18259
  }
17212
18260
  _inferRoles(task) {
@@ -17235,7 +18283,8 @@ import { randomUUID as randomUUID16 } from "node:crypto";
17235
18283
  import { randomUUID as randomUUID14 } from "node:crypto";
17236
18284
  import * as fsp14 from "node:fs/promises";
17237
18285
  import * as path19 from "node:path";
17238
- var KnowledgeGraph = class {
18286
+ var DEFAULT_MAX_NODES = 2e3;
18287
+ var KnowledgeGraph = class _KnowledgeGraph {
17239
18288
  nodes = /* @__PURE__ */ new Map();
17240
18289
  index = /* @__PURE__ */ new Map();
17241
18290
  // tag/field → node ids
@@ -17243,6 +18292,7 @@ var KnowledgeGraph = class {
17243
18292
  pendingDeliveries = /* @__PURE__ */ new Map();
17244
18293
  filePath;
17245
18294
  graphFilePath;
18295
+ maxNodes;
17246
18296
  /**
17247
18297
  * Stable per-node insertion sequence. `nodes` (a Map) preserves insertion
17248
18298
  * order even across `update()` (Map.set on an existing key keeps its slot),
@@ -17261,9 +18311,10 @@ var KnowledgeGraph = class {
17261
18311
  getIndex() {
17262
18312
  return this.index;
17263
18313
  }
17264
- constructor(sessionDir) {
18314
+ constructor(sessionDir, maxNodes = DEFAULT_MAX_NODES) {
17265
18315
  this.filePath = path19.join(sessionDir, "_knowledge_graph");
17266
18316
  this.graphFilePath = path19.join(this.filePath, "graph.jsonl");
18317
+ this.maxNodes = maxNodes;
17267
18318
  }
17268
18319
  // ── Write ──────────────────────────────────────────────────────────────
17269
18320
  /**
@@ -17275,6 +18326,7 @@ var KnowledgeGraph = class {
17275
18326
  this.nodes.set(full.id, full);
17276
18327
  this._trackSeq(full.id);
17277
18328
  this._addToIndex(full, this._indexKeys(full));
18329
+ this._prune();
17278
18330
  await this._persist(full);
17279
18331
  this._deliver(full);
17280
18332
  return full;
@@ -17287,10 +18339,48 @@ var KnowledgeGraph = class {
17287
18339
  const updated = { ...existing, ...patch };
17288
18340
  this.nodes.set(id, updated);
17289
18341
  this._addToIndex(updated, this._indexKeys(updated));
18342
+ this._prune();
17290
18343
  this._deliver(updated);
17291
18344
  await this._append(updated);
17292
18345
  return updated;
17293
18346
  }
18347
+ /**
18348
+ * True when the node is in a terminal / settled state — no future mutations
18349
+ * are expected. Such nodes are the first to be evicted when the in-memory
18350
+ * cap is reached. The JSONL file on disk retains the full history.
18351
+ */
18352
+ static _isTerminal(node) {
18353
+ if (node.type === "goal") return node.status === "done" || node.status === "failed";
18354
+ if (node.type === "change") return node.status === "rejected" || node.status === "rolled_back";
18355
+ if (node.type === "vote") return true;
18356
+ return false;
18357
+ }
18358
+ /**
18359
+ * Evict oldest terminal-state nodes when the in-memory cap is exceeded.
18360
+ * The JSONL file is the authoritative record; eviction only affects memory.
18361
+ */
18362
+ _prune() {
18363
+ if (this.nodes.size <= this.maxNodes) return;
18364
+ const toEvict = this.nodes.size - this.maxNodes;
18365
+ const terminal = [];
18366
+ for (const [id, node] of this.nodes) {
18367
+ if (_KnowledgeGraph._isTerminal(node)) terminal.push({ id, seq: this.seq.get(id) ?? 0 });
18368
+ }
18369
+ terminal.sort((a, b) => a.seq - b.seq);
18370
+ const evicted = new Set(terminal.slice(0, toEvict).map((e) => e.id));
18371
+ if (evicted.size === 0) return;
18372
+ for (const id of evicted) {
18373
+ const node = this.nodes.get(id);
18374
+ if (!node) continue;
18375
+ this._removeFromIndex(node, this._indexKeys(node));
18376
+ this.nodes.delete(id);
18377
+ this.seq.delete(id);
18378
+ for (const pending of this.pendingDeliveries.values()) {
18379
+ const idx = pending.findIndex((n) => n.id === id);
18380
+ if (idx >= 0) pending.splice(idx, 1);
18381
+ }
18382
+ }
18383
+ }
17294
18384
  // ── Read ───────────────────────────────────────────────────────────────
17295
18385
  get(id) {
17296
18386
  return this.nodes.get(id);
@@ -18723,6 +19813,7 @@ var AutonomousCoordinator = class _AutonomousCoordinator {
18723
19813
  mailbox;
18724
19814
  events;
18725
19815
  onCoordinatorEvent;
19816
+ logger;
18726
19817
  running = false;
18727
19818
  iterationCount = 0;
18728
19819
  lastSyncAt = 0;
@@ -18739,6 +19830,7 @@ var AutonomousCoordinator = class _AutonomousCoordinator {
18739
19830
  this.mailbox = opts.mailbox ?? void 0;
18740
19831
  this.events = opts.events ?? void 0;
18741
19832
  this.onCoordinatorEvent = opts.onCoordinatorEvent;
19833
+ this.logger = opts.logger;
18742
19834
  this.graph = new KnowledgeGraph(opts.sessionDir);
18743
19835
  this.dag = new TaskDAG();
18744
19836
  this.auction = new TaskAuctioneer({
@@ -18897,15 +19989,22 @@ var AutonomousCoordinator = class _AutonomousCoordinator {
18897
19989
  stop() {
18898
19990
  if (!this.running) return;
18899
19991
  this.running = false;
18900
- console.error(
18901
- JSON.stringify({
18902
- level: "error",
18903
- event: "autonomous_coordinator.stop_signal",
18904
- message: "stop signal received \u2014 shutting down",
18905
- iteration: this.iterationCount,
18906
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
18907
- })
18908
- );
19992
+ if (this.logger) {
19993
+ this.logger.error(
19994
+ "Stop signal received \u2014 shutting down",
19995
+ { event: "autonomous_coordinator.stop_signal", iteration: this.iterationCount }
19996
+ );
19997
+ } else {
19998
+ console.error(
19999
+ JSON.stringify({
20000
+ level: "error",
20001
+ event: "autonomous_coordinator.stop_signal",
20002
+ message: "stop signal received \u2014 shutting down",
20003
+ iteration: this.iterationCount,
20004
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
20005
+ })
20006
+ );
20007
+ }
18909
20008
  }
18910
20009
  /**
18911
20010
  * Report that a terminal worker (not a Director subagent) completed a claimed
@@ -19353,6 +20452,7 @@ export {
19353
20452
  AutonomousCoordinator,
19354
20453
  BUG_HUNTER_AGENT,
19355
20454
  BUILD_AGENTS,
20455
+ BrainDecisionLedger,
19356
20456
  BrainDecisionQueue,
19357
20457
  BrainMonitor,
19358
20458
  BudgetExceededError,
@@ -19375,6 +20475,7 @@ export {
19375
20475
  DefaultMultiAgentCoordinator,
19376
20476
  Director,
19377
20477
  DirectorAlertLevel,
20478
+ EscalationRoutingBrainArbiter,
19378
20479
  FLEET_ROSTER,
19379
20480
  FLEET_ROSTER_BUDGETS,
19380
20481
  FLEET_ROSTER_WITHACP,
@@ -19424,12 +20525,14 @@ export {
19424
20525
  applyRosterBudget,
19425
20526
  attachAutoExtend,
19426
20527
  attachDepWatcherBridge,
20528
+ brainDecisionKey,
19427
20529
  buildDownAlert,
19428
20530
  buildRecoveryAlert,
19429
20531
  composeDirectorPrompt,
19430
20532
  composeSubagentPrompt,
19431
20533
  createAgentMonitorService,
19432
20534
  createDelegateTool,
20535
+ createLedgerGuardBrainArbiter,
19433
20536
  createMailboxHooks,
19434
20537
  createMessage,
19435
20538
  detectEcosystem,
@@ -19493,6 +20596,7 @@ export {
19493
20596
  scoreAgents,
19494
20597
  startPackageOutdatedWatcher,
19495
20598
  subagentNeedsWorktree,
20599
+ terminalPolicyDecision,
19496
20600
  updatePackageOutdatedStatus,
19497
20601
  validateWatchdogOptions,
19498
20602
  withDisabledToolFiltering,