@wrongstack/core 0.285.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 (340) 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 +46 -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 +8 -14
  16. package/dist/coordination/delegate-tool.d.ts.map +1 -1
  17. package/dist/coordination/director/director-btw-notes.d.ts +20 -0
  18. package/dist/coordination/director/director-btw-notes.d.ts.map +1 -0
  19. package/dist/coordination/director/director-collab.d.ts +40 -0
  20. package/dist/coordination/director/director-collab.d.ts.map +1 -0
  21. package/dist/coordination/director.d.ts +28 -9
  22. package/dist/coordination/director.d.ts.map +1 -1
  23. package/dist/coordination/fleet-manager.d.ts +21 -0
  24. package/dist/coordination/fleet-manager.d.ts.map +1 -1
  25. package/dist/coordination/fleet.d.ts +6 -7
  26. package/dist/coordination/fleet.d.ts.map +1 -1
  27. package/dist/coordination/global-mailbox.d.ts +96 -21
  28. package/dist/coordination/global-mailbox.d.ts.map +1 -1
  29. package/dist/coordination/icoordinator.d.ts +1 -3
  30. package/dist/coordination/icoordinator.d.ts.map +1 -1
  31. package/dist/coordination/index.d.ts +2 -1
  32. package/dist/coordination/index.d.ts.map +1 -1
  33. package/dist/coordination/index.js +2176 -806
  34. package/dist/coordination/index.js.map +4 -4
  35. package/dist/coordination/knowledge-graph.d.ts +13 -1
  36. package/dist/coordination/knowledge-graph.d.ts.map +1 -1
  37. package/dist/coordination/mail-tools.d.ts.map +1 -1
  38. package/dist/coordination/mailbox-actions.d.ts +12 -13
  39. package/dist/coordination/mailbox-actions.d.ts.map +1 -1
  40. package/dist/coordination/mailbox-constants.d.ts +64 -0
  41. package/dist/coordination/mailbox-constants.d.ts.map +1 -0
  42. package/dist/coordination/mailbox-events.d.ts +32 -0
  43. package/dist/coordination/mailbox-events.d.ts.map +1 -0
  44. package/dist/coordination/mailbox-message-codec.d.ts +28 -0
  45. package/dist/coordination/mailbox-message-codec.d.ts.map +1 -0
  46. package/dist/coordination/mailbox-types.d.ts +130 -5
  47. package/dist/coordination/mailbox-types.d.ts.map +1 -1
  48. package/dist/coordination/mailbox.d.ts +21 -2
  49. package/dist/coordination/mailbox.d.ts.map +1 -1
  50. package/dist/coordination/package-author-tracker.d.ts.map +1 -1
  51. package/dist/coordination/subagent-result-tool.d.ts.map +1 -1
  52. package/dist/core/agent-loop.d.ts.map +1 -1
  53. package/dist/core/agent-response.d.ts +10 -1
  54. package/dist/core/agent-response.d.ts.map +1 -1
  55. package/dist/core/agent-tools.d.ts.map +1 -1
  56. package/dist/core/agent.d.ts +8 -0
  57. package/dist/core/agent.d.ts.map +1 -1
  58. package/dist/core/context.d.ts +25 -0
  59. package/dist/core/context.d.ts.map +1 -1
  60. package/dist/core/conversation-state.d.ts +6 -0
  61. package/dist/core/conversation-state.d.ts.map +1 -1
  62. package/dist/core/fallback-model.d.ts.map +1 -1
  63. package/dist/core/mailbox-loop.d.ts +12 -5
  64. package/dist/core/mailbox-loop.d.ts.map +1 -1
  65. package/dist/core/system-prompt-builder.d.ts +14 -6
  66. package/dist/core/system-prompt-builder.d.ts.map +1 -1
  67. package/dist/defaults/index.d.ts +0 -1
  68. package/dist/defaults/index.d.ts.map +1 -1
  69. package/dist/defaults/index.js +1690 -1998
  70. package/dist/defaults/index.js.map +4 -4
  71. package/dist/execution/auto-compaction-middleware.d.ts +8 -1
  72. package/dist/execution/auto-compaction-middleware.d.ts.map +1 -1
  73. package/dist/execution/autonomous-runner.d.ts +7 -1
  74. package/dist/execution/autonomous-runner.d.ts.map +1 -1
  75. package/dist/execution/autonomy-brain.d.ts +72 -11
  76. package/dist/execution/autonomy-brain.d.ts.map +1 -1
  77. package/dist/execution/brain-chain.d.ts +53 -0
  78. package/dist/execution/brain-chain.d.ts.map +1 -0
  79. package/dist/execution/brain-evaluation.d.ts +66 -0
  80. package/dist/execution/brain-evaluation.d.ts.map +1 -0
  81. package/dist/execution/brain-runtime.d.ts +171 -0
  82. package/dist/execution/brain-runtime.d.ts.map +1 -0
  83. package/dist/execution/compaction-core.d.ts.map +1 -1
  84. package/dist/execution/council-brain.d.ts +79 -0
  85. package/dist/execution/council-brain.d.ts.map +1 -0
  86. package/dist/execution/council-orchestrator.d.ts +32 -0
  87. package/dist/execution/council-orchestrator.d.ts.map +1 -0
  88. package/dist/execution/council-personas.d.ts +19 -0
  89. package/dist/execution/council-personas.d.ts.map +1 -0
  90. package/dist/execution/council-profiles.d.ts +34 -0
  91. package/dist/execution/council-profiles.d.ts.map +1 -0
  92. package/dist/execution/council-prompts.d.ts +21 -0
  93. package/dist/execution/council-prompts.d.ts.map +1 -0
  94. package/dist/execution/council-resolution.d.ts +57 -0
  95. package/dist/execution/council-resolution.d.ts.map +1 -0
  96. package/dist/execution/design-kit-loader.d.ts.map +1 -1
  97. package/dist/execution/enhance-recovery.d.ts +35 -0
  98. package/dist/execution/enhance-recovery.d.ts.map +1 -0
  99. package/dist/execution/error-handler.d.ts.map +1 -1
  100. package/dist/execution/eternal-autonomy.d.ts +12 -0
  101. package/dist/execution/eternal-autonomy.d.ts.map +1 -1
  102. package/dist/execution/index.d.ts +11 -1
  103. package/dist/execution/index.d.ts.map +1 -1
  104. package/dist/execution/index.js +3076 -526
  105. package/dist/execution/index.js.map +4 -4
  106. package/dist/execution/intelligent-compactor.d.ts +18 -7
  107. package/dist/execution/intelligent-compactor.d.ts.map +1 -1
  108. package/dist/execution/one-shot-llm.d.ts +52 -0
  109. package/dist/execution/one-shot-llm.d.ts.map +1 -0
  110. package/dist/execution/parallel-eternal-engine.d.ts +12 -0
  111. package/dist/execution/parallel-eternal-engine.d.ts.map +1 -1
  112. package/dist/execution/prompt-enhancer.d.ts +23 -4
  113. package/dist/execution/prompt-enhancer.d.ts.map +1 -1
  114. package/dist/execution/prompt-enhancer.js +24 -6
  115. package/dist/execution/prompt-enhancer.js.map +2 -2
  116. package/dist/execution/selective-compactor.d.ts +4 -4
  117. package/dist/execution/strategy-compactor.d.ts +7 -0
  118. package/dist/execution/strategy-compactor.d.ts.map +1 -1
  119. package/dist/execution/tool-executor.d.ts +19 -5
  120. package/dist/execution/tool-executor.d.ts.map +1 -1
  121. package/dist/hooks/registry.d.ts.map +1 -1
  122. package/dist/hooks/runner.d.ts.map +1 -1
  123. package/dist/hooks/shell-executor.d.ts.map +1 -1
  124. package/dist/hq/alerts.d.ts +5 -0
  125. package/dist/hq/alerts.d.ts.map +1 -1
  126. package/dist/hq/auth-store.d.ts +38 -10
  127. package/dist/hq/auth-store.d.ts.map +1 -1
  128. package/dist/hq/commands.d.ts +8 -4
  129. package/dist/hq/commands.d.ts.map +1 -1
  130. package/dist/hq/cost-bridge.d.ts +5 -3
  131. package/dist/hq/cost-bridge.d.ts.map +1 -1
  132. package/dist/hq/factory.d.ts +3 -0
  133. package/dist/hq/factory.d.ts.map +1 -1
  134. package/dist/hq/fleet-bridge.d.ts.map +1 -1
  135. package/dist/hq/index.d.ts +0 -1
  136. package/dist/hq/index.d.ts.map +1 -1
  137. package/dist/hq/index.js +1016 -283
  138. package/dist/hq/index.js.map +4 -4
  139. package/dist/hq/persistence.d.ts +42 -0
  140. package/dist/hq/persistence.d.ts.map +1 -1
  141. package/dist/hq/protocol/brain.d.ts +24 -0
  142. package/dist/hq/protocol/brain.d.ts.map +1 -0
  143. package/dist/hq/protocol/browser.d.ts +12 -0
  144. package/dist/hq/protocol/browser.d.ts.map +1 -0
  145. package/dist/hq/protocol/client.d.ts +69 -0
  146. package/dist/hq/protocol/client.d.ts.map +1 -0
  147. package/dist/hq/protocol/core.d.ts +130 -0
  148. package/dist/hq/protocol/core.d.ts.map +1 -0
  149. package/dist/hq/protocol/fleet.d.ts +73 -0
  150. package/dist/hq/protocol/fleet.d.ts.map +1 -0
  151. package/dist/hq/protocol/mailbox.d.ts +76 -0
  152. package/dist/hq/protocol/mailbox.d.ts.map +1 -0
  153. package/dist/hq/protocol/mcp.d.ts +51 -0
  154. package/dist/hq/protocol/mcp.d.ts.map +1 -0
  155. package/dist/hq/protocol/project.d.ts +26 -0
  156. package/dist/hq/protocol/project.d.ts.map +1 -0
  157. package/dist/hq/protocol/session.d.ts +153 -0
  158. package/dist/hq/protocol/session.d.ts.map +1 -0
  159. package/dist/hq/protocol/tool.d.ts +31 -0
  160. package/dist/hq/protocol/tool.d.ts.map +1 -0
  161. package/dist/hq/protocol.d.ts +10 -559
  162. package/dist/hq/protocol.d.ts.map +1 -1
  163. package/dist/hq/publisher.d.ts +4 -0
  164. package/dist/hq/publisher.d.ts.map +1 -1
  165. package/dist/index.d.ts +24 -8
  166. package/dist/index.d.ts.map +1 -1
  167. package/dist/index.js +7676 -3432
  168. package/dist/index.js.map +4 -4
  169. package/dist/infrastructure/index.js +13 -11
  170. package/dist/infrastructure/index.js.map +2 -2
  171. package/dist/infrastructure/token-counter.d.ts.map +1 -1
  172. package/dist/kernel/events/agent-events.d.ts +283 -0
  173. package/dist/kernel/events/agent-events.d.ts.map +1 -0
  174. package/dist/kernel/events/brain-events.d.ts +176 -0
  175. package/dist/kernel/events/brain-events.d.ts.map +1 -0
  176. package/dist/kernel/events/fleet-events.d.ts +39 -0
  177. package/dist/kernel/events/fleet-events.d.ts.map +1 -0
  178. package/dist/kernel/events/memory-events.d.ts +165 -0
  179. package/dist/kernel/events/memory-events.d.ts.map +1 -0
  180. package/dist/kernel/events/provider-events.d.ts +98 -0
  181. package/dist/kernel/events/provider-events.d.ts.map +1 -0
  182. package/dist/kernel/events/sdd-events.d.ts +118 -0
  183. package/dist/kernel/events/sdd-events.d.ts.map +1 -0
  184. package/dist/kernel/events/session-events.d.ts +227 -0
  185. package/dist/kernel/events/session-events.d.ts.map +1 -0
  186. package/dist/kernel/events/tool-events.d.ts +185 -0
  187. package/dist/kernel/events/tool-events.d.ts.map +1 -0
  188. package/dist/kernel/events/worktree-events.d.ts +58 -0
  189. package/dist/kernel/events/worktree-events.d.ts.map +1 -0
  190. package/dist/kernel/events.d.ts +12 -1282
  191. package/dist/kernel/events.d.ts.map +1 -1
  192. package/dist/kernel/index.js +22 -2
  193. package/dist/kernel/index.js.map +2 -2
  194. package/dist/kernel/run-controller.d.ts +15 -0
  195. package/dist/kernel/run-controller.d.ts.map +1 -1
  196. package/dist/mailbox-attach.d.ts.map +1 -1
  197. package/dist/models/index.js +19 -6
  198. package/dist/models/index.js.map +2 -2
  199. package/dist/models/llm-selector.d.ts +7 -0
  200. package/dist/models/llm-selector.d.ts.map +1 -1
  201. package/dist/observability/index.js +16 -15
  202. package/dist/observability/index.js.map +2 -2
  203. package/dist/plugins/auto-review-plugin.d.ts +36 -0
  204. package/dist/plugins/auto-review-plugin.d.ts.map +1 -0
  205. package/dist/plugins/chimera-plugin.d.ts +8 -0
  206. package/dist/plugins/chimera-plugin.d.ts.map +1 -1
  207. package/dist/security/index.d.ts +1 -0
  208. package/dist/security/index.d.ts.map +1 -1
  209. package/dist/security/index.js +260 -23
  210. package/dist/security/index.js.map +4 -4
  211. package/dist/security/permission-policy-schema.d.ts +77 -0
  212. package/dist/security/permission-policy-schema.d.ts.map +1 -0
  213. package/dist/security/permission-policy.d.ts +5 -0
  214. package/dist/security/permission-policy.d.ts.map +1 -1
  215. package/dist/security/secret-scrubber.d.ts.map +1 -1
  216. package/dist/security/secret-vault.d.ts +9 -0
  217. package/dist/security/secret-vault.d.ts.map +1 -1
  218. package/dist/session-registry.d.ts.map +1 -1
  219. package/dist/skills/index.js.map +2 -2
  220. package/dist/storage/config-loader.d.ts +10 -0
  221. package/dist/storage/config-loader.d.ts.map +1 -1
  222. package/dist/storage/goal-kanban.d.ts +69 -0
  223. package/dist/storage/goal-kanban.d.ts.map +1 -0
  224. package/dist/storage/goal-store.d.ts +1 -1
  225. package/dist/storage/goal-store.d.ts.map +1 -1
  226. package/dist/storage/index.d.ts +1 -1
  227. package/dist/storage/index.d.ts.map +1 -1
  228. package/dist/storage/index.js +572 -879
  229. package/dist/storage/index.js.map +4 -4
  230. package/dist/storage/memory-consolidator.d.ts +9 -1
  231. package/dist/storage/memory-consolidator.d.ts.map +1 -1
  232. package/dist/storage/plan-store.d.ts +1 -1
  233. package/dist/storage/plan-store.d.ts.map +1 -1
  234. package/dist/storage/queue-store.d.ts +4 -0
  235. package/dist/storage/queue-store.d.ts.map +1 -1
  236. package/dist/storage/session-store.d.ts +9 -0
  237. package/dist/storage/session-store.d.ts.map +1 -1
  238. package/dist/storage/task-store.d.ts +1 -1
  239. package/dist/storage/task-store.d.ts.map +1 -1
  240. package/dist/storage/todos-checkpoint.d.ts +2 -2
  241. package/dist/storage/todos-checkpoint.d.ts.map +1 -1
  242. package/dist/tools/council-tool.d.ts +25 -0
  243. package/dist/tools/council-tool.d.ts.map +1 -0
  244. package/dist/tools/index.d.ts +2 -0
  245. package/dist/tools/index.d.ts.map +1 -1
  246. package/dist/tools/index.js +1884 -12
  247. package/dist/tools/index.js.map +4 -4
  248. package/dist/tools/one-shot-llm-tool.d.ts +51 -0
  249. package/dist/tools/one-shot-llm-tool.d.ts.map +1 -0
  250. package/dist/types/config.d.ts +236 -4
  251. package/dist/types/config.d.ts.map +1 -1
  252. package/dist/types/context-window.d.ts.map +1 -1
  253. package/dist/types/council.d.ts +134 -0
  254. package/dist/types/council.d.ts.map +1 -0
  255. package/dist/types/default-config.d.ts +3 -3
  256. package/dist/types/hooks.d.ts +14 -0
  257. package/dist/types/hooks.d.ts.map +1 -1
  258. package/dist/types/index.d.ts +2 -0
  259. package/dist/types/index.d.ts.map +1 -1
  260. package/dist/types/index.js +40 -8
  261. package/dist/types/index.js.map +3 -3
  262. package/dist/types/multi-agent.d.ts +8 -0
  263. package/dist/types/multi-agent.d.ts.map +1 -1
  264. package/dist/types/observability.d.ts +5 -0
  265. package/dist/types/observability.d.ts.map +1 -1
  266. package/dist/types/one-shot-llm.d.ts +128 -0
  267. package/dist/types/one-shot-llm.d.ts.map +1 -0
  268. package/dist/types/plugin.d.ts +6 -0
  269. package/dist/types/plugin.d.ts.map +1 -1
  270. package/dist/types/provider.d.ts +17 -0
  271. package/dist/types/provider.d.ts.map +1 -1
  272. package/dist/types/system-prompt.d.ts +2 -1
  273. package/dist/types/system-prompt.d.ts.map +1 -1
  274. package/dist/types/tool-executor.d.ts +10 -0
  275. package/dist/types/tool-executor.d.ts.map +1 -1
  276. package/dist/types/tool.d.ts +7 -0
  277. package/dist/types/tool.d.ts.map +1 -1
  278. package/dist/utils/atomic-write.d.ts.map +1 -1
  279. package/dist/utils/glob-expand.d.ts.map +1 -1
  280. package/dist/utils/incoming-images.d.ts +39 -0
  281. package/dist/utils/incoming-images.d.ts.map +1 -0
  282. package/dist/utils/index.d.ts +1 -0
  283. package/dist/utils/index.d.ts.map +1 -1
  284. package/dist/utils/index.js +156 -11
  285. package/dist/utils/index.js.map +4 -4
  286. package/dist/utils/tool-wire-compact.d.ts +1 -0
  287. package/dist/utils/tool-wire-compact.d.ts.map +1 -1
  288. package/dist/utils/wstack-paths.d.ts +20 -4
  289. package/dist/utils/wstack-paths.d.ts.map +1 -1
  290. package/dist/worktree/worktree-manager.d.ts +13 -4
  291. package/dist/worktree/worktree-manager.d.ts.map +1 -1
  292. package/instructions/agents/browser.md +25 -16
  293. package/instructions/cli/next-task-predictor.md +5 -3
  294. package/instructions/coordination/subagent-baseline.md +17 -2
  295. package/instructions/leader-after-task.md +19 -8
  296. package/instructions/llm/council-judge.md +19 -0
  297. package/instructions/llm/council-voter.md +22 -0
  298. package/instructions/modes/architect.md +14 -9
  299. package/instructions/modes/audit-lite.md +9 -7
  300. package/instructions/modes/brief-system.md +12 -12
  301. package/instructions/modes/brief.md +7 -5
  302. package/instructions/modes/code-auditor.md +13 -8
  303. package/instructions/modes/code-reviewer.md +13 -8
  304. package/instructions/modes/debug-lite.md +7 -6
  305. package/instructions/modes/debugger.md +13 -9
  306. package/instructions/modes/devops.md +13 -9
  307. package/instructions/modes/plan-lite.md +7 -6
  308. package/instructions/modes/refactor-lite.md +7 -6
  309. package/instructions/modes/refactorer.md +14 -9
  310. package/instructions/modes/research-lite.md +8 -6
  311. package/instructions/modes/research-web.md +18 -55
  312. package/instructions/modes/review-lite.md +7 -6
  313. package/instructions/modes/teach-system.md +15 -15
  314. package/instructions/modes/teach.md +13 -12
  315. package/instructions/modes/test-lite.md +6 -5
  316. package/instructions/modes/tester.md +13 -9
  317. package/instructions/modes/ui-design.md +14 -8
  318. package/instructions/sections/tool/commit-hygiene.md +1 -7
  319. package/instructions/sections/tool/common-patterns.md +7 -1
  320. package/instructions/sections/tool/delegation-full.md +3 -31
  321. package/instructions/sections/tool/mailbox-compact.md +1 -1
  322. package/instructions/sections/tool/mailbox-full.md +6 -22
  323. package/instructions/system.md +294 -8
  324. package/package.json +2 -2
  325. package/skills/docker-deploy/SKILL.md +1 -1
  326. package/skills/mailbox-bridge/SKILL.md +16 -7
  327. package/skills/output-standards/SKILL.md +16 -16
  328. package/skills/wrongstack-mailbox/SKILL.md +57 -12
  329. package/dist/coordination/director-construction.d.ts +0 -90
  330. package/dist/coordination/director-construction.d.ts.map +0 -1
  331. package/dist/hq/agent-bridge.d.ts +0 -27
  332. package/dist/hq/agent-bridge.d.ts.map +0 -1
  333. package/dist/plugins/git-plugin.d.ts +0 -15
  334. package/dist/plugins/git-plugin.d.ts.map +0 -1
  335. package/dist/plugins/observability-plugin.d.ts +0 -19
  336. package/dist/plugins/observability-plugin.d.ts.map +0 -1
  337. package/dist/plugins/plan-plugin.d.ts +0 -16
  338. package/dist/plugins/plan-plugin.d.ts.map +0 -1
  339. package/dist/storage/memory-store.d.ts +0 -119
  340. 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") {
@@ -329,9 +375,17 @@ function globToRegex(pat) {
329
375
  return new RegExp(re + "$");
330
376
  }
331
377
  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);
