@wrongstack/core 0.286.0 → 0.287.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 (251) hide show
  1. package/dist/boot.d.ts.map +1 -1
  2. package/dist/coordination/brain-monitor.d.ts +15 -0
  3. package/dist/coordination/brain-monitor.d.ts.map +1 -1
  4. package/dist/coordination/delegate-tool.d.ts +8 -14
  5. package/dist/coordination/delegate-tool.d.ts.map +1 -1
  6. package/dist/coordination/director/director-btw-notes.d.ts +20 -0
  7. package/dist/coordination/director/director-btw-notes.d.ts.map +1 -0
  8. package/dist/coordination/director/director-collab.d.ts +40 -0
  9. package/dist/coordination/director/director-collab.d.ts.map +1 -0
  10. package/dist/coordination/director.d.ts +27 -8
  11. package/dist/coordination/director.d.ts.map +1 -1
  12. package/dist/coordination/fleet-manager.d.ts +21 -0
  13. package/dist/coordination/fleet-manager.d.ts.map +1 -1
  14. package/dist/coordination/fleet.d.ts +2 -3
  15. package/dist/coordination/fleet.d.ts.map +1 -1
  16. package/dist/coordination/global-mailbox.d.ts +36 -22
  17. package/dist/coordination/global-mailbox.d.ts.map +1 -1
  18. package/dist/coordination/icoordinator.d.ts +1 -3
  19. package/dist/coordination/icoordinator.d.ts.map +1 -1
  20. package/dist/coordination/index.js +708 -442
  21. package/dist/coordination/index.js.map +4 -4
  22. package/dist/coordination/mail-tools.d.ts.map +1 -1
  23. package/dist/coordination/mailbox-message-codec.d.ts +21 -1
  24. package/dist/coordination/mailbox-message-codec.d.ts.map +1 -1
  25. package/dist/coordination/mailbox-types.d.ts +42 -1
  26. package/dist/coordination/mailbox-types.d.ts.map +1 -1
  27. package/dist/core/agent-loop.d.ts.map +1 -1
  28. package/dist/core/agent-response.d.ts +10 -1
  29. package/dist/core/agent-response.d.ts.map +1 -1
  30. package/dist/core/agent-tools.d.ts.map +1 -1
  31. package/dist/core/agent.d.ts +8 -0
  32. package/dist/core/agent.d.ts.map +1 -1
  33. package/dist/core/context.d.ts +25 -0
  34. package/dist/core/context.d.ts.map +1 -1
  35. package/dist/core/conversation-state.d.ts +6 -0
  36. package/dist/core/conversation-state.d.ts.map +1 -1
  37. package/dist/core/fallback-model.d.ts.map +1 -1
  38. package/dist/core/mailbox-loop.d.ts +12 -5
  39. package/dist/core/mailbox-loop.d.ts.map +1 -1
  40. package/dist/core/system-prompt-builder.d.ts +14 -6
  41. package/dist/core/system-prompt-builder.d.ts.map +1 -1
  42. package/dist/defaults/index.d.ts +0 -1
  43. package/dist/defaults/index.d.ts.map +1 -1
  44. package/dist/defaults/index.js +834 -1465
  45. package/dist/defaults/index.js.map +4 -4
  46. package/dist/execution/auto-compaction-middleware.d.ts +8 -1
  47. package/dist/execution/auto-compaction-middleware.d.ts.map +1 -1
  48. package/dist/execution/compaction-core.d.ts.map +1 -1
  49. package/dist/execution/council-brain.d.ts.map +1 -1
  50. package/dist/execution/council-orchestrator.d.ts +32 -0
  51. package/dist/execution/council-orchestrator.d.ts.map +1 -0
  52. package/dist/execution/council-personas.d.ts +19 -0
  53. package/dist/execution/council-personas.d.ts.map +1 -0
  54. package/dist/execution/council-profiles.d.ts +34 -0
  55. package/dist/execution/council-profiles.d.ts.map +1 -0
  56. package/dist/execution/council-prompts.d.ts +21 -0
  57. package/dist/execution/council-prompts.d.ts.map +1 -0
  58. package/dist/execution/council-resolution.d.ts +57 -0
  59. package/dist/execution/council-resolution.d.ts.map +1 -0
  60. package/dist/execution/design-kit-loader.d.ts.map +1 -1
  61. package/dist/execution/enhance-recovery.d.ts +35 -0
  62. package/dist/execution/enhance-recovery.d.ts.map +1 -0
  63. package/dist/execution/error-handler.d.ts.map +1 -1
  64. package/dist/execution/index.d.ts +6 -0
  65. package/dist/execution/index.d.ts.map +1 -1
  66. package/dist/execution/index.js +1738 -161
  67. package/dist/execution/index.js.map +4 -4
  68. package/dist/execution/intelligent-compactor.d.ts +18 -7
  69. package/dist/execution/intelligent-compactor.d.ts.map +1 -1
  70. package/dist/execution/one-shot-llm.d.ts +52 -0
  71. package/dist/execution/one-shot-llm.d.ts.map +1 -0
  72. package/dist/execution/prompt-enhancer.d.ts +23 -4
  73. package/dist/execution/prompt-enhancer.d.ts.map +1 -1
  74. package/dist/execution/prompt-enhancer.js +24 -6
  75. package/dist/execution/prompt-enhancer.js.map +2 -2
  76. package/dist/execution/selective-compactor.d.ts +4 -4
  77. package/dist/execution/strategy-compactor.d.ts +7 -0
  78. package/dist/execution/strategy-compactor.d.ts.map +1 -1
  79. package/dist/execution/tool-executor.d.ts +19 -5
  80. package/dist/execution/tool-executor.d.ts.map +1 -1
  81. package/dist/hooks/registry.d.ts.map +1 -1
  82. package/dist/hooks/runner.d.ts.map +1 -1
  83. package/dist/hooks/shell-executor.d.ts.map +1 -1
  84. package/dist/hq/alerts.d.ts +5 -0
  85. package/dist/hq/alerts.d.ts.map +1 -1
  86. package/dist/hq/auth-store.d.ts +17 -7
  87. package/dist/hq/auth-store.d.ts.map +1 -1
  88. package/dist/hq/commands.d.ts +8 -4
  89. package/dist/hq/commands.d.ts.map +1 -1
  90. package/dist/hq/cost-bridge.d.ts +5 -3
  91. package/dist/hq/cost-bridge.d.ts.map +1 -1
  92. package/dist/hq/fleet-bridge.d.ts.map +1 -1
  93. package/dist/hq/index.d.ts +0 -1
  94. package/dist/hq/index.d.ts.map +1 -1
  95. package/dist/hq/index.js +471 -294
  96. package/dist/hq/index.js.map +4 -4
  97. package/dist/hq/persistence.d.ts +42 -0
  98. package/dist/hq/persistence.d.ts.map +1 -1
  99. package/dist/hq/protocol/brain.d.ts +24 -0
  100. package/dist/hq/protocol/brain.d.ts.map +1 -0
  101. package/dist/hq/protocol/browser.d.ts +12 -0
  102. package/dist/hq/protocol/browser.d.ts.map +1 -0
  103. package/dist/hq/protocol/client.d.ts +69 -0
  104. package/dist/hq/protocol/client.d.ts.map +1 -0
  105. package/dist/hq/protocol/core.d.ts +130 -0
  106. package/dist/hq/protocol/core.d.ts.map +1 -0
  107. package/dist/hq/protocol/fleet.d.ts +73 -0
  108. package/dist/hq/protocol/fleet.d.ts.map +1 -0
  109. package/dist/hq/protocol/mailbox.d.ts +76 -0
  110. package/dist/hq/protocol/mailbox.d.ts.map +1 -0
  111. package/dist/hq/protocol/mcp.d.ts +51 -0
  112. package/dist/hq/protocol/mcp.d.ts.map +1 -0
  113. package/dist/hq/protocol/project.d.ts +26 -0
  114. package/dist/hq/protocol/project.d.ts.map +1 -0
  115. package/dist/hq/protocol/session.d.ts +153 -0
  116. package/dist/hq/protocol/session.d.ts.map +1 -0
  117. package/dist/hq/protocol/tool.d.ts +31 -0
  118. package/dist/hq/protocol/tool.d.ts.map +1 -0
  119. package/dist/hq/protocol.d.ts +10 -611
  120. package/dist/hq/protocol.d.ts.map +1 -1
  121. package/dist/index.d.ts +12 -2
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +4540 -2283
  124. package/dist/index.js.map +4 -4
  125. package/dist/infrastructure/index.js +13 -11
  126. package/dist/infrastructure/index.js.map +2 -2
  127. package/dist/infrastructure/token-counter.d.ts.map +1 -1
  128. package/dist/kernel/events/agent-events.d.ts +283 -0
  129. package/dist/kernel/events/agent-events.d.ts.map +1 -0
  130. package/dist/kernel/events/brain-events.d.ts +176 -0
  131. package/dist/kernel/events/brain-events.d.ts.map +1 -0
  132. package/dist/kernel/events/fleet-events.d.ts +39 -0
  133. package/dist/kernel/events/fleet-events.d.ts.map +1 -0
  134. package/dist/kernel/events/memory-events.d.ts +165 -0
  135. package/dist/kernel/events/memory-events.d.ts.map +1 -0
  136. package/dist/kernel/events/provider-events.d.ts +98 -0
  137. package/dist/kernel/events/provider-events.d.ts.map +1 -0
  138. package/dist/kernel/events/sdd-events.d.ts +118 -0
  139. package/dist/kernel/events/sdd-events.d.ts.map +1 -0
  140. package/dist/kernel/events/session-events.d.ts +227 -0
  141. package/dist/kernel/events/session-events.d.ts.map +1 -0
  142. package/dist/kernel/events/tool-events.d.ts +185 -0
  143. package/dist/kernel/events/tool-events.d.ts.map +1 -0
  144. package/dist/kernel/events/worktree-events.d.ts +58 -0
  145. package/dist/kernel/events/worktree-events.d.ts.map +1 -0
  146. package/dist/kernel/events.d.ts +10 -1298
  147. package/dist/kernel/events.d.ts.map +1 -1
  148. package/dist/kernel/index.js +22 -2
  149. package/dist/kernel/index.js.map +2 -2
  150. package/dist/kernel/run-controller.d.ts +15 -0
  151. package/dist/kernel/run-controller.d.ts.map +1 -1
  152. package/dist/mailbox-attach.d.ts.map +1 -1
  153. package/dist/models/index.js +19 -6
  154. package/dist/models/index.js.map +2 -2
  155. package/dist/models/llm-selector.d.ts +7 -0
  156. package/dist/models/llm-selector.d.ts.map +1 -1
  157. package/dist/plugins/auto-review-plugin.d.ts +36 -0
  158. package/dist/plugins/auto-review-plugin.d.ts.map +1 -0
  159. package/dist/plugins/chimera-plugin.d.ts +8 -0
  160. package/dist/plugins/chimera-plugin.d.ts.map +1 -1
  161. package/dist/security/index.js +7 -2
  162. package/dist/security/index.js.map +2 -2
  163. package/dist/security/secret-scrubber.d.ts.map +1 -1
  164. package/dist/skills/index.js.map +2 -2
  165. package/dist/storage/config-loader.d.ts.map +1 -1
  166. package/dist/storage/goal-kanban.d.ts +69 -0
  167. package/dist/storage/goal-kanban.d.ts.map +1 -0
  168. package/dist/storage/index.d.ts +1 -1
  169. package/dist/storage/index.d.ts.map +1 -1
  170. package/dist/storage/index.js +425 -717
  171. package/dist/storage/index.js.map +4 -4
  172. package/dist/storage/memory-consolidator.d.ts +9 -1
  173. package/dist/storage/memory-consolidator.d.ts.map +1 -1
  174. package/dist/tools/council-tool.d.ts +25 -0
  175. package/dist/tools/council-tool.d.ts.map +1 -0
  176. package/dist/tools/index.d.ts +2 -0
  177. package/dist/tools/index.d.ts.map +1 -1
  178. package/dist/tools/index.js +1884 -12
  179. package/dist/tools/index.js.map +4 -4
  180. package/dist/tools/one-shot-llm-tool.d.ts +51 -0
  181. package/dist/tools/one-shot-llm-tool.d.ts.map +1 -0
  182. package/dist/types/config.d.ts +27 -3
  183. package/dist/types/config.d.ts.map +1 -1
  184. package/dist/types/context-window.d.ts.map +1 -1
  185. package/dist/types/council.d.ts +134 -0
  186. package/dist/types/council.d.ts.map +1 -0
  187. package/dist/types/default-config.d.ts +3 -3
  188. package/dist/types/hooks.d.ts +14 -0
  189. package/dist/types/hooks.d.ts.map +1 -1
  190. package/dist/types/index.d.ts +2 -0
  191. package/dist/types/index.d.ts.map +1 -1
  192. package/dist/types/index.js +29 -8
  193. package/dist/types/index.js.map +3 -3
  194. package/dist/types/one-shot-llm.d.ts +128 -0
  195. package/dist/types/one-shot-llm.d.ts.map +1 -0
  196. package/dist/types/plugin.d.ts +6 -0
  197. package/dist/types/plugin.d.ts.map +1 -1
  198. package/dist/types/provider.d.ts +17 -0
  199. package/dist/types/provider.d.ts.map +1 -1
  200. package/dist/types/system-prompt.d.ts +2 -1
  201. package/dist/types/system-prompt.d.ts.map +1 -1
  202. package/dist/types/tool-executor.d.ts +10 -0
  203. package/dist/types/tool-executor.d.ts.map +1 -1
  204. package/dist/utils/atomic-write.d.ts.map +1 -1
  205. package/dist/utils/index.js +75 -8
  206. package/dist/utils/index.js.map +3 -3
  207. package/dist/utils/tool-wire-compact.d.ts +1 -0
  208. package/dist/utils/tool-wire-compact.d.ts.map +1 -1
  209. package/dist/utils/wstack-paths.d.ts +20 -4
  210. package/dist/utils/wstack-paths.d.ts.map +1 -1
  211. package/instructions/cli/next-task-predictor.md +5 -3
  212. package/instructions/coordination/subagent-baseline.md +8 -7
  213. package/instructions/leader-after-task.md +18 -9
  214. package/instructions/llm/council-judge.md +19 -0
  215. package/instructions/llm/council-voter.md +22 -0
  216. package/instructions/modes/architect.md +14 -9
  217. package/instructions/modes/audit-lite.md +9 -7
  218. package/instructions/modes/brief-system.md +12 -12
  219. package/instructions/modes/brief.md +7 -5
  220. package/instructions/modes/code-auditor.md +13 -8
  221. package/instructions/modes/code-reviewer.md +13 -8
  222. package/instructions/modes/debug-lite.md +7 -6
  223. package/instructions/modes/debugger.md +13 -9
  224. package/instructions/modes/devops.md +13 -9
  225. package/instructions/modes/plan-lite.md +7 -6
  226. package/instructions/modes/refactor-lite.md +7 -6
  227. package/instructions/modes/refactorer.md +14 -9
  228. package/instructions/modes/research-lite.md +8 -6
  229. package/instructions/modes/research-web.md +18 -55
  230. package/instructions/modes/review-lite.md +7 -6
  231. package/instructions/modes/teach-system.md +15 -15
  232. package/instructions/modes/teach.md +13 -12
  233. package/instructions/modes/test-lite.md +6 -5
  234. package/instructions/modes/tester.md +13 -9
  235. package/instructions/modes/ui-design.md +14 -8
  236. package/instructions/sections/tool/commit-hygiene.md +1 -7
  237. package/instructions/sections/tool/common-patterns.md +5 -5
  238. package/instructions/sections/tool/delegation-full.md +3 -31
  239. package/instructions/sections/tool/mailbox-compact.md +1 -1
  240. package/instructions/sections/tool/mailbox-full.md +6 -22
  241. package/instructions/system.md +278 -39
  242. package/package.json +2 -2
  243. package/skills/mailbox-bridge/SKILL.md +16 -7
  244. package/skills/output-standards/SKILL.md +16 -16
  245. package/skills/wrongstack-mailbox/SKILL.md +6 -0
  246. package/dist/coordination/director-construction.d.ts +0 -90
  247. package/dist/coordination/director-construction.d.ts.map +0 -1
  248. package/dist/hq/agent-bridge.d.ts +0 -27
  249. package/dist/hq/agent-bridge.d.ts.map +0 -1
  250. package/dist/storage/memory-store.d.ts +0 -119
  251. package/dist/storage/memory-store.d.ts.map +0 -1