378
+ let firstGlob = pat.length;
379
+ for (let i = 0; i < pat.length; i++) {
380
+ if (GLOB_CHARS.has(expectDefined(pat[i]))) {
381
+ firstGlob = i;
382
+ break;
383
+ }
384
+ }
385
+ const cut = Math.max(
386
+ pat.lastIndexOf(SEP, firstGlob - 1),
387
+ pat.lastIndexOf("/", firstGlob - 1)
388
+ );
335
389
  return cut < 0 ? "." : pat.slice(0, cut);
336
390
  }
337
391
  async function expandGlob(pattern) {
@@ -579,11 +633,31 @@ function truncate(s, max) {
579
633
 
580
634
  // src/utils/wstack-paths.ts
581
635
  import { createHash } from "node:crypto";
636
+ import * as fs2 from "node:fs";
582
637
  import * as os from "node:os";
583
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
+ }
584
657
  function projectSlug(absRoot) {
585
- const base = slugify(path3.basename(absRoot));
586
- 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);
587
661
  return `${base}-${hash}`;
588
662
  }
589
663
  function slugify(name) {
@@ -1417,9 +1491,11 @@ function normalizeSubagentStructuredReport(value) {
1417
1491
  const filesExamined = stringArray(value["files_examined"]);
1418
1492
  const nextSteps = stringArray(value["suggested_next_steps"]);
1419
1493
  const confidence = value["confidence"];
1494
+ const completion = value["completion"];
1495
+ const remainingWork = optionalString(value["remaining_work"]);
1420
1496
  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
1497
  (item) => item.trim().length > MAX_ITEM_CHARS
1422
- ) || typeof confidence !== "number" || !Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
1498
+ ) || 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
1499
  return void 0;
1424
1500
  }
1425
1501
  const report = {
@@ -1427,7 +1503,9 @@ function normalizeSubagentStructuredReport(value) {
1427
1503
  findings: findings.map((item) => item.trim()).filter(Boolean),
1428
1504
  files_examined: filesExamined.map((item) => item.trim()).filter(Boolean),
1429
1505
  confidence,
1430
- suggested_next_steps: nextSteps.map((item) => item.trim()).filter(Boolean)
1506
+ suggested_next_steps: nextSteps.map((item) => item.trim()).filter(Boolean),
1507
+ ...completion === "complete" || completion === "partial" ? { completion } : {},
1508
+ ...remainingWork ? { remaining_work: remainingWork } : {}
1431
1509
  };
1432
1510
  return JSON.stringify(report).length <= MAX_SUBAGENT_STRUCTURED_REPORT_CHARS ? report : void 0;
1433
1511
  }
@@ -1440,6 +1518,8 @@ function formatSubagentStructuredReport(report) {
1440
1518
  const next = report.suggested_next_steps.length ? report.suggested_next_steps.map((item) => `- ${item}`).join("\n") : "- (none)";
1441
1519
  return [
1442
1520
  report.summary,
1521
+ report.completion ? `Completion: ${report.completion}` : void 0,
1522
+ report.remaining_work ? `Remaining work: ${report.remaining_work}` : void 0,
1443
1523
  `
1444
1524
  Findings:
1445
1525
  ${findings}`,
@@ -1449,7 +1529,7 @@ Files examined: ${files}`,
1449
1529
  `
1450
1530
  Suggested next steps:
1451
1531
  ${next}`
1452
- ].join("\n");
1532
+ ].filter((line) => typeof line === "string").join("\n");
1453
1533
  }
1454
1534
  function makeSubagentResultTool() {
1455
1535
  return {
@@ -1489,6 +1569,16 @@ function makeSubagentResultTool() {
1489
1569
  maxItems: MAX_NEXT_STEPS,
1490
1570
  items: { type: "string" },
1491
1571
  description: "Concrete follow-ups; use an empty array when none remain."
1572
+ },
1573
+ completion: {
1574
+ type: "string",
1575
+ enum: ["complete", "partial"],
1576
+ description: "Use `partial` only when the task is intentionally stopping at a clean checkpoint and another worker should continue."
1577
+ },
1578
+ remaining_work: {
1579
+ type: "string",
1580
+ maxLength: MAX_SUMMARY_CHARS,
1581
+ description: "Concrete work left for a successor. Required by runtime validation when completion is `partial`."
1492
1582
  }
1493
1583
  },
1494
1584
  required: ["summary", "findings", "files_examined", "confidence", "suggested_next_steps"],
@@ -1518,6 +1608,9 @@ function isRecord(value) {
1518
1608
  function nonEmptyString(value) {
1519
1609
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
1520
1610
  }
1611
+ function optionalString(value) {
1612
+ return value === void 0 ? void 0 : nonEmptyString(value);
1613
+ }
1521
1614
  function stringArray(value) {
1522
1615
  return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
1523
1616
  }
@@ -1800,7 +1893,7 @@ var TOOLS = {
1800
1893
  };
1801
1894
 
1802
1895
  // src/coordination/agents/agent-prompts.ts
1803
- import { readFileSync, statSync } from "node:fs";
1896
+ import { readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
1804
1897
  import * as os2 from "node:os";
1805
1898
  import * as path4 from "node:path";
1806
1899
  import { fileURLToPath } from "node:url";
@@ -1815,7 +1908,7 @@ function agentPrompt(id) {
1815
1908
  let resolved = "";
1816
1909
  for (const dir of agentPromptDirCandidates(envDir)) {
1817
1910
  try {
1818
- resolved = readFileSync(path4.join(dir, fileName), "utf8").trimEnd();
1911
+ resolved = readFileSync2(path4.join(dir, fileName), "utf8").trimEnd();
1819
1912
  break;
1820
1913
  } catch {
1821
1914
  }
@@ -1845,7 +1938,7 @@ function agentPromptDirCandidates(envDir) {
1845
1938
  }
1846
1939
  function isDirectory(candidate) {
1847
1940
  try {
1848
- return statSync(candidate).isDirectory();
1941
+ return statSync2(candidate).isDirectory();
1849
1942
  } catch {
1850
1943
  return false;
1851
1944
  }
@@ -3458,7 +3551,12 @@ var BrainDecisionQueue = class {
3458
3551
  if (this.opts.timeoutMs && this.opts.timeoutMs > 0) {
3459
3552
  entry.timer = setTimeout(() => {
3460
3553
  this.pending.delete(request.id);
3461
- resolve9({ type: "deny", reason: "Brain human decision timed out." });
3554
+ resolve9(
3555
+ this.opts.onTimeout?.(request) ?? {
3556
+ type: "deny",
3557
+ reason: "Brain human decision timed out."
3558
+ }
3559
+ );
3462
3560
  }, this.opts.timeoutMs);
3463
3561
  }
3464
3562
  this.pending.set(request.id, entry);
@@ -3475,6 +3573,46 @@ var BrainDecisionQueue = class {
3475
3573
  }
3476
3574
  }
3477
3575
  };
3576
+ function terminalPolicyDecision(request) {
3577
+ const recommended = request.options?.find((option) => option.recommended);
3578
+ if (recommended && (request.risk === "low" || request.risk === "medium")) {
3579
+ return {
3580
+ type: "answer",
3581
+ optionId: recommended.id,
3582
+ text: recommended.label,
3583
+ rationale: `Headless terminal policy: caller-recommended option accepted at ${request.risk} risk (no human available by configuration).`
3584
+ };
3585
+ }
3586
+ if (request.fallback === "continue") {
3587
+ return {
3588
+ type: "answer",
3589
+ text: "Continue with the caller default.",
3590
+ rationale: "Headless terminal policy: request declares continue as its safe fallback."
3591
+ };
3592
+ }
3593
+ return {
3594
+ type: "deny",
3595
+ reason: `Headless terminal policy: no safe automatic option for "${request.question}" (risk: ${request.risk}). The proposed action was not taken.`
3596
+ };
3597
+ }
3598
+ var EscalationRoutingBrainArbiter = class {
3599
+ constructor(inner, queue, getMode) {
3600
+ this.inner = inner;
3601
+ this.queue = queue;
3602
+ this.getMode = getMode;
3603
+ }
3604
+ inner;
3605
+ queue;
3606
+ getMode;
3607
+ async decide(request) {
3608
+ const decision = await this.inner.decide(request);
3609
+ if (decision.type !== "ask_human") return decision;
3610
+ if (this.getMode() === "interactive" && this.queue) {
3611
+ return this.queue.requestHumanDecision(request);
3612
+ }
3613
+ return terminalPolicyDecision(request);
3614
+ }
3615
+ };
3478
3616
  var HumanEscalatingBrainArbiter = class {
3479
3617
  constructor(inner, queue) {
3480
3618
  this.inner = inner;
@@ -3550,29 +3688,348 @@ function formatHumanPrompt(request) {
3550
3688
  return lines.join("\n");
3551
3689
  }
3552
3690
 
3691
+ // src/coordination/brain-ledger.ts
3692
+ import { appendFile, mkdir as mkdir2, readFile } from "node:fs/promises";
3693
+ import { dirname as dirname4 } from "node:path";
3694
+ var QUESTION_MAX = 200;
3695
+ var DETAIL_MAX = 240;
3696
+ var truncate2 = (s, max) => s === void 0 ? void 0 : s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
3697
+ function brainDecisionKey(source, question) {
3698
+ const normalized = question.toLowerCase().replace(/[^\s]*\d[^\s]*/g, "#").replace(/\s+/g, " ").trim().slice(0, 100);
3699
+ return `${source ?? "?"}:${normalized}`;
3700
+ }
3701
+ function fmtAge(ms) {
3702
+ const s = Math.max(0, Math.round(ms / 1e3));
3703
+ if (s < 60) return `${s}s ago`;
3704
+ if (s < 3600) return `${Math.round(s / 60)}m ago`;
3705
+ return `${Math.round(s / 3600)}h ago`;
3706
+ }
3707
+ function createLedgerGuardBrainArbiter(opts) {
3708
+ const denyAfter = opts.denyAfter ?? 3;
3709
+ return {
3710
+ async decide(request) {
3711
+ if (denyAfter > 0) {
3712
+ const streak = opts.failureStreakFor(request);
3713
+ if (streak >= denyAfter) {
3714
+ return {
3715
+ type: "deny",
3716
+ 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)})`
3717
+ };
3718
+ }
3719
+ }
3720
+ return opts.inner.decide(request);
3721
+ }
3722
+ };
3723
+ }
3724
+ var BrainDecisionLedger = class {
3725
+ constructor(opts) {
3726
+ this.opts = opts;
3727
+ this.maxMemoryEntries = opts.maxMemoryEntries ?? 500;
3728
+ this.retryWindowMs = opts.interventionRetryWindowMs ?? 10 * 6e4;
3729
+ this.now = opts.now ?? Date.now;
3730
+ }
3731
+ opts;
3732
+ entries = [];
3733
+ outcomeByRequest = /* @__PURE__ */ new Map();
3734
+ /** subagentId → decision request ids awaiting that subagent's task result. */
3735
+ pendingBudgetOutcomes = /* @__PURE__ */ new Map();
3736
+ lastInterventionByKind = /* @__PURE__ */ new Map();
3737
+ unsubscribers = [];
3738
+ /** Serialized fire-and-forget writes — awaited by `stop()` (drain). */
3739
+ writeChain = Promise.resolve();
3740
+ dirReady = false;
3741
+ maxMemoryEntries;
3742
+ retryWindowMs;
3743
+ now;
3744
+ /**
3745
+ * Subscribe to the EventBus and seed the in-memory ring from the JSONL
3746
+ * tail. The returned promise resolves once the seed load finished
3747
+ * (best-effort — a missing/corrupt file is an empty history, not an error).
3748
+ */
3749
+ async start() {
3750
+ const { events } = this.opts;
3751
+ this.unsubscribers.push(
3752
+ events.on("brain.decision_answered", (e) => {
3753
+ this.record({
3754
+ at: e.at,
3755
+ sessionId: e.sessionId,
3756
+ requestId: e.request.id,
3757
+ kind: "answered",
3758
+ source: e.request.source,
3759
+ risk: e.request.risk,
3760
+ question: truncate2(e.request.question, QUESTION_MAX),
3761
+ optionId: e.decision.type === "answer" ? e.decision.optionId : void 0,
3762
+ detail: e.decision.type === "answer" ? truncate2(e.decision.rationale ?? e.decision.text, DETAIL_MAX) : void 0
3763
+ });
3764
+ }),
3765
+ events.on("brain.decision_denied", (e) => {
3766
+ this.record({
3767
+ at: e.at,
3768
+ sessionId: e.sessionId,
3769
+ requestId: e.request.id,
3770
+ kind: "denied",
3771
+ source: e.request.source,
3772
+ risk: e.request.risk,
3773
+ question: truncate2(e.request.question, QUESTION_MAX),
3774
+ detail: e.decision.type === "deny" ? truncate2(e.decision.reason, DETAIL_MAX) : void 0
3775
+ });
3776
+ }),
3777
+ events.on("brain.decision_ask_human", (e) => {
3778
+ this.record({
3779
+ at: e.at,
3780
+ sessionId: e.sessionId,
3781
+ requestId: e.request.id,
3782
+ kind: "ask_human",
3783
+ source: e.request.source,
3784
+ risk: e.request.risk,
3785
+ question: truncate2(e.request.question, QUESTION_MAX)
3786
+ });
3787
+ }),
3788
+ events.on("brain.intervention", (e) => {
3789
+ this.record({
3790
+ at: e.at,
3791
+ sessionId: e.sessionId,
3792
+ requestId: e.request.id,
3793
+ kind: "intervention",
3794
+ source: e.request.source,
3795
+ risk: e.request.risk,
3796
+ question: truncate2(`[${e.kind}] ${e.request.question}`, QUESTION_MAX),
3797
+ detail: e.intervened ? "steered" : "observed only"
3798
+ });
3799
+ if (!e.intervened) return;
3800
+ const prev = this.lastInterventionByKind.get(e.kind);
3801
+ if (prev && e.at - prev.at <= this.retryWindowMs) {
3802
+ this.recordOutcome(
3803
+ prev.requestId,
3804
+ "failure",
3805
+ `same signal (${e.kind}) re-triggered ${fmtAge(e.at - prev.at).replace(" ago", " later")}`,
3806
+ e.sessionId
3807
+ );
3808
+ }
3809
+ this.lastInterventionByKind.set(e.kind, { requestId: e.request.id, at: e.at });
3810
+ }),
3811
+ // ── Outcome correlation: budget extensions ────────────────────────
3812
+ events.on("subagent.budget_extended", (e) => {
3813
+ const requestId = `director-budget-${e.subagentId}-${e.kind}`;
3814
+ if (!this.entries.some((entry) => entry.requestId === requestId)) return;
3815
+ let set = this.pendingBudgetOutcomes.get(e.subagentId);
3816
+ if (!set) {
3817
+ set = /* @__PURE__ */ new Set();
3818
+ this.pendingBudgetOutcomes.set(e.subagentId, set);
3819
+ }
3820
+ set.add(requestId);
3821
+ }),
3822
+ events.on("subagent.task_completed", (e) => {
3823
+ const pending = this.pendingBudgetOutcomes.get(e.subagentId);
3824
+ if (!pending) return;
3825
+ this.pendingBudgetOutcomes.delete(e.subagentId);
3826
+ const outcome = e.status === "success" ? "success" : "failure";
3827
+ for (const requestId of pending) {
3828
+ this.recordOutcome(
3829
+ requestId,
3830
+ outcome,
3831
+ `budget-extended subagent's task ${e.status}`,
3832
+ e.sessionId
3833
+ );
3834
+ }
3835
+ })
3836
+ );
3837
+ await this.load();
3838
+ }
3839
+ /** Unsubscribe and drain pending writes. Safe to call twice. */
3840
+ async stop() {
3841
+ for (const off of this.unsubscribers) off();
3842
+ this.unsubscribers.length = 0;
3843
+ await this.writeChain.catch(() => {
3844
+ });
3845
+ }
3846
+ /**
3847
+ * Attach a real-world outcome to an earlier decision. Appended to the
3848
+ * JSONL, folded into future digests, and re-emitted as `brain.outcome`.
3849
+ */
3850
+ recordOutcome(requestId, outcome, detail, sessionId) {
3851
+ this.outcomeByRequest.set(requestId, outcome);
3852
+ this.record({
3853
+ at: this.now(),
3854
+ sessionId,
3855
+ requestId,
3856
+ kind: "outcome",
3857
+ outcome,
3858
+ detail: truncate2(detail, DETAIL_MAX)
3859
+ });
3860
+ this.opts.events.emit("brain.outcome", {
3861
+ sessionId,
3862
+ requestId,
3863
+ outcome,
3864
+ detail,
3865
+ at: this.now()
3866
+ });
3867
+ }
3868
+ /** Last `n` ledger rows, oldest first. */
3869
+ tail(n) {
3870
+ return this.entries.slice(-n);
3871
+ }
3872
+ /**
3873
+ * Consecutive most-recent FAILURE outcomes among answered decisions of
3874
+ * this request's group. Decisions whose outcome is still unknown are
3875
+ * skipped (they neither extend nor break the streak); the first known
3876
+ * SUCCESS ends it. Powers the deterministic ledger guard: "the last K
3877
+ * times we approved this, it went wrong — stop approving".
3878
+ */
3879
+ failureStreakFor(request) {
3880
+ const key = brainDecisionKey(request.source, request.question);
3881
+ let streak = 0;
3882
+ for (let i = this.entries.length - 1; i >= 0; i--) {
3883
+ const e = this.entries[i];
3884
+ if (e?.kind !== "answered" || e.requestId === request.id || e.question === void 0 || brainDecisionKey(e.source, e.question) !== key) {
3885
+ continue;
3886
+ }
3887
+ const outcome = this.outcomeByRequest.get(e.requestId);
3888
+ if (outcome === void 0) continue;
3889
+ if (outcome === "success") break;
3890
+ streak += 1;
3891
+ }
3892
+ return streak;
3893
+ }
3894
+ /**
3895
+ * Short decision-history block for the LLM/council prompt: how similar
3896
+ * past decisions went and — when known — how they turned OUT. Returns
3897
+ * undefined when there is no relevant history (no prompt noise).
3898
+ */
3899
+ digestFor(request) {
3900
+ const key = brainDecisionKey(request.source, request.question);
3901
+ const related = this.entries.filter(
3902
+ (e) => (e.kind === "answered" || e.kind === "denied") && e.requestId !== request.id && e.question !== void 0 && brainDecisionKey(e.source, e.question) === key
3903
+ );
3904
+ if (related.length === 0) return void 0;
3905
+ const recent = related.slice(-5).reverse();
3906
+ const now = this.now();
3907
+ const lines = recent.map((e) => {
3908
+ const what = e.kind === "denied" ? "denied" : e.optionId ? `chose [${e.optionId}]` : "answered";
3909
+ const outcome = this.outcomeByRequest.get(e.requestId);
3910
+ const suffix = outcome === "failure" ? " \u2192 later FAILED" : outcome === "success" ? " \u2192 later succeeded" : "";
3911
+ return `- ${fmtAge(now - e.at)}: ${what}${suffix}`;
3912
+ });
3913
+ const approved = related.filter((e) => e.kind === "answered");
3914
+ const failedAfter = approved.filter(
3915
+ (e) => this.outcomeByRequest.get(e.requestId) === "failure"
3916
+ ).length;
3917
+ const summary = `Summary: ${related.length} similar decision(s); ${approved.length} answered${failedAfter > 0 ? ` (${failedAfter} later failed)` : ""}, ${related.length - approved.length} denied.`;
3918
+ return [...lines, summary].join("\n");
3919
+ }
3920
+ // ── Private ───────────────────────────────────────────────────────────
3921
+ record(entry) {
3922
+ this.entries.push(entry);
3923
+ if (this.entries.length > this.maxMemoryEntries) {
3924
+ const evictedRequestIds = /* @__PURE__ */ new Set();
3925
+ const evictCount = this.entries.length - this.maxMemoryEntries;
3926
+ const evicted = this.entries.splice(0, evictCount);
3927
+ for (const e of evicted) {
3928
+ if (e.requestId) evictedRequestIds.add(e.requestId);
3929
+ }
3930
+ if (evictedRequestIds.size > 0) {
3931
+ const remainingRequestIds = /* @__PURE__ */ new Set();
3932
+ for (const e of this.entries) {
3933
+ if (e.requestId) remainingRequestIds.add(e.requestId);
3934
+ }
3935
+ for (const id of evictedRequestIds) {
3936
+ if (!remainingRequestIds.has(id)) this.outcomeByRequest.delete(id);
3937
+ }
3938
+ }
3939
+ }
3940
+ this.append(entry);
3941
+ }
3942
+ append(entry) {
3943
+ this.writeChain = this.writeChain.then(async () => {
3944
+ if (!this.dirReady) {
3945
+ await mkdir2(dirname4(this.opts.filePath), { recursive: true });
3946
+ this.dirReady = true;
3947
+ }
3948
+ await appendFile(this.opts.filePath, `${JSON.stringify(entry)}
3949
+ `, "utf8");
3950
+ }).catch(() => {
3951
+ });
3952
+ }
3953
+ /** Seed the in-memory ring from the JSONL tail (cross-session learning). */
3954
+ async load() {
3955
+ let raw;
3956
+ try {
3957
+ raw = await readFile(this.opts.filePath, "utf8");
3958
+ } catch {
3959
+ return;
3960
+ }
3961
+ const lines = raw.split("\n").filter((l) => l.trim().length > 0);
3962
+ const seed = [];
3963
+ for (const line of lines.slice(-this.maxMemoryEntries)) {
3964
+ try {
3965
+ const parsed = JSON.parse(line);
3966
+ if (typeof parsed?.requestId === "string" && typeof parsed.at === "number") {
3967
+ seed.push(parsed);
3968
+ }
3969
+ } catch {
3970
+ }
3971
+ }
3972
+ this.entries.unshift(...seed);
3973
+ if (this.entries.length > this.maxMemoryEntries) {
3974
+ this.entries.splice(0, this.entries.length - this.maxMemoryEntries);
3975
+ }
3976
+ for (const e of this.entries) {
3977
+ if (e.kind === "outcome" && e.outcome) this.outcomeByRequest.set(e.requestId, e.outcome);
3978
+ }
3979
+ }
3980
+ };
3981
+
3553
3982
  // src/coordination/brain-monitor.ts
3554
3983
  import { randomUUID as randomUUID2 } from "node:crypto";
3984
+ var FILE_EDIT_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "patch", "multi_edit", "multiedit", "str_replace"]);
3985
+ function editedPath(input) {
3986
+ if (!input || typeof input !== "object") return void 0;
3987
+ const r = input;
3988
+ const candidate = r["file_path"] ?? r["path"] ?? r["filePath"] ?? r["file"];
3989
+ return typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
3990
+ }
3555
3991
  var BrainMonitor = class {
3556
3992
  constructor(opts) {
3557
3993
  this.opts = opts;
3558
3994
  this.toolFailureStreak = opts.toolFailureStreak ?? 3;
3559
3995
  this.errorStormCount = opts.errorStormCount ?? 4;
3560
3996
  this.errorStormWindowMs = opts.errorStormWindowMs ?? 6e4;
3997
+ this.stallMs = opts.stallMs ?? 3e5;
3998
+ this.stallCheckIntervalMs = opts.stallCheckIntervalMs ?? 3e4;
3999
+ this.fileChurnThreshold = opts.fileChurnThreshold ?? 5;
4000
+ this.fileChurnWindowMs = opts.fileChurnWindowMs ?? 6e5;
3561
4001
  this.cooldownMs = opts.cooldownMs ?? 12e4;
3562
4002
  }
3563
4003
  opts;
3564
4004
  failStreaks = /* @__PURE__ */ new Map();
3565
4005
  errorTimestamps = [];
4006
+ editTimestamps = /* @__PURE__ */ new Map();
3566
4007
  lastEngagedAt = /* @__PURE__ */ new Map();
3567
4008
  unsubscribers = [];
3568
4009
  engaging = false;
4010
+ activeRuns = 0;
4011
+ lastProgressAt = 0;
4012
+ stallTimer;
3569
4013
  toolFailureStreak;
3570
4014
  errorStormCount;
3571
4015
  errorStormWindowMs;
4016
+ stallMs;
4017
+ stallCheckIntervalMs;
4018
+ fileChurnThreshold;
4019
+ fileChurnWindowMs;
3572
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
+ }
3573
4026
  start() {
3574
4027
  this.unsubscribers.push(
3575
4028
  this.opts.events.on("tool.executed", (e) => {
4029
+ const leaderSid = this.resolveLeaderSessionId();
4030
+ if (leaderSid && e.sessionId && e.sessionId !== leaderSid) return;
4031
+ this.lastProgressAt = Date.now();
4032
+ this.trackFileChurn(e.name, e.ok, e.input);
3576
4033
  if (e.ok) {
3577
4034
  this.failStreaks.delete(e.name);
3578
4035
  return;
@@ -3592,8 +4049,50 @@ var BrainMonitor = class {
3592
4049
  }
3593
4050
  })
3594
4051
  );
4052
+ if (this.stallMs > 0) {
4053
+ this.unsubscribers.push(
4054
+ this.opts.events.on("agent.run.started", (e) => {
4055
+ const lsid = this.resolveLeaderSessionId();
4056
+ if (lsid && e.sessionId && e.sessionId !== lsid) return;
4057
+ this.activeRuns += 1;
4058
+ this.lastProgressAt = Date.now();
4059
+ }),
4060
+ this.opts.events.on("agent.run.completed", (e) => {
4061
+ const lsid = this.resolveLeaderSessionId();
4062
+ if (lsid && e.sessionId && e.sessionId !== lsid) return;
4063
+ this.activeRuns = Math.max(0, this.activeRuns - 1);
4064
+ }),
4065
+ this.opts.events.on("agent.run.error", (e) => {
4066
+ const lsid = this.resolveLeaderSessionId();
4067
+ if (lsid && e.sessionId && e.sessionId !== lsid) return;
4068
+ this.activeRuns = Math.max(0, this.activeRuns - 1);
4069
+ }),
4070
+ this.opts.events.on("iteration.started", (e) => {
4071
+ const lsid = this.resolveLeaderSessionId();
4072
+ if (lsid && e.sessionId && e.sessionId !== lsid) return;
4073
+ this.lastProgressAt = Date.now();
4074
+ })
4075
+ );
4076
+ this.stallTimer = setInterval(() => {
4077
+ if (this.activeRuns === 0 || this.lastProgressAt === 0) return;
4078
+ const idleMs = Date.now() - this.lastProgressAt;
4079
+ if (idleMs < this.stallMs) return;
4080
+ this.lastProgressAt = Date.now();
4081
+ void this.engage("agent_stall", {
4082
+ 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?`,
4083
+ context: [
4084
+ `Active runs: ${this.activeRuns}`,
4085
+ `Idle for: ${Math.round(idleMs / 1e3)}s`,
4086
+ `Stall threshold: ${Math.round(this.stallMs / 1e3)}s`
4087
+ ].join("\n")
4088
+ });
4089
+ }, this.stallCheckIntervalMs);
4090
+ this.stallTimer.unref?.();
4091
+ }
3595
4092
  this.unsubscribers.push(
3596
4093
  this.opts.events.on("error", (e) => {
4094
+ const lsid = this.resolveLeaderSessionId();
4095
+ if (lsid && e.sessionId && e.sessionId !== lsid) return;
3597
4096
  const now = Date.now();
3598
4097
  this.errorTimestamps.push(now);
3599
4098
  this.errorTimestamps = this.errorTimestamps.filter(
@@ -3616,6 +4115,37 @@ var BrainMonitor = class {
3616
4115
  this.unsubscribers.length = 0;
3617
4116
  this.failStreaks.clear();
3618
4117
  this.errorTimestamps = [];
4118
+ this.editTimestamps.clear();
4119
+ if (this.stallTimer) {
4120
+ clearInterval(this.stallTimer);
4121
+ this.stallTimer = void 0;
4122
+ }
4123
+ this.activeRuns = 0;
4124
+ this.lastProgressAt = 0;
4125
+ }
4126
+ /** Sliding-window count of successful edits per file → churn signal. */
4127
+ trackFileChurn(toolName, ok, input) {
4128
+ if (!ok || !FILE_EDIT_TOOLS.has(toolName.toLowerCase())) return;
4129
+ const path20 = editedPath(input);
4130
+ if (!path20) return;
4131
+ const now = Date.now();
4132
+ const stamps = (this.editTimestamps.get(path20) ?? []).filter(
4133
+ (t) => now - t <= this.fileChurnWindowMs
4134
+ );
4135
+ stamps.push(now);
4136
+ if (stamps.length >= this.fileChurnThreshold) {
4137
+ this.editTimestamps.delete(path20);
4138
+ void this.engage("file_churn", {
4139
+ 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?`,
4140
+ context: [
4141
+ `File: ${path20}`,
4142
+ `Edits in window: ${stamps.length}`,
4143
+ `Window: ${Math.round(this.fileChurnWindowMs / 1e3)}s`
4144
+ ].join("\n")
4145
+ });
4146
+ return;
4147
+ }
4148
+ this.editTimestamps.set(path20, stamps);
3619
4149
  }
3620
4150
  async engage(kind, input) {
3621
4151
  const last = this.lastEngagedAt.get(kind) ?? 0;
@@ -4422,76 +4952,45 @@ import * as path5 from "node:path";
4422
4952
  import { randomUUID as randomUUID4 } from "node:crypto";
4423
4953
 
4424
4954
  // 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
- };
4955
+ function defineAgent(id, name, promptRole = id, provider) {
4956
+ return {
4957
+ id,
4958
+ name,
4959
+ role: id,
4960
+ prompt: agentPrompt(promptRole),
4961
+ ...provider === void 0 ? {} : { provider }
4962
+ };
4963
+ }
4964
+ var AUDIT_LOG_AGENT = defineAgent("audit-log", "Audit Log");
4965
+ var BUG_HUNTER_AGENT = defineAgent("bug-hunter", "Bug Hunter");
4966
+ var REFACTOR_PLANNER_AGENT = defineAgent("refactor-planner", "Refactor Planner");
4967
+ var SECURITY_SCANNER_AGENT = defineAgent("security-scanner", "Security Scanner");
4968
+ var SHADOW_AGENT = defineAgent("shadow-agent", "Shadow");
4969
+ var CRITIC_AGENT = defineAgent("critic", "Critic");
4471
4970
  var FLEET_ROSTER = {
4472
4971
  "audit-log": AUDIT_LOG_AGENT,
4473
4972
  "bug-hunter": BUG_HUNTER_AGENT,
4474
4973
  "refactor-planner": REFACTOR_PLANNER_AGENT,
4475
4974
  "security-scanner": SECURITY_SCANNER_AGENT,
4476
- "critic": CRITIC_AGENT,
4975
+ critic: CRITIC_AGENT,
4477
4976
  "shadow-agent": SHADOW_AGENT,
4478
4977
  ...Object.fromEntries(
4479
4978
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.config])
4480
4979
  )
4481
4980
  };
4482
- var DEFAULT_IDLE_TIMEOUT_MS = 15 * 60 * 1e3;
4981
+ var DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
4483
4982
  var FLEET_ROSTER_BUDGETS = {
4484
- "audit-log": { timeoutMs: 7.5 * 60 * 60 * 1e3, maxIterations: 5e3, maxToolCalls: 15e3 },
4485
- "bug-hunter": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
4486
- "refactor-planner": { timeoutMs: 7.5 * 60 * 60 * 1e3, maxIterations: 6e3, maxToolCalls: 18e3 },
4487
- "security-scanner": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
4488
- "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 },
4489
4988
  "shadow-agent": {
4490
4989
  idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
4491
- maxIterations: 2e3,
4492
- maxToolCalls: 5e3,
4493
- maxTokens: 6e4,
4494
- maxCostUsd: 1
4990
+ maxIterations: 1e3,
4991
+ maxToolCalls: 2500,
4992
+ maxTokens: 3e4,
4993
+ maxCostUsd: 0.5
4495
4994
  },
4496
4995
  ...Object.fromEntries(
4497
4996
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.budget])
@@ -4499,8 +4998,8 @@ var FLEET_ROSTER_BUDGETS = {
4499
4998
  };
4500
4999
  var GENERIC_SUBAGENT_BUDGET = {
4501
5000
  idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
4502
- maxIterations: 5e3,
4503
- maxToolCalls: 15e3
5001
+ maxIterations: 2500,
5002
+ maxToolCalls: 7500
4504
5003
  };
4505
5004
  function applyRosterBudget(cfg) {
4506
5005
  const roleBudget = cfg.role ? FLEET_ROSTER_BUDGETS[cfg.role] : void 0;
@@ -4522,41 +5021,11 @@ function applyRosterBudget(cfg) {
4522
5021
  };
4523
5022
  }
4524
5023
  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
- };
5024
+ var CLINE_AGENT = defineAgent("cline", "Cline", "acp-cline", "acp");
5025
+ var GEMINI_CLI_AGENT = defineAgent("gemini-cli", "Gemini CLI", "acp-gemini-cli", "acp");
5026
+ var COPILOT_AGENT = defineAgent("copilot", "GitHub Copilot", "acp-copilot", "acp");
5027
+ var OPENHANDS_AGENT = defineAgent("openhands", "OpenHands", "acp-openhands", "acp");
5028
+ var GOOSE_AGENT = defineAgent("goose", "Goose", "acp-goose", "acp");
4560
5029
  var ACP_AGENTS = [
4561
5030
  CLINE_AGENT,
4562
5031
  GEMINI_CLI_AGENT,
@@ -4564,11 +5033,14 @@ var ACP_AGENTS = [
4564
5033
  OPENHANDS_AGENT,
4565
5034
  GOOSE_AGENT
4566
5035
  ];
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 };
5036
+ var ACP_AGENT_BUDGET = {
5037
+ timeoutMs: 10 * 60 * 60 * 1e3,
5038
+ maxIterations: 8e3,
5039
+ maxToolCalls: 2e4
5040
+ };
5041
+ for (const agent of ACP_AGENTS) {
5042
+ FLEET_ROSTER_BUDGETS[agent.role] = { ...ACP_AGENT_BUDGET };
5043
+ }
4572
5044
  var FLEET_ROSTER_WITHACP = {
4573
5045
  ...FLEET_ROSTER,
4574
5046
  ...Object.fromEntries(ACP_AGENTS.map((a) => [a.role, a]))
@@ -4576,75 +5048,81 @@ var FLEET_ROSTER_WITHACP = {
4576
5048
 
4577
5049
  // src/coordination/delegate-tool.ts
4578
5050
  function createDelegateTool(opts) {
4579
- const defaultTimeoutMs = opts.defaultTimeoutMs ?? 30 * 60 * 1e3;
5051
+ const defaultTimeoutMs = opts.defaultTimeoutMs ?? 4 * 60 * 60 * 1e3;
4580
5052
  const rosterIds = opts.roster ? Object.keys(opts.roster) : [];
4581
5053
  const inputSchema = {
4582
5054
  type: "object",
4583
5055
  properties: {
4584
5056
  task: {
4585
5057
  type: "string",
4586
- 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)."
4587
5059
  },
4588
5060
  role: {
4589
5061
  type: "string",
4590
- 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.",
4591
- 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."
4592
5063
  },
4593
5064
  name: {
4594
5065
  type: "string",
4595
- 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."
4596
5067
  },
4597
5068
  provider: {
4598
5069
  type: "string",
4599
- 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.'
4600
5071
  },
4601
5072
  model: {
4602
5073
  type: "string",
4603
- description: "Model id within the provider. Defaults to the host model when omitted."
5074
+ description: "Model id within the provider. Defaults to host model."
4604
5075
  },
4605
5076
  systemPromptOverride: {
4606
5077
  type: "string",
4607
- description: "Optional extra prompt text appended to the role baseline."
5078
+ description: "Extra prompt text appended to the role baseline."
4608
5079
  },
4609
5080
  timeoutMs: {
4610
5081
  type: "number",
4611
5082
  minimum: 1,
4612
- 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.`
4613
5084
  },
4614
5085
  maxIterations: {
4615
5086
  type: "number",
4616
5087
  minimum: 1,
4617
- 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."
4618
5089
  },
4619
5090
  maxToolCalls: {
4620
5091
  type: "number",
4621
5092
  minimum: 1,
4622
- 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."
4623
5094
  },
4624
5095
  idleTimeoutMs: {
4625
5096
  type: "number",
4626
5097
  minimum: 1,
4627
- 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."
4628
5099
  },
4629
5100
  maxTokens: {
4630
5101
  type: "number",
4631
5102
  minimum: 1,
4632
- 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."
4633
5104
  },
4634
5105
  maxCostUsd: {
4635
5106
  type: "number",
4636
5107
  minimum: 0,
4637
- description: "Maximum estimated USD cost the subagent may incur. Unset = use the role/coordinator default."
5108
+ description: "Max estimated USD cost. Unset = role default."
5109
+ },
5110
+ maxHandoffs: {
5111
+ type: "number",
5112
+ minimum: 0,
5113
+ maximum: 8,
5114
+ description: "Max fresh-worker continuations after budget exhaustion. Default 1. Each gets the prior partial report."
4638
5115
  }
4639
5116
  },
4640
5117
  required: ["task"]
4641
5118
  };
4642
5119
  return {
4643
5120
  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.",
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.",
4646
5123
  permission: "auto",
4647
5124
  mutating: false,
5125
+ managesOwnTimeout: true,
4648
5126
  capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
4649
5127
  inputSchema,
4650
5128
  async execute(input, _ctx, execOpts) {
@@ -4668,10 +5146,9 @@ function createDelegateTool(opts) {
4668
5146
  director = await opts.host.promoteToDirector();
4669
5147
  }
4670
5148
  if (!director) {
4671
- const reason = opts.host.getPromotionBlockReason?.();
4672
5149
  return {
4673
5150
  ok: false,
4674
- 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."
4675
5152
  };
4676
5153
  }
4677
5154
  const timeoutMs = i.timeoutMs ?? defaultTimeoutMs;
@@ -4684,7 +5161,7 @@ function createDelegateTool(opts) {
4684
5161
  error: `Unknown role "${i.role}". Available: ${rosterIds.join(", ") || "(no roster configured)"}.`
4685
5162
  };
4686
5163
  }
4687
- cfg = instantiateRosterConfig(i.role, base);
5164
+ cfg = instantiateRosterConfig(i.role, base, i.timeoutMs, defaultTimeoutMs);
4688
5165
  if (i.systemPromptOverride) cfg.systemPromptOverride = i.systemPromptOverride;
4689
5166
  if (i.provider) cfg.provider = i.provider;
4690
5167
  if (i.model) cfg.model = i.model;
@@ -4723,172 +5200,175 @@ function createDelegateTool(opts) {
4723
5200
  cfg.timeoutMs = Math.max(3e4, timeoutMs - SUBAGENT_TIMEOUT_BUFFER_MS);
4724
5201
  }
4725
5202
  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
5203
  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,
5204
+ const maxHandoffs = Math.min(8, Math.max(0, Math.floor(i.maxHandoffs ?? 1)));
5205
+ const handoffs = [];
5206
+ let delegatedTask = i.task;
5207
+ let handoffCount = 0;
5208
+ for (; ; ) {
5209
+ const attemptConfig = handoffCount === 0 ? cfg : freshHandoffConfig(cfg, i.role, handoffCount);
5210
+ const subagentId = await dir.spawn(attemptConfig);
5211
+ const taskId = await dir.assign({
5212
+ id: randomUUID4(),
5213
+ description: delegatedTask,
4783
5214
  subagentId
4784
5215
  });
4785
- return {
4786
- ok: false,
4787
- stopReason: "aborted",
4788
- error: "Delegated task aborted \u2014 the run was interrupted.",
5216
+ const result = await awaitDelegateAttempt(
5217
+ dir,
4789
5218
  subagentId,
4790
5219
  taskId,
4791
- partial: partial2
4792
- };
4793
- }
4794
- if ("__timeout" in result) {
4795
- const partial2 = await readSubagentPartial(opts, subagentId);
5220
+ timeoutMs,
5221
+ abortSignal
5222
+ );
5223
+ if ("__aborted" in result) {
5224
+ try {
5225
+ await dir.terminate(subagentId);
5226
+ } catch {
5227
+ }
5228
+ const partial2 = await readSubagentPartial(opts, subagentId);
5229
+ opts.events?.emit("delegate.completed", {
5230
+ sessionId,
5231
+ target,
5232
+ task: i.task,
5233
+ ok: false,
5234
+ status: "aborted",
5235
+ summary: `[${target}] aborted \u2014 the run was interrupted`,
5236
+ durationMs: 0,
5237
+ iterations: partial2?.events ?? 0,
5238
+ toolCalls: partial2?.toolUsesObserved ?? 0,
5239
+ subagentId
5240
+ });
5241
+ return {
5242
+ ok: false,
5243
+ stopReason: "aborted",
5244
+ error: "Delegated task aborted \u2014 the run was interrupted.",
5245
+ subagentId,
5246
+ taskId,
5247
+ partial: partial2,
5248
+ ...handoffs.length > 0 ? { handoffs } : {}
5249
+ };
5250
+ }
5251
+ if ("__timeout" in result) {
5252
+ try {
5253
+ await dir.terminate(subagentId);
5254
+ } catch {
5255
+ }
5256
+ const partial2 = await readSubagentPartial(opts, subagentId);
5257
+ opts.events?.emit("delegate.completed", {
5258
+ sessionId,
5259
+ target,
5260
+ task: i.task,
5261
+ ok: false,
5262
+ status: "host_timeout",
5263
+ summary: `[${target}] timed out \u2014 no progress within ${Math.round(timeoutMs / 1e3)}s`,
5264
+ durationMs: timeoutMs,
5265
+ iterations: partial2?.events ?? 0,
5266
+ toolCalls: partial2?.toolUsesObserved ?? 0,
5267
+ subagentId
5268
+ });
5269
+ return {
5270
+ ok: false,
5271
+ stopReason: "host_timeout",
5272
+ error: `Subagent timed out: it did not finish or report progress within ${timeoutMs}ms.`,
5273
+ hint: "Raise timeoutMs for unusually long single operations, or split the remaining work.",
5274
+ subagentId,
5275
+ taskId,
5276
+ partial: partial2,
5277
+ ...handoffs.length > 0 ? { handoffs } : {}
5278
+ };
5279
+ }
5280
+ if ("__emptyResult" in result) {
5281
+ const partial2 = await readSubagentPartial(opts, subagentId);
5282
+ opts.events?.emit("delegate.completed", {
5283
+ sessionId,
5284
+ target,
5285
+ task: i.task,
5286
+ ok: false,
5287
+ status: "empty_result",
5288
+ summary: `[${target}] completed without a task result`,
5289
+ durationMs: 0,
5290
+ iterations: partial2?.events ?? 0,
5291
+ toolCalls: partial2?.toolUsesObserved ?? 0,
5292
+ subagentId
5293
+ });
5294
+ return {
5295
+ ok: false,
5296
+ stopReason: "error",
5297
+ error: "Director returned no task result for the delegated task.",
5298
+ hint: "Check fleet state, then retry or reassign the task.",
5299
+ subagentId,
5300
+ taskId,
5301
+ partial: partial2,
5302
+ ...handoffs.length > 0 ? { handoffs } : {}
5303
+ };
5304
+ }
5305
+ const partial = result.status === "success" ? void 0 : result.partial ? {
5306
+ lastAssistantText: result.partial.text,
5307
+ toolUsesObserved: result.toolCalls,
5308
+ events: result.iterations
5309
+ } : await readSubagentPartial(opts, subagentId);
5310
+ const continuation = continuationFor(result, partial, attemptConfig);
5311
+ if (continuation && handoffCount < maxHandoffs) {
5312
+ handoffs.push({
5313
+ fromSubagentId: result.subagentId,
5314
+ fromTaskId: result.taskId,
5315
+ status: result.status,
5316
+ errorKind: result.error?.kind,
5317
+ summary: continuation.summary,
5318
+ remainingWork: continuation.remainingWork
5319
+ });
5320
+ handoffCount += 1;
5321
+ delegatedTask = buildHandoffTask(i.task, continuation, handoffCount, maxHandoffs);
5322
+ continue;
5323
+ }
5324
+ const incomplete = result.report?.completion === "partial";
5325
+ const baseStopReason = incomplete ? "handoff_limit" : result.status === "success" ? "end_turn" : result.status === "timeout" ? "subagent_timeout" : result.status === "stopped" ? "aborted" : "budget_exhausted";
5326
+ const errorKind = result.error?.kind;
5327
+ const retryable = result.error?.retryable;
5328
+ const backoffMs = result.error?.backoffMs;
5329
+ const summary = incomplete ? `[${target}] partial checkpoint \u2014 handoff limit reached after ${handoffCount} continuation(s)` : buildDelegateSummary(i.role, result);
5330
+ let costUsd;
5331
+ try {
5332
+ costUsd = dir.snapshot().perSubagent[result.subagentId]?.cost;
5333
+ } catch {
5334
+ costUsd = void 0;
5335
+ }
4796
5336
  opts.events?.emit("delegate.completed", {
4797
5337
  sessionId,
4798
5338
  target,
4799
5339
  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
5340
+ ok: result.status === "success" && !incomplete,
5341
+ status: incomplete ? "partial" : result.status,
5342
+ summary,
5343
+ durationMs: result.durationMs,
5344
+ iterations: result.iterations,
5345
+ toolCalls: result.toolCalls,
5346
+ costUsd,
5347
+ subagentId: result.subagentId
4807
5348
  });
4808
5349
  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
5350
+ ok: result.status === "success" && !incomplete,
5351
+ status: incomplete ? "partial" : result.status,
5352
+ stopReason: baseStopReason,
5353
+ errorKind,
5354
+ retryable,
5355
+ backoffMs,
5356
+ subagentId: result.subagentId,
5357
+ taskId: result.taskId,
5358
+ result: result.result,
5359
+ report: result.report,
5360
+ error: result.error,
5361
+ iterations: result.iterations,
5362
+ toolCalls: result.toolCalls,
5363
+ durationMs: result.durationMs,
5364
+ ...partial ? { partial } : {},
5365
+ ...handoffs.length > 0 ? { handoffs } : {},
5366
+ ...incomplete ? {
5367
+ hint: "A clean partial checkpoint remains. Reinvoke delegate with a larger maxHandoffs or assign report.remaining_work explicitly."
5368
+ } : hintForKind(errorKind, retryable, backoffMs, partial) ? { hint: hintForKind(errorKind, retryable, backoffMs, partial) } : {},
5369
+ summary
4816
5370
  };
4817
5371
  }
4818
- if ("__emptyResult" in result) {
4819
- const partial2 = await readSubagentPartial(opts, subagentId);
4820
- opts.events?.emit("delegate.completed", {
4821
- sessionId,
4822
- target,
4823
- 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
4831
- });
4832
- 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
4840
- };
4841
- }
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
5372
  } catch (err) {
4893
5373
  const message = toErrorMessage(err);
4894
5374
  opts.events?.emit("delegate.completed", {
@@ -4911,10 +5391,103 @@ function createDelegateTool(opts) {
4911
5391
  }
4912
5392
  };
4913
5393
  }
4914
- function instantiateRosterConfig(role, base) {
5394
+ async function awaitDelegateAttempt(director, subagentId, taskId, timeoutMs, abortSignal) {
5395
+ return new Promise((resolve9) => {
5396
+ let settled = false;
5397
+ let timer;
5398
+ let offAbort = () => {
5399
+ };
5400
+ const finish = (value) => {
5401
+ if (settled) return;
5402
+ settled = true;
5403
+ if (timer) clearTimeout(timer);
5404
+ offTool();
5405
+ offIter();
5406
+ offProgress();
5407
+ offAbort();
5408
+ resolve9(value);
5409
+ };
5410
+ const arm = () => {
5411
+ if (timer) clearTimeout(timer);
5412
+ timer = setTimeout(() => finish({ __timeout: true }), timeoutMs);
5413
+ };
5414
+ const bump = (event) => {
5415
+ if (event.subagentId === subagentId) arm();
5416
+ };
5417
+ const offTool = director.fleet.filter("tool.executed", bump);
5418
+ const offIter = director.fleet.filter("iteration.started", bump);
5419
+ const offProgress = director.fleet.filter("tool.progress", bump);
5420
+ if (abortSignal) {
5421
+ const onAbort = () => finish({ __aborted: true });
5422
+ abortSignal.addEventListener("abort", onAbort, { once: true });
5423
+ offAbort = () => abortSignal.removeEventListener("abort", onAbort);
5424
+ if (abortSignal.aborted) onAbort();
5425
+ }
5426
+ arm();
5427
+ director.awaitTasks([taskId]).then((results) => finish(results[0] ?? { __emptyResult: true })).catch(() => finish({ __timeout: true }));
5428
+ });
5429
+ }
5430
+ function freshHandoffConfig(cfg, role, handoffCount) {
5431
+ return {
5432
+ ...cfg,
5433
+ id: role ? `${role}-${randomUUID4().slice(0, 8)}` : `${cfg.name.toLowerCase().replace(/[^a-z0-9]+/g, "-") || "subagent"}-handoff-${handoffCount}-${randomUUID4().slice(0, 6)}`
5434
+ };
5435
+ }
5436
+ function continuationFor(result, partial, config) {
5437
+ if (result.report?.completion === "partial" && result.report.remaining_work) {
5438
+ return {
5439
+ summary: result.report.summary,
5440
+ remainingWork: result.report.remaining_work,
5441
+ partialText: typeof result.result === "string" ? result.result : partial?.lastAssistantText
5442
+ };
5443
+ }
5444
+ const budgetKinds = /* @__PURE__ */ new Set([
5445
+ "budget_iterations",
5446
+ "budget_tool_calls",
5447
+ "budget_tokens",
5448
+ "budget_cost",
5449
+ "budget_timeout"
5450
+ ]);
5451
+ const partialText = result.partial?.text ?? partial?.lastAssistantText;
5452
+ const mayHaveIsolatedWrites = config.worktree !== false && config.worktree !== "off" && (!config.tools || config.tools.some(
5453
+ (tool) => ["write", "edit", "replace", "patch", "bash", "exec", "install", "format"].includes(tool)
5454
+ ));
5455
+ if (!mayHaveIsolatedWrites && result.status !== "success" && result.error?.kind && budgetKinds.has(result.error.kind) && partialText) {
5456
+ return {
5457
+ summary: `Prior worker stopped at ${result.error.kind} after ${result.iterations} iterations and ${result.toolCalls} tool calls.`,
5458
+ remainingWork: "Inspect the existing workspace and finish only the work that remains from the original task.",
5459
+ partialText
5460
+ };
5461
+ }
5462
+ return void 0;
5463
+ }
5464
+ function buildHandoffTask(originalTask, continuation, handoffCount, maxHandoffs) {
5465
+ const partial = continuation.partialText?.trim().slice(-6e3);
5466
+ return [
5467
+ `Continue an oversized delegated task as fresh worker ${handoffCount} of ${maxHandoffs}.`,
5468
+ "Do not blindly repeat completed actions. Inspect the current workspace, git diff, tests, and any files named below before changing anything.",
5469
+ 'If the remaining work is still too large, stop at a clean checkpoint and call submit_result with completion="partial" plus concrete remaining_work.',
5470
+ "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.",
5471
+ `Original task:
5472
+ ${originalTask}`,
5473
+ `Prior checkpoint summary:
5474
+ ${continuation.summary}`,
5475
+ `Remaining work:
5476
+ ${continuation.remainingWork}`,
5477
+ partial ? `Prior worker's last useful output:
5478
+ ${partial}` : ""
5479
+ ].filter(Boolean).join("\n\n");
5480
+ }
5481
+ function instantiateRosterConfig(role, base, requestedTimeoutMs, defaultTimeoutMs) {
4915
5482
  const withBudget = applyRosterBudget({ ...base, role });
5483
+ const rosterTimeoutMs = FLEET_ROSTER_BUDGETS[role]?.timeoutMs;
4916
5484
  return {
4917
5485
  ...withBudget,
5486
+ // Without an explicit host wait, apply the role's tuned multi-hour
5487
+ // wall-clock budget. With an explicit wait, leave this unset so the buffer
5488
+ // logic above derives a slightly shorter child budget without clamping the
5489
+ // role defaults used by ordinary calls.
5490
+ timeoutMs: requestedTimeoutMs === void 0 ? rosterTimeoutMs ?? defaultTimeoutMs : void 0,
4918
5491
  // Give each spawn a fresh id so parallel or repeated delegates
4919
5492
  // can use the same role safely.
4920
5493
  id: `${role}-${randomUUID4().slice(0, 8)}`
@@ -5100,13 +5673,13 @@ function makeDependencyWatcherConfig(opts) {
5100
5673
  const globPatterns = patterns.filter((p) => p.includes("*"));
5101
5674
  const plainPatterns = patterns.filter((p) => !p.includes("*"));
5102
5675
  function matchesPattern(filePath) {
5103
- const basename8 = filePath.split("/").pop()?.split("\\").pop() ?? "";
5104
- if (plainPatterns.includes(basename8)) return true;
5676
+ const basename7 = filePath.split("/").pop()?.split("\\").pop() ?? "";
5677
+ if (plainPatterns.includes(basename7)) return true;
5105
5678
  for (const gp of globPatterns) {
5106
5679
  const regex = new RegExp(
5107
5680
  "^" + gp.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"
5108
5681
  );
5109
- if (regex.test(basename8)) return true;
5682
+ if (regex.test(basename7)) return true;
5110
5683
  }
5111
5684
  return false;
5112
5685
  }
@@ -5446,8 +6019,86 @@ var FleetContextOverflowError = class extends Error {
5446
6019
  }
5447
6020
  };
5448
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
+
5449
6100
  // src/utils/instruction-file.ts
5450
- import { readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
6101
+ import { readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
5451
6102
  import * as path6 from "node:path";
5452
6103
  import { fileURLToPath as fileURLToPath2 } from "node:url";
5453
6104
  var textCache = /* @__PURE__ */ new Map();
@@ -5458,7 +6109,7 @@ function readBundledInstructionText(relativePath) {
5458
6109
  let resolved = "";
5459
6110
  for (const root of instructionRootCandidates()) {
5460
6111
  try {
5461
- resolved = readFileSync2(path6.join(root, relativePath), "utf8").trimEnd();
6112
+ resolved = readFileSync3(path6.join(root, relativePath), "utf8").trimEnd();
5462
6113
  break;
5463
6114
  } catch {
5464
6115
  }
@@ -5485,7 +6136,7 @@ function instructionRootCandidates() {
5485
6136
  }
5486
6137
  function isDirectory2(candidate) {
5487
6138
  try {
5488
- return statSync2(candidate).isDirectory();
6139
+ return statSync3(candidate).isDirectory();
5489
6140
  } catch {
5490
6141
  return false;
5491
6142
  }
@@ -8993,13 +9644,12 @@ var Director = class _Director {
8993
9644
  * coordinator from keeping workers alive for tasks that will never arrive.
8994
9645
  */
8995
9646
  workCompleteFlag = false;
8996
- /** Pending /btw notes stashed by the leader agent (see setLeaderBtwNote). */
8997
- _leaderBtwNotes = [];
8998
- /** Active collab sessions tracked by sessionId (see spawnCollab).
8999
- * The tuple holds the session and its Director-registered listener unsubs.
9000
- * Calling the unsubs on cancel/premature-cleanup prevents listener accumulation
9001
- * on CollabSession (EventEmitter) across many spawnCollab() calls. */
9002
- _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;
9003
9653
  /** Prevents large `ask_subagent` answers from bloating the leader's context window. */
9004
9654
  largeAnswerStore;
9005
9655
  constructor(opts) {
@@ -9011,7 +9661,7 @@ var Director = class _Director {
9011
9661
  this.subagentBaseline = opts.subagentBaseline ?? DEFAULT_SUBAGENT_BASELINE;
9012
9662
  this.taskResultNotifier = opts.taskResultNotifier;
9013
9663
  this.subagentIdleTimeoutMs = typeof opts.subagentIdleTimeoutMs === "number" && Number.isFinite(opts.subagentIdleTimeoutMs) && opts.subagentIdleTimeoutMs >= 0 ? opts.subagentIdleTimeoutMs : void 0;
9014
- this.retireSubagentOnTaskComplete = opts.retireSubagentOnTaskComplete ?? false;
9664
+ this.retireSubagentOnTaskComplete = opts.retireSubagentOnTaskComplete ?? true;
9015
9665
  this.sharedScratchpadPath = opts.sharedScratchpadPath ?? null;
9016
9666
  this.maxSpawns = opts.maxSpawns ?? Number.POSITIVE_INFINITY;
9017
9667
  this.maxSpawnDepth = opts.fleetManager?.maxSpawnDepth ?? resolveMaxSpawnDepth(opts.maxSpawnDepth);
@@ -9022,7 +9672,7 @@ var Director = class _Director {
9022
9672
  this.dispatchClassifier = opts.dispatchClassifier;
9023
9673
  this.maxFleetCostUsd = opts.directorBudget?.maxCostUsd ?? Number.POSITIVE_INFINITY;
9024
9674
  this.maxFleetTokens = opts.directorBudget?.maxTokens ?? Number.POSITIVE_INFINITY;
9025
- this.maxBudgetExtensions = opts.maxBudgetExtensions ?? 5;
9675
+ this.maxBudgetExtensions = opts.maxBudgetExtensions ?? 12;
9026
9676
  this.maxLeaderContextLoad = opts.maxLeaderContextLoad ?? 0.85;
9027
9677
  this.maxContext = opts.maxContext ?? 128e3;
9028
9678
  this.modelMatrix = opts.modelMatrix;
@@ -9076,92 +9726,103 @@ var Director = class _Director {
9076
9726
  );
9077
9727
  this.coordinator.setFleetBus(this.fleet);
9078
9728
  this.fleetManager?.setCoordinator(this.coordinator);
9079
- this.taskCompletedListener = (payload) => {
9080
- const r = payload.result;
9081
- const internalTask = this.internalTaskIds.delete(r.taskId);
9082
- if (!internalTask) {
9083
- this.completed.set(r.taskId, r);
9084
- if (this.completed.size > _Director.MAX_COMPLETED) {
9085
- const toDelete = this.completed.size - _Director.MAX_COMPLETED;
9086
- const keys = [...this.completed.keys()].slice(0, toDelete);
9087
- for (const k of keys) this.completed.delete(k);
9088
- }
9089
- }
9090
- const waiter = this.taskWaiters.get(r.taskId);
9091
- if (waiter) {
9092
- waiter.resolve(r);
9093
- this.taskWaiters.delete(r.taskId);
9094
- }
9095
- let anyConsumed = false;
9096
- for (const aw of [...this.anyWaiters]) {
9097
- if (!aw.ids.has(r.taskId)) continue;
9098
- if (aw.timer) clearTimeout(aw.timer);
9099
- this.anyWaiters.delete(aw);
9100
- aw.resolve(r);
9101
- anyConsumed = true;
9102
- }
9103
- const consumedInBand = !!waiter || anyConsumed;
9104
- if (internalTask) return;
9105
- const title = this.taskDescriptions.get(r.taskId) ?? payload.task.description ?? r.taskId;
9106
- if (!consumedInBand && this.taskResultNotifier) {
9107
- const resultText = typeof r.result === "string" ? r.result : r.result !== void 0 ? safeStringify(r.result) : void 0;
9108
- try {
9109
- void Promise.resolve(
9110
- this.taskResultNotifier({
9111
- taskId: r.taskId,
9112
- title,
9113
- status: r.status,
9114
- subagentId: r.subagentId,
9115
- subagentName: this.manifestEntries.get(r.subagentId)?.name,
9116
- resultText,
9117
- errorText: r.error ? `${r.error.kind}: ${r.error.message}` : void 0,
9118
- partialText: r.partial?.text,
9119
- report: r.report,
9120
- iterations: r.iterations,
9121
- toolCalls: r.toolCalls,
9122
- durationMs: r.durationMs
9123
- })
9124
- ).catch(() => {
9125
- });
9126
- } catch {
9127
- }
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 {
9128
9788
  }
9129
- const failed = r.status !== "success";
9130
- const errorString = r.error ? `${r.error.kind}: ${r.error.message}` : void 0;
9131
- this.stateCheckpoint?.recordTaskStatus(r.taskId, {
9132
- status: failed ? r.status : "completed",
9133
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
9134
- iterations: r.iterations,
9135
- toolCalls: r.toolCalls,
9136
- durationMs: r.durationMs,
9137
- error: errorString
9138
- });
9139
- this.stateCheckpoint?.setUsage(this.usage.snapshot());
9140
- void this.appendSessionEvent(
9141
- failed ? {
9142
- type: "task_failed",
9143
- ts: (/* @__PURE__ */ new Date()).toISOString(),
9144
- taskId: r.taskId,
9145
- title,
9146
- error: errorString ?? r.status
9147
- } : {
9148
- type: "task_completed",
9149
- ts: (/* @__PURE__ */ new Date()).toISOString(),
9150
- taskId: r.taskId,
9151
- title
9152
- }
9153
- );
9154
- if (this.fleetManager) {
9155
- void this.fleetManager.flushManifest();
9156
- } else {
9157
- 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
9158
9813
  }
9159
- this.armSubagentIdleRetirement(
9160
- r.subagentId,
9161
- this.retireSubagentOnTaskComplete ? 0 : this.subagentIdleTimeoutMs
9162
- );
9163
- };
9164
- 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() {
9165
9826
  const extendCounts = /* @__PURE__ */ new Map();
9166
9827
  const progressBySubagent = /* @__PURE__ */ new Map();
9167
9828
  const lastTimeoutProgress = /* @__PURE__ */ new Map();
@@ -9271,7 +9932,7 @@ var Director = class _Director {
9271
9932
  payload.deny();
9272
9933
  return;
9273
9934
  }
9274
- if (decision.optionId === "stop" || /\bstop\b/i.test(decision.text)) {
9935
+ if (decision.optionId === "stop") {
9275
9936
  payload.deny();
9276
9937
  return;
9277
9938
  }
@@ -9281,7 +9942,6 @@ var Director = class _Director {
9281
9942
  }
9282
9943
  grantExtension();
9283
9944
  });
9284
- this.largeAnswerStore = new LargeAnswerStore(2e3);
9285
9945
  }
9286
9946
  /**
9287
9947
  * Record a granted budget extension and broadcast it on the FleetBus so
@@ -9339,10 +9999,7 @@ var Director = class _Director {
9339
9999
  * programmatically without going through the slash-command path.
9340
10000
  */
9341
10001
  setLeaderBtwNote(note) {
9342
- const trimmed = note.trim();
9343
- if (!trimmed) return this._leaderBtwNotes.length;
9344
- this._leaderBtwNotes = [...this._leaderBtwNotes, trimmed].slice(-20);
9345
- return this._leaderBtwNotes.length;
10002
+ return this.btwNotes.add(note);
9346
10003
  }
9347
10004
  /**
9348
10005
  * Read and clear all pending /btw notes the leader has stashed.
@@ -9353,16 +10010,14 @@ var Director = class _Director {
9353
10010
  * to cancel the collab session or let it continue.
9354
10011
  */
9355
10012
  getLeaderBtwNotes() {
9356
- const notes = this._leaderBtwNotes;
9357
- this._leaderBtwNotes = [];
9358
- return notes;
10013
+ return this.btwNotes.drain();
9359
10014
  }
9360
10015
  /**
9361
10016
  * Peek at pending /btw notes without consuming them.
9362
10017
  * Useful for UI to show "N pending notes" without clearing them.
9363
10018
  */
9364
10019
  peekLeaderBtwNotes() {
9365
- return [...this._leaderBtwNotes];
10020
+ return this.btwNotes.peek();
9366
10021
  }
9367
10022
  /**
9368
10023
  * Drain (read + clear) all /btw notes in one call.
@@ -9382,18 +10037,7 @@ var Director = class _Director {
9382
10037
  * The CollabDebugReport will reflect 'cancelled' disposition when awaited.
9383
10038
  */
9384
10039
  cancelCollabSession(sessionId, reason = "Director cancelled") {
9385
- const entry = this._activeCollabSessions.get(sessionId);
9386
- if (!entry || entry.session.isCancelled()) return;
9387
- for (const unsub of entry.unsubs) unsub();
9388
- entry.session.cancel(reason);
9389
- for (const [_role, subagentId] of entry.session.getSubagentIds()) {
9390
- this.coordinator.stop(subagentId).catch((err) => {
9391
- this.logger?.debug(`stop subagent ${subagentId} failed (may have already completed)`, {
9392
- subagentId,
9393
- err: err instanceof Error ? err.message : String(err)
9394
- });
9395
- });
9396
- }
10040
+ this.collab.cancel(sessionId, reason);
9397
10041
  }
9398
10042
  /**
9399
10043
  * Subscribe a callback to be notified whenever a collab session raises
@@ -9402,9 +10046,7 @@ var Director = class _Director {
9402
10046
  * Returns an unsubscribe function.
9403
10047
  */
9404
10048
  onCollabAlert(handler) {
9405
- return this.fleet.filter("collab.warning", (e) => {
9406
- handler(e.payload);
9407
- });
10049
+ return this.collab.onAlert(handler);
9408
10050
  }
9409
10051
  /**
9410
10052
  * Returns all active (not yet settled) collab session ids.
@@ -9412,7 +10054,7 @@ var Director = class _Director {
9412
10054
  * the host to know what can be cancelled.
9413
10055
  */
9414
10056
  activeCollabSessions() {
9415
- return Array.from(this._activeCollabSessions.keys());
10057
+ return this.collab.activeSessionIds();
9416
10058
  }
9417
10059
  /** Best-effort session-writer append. Swallows failures — the director
9418
10060
  * must not break a fleet run because the session JSONL handle closed. */
@@ -9478,6 +10120,77 @@ var Director = class _Director {
9478
10120
  );
9479
10121
  }
9480
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) {
9481
10194
  if (!config.model && this.modelMatrix) {
9482
10195
  const matrix = typeof this.modelMatrix === "function" ? this.modelMatrix() : this.modelMatrix;
9483
10196
  const resolution = resolveModelMatrixResolution(matrix, config.role);
@@ -9488,6 +10201,8 @@ var Director = class _Director {
9488
10201
  if (entry.modelRuntime) config.modelRuntime = entry.modelRuntime;
9489
10202
  }
9490
10203
  }
10204
+ }
10205
+ enforceSpawnCaps(config) {
9491
10206
  if (this.fleetManager) {
9492
10207
  const rejection = this.fleetManager.canSpawn(config);
9493
10208
  if (rejection) {
@@ -9530,7 +10245,8 @@ var Director = class _Director {
9530
10245
  }
9531
10246
  }
9532
10247
  }
9533
- let result;
10248
+ }
10249
+ assignSpawnNickname(config) {
9534
10250
  const needsNickname = config.name === config.role || !config.name || config.name === "subagent" || config.name === "adhoc";
9535
10251
  if (needsNickname) {
9536
10252
  const role = config.role ?? "subagent";
@@ -9542,6 +10258,8 @@ var Director = class _Director {
9542
10258
  this._usedNicknames.add(key);
9543
10259
  }
9544
10260
  }
10261
+ }
10262
+ applySpawnLineage(config) {
9545
10263
  const budget = this.fleetManager ? this.fleetManager.budgetSnapshot() : (() => {
9546
10264
  const total = this.usage.snapshot().total;
9547
10265
  const usedTokens = (total?.input ?? 0) + (total?.output ?? 0);
@@ -9570,70 +10288,6 @@ var Director = class _Director {
9570
10288
  ...Number.isFinite(budget.remainingCostUsd) ? { remainingCostUsd: budget.remainingCostUsd } : {}
9571
10289
  }
9572
10290
  };
9573
- result = await this.coordinator.spawn(config);
9574
- if (this.fleetManager) {
9575
- this.fleetManager.recordSpawn(result.subagentId, config, priceLookup);
9576
- } else {
9577
- this.spawnCount += 1;
9578
- this.subagentMeta.set(result.subagentId, {
9579
- provider: config.provider,
9580
- model: config.model
9581
- });
9582
- if (priceLookup && config.provider && config.model) {
9583
- this.priceLookups.set(`${config.provider}/${config.model}`, priceLookup);
9584
- }
9585
- }
9586
- const subagentBridge = new InMemoryAgentBridge(
9587
- { agentId: result.subagentId, coordinatorId: this.id },
9588
- this.transport
9589
- );
9590
- this.coordinator.setSubagentBridge(result.subagentId, subagentBridge);
9591
- this.subagentBridges.set(result.subagentId, subagentBridge);
9592
- this.fleet.emit({
9593
- subagentId: result.subagentId,
9594
- ts: Date.now(),
9595
- type: "subagent.spawned",
9596
- payload: {
9597
- subagentId: result.subagentId,
9598
- taskId: "",
9599
- // taskId will be set when assign() is called
9600
- name: config.name,
9601
- role: config.role,
9602
- provider: config.provider,
9603
- model: config.model
9604
- }
9605
- });
9606
- if (!this.fleetManager) {
9607
- this.manifestEntries.set(result.subagentId, {
9608
- subagentId: result.subagentId,
9609
- name: config.name,
9610
- role: config.role,
9611
- provider: config.provider,
9612
- model: config.model,
9613
- taskIds: []
9614
- });
9615
- const spawnedAt = (/* @__PURE__ */ new Date()).toISOString();
9616
- this.stateCheckpoint?.recordSpawn(
9617
- {
9618
- id: result.subagentId,
9619
- name: config.name,
9620
- role: config.role,
9621
- provider: config.provider,
9622
- model: config.model,
9623
- spawnedAt
9624
- },
9625
- this.spawnCount
9626
- );
9627
- void this.appendSessionEvent({
9628
- type: "agent_spawned",
9629
- ts: spawnedAt,
9630
- agentId: result.subagentId,
9631
- role: config.role ?? config.name
9632
- });
9633
- this.scheduleManifest();
9634
- }
9635
- this.armSubagentIdleRetirement(result.subagentId, this.subagentIdleTimeoutMs);
9636
- return result.subagentId;
9637
10291
  }
9638
10292
  /**
9639
10293
  * Synchronously ask a subagent something via the bridge. Sends a
@@ -9758,6 +10412,23 @@ var Director = class _Director {
9758
10412
  await atomicWrite(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
9759
10413
  return this.manifestPath;
9760
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
+ }
9761
10432
  /**
9762
10433
  * Tear down the director: stop every subagent, close every bridge
9763
10434
  * endpoint, and (when configured) write the final manifest. Idempotent
@@ -9769,8 +10440,17 @@ var Director = class _Director {
9769
10440
  this.coordinator.off("task.completed", this.taskCompletedListener);
9770
10441
  this.taskCompletedListener = null;
9771
10442
  }
9772
- for (const aw of this.anyWaiters) {
10443
+ for (const aw of [...this.anyWaiters]) {
9773
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
+ });
9774
10454
  }
9775
10455
  this.anyWaiters.clear();
9776
10456
  if (this.toolExecFilter) {
@@ -9784,6 +10464,17 @@ var Director = class _Director {
9784
10464
  for (const timer of this.subagentIdleTimers.values()) clearTimeout(timer);
9785
10465
  this.subagentIdleTimers.clear();
9786
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();
9787
10478
  for (const b of this.subagentBridges.values()) {
9788
10479
  await b.stop().catch((err) => this.logShutdownError("subagent_bridge_stop", err));
9789
10480
  }
@@ -9791,6 +10482,7 @@ var Director = class _Director {
9791
10482
  await this.bridge.stop().catch((err) => this.logShutdownError("director_bridge_stop", err));
9792
10483
  if (this.fleetManager) {
9793
10484
  await this.fleetManager.flushManifest().catch((err) => this.logShutdownError("fleet_manifest_flush", err));
10485
+ await this.manifestWriteChain.catch(() => void 0);
9794
10486
  } else if (this.manifestPath) {
9795
10487
  await this.writeManifest().catch((err) => this.logShutdownError("manifest_write", err));
9796
10488
  }
@@ -9799,6 +10491,7 @@ var Director = class _Director {
9799
10491
  await this.stateCheckpoint.flush().catch((err) => this.logShutdownError("state_checkpoint_flush", err));
9800
10492
  await this.stateCheckpoint.releaseLock().catch((err) => this.logShutdownError("state_checkpoint_lock_release", err));
9801
10493
  }
10494
+ this.largeAnswerStore.clear();
9802
10495
  }
9803
10496
  /**
9804
10497
  * Funnel for shutdown-phase errors. We can't throw — `shutdown()` is
@@ -10000,9 +10693,18 @@ var Director = class _Director {
10000
10693
  }
10001
10694
  async terminate(subagentId) {
10002
10695
  await this.coordinator.stop(subagentId);
10696
+ void this.remove(subagentId).catch(
10697
+ (err) => this.logShutdownError("terminate_remove", err)
10698
+ );
10003
10699
  }
10004
10700
  async terminateAll() {
10701
+ const ids = this.status().subagents.map((s) => s.id);
10005
10702
  await this.coordinator.stopAll();
10703
+ for (const id of ids) {
10704
+ void this.remove(id).catch(
10705
+ (err) => this.logShutdownError("terminate_all_remove", err)
10706
+ );
10707
+ }
10006
10708
  }
10007
10709
  async remove(subagentId) {
10008
10710
  this.clearSubagentIdleRetirement(subagentId);
@@ -10042,7 +10744,7 @@ var Director = class _Director {
10042
10744
  const timer = setTimeout(() => {
10043
10745
  this.subagentIdleTimers.delete(subagentId);
10044
10746
  const entry = this.coordinator.getStatus().subagents.find((a) => a.id === subagentId);
10045
- if (!entry || entry.status !== "idle") return;
10747
+ if (entry?.status !== "idle") return;
10046
10748
  if (this.coordinator.listPendingTasks().some((task) => task.subagentId === subagentId)) {
10047
10749
  this.armSubagentIdleRetirement(subagentId, this.subagentIdleTimeoutMs);
10048
10750
  return;
@@ -10246,22 +10948,7 @@ var Director = class _Director {
10246
10948
  * three agents complete or the session times out.
10247
10949
  */
10248
10950
  async spawnCollab(options) {
10249
- const session = new CollabSession(this, this.fleet, {
10250
- ...options,
10251
- onBudgetWarning: (alert) => {
10252
- return options.onBudgetWarning?.(alert) ?? "ignore";
10253
- }
10254
- });
10255
- const doneHandler = () => this._activeCollabSessions.delete(session.sessionId);
10256
- const errorHandler = () => this._activeCollabSessions.delete(session.sessionId);
10257
- session.on("session.done", doneHandler);
10258
- session.on("session.error", errorHandler);
10259
- const unsubs = [
10260
- () => session.off("session.done", doneHandler),
10261
- () => session.off("session.error", errorHandler)
10262
- ];
10263
- this._activeCollabSessions.set(session.sessionId, { session, unsubs });
10264
- return session.start();
10951
+ return this.collab.spawn(options);
10265
10952
  }
10266
10953
  /**
10267
10954
  * Resume from a prior checkpoint snapshot (loaded via
@@ -11634,6 +12321,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
11634
12321
  projectRoot;
11635
12322
  checkpointCas;
11636
12323
  isSessionInUse;
12324
+ logger;
11637
12325
  /**
11638
12326
  * In-memory cache for load() results, keyed by session ID. The cache is
11639
12327
  * invalidated when the file's mtimeMs or size changes (indicating the
@@ -11659,6 +12347,18 @@ var DefaultSessionStore = class _DefaultSessionStore {
11659
12347
  this.events = opts.events;
11660
12348
  this.secretScrubber = opts.secretScrubber;
11661
12349
  this.isSessionInUse = opts.isSessionInUse;
12350
+ this.logger = opts.logger;
12351
+ }
12352
+ /**
12353
+ * Emit a structured warning. Uses the configured Logger when available;
12354
+ * falls back to console.warn(JSON) so warnings are never silently dropped.
12355
+ */
12356
+ logWarn(msg, ctx) {
12357
+ if (this.logger) {
12358
+ this.logger.warn(msg, ctx);
12359
+ } else {
12360
+ console.warn(JSON.stringify({ ...ctx, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
12361
+ }
11662
12362
  }
11663
12363
  /**
11664
12364
  * Clear the load() cache. Useful for testing or when the caller knows
@@ -11760,12 +12460,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
11760
12460
  this.emitWrite(id, file, "create", "success", Date.now() - t0);
11761
12461
  return writer;
11762
12462
  } 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
- })));
12463
+ await handle.close().catch((e) => this.logWarn(
12464
+ "Session handle close failed",
12465
+ { event: "session_store.handle_close_failed", message: e instanceof Error ? e.message : String(e) }
12466
+ ));
11769
12467
  this.emitError(id, file, "create", toErrorMessage(err), true);
11770
12468
  throw err;
11771
12469
  }
@@ -11896,12 +12594,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
11896
12594
  this.emitWrite(id, file, "resume", "success", Date.now() - t0);
11897
12595
  return { writer, data: resumedData };
11898
12596
  } 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
- })));
12597
+ await handle.close().catch((e) => this.logWarn(
12598
+ "Session handle close failed",
12599
+ { event: "session_store.handle_close_failed", message: e instanceof Error ? e.message : String(e) }
12600
+ ));
11905
12601
  this.emitError(id, file, "resume", toErrorMessage(err), true);
11906
12602
  throw err;
11907
12603
  }
@@ -12522,13 +13218,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
12522
13218
  await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
12523
13219
  const msg = toErrorMessage(err);
12524
13220
  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
- }));
13221
+ this.logWarn(
13222
+ "Session manifest write failed",
13223
+ { event: "session_store.manifest_write_failed", sessionId: id, message: msg }
13224
+ );
12532
13225
  });
12533
13226
  outcome = "failure";
12534
13227
  errorMsg = "summary fallback \xE2\u20AC\u201D manifest rebuilt";
@@ -12938,6 +13631,16 @@ var FleetManager = class {
12938
13631
  manifestTimer = null;
12939
13632
  manifestWriteChain = Promise.resolve();
12940
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;
12941
13644
  manifestDebounceMs;
12942
13645
  /** Fleet-wide cost cap. Infinity = no cap. Distinct from SubagentBudget limits,
12943
13646
  * which track per-subagent spend — this field caps the entire fleet total. */
@@ -13119,6 +13822,7 @@ var FleetManager = class {
13119
13822
  * @param priceLookup Optional per-subagent pricing data
13120
13823
  */
13121
13824
  recordSpawn(subagentId, config, priceLookup) {
13825
+ this.closing = false;
13122
13826
  this.spawnCount += 1;
13123
13827
  this.subagentMeta.set(subagentId, {
13124
13828
  provider: config.provider,
@@ -13156,6 +13860,7 @@ var FleetManager = class {
13156
13860
  }
13157
13861
  async writeManifest() {
13158
13862
  if (!this.manifestPath) return null;
13863
+ if (this.closing) return this.manifestPath;
13159
13864
  this.clearManifestTimer();
13160
13865
  const write = this.manifestWriteChain.catch(() => void 0).then(() => this.writeManifestNow());
13161
13866
  this.manifestWriteChain = write.catch(() => void 0);
@@ -13204,7 +13909,7 @@ var FleetManager = class {
13204
13909
  * When `manifestDebounceMs` is 0, writes are synchronous (no debounce).
13205
13910
  */
13206
13911
  scheduleManifest() {
13207
- if (this.disposed) return;
13912
+ if (this.disposed || this.closing) return;
13208
13913
  if (!this.manifestPath) return;
13209
13914
  if (this.manifestDebounceMs === 0) {
13210
13915
  void this.writeManifest().catch((err) => {
@@ -13235,6 +13940,22 @@ var FleetManager = class {
13235
13940
  process.emitWarning(`FleetManager manifest write failed: ${detail}`, "FleetManagerWarning");
13236
13941
  });
13237
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
+ }
13238
13959
  clearManifestTimer() {
13239
13960
  if (!this.manifestTimer) return;
13240
13961
  clearTimeout(this.manifestTimer);
@@ -13320,6 +14041,184 @@ import { randomUUID as randomUUID10 } from "node:crypto";
13320
14041
  import * as fsp11 from "node:fs/promises";
13321
14042
  import * as path14 from "node:path";
13322
14043
 
14044
+ // src/coordination/mailbox-constants.ts
14045
+ var AGENT_STALE_MS = 6e4;
14046
+ var CLIENT_STALE_MS = 6e4;
14047
+ var HEARTBEAT_THROTTLE_MS = 5e3;
14048
+ var REGISTRY_CACHE_TTL_MS = 2e3;
14049
+ var LINE_SEPARATOR = "\n";
14050
+ var MESSAGE_CACHE_MAX_ENTRIES = 1e4;
14051
+ var AUTO_COMPACT_INTERVAL_MS = 3e5;
14052
+ var AUTO_COMPACT_READ_MAX_AGE_MS = 6e5;
14053
+ var AUTO_COMPACT_DEFAULT_TTL_MS = 864e5;
14054
+
14055
+ // src/coordination/mailbox-message-codec.ts
14056
+ var MESSAGE_TYPES = /* @__PURE__ */ new Set([
14057
+ "note",
14058
+ "ask",
14059
+ "assign",
14060
+ "steer",
14061
+ "btw",
14062
+ "broadcast",
14063
+ "status",
14064
+ "result",
14065
+ "review",
14066
+ "control"
14067
+ ]);
14068
+ var PRIORITIES = /* @__PURE__ */ new Set(["low", "normal", "high"]);
14069
+ var TASK_STATUSES = /* @__PURE__ */ new Set([
14070
+ "pending",
14071
+ "in_progress",
14072
+ "completed",
14073
+ "failed",
14074
+ "idle",
14075
+ "running",
14076
+ "streaming",
14077
+ "waiting_user",
14078
+ "error",
14079
+ "offline",
14080
+ "busy"
14081
+ ]);
14082
+ function isRecord3(value) {
14083
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14084
+ }
14085
+ function requiredString(record, key) {
14086
+ const value = record[key];
14087
+ if (typeof value !== "string") {
14088
+ throw new TypeError(`mailbox message field "${key}" must be a string`);
14089
+ }
14090
+ return value;
14091
+ }
14092
+ function optionalString2(record, key) {
14093
+ const value = record[key];
14094
+ if (value === void 0) return {};
14095
+ if (typeof value !== "string") {
14096
+ throw new TypeError(`mailbox message field "${key}" must be a string when present`);
14097
+ }
14098
+ return { [key]: value };
14099
+ }
14100
+ function parseMessageType(value) {
14101
+ if (value === "info") return "note";
14102
+ if (value === "task") return "assign";
14103
+ if (typeof value === "string" && MESSAGE_TYPES.has(value)) {
14104
+ return value;
14105
+ }
14106
+ throw new TypeError('mailbox message field "type" is invalid');
14107
+ }
14108
+ function normalizeMailboxMessageType(value) {
14109
+ return parseMessageType(value);
14110
+ }
14111
+ function parsePriority(value) {
14112
+ if (typeof value === "string" && PRIORITIES.has(value)) {
14113
+ return value;
14114
+ }
14115
+ if (typeof value === "string") return "normal";
14116
+ throw new TypeError('mailbox message field "priority" must be a string');
14117
+ }
14118
+ function parseReadReceipts(record, to) {
14119
+ const value = record["readBy"];
14120
+ if (value === void 0) {
14121
+ const legacyReadAt = record["readAt"];
14122
+ return record["read"] === true && typeof legacyReadAt === "string" ? { [to || "unknown"]: legacyReadAt } : {};
14123
+ }
14124
+ if (!isRecord3(value)) {
14125
+ throw new TypeError('mailbox message field "readBy" must be an object');
14126
+ }
14127
+ const receipts = {};
14128
+ for (const [agentId, timestamp] of Object.entries(value)) {
14129
+ if (typeof timestamp !== "string") {
14130
+ throw new TypeError("mailbox message read receipt timestamps must be strings");
14131
+ }
14132
+ receipts[agentId] = timestamp;
14133
+ }
14134
+ return receipts;
14135
+ }
14136
+ function parseTaskContext(value) {
14137
+ if (value === void 0) return void 0;
14138
+ if (!isRecord3(value)) {
14139
+ throw new TypeError('mailbox message field "taskContext" must be an object');
14140
+ }
14141
+ const status = value["status"];
14142
+ if (status !== void 0 && (typeof status !== "string" || !TASK_STATUSES.has(status))) {
14143
+ throw new TypeError("mailbox message taskContext status is invalid");
14144
+ }
14145
+ return {
14146
+ ...optionalString2(value, "agentRole"),
14147
+ ...optionalString2(value, "agentName"),
14148
+ ...optionalString2(value, "taskId"),
14149
+ ...status === void 0 ? {} : { status }
14150
+ };
14151
+ }
14152
+ function parseMailboxMessage(value) {
14153
+ if (!isRecord3(value)) throw new TypeError("mailbox message must be an object");
14154
+ const to = value["to"] === void 0 ? "" : requiredString(value, "to");
14155
+ const completed = value["completed"];
14156
+ if (typeof completed !== "boolean") {
14157
+ throw new TypeError('mailbox message field "completed" must be a boolean');
14158
+ }
14159
+ const taskContext = parseTaskContext(value["taskContext"]);
14160
+ return {
14161
+ id: requiredString(value, "id"),
14162
+ from: requiredString(value, "from"),
14163
+ to,
14164
+ type: parseMessageType(value["type"]),
14165
+ subject: requiredString(value, "subject"),
14166
+ body: requiredString(value, "body"),
14167
+ priority: parsePriority(value["priority"]),
14168
+ readBy: parseReadReceipts(value, to),
14169
+ completed,
14170
+ timestamp: requiredString(value, "timestamp"),
14171
+ ...optionalString2(value, "completedBy"),
14172
+ ...optionalString2(value, "outcome"),
14173
+ ...optionalString2(value, "completedAt"),
14174
+ ...optionalString2(value, "deletedAt"),
14175
+ ...optionalString2(value, "deletedBy"),
14176
+ ...optionalString2(value, "replyTo"),
14177
+ ...optionalString2(value, "senderSessionId"),
14178
+ ...optionalString2(value, "expiresAt"),
14179
+ ...taskContext === void 0 ? {} : { taskContext }
14180
+ };
14181
+ }
14182
+ function parseMailboxMessageLine(line) {
14183
+ return parseMailboxMessage(JSON.parse(line));
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
+ }
14221
+
13323
14222
  // src/coordination/mailbox-types.ts
13324
14223
  function normalizeRecipient(to) {
13325
14224
  return to.trim().toLowerCase() === "all" ? "*" : to.trim();
@@ -13328,13 +14227,6 @@ function normalizeRecipient(to) {
13328
14227
  // src/coordination/global-mailbox.ts
13329
14228
  var MAILBOX_FILE = "_mailbox.jsonl";
13330
14229
  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
14230
  function resolveProjectDir(projectRoot, globalRoot) {
13339
14231
  return path14.join(globalRoot, "projects", projectSlug(projectRoot));
13340
14232
  }
@@ -13349,6 +14241,12 @@ var GlobalMailbox = class {
13349
14241
  _events;
13350
14242
  /** Optional HQ publisher for cross-project command-center telemetry. */
13351
14243
  _hqPublisher;
14244
+ /**
14245
+ * Optional SSE event emitter for real-time push to HTTP bridge clients.
14246
+ * When present, `send`/`ack`/`softDelete`/`restore` emit events that
14247
+ * connected SSE subscribers receive instantly.
14248
+ */
14249
+ eventEmitter;
13352
14250
  /**
13353
14251
  * Local cache of the agent registry to avoid re-reading on every call.
13354
14252
  * Time-bounded: the registry file is shared ACROSS PROCESSES (that's the
@@ -13391,20 +14289,47 @@ var GlobalMailbox = class {
13391
14289
  * appended message). The chain runs each read to completion before
13392
14290
  * the next starts, in issue order — readers are read-only and never
13393
14291
  * conflict with each other on content, only on the cache mutation
13394
- * that follows the read. Pattern mirrors `DefaultMemoryStore.runSerialized`.
14292
+ * that follows the read. Pattern mirrors a serialized mutation chain.
13395
14293
  */
13396
14294
  _readChain = Promise.resolve([]);
14295
+ /**
14296
+ * Recipient → Set of indices into `_messageCache`. Maintained alongside
14297
+ * the cache so `query({ to })` and `unreadCount(agentId)` can skip the
14298
+ * O(N) full scan and iterate only the messages addressed to the target
14299
+ * recipient (plus broadcasts `*`).
14300
+ *
14301
+ * The index is rebuilt whenever the cache array is replaced
14302
+ * (`_setMessageCache`), extended when new messages are pushed
14303
+ * (`_pushToCache`), and cleared on `close()`.
14304
+ * It is `null` when the cache itself is null (> MESSAGE_CACHE_MAX_ENTRIES
14305
+ * or never populated).
14306
+ *
14307
+ * Safety: array indices are valid only as long as the cache array
14308
+ * reference hasn't changed. Since we rebuild the index in the same
14309
+ * synchronous step that replaces the cache, a reader that calls
14310
+ * `_readMessagesCached()` either gets the old array + old index, or
14311
+ * the new array + new index — never a mismatch.
14312
+ */
14313
+ _recipientIndex = null;
14314
+ /**
14315
+ * Sender → Set of indices into `_messageCache`. Same lifecycle and
14316
+ * safety properties as `_recipientIndex`, but keyed on `msg.from`
14317
+ * so `query({ from })` can skip the full scan.
14318
+ */
14319
+ _senderIndex = null;
13397
14320
  /**
13398
14321
  * @param projectDir — `~/.wrongstack/projects/<slug>/`
13399
14322
  * @param events — optional EventBus for real-time TUI/WebUI notifications
13400
14323
  * @param hqPublisher — optional HQ publisher, or getter, for cross-project telemetry
14324
+ * @param eventEmitter — optional SSE event emitter for HTTP bridge push
13401
14325
  */
13402
- constructor(projectDir, events, hqPublisher) {
14326
+ constructor(projectDir, events, hqPublisher, eventEmitter) {
13403
14327
  this.messagePath = path14.join(projectDir, MAILBOX_FILE);
13404
14328
  this.registryPath = path14.join(projectDir, "_mailbox.registry.json");
13405
14329
  this.clientRegistryPath = path14.join(projectDir, CLIENT_REGISTRY_FILE);
13406
14330
  this._events = events;
13407
14331
  this._hqPublisher = hqPublisher;
14332
+ this.eventEmitter = eventEmitter;
13408
14333
  }
13409
14334
  get hqMailboxId() {
13410
14335
  return `${path14.basename(path14.dirname(this.messagePath))}:mailbox`;
@@ -13433,7 +14358,7 @@ var GlobalMailbox = class {
13433
14358
  // "all" is an accepted spelling of the broadcast address — canonical
13434
14359
  // form on disk is '*' so every query/checker matches it.
13435
14360
  to: normalizeRecipient(input.to),
13436
- type: input.type,
14361
+ type: normalizeMailboxMessageType(input.type),
13437
14362
  subject: input.subject,
13438
14363
  body: input.body,
13439
14364
  priority: input.priority ?? "normal",
@@ -13441,7 +14366,8 @@ var GlobalMailbox = class {
13441
14366
  completed: false,
13442
14367
  timestamp: now,
13443
14368
  replyTo: input.replyTo,
13444
- taskContext: input.taskContext
14369
+ taskContext: input.taskContext,
14370
+ expiresAt: input.ttlMs !== void 0 ? new Date(Date.now() + input.ttlMs).toISOString() : void 0
13445
14371
  };
13446
14372
  const line = JSON.stringify(msg) + LINE_SEPARATOR;
13447
14373
  await fsp11.mkdir(path14.dirname(this.messagePath), { recursive: true });
@@ -13456,21 +14382,49 @@ var GlobalMailbox = class {
13456
14382
  message: msg
13457
14383
  });
13458
14384
  this.publishHqMailboxSnapshot();
14385
+ this._events?.emitCustom("mailbox.message_sent", {
14386
+ messageId: msg.id,
14387
+ from: msg.from,
14388
+ to: msg.to,
14389
+ type: msg.type,
14390
+ subject: msg.subject
14391
+ });
14392
+ this.eventEmitter?.emit({
14393
+ type: "message.sent",
14394
+ messageId: msg.id,
14395
+ from: msg.from,
14396
+ to: msg.to,
14397
+ timestamp: now
14398
+ });
13459
14399
  return msg;
13460
14400
  }
13461
14401
  async query(q) {
14402
+ const queryType = q.type === void 0 ? void 0 : normalizeMailboxMessageType(q.type);
13462
14403
  const all = await this._readMessagesCached();
13463
14404
  const limit = q.limit ?? 50;
13464
14405
  const order = q.minPriority !== void 0 ? { low: 0, normal: 1, high: 2 } : null;
13465
14406
  const minPriorityRank = order && q.minPriority !== void 0 ? order[q.minPriority] : 0;
13466
14407
  const out = [];
13467
- for (let i = 0; i < all.length; i++) {
13468
- const m = all[i];
14408
+ let candidates;
14409
+ if (q.to !== void 0 && this._recipientIndex !== null && q.from === void 0 && q.type === void 0 && q.minPriority === void 0) {
14410
+ const indices = /* @__PURE__ */ new Set();
14411
+ const direct = this._recipientIndex.get(q.to);
14412
+ if (direct !== void 0) for (const i of direct) indices.add(i);
14413
+ const broadcasts = this._recipientIndex.get("*");
14414
+ if (broadcasts !== void 0) for (const i of broadcasts) indices.add(i);
14415
+ candidates = [...indices].sort((a, b) => a - b).map((i) => all[i]);
14416
+ } else if (q.from !== void 0 && q.to === void 0 && this._senderIndex !== null && q.type === void 0 && q.minPriority === void 0) {
14417
+ const indices = this._senderIndex.get(q.from);
14418
+ candidates = indices !== void 0 ? [...indices].sort((a, b) => a - b).map((i) => all[i]) : [];
14419
+ } else {
14420
+ candidates = all;
14421
+ }
14422
+ for (const m of candidates) {
13469
14423
  if (q.to !== void 0 && m.to !== q.to && m.to !== "*") continue;
13470
14424
  if (q.from !== void 0 && m.from !== q.from) continue;
13471
14425
  if (q.unreadBy !== void 0 && q.unreadBy in m.readBy) continue;
13472
14426
  if (q.incompleteOnly && m.completed) continue;
13473
- if (q.type !== void 0 && m.type !== q.type) continue;
14427
+ if (queryType !== void 0 && m.type !== queryType) continue;
13474
14428
  if (order !== null && (order[m.priority] ?? 1) < minPriorityRank) {
13475
14429
  continue;
13476
14430
  }
@@ -13485,42 +14439,100 @@ var GlobalMailbox = class {
13485
14439
  const updated = await this.ackMany({ acks: [input] });
13486
14440
  return updated.length > 0 ? updated[0] : null;
13487
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
+ };
13488
14458
  async ackMany(input) {
13489
14459
  if (input.acks.length === 0) return [];
14460
+ const now = (/* @__PURE__ */ new Date()).toISOString();
14461
+ const ackRecords = [];
13490
14462
  const updated = [];
14463
+ const targetIds = /* @__PURE__ */ new Set();
13491
14464
  const byId = /* @__PURE__ */ new Map();
13492
14465
  for (const a of input.acks) {
13493
14466
  byId.set(a.messageId, a);
14467
+ targetIds.add(a.messageId);
13494
14468
  }
13495
- await withFileLock(this.messagePath, async () => {
13496
- const all = await this._readMessagesFresh();
13497
- const now = (/* @__PURE__ */ new Date()).toISOString();
13498
- let changed = false;
13499
- for (const msg of all) {
13500
- const a = byId.get(msg.id);
13501
- if (!a) continue;
13502
- updated.push(msg);
13503
- if (a.read !== false && !(a.readerId in msg.readBy)) {
13504
- msg.readBy[a.readerId] = now;
13505
- changed = true;
13506
- }
13507
- if (a.completed && !msg.completed) {
13508
- msg.completed = true;
13509
- msg.completedBy = a.readerId;
13510
- msg.completedAt = now;
13511
- changed = true;
13512
- }
13513
- if (a.outcome !== void 0 && msg.outcome !== a.outcome) {
13514
- msg.outcome = a.outcome;
13515
- changed = true;
13516
- }
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;
13517
14483
  }
13518
- if (changed) {
13519
- const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
13520
- 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
+ }
13521
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");
13522
14519
  const { mtimeMs, size } = await this._statMessageFile();
13523
- 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
+ }
13524
14536
  });
13525
14537
  for (const message of updated) {
13526
14538
  this.publishHqMailboxEvent({
@@ -13528,6 +14540,13 @@ var GlobalMailbox = class {
13528
14540
  action: message.completed ? "message.completed" : "message.read",
13529
14541
  message
13530
14542
  });
14543
+ this.eventEmitter?.emit({
14544
+ type: "message.acked",
14545
+ messageId: message.id,
14546
+ from: message.from,
14547
+ to: message.to,
14548
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
14549
+ });
13531
14550
  }
13532
14551
  if (updated.length > 0) this.publishHqMailboxSnapshot();
13533
14552
  return updated;
@@ -13535,68 +14554,108 @@ var GlobalMailbox = class {
13535
14554
  async unreadCount(forAgentId) {
13536
14555
  const all = await this._readMessagesCached();
13537
14556
  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++;
14557
+ if (this._recipientIndex !== null) {
14558
+ const indices = /* @__PURE__ */ new Set();
14559
+ const direct = this._recipientIndex.get(forAgentId);
14560
+ if (direct !== void 0) for (const i of direct) indices.add(i);
14561
+ const broadcasts = this._recipientIndex.get("*");
14562
+ if (broadcasts !== void 0) for (const i of broadcasts) indices.add(i);
14563
+ for (const i of indices) {
14564
+ const m = all[i];
14565
+ if (!(forAgentId in m.readBy) && !m.completed) count++;
14566
+ }
14567
+ } else {
14568
+ for (let i = 0; i < all.length; i++) {
14569
+ const m = all[i];
14570
+ if ((m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed) {
14571
+ count++;
14572
+ }
13542
14573
  }
13543
14574
  }
13544
14575
  return count;
13545
14576
  }
13546
14577
  async softDelete(mailId, by) {
13547
- 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
+ };
13548
14594
  await withFileLock(this.messagePath, async () => {
13549
- const all = await this._readMessagesFresh();
13550
- const now = (/* @__PURE__ */ new Date()).toISOString();
13551
- for (const m of all) {
13552
- if (m.id !== mailId) continue;
13553
- if (m.deletedAt !== void 0) {
13554
- updated = m;
13555
- continue;
13556
- }
13557
- m.deletedAt = now;
13558
- m.deletedBy = by;
13559
- updated = m;
13560
- }
13561
- const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
13562
- await fsp11.writeFile(this.messagePath, serialized, "utf8");
14595
+ await fsp11.appendFile(this.messagePath, serializeAckRecord(ack), "utf8");
13563
14596
  const { mtimeMs, size } = await this._statMessageFile();
13564
- this._setMessageCache(all, mtimeMs, size);
14597
+ this._applyAckToCache(ack);
14598
+ this._messageCacheMtime = mtimeMs;
14599
+ this._messageCacheSize = size;
13565
14600
  });
13566
- if (updated !== null) {
13567
- this.publishHqMailboxEvent({
13568
- mailboxId: this.hqMailboxId,
13569
- action: "message.updated",
13570
- message: updated
13571
- });
13572
- this.publishHqMailboxSnapshot();
13573
- }
13574
- 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;
13575
14618
  }
13576
14619
  async restore(mailId) {
13577
- 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
+ };
13578
14635
  await withFileLock(this.messagePath, async () => {
13579
- const all = await this._readMessagesFresh();
13580
- for (const m of all) {
13581
- if (m.id !== mailId) continue;
13582
- delete m.deletedAt;
13583
- delete m.deletedBy;
13584
- updated = m;
13585
- }
13586
- const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
13587
- await fsp11.writeFile(this.messagePath, serialized, "utf8");
14636
+ await fsp11.appendFile(this.messagePath, serializeAckRecord(ack), "utf8");
13588
14637
  const { mtimeMs, size } = await this._statMessageFile();
13589
- this._setMessageCache(all, mtimeMs, size);
14638
+ this._applyAckToCache(ack);
14639
+ this._messageCacheMtime = mtimeMs;
14640
+ this._messageCacheSize = size;
14641
+ });
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
13590
14649
  });
13591
- if (updated !== null) {
13592
- this.publishHqMailboxEvent({
13593
- mailboxId: this.hqMailboxId,
13594
- action: "message.updated",
13595
- message: updated
13596
- });
13597
- this.publishHqMailboxSnapshot();
13598
- }
13599
- return updated;
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;
13600
14659
  }
13601
14660
  // ── Agent registry ──────────────────────────────────────────────────────
13602
14661
  async registerAgent(input) {
@@ -13727,8 +14786,19 @@ var GlobalMailbox = class {
13727
14786
  }
13728
14787
  async getAgentStatuses() {
13729
14788
  await this._ensureRegistry();
13730
- const registry = await this._readRegistry();
14789
+ let registry = await this._readRegistry();
14790
+ const before = registry.size;
13731
14791
  this._pruneStaleInPlace(registry);
14792
+ if (registry.size < before) {
14793
+ await withFileLock(this.registryPath, async () => {
14794
+ const fresh = await this._readRegistry({ fresh: true });
14795
+ this._pruneStaleInPlace(fresh);
14796
+ this._registryCache = fresh;
14797
+ this._registryCacheAt = Date.now();
14798
+ await this._writeRegistry(fresh);
14799
+ registry = fresh;
14800
+ });
14801
+ }
13732
14802
  const now = Date.now();
13733
14803
  return Array.from(registry.values()).map((a) => ({
13734
14804
  agentId: a.agentId,
@@ -13780,6 +14850,20 @@ var GlobalMailbox = class {
13780
14850
  });
13781
14851
  this.publishHqMailboxSnapshot();
13782
14852
  }
14853
+ async deregisterClient(clientId) {
14854
+ await this._ensureClientRegistry();
14855
+ await withFileLock(this.clientRegistryPath, async () => {
14856
+ const registry = await this._readClientRegistry({ fresh: true });
14857
+ this._pruneStaleClientsInPlace(registry);
14858
+ registry.delete(clientId);
14859
+ this._clientRegistryCache = registry;
14860
+ this._clientRegistryCacheAt = Date.now();
14861
+ await this._writeClientRegistry(registry);
14862
+ });
14863
+ this._lastClientHeartbeat.delete(clientId);
14864
+ this._events?.emitCustom("mailbox.client_deregistered", { clientId });
14865
+ this.publishHqMailboxSnapshot();
14866
+ }
13783
14867
  async clientHeartbeat(input) {
13784
14868
  const last = this._lastClientHeartbeat.get(input.clientId) ?? 0;
13785
14869
  const now = Date.now();
@@ -13808,8 +14892,19 @@ var GlobalMailbox = class {
13808
14892
  }
13809
14893
  async getClientStatuses() {
13810
14894
  await this._ensureClientRegistry();
13811
- const registry = await this._readClientRegistry();
14895
+ let registry = await this._readClientRegistry();
14896
+ const before = registry.size;
13812
14897
  this._pruneStaleClientsInPlace(registry);
14898
+ if (registry.size < before) {
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
+ });
14907
+ }
13813
14908
  const now = Date.now();
13814
14909
  return Array.from(registry.values()).map((c) => ({
13815
14910
  clientId: c.clientId,
@@ -13821,6 +14916,32 @@ var GlobalMailbox = class {
13821
14916
  pid: c.pid
13822
14917
  })).sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt));
13823
14918
  }
14919
+ /**
14920
+ * Explicitly purge stale clients from the registry and write back to disk.
14921
+ * Removes client entries whose lastSeenAt is older than CLIENT_STALE_MS.
14922
+ * Returns the number of entries purged.
14923
+ *
14924
+ * Idempotent — safe to call regularly. The same pruning runs implicitly
14925
+ * inside registerClient, clientHeartbeat, and getClientStatuses, but if
14926
+ * no client registers or heartbeats for a long time, this public method
14927
+ * ensures the file gets cleaned up on demand.
14928
+ */
14929
+ async purgeClients() {
14930
+ await this._ensureClientRegistry();
14931
+ let purged = 0;
14932
+ await withFileLock(this.clientRegistryPath, async () => {
14933
+ const registry = await this._readClientRegistry({ fresh: true });
14934
+ const before = registry.size;
14935
+ this._pruneStaleClientsInPlace(registry);
14936
+ purged = before - registry.size;
14937
+ if (purged > 0) {
14938
+ this._clientRegistryCache = registry;
14939
+ this._clientRegistryCacheAt = Date.now();
14940
+ await this._writeClientRegistry(registry);
14941
+ }
14942
+ });
14943
+ return purged;
14944
+ }
13824
14945
  // ── Lifecycle ───────────────────────────────────────────────────────────
13825
14946
  async close() {
13826
14947
  this._registryCache = null;
@@ -13828,6 +14949,8 @@ var GlobalMailbox = class {
13828
14949
  this._messageCache = null;
13829
14950
  this._messageCacheMtime = -1;
13830
14951
  this._messageCacheSize = -1;
14952
+ this._recipientIndex = null;
14953
+ this._senderIndex = null;
13831
14954
  }
13832
14955
  async clearAll() {
13833
14956
  await withFileLock(this.messagePath, async () => {
@@ -13876,6 +14999,100 @@ var GlobalMailbox = class {
13876
14999
  remaining
13877
15000
  };
13878
15001
  }
15002
+ // ── Auto-compaction ─────────────────────────────────────────────────────
15003
+ _autoCompactTimer = null;
15004
+ async autoCompact(opts) {
15005
+ const readMaxAgeMs = opts?.readMaxAgeMs ?? AUTO_COMPACT_READ_MAX_AGE_MS;
15006
+ const defaultTtlMs = opts?.defaultTtlMs ?? AUTO_COMPACT_DEFAULT_TTL_MS;
15007
+ const completedMaxAgeMs = opts?.completedMaxAgeMs ?? 864e5;
15008
+ const incompleteMaxAgeMs = opts?.incompleteMaxAgeMs ?? 6048e5;
15009
+ let expiredPurged = 0;
15010
+ let readByAllPurged = 0;
15011
+ let completedPurged = 0;
15012
+ let incompletePurged = 0;
15013
+ let remaining = 0;
15014
+ let onlineAgentIds = null;
15015
+ try {
15016
+ const statuses = await this.getAgentStatuses();
15017
+ const online = statuses.filter((s) => s.online);
15018
+ onlineAgentIds = online.length > 0 ? new Set(online.map((s) => s.agentId)) : null;
15019
+ } catch {
15020
+ onlineAgentIds = null;
15021
+ }
15022
+ await withFileLock(this.messagePath, async () => {
15023
+ const all = await this._readMessagesFresh();
15024
+ const now = Date.now();
15025
+ const cutoffReadAge = now - readMaxAgeMs;
15026
+ const cutoffCompleted = now - completedMaxAgeMs;
15027
+ const cutoffIncomplete = now - incompleteMaxAgeMs;
15028
+ const cutoffDefaultTtl = now - defaultTtlMs;
15029
+ const kept = [];
15030
+ for (const msg of all) {
15031
+ const msgTime = new Date(msg.timestamp).getTime();
15032
+ if (msg.expiresAt !== void 0) {
15033
+ if (new Date(msg.expiresAt).getTime() < now) {
15034
+ expiredPurged++;
15035
+ continue;
15036
+ }
15037
+ } else if (msgTime < cutoffDefaultTtl) {
15038
+ expiredPurged++;
15039
+ continue;
15040
+ }
15041
+ if (onlineAgentIds !== null && !msg.completed) {
15042
+ const readByAll = onlineAgentIds.size > 0 && [...onlineAgentIds].every((id) => id in msg.readBy);
15043
+ if (readByAll) {
15044
+ const readTimes = Object.values(msg.readBy).map((t) => new Date(t).getTime());
15045
+ const latestRead = Math.max(...readTimes);
15046
+ if (latestRead < cutoffReadAge) {
15047
+ readByAllPurged++;
15048
+ continue;
15049
+ }
15050
+ }
15051
+ }
15052
+ const completedTime = msg.completedAt ? new Date(msg.completedAt).getTime() : 0;
15053
+ if (msg.completed && completedTime < cutoffCompleted) {
15054
+ completedPurged++;
15055
+ continue;
15056
+ }
15057
+ if (!msg.completed && msgTime < cutoffIncomplete) {
15058
+ incompletePurged++;
15059
+ continue;
15060
+ }
15061
+ kept.push(msg);
15062
+ }
15063
+ remaining = kept.length;
15064
+ if (kept.length < all.length) {
15065
+ const content = kept.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
15066
+ await fsp11.writeFile(this.messagePath, content, "utf8");
15067
+ }
15068
+ const { mtimeMs, size } = await this._statMessageFile();
15069
+ this._setMessageCache(kept, mtimeMs, size);
15070
+ });
15071
+ const totalRemoved = expiredPurged + readByAllPurged + completedPurged + incompletePurged;
15072
+ return {
15073
+ expiredRemoved: expiredPurged,
15074
+ readByAllRemoved: readByAllPurged,
15075
+ stalePurged: completedPurged + incompletePurged,
15076
+ totalRemoved,
15077
+ remaining
15078
+ };
15079
+ }
15080
+ startAutoCompactTimer(opts) {
15081
+ if (this._autoCompactTimer !== null) {
15082
+ clearInterval(this._autoCompactTimer);
15083
+ }
15084
+ const intervalMs = opts?.intervalMs ?? AUTO_COMPACT_INTERVAL_MS;
15085
+ const timer = setInterval(() => {
15086
+ this.autoCompact(opts).catch(() => {
15087
+ });
15088
+ }, intervalMs);
15089
+ timer.unref?.();
15090
+ this._autoCompactTimer = timer;
15091
+ return () => {
15092
+ clearInterval(timer);
15093
+ this._autoCompactTimer = null;
15094
+ };
15095
+ }
13879
15096
  // ── Internal ────────────────────────────────────────────────────────────
13880
15097
  /**
13881
15098
  * Read all messages from the JSONL file. Always reads + parses the file.
@@ -13892,61 +15109,23 @@ var GlobalMailbox = class {
13892
15109
  throw err;
13893
15110
  }
13894
15111
  }
13895
- /**
13896
- * Read only newly-appended bytes from the file and append them to the
13897
- * in-memory cache. This avoids re-reading and re-parsing the entire file
13898
- * when another process appended messages since our last read.
13899
- *
13900
- * Only safe when the file grew in size (messages are append-only). When
13901
- * the file was rewritten (ack/purge changed existing content), callers
13902
- * must fall back to {@link _readMessages}.
13903
- *
13904
- * @returns The (now up-to-date) message cache.
13905
- */
13906
- async _readNewMessagesOnly(fd, oldSize, newSize) {
13907
- const tailLen = newSize - oldSize;
13908
- const buf = Buffer.alloc(tailLen);
13909
- await fd.read(buf, 0, tailLen, oldSize);
13910
- const tail = buf.toString("utf8");
13911
- for (const line of tail.split(LINE_SEPARATOR)) {
13912
- 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"];
13923
- }
13924
- this._messageCache.push(parsed);
13925
- } catch {
13926
- }
13927
- }
13928
- return this._messageCache;
13929
- }
13930
- /** Parse a JSONL string into MailboxMessage[], including migration. */
15112
+ /** Parse a JSONL string into MailboxMessage[], applying ack records. */
13931
15113
  _parseLines(raw) {
13932
15114
  const lines = raw.split(LINE_SEPARATOR).filter((l) => l.trim().length > 0);
13933
15115
  const messages = [];
15116
+ const acks = [];
13934
15117
  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
- }
15118
+ const parsed = parseMailboxLine(line);
15119
+ if (isAckRecord(parsed)) {
15120
+ acks.push(parsed);
15121
+ } else if (parsed !== null) {
13946
15122
  messages.push(parsed);
13947
- } catch {
13948
15123
  }
13949
15124
  }
15125
+ for (const ack of acks) {
15126
+ const target = messages.find((m) => m.id === ack.messageId);
15127
+ if (target) applyAckToMessage(target, ack);
15128
+ }
13950
15129
  return messages;
13951
15130
  }
13952
15131
  /**
@@ -14028,17 +15207,6 @@ var GlobalMailbox = class {
14028
15207
  if (this._messageCache !== null && this._messageCacheMtime === st.mtimeMs && this._messageCacheSize === st.size) {
14029
15208
  return this._messageCache;
14030
15209
  }
14031
- if (this._messageCache !== null && this._messageCacheSize >= 0 && st.size > this._messageCacheSize) {
14032
- const fd = await fsp11.open(this.messagePath, "r");
14033
- try {
14034
- const updated = await this._readNewMessagesOnly(fd, this._messageCacheSize, st.size);
14035
- this._messageCacheMtime = st.mtimeMs;
14036
- this._messageCacheSize = st.size;
14037
- return updated;
14038
- } finally {
14039
- await fd.close();
14040
- }
14041
- }
14042
15210
  const all = await this._readMessages();
14043
15211
  this._setMessageCache(all, st.mtimeMs, st.size);
14044
15212
  return all;
@@ -14070,11 +15238,61 @@ var GlobalMailbox = class {
14070
15238
  this._messageCache = null;
14071
15239
  this._messageCacheMtime = -1;
14072
15240
  this._messageCacheSize = -1;
15241
+ this._recipientIndex = null;
15242
+ this._senderIndex = null;
14073
15243
  return;
14074
15244
  }
14075
15245
  this._messageCache = messages;
14076
15246
  this._messageCacheMtime = mtime;
14077
15247
  this._messageCacheSize = size;
15248
+ this._rebuildRecipientIndex();
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
+ }
15265
+ /**
15266
+ * Build the recipient AND sender indexes from the current `_messageCache`.
15267
+ * Called only from `_setMessageCache` so both indexes are always in sync
15268
+ * with a freshly-replaced cache array.
15269
+ */
15270
+ _rebuildRecipientIndex() {
15271
+ const cache = this._messageCache;
15272
+ if (cache === null) {
15273
+ this._recipientIndex = null;
15274
+ this._senderIndex = null;
15275
+ return;
15276
+ }
15277
+ const recIdx = /* @__PURE__ */ new Map();
15278
+ const sndIdx = /* @__PURE__ */ new Map();
15279
+ for (let i = 0; i < cache.length; i++) {
15280
+ const msg = cache[i];
15281
+ let rSet = recIdx.get(msg.to);
15282
+ if (rSet === void 0) {
15283
+ rSet = /* @__PURE__ */ new Set();
15284
+ recIdx.set(msg.to, rSet);
15285
+ }
15286
+ rSet.add(i);
15287
+ let sSet = sndIdx.get(msg.from);
15288
+ if (sSet === void 0) {
15289
+ sSet = /* @__PURE__ */ new Set();
15290
+ sndIdx.set(msg.from, sSet);
15291
+ }
15292
+ sSet.add(i);
15293
+ }
15294
+ this._recipientIndex = recIdx;
15295
+ this._senderIndex = sndIdx;
14078
15296
  }
14079
15297
  /**
14080
15298
  * Append a single just-sent message to the in-memory cache without
@@ -14101,6 +15319,23 @@ var GlobalMailbox = class {
14101
15319
  return;
14102
15320
  }
14103
15321
  this._messageCache.push(msg);
15322
+ const newIdx = this._messageCache.length - 1;
15323
+ if (this._recipientIndex !== null) {
15324
+ let set = this._recipientIndex.get(msg.to);
15325
+ if (set === void 0) {
15326
+ set = /* @__PURE__ */ new Set();
15327
+ this._recipientIndex.set(msg.to, set);
15328
+ }
15329
+ set.add(newIdx);
15330
+ }
15331
+ if (this._senderIndex !== null) {
15332
+ let set = this._senderIndex.get(msg.from);
15333
+ if (set === void 0) {
15334
+ set = /* @__PURE__ */ new Set();
15335
+ this._senderIndex.set(msg.from, set);
15336
+ }
15337
+ set.add(newIdx);
15338
+ }
14104
15339
  this._messageCacheMtime = mtime;
14105
15340
  this._messageCacheSize = size;
14106
15341
  }
@@ -14132,22 +15367,13 @@ var GlobalMailbox = class {
14132
15367
  }
14133
15368
  }
14134
15369
  _pruneStaleInPlace(registry) {
14135
- const staleCutoff = Date.now() - AGENT_STALE_MS;
14136
- const purgeCutoff = Date.now() - AGENT_PURGE_MS;
14137
- const toDelete = [];
15370
+ const cutoff = Date.now() - AGENT_STALE_MS;
14138
15371
  for (const [id, agent] of registry) {
14139
- const lastSeen = new Date(agent.lastSeenAt).getTime();
14140
- if (lastSeen < purgeCutoff) {
14141
- toDelete.push(id);
14142
- continue;
14143
- }
14144
- if (lastSeen < staleCutoff) {
14145
- agent.status = "idle";
15372
+ const lastSeen = Date.parse(agent.lastSeenAt);
15373
+ if (!Number.isFinite(lastSeen) || lastSeen < cutoff) {
15374
+ registry.delete(id);
14146
15375
  }
14147
15376
  }
14148
- for (const id of toDelete) {
14149
- registry.delete(id);
14150
- }
14151
15377
  }
14152
15378
  async _writeRegistry(registry) {
14153
15379
  const obj = {};
@@ -14188,9 +15414,10 @@ var GlobalMailbox = class {
14188
15414
  }
14189
15415
  _pruneStaleClientsInPlace(registry) {
14190
15416
  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();
15417
+ for (const [id, client] of registry) {
15418
+ const lastSeen = Date.parse(client.lastSeenAt);
15419
+ if (!Number.isFinite(lastSeen) || lastSeen < cutoff) {
15420
+ registry.delete(id);
14194
15421
  }
14195
15422
  }
14196
15423
  }
@@ -14248,7 +15475,7 @@ function makeMailboxTool(opts = {}) {
14248
15475
  description: "Which mailbox operation to perform."
14249
15476
  },
14250
15477
  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." },
15478
+ type: { type: "string", enum: ["note", "ask", "assign", "steer", "btw", "broadcast", "status", "result", "review", "control"], description: "Message type." },
14252
15479
  subject: { type: "string", description: "Short subject line." },
14253
15480
  body: { type: "string", description: "Full message content." },
14254
15481
  priority: { type: "string", enum: ["low", "normal", "high"] },
@@ -14490,7 +15717,7 @@ function makeFleetStatusTool(opts = {}) {
14490
15717
  const resolveMailbox = opts.resolveMailbox ?? ((ctx) => new GlobalMailbox(opts.projectDir ?? defaultResolveProjectDir(ctx), opts.events));
14491
15718
  return {
14492
15719
  name: "fleet_status",
14493
- 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.",
14494
15721
  usageHint: "fleet_status (optionally: onlineOnly=false to include recently-offline agents)",
14495
15722
  category: "Coordination",
14496
15723
  permission: "auto",
@@ -15154,7 +16381,7 @@ function makeMailSendTool(opts = {}) {
15154
16381
  const resolveMailbox = makeResolver(opts);
15155
16382
  return {
15156
16383
  name: "mail_send",
15157
- 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).',
15158
16385
  usageHint: 'mail_send to="<id>" type="review" body="please skim <file>"',
15159
16386
  category: "Coordination",
15160
16387
  permission: "auto",
@@ -15214,7 +16441,7 @@ function makeMailInboxTool(opts = {}) {
15214
16441
  const resolveMailbox = makeResolver(opts);
15215
16442
  return {
15216
16443
  name: "mail_inbox",
15217
- 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.',
15218
16445
  usageHint: 'mail_inbox (optionally: limit=10, markRead=false to peek, completed=true outcome="handled")',
15219
16446
  category: "Coordination",
15220
16447
  permission: "auto",
@@ -15302,14 +16529,18 @@ var DefaultMailbox = class {
15302
16529
  _messageCache = null;
15303
16530
  _messageCacheMtime = -1;
15304
16531
  _messageCacheSize = -1;
16532
+ /** True when the cache is only the newest MESSAGE_CACHE_MAX_ENTRIES records. */
16533
+ _messageCacheTruncated = false;
15305
16534
  /** Primary index: recipient → Set of messages (points into _messageCache). */
15306
16535
  _byTo = /* @__PURE__ */ new Map();
15307
16536
  /** Secondary index: sender → Set of messages (points into _messageCache). */
15308
16537
  _byFrom = /* @__PURE__ */ new Map();
15309
16538
  /** Counts malformed JSONL lines skipped during parsing for observability. */
15310
16539
  _corruptionCount = 0;
15311
- constructor(sessionDir) {
16540
+ logger;
16541
+ constructor(sessionDir, opts) {
15312
16542
  this.filePath = path15.join(sessionDir, MAILBOX_FILE2);
16543
+ this.logger = opts?.logger;
15313
16544
  }
15314
16545
  get mailboxPath() {
15315
16546
  return this.filePath;
@@ -15327,7 +16558,7 @@ var DefaultMailbox = class {
15327
16558
  // "all" is an accepted spelling of the broadcast address — canonical
15328
16559
  // form on disk is '*' so every query/checker matches it.
15329
16560
  to: normalizeRecipient(input.to),
15330
- type: input.type,
16561
+ type: normalizeMailboxMessageType(input.type),
15331
16562
  subject: input.subject,
15332
16563
  body: input.body,
15333
16564
  priority: input.priority ?? "normal",
@@ -15350,12 +16581,14 @@ var DefaultMailbox = class {
15350
16581
  }
15351
16582
  // ── Query ─────────────────────────────────────────────────────────────
15352
16583
  async query(q) {
16584
+ const queryType = q.type === void 0 ? void 0 : normalizeMailboxMessageType(q.type);
15353
16585
  const needFullScan = q.unreadBy !== void 0 || q.since !== void 0;
16586
+ const cached = await this._readAllCached(false);
15354
16587
  let candidates;
16588
+ let candidatesComplete = !this._messageCacheTruncated || cached !== this._messageCache;
15355
16589
  if (needFullScan) {
15356
- candidates = await this._readAllCached();
16590
+ candidates = cached;
15357
16591
  } else {
15358
- await this._readAllCached();
15359
16592
  if (q.to !== void 0) {
15360
16593
  const direct = this._byTo.get(q.to);
15361
16594
  const broadcast = this._byTo.get("*");
@@ -15363,10 +16596,12 @@ var DefaultMailbox = class {
15363
16596
  if (direct) for (const m of direct) combined.set(m.id, m);
15364
16597
  if (broadcast) for (const m of broadcast) combined.set(m.id, m);
15365
16598
  candidates = Array.from(combined.values());
16599
+ candidatesComplete = !this._messageCacheTruncated;
15366
16600
  } else if (q.from !== void 0) {
15367
16601
  candidates = Array.from(this._byFrom.get(q.from) ?? []);
16602
+ candidatesComplete = !this._messageCacheTruncated;
15368
16603
  } else {
15369
- candidates = await this._readAllCached();
16604
+ candidates = cached;
15370
16605
  }
15371
16606
  }
15372
16607
  const limit = q.limit ?? 50;
@@ -15377,27 +16612,37 @@ var DefaultMailbox = class {
15377
16612
  if (q.from !== void 0 && msg.from !== q.from) return false;
15378
16613
  if (q.unreadBy !== void 0 && q.unreadBy in msg.readBy) return false;
15379
16614
  if (q.incompleteOnly && msg.completed) return false;
15380
- if (q.type !== void 0 && msg.type !== q.type) return false;
16615
+ if (queryType !== void 0 && msg.type !== queryType) return false;
15381
16616
  if (order !== null && (order[msg.priority] ?? 1) < minPriorityRank)
15382
16617
  return false;
15383
16618
  if (q.since !== void 0 && msg.timestamp <= q.since) return false;
15384
16619
  return true;
15385
16620
  };
15386
- if (order === null) {
16621
+ const selectNewest = (source) => {
15387
16622
  const out = [];
15388
- for (let i = candidates.length - 1; i >= 0 && out.length < limit; i--) {
15389
- const msg = candidates[i];
16623
+ for (let i = source.length - 1; i >= 0 && out.length < limit; i--) {
16624
+ const msg = source[i];
15390
16625
  if (passes(msg)) out.push(msg);
15391
16626
  }
15392
16627
  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);
16628
+ };
16629
+ if (order === null) {
16630
+ const out = selectNewest(candidates);
16631
+ if (out.length >= limit || candidatesComplete) return out;
16632
+ return selectNewest(await this._readAll());
16633
+ }
16634
+ const selectByPriority = (source) => {
16635
+ const filtered2 = [];
16636
+ for (let i = source.length - 1; i >= 0; i--) {
16637
+ const msg = source[i];
16638
+ if (passes(msg)) filtered2.push(msg);
16639
+ }
16640
+ filtered2.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
16641
+ return filtered2.slice(0, limit);
16642
+ };
16643
+ const filtered = selectByPriority(candidates);
16644
+ if (filtered.length >= limit || candidatesComplete) return filtered;
16645
+ return selectByPriority(await this._readAll());
15401
16646
  }
15402
16647
  // ── Ack ───────────────────────────────────────────────────────────────
15403
16648
  async ack(input) {
@@ -15454,7 +16699,7 @@ var DefaultMailbox = class {
15454
16699
  for (const m of all) {
15455
16700
  if (m.type !== "status") continue;
15456
16701
  const existing = latest.get(m.from);
15457
- if (existing && m.timestamp <= existing.lastActivityAt) continue;
16702
+ if (existing && m.timestamp < existing.lastActivityAt) continue;
15458
16703
  latest.set(m.from, {
15459
16704
  agentId: m.from,
15460
16705
  name: m.taskContext?.agentName ?? m.from,
@@ -15496,6 +16741,7 @@ var DefaultMailbox = class {
15496
16741
  this._messageCache = null;
15497
16742
  this._messageCacheMtime = -1;
15498
16743
  this._messageCacheSize = -1;
16744
+ this._messageCacheTruncated = false;
15499
16745
  this._byTo.clear();
15500
16746
  this._byFrom.clear();
15501
16747
  }
@@ -15547,13 +16793,32 @@ var DefaultMailbox = class {
15547
16793
  };
15548
16794
  }
15549
16795
  // ── Client registry stubs (not applicable per-session) ─────────────────
16796
+ async autoCompact(_opts) {
16797
+ const purged = await this.purgeStale(_opts);
16798
+ return {
16799
+ readByAllRemoved: 0,
16800
+ expiredRemoved: 0,
16801
+ stalePurged: purged.totalPurged,
16802
+ totalRemoved: purged.totalPurged,
16803
+ remaining: purged.remaining
16804
+ };
16805
+ }
16806
+ startAutoCompactTimer(_opts) {
16807
+ return () => {
16808
+ };
16809
+ }
15550
16810
  async registerClient(_input) {
15551
16811
  }
15552
16812
  async clientHeartbeat(_input) {
15553
16813
  }
16814
+ async deregisterClient(_clientId) {
16815
+ }
15554
16816
  async getClientStatuses() {
15555
16817
  return [];
15556
16818
  }
16819
+ async purgeClients() {
16820
+ return 0;
16821
+ }
15557
16822
  // ── Internal ──────────────────────────────────────────────────────────
15558
16823
  async _readAll() {
15559
16824
  try {
@@ -15567,60 +16832,62 @@ var DefaultMailbox = class {
15567
16832
  /**
15568
16833
  * Read only newly-appended bytes from the file and append them to the
15569
16834
  * in-memory cache, avoiding a full re-read when the file only grew.
16835
+ *
16836
+ * Guards against a half-written tail: when the tail doesn't end with
16837
+ * LINE_SEPARATOR, another process may be mid-append. Parsing a partial
16838
+ * JSON line would throw (caught below), but worse: the NEXT incremental
16839
+ * read would skip that same line because the size tracker already advanced.
15570
16840
  */
15571
16841
  async _readNewMessagesOnly(fd, oldSize, newSize) {
15572
16842
  const tailLen = newSize - oldSize;
15573
16843
  const buf = Buffer.alloc(tailLen);
15574
16844
  await fd.read(buf, 0, tailLen, oldSize);
15575
16845
  const tail = buf.toString("utf8");
15576
- for (const line of tail.split(LINE_SEPARATOR2)) {
16846
+ if (!tail.endsWith(LINE_SEPARATOR2)) {
16847
+ const lastSeparatorIdx = tail.lastIndexOf(LINE_SEPARATOR2);
16848
+ if (lastSeparatorIdx === -1) {
16849
+ return this._messageCache;
16850
+ }
16851
+ const completeTail = tail.slice(0, lastSeparatorIdx + 1);
16852
+ this._appendIncrementalLines(completeTail);
16853
+ this._messageCacheSize = oldSize + lastSeparatorIdx + 1;
16854
+ return this._messageCache;
16855
+ }
16856
+ this._appendIncrementalLines(tail);
16857
+ this._messageCacheSize = newSize;
16858
+ return this._messageCache;
16859
+ }
16860
+ _appendIncrementalLines(raw) {
16861
+ for (const line of raw.split(LINE_SEPARATOR2)) {
15577
16862
  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}`
16863
+ const msg = this._parseLine(line, "incremental read");
16864
+ if (msg === null) continue;
16865
+ this._pushToCache(msg);
16866
+ }
16867
+ }
16868
+ _parseLine(line, source) {
16869
+ try {
16870
+ return parseMailboxMessageLine(line);
16871
+ } catch (err) {
16872
+ this._corruptionCount++;
16873
+ if (this.logger) {
16874
+ this.logger.debug(
16875
+ "Skipped malformed mailbox line",
16876
+ { source, error: err.message, corruptionCount: this._corruptionCount }
15596
16877
  );
16878
+ } else {
16879
+ console.debug(`[mailbox] skipped malformed line during ${source}: ${err.message}`);
15597
16880
  }
16881
+ return null;
15598
16882
  }
15599
- return this._messageCache;
15600
16883
  }
15601
16884
  /** Parse a JSONL string into MailboxMessage[], including migration. */
15602
16885
  _parseLines(raw) {
15603
16886
  const lines = raw.split(LINE_SEPARATOR2).filter((l) => l.trim().length > 0);
15604
16887
  const messages = [];
15605
16888
  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
- }
16889
+ const message = this._parseLine(line, "full parse");
16890
+ if (message !== null) messages.push(message);
15624
16891
  }
15625
16892
  return messages;
15626
16893
  }
@@ -15641,10 +16908,11 @@ var DefaultMailbox = class {
15641
16908
  throw err;
15642
16909
  }
15643
16910
  }