@@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto";
4
4
  // src/utils/atomic-write.ts
5
5
  import { randomBytes } from "node:crypto";
6
6
  import * as fs from "node:fs/promises";
7
+ import { watch as watchDir } from "node:fs";
7
8
  import * as path from "node:path";
8
9
  async function atomicWrite(targetPath, content, opts = {}) {
9
10
  const dir = path.dirname(targetPath);
@@ -71,7 +72,7 @@ async function withFileLock(targetPath, fn, opts = {}) {
71
72
  await fs.mkdir(dir, { recursive: true });
72
73
  continue;
73
74
  }
74
- if (code !== "EEXIST") throw err;
75
+ if (code !== "EEXIST" && code !== "EPERM") throw err;
75
76
  try {
76
77
  const stat9 = await fs.stat(lockPath);
77
78
  if (Date.now() - stat9.mtimeMs > staleMs) {
@@ -81,7 +82,8 @@ async function withFileLock(targetPath, fn, opts = {}) {
81
82
  } catch {
82
83
  continue;
83
84
  }
84
- if (Date.now() - started >= timeoutMs) {
85
+ const elapsed = Date.now() - started;
86
+ if (elapsed >= timeoutMs) {
85
87
  throw new FsError({
86
88
  message: `Timed out waiting for file lock: ${targetPath}`,
87
89
  code: "FS_ATOMIC_WRITE_FAILED",
@@ -89,7 +91,7 @@ async function withFileLock(targetPath, fn, opts = {}) {
89
91
  context: { timeoutMs }
90
92
  });
91
93
  }
92
- await new Promise((resolve9) => setTimeout(resolve9, 25));
94
+ await waitForLockRelease(lockPath, timeoutMs - elapsed);
93
95
  }
94
96
  }
95
97
  try {
@@ -105,6 +107,50 @@ async function withFileLock(targetPath, fn, opts = {}) {
105
107
  }
106
108
  }
107
109
  }
110
+ async function waitForLockRelease(lockPath, remainingMs) {
111
+ const parentDir = path.dirname(lockPath);
112
+ const lockName = path.basename(lockPath);
113
+ const intervalMs = Math.min(remainingMs, 100);
114
+ return new Promise((resolve9) => {
115
+ let settled = false;
116
+ let watcher = null;
117
+ const timer = setTimeout(() => {
118
+ settled = true;
119
+ watcher?.close();
120
+ resolve9();
121
+ }, intervalMs);
122
+ try {
123
+ watcher = watchDir(parentDir, (eventType, filename) => {
124
+ if (settled) return;
125
+ if (filename === lockName && (eventType === "rename" || eventType === "change")) {
126
+ settled = true;
127
+ clearTimeout(timer);
128
+ watcher?.close();
129
+ resolve9();
130
+ }
131
+ });
132
+ } catch {
133
+ clearTimeout(timer);
134
+ if (!settled) {
135
+ settled = true;
136
+ setTimeout(resolve9, Math.min(remainingMs, 25));
137
+ }
138
+ return;
139
+ }
140
+ fs.access(lockPath).then(
141
+ () => {
142
+ },
143
+ () => {
144
+ if (!settled) {
145
+ settled = true;
146
+ clearTimeout(timer);
147
+ watcher?.close();
148
+ resolve9();
149
+ }
150
+ }
151
+ );
152
+ });
153
+ }
108
154
  var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
109
155
  async function renameWithRetry(from, to) {
110
156
  if (process.platform !== "win32") {
@@ -587,11 +633,31 @@ function truncate(s, max) {
587
633
 
588
634
  // src/utils/wstack-paths.ts
589
635
  import { createHash } from "node:crypto";
636
+ import * as fs2 from "node:fs";
590
637
  import * as os from "node:os";
591
638
  import * as path3 from "node:path";
639
+ function canonicalProjectRoot(absRoot) {
640
+ const checkoutRoot = path3.resolve(absRoot);
641
+ const dotGit = path3.join(checkoutRoot, ".git");
642
+ try {
643
+ if (!fs2.statSync(dotGit).isFile()) return checkoutRoot;
644
+ const gitDirLine = fs2.readFileSync(dotGit, "utf8").trim();
645
+ const match = /^gitdir:\s*(.+)$/i.exec(gitDirLine);
646
+ if (!match?.[1]) return checkoutRoot;
647
+ const gitDir = path3.resolve(checkoutRoot, match[1].trim());
648
+ const commonDirFile = path3.join(gitDir, "commondir");
649
+ if (!fs2.statSync(commonDirFile).isFile()) return checkoutRoot;
650
+ const commonDir = path3.resolve(gitDir, fs2.readFileSync(commonDirFile, "utf8").trim());
651
+ if (path3.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
652
+ return path3.dirname(commonDir);
653
+ } catch {
654
+ return checkoutRoot;
655
+ }
656
+ }
592
657
  function projectSlug(absRoot) {
593
- const base = slugify(path3.basename(absRoot));
594
- const hash = createHash("sha256").update(path3.resolve(absRoot)).digest("hex").slice(0, 6);
658
+ const identityRoot = canonicalProjectRoot(absRoot);
659
+ const base = slugify(path3.basename(identityRoot));
660
+ const hash = createHash("sha256").update(identityRoot).digest("hex").slice(0, 6);
595
661
  return `${base}-${hash}`;
596
662
  }
597
663
  function slugify(name) {
@@ -1827,7 +1893,7 @@ var TOOLS = {
1827
1893
  };
1828
1894
 
1829
1895
  // src/coordination/agents/agent-prompts.ts
1830
- import { readFileSync, statSync } from "node:fs";
1896
+ import { readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
1831
1897
  import * as os2 from "node:os";
1832
1898
  import * as path4 from "node:path";
1833
1899
  import { fileURLToPath } from "node:url";
@@ -1842,7 +1908,7 @@ function agentPrompt(id) {
1842
1908
  let resolved = "";
1843
1909
  for (const dir of agentPromptDirCandidates(envDir)) {
1844
1910
  try {
1845
- resolved = readFileSync(path4.join(dir, fileName), "utf8").trimEnd();
1911
+ resolved = readFileSync2(path4.join(dir, fileName), "utf8").trimEnd();
1846
1912
  break;
1847
1913
  } catch {
1848
1914
  }
@@ -1872,7 +1938,7 @@ function agentPromptDirCandidates(envDir) {
1872
1938
  }
1873
1939
  function isDirectory(candidate) {
1874
1940
  try {
1875
- return statSync(candidate).isDirectory();
1941
+ return statSync2(candidate).isDirectory();
1876
1942
  } catch {
1877
1943
  return false;
1878
1944
  }
@@ -3624,7 +3690,7 @@ function formatHumanPrompt(request) {
3624
3690
 
3625
3691
  // src/coordination/brain-ledger.ts
3626
3692
  import { appendFile, mkdir as mkdir2, readFile } from "node:fs/promises";
3627
- import { dirname as dirname3 } from "node:path";
3693
+ import { dirname as dirname4 } from "node:path";
3628
3694
  var QUESTION_MAX = 200;
3629
3695
  var DETAIL_MAX = 240;
3630
3696
  var truncate2 = (s, max) => s === void 0 ? void 0 : s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
@@ -3876,7 +3942,7 @@ var BrainDecisionLedger = class {
3876
3942
  append(entry) {
3877
3943
  this.writeChain = this.writeChain.then(async () => {
3878
3944
  if (!this.dirReady) {
3879
- await mkdir2(dirname3(this.opts.filePath), { recursive: true });
3945
+ await mkdir2(dirname4(this.opts.filePath), { recursive: true });
3880
3946
  this.dirReady = true;
3881
3947
  }
3882
3948
  await appendFile(this.opts.filePath, `${JSON.stringify(entry)}
@@ -3952,9 +4018,16 @@ var BrainMonitor = class {
3952
4018
  fileChurnThreshold;
3953
4019
  fileChurnWindowMs;
3954
4020
  cooldownMs;
4021
+ /** Resolve the leader's own session id for event filtering. */
4022
+ resolveLeaderSessionId() {
4023
+ const id = this.opts.leaderSessionId;
4024
+ return typeof id === "function" ? id() : id;
4025
+ }
3955
4026
  start() {
3956
4027
  this.unsubscribers.push(
3957
4028
  this.opts.events.on("tool.executed", (e) => {
4029
+ const leaderSid = this.resolveLeaderSessionId();
4030
+ if (leaderSid && e.sessionId && e.sessionId !== leaderSid) return;
3958
4031
  this.lastProgressAt = Date.now();
3959
4032
  this.trackFileChurn(e.name, e.ok, e.input);
3960
4033
  if (e.ok) {
@@ -3978,17 +4051,25 @@ var BrainMonitor = class {
3978
4051
  );
3979
4052
  if (this.stallMs > 0) {
3980
4053
  this.unsubscribers.push(
3981
- this.opts.events.on("agent.run.started", () => {
4054
+ this.opts.events.on("agent.run.started", (e) => {
4055
+ const lsid = this.resolveLeaderSessionId();
4056
+ if (lsid && e.sessionId && e.sessionId !== lsid) return;
3982
4057
  this.activeRuns += 1;
3983
4058
  this.lastProgressAt = Date.now();
3984
4059
  }),
3985
- this.opts.events.on("agent.run.completed", () => {
4060
+ this.opts.events.on("agent.run.completed", (e) => {
4061
+ const lsid = this.resolveLeaderSessionId();
4062
+ if (lsid && e.sessionId && e.sessionId !== lsid) return;
3986
4063
  this.activeRuns = Math.max(0, this.activeRuns - 1);
3987
4064
  }),
3988
- this.opts.events.on("agent.run.error", () => {
4065
+ this.opts.events.on("agent.run.error", (e) => {
4066
+ const lsid = this.resolveLeaderSessionId();
4067
+ if (lsid && e.sessionId && e.sessionId !== lsid) return;
3989
4068
  this.activeRuns = Math.max(0, this.activeRuns - 1);
3990
4069
  }),
3991
- this.opts.events.on("iteration.started", () => {
4070
+ this.opts.events.on("iteration.started", (e) => {
4071
+ const lsid = this.resolveLeaderSessionId();
4072
+ if (lsid && e.sessionId && e.sessionId !== lsid) return;
3992
4073
  this.lastProgressAt = Date.now();
3993
4074
  })
3994
4075
  );
@@ -4010,6 +4091,8 @@ var BrainMonitor = class {
4010
4091
  }
4011
4092
  this.unsubscribers.push(
4012
4093
  this.opts.events.on("error", (e) => {
4094
+ const lsid = this.resolveLeaderSessionId();
4095
+ if (lsid && e.sessionId && e.sessionId !== lsid) return;
4013
4096
  const now = Date.now();
4014
4097
  this.errorTimestamps.push(now);
4015
4098
  this.errorTimestamps = this.errorTimestamps.filter(
@@ -4895,19 +4978,19 @@ var FLEET_ROSTER = {
4895
4978
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.config])
4896
4979
  )
4897
4980
  };
4898
- var DEFAULT_IDLE_TIMEOUT_MS = 15 * 60 * 1e3;
4981
+ var DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
4899
4982
  var FLEET_ROSTER_BUDGETS = {
4900
- "audit-log": { timeoutMs: 7.5 * 60 * 60 * 1e3, maxIterations: 5e3, maxToolCalls: 15e3 },
4901
- "bug-hunter": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
4902
- "refactor-planner": { timeoutMs: 7.5 * 60 * 60 * 1e3, maxIterations: 6e3, maxToolCalls: 18e3 },
4903
- "security-scanner": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
4904
- critic: { timeoutMs: 5 * 60 * 60 * 1e3, maxIterations: 4e3, maxToolCalls: 12e3 },
4983
+ "audit-log": { timeoutMs: 3 * 60 * 60 * 1e3, maxIterations: 2500, maxToolCalls: 7500 },
4984
+ "bug-hunter": { timeoutMs: 5 * 60 * 60 * 1e3, maxIterations: 4e3, maxToolCalls: 1e4 },
4985
+ "refactor-planner": { timeoutMs: 4 * 60 * 60 * 1e3, maxIterations: 3e3, maxToolCalls: 9e3 },
4986
+ "security-scanner": { timeoutMs: 5 * 60 * 60 * 1e3, maxIterations: 4e3, maxToolCalls: 1e4 },
4987
+ critic: { timeoutMs: 3 * 60 * 60 * 1e3, maxIterations: 2e3, maxToolCalls: 6e3 },
4905
4988
  "shadow-agent": {
4906
4989
  idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
4907
- maxIterations: 2e3,
4908
- maxToolCalls: 5e3,
4909
- maxTokens: 6e4,
4910
- maxCostUsd: 1
4990
+ maxIterations: 1e3,
4991
+ maxToolCalls: 2500,
4992
+ maxTokens: 3e4,
4993
+ maxCostUsd: 0.5
4911
4994
  },
4912
4995
  ...Object.fromEntries(
4913
4996
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.budget])
@@ -4915,8 +4998,8 @@ var FLEET_ROSTER_BUDGETS = {
4915
4998
  };
4916
4999
  var GENERIC_SUBAGENT_BUDGET = {
4917
5000
  idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
4918
- maxIterations: 5e3,
4919
- maxToolCalls: 15e3
5001
+ maxIterations: 2500,
5002
+ maxToolCalls: 7500
4920
5003
  };
4921
5004
  function applyRosterBudget(cfg) {
4922
5005
  const roleBudget = cfg.role ? FLEET_ROSTER_BUDGETS[cfg.role] : void 0;
@@ -4972,72 +5055,71 @@ function createDelegateTool(opts) {
4972
5055
  properties: {
4973
5056
  task: {
4974
5057
  type: "string",
4975
- description: "What the subagent should do \u2014 natural language, complete sentence(s). The subagent has its own tool slice, its own LLM call, and returns when its task is done."
5058
+ description: "What the subagent should do \u2014 natural language, complete sentence(s)."
4976
5059
  },
4977
5060
  role: {
4978
5061
  type: "string",
4979
- description: rosterIds.length > 0 ? `Roster role (preferred). One of: ${rosterIds.join(", ")}. Picks a pre-tuned config (prompt, budgets, tools) for that role.` : "No roster is configured \u2014 pass `name` instead.",
4980
- enum: rosterIds.length > 0 ? rosterIds : void 0
5062
+ description: rosterIds.length > 0 ? "Roster role id. Common: bug-hunter, security-scanner, refactor-planner, critic, audit-log, executor, shadow-agent, architect." : "No roster configured \u2014 pass `name` instead."
4981
5063
  },
4982
5064
  name: {
4983
5065
  type: "string",
4984
- description: "Display name for free-form subagents (not using a roster role). The subagent gets a large default budget (3h, 5000 iter, 15000 tool calls). Required when `role` is omitted."
5066
+ description: "Display name for free-form subagents (no roster role). Required when `role` is omitted."
4985
5067
  },
4986
5068
  provider: {
4987
5069
  type: "string",
4988
- description: 'Provider id (e.g. "anthropic", "openai"). Defaults to the host provider when omitted.'
5070
+ description: 'Provider id (e.g. "anthropic", "openai"). Defaults to host provider.'
4989
5071
  },
4990
5072
  model: {
4991
5073
  type: "string",
4992
- description: "Model id within the provider. Defaults to the host model when omitted."
5074
+ description: "Model id within the provider. Defaults to host model."
4993
5075
  },
4994
5076
  systemPromptOverride: {
4995
5077
  type: "string",
4996
- description: "Optional extra prompt text appended to the role baseline."
5078
+ description: "Extra prompt text appended to the role baseline."
4997
5079
  },
4998
5080
  timeoutMs: {
4999
5081
  type: "number",
5000
5082
  minimum: 1,
5001
- description: `Wall-clock budget for this delegate in milliseconds. No hard cap \u2014 set as high as the task realistically needs (a monorepo audit can take hours, a single-file lint takes seconds). Default ${Math.round(defaultTimeoutMs / 1e3 / 60)} minutes.`
5083
+ description: `Wall-clock budget in ms (default ${Math.round(defaultTimeoutMs / 1e3 / 60)} min). No hard cap \u2014 set as high as the task needs.`
5002
5084
  },
5003
5085
  maxIterations: {
5004
5086
  type: "number",
5005
5087
  minimum: 1,
5006
- description: "Maximum LLM iterations the subagent may take. Unset = use the role/coordinator default. Raise this for tasks with many tool-think-tool cycles (deep code analysis, multi-file refactors)."
5088
+ description: "Maximum LLM iterations. Unset = role default. Raise for deep multi-step tasks."
5007
5089
  },
5008
5090
  maxToolCalls: {
5009
5091
  type: "number",
5010
5092
  minimum: 1,
5011
- description: "Maximum number of tool invocations the subagent may make. Unset = use the role/coordinator default. Raise this for tasks that touch many files (large grep + read + report)."
5093
+ description: "Maximum tool invocations. Unset = role default. Raise for file-heavy tasks."
5012
5094
  },
5013
5095
  idleTimeoutMs: {
5014
5096
  type: "number",
5015
5097
  minimum: 1,
5016
- description: "Idle timeout in ms: reap the subagent after this long with no activity. Resets on every iteration/tool call. Unset = use the role/coordinator default."
5098
+ description: "Idle timeout in ms. Resets on activity. Unset = role default."
5017
5099
  },
5018
5100
  maxTokens: {
5019
5101
  type: "number",
5020
5102
  minimum: 1,
5021
- description: "Maximum total tokens (input + output) the subagent may use. Unset = use the role/coordinator default."
5103
+ description: "Max total tokens (input+output). Unset = role default."
5022
5104
  },
5023
5105
  maxCostUsd: {
5024
5106
  type: "number",
5025
5107
  minimum: 0,
5026
- description: "Maximum estimated USD cost the subagent may incur. Unset = use the role/coordinator default."
5108
+ description: "Max estimated USD cost. Unset = role default."
5027
5109
  },
5028
5110
  maxHandoffs: {
5029
5111
  type: "number",
5030
5112
  minimum: 0,
5031
5113
  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."
5114
+ description: "Max fresh-worker continuations after budget exhaustion. Default 1. Each gets the prior partial report."
5033
5115
  }
5034
5116
  },
5035
5117
  required: ["task"]
5036
5118
  };
5037
5119
  return {
5038
5120
  name: "delegate",
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.",
5121
+ description: "Hand a piece of work to a subagent and wait for its result. Has own context, LLM call, auto-extending budget. Can continue remaining work with a fresh subagent on partial completion (maxHandoffs, default 1). Workers cannot recursively spawn. Use roster roles when possible; otherwise pass `name` + `task`.",
5122
+ usageHint: "Set `task` to a complete instruction. Pick `role` from roster or pass `name` for free-form. Override `timeoutMs`/`maxIterations`/`maxToolCalls` only when needed.",
5041
5123
  permission: "auto",
5042
5124
  mutating: false,
5043
5125
  managesOwnTimeout: true,
@@ -5064,10 +5146,9 @@ function createDelegateTool(opts) {
5064
5146
  director = await opts.host.promoteToDirector();
5065
5147
  }
5066
5148
  if (!director) {
5067
- const reason = opts.host.getPromotionBlockReason?.();
5068
5149
  return {
5069
5150
  ok: false,
5070
- error: reason ?? "Director could not be activated \u2014 multi-agent host already running in legacy non-director mode. Restart with `--director` for fleet support."
5151
+ error: "Director could not be activated \u2014 fleet orchestration is unavailable."
5071
5152
  };
5072
5153
  }
5073
5154
  const timeoutMs = i.timeoutMs ?? defaultTimeoutMs;
@@ -5938,8 +6019,86 @@ var FleetContextOverflowError = class extends Error {
5938
6019
  }
5939
6020
  };
5940
6021
 
6022
+ // src/coordination/director/director-collab.ts
6023
+ var DirectorCollabController = class {
6024
+ constructor(deps) {
6025
+ this.deps = deps;
6026
+ }
6027
+ deps;
6028
+ /** Active collab sessions tracked by sessionId (see spawn).
6029
+ * The tuple holds the session and its Director-registered listener unsubs.
6030
+ * Calling the unsubs on cancel/premature-cleanup prevents listener accumulation
6031
+ * on CollabSession (EventEmitter) across many spawn() calls. */
6032
+ activeSessions = /* @__PURE__ */ new Map();
6033
+ async spawn(options) {
6034
+ const session = new CollabSession(this.deps.director, this.deps.fleet, {
6035
+ ...options,
6036
+ onBudgetWarning: (alert) => {
6037
+ return options.onBudgetWarning?.(alert) ?? "ignore";
6038
+ }
6039
+ });
6040
+ const doneHandler = () => this.activeSessions.delete(session.sessionId);
6041
+ const errorHandler = () => this.activeSessions.delete(session.sessionId);
6042
+ session.on("session.done", doneHandler);
6043
+ session.on("session.error", errorHandler);
6044
+ const unsubs = [
6045
+ () => session.off("session.done", doneHandler),
6046
+ () => session.off("session.error", errorHandler)
6047
+ ];
6048
+ this.activeSessions.set(session.sessionId, { session, unsubs });
6049
+ return session.start();
6050
+ }
6051
+ cancel(sessionId, reason = "Director cancelled") {
6052
+ const entry = this.activeSessions.get(sessionId);
6053
+ if (!entry || entry.session.isCancelled()) return;
6054
+ for (const unsub of entry.unsubs) unsub();
6055
+ entry.session.cancel(reason);
6056
+ for (const [_role, subagentId] of entry.session.getSubagentIds()) {
6057
+ this.deps.coordinator.stop(subagentId).catch((err) => {
6058
+ this.deps.logger?.debug(
6059
+ `stop subagent ${subagentId} failed (may have already completed)`,
6060
+ {
6061
+ subagentId,
6062
+ err: err instanceof Error ? err.message : String(err)
6063
+ }
6064
+ );
6065
+ });
6066
+ }
6067
+ }
6068
+ onAlert(handler) {
6069
+ return this.deps.fleet.filter("collab.warning", (e) => {
6070
+ handler(e.payload);
6071
+ });
6072
+ }
6073
+ activeSessionIds() {
6074
+ return Array.from(this.activeSessions.keys());
6075
+ }
6076
+ };
6077
+
6078
+ // src/coordination/director/director-btw-notes.ts
6079
+ var DirectorBtwNotes = class {
6080
+ notes = [];
6081
+ /** Stash a note (trimmed; empty ignored). Returns the pending count. Cap 20. */
6082
+ add(note) {
6083
+ const trimmed = note.trim();
6084
+ if (!trimmed) return this.notes.length;
6085
+ this.notes = [...this.notes, trimmed].slice(-20);
6086
+ return this.notes.length;
6087
+ }
6088
+ /** Read and clear all pending notes, in FIFO order (empty array when none). */
6089
+ drain() {
6090
+ const notes = this.notes;
6091
+ this.notes = [];
6092
+ return notes;
6093
+ }
6094
+ /** Peek at pending notes without consuming them. */
6095
+ peek() {
6096
+ return [...this.notes];
6097
+ }
6098
+ };
6099
+
5941
6100
  // src/utils/instruction-file.ts
5942
- import { readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
6101
+ import { readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
5943
6102
  import * as path6 from "node:path";
5944
6103
  import { fileURLToPath as fileURLToPath2 } from "node:url";
5945
6104
  var textCache = /* @__PURE__ */ new Map();
@@ -5950,7 +6109,7 @@ function readBundledInstructionText(relativePath) {
5950
6109
  let resolved = "";
5951
6110
  for (const root of instructionRootCandidates()) {
5952
6111
  try {
5953
- resolved = readFileSync2(path6.join(root, relativePath), "utf8").trimEnd();
6112
+ resolved = readFileSync3(path6.join(root, relativePath), "utf8").trimEnd();
5954
6113
  break;
5955
6114
  } catch {
5956
6115
  }
@@ -5977,7 +6136,7 @@ function instructionRootCandidates() {
5977
6136
  }
5978
6137
  function isDirectory2(candidate) {
5979
6138
  try {
5980
- return statSync2(candidate).isDirectory();
6139
+ return statSync3(candidate).isDirectory();
5981
6140
  } catch {
5982
6141
  return false;
5983
6142
  }
@@ -9485,13 +9644,12 @@ var Director = class _Director {
9485
9644
  * coordinator from keeping workers alive for tasks that will never arrive.
9486
9645
  */
9487
9646
  workCompleteFlag = false;
9488
- /** Pending /btw notes stashed by the leader agent (see setLeaderBtwNote). */
9489
- _leaderBtwNotes = [];
9490
- /** Active collab sessions tracked by sessionId (see spawnCollab).
9491
- * The tuple holds the session and its Director-registered listener unsubs.
9492
- * Calling the unsubs on cancel/premature-cleanup prevents listener accumulation
9493
- * on CollabSession (EventEmitter) across many spawnCollab() calls. */
9494
- _activeCollabSessions = /* @__PURE__ */ new Map();
9647
+ /** Pending /btw notes stashed by the leader agent (see setLeaderBtwNote).
9648
+ * Owned by DirectorBtwNotes (R4); the public btw methods delegate to it. */
9649
+ btwNotes = new DirectorBtwNotes();
9650
+ /** Owns active collab-debug sessions (spawn/cancel/alert/list). Extracted
9651
+ * from Director in R4; the public collab methods below delegate to it. */
9652
+ collab;
9495
9653
  /** Prevents large `ask_subagent` answers from bloating the leader's context window. */
9496
9654
  largeAnswerStore;
9497
9655
  constructor(opts) {
@@ -9568,92 +9726,103 @@ var Director = class _Director {
9568
9726
  );
9569
9727
  this.coordinator.setFleetBus(this.fleet);
9570
9728
  this.fleetManager?.setCoordinator(this.coordinator);
9571
- this.taskCompletedListener = (payload) => {
9572
- const r = payload.result;
9573
- const internalTask = this.internalTaskIds.delete(r.taskId);
9574
- if (!internalTask) {
9575
- this.completed.set(r.taskId, r);
9576
- if (this.completed.size > _Director.MAX_COMPLETED) {
9577
- const toDelete = this.completed.size - _Director.MAX_COMPLETED;
9578
- const keys = [...this.completed.keys()].slice(0, toDelete);
9579
- for (const k of keys) this.completed.delete(k);
9580
- }
9581
- }
9582
- const waiter = this.taskWaiters.get(r.taskId);
9583
- if (waiter) {
9584
- waiter.resolve(r);
9585
- this.taskWaiters.delete(r.taskId);
9586
- }
9587
- let anyConsumed = false;
9588
- for (const aw of [...this.anyWaiters]) {
9589
- if (!aw.ids.has(r.taskId)) continue;
9590
- if (aw.timer) clearTimeout(aw.timer);
9591
- this.anyWaiters.delete(aw);
9592
- aw.resolve(r);
9593
- anyConsumed = true;
9594
- }
9595
- const consumedInBand = !!waiter || anyConsumed;
9596
- if (internalTask) return;
9597
- const title = this.taskDescriptions.get(r.taskId) ?? payload.task.description ?? r.taskId;
9598
- if (!consumedInBand && this.taskResultNotifier) {
9599
- const resultText = typeof r.result === "string" ? r.result : r.result !== void 0 ? safeStringify(r.result) : void 0;
9600
- try {
9601
- void Promise.resolve(
9602
- this.taskResultNotifier({
9603
- taskId: r.taskId,
9604
- title,
9605
- status: r.status,
9606
- subagentId: r.subagentId,
9607
- subagentName: this.manifestEntries.get(r.subagentId)?.name,
9608
- resultText,
9609
- errorText: r.error ? `${r.error.kind}: ${r.error.message}` : void 0,
9610
- partialText: r.partial?.text,
9611
- report: r.report,
9612
- iterations: r.iterations,
9613
- toolCalls: r.toolCalls,
9614
- durationMs: r.durationMs
9615
- })
9616
- ).catch(() => {
9617
- });
9618
- } catch {
9619
- }
9729
+ this.taskCompletedListener = (payload) => this.handleTaskCompleted(payload);
9730
+ this.coordinator.on("task.completed", this.taskCompletedListener);
9731
+ this.wireBudgetPolicy();
9732
+ this.largeAnswerStore = new LargeAnswerStore(2e3);
9733
+ this.collab = new DirectorCollabController({
9734
+ director: this,
9735
+ fleet: this.fleet,
9736
+ coordinator: this.coordinator,
9737
+ logger: this.logger
9738
+ });
9739
+ }
9740
+ handleTaskCompleted(payload) {
9741
+ const r = payload.result;
9742
+ const internalTask = this.internalTaskIds.delete(r.taskId);
9743
+ if (!internalTask) {
9744
+ this.completed.set(r.taskId, r);
9745
+ if (this.completed.size > _Director.MAX_COMPLETED) {
9746
+ const toDelete = this.completed.size - _Director.MAX_COMPLETED;
9747
+ const keys = [...this.completed.keys()].slice(0, toDelete);
9748
+ for (const k of keys) this.completed.delete(k);
9749
+ }
9750
+ }
9751
+ const waiter = this.taskWaiters.get(r.taskId);
9752
+ if (waiter) {
9753
+ waiter.resolve(r);
9754
+ this.taskWaiters.delete(r.taskId);
9755
+ }
9756
+ let anyConsumed = false;
9757
+ for (const aw of [...this.anyWaiters]) {
9758
+ if (!aw.ids.has(r.taskId)) continue;
9759
+ if (aw.timer) clearTimeout(aw.timer);
9760
+ this.anyWaiters.delete(aw);
9761
+ aw.resolve(r);
9762
+ anyConsumed = true;
9763
+ }
9764
+ const consumedInBand = !!waiter || anyConsumed;
9765
+ if (internalTask) return;
9766
+ const title = this.taskDescriptions.get(r.taskId) ?? payload.task.description ?? r.taskId;
9767
+ if (!consumedInBand && this.taskResultNotifier) {
9768
+ const resultText = typeof r.result === "string" ? r.result : r.result !== void 0 ? safeStringify(r.result) : void 0;
9769
+ try {
9770
+ void Promise.resolve(
9771
+ this.taskResultNotifier({
9772
+ taskId: r.taskId,
9773
+ title,
9774
+ status: r.status,
9775
+ subagentId: r.subagentId,
9776
+ subagentName: this.manifestEntries.get(r.subagentId)?.name,
9777
+ resultText,
9778
+ errorText: r.error ? `${r.error.kind}: ${r.error.message}` : void 0,
9779
+ partialText: r.partial?.text,
9780
+ report: r.report,
9781
+ iterations: r.iterations,
9782
+ toolCalls: r.toolCalls,
9783
+ durationMs: r.durationMs
9784
+ })
9785
+ ).catch(() => {
9786
+ });
9787
+ } catch {
9620
9788
  }
9621
- const failed = r.status !== "success";
9622
- const errorString = r.error ? `${r.error.kind}: ${r.error.message}` : void 0;
9623
- this.stateCheckpoint?.recordTaskStatus(r.taskId, {
9624
- status: failed ? r.status : "completed",
9625
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
9626
- iterations: r.iterations,
9627
- toolCalls: r.toolCalls,
9628
- durationMs: r.durationMs,
9629
- error: errorString
9630
- });
9631
- this.stateCheckpoint?.setUsage(this.usage.snapshot());
9632
- void this.appendSessionEvent(
9633
- failed ? {
9634
- type: "task_failed",
9635
- ts: (/* @__PURE__ */ new Date()).toISOString(),
9636
- taskId: r.taskId,
9637
- title,
9638
- error: errorString ?? r.status
9639
- } : {
9640
- type: "task_completed",
9641
- ts: (/* @__PURE__ */ new Date()).toISOString(),
9642
- taskId: r.taskId,
9643
- title
9644
- }
9645
- );
9646
- if (this.fleetManager) {
9647
- void this.fleetManager.flushManifest();
9648
- } else {
9649
- this.scheduleManifest();
9789
+ }
9790
+ const failed = r.status !== "success";
9791
+ const errorString = r.error ? `${r.error.kind}: ${r.error.message}` : void 0;
9792
+ this.stateCheckpoint?.recordTaskStatus(r.taskId, {
9793
+ status: failed ? r.status : "completed",
9794
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
9795
+ iterations: r.iterations,
9796
+ toolCalls: r.toolCalls,
9797
+ durationMs: r.durationMs,
9798
+ error: errorString
9799
+ });
9800
+ this.stateCheckpoint?.setUsage(this.usage.snapshot());
9801
+ void this.appendSessionEvent(
9802
+ failed ? {
9803
+ type: "task_failed",
9804
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
9805
+ taskId: r.taskId,
9806
+ title,
9807
+ error: errorString ?? r.status
9808
+ } : {
9809
+ type: "task_completed",
9810
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
9811
+ taskId: r.taskId,
9812
+ title
9650
9813
  }
9651
- this.armSubagentIdleRetirement(
9652
- r.subagentId,
9653
- this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleTimeoutMs
9654
- );
9655
- };
9656
- this.coordinator.on("task.completed", this.taskCompletedListener);
9814
+ );
9815
+ if (this.fleetManager) {
9816
+ void this.fleetManager.flushManifest();
9817
+ } else {
9818
+ this.scheduleManifest();
9819
+ }
9820
+ this.armSubagentIdleRetirement(
9821
+ r.subagentId,
9822
+ this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleTimeoutMs
9823
+ );
9824
+ }
9825
+ wireBudgetPolicy() {
9657
9826
  const extendCounts = /* @__PURE__ */ new Map();
9658
9827
  const progressBySubagent = /* @__PURE__ */ new Map();
9659
9828
  const lastTimeoutProgress = /* @__PURE__ */ new Map();
@@ -9773,7 +9942,6 @@ var Director = class _Director {
9773
9942
  }
9774
9943
  grantExtension();
9775
9944
  });
9776
- this.largeAnswerStore = new LargeAnswerStore(2e3);
9777
9945
  }
9778
9946
  /**
9779
9947
  * Record a granted budget extension and broadcast it on the FleetBus so
@@ -9831,10 +9999,7 @@ var Director = class _Director {
9831
9999
  * programmatically without going through the slash-command path.
9832
10000
  */
9833
10001
  setLeaderBtwNote(note) {
9834
- const trimmed = note.trim();
9835
- if (!trimmed) return this._leaderBtwNotes.length;
9836
- this._leaderBtwNotes = [...this._leaderBtwNotes, trimmed].slice(-20);
9837
- return this._leaderBtwNotes.length;
10002
+ return this.btwNotes.add(note);
9838
10003
  }
9839
10004
  /**
9840
10005
  * Read and clear all pending /btw notes the leader has stashed.
@@ -9845,16 +10010,14 @@ var Director = class _Director {
9845
10010
  * to cancel the collab session or let it continue.
9846
10011
  */
9847
10012
  getLeaderBtwNotes() {
9848
- const notes = this._leaderBtwNotes;
9849
- this._leaderBtwNotes = [];
9850
- return notes;
10013
+ return this.btwNotes.drain();
9851
10014
  }
9852
10015
  /**
9853
10016
  * Peek at pending /btw notes without consuming them.
9854
10017
  * Useful for UI to show "N pending notes" without clearing them.
9855
10018
  */
9856
10019
  peekLeaderBtwNotes() {
9857
- return [...this._leaderBtwNotes];
10020
+ return this.btwNotes.peek();
9858
10021
  }
9859
10022
  /**
9860
10023
  * Drain (read + clear) all /btw notes in one call.
@@ -9874,18 +10037,7 @@ var Director = class _Director {
9874
10037
  * The CollabDebugReport will reflect 'cancelled' disposition when awaited.
9875
10038
  */
9876
10039
  cancelCollabSession(sessionId, reason = "Director cancelled") {
9877
- const entry = this._activeCollabSessions.get(sessionId);
9878
- if (!entry || entry.session.isCancelled()) return;
9879
- for (const unsub of entry.unsubs) unsub();
9880
- entry.session.cancel(reason);
9881
- for (const [_role, subagentId] of entry.session.getSubagentIds()) {
9882
- this.coordinator.stop(subagentId).catch((err) => {
9883
- this.logger?.debug(`stop subagent ${subagentId} failed (may have already completed)`, {
9884
- subagentId,
9885
- err: err instanceof Error ? err.message : String(err)
9886
- });
9887
- });
9888
- }
10040
+ this.collab.cancel(sessionId, reason);
9889
10041
  }
9890
10042
  /**
9891
10043
  * Subscribe a callback to be notified whenever a collab session raises
@@ -9894,9 +10046,7 @@ var Director = class _Director {
9894
10046
  * Returns an unsubscribe function.
9895
10047
  */
9896
10048
  onCollabAlert(handler) {
9897
- return this.fleet.filter("collab.warning", (e) => {
9898
- handler(e.payload);
9899
- });
10049
+ return this.collab.onAlert(handler);
9900
10050
  }
9901
10051
  /**
9902
10052
  * Returns all active (not yet settled) collab session ids.
@@ -9904,7 +10054,7 @@ var Director = class _Director {
9904
10054
  * the host to know what can be cancelled.
9905
10055
  */
9906
10056
  activeCollabSessions() {
9907
- return Array.from(this._activeCollabSessions.keys());
10057
+ return this.collab.activeSessionIds();
9908
10058
  }
9909
10059
  /** Best-effort session-writer append. Swallows failures — the director
9910
10060
  * must not break a fleet run because the session JSONL handle closed. */
@@ -9970,6 +10120,77 @@ var Director = class _Director {
9970
10120
  );
9971
10121
  }
9972
10122
  const config = { ...callerConfig };
10123
+ this.resolveSpawnModel(config);
10124
+ this.enforceSpawnCaps(config);
10125
+ let result;
10126
+ this.assignSpawnNickname(config);
10127
+ this.applySpawnLineage(config);
10128
+ result = await this.coordinator.spawn(config);
10129
+ if (this.fleetManager) {
10130
+ this.fleetManager.recordSpawn(result.subagentId, config, priceLookup);
10131
+ } else {
10132
+ this.spawnCount += 1;
10133
+ this.subagentMeta.set(result.subagentId, {
10134
+ provider: config.provider,
10135
+ model: config.model
10136
+ });
10137
+ if (priceLookup && config.provider && config.model) {
10138
+ this.priceLookups.set(`${config.provider}/${config.model}`, priceLookup);
10139
+ }
10140
+ }
10141
+ const subagentBridge = new InMemoryAgentBridge(
10142
+ { agentId: result.subagentId, coordinatorId: this.id },
10143
+ this.transport
10144
+ );
10145
+ this.coordinator.setSubagentBridge(result.subagentId, subagentBridge);
10146
+ this.subagentBridges.set(result.subagentId, subagentBridge);
10147
+ this.fleet.emit({
10148
+ subagentId: result.subagentId,
10149
+ ts: Date.now(),
10150
+ type: "subagent.spawned",
10151
+ payload: {
10152
+ subagentId: result.subagentId,
10153
+ taskId: "",
10154
+ // taskId will be set when assign() is called
10155
+ name: config.name,
10156
+ role: config.role,
10157
+ provider: config.provider,
10158
+ model: config.model
10159
+ }
10160
+ });
10161
+ if (!this.fleetManager) {
10162
+ this.manifestEntries.set(result.subagentId, {
10163
+ subagentId: result.subagentId,
10164
+ name: config.name,
10165
+ role: config.role,
10166
+ provider: config.provider,
10167
+ model: config.model,
10168
+ taskIds: []
10169
+ });
10170
+ const spawnedAt = (/* @__PURE__ */ new Date()).toISOString();
10171
+ this.stateCheckpoint?.recordSpawn(
10172
+ {
10173
+ id: result.subagentId,
10174
+ name: config.name,
10175
+ role: config.role,
10176
+ provider: config.provider,
10177
+ model: config.model,
10178
+ spawnedAt
10179
+ },
10180
+ this.spawnCount
10181
+ );
10182
+ void this.appendSessionEvent({
10183
+ type: "agent_spawned",
10184
+ ts: spawnedAt,
10185
+ agentId: result.subagentId,
10186
+ role: config.role ?? config.name
10187
+ });
10188
+ this.scheduleManifest();
10189
+ }
10190
+ this.armSubagentIdleRetirement(result.subagentId, this.subagentIdleTimeoutMs);
10191
+ return result.subagentId;
10192
+ }
10193
+ resolveSpawnModel(config) {
9973
10194
  if (!config.model && this.modelMatrix) {
9974
10195
  const matrix = typeof this.modelMatrix === "function" ? this.modelMatrix() : this.modelMatrix;
9975
10196
  const resolution = resolveModelMatrixResolution(matrix, config.role);
@@ -9980,6 +10201,8 @@ var Director = class _Director {
9980
10201
  if (entry.modelRuntime) config.modelRuntime = entry.modelRuntime;
9981
10202
  }
9982
10203
  }
10204
+ }
10205
+ enforceSpawnCaps(config) {
9983
10206
  if (this.fleetManager) {
9984
10207
  const rejection = this.fleetManager.canSpawn(config);
9985
10208
  if (rejection) {
@@ -10022,7 +10245,8 @@ var Director = class _Director {
10022
10245
  }
10023
10246
  }
10024
10247
  }
10025
- let result;
10248
+ }
10249
+ assignSpawnNickname(config) {
10026
10250
  const needsNickname = config.name === config.role || !config.name || config.name === "subagent" || config.name === "adhoc";
10027
10251
  if (needsNickname) {
10028
10252
  const role = config.role ?? "subagent";
@@ -10034,6 +10258,8 @@ var Director = class _Director {
10034
10258
  this._usedNicknames.add(key);
10035
10259
  }
10036
10260
  }
10261
+ }
10262
+ applySpawnLineage(config) {
10037
10263
  const budget = this.fleetManager ? this.fleetManager.budgetSnapshot() : (() => {
10038
10264
  const total = this.usage.snapshot().total;
10039
10265
  const usedTokens = (total?.input ?? 0) + (total?.output ?? 0);
@@ -10062,70 +10288,6 @@ var Director = class _Director {
10062
10288
  ...Number.isFinite(budget.remainingCostUsd) ? { remainingCostUsd: budget.remainingCostUsd } : {}
10063
10289
  }
10064
10290
  };
10065
- result = await this.coordinator.spawn(config);
10066
- if (this.fleetManager) {
10067
- this.fleetManager.recordSpawn(result.subagentId, config, priceLookup);
10068
- } else {
10069
- this.spawnCount += 1;
10070
- this.subagentMeta.set(result.subagentId, {
10071
- provider: config.provider,
10072
- model: config.model
10073
- });
10074
- if (priceLookup && config.provider && config.model) {
10075
- this.priceLookups.set(`${config.provider}/${config.model}`, priceLookup);
10076
- }
10077
- }
10078
- const subagentBridge = new InMemoryAgentBridge(
10079
- { agentId: result.subagentId, coordinatorId: this.id },
10080
- this.transport
10081
- );
10082
- this.coordinator.setSubagentBridge(result.subagentId, subagentBridge);
10083
- this.subagentBridges.set(result.subagentId, subagentBridge);
10084
- this.fleet.emit({
10085
- subagentId: result.subagentId,
10086
- ts: Date.now(),
10087
- type: "subagent.spawned",
10088
- payload: {
10089
- subagentId: result.subagentId,
10090
- taskId: "",
10091
- // taskId will be set when assign() is called
10092
- name: config.name,
10093
- role: config.role,
10094
- provider: config.provider,
10095
- model: config.model
10096
- }
10097
- });
10098
- if (!this.fleetManager) {
10099
- this.manifestEntries.set(result.subagentId, {
10100
- subagentId: result.subagentId,
10101
- name: config.name,
10102
- role: config.role,
10103
- provider: config.provider,
10104
- model: config.model,
10105
- taskIds: []
10106
- });
10107
- const spawnedAt = (/* @__PURE__ */ new Date()).toISOString();
10108
- this.stateCheckpoint?.recordSpawn(
10109
- {
10110
- id: result.subagentId,
10111
- name: config.name,
10112
- role: config.role,
10113
- provider: config.provider,
10114
- model: config.model,
10115
- spawnedAt
10116
- },
10117
- this.spawnCount
10118
- );
10119
- void this.appendSessionEvent({
10120
- type: "agent_spawned",
10121
- ts: spawnedAt,
10122
- agentId: result.subagentId,
10123
- role: config.role ?? config.name
10124
- });
10125
- this.scheduleManifest();
10126
- }
10127
- this.armSubagentIdleRetirement(result.subagentId, this.subagentIdleTimeoutMs);
10128
- return result.subagentId;
10129
10291
  }
10130
10292
  /**
10131
10293
  * Synchronously ask a subagent something via the bridge. Sends a
@@ -10250,6 +10412,23 @@ var Director = class _Director {
10250
10412
  await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
10251
10413
  return this.manifestPath;
10252
10414
  }
10415
+ /**
10416
+ * Cancel the pending manifest debounce timer and drain any in-flight
10417
+ * manifest write so that, once this resolves, no Director-owned manifest
10418
+ * write is armed or running.
10419
+ *
10420
+ * Unlike `shutdown()`, this does NOT tear down the coordinator, bridges,
10421
+ * or waiters — it is the lightweight quiesce that `MultiAgentHost.stopAll()`
10422
+ * needs. `stopAll()` flushes the FleetManager's manifest but never touched
10423
+ * the Director's own writer, leaving its armed debounce timer to fire ~2s
10424
+ * later. Under CPU starvation that late, un-awaited `atomicWrite` (temp
10425
+ * sibling + rename) races a caller that deletes the manifest directory,
10426
+ * producing an `ENOTEMPTY` on the parent `rmdir` (Windows especially).
10427
+ */
10428
+ async quiesceManifest() {
10429
+ this.clearManifestTimer();
10430
+ await this.manifestWriteChain.catch(() => void 0);
10431
+ }
10253
10432
  /**
10254
10433
  * Tear down the director: stop every subagent, close every bridge
10255
10434
  * endpoint, and (when configured) write the final manifest. Idempotent
@@ -10261,8 +10440,17 @@ var Director = class _Director {
10261
10440
  this.coordinator.off("task.completed", this.taskCompletedListener);
10262
10441
  this.taskCompletedListener = null;
10263
10442
  }
10264
- for (const aw of this.anyWaiters) {
10443
+ for (const aw of [...this.anyWaiters]) {
10265
10444
  if (aw.timer) clearTimeout(aw.timer);
10445
+ const firstId = [...aw.ids][0];
10446
+ aw.resolve({
10447
+ taskId: firstId ?? "unknown",
10448
+ subagentId: "director",
10449
+ status: "stopped",
10450
+ iterations: 0,
10451
+ toolCalls: 0,
10452
+ durationMs: 0
10453
+ });
10266
10454
  }
10267
10455
  this.anyWaiters.clear();
10268
10456
  if (this.toolExecFilter) {
@@ -10276,6 +10464,17 @@ var Director = class _Director {
10276
10464
  for (const timer of this.subagentIdleTimers.values()) clearTimeout(timer);
10277
10465
  this.subagentIdleTimers.clear();
10278
10466
  await this.coordinator.stopAll();
10467
+ for (const [id, waiter] of this.taskWaiters) {
10468
+ waiter.resolve({
10469
+ taskId: id,
10470
+ subagentId: "director",
10471
+ status: "stopped",
10472
+ iterations: 0,
10473
+ toolCalls: 0,
10474
+ durationMs: 0
10475
+ });
10476
+ }
10477
+ this.taskWaiters.clear();
10279
10478
  for (const b of this.subagentBridges.values()) {
10280
10479
  await b.stop().catch((err) => this.logShutdownError("subagent_bridge_stop", err));
10281
10480
  }
@@ -10283,6 +10482,7 @@ var Director = class _Director {
10283
10482
  await this.bridge.stop().catch((err) => this.logShutdownError("director_bridge_stop", err));
10284
10483
  if (this.fleetManager) {
10285
10484
  await this.fleetManager.flushManifest().catch((err) => this.logShutdownError("fleet_manifest_flush", err));
10485
+ await this.manifestWriteChain.catch(() => void 0);
10286
10486
  } else if (this.manifestPath) {
10287
10487
  await this.writeManifest().catch((err) => this.logShutdownError("manifest_write", err));
10288
10488
  }
@@ -10748,22 +10948,7 @@ var Director = class _Director {
10748
10948
  * three agents complete or the session times out.
10749
10949
  */
10750
10950
  async spawnCollab(options) {
10751
- const session = new CollabSession(this, this.fleet, {
10752
- ...options,
10753
- onBudgetWarning: (alert) => {
10754
- return options.onBudgetWarning?.(alert) ?? "ignore";
10755
- }
10756
- });
10757
- const doneHandler = () => this._activeCollabSessions.delete(session.sessionId);
10758
- const errorHandler = () => this._activeCollabSessions.delete(session.sessionId);
10759
- session.on("session.done", doneHandler);
10760
- session.on("session.error", errorHandler);
10761
- const unsubs = [
10762
- () => session.off("session.done", doneHandler),
10763
- () => session.off("session.error", errorHandler)
10764
- ];
10765
- this._activeCollabSessions.set(session.sessionId, { session, unsubs });
10766
- return session.start();
10951
+ return this.collab.spawn(options);
10767
10952
  }
10768
10953
  /**
10769
10954
  * Resume from a prior checkpoint snapshot (loaded via
@@ -13446,6 +13631,16 @@ var FleetManager = class {
13446
13631
  manifestTimer = null;
13447
13632
  manifestWriteChain = Promise.resolve();
13448
13633
  disposed = false;
13634
+ /**
13635
+ * Teardown freeze. Set by `closeManifest()` after the final manifest is
13636
+ * flushed; makes any subsequent `scheduleManifest`/`writeManifest`/
13637
+ * `flushManifest` a no-op so a late fire-and-forget write (e.g. the
13638
+ * task-completion flush the Director issues via `void flushManifest()`)
13639
+ * cannot land an `atomicWrite` in the manifest directory while a caller is
13640
+ * deleting it. Cleared by `recordSpawn` so a stop-then-spawn-again host
13641
+ * resumes writing.
13642
+ */
13643
+ closing = false;
13449
13644
  manifestDebounceMs;
13450
13645
  /** Fleet-wide cost cap. Infinity = no cap. Distinct from SubagentBudget limits,
13451
13646
  * which track per-subagent spend — this field caps the entire fleet total. */
@@ -13627,6 +13822,7 @@ var FleetManager = class {
13627
13822
  * @param priceLookup Optional per-subagent pricing data
13628
13823
  */
13629
13824
  recordSpawn(subagentId, config, priceLookup) {
13825
+ this.closing = false;
13630
13826
  this.spawnCount += 1;
13631
13827
  this.subagentMeta.set(subagentId, {
13632
13828
  provider: config.provider,
@@ -13664,6 +13860,7 @@ var FleetManager = class {
13664
13860
  }
13665
13861
  async writeManifest() {
13666
13862
  if (!this.manifestPath) return null;
13863
+ if (this.closing) return this.manifestPath;
13667
13864
  this.clearManifestTimer();
13668
13865
  const write = this.manifestWriteChain.catch(() => void 0).then(() => this.writeManifestNow());
13669
13866
  this.manifestWriteChain = write.catch(() => void 0);
@@ -13712,7 +13909,7 @@ var FleetManager = class {
13712
13909
  * When `manifestDebounceMs` is 0, writes are synchronous (no debounce).
13713
13910
  */
13714
13911
  scheduleManifest() {
13715
- if (this.disposed) return;
13912
+ if (this.disposed || this.closing) return;
13716
13913
  if (!this.manifestPath) return;
13717
13914
  if (this.manifestDebounceMs === 0) {
13718
13915
  void this.writeManifest().catch((err) => {
@@ -13743,6 +13940,22 @@ var FleetManager = class {
13743
13940
  process.emitWarning(`FleetManager manifest write failed: ${detail}`, "FleetManagerWarning");
13744
13941
  });
13745
13942
  }
13943
+ /**
13944
+ * Teardown flush for callers that will delete the manifest directory next
13945
+ * (host `stopAll`/`dispose`). Writes the final manifest, then freezes the
13946
+ * writer so no later fire-and-forget write can start, and drains anything
13947
+ * still in flight. After this resolves it is safe to `rm -rf` the manifest
13948
+ * directory: no `atomicWrite` (temp sibling + rename) can race the removal.
13949
+ *
13950
+ * The freeze is lifted by the next `recordSpawn`, so a stop-then-spawn-again
13951
+ * host keeps persisting manifests.
13952
+ */
13953
+ async closeManifest() {
13954
+ if (!this.manifestPath) return;
13955
+ await this.flushManifest();
13956
+ this.closing = true;
13957
+ await this.manifestWriteChain.catch(() => void 0);
13958
+ }
13746
13959
  clearManifestTimer() {
13747
13960
  if (!this.manifestTimer) return;
13748
13961
  clearTimeout(this.manifestTimer);
@@ -13969,6 +14182,42 @@ function parseMailboxMessage(value) {
13969
14182
  function parseMailboxMessageLine(line) {
13970
14183
  return parseMailboxMessage(JSON.parse(line));
13971
14184
  }
14185
+ function isAckRecord(value) {
14186
+ return typeof value === "object" && value !== null && !Array.isArray(value) && value["__ack"] === true;
14187
+ }
14188
+ function parseMailboxLine(line) {
14189
+ try {
14190
+ const parsed = JSON.parse(line);
14191
+ if (isAckRecord(parsed)) return parsed;
14192
+ return parseMailboxMessage(parsed);
14193
+ } catch {
14194
+ return null;
14195
+ }
14196
+ }
14197
+ function applyAckToMessage(msg, ack) {
14198
+ if (ack.read && !(ack.readerId in msg.readBy)) {
14199
+ msg.readBy[ack.readerId] = ack.timestamp;
14200
+ }
14201
+ if (ack.completed && !msg.completed) {
14202
+ msg.completed = true;
14203
+ msg.completedBy = ack.completedBy ?? ack.readerId;
14204
+ msg.completedAt = ack.timestamp;
14205
+ }
14206
+ if (ack.outcome !== void 0 && msg.outcome !== ack.outcome) {
14207
+ msg.outcome = ack.outcome;
14208
+ }
14209
+ if (ack.deleted === true) {
14210
+ msg.deletedAt = ack.timestamp;
14211
+ msg.deletedBy = ack.deletedBy ?? ack.readerId;
14212
+ }
14213
+ if (ack.deleted === false) {
14214
+ delete msg.deletedAt;
14215
+ delete msg.deletedBy;
14216
+ }
14217
+ }
14218
+ function serializeAckRecord(ack) {
14219
+ return JSON.stringify(ack) + "\n";
14220
+ }
13972
14221
 
13973
14222
  // src/coordination/mailbox-types.ts
13974
14223
  function normalizeRecipient(to) {
@@ -14040,7 +14289,7 @@ var GlobalMailbox = class {
14040
14289
  * appended message). The chain runs each read to completion before
14041
14290
  * the next starts, in issue order — readers are read-only and never
14042
14291
  * conflict with each other on content, only on the cache mutation
14043
- * that follows the read. Pattern mirrors `DefaultMemoryStore.runSerialized`.
14292
+ * that follows the read. Pattern mirrors a serialized mutation chain.
14044
14293
  */
14045
14294
  _readChain = Promise.resolve([]);
14046
14295
  /**
@@ -14051,7 +14300,7 @@ var GlobalMailbox = class {
14051
14300
  *
14052
14301
  * The index is rebuilt whenever the cache array is replaced
14053
14302
  * (`_setMessageCache`), extended when new messages are pushed
14054
- * (`_pushToCache`, `_readNewMessagesOnly`), and cleared on `close()`.
14303
+ * (`_pushToCache`), and cleared on `close()`.
14055
14304
  * It is `null` when the cache itself is null (> MESSAGE_CACHE_MAX_ENTRIES
14056
14305
  * or never populated).
14057
14306
  *
@@ -14190,42 +14439,100 @@ var GlobalMailbox = class {
14190
14439
  const updated = await this.ackMany({ acks: [input] });
14191
14440
  return updated.length > 0 ? updated[0] : null;
14192
14441
  }
14442
+ /** Diagnostic counters exposed for test/tool introspection. */
14443
+ diag = {
14444
+ ackManyCacheDesync: 0,
14445
+ ackManyPreLockMtime: 0,
14446
+ ackManyPostLockMtime: 0,
14447
+ ackManyPreLockSize: 0,
14448
+ ackManyPostLockSize: 0,
14449
+ ackManyMessageCount: 0,
14450
+ ackManyAckCount: 0,
14451
+ cacheHitCount: 0,
14452
+ cacheMissCount: 0,
14453
+ cacheDesyncNoOp: 0,
14454
+ applyAckTargetMissing: 0,
14455
+ setMessageCacheCount: 0,
14456
+ pushToCacheCount: 0
14457
+ };
14193
14458
  async ackMany(input) {
14194
14459
  if (input.acks.length === 0) return [];
14460
+ const now = (/* @__PURE__ */ new Date()).toISOString();
14461
+ const ackRecords = [];
14195
14462
  const updated = [];
14463
+ const targetIds = /* @__PURE__ */ new Set();
14196
14464
  const byId = /* @__PURE__ */ new Map();
14197
14465
  for (const a of input.acks) {
14198
14466
  byId.set(a.messageId, a);
14467
+ targetIds.add(a.messageId);
14199
14468
  }
14200
- await withFileLock(this.messagePath, async () => {
14201
- const all = await this._readMessagesFresh();
14202
- const now = (/* @__PURE__ */ new Date()).toISOString();
14203
- let changed = false;
14204
- for (const msg of all) {
14205
- const a = byId.get(msg.id);
14206
- if (!a) continue;
14207
- updated.push(msg);
14208
- if (a.read !== false && !(a.readerId in msg.readBy)) {
14209
- msg.readBy[a.readerId] = now;
14210
- changed = true;
14211
- }
14212
- if (a.completed && !msg.completed) {
14213
- msg.completed = true;
14214
- msg.completedBy = a.readerId;
14215
- msg.completedAt = now;
14216
- changed = true;
14217
- }
14218
- if (a.outcome !== void 0 && msg.outcome !== a.outcome) {
14219
- msg.outcome = a.outcome;
14220
- changed = true;
14221
- }
14469
+ const preLockMtime = this._messageCacheMtime;
14470
+ const preLockSize = this._messageCacheSize;
14471
+ this.diag.ackManyPreLockMtime = preLockMtime;
14472
+ this.diag.ackManyPreLockSize = preLockSize;
14473
+ const all = await this._readMessagesCached();
14474
+ for (const msg of all) {
14475
+ const a = byId.get(msg.id);
14476
+ if (!a) continue;
14477
+ const needsRead = a.read !== false && !(a.readerId in msg.readBy);
14478
+ const needsComplete = a.completed && !msg.completed;
14479
+ const needsOutcome = a.outcome !== void 0 && msg.outcome !== a.outcome;
14480
+ if (!needsRead && !needsComplete && !needsOutcome) {
14481
+ updated.push({ ...msg, readBy: { ...msg.readBy } });
14482
+ continue;
14222
14483
  }
14223
- if (changed) {
14224
- const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
14225
- await fsp11.writeFile(this.messagePath, serialized, "utf8");
14484
+ const copy = { ...msg, readBy: { ...msg.readBy } };
14485
+ if (needsRead) copy.readBy[a.readerId] = now;
14486
+ if (needsComplete) {
14487
+ copy.completed = true;
14488
+ copy.completedBy = a.readerId;
14489
+ copy.completedAt = now;
14490
+ }
14491
+ if (needsOutcome) copy.outcome = a.outcome;
14492
+ updated.push(copy);
14493
+ ackRecords.push({
14494
+ __ack: true,
14495
+ messageId: msg.id,
14496
+ readerId: a.readerId,
14497
+ timestamp: now,
14498
+ read: a.read !== false,
14499
+ completed: a.completed,
14500
+ completedBy: a.completed ? a.readerId : void 0,
14501
+ outcome: a.outcome
14502
+ });
14503
+ }
14504
+ if (ackRecords.length === 0) {
14505
+ if (updated.length > 0) {
14506
+ for (const message of updated) {
14507
+ this.publishHqMailboxEvent({
14508
+ mailboxId: this.hqMailboxId,
14509
+ action: message.completed ? "message.completed" : "message.read",
14510
+ message
14511
+ });
14512
+ }
14226
14513
  }
14514
+ return updated;
14515
+ }
14516
+ const serialized = ackRecords.map((a) => serializeAckRecord(a)).join("");
14517
+ await withFileLock(this.messagePath, async () => {
14518
+ await fsp11.appendFile(this.messagePath, serialized, "utf8");
14227
14519
  const { mtimeMs, size } = await this._statMessageFile();
14228
- this._setMessageCache(all, mtimeMs, size);
14520
+ for (const ack of ackRecords) {
14521
+ this._applyAckToCache(ack);
14522
+ }
14523
+ this._messageCacheMtime = mtimeMs;
14524
+ this._messageCacheSize = size;
14525
+ this.diag.ackManyPostLockMtime = mtimeMs;
14526
+ this.diag.ackManyPostLockSize = size;
14527
+ this.diag.ackManyMessageCount = this._messageCache?.length ?? -1;
14528
+ this.diag.ackManyAckCount = ackRecords.length;
14529
+ const expectedMinSize = preLockSize + serialized.length;
14530
+ if (preLockSize > 0 && size > expectedMinSize) {
14531
+ const extraBytes = size - expectedMinSize;
14532
+ console.error(
14533
+ `[MAILBOX-DIAG] ackMany: post-lock size ${size} exceeds expected ${expectedMinSize} by ${extraBytes}B (mtime ${preLockMtime}->${mtimeMs}). Cache may be missing cross-process messages. diag.ackManyCacheDesync=${++this.diag.ackManyCacheDesync}`
14534
+ );
14535
+ }
14229
14536
  });
14230
14537
  for (const message of updated) {
14231
14538
  this.publishHqMailboxEvent({
@@ -14268,87 +14575,87 @@ var GlobalMailbox = class {
14268
14575
  return count;
14269
14576
  }
14270
14577
  async softDelete(mailId, by) {
14271
- let updated = null;
14578
+ const all = await this._readMessagesCached();
14579
+ const target = all.find((m) => m.id === mailId);
14580
+ if (target === void 0) return null;
14581
+ if (target.deletedAt !== void 0) {
14582
+ return { ...target, readBy: { ...target.readBy } };
14583
+ }
14584
+ const now = (/* @__PURE__ */ new Date()).toISOString();
14585
+ const ack = {
14586
+ __ack: true,
14587
+ messageId: mailId,
14588
+ readerId: by,
14589
+ timestamp: now,
14590
+ read: true,
14591
+ deleted: true,
14592
+ deletedBy: by
14593
+ };
14272
14594
  await withFileLock(this.messagePath, async () => {
14273
- const all = await this._readMessagesFresh();
14274
- const now = (/* @__PURE__ */ new Date()).toISOString();
14275
- let changed = false;
14276
- for (const m of all) {
14277
- if (m.id !== mailId) continue;
14278
- if (m.deletedAt !== void 0) {
14279
- updated = m;
14280
- continue;
14281
- }
14282
- m.deletedAt = now;
14283
- m.deletedBy = by;
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");
14290
- }
14595
+ await fsp11.appendFile(this.messagePath, serializeAckRecord(ack), "utf8");
14291
14596
  const { mtimeMs, size } = await this._statMessageFile();
14292
- this._setMessageCache(all, mtimeMs, size);
14597
+ this._applyAckToCache(ack);
14598
+ this._messageCacheMtime = mtimeMs;
14599
+ this._messageCacheSize = size;
14293
14600
  });
14294
- if (updated !== null) {
14295
- const msg = updated;
14296
- this.publishHqMailboxEvent({
14297
- mailboxId: this.hqMailboxId,
14298
- action: "message.updated",
14299
- message: msg
14300
- });
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
- });
14309
- }
14310
- return updated;
14601
+ const msg = { ...target, readBy: { ...target.readBy } };
14602
+ msg.deletedAt = now;
14603
+ msg.deletedBy = by;
14604
+ this.publishHqMailboxEvent({
14605
+ mailboxId: this.hqMailboxId,
14606
+ action: "message.updated",
14607
+ message: msg
14608
+ });
14609
+ this.publishHqMailboxSnapshot();
14610
+ this.eventEmitter?.emit({
14611
+ type: "message.deleted",
14612
+ messageId: msg.id,
14613
+ from: msg.from,
14614
+ to: msg.to,
14615
+ timestamp: msg.deletedAt ?? now
14616
+ });
14617
+ return msg;
14311
14618
  }
14312
14619
  async restore(mailId) {
14313
- let updated = null;
14620
+ const all = await this._readMessagesCached();
14621
+ const target = all.find((m) => m.id === mailId);
14622
+ if (target === void 0) return null;
14623
+ if (target.deletedAt === void 0 && target.deletedBy === void 0) {
14624
+ return { ...target, readBy: { ...target.readBy } };
14625
+ }
14626
+ const now = (/* @__PURE__ */ new Date()).toISOString();
14627
+ const ack = {
14628
+ __ack: true,
14629
+ messageId: mailId,
14630
+ readerId: "",
14631
+ timestamp: now,
14632
+ read: true,
14633
+ deleted: false
14634
+ };
14314
14635
  await withFileLock(this.messagePath, async () => {
14315
- const all = await this._readMessagesFresh();
14316
- let changed = false;
14317
- for (const m of all) {
14318
- if (m.id !== mailId) continue;
14319
- if (m.deletedAt === void 0 && m.deletedBy === void 0) {
14320
- updated = m;
14321
- continue;
14322
- }
14323
- delete m.deletedAt;
14324
- delete m.deletedBy;
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");
14331
- }
14636
+ await fsp11.appendFile(this.messagePath, serializeAckRecord(ack), "utf8");
14332
14637
  const { mtimeMs, size } = await this._statMessageFile();
14333
- this._setMessageCache(all, mtimeMs, size);
14638
+ this._applyAckToCache(ack);
14639
+ this._messageCacheMtime = mtimeMs;
14640
+ this._messageCacheSize = size;
14334
14641
  });
14335
- if (updated !== null) {
14336
- const msg = updated;
14337
- this.publishHqMailboxEvent({
14338
- mailboxId: this.hqMailboxId,
14339
- action: "message.updated",
14340
- message: msg
14341
- });
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
- });
14350
- }
14351
- return updated;
14642
+ const msg = { ...target, readBy: { ...target.readBy } };
14643
+ delete msg.deletedAt;
14644
+ delete msg.deletedBy;
14645
+ this.publishHqMailboxEvent({
14646
+ mailboxId: this.hqMailboxId,
14647
+ action: "message.updated",
14648
+ message: msg
14649
+ });
14650
+ this.publishHqMailboxSnapshot();
14651
+ this.eventEmitter?.emit({
14652
+ type: "message.restored",
14653
+ messageId: msg.id,
14654
+ from: msg.from,
14655
+ to: msg.to,
14656
+ timestamp: now
14657
+ });
14658
+ return msg;
14352
14659
  }
14353
14660
  // ── Agent registry ──────────────────────────────────────────────────────
14354
14661
  async registerAgent(input) {
@@ -14585,11 +14892,18 @@ var GlobalMailbox = class {
14585
14892
  }
14586
14893
  async getClientStatuses() {
14587
14894
  await this._ensureClientRegistry();
14588
- const registry = await this._readClientRegistry();
14895
+ let registry = await this._readClientRegistry();
14589
14896
  const before = registry.size;
14590
14897
  this._pruneStaleClientsInPlace(registry);
14591
14898
  if (registry.size < before) {
14592
- await this._writeClientRegistry(registry);
14899
+ await withFileLock(this.clientRegistryPath, async () => {
14900
+ registry = await this._readClientRegistry({ fresh: true });
14901
+ const postLockBefore = registry.size;
14902
+ this._pruneStaleClientsInPlace(registry);
14903
+ if (registry.size < postLockBefore) {
14904
+ await this._writeClientRegistry(registry);
14905
+ }
14906
+ });
14593
14907
  }
14594
14908
  const now = Date.now();
14595
14909
  return Array.from(registry.values()).map((c) => ({
@@ -14795,75 +15109,22 @@ var GlobalMailbox = class {
14795
15109
  throw err;
14796
15110
  }
14797
15111
  }
14798
- /**
14799
- * Read only newly-appended bytes from the file and append them to the
14800
- * in-memory cache. This avoids re-reading and re-parsing the entire file
14801
- * when another process appended messages since our last read.
14802
- *
14803
- * Only safe when the file grew in size (messages are append-only). When
14804
- * the file was rewritten (ack/purge changed existing content), callers
14805
- * must fall back to {@link _readMessages}.
14806
- *
14807
- * @returns The (now up-to-date) message cache.
14808
- */
14809
- async _readNewMessagesOnly(fd, oldSize, newSize) {
14810
- const tailLen = newSize - oldSize;
14811
- const buf = Buffer.alloc(tailLen);
14812
- await fd.read(buf, 0, tailLen, oldSize);
14813
- const tail = buf.toString("utf8");
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)) {
14830
- if (!line.trim()) continue;
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);
14840
- }
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);
14850
- }
14851
- }
14852
- }
14853
- _parseLine(line) {
14854
- try {
14855
- return parseMailboxMessageLine(line);
14856
- } catch {
14857
- return null;
14858
- }
14859
- }
14860
- /** Parse a JSONL string into MailboxMessage[], including migration. */
15112
+ /** Parse a JSONL string into MailboxMessage[], applying ack records. */
14861
15113
  _parseLines(raw) {
14862
15114
  const lines = raw.split(LINE_SEPARATOR).filter((l) => l.trim().length > 0);
14863
15115
  const messages = [];
15116
+ const acks = [];
14864
15117
  for (const line of lines) {
14865
- const message = this._parseLine(line);
14866
- if (message !== null) messages.push(message);
15118
+ const parsed = parseMailboxLine(line);
15119
+ if (isAckRecord(parsed)) {
15120
+ acks.push(parsed);
15121
+ } else if (parsed !== null) {
15122
+ messages.push(parsed);
15123
+ }
15124
+ }
15125
+ for (const ack of acks) {
15126
+ const target = messages.find((m) => m.id === ack.messageId);
15127
+ if (target) applyAckToMessage(target, ack);
14867
15128
  }
14868
15129
  return messages;
14869
15130
  }
@@ -14946,16 +15207,6 @@ var GlobalMailbox = class {
14946
15207
  if (this._messageCache !== null && this._messageCacheMtime === st.mtimeMs && this._messageCacheSize === st.size) {
14947
15208
  return this._messageCache;
14948
15209
  }
14949
- if (this._messageCache !== null && this._messageCacheSize >= 0 && st.size > this._messageCacheSize) {
14950
- const fd = await fsp11.open(this.messagePath, "r");
14951
- try {
14952
- const updated = await this._readNewMessagesOnly(fd, this._messageCacheSize, st.size);
14953
- this._messageCacheMtime = st.mtimeMs;
14954
- return updated;
14955
- } finally {
14956
- await fd.close();
14957
- }
14958
- }
14959
15210
  const all = await this._readMessages();
14960
15211
  this._setMessageCache(all, st.mtimeMs, st.size);
14961
15212
  return all;
@@ -14996,6 +15247,21 @@ var GlobalMailbox = class {
14996
15247
  this._messageCacheSize = size;
14997
15248
  this._rebuildRecipientIndex();
14998
15249
  }
15250
+ /**
15251
+ * Fold a single ack record into the in-memory cache, mutating the target
15252
+ * message in place. Called from `ackMany` after appending the record to
15253
+ * disk so the cache reflects the ack without a full re-read + re-parse.
15254
+ *
15255
+ * An ack only touches `readBy`/`completed*`/`outcome` — none of which
15256
+ * affect the recipient/sender indexes — so no index rebuild is needed.
15257
+ * No-op when the cache is empty or the target message isn't cached.
15258
+ */
15259
+ _applyAckToCache(ack) {
15260
+ const cache = this._messageCache;
15261
+ if (cache === null) return;
15262
+ const target = cache.find((m) => m.id === ack.messageId);
15263
+ if (target !== void 0) applyAckToMessage(target, ack);
15264
+ }
14999
15265
  /**
15000
15266
  * Build the recipient AND sender indexes from the current `_messageCache`.
15001
15267
  * Called only from `_setMessageCache` so both indexes are always in sync
@@ -15451,7 +15717,7 @@ function makeFleetStatusTool(opts = {}) {
15451
15717
  const resolveMailbox = opts.resolveMailbox ?? ((ctx) => new GlobalMailbox(opts.projectDir ?? defaultResolveProjectDir(ctx), opts.events));
15452
15718
  return {
15453
15719
  name: "fleet_status",
15454
- description: "Live snapshot of every agent working on this project (this process AND other terminals/TUIs/WebUIs): who is online, what task each is on, which tool is running, and progress counters. Check it before starting work that might overlap with a peer, when deciding whether to wait for someone or proceed, or when a task mentions another agent. Read-only. To talk to a peer, use mail_send with the returned id.",
15720
+ description: "Live snapshot of every agent working on this canonical project (all clients, processes, sessions, branches, and linked Git worktrees): who is online, what task each is on, which tool is running, and progress counters. Check it before starting work that might overlap with a peer, when deciding whether to wait for someone or proceed, or when a task mentions another agent. Read-only. To talk to a peer, use mail_send with the returned id.",
15455
15721
  usageHint: "fleet_status (optionally: onlineOnly=false to include recently-offline agents)",
15456
15722
  category: "Coordination",
15457
15723
  permission: "auto",
@@ -16115,7 +16381,7 @@ function makeMailSendTool(opts = {}) {
16115
16381
  const resolveMailbox = makeResolver(opts);
16116
16382
  return {
16117
16383
  name: "mail_send",
16118
- description: 'Send a mail to other agents working on this project (other terminals, TUIs, WebUIs). Use it to hand off work, ask questions, announce what you just did, or request a review (type="review" \u2014 passive ask, no immediate reply required). to="*" broadcasts to everyone; to="leader" reaches every leader process; an exact id like "leader@a1b2c3d4" reaches one agent. Recipients see your mail automatically before their next step. Pick the type that matches the intent: note (default), ask (blocking question), assign (task), steer (mid-task direction), result (completion notice), review (passive ask), btw/status/broadcast/control (informational).',
16384
+ description: 'Send mail to any agent on this canonical project, across clients, processes, sessions, branches, and linked Git worktrees (CLI, TUI, WebUI, ACP/MCP/HTTP). Use it to hand off work, ask questions, announce what you just did, or request a review (type="review" \u2014 passive ask, no immediate reply required). to="*" broadcasts to everyone; to="leader" reaches every leader process; an exact id like "leader@a1b2c3d4" reaches one agent. Recipients see your mail automatically before their next step. Pick the type that matches the intent: note (default), ask (blocking question), assign (task), steer (mid-task direction), result (completion notice), review (passive ask), btw/status/broadcast/control (informational).',
16119
16385
  usageHint: 'mail_send to="<id>" type="review" body="please skim <file>"',
16120
16386
  category: "Coordination",
16121
16387
  permission: "auto",
@@ -16175,7 +16441,7 @@ function makeMailInboxTool(opts = {}) {
16175
16441
  const resolveMailbox = makeResolver(opts);
16176
16442
  return {
16177
16443
  name: "mail_inbox",
16178
- description: 'Read your unread mail from other agents on this project and mark it read. Covers mail addressed to you directly, to your base name (e.g. "leader"), and broadcasts ("*"). Urgent steer/btw mail is already injected automatically \u2014 use this to catch up on notes, questions, handoffs, results, and review requests (type="review" \u2014 passive asks where no reply is required). Best called after a long stretch of tool work. Set completed=true to finish every returned message in the same call.',
16444
+ description: 'Read your unread project-wide mail from agents in any client, session, branch, or linked Git worktree and mark it read. Covers mail addressed to you directly, to your base name (e.g. "leader"), and broadcasts ("*"). Urgent steer/btw mail is already injected automatically \u2014 use this to catch up on notes, questions, handoffs, results, and review requests (type="review" \u2014 passive asks where no reply is required). Best called after a long stretch of tool work. Set completed=true to finish every returned message in the same call.',
16179
16445
  usageHint: 'mail_inbox (optionally: limit=10, markRead=false to peek, completed=true outcome="handled")',
16180
16446
  category: "Coordination",
16181
16447
  permission: "auto",
@@ -16990,7 +17256,7 @@ function createMailboxHooks(opts) {
16990
17256
  var NULL_FLEET_BUS = new FleetBus();
16991
17257
 
16992
17258
  // src/coordination/package-author-tracker.ts
16993
- import * as fs2 from "node:fs/promises";
17259
+ import * as fs3 from "node:fs/promises";
16994
17260
  import * as path16 from "node:path";
16995
17261
  var DEFAULT_MAX_ENTRIES = 1e4;
16996
17262
  var LOG_FILENAME = "package-authors.json";
@@ -16999,7 +17265,7 @@ function logPath(storageDir) {
16999
17265
  }
17000
17266
  async function loadLog(storageDir, projectRoot) {
17001
17267
  try {
17002
- const raw = await fs2.readFile(logPath(storageDir), "utf-8");
17268
+ const raw = await fs3.readFile(logPath(storageDir), "utf-8");
17003
17269
  const parsed = JSON.parse(raw);
17004
17270
  if (!parsed.entries || !Array.isArray(parsed.entries)) {
17005
17271
  return { projectRoot, entries: [] };
@@ -17013,10 +17279,10 @@ async function loadLog(storageDir, projectRoot) {
17013
17279
  }
17014
17280
  }
17015
17281
  async function saveLog(storageDir, log) {
17016
- await fs2.mkdir(storageDir, { recursive: true });
17282
+ await fs3.mkdir(storageDir, { recursive: true });
17017
17283
  const tmp = `${logPath(storageDir)}.tmp.${Date.now()}`;
17018
- await fs2.writeFile(tmp, JSON.stringify(log, null, 2) + "\n", "utf-8");
17019
- await fs2.rename(tmp, logPath(storageDir));
17284
+ await fs3.writeFile(tmp, JSON.stringify(log, null, 2) + "\n", "utf-8");
17285
+ await fs3.rename(tmp, logPath(storageDir));
17020
17286
  }
17021
17287
  function detectEcosystem(manifestPath) {
17022
17288
  const name = path16.win32.basename(manifestPath).toLowerCase();
@@ -17586,7 +17852,7 @@ var AdaptiveConcurrencyController = class {
17586
17852
  };
17587
17853
 
17588
17854
  // src/coordination/agent-monitor.ts
17589
- import * as fs3 from "node:fs/promises";
17855
+ import * as fs4 from "node:fs/promises";
17590
17856
  import * as path18 from "node:path";
17591
17857
  var AgentMonitorService = class _AgentMonitorService {
17592
17858
  _fleetBus;
@@ -17830,9 +18096,9 @@ var AgentMonitorService = class _AgentMonitorService {
17830
18096
  * firehose (or, worse, only the first word).
17831
18097
  */
17832
18098
  _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;
18099
+ const open6 = this._openStreams.get(subagentId);
18100
+ if (open6 && open6.entry.kind === kind && open6.entry.iteration === iteration && open6.entry.content.length + text.length <= _AgentMonitorService._MAX_SEGMENT_CHARS) {
18101
+ open6.entry.content += text;
17836
18102
  return;
17837
18103
  }
17838
18104
  this._closeStream(subagentId);
@@ -17856,12 +18122,12 @@ var AgentMonitorService = class _AgentMonitorService {
17856
18122
  * announce the completed entry on the local bus + HQ callback.
17857
18123
  */
17858
18124
  _closeStream(subagentId) {
17859
- const open7 = this._openStreams.get(subagentId);
17860
- if (!open7) return;
18125
+ const open6 = this._openStreams.get(subagentId);
18126
+ if (!open6) return;
17861
18127
  this._openStreams.delete(subagentId);
17862
- this._appendToFile(subagentId, open7.entry).catch(() => {
18128
+ this._appendToFile(subagentId, open6.entry).catch(() => {
17863
18129
  });
17864
- this._emitEntry(open7.entry);
18130
+ this._emitEntry(open6.entry);
17865
18131
  }
17866
18132
  _addEntry(subagentId, entry) {
17867
18133
  const session = this._sessions.get(subagentId);
@@ -17892,10 +18158,10 @@ var AgentMonitorService = class _AgentMonitorService {
17892
18158
  }
17893
18159
  async _appendToFile(subagentId, entry) {
17894
18160
  const dir = path18.join(this._transcriptsDir, subagentId);
17895
- await fs3.mkdir(dir, { recursive: true });
18161
+ await fs4.mkdir(dir, { recursive: true });
17896
18162
  const filePath = path18.join(dir, "transcript.jsonl");
17897
18163
  const line = JSON.stringify(entry) + "\n";
17898
- await fs3.appendFile(filePath, line, { encoding: "utf8" });
18164
+ await fs4.appendFile(filePath, line, { encoding: "utf8" });
17899
18165
  }
17900
18166
  _uid() {
17901
18167
  return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;