15644
- async _readAllCached() {
16911
+ async _readAllCached(requireComplete = true) {
15645
16912
  try {
15646
16913
  const st = await fsp12.stat(this.filePath);
15647
16914
  if (this._messageCache !== null && this._messageCacheMtime === st.mtimeMs && this._messageCacheSize === st.size) {
16915
+ if (requireComplete && this._messageCacheTruncated) return this._readAll();
15648
16916
  return this._messageCache;
15649
16917
  }
15650
16918
  if (this._messageCache !== null && this._messageCacheSize >= 0 && st.size > this._messageCacheSize) {
@@ -15652,7 +16920,7 @@ var DefaultMailbox = class {
15652
16920
  try {
15653
16921
  const updated = await this._readNewMessagesOnly(fd, this._messageCacheSize, st.size);
15654
16922
  this._messageCacheMtime = st.mtimeMs;
15655
- this._messageCacheSize = st.size;
16923
+ if (requireComplete && this._messageCacheTruncated) return this._readAll();
15656
16924
  return updated;
15657
16925
  } finally {
15658
16926
  await fd.close();
@@ -15670,28 +16938,18 @@ var DefaultMailbox = class {
15670
16938
  }
15671
16939
  }
15672
16940
  _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);
16941
+ this._messageCacheTruncated = messages.length > MESSAGE_CACHE_MAX_ENTRIES2;
16942
+ this._messageCache = this._messageCacheTruncated ? messages.slice(-MESSAGE_CACHE_MAX_ENTRIES2) : messages;
16943
+ this._buildIndexes(this._messageCache);
15683
16944
  this._messageCacheMtime = mtime;
15684
16945
  this._messageCacheSize = size;
15685
16946
  }
15686
16947
  _pushToCache(msg) {
15687
16948
  if (this._messageCache === null) return;
15688
16949
  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;
16950
+ const evicted = this._messageCache.shift();
16951
+ if (evicted !== void 0) this._unindexMsg(evicted);
16952
+ this._messageCacheTruncated = true;
15695
16953
  }
15696
16954
  this._messageCache.push(msg);
15697
16955
  this._indexMsg(msg);
@@ -15719,6 +16977,15 @@ var DefaultMailbox = class {
15719
16977
  this._byFrom.set(msg.from, /* @__PURE__ */ new Set([msg]));
15720
16978
  }
15721
16979
  }
16980
+ /** Remove an evicted message from both indexes without rebuilding them. */
16981
+ _unindexMsg(msg) {
16982
+ const toSet = this._byTo.get(msg.to);
16983
+ toSet?.delete(msg);
16984
+ if (toSet?.size === 0) this._byTo.delete(msg.to);
16985
+ const fromSet = this._byFrom.get(msg.from);
16986
+ fromSet?.delete(msg);
16987
+ if (fromSet?.size === 0) this._byFrom.delete(msg.from);
16988
+ }
15722
16989
  };
15723
16990
 
15724
16991
  // src/coordination/mailbox-actions.ts
@@ -15989,7 +17256,7 @@ function createMailboxHooks(opts) {
15989
17256
  var NULL_FLEET_BUS = new FleetBus();
15990
17257
 
15991
17258
  // src/coordination/package-author-tracker.ts
15992
- import * as fs2 from "node:fs/promises";
17259
+ import * as fs3 from "node:fs/promises";
15993
17260
  import * as path16 from "node:path";
15994
17261
  var DEFAULT_MAX_ENTRIES = 1e4;
15995
17262
  var LOG_FILENAME = "package-authors.json";
@@ -15998,7 +17265,7 @@ function logPath(storageDir) {
15998
17265
  }
15999
17266
  async function loadLog(storageDir, projectRoot) {
16000
17267
  try {
16001
- const raw = await fs2.readFile(logPath(storageDir), "utf-8");
17268
+ const raw = await fs3.readFile(logPath(storageDir), "utf-8");
16002
17269
  const parsed = JSON.parse(raw);
16003
17270
  if (!parsed.entries || !Array.isArray(parsed.entries)) {
16004
17271
  return { projectRoot, entries: [] };
@@ -16012,13 +17279,13 @@ async function loadLog(storageDir, projectRoot) {
16012
17279
  }
16013
17280
  }
16014
17281
  async function saveLog(storageDir, log) {
16015
- await fs2.mkdir(storageDir, { recursive: true });
17282
+ await fs3.mkdir(storageDir, { recursive: true });
16016
17283
  const tmp = `${logPath(storageDir)}.tmp.${Date.now()}`;
16017
- await fs2.writeFile(tmp, JSON.stringify(log, null, 2) + "\n", "utf-8");
16018
- 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));
16019
17286
  }
16020
17287
  function detectEcosystem(manifestPath) {
16021
- const name = path16.basename(manifestPath).toLowerCase();
17288
+ const name = path16.win32.basename(manifestPath).toLowerCase();
16022
17289
  if (name === "package.json") return "npm";
16023
17290
  if (name === "go.mod") return "go";
16024
17291
  if (name === "cargo.toml") return "cargo";
@@ -16381,7 +17648,8 @@ function isProcessAlive(pid) {
16381
17648
  encoding: "utf-8",
16382
17649
  stdio: ["pipe", "pipe", "pipe"]
16383
17650
  });
16384
- return /\b"?,?\d+,?"?/.test(out) || out.includes(String(pid));
17651
+ const match = /(?:^|\n)"[^"]*","(\d+)"/.exec(out);
17652
+ return match !== null && match[1] === String(pid);
16385
17653
  } catch {
16386
17654
  return false;
16387
17655
  }
@@ -16584,9 +17852,9 @@ var AdaptiveConcurrencyController = class {
16584
17852
  };
16585
17853
 
16586
17854
  // src/coordination/agent-monitor.ts
16587
- import * as fs3 from "node:fs/promises";
17855
+ import * as fs4 from "node:fs/promises";
16588
17856
  import * as path18 from "node:path";
16589
- var AgentMonitorService = class {
17857
+ var AgentMonitorService = class _AgentMonitorService {
16590
17858
  _fleetBus;
16591
17859
  _events;
16592
17860
  _sessionId;
@@ -16596,6 +17864,15 @@ var AgentMonitorService = class {
16596
17864
  _onEntry;
16597
17865
  /** Per-subagent virtual sessions. */
16598
17866
  _sessions = /* @__PURE__ */ new Map();
17867
+ /**
17868
+ * Per-subagent OPEN streaming segment. Provider text/thinking deltas
17869
+ * arrive word-by-word; writing one transcript entry per delta fragments
17870
+ * every surface (ring, JSONL, HQ) into unreadable single-word lines.
17871
+ * Instead, consecutive same-kind/same-iteration deltas append into ONE
17872
+ * entry that is already in the ring (live views see it grow in place);
17873
+ * the JSONL line is written once, when the segment closes.
17874
+ */
17875
+ _openStreams = /* @__PURE__ */ new Map();
16599
17876
  /** Disposers for FleetBus subscriptions, keyed by subagentId. */
16600
17877
  _subscriptions = /* @__PURE__ */ new Map();
16601
17878
  /** Generic fleet-wide subscription disposer. */
@@ -16658,6 +17935,9 @@ var AgentMonitorService = class {
16658
17935
  stop() {
16659
17936
  if (!this._started) return;
16660
17937
  this._started = false;
17938
+ for (const subagentId of [...this._openStreams.keys()]) {
17939
+ this._closeStream(subagentId);
17940
+ }
16661
17941
  if (this._fleetDisposer) {
16662
17942
  this._fleetDisposer();
16663
17943
  this._fleetDisposer = void 0;
@@ -16726,6 +18006,7 @@ var AgentMonitorService = class {
16726
18006
  }
16727
18007
  /** Drop a retired subagent from every live in-memory monitor surface. */
16728
18008
  removeSubagent(subagentId) {
18009
+ this._closeStream(subagentId);
16729
18010
  this._sessions.delete(subagentId);
16730
18011
  const dispose = this._subscriptions.get(subagentId);
16731
18012
  if (dispose) dispose();
@@ -16739,31 +18020,13 @@ var AgentMonitorService = class {
16739
18020
  case "provider.text_delta": {
16740
18021
  const text = payload.text;
16741
18022
  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
- });
18023
+ this._appendStreamDelta(subagentId, session, "text", text, payload.iteration ?? 0);
16752
18024
  break;
16753
18025
  }
16754
18026
  case "provider.thinking_delta": {
16755
18027
  const text = payload.text;
16756
18028
  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
- });
18029
+ this._appendStreamDelta(subagentId, session, "thinking", text, payload.iteration ?? 0);
16767
18030
  break;
16768
18031
  }
16769
18032
  case "tool.started": {
@@ -16820,15 +18083,66 @@ var AgentMonitorService = class {
16820
18083
  }
16821
18084
  }
16822
18085
  }
18086
+ /** Max characters accumulated into one streaming segment before rolling over. */
18087
+ static _MAX_SEGMENT_CHARS = 2e4;
18088
+ /**
18089
+ * Fold a provider text/thinking delta into the subagent's open streaming
18090
+ * segment, or open a new one. A segment breaks on kind change, iteration
18091
+ * change, size cap, or any non-delta entry (see `_addEntry`). The entry
18092
+ * is pushed to the ring when the segment OPENS (live views poll the ring
18093
+ * and watch it grow in place); the JSONL line AND the timeline/HQ
18094
+ * announcement happen once, on close, with the COMPLETE segment — so
18095
+ * downstream consumers get one whole message instead of a word-per-event
18096
+ * firehose (or, worse, only the first word).
18097
+ */
18098
+ _appendStreamDelta(subagentId, session, kind, text, iteration) {
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;
18102
+ return;
18103
+ }
18104
+ this._closeStream(subagentId);
18105
+ const entry = {
18106
+ id: this._uid(),
18107
+ subagentId,
18108
+ agentName: session.agentName,
18109
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
18110
+ kind,
18111
+ content: text,
18112
+ iteration
18113
+ };
18114
+ this._openStreams.set(subagentId, { entry });
18115
+ session.transcript.push(entry);
18116
+ if (session.transcript.length > this._maxEntries) {
18117
+ session.transcript.splice(0, session.transcript.length - this._maxEntries);
18118
+ }
18119
+ }
18120
+ /**
18121
+ * Close the subagent's open streaming segment: persist it to JSONL and
18122
+ * announce the completed entry on the local bus + HQ callback.
18123
+ */
18124
+ _closeStream(subagentId) {
18125
+ const open6 = this._openStreams.get(subagentId);
18126
+ if (!open6) return;
18127
+ this._openStreams.delete(subagentId);
18128
+ this._appendToFile(subagentId, open6.entry).catch(() => {
18129
+ });
18130
+ this._emitEntry(open6.entry);
18131
+ }
16823
18132
  _addEntry(subagentId, entry) {
16824
18133
  const session = this._sessions.get(subagentId);
16825
18134
  if (!session) return;
18135
+ this._closeStream(subagentId);
16826
18136
  session.transcript.push(entry);
16827
18137
  if (session.transcript.length > this._maxEntries) {
16828
18138
  session.transcript.splice(0, session.transcript.length - this._maxEntries);
16829
18139
  }
16830
18140
  this._appendToFile(subagentId, entry).catch(() => {
16831
18141
  });
18142
+ this._emitEntry(entry);
18143
+ }
18144
+ /** Announce a transcript entry on the local bus + HQ callback. */
18145
+ _emitEntry(entry) {
16832
18146
  this._events.emit("agent.timeline.message", {
16833
18147
  sessionId: this._sessionId,
16834
18148
  subagentId: entry.subagentId,
@@ -16844,10 +18158,10 @@ var AgentMonitorService = class {
16844
18158
  }
16845
18159
  async _appendToFile(subagentId, entry) {
16846
18160
  const dir = path18.join(this._transcriptsDir, subagentId);
16847
- await fs3.mkdir(dir, { recursive: true });
18161
+ await fs4.mkdir(dir, { recursive: true });
16848
18162
  const filePath = path18.join(dir, "transcript.jsonl");
16849
18163
  const line = JSON.stringify(entry) + "\n";
16850
- await fs3.appendFile(filePath, line, { encoding: "utf8" });
18164
+ await fs4.appendFile(filePath, line, { encoding: "utf8" });
16851
18165
  }
16852
18166
  _uid() {
16853
18167
  return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
@@ -16855,8 +18169,11 @@ var AgentMonitorService = class {
16855
18169
  _formatToolUse(name, input) {
16856
18170
  if (input === void 0) return `${name}()`;
16857
18171
  const body = this._stringifyForTimeline(input);
16858
- return body ? `${name}
16859
- ${body}` : `${name}()`;
18172
+ if (!body) return `${name}()`;
18173
+ const inline = body.replace(/\s+/g, " ").trim();
18174
+ const head = inline.length <= 160 ? `${name}(${inline})` : `${name}(${inline.slice(0, 159)}\u2026)`;
18175
+ return `${head}
18176
+ ${body}`;
16860
18177
  }
16861
18178
  _formatToolResult(summary, output, outputBytes) {
16862
18179
  if (!output) return summary;
@@ -16893,8 +18210,6 @@ var AutonomousBrain = class {
16893
18210
  maxRetries;
16894
18211
  consensusRiskThreshold;
16895
18212
  selfImprove;
16896
- /** Decision history for self-improvement and audit. */
16897
- decisionHistory = [];
16898
18213
  /** Tracks failure patterns for self-improvement. */
16899
18214
  failurePatterns = /* @__PURE__ */ new Map();
16900
18215
  RISK_ORDER = ["low", "medium", "high", "critical"];
@@ -17206,7 +18521,6 @@ ${ctx.error}`);
17206
18521
  madeAt: (/* @__PURE__ */ new Date()).toISOString(),
17207
18522
  context: input.context
17208
18523
  });
17209
- this.decisionHistory.push(node);
17210
18524
  return node;
17211
18525
  }
17212
18526
  _inferRoles(task) {
@@ -17235,7 +18549,8 @@ import { randomUUID as randomUUID16 } from "node:crypto";
17235
18549
  import { randomUUID as randomUUID14 } from "node:crypto";
17236
18550
  import * as fsp14 from "node:fs/promises";
17237
18551
  import * as path19 from "node:path";
17238
- var KnowledgeGraph = class {
18552
+ var DEFAULT_MAX_NODES = 2e3;
18553
+ var KnowledgeGraph = class _KnowledgeGraph {
17239
18554
  nodes = /* @__PURE__ */ new Map();
17240
18555
  index = /* @__PURE__ */ new Map();
17241
18556
  // tag/field → node ids
@@ -17243,6 +18558,7 @@ var KnowledgeGraph = class {
17243
18558
  pendingDeliveries = /* @__PURE__ */ new Map();
17244
18559
  filePath;
17245
18560
  graphFilePath;
18561
+ maxNodes;
17246
18562
  /**
17247
18563
  * Stable per-node insertion sequence. `nodes` (a Map) preserves insertion
17248
18564
  * order even across `update()` (Map.set on an existing key keeps its slot),
@@ -17261,9 +18577,10 @@ var KnowledgeGraph = class {
17261
18577
  getIndex() {
17262
18578
  return this.index;
17263
18579
  }
17264
- constructor(sessionDir) {
18580
+ constructor(sessionDir, maxNodes = DEFAULT_MAX_NODES) {
17265
18581
  this.filePath = path19.join(sessionDir, "_knowledge_graph");
17266
18582
  this.graphFilePath = path19.join(this.filePath, "graph.jsonl");
18583
+ this.maxNodes = maxNodes;
17267
18584
  }
17268
18585
  // ── Write ──────────────────────────────────────────────────────────────
17269
18586
  /**
@@ -17275,6 +18592,7 @@ var KnowledgeGraph = class {
17275
18592
  this.nodes.set(full.id, full);
17276
18593
  this._trackSeq(full.id);
17277
18594
  this._addToIndex(full, this._indexKeys(full));
18595
+ this._prune();
17278
18596
  await this._persist(full);
17279
18597
  this._deliver(full);
17280
18598
  return full;
@@ -17287,10 +18605,48 @@ var KnowledgeGraph = class {
17287
18605
  const updated = { ...existing, ...patch };
17288
18606
  this.nodes.set(id, updated);
17289
18607
  this._addToIndex(updated, this._indexKeys(updated));
18608
+ this._prune();
17290
18609
  this._deliver(updated);
17291
18610
  await this._append(updated);
17292
18611
  return updated;
17293
18612
  }
18613
+ /**
18614
+ * True when the node is in a terminal / settled state — no future mutations
18615
+ * are expected. Such nodes are the first to be evicted when the in-memory
18616
+ * cap is reached. The JSONL file on disk retains the full history.
18617
+ */
18618
+ static _isTerminal(node) {
18619
+ if (node.type === "goal") return node.status === "done" || node.status === "failed";
18620
+ if (node.type === "change") return node.status === "rejected" || node.status === "rolled_back";
18621
+ if (node.type === "vote") return true;
18622
+ return false;
18623
+ }
18624
+ /**
18625
+ * Evict oldest terminal-state nodes when the in-memory cap is exceeded.
18626
+ * The JSONL file is the authoritative record; eviction only affects memory.
18627
+ */
18628
+ _prune() {
18629
+ if (this.nodes.size <= this.maxNodes) return;
18630
+ const toEvict = this.nodes.size - this.maxNodes;
18631
+ const terminal = [];
18632
+ for (const [id, node] of this.nodes) {
18633
+ if (_KnowledgeGraph._isTerminal(node)) terminal.push({ id, seq: this.seq.get(id) ?? 0 });
18634
+ }
18635
+ terminal.sort((a, b) => a.seq - b.seq);
18636
+ const evicted = new Set(terminal.slice(0, toEvict).map((e) => e.id));
18637
+ if (evicted.size === 0) return;
18638
+ for (const id of evicted) {
18639
+ const node = this.nodes.get(id);
18640
+ if (!node) continue;
18641
+ this._removeFromIndex(node, this._indexKeys(node));
18642
+ this.nodes.delete(id);
18643
+ this.seq.delete(id);
18644
+ for (const pending of this.pendingDeliveries.values()) {
18645
+ const idx = pending.findIndex((n) => n.id === id);
18646
+ if (idx >= 0) pending.splice(idx, 1);
18647
+ }
18648
+ }
18649
+ }
17294
18650
  // ── Read ───────────────────────────────────────────────────────────────
17295
18651
  get(id) {
17296
18652
  return this.nodes.get(id);
@@ -18723,6 +20079,7 @@ var AutonomousCoordinator = class _AutonomousCoordinator {
18723
20079
  mailbox;
18724
20080
  events;
18725
20081
  onCoordinatorEvent;
20082
+ logger;
18726
20083
  running = false;
18727
20084
  iterationCount = 0;
18728
20085
  lastSyncAt = 0;
@@ -18739,6 +20096,7 @@ var AutonomousCoordinator = class _AutonomousCoordinator {
18739
20096
  this.mailbox = opts.mailbox ?? void 0;
18740
20097
  this.events = opts.events ?? void 0;
18741
20098
  this.onCoordinatorEvent = opts.onCoordinatorEvent;
20099
+ this.logger = opts.logger;
18742
20100
  this.graph = new KnowledgeGraph(opts.sessionDir);
18743
20101
  this.dag = new TaskDAG();
18744
20102
  this.auction = new TaskAuctioneer({
@@ -18897,15 +20255,22 @@ var AutonomousCoordinator = class _AutonomousCoordinator {
18897
20255
  stop() {
18898
20256
  if (!this.running) return;
18899
20257
  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
- );
20258
+ if (this.logger) {
20259
+ this.logger.error(
20260
+ "Stop signal received \u2014 shutting down",
20261
+ { event: "autonomous_coordinator.stop_signal", iteration: this.iterationCount }
20262
+ );
20263
+ } else {
20264
+ console.error(
20265
+ JSON.stringify({
20266
+ level: "error",
20267
+ event: "autonomous_coordinator.stop_signal",
20268
+ message: "stop signal received \u2014 shutting down",
20269
+ iteration: this.iterationCount,
20270
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
20271
+ })
20272
+ );
20273
+ }
18909
20274
  }
18910
20275
  /**
18911
20276
  * Report that a terminal worker (not a Director subagent) completed a claimed
@@ -19353,6 +20718,7 @@ export {
19353
20718
  AutonomousCoordinator,
19354
20719
  BUG_HUNTER_AGENT,
19355
20720
  BUILD_AGENTS,
20721
+ BrainDecisionLedger,
19356
20722
  BrainDecisionQueue,
19357
20723
  BrainMonitor,
19358
20724
  BudgetExceededError,
@@ -19375,6 +20741,7 @@ export {
19375
20741
  DefaultMultiAgentCoordinator,
19376
20742
  Director,
19377
20743
  DirectorAlertLevel,
20744
+ EscalationRoutingBrainArbiter,
19378
20745
  FLEET_ROSTER,
19379
20746
  FLEET_ROSTER_BUDGETS,
19380
20747
  FLEET_ROSTER_WITHACP,
@@ -19424,12 +20791,14 @@ export {
19424
20791
  applyRosterBudget,
19425
20792
  attachAutoExtend,
19426
20793
  attachDepWatcherBridge,
20794
+ brainDecisionKey,
19427
20795
  buildDownAlert,
19428
20796
  buildRecoveryAlert,
19429
20797
  composeDirectorPrompt,
19430
20798
  composeSubagentPrompt,
19431
20799
  createAgentMonitorService,
19432
20800
  createDelegateTool,
20801
+ createLedgerGuardBrainArbiter,
19433
20802
  createMailboxHooks,
19434
20803
  createMessage,
19435
20804
  detectEcosystem,
@@ -19493,6 +20862,7 @@ export {
19493
20862
  scoreAgents,
19494
20863
  startPackageOutdatedWatcher,
19495
20864
  subagentNeedsWorktree,
20865
+ terminalPolicyDecision,
19496
20866
  updatePackageOutdatedStatus,
19497
20867
  validateWatchdogOptions,
19498
20868
  withDisabledToolFiltering,