@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
package/dist/hq/index.js CHANGED
@@ -1,11 +1,13 @@
1
- // src/hq/protocol.ts
2
- var HQ_PROTOCOL_VERSION = 1;
1
+ // src/hq/protocol/tool.ts
3
2
  var DEFAULT_HQ_REDACTION_POLICY = {
4
3
  rawContent: true,
5
4
  toolArgs: "full",
6
5
  paths: "full"
7
6
  };
8
7
  var HQ_TRANSCRIPT_TEXT_CAP = 16e3;
8
+
9
+ // src/hq/protocol/core.ts
10
+ var HQ_PROTOCOL_VERSION = 1;
9
11
  var KNOWN_HQ_CLIENT_FRAME_TYPES = /* @__PURE__ */ new Set([
10
12
  "client.hello",
11
13
  "client.event",
@@ -140,8 +142,57 @@ var KNOWN_HQ_EVENT_PAYLOAD_TYPES = /* @__PURE__ */ new Set([
140
142
  "tool.started",
141
143
  "tool.completed",
142
144
  "session.usage",
143
- "client.heartbeat"
145
+ "client.heartbeat",
146
+ "mcp.health.snapshot",
147
+ "mcp.operation"
148
+ ]);
149
+ function isHqMcpLatencySummary(x) {
150
+ if (typeof x !== "object" || x === null) return false;
151
+ const v = x;
152
+ return typeof v.count === "number" && (v.lastMs === void 0 || typeof v.lastMs === "number") && (v.minMs === void 0 || typeof v.minMs === "number") && (v.maxMs === void 0 || typeof v.maxMs === "number") && (v.p50Ms === void 0 || typeof v.p50Ms === "number") && (v.p95Ms === void 0 || typeof v.p95Ms === "number");
153
+ }
154
+ function isHqMcpFailureCounts(x) {
155
+ if (typeof x !== "object" || x === null) return false;
156
+ const v = x;
157
+ return typeof v.transport === "number" && typeof v.protocol === "number" && typeof v.tool === "number";
158
+ }
159
+ function isHqMcpHealthCheck(x) {
160
+ if (typeof x !== "object" || x === null) return false;
161
+ const v = x;
162
+ return typeof v.name === "string" && typeof v.passed === "boolean" && (v.value === void 0 || typeof v.value === "number") && (v.threshold === void 0 || typeof v.threshold === "number");
163
+ }
164
+ var HQ_MCP_HEALTH_STATES = /* @__PURE__ */ new Set([
165
+ "disabled",
166
+ "dormant",
167
+ "connecting",
168
+ "healthy",
169
+ "degraded",
170
+ "failed"
171
+ ]);
172
+ var HQ_MCP_CONNECTION_STATES = /* @__PURE__ */ new Set([
173
+ "idle",
174
+ "dormant",
175
+ "connecting",
176
+ "connected",
177
+ "reconnecting",
178
+ "disconnected",
179
+ "failed"
144
180
  ]);
181
+ function isHqMcpServerHealth(x) {
182
+ if (typeof x !== "object" || x === null) return false;
183
+ const v = x;
184
+ return typeof v.name === "string" && typeof v.projectId === "string" && typeof v.clientId === "string" && typeof v.connectionState === "string" && HQ_MCP_CONNECTION_STATES.has(v.connectionState) && typeof v.healthState === "string" && HQ_MCP_HEALTH_STATES.has(v.healthState) && (v.lastSuccessAt === void 0 || typeof v.lastSuccessAt === "string") && (v.lastFailureAt === void 0 || typeof v.lastFailureAt === "string") && (v.lastFailureKind === void 0 || v.lastFailureKind === "transport" || v.lastFailureKind === "protocol" || v.lastFailureKind === "tool") && typeof v.consecutiveFailures === "number" && isHqMcpFailureCounts(v.failures) && typeof v.reconnectCount === "number" && typeof v.wakeCount === "number" && typeof v.sleepCount === "number" && typeof v.restartCount === "number" && isHqMcpLatencySummary(v.connectionLatency) && isHqMcpLatencySummary(v.discoveryLatency) && isHqMcpLatencySummary(v.callLatency) && typeof v.inFlightCalls === "number" && typeof v.peakInFlightCalls === "number" && Array.isArray(v.healthChecks) && v.healthChecks.every(isHqMcpHealthCheck);
185
+ }
186
+ function isHqMcpHealthSnapshotPayload(x) {
187
+ if (typeof x !== "object" || x === null) return false;
188
+ const v = x;
189
+ return Array.isArray(v.servers) && v.servers.every(isHqMcpServerHealth);
190
+ }
191
+ function isHqMcpOperationPayload(x) {
192
+ if (typeof x !== "object" || x === null) return false;
193
+ const v = x;
194
+ return typeof v.operation === "object" && v.operation !== null && Array.isArray(v.servers) && v.servers.every(isHqMcpServerHealth);
195
+ }
145
196
  function isHqMailboxMessageSummary(x) {
146
197
  if (typeof x !== "object" || x === null) return false;
147
198
  const v = x;
@@ -351,6 +402,10 @@ function parseHqEventPayload(eventType, payload) {
351
402
  return isHqUsagePayload(payload) ? { ok: true, payload } : { ok: false, reason: "malformed-payload" };
352
403
  case "client.heartbeat":
353
404
  return isHqClientHeartbeatPayload(payload) ? { ok: true, payload } : { ok: false, reason: "malformed-payload" };
405
+ case "mcp.health.snapshot":
406
+ return isHqMcpHealthSnapshotPayload(payload) ? { ok: true, payload } : { ok: false, reason: "malformed-payload" };
407
+ case "mcp.operation":
408
+ return isHqMcpOperationPayload(payload) ? { ok: true, payload } : { ok: false, reason: "malformed-payload" };
354
409
  default: {
355
410
  const _exhaustive = eventType;
356
411
  return _exhaustive;
@@ -490,7 +545,12 @@ function hasCredentialAnchors(text) {
490
545
  text.includes("_TOKEN=") || // ACCESS_TOKEN=, AUTH_TOKEN=, ...
491
546
  text.includes("_SECRET=") || // API_SECRET=, CLIENT_SECRET=, ...
492
547
  text.includes("_PASSWORD=") || // DB_PASSWORD=, ROOT_PASSWORD=, ...
493
- text.includes("mongodb://") || text.includes("mongodb+srv://") || text.includes("postgres://") || text.includes("postgresql://") || text.includes("mysql://") || text.includes("redis://");
548
+ text.includes("mongodb://") || text.includes("mongodb+srv://") || text.includes("postgres://") || text.includes("postgresql://") || text.includes("mysql://") || text.includes("redis://") || // JSON-style credential keys: tool outputs often serialise objects as
549
+ // raw JSON strings rather than parsed objects. The value may not start
550
+ // with a recognisable prefix (sk-, ghp_, etc.), so we anchor on common
551
+ // key names. The `"` prefix avoids matching prose that happens to mention
552
+ // these words — JSON serialisation always quotes string keys.
553
+ text.includes('"apiKey"') || text.includes('"api_key"') || text.includes('"token"') || text.includes('"secret"') || text.includes('"password"') || text.includes('"authorization"') || text.includes('"bearer"') || text.includes('"private_key"') || text.includes('"access_token"') || text.includes('"refresh_token"') || text.includes('"client_secret"');
494
554
  }
495
555
  var DefaultSecretScrubber = class {
496
556
  scrub(text) {
@@ -941,6 +1001,7 @@ var HqPublisher = class {
941
1001
  this.reconnectMaxMs = options.reconnectMaxMs ?? DEFAULT_RECONNECT_MAX_MS;
942
1002
  this.maxQueuedMessages = options.maxQueuedMessages ?? DEFAULT_MAX_QUEUED_MESSAGES;
943
1003
  this.resolvedRedactionPolicy = resolveHqRedactionPolicy(options.redactionPolicy);
1004
+ this.logger = options.logger;
944
1005
  }
945
1006
  options;
946
1007
  socketFactory;
@@ -952,6 +1013,7 @@ var HqPublisher = class {
952
1013
  reconnectMaxMs;
953
1014
  maxQueuedMessages;
954
1015
  resolvedRedactionPolicy;
1016
+ logger;
955
1017
  socket = null;
956
1018
  seq = 0;
957
1019
  queue = [];
@@ -1290,6 +1352,10 @@ var HqPublisher = class {
1290
1352
  emitConnectWarning() {
1291
1353
  const attempt = this.lastAttempt;
1292
1354
  const message = `WrongStack HQ publisher: ${this.reconnectAttempt} consecutive connection failures${attempt !== null ? ` to ${attempt.url} (client token ${attempt.hadToken ? "present" : "absent"})` : ""}. Either the HQ server is unreachable or it rejected the token (401). If HQ runs in client-token mode, verify WRONGSTACK_HQ_TOKEN / auth.json. Retries continue with backoff.`;
1355
+ if (this.logger) {
1356
+ this.logger.warn(message, { event: "hq.publisher.connect_failed" });
1357
+ return;
1358
+ }
1293
1359
  const warn = this.options.warn ?? ((msg) => console.warn(
1294
1360
  JSON.stringify({
1295
1361
  level: "warn",
@@ -1322,7 +1388,7 @@ var HqPublisher = class {
1322
1388
 
1323
1389
  // src/hq/factory.ts
1324
1390
  import { createHash as createHash2, randomUUID as randomUUID4 } from "node:crypto";
1325
- import * as fs3 from "node:fs";
1391
+ import * as fs4 from "node:fs";
1326
1392
  import { hostname } from "node:os";
1327
1393
  import { basename as basename5 } from "node:path";
1328
1394
 
@@ -1333,7 +1399,8 @@ import * as path4 from "node:path";
1333
1399
 
1334
1400
  // src/utils/atomic-write.ts
1335
1401
  import { randomBytes } from "node:crypto";
1336
- import * as fs from "node:fs/promises";
1402
+ import * as fs2 from "node:fs/promises";
1403
+ import { watch as watchDir } from "node:fs";
1337
1404
  import * as path3 from "node:path";
1338
1405
 
1339
1406
  // src/utils/session-scoped-path.ts
@@ -1360,14 +1427,34 @@ function invalid(sessionId) {
1360
1427
 
1361
1428
  // src/utils/wstack-paths.ts
1362
1429
  import { createHash } from "node:crypto";
1430
+ import * as fs from "node:fs";
1363
1431
  import * as os from "node:os";
1364
1432
  import * as path2 from "node:path";
1433
+ function canonicalProjectRoot(absRoot) {
1434
+ const checkoutRoot = path2.resolve(absRoot);
1435
+ const dotGit = path2.join(checkoutRoot, ".git");
1436
+ try {
1437
+ if (!fs.statSync(dotGit).isFile()) return checkoutRoot;
1438
+ const gitDirLine = fs.readFileSync(dotGit, "utf8").trim();
1439
+ const match = /^gitdir:\s*(.+)$/i.exec(gitDirLine);
1440
+ if (!match?.[1]) return checkoutRoot;
1441
+ const gitDir = path2.resolve(checkoutRoot, match[1].trim());
1442
+ const commonDirFile = path2.join(gitDir, "commondir");
1443
+ if (!fs.statSync(commonDirFile).isFile()) return checkoutRoot;
1444
+ const commonDir = path2.resolve(gitDir, fs.readFileSync(commonDirFile, "utf8").trim());
1445
+ if (path2.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
1446
+ return path2.dirname(commonDir);
1447
+ } catch {
1448
+ return checkoutRoot;
1449
+ }
1450
+ }
1365
1451
  function projectHash(absRoot) {
1366
- return createHash("sha256").update(path2.resolve(absRoot)).digest("hex").slice(0, 12);
1452
+ return createHash("sha256").update(canonicalProjectRoot(absRoot)).digest("hex").slice(0, 12);
1367
1453
  }
1368
1454
  function projectSlug(absRoot) {
1369
- const base = slugify(path2.basename(absRoot));
1370
- const hash = createHash("sha256").update(path2.resolve(absRoot)).digest("hex").slice(0, 6);
1455
+ const identityRoot = canonicalProjectRoot(absRoot);
1456
+ const base = slugify(path2.basename(identityRoot));
1457
+ const hash = createHash("sha256").update(identityRoot).digest("hex").slice(0, 6);
1371
1458
  return `${base}-${hash}`;
1372
1459
  }
1373
1460
  function slugify(name) {
@@ -1538,16 +1625,16 @@ var FsError = class extends WrongStackError {
1538
1625
  // src/utils/atomic-write.ts
1539
1626
  async function atomicWrite(targetPath, content, opts = {}) {
1540
1627
  const dir = path3.dirname(targetPath);
1541
- await fs.mkdir(dir, { recursive: true });
1628
+ await fs2.mkdir(dir, { recursive: true });
1542
1629
  const tmp = path3.join(dir, `.${path3.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
1543
1630
  try {
1544
1631
  if (typeof content === "string") {
1545
- await fs.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
1632
+ await fs2.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
1546
1633
  } else {
1547
- await fs.writeFile(tmp, content, { flag: "wx" });
1634
+ await fs2.writeFile(tmp, content, { flag: "wx" });
1548
1635
  }
1549
1636
  try {
1550
- const fh = await fs.open(tmp, "r+");
1637
+ const fh = await fs2.open(tmp, "r+");
1551
1638
  try {
1552
1639
  await fh.sync();
1553
1640
  } finally {
@@ -1557,24 +1644,24 @@ async function atomicWrite(targetPath, content, opts = {}) {
1557
1644
  }
1558
1645
  let mode;
1559
1646
  try {
1560
- const stat4 = await fs.stat(targetPath);
1647
+ const stat4 = await fs2.stat(targetPath);
1561
1648
  mode = stat4.mode & 511;
1562
1649
  } catch {
1563
1650
  mode = opts.mode;
1564
1651
  }
1565
1652
  if (mode !== void 0) {
1566
- await fs.chmod(tmp, mode);
1653
+ await fs2.chmod(tmp, mode);
1567
1654
  }
1568
1655
  await renameWithRetry(tmp, targetPath);
1569
1656
  if (mode !== void 0 && process.platform === "win32") {
1570
1657
  try {
1571
- await fs.chmod(targetPath, mode);
1658
+ await fs2.chmod(targetPath, mode);
1572
1659
  } catch {
1573
1660
  }
1574
1661
  }
1575
1662
  } catch (err) {
1576
1663
  try {
1577
- await fs.unlink(tmp);
1664
+ await fs2.unlink(tmp);
1578
1665
  } catch {
1579
1666
  }
1580
1667
  throw err;
@@ -1582,7 +1669,7 @@ async function atomicWrite(targetPath, content, opts = {}) {
1582
1669
  }
1583
1670
  async function withFileLock(targetPath, fn, opts = {}) {
1584
1671
  const dir = path3.dirname(targetPath);
1585
- await fs.mkdir(dir, { recursive: true });
1672
+ await fs2.mkdir(dir, { recursive: true });
1586
1673
  const lockPath = path3.join(dir, `.${path3.basename(targetPath)}.lock`);
1587
1674
  const timeoutMs = opts.timeoutMs ?? 5e3;
1588
1675
  const staleMs = opts.staleMs ?? 3e4;
@@ -1590,26 +1677,27 @@ async function withFileLock(targetPath, fn, opts = {}) {
1590
1677
  let handle;
1591
1678
  for (; ; ) {
1592
1679
  try {
1593
- handle = await fs.open(lockPath, "wx");
1680
+ handle = await fs2.open(lockPath, "wx");
1594
1681
  await handle.writeFile(`${process.pid}:${Date.now()}`);
1595
1682
  break;
1596
1683
  } catch (err) {
1597
1684
  const code = err.code;
1598
1685
  if (code === "ENOENT") {
1599
- await fs.mkdir(dir, { recursive: true });
1686
+ await fs2.mkdir(dir, { recursive: true });
1600
1687
  continue;
1601
1688
  }
1602
- if (code !== "EEXIST") throw err;
1689
+ if (code !== "EEXIST" && code !== "EPERM") throw err;
1603
1690
  try {
1604
- const stat4 = await fs.stat(lockPath);
1691
+ const stat4 = await fs2.stat(lockPath);
1605
1692
  if (Date.now() - stat4.mtimeMs > staleMs) {
1606
- await fs.unlink(lockPath);
1693
+ await fs2.unlink(lockPath);
1607
1694
  continue;
1608
1695
  }
1609
1696
  } catch {
1610
1697
  continue;
1611
1698
  }
1612
- if (Date.now() - started >= timeoutMs) {
1699
+ const elapsed = Date.now() - started;
1700
+ if (elapsed >= timeoutMs) {
1613
1701
  throw new FsError({
1614
1702
  message: `Timed out waiting for file lock: ${targetPath}`,
1615
1703
  code: "FS_ATOMIC_WRITE_FAILED",
@@ -1617,7 +1705,7 @@ async function withFileLock(targetPath, fn, opts = {}) {
1617
1705
  context: { timeoutMs }
1618
1706
  });
1619
1707
  }
1620
- await new Promise((resolve4) => setTimeout(resolve4, 25));
1708
+ await waitForLockRelease(lockPath, timeoutMs - elapsed);
1621
1709
  }
1622
1710
  }
1623
1711
  try {
@@ -1628,22 +1716,66 @@ async function withFileLock(targetPath, fn, opts = {}) {
1628
1716
  } catch {
1629
1717
  }
1630
1718
  try {
1631
- await fs.unlink(lockPath);
1719
+ await fs2.unlink(lockPath);
1632
1720
  } catch {
1633
1721
  }
1634
1722
  }
1635
1723
  }
1724
+ async function waitForLockRelease(lockPath, remainingMs) {
1725
+ const parentDir = path3.dirname(lockPath);
1726
+ const lockName = path3.basename(lockPath);
1727
+ const intervalMs = Math.min(remainingMs, 100);
1728
+ return new Promise((resolve4) => {
1729
+ let settled = false;
1730
+ let watcher = null;
1731
+ const timer = setTimeout(() => {
1732
+ settled = true;
1733
+ watcher?.close();
1734
+ resolve4();
1735
+ }, intervalMs);
1736
+ try {
1737
+ watcher = watchDir(parentDir, (eventType, filename) => {
1738
+ if (settled) return;
1739
+ if (filename === lockName && (eventType === "rename" || eventType === "change")) {
1740
+ settled = true;
1741
+ clearTimeout(timer);
1742
+ watcher?.close();
1743
+ resolve4();
1744
+ }
1745
+ });
1746
+ } catch {
1747
+ clearTimeout(timer);
1748
+ if (!settled) {
1749
+ settled = true;
1750
+ setTimeout(resolve4, Math.min(remainingMs, 25));
1751
+ }
1752
+ return;
1753
+ }
1754
+ fs2.access(lockPath).then(
1755
+ () => {
1756
+ },
1757
+ () => {
1758
+ if (!settled) {
1759
+ settled = true;
1760
+ clearTimeout(timer);
1761
+ watcher?.close();
1762
+ resolve4();
1763
+ }
1764
+ }
1765
+ );
1766
+ });
1767
+ }
1636
1768
  var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
1637
1769
  async function renameWithRetry(from, to) {
1638
1770
  if (process.platform !== "win32") {
1639
- await fs.rename(from, to);
1771
+ await fs2.rename(from, to);
1640
1772
  return;
1641
1773
  }
1642
1774
  const delays = [10, 25, 60, 120, 250];
1643
1775
  let lastErr;
1644
1776
  for (let i = 0; i <= delays.length; i++) {
1645
1777
  try {
1646
- await fs.rename(from, to);
1778
+ await fs2.rename(from, to);
1647
1779
  return;
1648
1780
  } catch (err) {
1649
1781
  lastErr = err;
@@ -1657,6 +1789,181 @@ async function renameWithRetry(from, to) {
1657
1789
  throw lastErr;
1658
1790
  }
1659
1791
 
1792
+ // src/coordination/mailbox-constants.ts
1793
+ var AGENT_STALE_MS = 6e4;
1794
+ var CLIENT_STALE_MS = 6e4;
1795
+ var HEARTBEAT_THROTTLE_MS = 5e3;
1796
+ var REGISTRY_CACHE_TTL_MS = 2e3;
1797
+ var LINE_SEPARATOR = "\n";
1798
+ var MESSAGE_CACHE_MAX_ENTRIES = 1e4;
1799
+ var AUTO_COMPACT_INTERVAL_MS = 3e5;
1800
+ var AUTO_COMPACT_READ_MAX_AGE_MS = 6e5;
1801
+ var AUTO_COMPACT_DEFAULT_TTL_MS = 864e5;
1802
+
1803
+ // src/coordination/mailbox-message-codec.ts
1804
+ var MESSAGE_TYPES = /* @__PURE__ */ new Set([
1805
+ "note",
1806
+ "ask",
1807
+ "assign",
1808
+ "steer",
1809
+ "btw",
1810
+ "broadcast",
1811
+ "status",
1812
+ "result",
1813
+ "review",
1814
+ "control"
1815
+ ]);
1816
+ var PRIORITIES = /* @__PURE__ */ new Set(["low", "normal", "high"]);
1817
+ var TASK_STATUSES = /* @__PURE__ */ new Set([
1818
+ "pending",
1819
+ "in_progress",
1820
+ "completed",
1821
+ "failed",
1822
+ "idle",
1823
+ "running",
1824
+ "streaming",
1825
+ "waiting_user",
1826
+ "error",
1827
+ "offline",
1828
+ "busy"
1829
+ ]);
1830
+ function isRecord(value) {
1831
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1832
+ }
1833
+ function requiredString(record, key) {
1834
+ const value = record[key];
1835
+ if (typeof value !== "string") {
1836
+ throw new TypeError(`mailbox message field "${key}" must be a string`);
1837
+ }
1838
+ return value;
1839
+ }
1840
+ function optionalString(record, key) {
1841
+ const value = record[key];
1842
+ if (value === void 0) return {};
1843
+ if (typeof value !== "string") {
1844
+ throw new TypeError(`mailbox message field "${key}" must be a string when present`);
1845
+ }
1846
+ return { [key]: value };
1847
+ }
1848
+ function parseMessageType(value) {
1849
+ if (value === "info") return "note";
1850
+ if (value === "task") return "assign";
1851
+ if (typeof value === "string" && MESSAGE_TYPES.has(value)) {
1852
+ return value;
1853
+ }
1854
+ throw new TypeError('mailbox message field "type" is invalid');
1855
+ }
1856
+ function normalizeMailboxMessageType(value) {
1857
+ return parseMessageType(value);
1858
+ }
1859
+ function parsePriority(value) {
1860
+ if (typeof value === "string" && PRIORITIES.has(value)) {
1861
+ return value;
1862
+ }
1863
+ if (typeof value === "string") return "normal";
1864
+ throw new TypeError('mailbox message field "priority" must be a string');
1865
+ }
1866
+ function parseReadReceipts(record, to) {
1867
+ const value = record["readBy"];
1868
+ if (value === void 0) {
1869
+ const legacyReadAt = record["readAt"];
1870
+ return record["read"] === true && typeof legacyReadAt === "string" ? { [to || "unknown"]: legacyReadAt } : {};
1871
+ }
1872
+ if (!isRecord(value)) {
1873
+ throw new TypeError('mailbox message field "readBy" must be an object');
1874
+ }
1875
+ const receipts = {};
1876
+ for (const [agentId, timestamp] of Object.entries(value)) {
1877
+ if (typeof timestamp !== "string") {
1878
+ throw new TypeError("mailbox message read receipt timestamps must be strings");
1879
+ }
1880
+ receipts[agentId] = timestamp;
1881
+ }
1882
+ return receipts;
1883
+ }
1884
+ function parseTaskContext(value) {
1885
+ if (value === void 0) return void 0;
1886
+ if (!isRecord(value)) {
1887
+ throw new TypeError('mailbox message field "taskContext" must be an object');
1888
+ }
1889
+ const status = value["status"];
1890
+ if (status !== void 0 && (typeof status !== "string" || !TASK_STATUSES.has(status))) {
1891
+ throw new TypeError("mailbox message taskContext status is invalid");
1892
+ }
1893
+ return {
1894
+ ...optionalString(value, "agentRole"),
1895
+ ...optionalString(value, "agentName"),
1896
+ ...optionalString(value, "taskId"),
1897
+ ...status === void 0 ? {} : { status }
1898
+ };
1899
+ }
1900
+ function parseMailboxMessage(value) {
1901
+ if (!isRecord(value)) throw new TypeError("mailbox message must be an object");
1902
+ const to = value["to"] === void 0 ? "" : requiredString(value, "to");
1903
+ const completed = value["completed"];
1904
+ if (typeof completed !== "boolean") {
1905
+ throw new TypeError('mailbox message field "completed" must be a boolean');
1906
+ }
1907
+ const taskContext = parseTaskContext(value["taskContext"]);
1908
+ return {
1909
+ id: requiredString(value, "id"),
1910
+ from: requiredString(value, "from"),
1911
+ to,
1912
+ type: parseMessageType(value["type"]),
1913
+ subject: requiredString(value, "subject"),
1914
+ body: requiredString(value, "body"),
1915
+ priority: parsePriority(value["priority"]),
1916
+ readBy: parseReadReceipts(value, to),
1917
+ completed,
1918
+ timestamp: requiredString(value, "timestamp"),
1919
+ ...optionalString(value, "completedBy"),
1920
+ ...optionalString(value, "outcome"),
1921
+ ...optionalString(value, "completedAt"),
1922
+ ...optionalString(value, "deletedAt"),
1923
+ ...optionalString(value, "deletedBy"),
1924
+ ...optionalString(value, "replyTo"),
1925
+ ...optionalString(value, "senderSessionId"),
1926
+ ...optionalString(value, "expiresAt"),
1927
+ ...taskContext === void 0 ? {} : { taskContext }
1928
+ };
1929
+ }
1930
+ function isAckRecord(value) {
1931
+ return typeof value === "object" && value !== null && !Array.isArray(value) && value["__ack"] === true;
1932
+ }
1933
+ function parseMailboxLine(line) {
1934
+ try {
1935
+ const parsed = JSON.parse(line);
1936
+ if (isAckRecord(parsed)) return parsed;
1937
+ return parseMailboxMessage(parsed);
1938
+ } catch {
1939
+ return null;
1940
+ }
1941
+ }
1942
+ function applyAckToMessage(msg, ack) {
1943
+ if (ack.read && !(ack.readerId in msg.readBy)) {
1944
+ msg.readBy[ack.readerId] = ack.timestamp;
1945
+ }
1946
+ if (ack.completed && !msg.completed) {
1947
+ msg.completed = true;
1948
+ msg.completedBy = ack.completedBy ?? ack.readerId;
1949
+ msg.completedAt = ack.timestamp;
1950
+ }
1951
+ if (ack.outcome !== void 0 && msg.outcome !== ack.outcome) {
1952
+ msg.outcome = ack.outcome;
1953
+ }
1954
+ if (ack.deleted === true) {
1955
+ msg.deletedAt = ack.timestamp;
1956
+ msg.deletedBy = ack.deletedBy ?? ack.readerId;
1957
+ }
1958
+ if (ack.deleted === false) {
1959
+ delete msg.deletedAt;
1960
+ delete msg.deletedBy;
1961
+ }
1962
+ }
1963
+ function serializeAckRecord(ack) {
1964
+ return JSON.stringify(ack) + "\n";
1965
+ }
1966
+
1660
1967
  // src/coordination/mailbox-types.ts
1661
1968
  function normalizeRecipient(to) {
1662
1969
  return to.trim().toLowerCase() === "all" ? "*" : to.trim();
@@ -1665,13 +1972,6 @@ function normalizeRecipient(to) {
1665
1972
  // src/coordination/global-mailbox.ts
1666
1973
  var MAILBOX_FILE = "_mailbox.jsonl";
1667
1974
  var CLIENT_REGISTRY_FILE = "_mailbox.clients.json";
1668
- var AGENT_STALE_MS = 6e4;
1669
- var AGENT_PURGE_MS = 864e5;
1670
- var CLIENT_STALE_MS = 6e4;
1671
- var HEARTBEAT_THROTTLE_MS = 5e3;
1672
- var REGISTRY_CACHE_TTL_MS = 2e3;
1673
- var LINE_SEPARATOR = "\n";
1674
- var MESSAGE_CACHE_MAX_ENTRIES = 1e4;
1675
1975
  var GlobalMailbox = class {
1676
1976
  /** Path to the JSONL message file. */
1677
1977
  messagePath;
@@ -1683,6 +1983,12 @@ var GlobalMailbox = class {
1683
1983
  _events;
1684
1984
  /** Optional HQ publisher for cross-project command-center telemetry. */
1685
1985
  _hqPublisher;
1986
+ /**
1987
+ * Optional SSE event emitter for real-time push to HTTP bridge clients.
1988
+ * When present, `send`/`ack`/`softDelete`/`restore` emit events that
1989
+ * connected SSE subscribers receive instantly.
1990
+ */
1991
+ eventEmitter;
1686
1992
  /**
1687
1993
  * Local cache of the agent registry to avoid re-reading on every call.
1688
1994
  * Time-bounded: the registry file is shared ACROSS PROCESSES (that's the
@@ -1725,20 +2031,47 @@ var GlobalMailbox = class {
1725
2031
  * appended message). The chain runs each read to completion before
1726
2032
  * the next starts, in issue order — readers are read-only and never
1727
2033
  * conflict with each other on content, only on the cache mutation
1728
- * that follows the read. Pattern mirrors `DefaultMemoryStore.runSerialized`.
2034
+ * that follows the read. Pattern mirrors a serialized mutation chain.
1729
2035
  */
1730
2036
  _readChain = Promise.resolve([]);
2037
+ /**
2038
+ * Recipient → Set of indices into `_messageCache`. Maintained alongside
2039
+ * the cache so `query({ to })` and `unreadCount(agentId)` can skip the
2040
+ * O(N) full scan and iterate only the messages addressed to the target
2041
+ * recipient (plus broadcasts `*`).
2042
+ *
2043
+ * The index is rebuilt whenever the cache array is replaced
2044
+ * (`_setMessageCache`), extended when new messages are pushed
2045
+ * (`_pushToCache`), and cleared on `close()`.
2046
+ * It is `null` when the cache itself is null (> MESSAGE_CACHE_MAX_ENTRIES
2047
+ * or never populated).
2048
+ *
2049
+ * Safety: array indices are valid only as long as the cache array
2050
+ * reference hasn't changed. Since we rebuild the index in the same
2051
+ * synchronous step that replaces the cache, a reader that calls
2052
+ * `_readMessagesCached()` either gets the old array + old index, or
2053
+ * the new array + new index — never a mismatch.
2054
+ */
2055
+ _recipientIndex = null;
2056
+ /**
2057
+ * Sender → Set of indices into `_messageCache`. Same lifecycle and
2058
+ * safety properties as `_recipientIndex`, but keyed on `msg.from`
2059
+ * so `query({ from })` can skip the full scan.
2060
+ */
2061
+ _senderIndex = null;
1731
2062
  /**
1732
2063
  * @param projectDir — `~/.wrongstack/projects/<slug>/`
1733
2064
  * @param events — optional EventBus for real-time TUI/WebUI notifications
1734
2065
  * @param hqPublisher — optional HQ publisher, or getter, for cross-project telemetry
2066
+ * @param eventEmitter — optional SSE event emitter for HTTP bridge push
1735
2067
  */
1736
- constructor(projectDir, events, hqPublisher) {
2068
+ constructor(projectDir, events, hqPublisher, eventEmitter) {
1737
2069
  this.messagePath = path4.join(projectDir, MAILBOX_FILE);
1738
2070
  this.registryPath = path4.join(projectDir, "_mailbox.registry.json");
1739
2071
  this.clientRegistryPath = path4.join(projectDir, CLIENT_REGISTRY_FILE);
1740
2072
  this._events = events;
1741
2073
  this._hqPublisher = hqPublisher;
2074
+ this.eventEmitter = eventEmitter;
1742
2075
  }
1743
2076
  get hqMailboxId() {
1744
2077
  return `${path4.basename(path4.dirname(this.messagePath))}:mailbox`;
@@ -1767,7 +2100,7 @@ var GlobalMailbox = class {
1767
2100
  // "all" is an accepted spelling of the broadcast address — canonical
1768
2101
  // form on disk is '*' so every query/checker matches it.
1769
2102
  to: normalizeRecipient(input.to),
1770
- type: input.type,
2103
+ type: normalizeMailboxMessageType(input.type),
1771
2104
  subject: input.subject,
1772
2105
  body: input.body,
1773
2106
  priority: input.priority ?? "normal",
@@ -1775,7 +2108,8 @@ var GlobalMailbox = class {
1775
2108
  completed: false,
1776
2109
  timestamp: now,
1777
2110
  replyTo: input.replyTo,
1778
- taskContext: input.taskContext
2111
+ taskContext: input.taskContext,
2112
+ expiresAt: input.ttlMs !== void 0 ? new Date(Date.now() + input.ttlMs).toISOString() : void 0
1779
2113
  };
1780
2114
  const line = JSON.stringify(msg) + LINE_SEPARATOR;
1781
2115
  await fsp.mkdir(path4.dirname(this.messagePath), { recursive: true });
@@ -1790,21 +2124,49 @@ var GlobalMailbox = class {
1790
2124
  message: msg
1791
2125
  });
1792
2126
  this.publishHqMailboxSnapshot();
2127
+ this._events?.emitCustom("mailbox.message_sent", {
2128
+ messageId: msg.id,
2129
+ from: msg.from,
2130
+ to: msg.to,
2131
+ type: msg.type,
2132
+ subject: msg.subject
2133
+ });
2134
+ this.eventEmitter?.emit({
2135
+ type: "message.sent",
2136
+ messageId: msg.id,
2137
+ from: msg.from,
2138
+ to: msg.to,
2139
+ timestamp: now
2140
+ });
1793
2141
  return msg;
1794
2142
  }
1795
2143
  async query(q) {
2144
+ const queryType = q.type === void 0 ? void 0 : normalizeMailboxMessageType(q.type);
1796
2145
  const all = await this._readMessagesCached();
1797
2146
  const limit = q.limit ?? 50;
1798
2147
  const order = q.minPriority !== void 0 ? { low: 0, normal: 1, high: 2 } : null;
1799
2148
  const minPriorityRank = order && q.minPriority !== void 0 ? order[q.minPriority] : 0;
1800
2149
  const out = [];
1801
- for (let i = 0; i < all.length; i++) {
1802
- const m = all[i];
2150
+ let candidates;
2151
+ if (q.to !== void 0 && this._recipientIndex !== null && q.from === void 0 && q.type === void 0 && q.minPriority === void 0) {
2152
+ const indices = /* @__PURE__ */ new Set();
2153
+ const direct = this._recipientIndex.get(q.to);
2154
+ if (direct !== void 0) for (const i of direct) indices.add(i);
2155
+ const broadcasts = this._recipientIndex.get("*");
2156
+ if (broadcasts !== void 0) for (const i of broadcasts) indices.add(i);
2157
+ candidates = [...indices].sort((a, b) => a - b).map((i) => all[i]);
2158
+ } else if (q.from !== void 0 && q.to === void 0 && this._senderIndex !== null && q.type === void 0 && q.minPriority === void 0) {
2159
+ const indices = this._senderIndex.get(q.from);
2160
+ candidates = indices !== void 0 ? [...indices].sort((a, b) => a - b).map((i) => all[i]) : [];
2161
+ } else {
2162
+ candidates = all;
2163
+ }
2164
+ for (const m of candidates) {
1803
2165
  if (q.to !== void 0 && m.to !== q.to && m.to !== "*") continue;
1804
2166
  if (q.from !== void 0 && m.from !== q.from) continue;
1805
2167
  if (q.unreadBy !== void 0 && q.unreadBy in m.readBy) continue;
1806
2168
  if (q.incompleteOnly && m.completed) continue;
1807
- if (q.type !== void 0 && m.type !== q.type) continue;
2169
+ if (queryType !== void 0 && m.type !== queryType) continue;
1808
2170
  if (order !== null && (order[m.priority] ?? 1) < minPriorityRank) {
1809
2171
  continue;
1810
2172
  }
@@ -1819,42 +2181,100 @@ var GlobalMailbox = class {
1819
2181
  const updated = await this.ackMany({ acks: [input] });
1820
2182
  return updated.length > 0 ? updated[0] : null;
1821
2183
  }
2184
+ /** Diagnostic counters exposed for test/tool introspection. */
2185
+ diag = {
2186
+ ackManyCacheDesync: 0,
2187
+ ackManyPreLockMtime: 0,
2188
+ ackManyPostLockMtime: 0,
2189
+ ackManyPreLockSize: 0,
2190
+ ackManyPostLockSize: 0,
2191
+ ackManyMessageCount: 0,
2192
+ ackManyAckCount: 0,
2193
+ cacheHitCount: 0,
2194
+ cacheMissCount: 0,
2195
+ cacheDesyncNoOp: 0,
2196
+ applyAckTargetMissing: 0,
2197
+ setMessageCacheCount: 0,
2198
+ pushToCacheCount: 0
2199
+ };
1822
2200
  async ackMany(input) {
1823
2201
  if (input.acks.length === 0) return [];
2202
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2203
+ const ackRecords = [];
1824
2204
  const updated = [];
2205
+ const targetIds = /* @__PURE__ */ new Set();
1825
2206
  const byId = /* @__PURE__ */ new Map();
1826
2207
  for (const a of input.acks) {
1827
2208
  byId.set(a.messageId, a);
2209
+ targetIds.add(a.messageId);
1828
2210
  }
1829
- await withFileLock(this.messagePath, async () => {
1830
- const all = await this._readMessagesFresh();
1831
- const now = (/* @__PURE__ */ new Date()).toISOString();
1832
- let changed = false;
1833
- for (const msg of all) {
1834
- const a = byId.get(msg.id);
1835
- if (!a) continue;
1836
- updated.push(msg);
1837
- if (a.read !== false && !(a.readerId in msg.readBy)) {
1838
- msg.readBy[a.readerId] = now;
1839
- changed = true;
1840
- }
1841
- if (a.completed && !msg.completed) {
1842
- msg.completed = true;
1843
- msg.completedBy = a.readerId;
1844
- msg.completedAt = now;
1845
- changed = true;
1846
- }
1847
- if (a.outcome !== void 0 && msg.outcome !== a.outcome) {
1848
- msg.outcome = a.outcome;
1849
- changed = true;
1850
- }
2211
+ const preLockMtime = this._messageCacheMtime;
2212
+ const preLockSize = this._messageCacheSize;
2213
+ this.diag.ackManyPreLockMtime = preLockMtime;
2214
+ this.diag.ackManyPreLockSize = preLockSize;
2215
+ const all = await this._readMessagesCached();
2216
+ for (const msg of all) {
2217
+ const a = byId.get(msg.id);
2218
+ if (!a) continue;
2219
+ const needsRead = a.read !== false && !(a.readerId in msg.readBy);
2220
+ const needsComplete = a.completed && !msg.completed;
2221
+ const needsOutcome = a.outcome !== void 0 && msg.outcome !== a.outcome;
2222
+ if (!needsRead && !needsComplete && !needsOutcome) {
2223
+ updated.push({ ...msg, readBy: { ...msg.readBy } });
2224
+ continue;
1851
2225
  }
1852
- if (changed) {
1853
- const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
1854
- await fsp.writeFile(this.messagePath, serialized, "utf8");
2226
+ const copy = { ...msg, readBy: { ...msg.readBy } };
2227
+ if (needsRead) copy.readBy[a.readerId] = now;
2228
+ if (needsComplete) {
2229
+ copy.completed = true;
2230
+ copy.completedBy = a.readerId;
2231
+ copy.completedAt = now;
1855
2232
  }
2233
+ if (needsOutcome) copy.outcome = a.outcome;
2234
+ updated.push(copy);
2235
+ ackRecords.push({
2236
+ __ack: true,
2237
+ messageId: msg.id,
2238
+ readerId: a.readerId,
2239
+ timestamp: now,
2240
+ read: a.read !== false,
2241
+ completed: a.completed,
2242
+ completedBy: a.completed ? a.readerId : void 0,
2243
+ outcome: a.outcome
2244
+ });
2245
+ }
2246
+ if (ackRecords.length === 0) {
2247
+ if (updated.length > 0) {
2248
+ for (const message of updated) {
2249
+ this.publishHqMailboxEvent({
2250
+ mailboxId: this.hqMailboxId,
2251
+ action: message.completed ? "message.completed" : "message.read",
2252
+ message
2253
+ });
2254
+ }
2255
+ }
2256
+ return updated;
2257
+ }
2258
+ const serialized = ackRecords.map((a) => serializeAckRecord(a)).join("");
2259
+ await withFileLock(this.messagePath, async () => {
2260
+ await fsp.appendFile(this.messagePath, serialized, "utf8");
1856
2261
  const { mtimeMs, size } = await this._statMessageFile();
1857
- this._setMessageCache(all, mtimeMs, size);
2262
+ for (const ack of ackRecords) {
2263
+ this._applyAckToCache(ack);
2264
+ }
2265
+ this._messageCacheMtime = mtimeMs;
2266
+ this._messageCacheSize = size;
2267
+ this.diag.ackManyPostLockMtime = mtimeMs;
2268
+ this.diag.ackManyPostLockSize = size;
2269
+ this.diag.ackManyMessageCount = this._messageCache?.length ?? -1;
2270
+ this.diag.ackManyAckCount = ackRecords.length;
2271
+ const expectedMinSize = preLockSize + serialized.length;
2272
+ if (preLockSize > 0 && size > expectedMinSize) {
2273
+ const extraBytes = size - expectedMinSize;
2274
+ console.error(
2275
+ `[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}`
2276
+ );
2277
+ }
1858
2278
  });
1859
2279
  for (const message of updated) {
1860
2280
  this.publishHqMailboxEvent({
@@ -1862,6 +2282,13 @@ var GlobalMailbox = class {
1862
2282
  action: message.completed ? "message.completed" : "message.read",
1863
2283
  message
1864
2284
  });
2285
+ this.eventEmitter?.emit({
2286
+ type: "message.acked",
2287
+ messageId: message.id,
2288
+ from: message.from,
2289
+ to: message.to,
2290
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2291
+ });
1865
2292
  }
1866
2293
  if (updated.length > 0) this.publishHqMailboxSnapshot();
1867
2294
  return updated;
@@ -1869,68 +2296,108 @@ var GlobalMailbox = class {
1869
2296
  async unreadCount(forAgentId) {
1870
2297
  const all = await this._readMessagesCached();
1871
2298
  let count = 0;
1872
- for (let i = 0; i < all.length; i++) {
1873
- const m = all[i];
1874
- if ((m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed) {
1875
- count++;
2299
+ if (this._recipientIndex !== null) {
2300
+ const indices = /* @__PURE__ */ new Set();
2301
+ const direct = this._recipientIndex.get(forAgentId);
2302
+ if (direct !== void 0) for (const i of direct) indices.add(i);
2303
+ const broadcasts = this._recipientIndex.get("*");
2304
+ if (broadcasts !== void 0) for (const i of broadcasts) indices.add(i);
2305
+ for (const i of indices) {
2306
+ const m = all[i];
2307
+ if (!(forAgentId in m.readBy) && !m.completed) count++;
2308
+ }
2309
+ } else {
2310
+ for (let i = 0; i < all.length; i++) {
2311
+ const m = all[i];
2312
+ if ((m.to === forAgentId || m.to === "*") && !(forAgentId in m.readBy) && !m.completed) {
2313
+ count++;
2314
+ }
1876
2315
  }
1877
2316
  }
1878
2317
  return count;
1879
2318
  }
1880
2319
  async softDelete(mailId, by) {
1881
- let updated = null;
2320
+ const all = await this._readMessagesCached();
2321
+ const target = all.find((m) => m.id === mailId);
2322
+ if (target === void 0) return null;
2323
+ if (target.deletedAt !== void 0) {
2324
+ return { ...target, readBy: { ...target.readBy } };
2325
+ }
2326
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2327
+ const ack = {
2328
+ __ack: true,
2329
+ messageId: mailId,
2330
+ readerId: by,
2331
+ timestamp: now,
2332
+ read: true,
2333
+ deleted: true,
2334
+ deletedBy: by
2335
+ };
1882
2336
  await withFileLock(this.messagePath, async () => {
1883
- const all = await this._readMessagesFresh();
1884
- const now = (/* @__PURE__ */ new Date()).toISOString();
1885
- for (const m of all) {
1886
- if (m.id !== mailId) continue;
1887
- if (m.deletedAt !== void 0) {
1888
- updated = m;
1889
- continue;
1890
- }
1891
- m.deletedAt = now;
1892
- m.deletedBy = by;
1893
- updated = m;
1894
- }
1895
- const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
1896
- await fsp.writeFile(this.messagePath, serialized, "utf8");
2337
+ await fsp.appendFile(this.messagePath, serializeAckRecord(ack), "utf8");
1897
2338
  const { mtimeMs, size } = await this._statMessageFile();
1898
- this._setMessageCache(all, mtimeMs, size);
2339
+ this._applyAckToCache(ack);
2340
+ this._messageCacheMtime = mtimeMs;
2341
+ this._messageCacheSize = size;
1899
2342
  });
1900
- if (updated !== null) {
1901
- this.publishHqMailboxEvent({
1902
- mailboxId: this.hqMailboxId,
1903
- action: "message.updated",
1904
- message: updated
1905
- });
1906
- this.publishHqMailboxSnapshot();
1907
- }
1908
- return updated;
2343
+ const msg = { ...target, readBy: { ...target.readBy } };
2344
+ msg.deletedAt = now;
2345
+ msg.deletedBy = by;
2346
+ this.publishHqMailboxEvent({
2347
+ mailboxId: this.hqMailboxId,
2348
+ action: "message.updated",
2349
+ message: msg
2350
+ });
2351
+ this.publishHqMailboxSnapshot();
2352
+ this.eventEmitter?.emit({
2353
+ type: "message.deleted",
2354
+ messageId: msg.id,
2355
+ from: msg.from,
2356
+ to: msg.to,
2357
+ timestamp: msg.deletedAt ?? now
2358
+ });
2359
+ return msg;
1909
2360
  }
1910
2361
  async restore(mailId) {
1911
- let updated = null;
2362
+ const all = await this._readMessagesCached();
2363
+ const target = all.find((m) => m.id === mailId);
2364
+ if (target === void 0) return null;
2365
+ if (target.deletedAt === void 0 && target.deletedBy === void 0) {
2366
+ return { ...target, readBy: { ...target.readBy } };
2367
+ }
2368
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2369
+ const ack = {
2370
+ __ack: true,
2371
+ messageId: mailId,
2372
+ readerId: "",
2373
+ timestamp: now,
2374
+ read: true,
2375
+ deleted: false
2376
+ };
1912
2377
  await withFileLock(this.messagePath, async () => {
1913
- const all = await this._readMessagesFresh();
1914
- for (const m of all) {
1915
- if (m.id !== mailId) continue;
1916
- delete m.deletedAt;
1917
- delete m.deletedBy;
1918
- updated = m;
1919
- }
1920
- const serialized = all.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
1921
- await fsp.writeFile(this.messagePath, serialized, "utf8");
2378
+ await fsp.appendFile(this.messagePath, serializeAckRecord(ack), "utf8");
1922
2379
  const { mtimeMs, size } = await this._statMessageFile();
1923
- this._setMessageCache(all, mtimeMs, size);
2380
+ this._applyAckToCache(ack);
2381
+ this._messageCacheMtime = mtimeMs;
2382
+ this._messageCacheSize = size;
1924
2383
  });
1925
- if (updated !== null) {
1926
- this.publishHqMailboxEvent({
1927
- mailboxId: this.hqMailboxId,
1928
- action: "message.updated",
1929
- message: updated
1930
- });
1931
- this.publishHqMailboxSnapshot();
1932
- }
1933
- return updated;
2384
+ const msg = { ...target, readBy: { ...target.readBy } };
2385
+ delete msg.deletedAt;
2386
+ delete msg.deletedBy;
2387
+ this.publishHqMailboxEvent({
2388
+ mailboxId: this.hqMailboxId,
2389
+ action: "message.updated",
2390
+ message: msg
2391
+ });
2392
+ this.publishHqMailboxSnapshot();
2393
+ this.eventEmitter?.emit({
2394
+ type: "message.restored",
2395
+ messageId: msg.id,
2396
+ from: msg.from,
2397
+ to: msg.to,
2398
+ timestamp: now
2399
+ });
2400
+ return msg;
1934
2401
  }
1935
2402
  // ── Agent registry ──────────────────────────────────────────────────────
1936
2403
  async registerAgent(input) {
@@ -2061,8 +2528,19 @@ var GlobalMailbox = class {
2061
2528
  }
2062
2529
  async getAgentStatuses() {
2063
2530
  await this._ensureRegistry();
2064
- const registry = await this._readRegistry();
2531
+ let registry = await this._readRegistry();
2532
+ const before = registry.size;
2065
2533
  this._pruneStaleInPlace(registry);
2534
+ if (registry.size < before) {
2535
+ await withFileLock(this.registryPath, async () => {
2536
+ const fresh = await this._readRegistry({ fresh: true });
2537
+ this._pruneStaleInPlace(fresh);
2538
+ this._registryCache = fresh;
2539
+ this._registryCacheAt = Date.now();
2540
+ await this._writeRegistry(fresh);
2541
+ registry = fresh;
2542
+ });
2543
+ }
2066
2544
  const now = Date.now();
2067
2545
  return Array.from(registry.values()).map((a) => ({
2068
2546
  agentId: a.agentId,
@@ -2114,6 +2592,20 @@ var GlobalMailbox = class {
2114
2592
  });
2115
2593
  this.publishHqMailboxSnapshot();
2116
2594
  }
2595
+ async deregisterClient(clientId) {
2596
+ await this._ensureClientRegistry();
2597
+ await withFileLock(this.clientRegistryPath, async () => {
2598
+ const registry = await this._readClientRegistry({ fresh: true });
2599
+ this._pruneStaleClientsInPlace(registry);
2600
+ registry.delete(clientId);
2601
+ this._clientRegistryCache = registry;
2602
+ this._clientRegistryCacheAt = Date.now();
2603
+ await this._writeClientRegistry(registry);
2604
+ });
2605
+ this._lastClientHeartbeat.delete(clientId);
2606
+ this._events?.emitCustom("mailbox.client_deregistered", { clientId });
2607
+ this.publishHqMailboxSnapshot();
2608
+ }
2117
2609
  async clientHeartbeat(input) {
2118
2610
  const last = this._lastClientHeartbeat.get(input.clientId) ?? 0;
2119
2611
  const now = Date.now();
@@ -2142,8 +2634,19 @@ var GlobalMailbox = class {
2142
2634
  }
2143
2635
  async getClientStatuses() {
2144
2636
  await this._ensureClientRegistry();
2145
- const registry = await this._readClientRegistry();
2637
+ let registry = await this._readClientRegistry();
2638
+ const before = registry.size;
2146
2639
  this._pruneStaleClientsInPlace(registry);
2640
+ if (registry.size < before) {
2641
+ await withFileLock(this.clientRegistryPath, async () => {
2642
+ registry = await this._readClientRegistry({ fresh: true });
2643
+ const postLockBefore = registry.size;
2644
+ this._pruneStaleClientsInPlace(registry);
2645
+ if (registry.size < postLockBefore) {
2646
+ await this._writeClientRegistry(registry);
2647
+ }
2648
+ });
2649
+ }
2147
2650
  const now = Date.now();
2148
2651
  return Array.from(registry.values()).map((c) => ({
2149
2652
  clientId: c.clientId,
@@ -2155,6 +2658,32 @@ var GlobalMailbox = class {
2155
2658
  pid: c.pid
2156
2659
  })).sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt));
2157
2660
  }
2661
+ /**
2662
+ * Explicitly purge stale clients from the registry and write back to disk.
2663
+ * Removes client entries whose lastSeenAt is older than CLIENT_STALE_MS.
2664
+ * Returns the number of entries purged.
2665
+ *
2666
+ * Idempotent — safe to call regularly. The same pruning runs implicitly
2667
+ * inside registerClient, clientHeartbeat, and getClientStatuses, but if
2668
+ * no client registers or heartbeats for a long time, this public method
2669
+ * ensures the file gets cleaned up on demand.
2670
+ */
2671
+ async purgeClients() {
2672
+ await this._ensureClientRegistry();
2673
+ let purged = 0;
2674
+ await withFileLock(this.clientRegistryPath, async () => {
2675
+ const registry = await this._readClientRegistry({ fresh: true });
2676
+ const before = registry.size;
2677
+ this._pruneStaleClientsInPlace(registry);
2678
+ purged = before - registry.size;
2679
+ if (purged > 0) {
2680
+ this._clientRegistryCache = registry;
2681
+ this._clientRegistryCacheAt = Date.now();
2682
+ await this._writeClientRegistry(registry);
2683
+ }
2684
+ });
2685
+ return purged;
2686
+ }
2158
2687
  // ── Lifecycle ───────────────────────────────────────────────────────────
2159
2688
  async close() {
2160
2689
  this._registryCache = null;
@@ -2162,6 +2691,8 @@ var GlobalMailbox = class {
2162
2691
  this._messageCache = null;
2163
2692
  this._messageCacheMtime = -1;
2164
2693
  this._messageCacheSize = -1;
2694
+ this._recipientIndex = null;
2695
+ this._senderIndex = null;
2165
2696
  }
2166
2697
  async clearAll() {
2167
2698
  await withFileLock(this.messagePath, async () => {
@@ -2210,6 +2741,100 @@ var GlobalMailbox = class {
2210
2741
  remaining
2211
2742
  };
2212
2743
  }
2744
+ // ── Auto-compaction ─────────────────────────────────────────────────────
2745
+ _autoCompactTimer = null;
2746
+ async autoCompact(opts) {
2747
+ const readMaxAgeMs = opts?.readMaxAgeMs ?? AUTO_COMPACT_READ_MAX_AGE_MS;
2748
+ const defaultTtlMs = opts?.defaultTtlMs ?? AUTO_COMPACT_DEFAULT_TTL_MS;
2749
+ const completedMaxAgeMs = opts?.completedMaxAgeMs ?? 864e5;
2750
+ const incompleteMaxAgeMs = opts?.incompleteMaxAgeMs ?? 6048e5;
2751
+ let expiredPurged = 0;
2752
+ let readByAllPurged = 0;
2753
+ let completedPurged = 0;
2754
+ let incompletePurged = 0;
2755
+ let remaining = 0;
2756
+ let onlineAgentIds = null;
2757
+ try {
2758
+ const statuses = await this.getAgentStatuses();
2759
+ const online = statuses.filter((s) => s.online);
2760
+ onlineAgentIds = online.length > 0 ? new Set(online.map((s) => s.agentId)) : null;
2761
+ } catch {
2762
+ onlineAgentIds = null;
2763
+ }
2764
+ await withFileLock(this.messagePath, async () => {
2765
+ const all = await this._readMessagesFresh();
2766
+ const now = Date.now();
2767
+ const cutoffReadAge = now - readMaxAgeMs;
2768
+ const cutoffCompleted = now - completedMaxAgeMs;
2769
+ const cutoffIncomplete = now - incompleteMaxAgeMs;
2770
+ const cutoffDefaultTtl = now - defaultTtlMs;
2771
+ const kept = [];
2772
+ for (const msg of all) {
2773
+ const msgTime = new Date(msg.timestamp).getTime();
2774
+ if (msg.expiresAt !== void 0) {
2775
+ if (new Date(msg.expiresAt).getTime() < now) {
2776
+ expiredPurged++;
2777
+ continue;
2778
+ }
2779
+ } else if (msgTime < cutoffDefaultTtl) {
2780
+ expiredPurged++;
2781
+ continue;
2782
+ }
2783
+ if (onlineAgentIds !== null && !msg.completed) {
2784
+ const readByAll = onlineAgentIds.size > 0 && [...onlineAgentIds].every((id) => id in msg.readBy);
2785
+ if (readByAll) {
2786
+ const readTimes = Object.values(msg.readBy).map((t) => new Date(t).getTime());
2787
+ const latestRead = Math.max(...readTimes);
2788
+ if (latestRead < cutoffReadAge) {
2789
+ readByAllPurged++;
2790
+ continue;
2791
+ }
2792
+ }
2793
+ }
2794
+ const completedTime = msg.completedAt ? new Date(msg.completedAt).getTime() : 0;
2795
+ if (msg.completed && completedTime < cutoffCompleted) {
2796
+ completedPurged++;
2797
+ continue;
2798
+ }
2799
+ if (!msg.completed && msgTime < cutoffIncomplete) {
2800
+ incompletePurged++;
2801
+ continue;
2802
+ }
2803
+ kept.push(msg);
2804
+ }
2805
+ remaining = kept.length;
2806
+ if (kept.length < all.length) {
2807
+ const content = kept.map((m) => JSON.stringify(m)).join(LINE_SEPARATOR) + LINE_SEPARATOR;
2808
+ await fsp.writeFile(this.messagePath, content, "utf8");
2809
+ }
2810
+ const { mtimeMs, size } = await this._statMessageFile();
2811
+ this._setMessageCache(kept, mtimeMs, size);
2812
+ });
2813
+ const totalRemoved = expiredPurged + readByAllPurged + completedPurged + incompletePurged;
2814
+ return {
2815
+ expiredRemoved: expiredPurged,
2816
+ readByAllRemoved: readByAllPurged,
2817
+ stalePurged: completedPurged + incompletePurged,
2818
+ totalRemoved,
2819
+ remaining
2820
+ };
2821
+ }
2822
+ startAutoCompactTimer(opts) {
2823
+ if (this._autoCompactTimer !== null) {
2824
+ clearInterval(this._autoCompactTimer);
2825
+ }
2826
+ const intervalMs = opts?.intervalMs ?? AUTO_COMPACT_INTERVAL_MS;
2827
+ const timer = setInterval(() => {
2828
+ this.autoCompact(opts).catch(() => {
2829
+ });
2830
+ }, intervalMs);
2831
+ timer.unref?.();
2832
+ this._autoCompactTimer = timer;
2833
+ return () => {
2834
+ clearInterval(timer);
2835
+ this._autoCompactTimer = null;
2836
+ };
2837
+ }
2213
2838
  // ── Internal ────────────────────────────────────────────────────────────
2214
2839
  /**
2215
2840
  * Read all messages from the JSONL file. Always reads + parses the file.
@@ -2226,61 +2851,23 @@ var GlobalMailbox = class {
2226
2851
  throw err;
2227
2852
  }
2228
2853
  }
2229
- /**
2230
- * Read only newly-appended bytes from the file and append them to the
2231
- * in-memory cache. This avoids re-reading and re-parsing the entire file
2232
- * when another process appended messages since our last read.
2233
- *
2234
- * Only safe when the file grew in size (messages are append-only). When
2235
- * the file was rewritten (ack/purge changed existing content), callers
2236
- * must fall back to {@link _readMessages}.
2237
- *
2238
- * @returns The (now up-to-date) message cache.
2239
- */
2240
- async _readNewMessagesOnly(fd, oldSize, newSize) {
2241
- const tailLen = newSize - oldSize;
2242
- const buf = Buffer.alloc(tailLen);
2243
- await fd.read(buf, 0, tailLen, oldSize);
2244
- const tail = buf.toString("utf8");
2245
- for (const line of tail.split(LINE_SEPARATOR)) {
2246
- if (!line.trim()) continue;
2247
- try {
2248
- const parsed = JSON.parse(line);
2249
- if (!parsed["readBy"]) {
2250
- const readBy = {};
2251
- if (parsed["read"] && parsed["readAt"]) {
2252
- readBy[parsed["to"]] = parsed["readAt"];
2253
- }
2254
- parsed["readBy"] = readBy;
2255
- delete parsed["read"];
2256
- delete parsed["readAt"];
2257
- }
2258
- this._messageCache.push(parsed);
2259
- } catch {
2260
- }
2261
- }
2262
- return this._messageCache;
2263
- }
2264
- /** Parse a JSONL string into MailboxMessage[], including migration. */
2854
+ /** Parse a JSONL string into MailboxMessage[], applying ack records. */
2265
2855
  _parseLines(raw) {
2266
2856
  const lines = raw.split(LINE_SEPARATOR).filter((l) => l.trim().length > 0);
2267
2857
  const messages = [];
2858
+ const acks = [];
2268
2859
  for (const line of lines) {
2269
- try {
2270
- const parsed = JSON.parse(line);
2271
- if (!parsed["readBy"]) {
2272
- const readBy = {};
2273
- if (parsed["read"] && parsed["readAt"]) {
2274
- readBy[parsed["to"]] = parsed["readAt"];
2275
- }
2276
- parsed["readBy"] = readBy;
2277
- delete parsed["read"];
2278
- delete parsed["readAt"];
2279
- }
2860
+ const parsed = parseMailboxLine(line);
2861
+ if (isAckRecord(parsed)) {
2862
+ acks.push(parsed);
2863
+ } else if (parsed !== null) {
2280
2864
  messages.push(parsed);
2281
- } catch {
2282
2865
  }
2283
2866
  }
2867
+ for (const ack of acks) {
2868
+ const target = messages.find((m) => m.id === ack.messageId);
2869
+ if (target) applyAckToMessage(target, ack);
2870
+ }
2284
2871
  return messages;
2285
2872
  }
2286
2873
  /**
@@ -2362,17 +2949,6 @@ var GlobalMailbox = class {
2362
2949
  if (this._messageCache !== null && this._messageCacheMtime === st.mtimeMs && this._messageCacheSize === st.size) {
2363
2950
  return this._messageCache;
2364
2951
  }
2365
- if (this._messageCache !== null && this._messageCacheSize >= 0 && st.size > this._messageCacheSize) {
2366
- const fd = await fsp.open(this.messagePath, "r");
2367
- try {
2368
- const updated = await this._readNewMessagesOnly(fd, this._messageCacheSize, st.size);
2369
- this._messageCacheMtime = st.mtimeMs;
2370
- this._messageCacheSize = st.size;
2371
- return updated;
2372
- } finally {
2373
- await fd.close();
2374
- }
2375
- }
2376
2952
  const all = await this._readMessages();
2377
2953
  this._setMessageCache(all, st.mtimeMs, st.size);
2378
2954
  return all;
@@ -2404,11 +2980,61 @@ var GlobalMailbox = class {
2404
2980
  this._messageCache = null;
2405
2981
  this._messageCacheMtime = -1;
2406
2982
  this._messageCacheSize = -1;
2983
+ this._recipientIndex = null;
2984
+ this._senderIndex = null;
2407
2985
  return;
2408
2986
  }
2409
2987
  this._messageCache = messages;
2410
2988
  this._messageCacheMtime = mtime;
2411
2989
  this._messageCacheSize = size;
2990
+ this._rebuildRecipientIndex();
2991
+ }
2992
+ /**
2993
+ * Fold a single ack record into the in-memory cache, mutating the target
2994
+ * message in place. Called from `ackMany` after appending the record to
2995
+ * disk so the cache reflects the ack without a full re-read + re-parse.
2996
+ *
2997
+ * An ack only touches `readBy`/`completed*`/`outcome` — none of which
2998
+ * affect the recipient/sender indexes — so no index rebuild is needed.
2999
+ * No-op when the cache is empty or the target message isn't cached.
3000
+ */
3001
+ _applyAckToCache(ack) {
3002
+ const cache = this._messageCache;
3003
+ if (cache === null) return;
3004
+ const target = cache.find((m) => m.id === ack.messageId);
3005
+ if (target !== void 0) applyAckToMessage(target, ack);
3006
+ }
3007
+ /**
3008
+ * Build the recipient AND sender indexes from the current `_messageCache`.
3009
+ * Called only from `_setMessageCache` so both indexes are always in sync
3010
+ * with a freshly-replaced cache array.
3011
+ */
3012
+ _rebuildRecipientIndex() {
3013
+ const cache = this._messageCache;
3014
+ if (cache === null) {
3015
+ this._recipientIndex = null;
3016
+ this._senderIndex = null;
3017
+ return;
3018
+ }
3019
+ const recIdx = /* @__PURE__ */ new Map();
3020
+ const sndIdx = /* @__PURE__ */ new Map();
3021
+ for (let i = 0; i < cache.length; i++) {
3022
+ const msg = cache[i];
3023
+ let rSet = recIdx.get(msg.to);
3024
+ if (rSet === void 0) {
3025
+ rSet = /* @__PURE__ */ new Set();
3026
+ recIdx.set(msg.to, rSet);
3027
+ }
3028
+ rSet.add(i);
3029
+ let sSet = sndIdx.get(msg.from);
3030
+ if (sSet === void 0) {
3031
+ sSet = /* @__PURE__ */ new Set();
3032
+ sndIdx.set(msg.from, sSet);
3033
+ }
3034
+ sSet.add(i);
3035
+ }
3036
+ this._recipientIndex = recIdx;
3037
+ this._senderIndex = sndIdx;
2412
3038
  }
2413
3039
  /**
2414
3040
  * Append a single just-sent message to the in-memory cache without
@@ -2435,6 +3061,23 @@ var GlobalMailbox = class {
2435
3061
  return;
2436
3062
  }
2437
3063
  this._messageCache.push(msg);
3064
+ const newIdx = this._messageCache.length - 1;
3065
+ if (this._recipientIndex !== null) {
3066
+ let set = this._recipientIndex.get(msg.to);
3067
+ if (set === void 0) {
3068
+ set = /* @__PURE__ */ new Set();
3069
+ this._recipientIndex.set(msg.to, set);
3070
+ }
3071
+ set.add(newIdx);
3072
+ }
3073
+ if (this._senderIndex !== null) {
3074
+ let set = this._senderIndex.get(msg.from);
3075
+ if (set === void 0) {
3076
+ set = /* @__PURE__ */ new Set();
3077
+ this._senderIndex.set(msg.from, set);
3078
+ }
3079
+ set.add(newIdx);
3080
+ }
2438
3081
  this._messageCacheMtime = mtime;
2439
3082
  this._messageCacheSize = size;
2440
3083
  }
@@ -2466,22 +3109,13 @@ var GlobalMailbox = class {
2466
3109
  }
2467
3110
  }
2468
3111
  _pruneStaleInPlace(registry) {
2469
- const staleCutoff = Date.now() - AGENT_STALE_MS;
2470
- const purgeCutoff = Date.now() - AGENT_PURGE_MS;
2471
- const toDelete = [];
3112
+ const cutoff = Date.now() - AGENT_STALE_MS;
2472
3113
  for (const [id, agent] of registry) {
2473
- const lastSeen = new Date(agent.lastSeenAt).getTime();
2474
- if (lastSeen < purgeCutoff) {
2475
- toDelete.push(id);
2476
- continue;
2477
- }
2478
- if (lastSeen < staleCutoff) {
2479
- agent.status = "idle";
3114
+ const lastSeen = Date.parse(agent.lastSeenAt);
3115
+ if (!Number.isFinite(lastSeen) || lastSeen < cutoff) {
3116
+ registry.delete(id);
2480
3117
  }
2481
3118
  }
2482
- for (const id of toDelete) {
2483
- registry.delete(id);
2484
- }
2485
3119
  }
2486
3120
  async _writeRegistry(registry) {
2487
3121
  const obj = {};
@@ -2522,9 +3156,10 @@ var GlobalMailbox = class {
2522
3156
  }
2523
3157
  _pruneStaleClientsInPlace(registry) {
2524
3158
  const cutoff = Date.now() - CLIENT_STALE_MS;
2525
- for (const client of registry.values()) {
2526
- if (new Date(client.lastSeenAt).getTime() < cutoff) {
2527
- client.lastSeenAt = new Date(cutoff).toISOString();
3159
+ for (const [id, client] of registry) {
3160
+ const lastSeen = Date.parse(client.lastSeenAt);
3161
+ if (!Number.isFinite(lastSeen) || lastSeen < cutoff) {
3162
+ registry.delete(id);
2528
3163
  }
2529
3164
  }
2530
3165
  }
@@ -2540,8 +3175,8 @@ var GlobalMailbox = class {
2540
3175
  };
2541
3176
 
2542
3177
  // src/hq/auth-store.ts
2543
- import { randomUUID as randomUUID3 } from "node:crypto";
2544
- import * as fs2 from "node:fs/promises";
3178
+ import { randomBytes as randomBytes2, randomUUID as randomUUID3, scrypt, timingSafeEqual } from "node:crypto";
3179
+ import * as fs3 from "node:fs/promises";
2545
3180
  import * as syncFs from "node:fs";
2546
3181
  import * as path5 from "node:path";
2547
3182
  var HQ_AUTH_FILE_VERSION = 1;
@@ -2601,29 +3236,31 @@ function readHqRuntimeFileSync(dataDir) {
2601
3236
  return void 0;
2602
3237
  }
2603
3238
  }
2604
- async function readHqAuthFile(dataDir, opts = {}) {
3239
+ async function readHqAuthFile(dataDir, _opts = {}) {
2605
3240
  const file = hqAuthFilePath(dataDir);
2606
3241
  let raw;
2607
3242
  try {
2608
- raw = await fs2.readFile(file, "utf8");
3243
+ raw = await fs3.readFile(file, "utf8");
2609
3244
  } catch (err) {
2610
3245
  if (err.code === "ENOENT") return emptyHqAuthFile();
2611
- opts.warn?.(`HQ auth file read failed at ${file}: ${err.message}`);
2612
- return emptyHqAuthFile();
3246
+ throw new Error(
3247
+ `HQ auth file at ${file} cannot be read: ${err.message}. Fix the file permissions or delete it to start with an empty auth state.`
3248
+ );
2613
3249
  }
3250
+ let parsed;
2614
3251
  try {
2615
- const parsed = JSON.parse(raw);
2616
- if (parsed.version !== HQ_AUTH_FILE_VERSION) {
2617
- opts.warn?.(
2618
- `HQ auth file at ${file} has unsupported version ${String(parsed.version)} (expected ${String(HQ_AUTH_FILE_VERSION)}); ignoring stored policy/tokens.`
2619
- );
2620
- return emptyHqAuthFile();
2621
- }
2622
- return parsed;
3252
+ parsed = JSON.parse(raw);
2623
3253
  } catch (err) {
2624
- opts.warn?.(`HQ auth file at ${file} is not valid JSON; ignoring stored policy/tokens: ${err.message}`);
2625
- return emptyHqAuthFile();
3254
+ throw new Error(
3255
+ `HQ auth file at ${file} is not valid JSON: ${err.message}. Fix the file or delete it to start with an empty auth state.`
3256
+ );
2626
3257
  }
3258
+ if (parsed.version !== HQ_AUTH_FILE_VERSION) {
3259
+ throw new Error(
3260
+ `HQ auth file at ${file} has unsupported version ${String(parsed.version)} (expected ${String(HQ_AUTH_FILE_VERSION)}). Remove or update the file to match the current schema version.`
3261
+ );
3262
+ }
3263
+ return parsed;
2627
3264
  }
2628
3265
  async function writeHqAuthFile(dataDir, file) {
2629
3266
  const target = hqAuthFilePath(dataDir);
@@ -2638,7 +3275,7 @@ async function writeHqAuthFile(dataDir, file) {
2638
3275
  async function ensureHqFirstRunAuthFile(dataDir, opts = {}) {
2639
3276
  const file = hqAuthFilePath(dataDir);
2640
3277
  try {
2641
- await fs2.access(file);
3278
+ await fs3.access(file);
2642
3279
  return { authFile: await readHqAuthFile(dataDir, opts), created: false };
2643
3280
  } catch (err) {
2644
3281
  if (err.code !== "ENOENT") {
@@ -2654,6 +3291,10 @@ async function ensureHqFirstRunAuthFile(dataDir, opts = {}) {
2654
3291
  browserTokens: [browserToken],
2655
3292
  clientTokens: [clientToken]
2656
3293
  };
3294
+ if (opts.password) {
3295
+ authFile.passwordHash = await hashHqPassword(opts.password);
3296
+ authFile.cookieSecret = mintHqCookieSecret();
3297
+ }
2657
3298
  await writeHqAuthFile(dataDir, authFile);
2658
3299
  return { authFile: await readHqAuthFile(dataDir, opts), created: true, browserToken, clientToken };
2659
3300
  }
@@ -2663,6 +3304,42 @@ async function mutateHqAuthFile(dataDir, mutator, opts = {}) {
2663
3304
  await writeHqAuthFile(dataDir, next);
2664
3305
  return next;
2665
3306
  }
3307
+ var HQ_PASSWORD_SALT_BYTES = 16;
3308
+ var HQ_PASSWORD_HASH_BYTES = 32;
3309
+ var HQ_PASSWORD_SCRYPT_PARAMS = { N: 16384, r: 8, p: 1 };
3310
+ function hashHqPassword(password) {
3311
+ return new Promise((resolve4, reject) => {
3312
+ const salt = randomBytes2(HQ_PASSWORD_SALT_BYTES);
3313
+ scrypt(password, salt, HQ_PASSWORD_HASH_BYTES, HQ_PASSWORD_SCRYPT_PARAMS, (err, derivedKey) => {
3314
+ if (err) return reject(err);
3315
+ const payload = `scrypt$${salt.toString("base64url")}$${derivedKey.toString("base64url")}`;
3316
+ resolve4(payload);
3317
+ });
3318
+ });
3319
+ }
3320
+ async function verifyHqPassword(password, hash) {
3321
+ const parts = hash.split("$");
3322
+ if (parts.length !== 3 || parts[0] !== "scrypt" || !parts[1] || !parts[2]) return false;
3323
+ let salt;
3324
+ let expected;
3325
+ try {
3326
+ salt = Buffer.from(parts[1], "base64url");
3327
+ expected = Buffer.from(parts[2], "base64url");
3328
+ } catch {
3329
+ return false;
3330
+ }
3331
+ if (salt.length === 0 || expected.length === 0) return false;
3332
+ const derived = await new Promise((resolve4, reject) => {
3333
+ scrypt(password, salt, expected.length, HQ_PASSWORD_SCRYPT_PARAMS, (err, key) => {
3334
+ if (err) reject(err);
3335
+ else resolve4(key);
3336
+ });
3337
+ });
3338
+ return timingSafeEqual(derived, expected);
3339
+ }
3340
+ function mintHqCookieSecret() {
3341
+ return randomBytes2(32).toString("base64url");
3342
+ }
2666
3343
  function mintHqToken(label) {
2667
3344
  const now = (/* @__PURE__ */ new Date()).toISOString();
2668
3345
  return {
@@ -2731,7 +3408,7 @@ function discoverLocalHqEndpoint(options = {}) {
2731
3408
  }
2732
3409
  function readFirstClientTokenFromAuthFile(dataDir) {
2733
3410
  try {
2734
- const raw = fs3.readFileSync(hqAuthFilePath(dataDir), "utf8");
3411
+ const raw = fs4.readFileSync(hqAuthFilePath(dataDir), "utf8");
2735
3412
  const parsed = JSON.parse(raw);
2736
3413
  return parsed.clientTokens?.find((t) => t.token.trim().length > 0)?.token;
2737
3414
  } catch {
@@ -2833,70 +3510,16 @@ function createHqPublisherFromEnv(options) {
2833
3510
  // Auto-discovery: re-read the local HQ runtime marker + client token on
2834
3511
  // every connect attempt so late-started/restarted HQs are picked up.
2835
3512
  ...config.discover ? { resolveEndpoint: () => discoverLocalHqEndpoint({ dataDir: discoveryDataDir }) } : {},
2836
- ...options.discoveryPollMs !== void 0 ? { discoveryPollMs: options.discoveryPollMs } : {}
3513
+ ...options.discoveryPollMs !== void 0 ? { discoveryPollMs: options.discoveryPollMs } : {},
3514
+ ...options.logger !== void 0 ? { logger: options.logger } : {}
2837
3515
  });
2838
3516
  }
2839
3517
  function createGlobalMailbox(options) {
2840
3518
  return new GlobalMailbox(options.projectDir, options.events, options.hqPublisher);
2841
3519
  }
2842
3520
 
2843
- // src/hq/agent-bridge.ts
2844
- function startAgentMonitorEventBridge(opts) {
2845
- const { events, clientId, projectId, publish } = opts;
2846
- const seq = { current: 0 };
2847
- function nextSeq() {
2848
- seq.current += 1;
2849
- return seq.current;
2850
- }
2851
- function buildEnvelope(type, payload, sessionId) {
2852
- return createHqEventEnvelope({
2853
- id: `${Date.now().toString(36)}-${nextSeq()}`,
2854
- type,
2855
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2856
- clientId,
2857
- projectId,
2858
- seq: nextSeq(),
2859
- payload,
2860
- // Carry the parent session id so HQ can scope per-agent transcripts by
2861
- // (sessionId, subagentId) — without it, two sessions whose leaders both
2862
- // use the default id 'leader' would collide into one shared ring.
2863
- ...sessionId !== void 0 ? { sessionId } : {}
2864
- });
2865
- }
2866
- const offMessage = events.on("agent.timeline.message", (payload) => {
2867
- if (!publish) return;
2868
- const msgPayload = {
2869
- subagentId: payload.subagentId,
2870
- agentName: payload.agentName,
2871
- content: payload.content,
2872
- kind: payload.kind,
2873
- iteration: payload.iteration,
2874
- ts: payload.ts
2875
- };
2876
- if (payload.toolName !== void 0) msgPayload.toolName = payload.toolName;
2877
- if (payload.costUsd !== void 0) msgPayload.costUsd = payload.costUsd;
2878
- publish(buildEnvelope("agent.message", msgPayload, payload.sessionId));
2879
- });
2880
- const offStatus = events.on("agent.status_changed", (payload) => {
2881
- if (!publish) return;
2882
- const statusPayload = {
2883
- subagentId: payload.subagentId,
2884
- agentName: payload.agentName,
2885
- status: payload.status,
2886
- ts: payload.ts
2887
- };
2888
- if (payload.summary !== void 0) statusPayload.summary = payload.summary;
2889
- if (payload.task !== void 0) statusPayload.task = payload.task;
2890
- publish(buildEnvelope("agent.status", statusPayload));
2891
- });
2892
- return () => {
2893
- offMessage();
2894
- offStatus();
2895
- };
2896
- }
2897
-
2898
3521
  // src/hq/session-bridge.ts
2899
- import * as fs4 from "node:fs";
3522
+ import * as fs5 from "node:fs";
2900
3523
  import * as fsp2 from "node:fs/promises";
2901
3524
 
2902
3525
  // src/hq/transcript-mapper.ts
@@ -3166,7 +3789,7 @@ function startSessionTelemetryBridge(opts) {
3166
3789
  function setupWatcher() {
3167
3790
  if (disposed || watcher) return;
3168
3791
  try {
3169
- const nextWatcher = fs4.watch(sessionFile, () => {
3792
+ const nextWatcher = fs5.watch(sessionFile, () => {
3170
3793
  if (watchPending || disposed) return;
3171
3794
  watchPending = true;
3172
3795
  setTimeout(() => {
@@ -3263,7 +3886,11 @@ function startFleetTelemetryBridge(opts) {
3263
3886
  const subagents = stats.subagentStatuses.map((s) => ({
3264
3887
  subagentId: s.subagentId,
3265
3888
  ...s.taskId ? { task: s.taskId } : {},
3266
- status: normalizeSubagentStatus(s.status)
3889
+ status: normalizeSubagentStatus(s.status),
3890
+ ...s.model !== void 0 ? { model: s.model } : {},
3891
+ ...s.costUsd !== void 0 ? { costUsd: s.costUsd } : {},
3892
+ ...s.runtimeMs !== void 0 ? { runtimeMs: s.runtimeMs } : {},
3893
+ ...s.lastActivityAt !== void 0 ? { lastActivityAt: s.lastActivityAt } : {}
3267
3894
  }));
3268
3895
  return {
3269
3896
  runId,
@@ -3271,6 +3898,7 @@ function startFleetTelemetryBridge(opts) {
3271
3898
  queuedTasks: stats.pending,
3272
3899
  completedTasks: stats.completed,
3273
3900
  failedTasks: stats.stopped,
3901
+ ...stats.totalCostUsd !== void 0 ? { totalCostUsd: stats.totalCostUsd } : {},
3274
3902
  subagents
3275
3903
  };
3276
3904
  }
@@ -3586,6 +4214,10 @@ function startCostTelemetryBridge(opts) {
3586
4214
  totalTokens: p.usage.input + p.usage.output,
3587
4215
  costUsd: p.cost.total
3588
4216
  };
4217
+ if (p.provider !== void 0) payload.provider = p.provider;
4218
+ if (p.model !== void 0) payload.model = p.model;
4219
+ if (p.usage.cacheRead !== void 0) payload.cacheRead = p.usage.cacheRead;
4220
+ if (p.usage.cacheWrite !== void 0) payload.cacheWrite = p.usage.cacheWrite;
3589
4221
  const sessionId = opts.sessionId ?? p.sessionId;
3590
4222
  publisher.publishEvent({
3591
4223
  type: "session.usage",
@@ -3602,7 +4234,7 @@ function startCostTelemetryBridge(opts) {
3602
4234
  }
3603
4235
 
3604
4236
  // src/hq/persistence.ts
3605
- import * as fs5 from "node:fs/promises";
4237
+ import * as fs6 from "node:fs/promises";
3606
4238
  import * as path6 from "node:path";
3607
4239
  var DEFAULT_EVENT_LOG_MAX_LINES = 5e4;
3608
4240
  var DEFAULT_EVENT_LOG_ROTATE_KEEP = 2e4;
@@ -3634,7 +4266,7 @@ var HqEventLog = class {
3634
4266
  this.counted = true;
3635
4267
  }
3636
4268
  const line = JSON.stringify(event) + "\n";
3637
- await fs5.appendFile(this.filePath, line, { encoding: "utf8" });
4269
+ await fs6.appendFile(this.filePath, line, { encoding: "utf8" });
3638
4270
  this.lineCount += 1;
3639
4271
  if (this.lineCount >= this.maxLines) {
3640
4272
  await this.rotate();
@@ -3643,7 +4275,7 @@ var HqEventLog = class {
3643
4275
  async rotate() {
3644
4276
  await withFileLock(this.filePath, async () => {
3645
4277
  try {
3646
- const content = await fs5.readFile(this.filePath, "utf8");
4278
+ const content = await fs6.readFile(this.filePath, "utf8");
3647
4279
  const lines = content.split("\n").filter((l) => l.length > 0);
3648
4280
  if (lines.length <= this.rotateKeep) {
3649
4281
  this.lineCount = lines.length;
@@ -3658,7 +4290,7 @@ var HqEventLog = class {
3658
4290
  }
3659
4291
  async countLines() {
3660
4292
  try {
3661
- const content = await fs5.readFile(this.filePath, "utf8");
4293
+ const content = await fs6.readFile(this.filePath, "utf8");
3662
4294
  return content.split("\n").filter((l) => l.length > 0).length;
3663
4295
  } catch {
3664
4296
  return 0;
@@ -3671,7 +4303,7 @@ var HqEventLog = class {
3671
4303
  async recent(limit, typeFilter) {
3672
4304
  let content;
3673
4305
  try {
3674
- content = await fs5.readFile(this.filePath, "utf8");
4306
+ content = await fs6.readFile(this.filePath, "utf8");
3675
4307
  } catch {
3676
4308
  return [];
3677
4309
  }
@@ -3713,7 +4345,7 @@ var HqSnapshotStore = class {
3713
4345
  /** Read the last persisted snapshot, or `null` if none. */
3714
4346
  async load() {
3715
4347
  try {
3716
- const content = await fs5.readFile(this.filePath, "utf8");
4348
+ const content = await fs6.readFile(this.filePath, "utf8");
3717
4349
  return JSON.parse(content);
3718
4350
  } catch {
3719
4351
  return null;
@@ -3747,6 +4379,21 @@ var HqTimeseriesStore = class {
3747
4379
  if (signal.outputTokens !== void 0) bucket.outputTokens += signal.outputTokens;
3748
4380
  if (signal.toolCalls !== void 0) bucket.toolCalls += signal.toolCalls;
3749
4381
  if (signal.activeAgents !== void 0) bucket.activeAgents = signal.activeAgents;
4382
+ if (signal.model !== void 0 || signal.provider !== void 0) {
4383
+ const entry = {
4384
+ costUsd: signal.costUsd ?? 0,
4385
+ inputTokens: signal.inputTokens ?? 0,
4386
+ outputTokens: signal.outputTokens ?? 0,
4387
+ ...signal.cacheRead !== void 0 ? { cacheRead: signal.cacheRead } : {},
4388
+ ...signal.cacheWrite !== void 0 ? { cacheWrite: signal.cacheWrite } : {}
4389
+ };
4390
+ if (signal.model !== void 0) {
4391
+ bucket.byModel = foldBreakdown(bucket.byModel, signal.model, entry);
4392
+ }
4393
+ if (signal.provider !== void 0) {
4394
+ bucket.byProvider = foldBreakdown(bucket.byProvider, signal.provider, entry);
4395
+ }
4396
+ }
3750
4397
  if (this.buckets.size > this.maxBuckets) {
3751
4398
  const sorted = Array.from(this.buckets.keys()).sort((a, b) => a - b);
3752
4399
  while (this.buckets.size > this.maxBuckets && sorted.length > 0) {
@@ -3771,7 +4418,7 @@ var HqTimeseriesStore = class {
3771
4418
  async flushInternal(toWrite) {
3772
4419
  const lines = toWrite.map((b) => JSON.stringify(b)).join("\n") + "\n";
3773
4420
  await withFileLock(this.filePath, async () => {
3774
- await fs5.appendFile(this.filePath, lines, { encoding: "utf8" });
4421
+ await fs6.appendFile(this.filePath, lines, { encoding: "utf8" });
3775
4422
  });
3776
4423
  const sorted = Array.from(this.buckets.keys()).sort((a, b) => a - b);
3777
4424
  while (this.buckets.size > this.maxBuckets) {
@@ -3790,7 +4437,7 @@ var HqTimeseriesStore = class {
3790
4437
  async load() {
3791
4438
  let content;
3792
4439
  try {
3793
- content = await fs5.readFile(this.filePath, "utf8");
4440
+ content = await fs6.readFile(this.filePath, "utf8");
3794
4441
  } catch {
3795
4442
  return;
3796
4443
  }
@@ -3811,11 +4458,66 @@ var HqTimeseriesStore = class {
3811
4458
  }
3812
4459
  }
3813
4460
  };
4461
+ function foldBreakdown(map, key, entry) {
4462
+ const next = { ...map };
4463
+ const existing = next[key];
4464
+ if (existing === void 0) {
4465
+ next[key] = { ...entry };
4466
+ } else {
4467
+ next[key] = {
4468
+ costUsd: existing.costUsd + entry.costUsd,
4469
+ inputTokens: existing.inputTokens + entry.inputTokens,
4470
+ outputTokens: existing.outputTokens + entry.outputTokens,
4471
+ cacheRead: existing.cacheRead !== void 0 || entry.cacheRead !== void 0 ? (existing.cacheRead ?? 0) + (entry.cacheRead ?? 0) : void 0,
4472
+ cacheWrite: existing.cacheWrite !== void 0 || entry.cacheWrite !== void 0 ? (existing.cacheWrite ?? 0) + (entry.cacheWrite ?? 0) : void 0
4473
+ };
4474
+ }
4475
+ return next;
4476
+ }
4477
+ var HqSimpleLog = class {
4478
+ filePath;
4479
+ writeChain = Promise.resolve();
4480
+ constructor(dataDir, filename) {
4481
+ this.filePath = path6.join(dataDir, filename);
4482
+ }
4483
+ /** Append one record as a JSON line. Best-effort, never rejects. */
4484
+ append(record) {
4485
+ this.writeChain = this.writeChain.then(() => fs6.appendFile(this.filePath, `${JSON.stringify(record)}
4486
+ `, { encoding: "utf8" })).catch(() => {
4487
+ });
4488
+ }
4489
+ /** Resolves once all queued appends have settled. For tests. */
4490
+ async drain() {
4491
+ await this.writeChain.catch(() => {
4492
+ });
4493
+ }
4494
+ /** Read all records oldest-first. Returns `[]` if the file doesn't exist. */
4495
+ async readAll() {
4496
+ let content;
4497
+ try {
4498
+ content = await fs6.readFile(this.filePath, "utf8");
4499
+ } catch {
4500
+ return [];
4501
+ }
4502
+ const out = [];
4503
+ for (const line of content.split("\n")) {
4504
+ const trimmed = line.trim();
4505
+ if (!trimmed) continue;
4506
+ try {
4507
+ out.push(JSON.parse(trimmed));
4508
+ } catch {
4509
+ }
4510
+ }
4511
+ return out;
4512
+ }
4513
+ };
3814
4514
  function createHqPersistence(dataDir) {
3815
4515
  return {
3816
4516
  eventLog: new HqEventLog({ dataDir }),
3817
4517
  snapshotStore: new HqSnapshotStore({ dataDir }),
3818
- timeseries: new HqTimeseriesStore({ dataDir })
4518
+ timeseries: new HqTimeseriesStore({ dataDir }),
4519
+ commandLog: new HqSimpleLog(dataDir, "commands.jsonl"),
4520
+ alertLog: new HqSimpleLog(dataDir, "alerts.jsonl")
3819
4521
  };
3820
4522
  }
3821
4523
 
@@ -3896,18 +4598,24 @@ function validateHqCommand(queued) {
3896
4598
  var HqCommandAuditLog = class {
3897
4599
  entries = [];
3898
4600
  max;
3899
- constructor(max = 1e3) {
4601
+ onPersist;
4602
+ constructor(max = 1e3, onPersist) {
3900
4603
  this.max = max;
4604
+ this.onPersist = onPersist;
3901
4605
  }
3902
4606
  record(entry) {
3903
4607
  this.entries.push(entry);
3904
4608
  if (this.entries.length > this.max) {
3905
4609
  this.entries.splice(0, this.entries.length - this.max);
3906
4610
  }
4611
+ this.onPersist?.(entry);
3907
4612
  }
3908
4613
  update(commandId, patch) {
3909
4614
  const entry = this.entries.find((e) => e.commandId === commandId);
3910
- if (entry) Object.assign(entry, patch);
4615
+ if (entry) {
4616
+ Object.assign(entry, patch);
4617
+ this.onPersist?.(entry);
4618
+ }
3911
4619
  }
3912
4620
  /** Update only when the command belongs to the authenticated client. */
3913
4621
  updateForClient(commandId, clientId, patch) {
@@ -3916,8 +4624,18 @@ var HqCommandAuditLog = class {
3916
4624
  );
3917
4625
  if (!entry) return false;
3918
4626
  Object.assign(entry, patch);
4627
+ this.onPersist?.(entry);
3919
4628
  return true;
3920
4629
  }
4630
+ /** Seed the ring from a durable store on boot (no persist callback fired). */
4631
+ seed(entries) {
4632
+ for (const entry of entries) {
4633
+ this.entries.push(entry);
4634
+ }
4635
+ if (this.entries.length > this.max) {
4636
+ this.entries.splice(0, this.entries.length - this.max);
4637
+ }
4638
+ }
3921
4639
  recent(limit = 200) {
3922
4640
  return this.entries.slice(-limit);
3923
4641
  }
@@ -3992,9 +4710,11 @@ var HqAlertEngine = class {
3992
4710
  maxHistory;
3993
4711
  timer = null;
3994
4712
  onAlert;
4713
+ onPersist;
3995
4714
  constructor(opts) {
3996
4715
  this.onAlert = opts.onAlert;
3997
4716
  this.maxHistory = opts.maxHistory ?? 500;
4717
+ this.onPersist = opts.onPersist;
3998
4718
  }
3999
4719
  /**
4000
4720
  * Evaluate all rules against the snapshot. Emits (via the `onAlert`
@@ -4024,6 +4744,7 @@ var HqAlertEngine = class {
4024
4744
  if (this.history.length > this.maxHistory) this.history.splice(0, this.history.length - this.maxHistory);
4025
4745
  fired.push(alert);
4026
4746
  this.onAlert(alert);
4747
+ this.onPersist?.(alert);
4027
4748
  } else {
4028
4749
  existing.lastFiredAt = now;
4029
4750
  }
@@ -4044,6 +4765,15 @@ var HqAlertEngine = class {
4044
4765
  recentAlerts(limit = 100) {
4045
4766
  return this.history.slice(-limit);
4046
4767
  }
4768
+ /** Seed the history from a durable store on boot (no alert callbacks fired). */
4769
+ seed(alerts) {
4770
+ for (const alert of alerts) {
4771
+ this.history.push(alert);
4772
+ }
4773
+ if (this.history.length > this.maxHistory) {
4774
+ this.history.splice(0, this.history.length - this.maxHistory);
4775
+ }
4776
+ }
4047
4777
  /**
4048
4778
  * Start periodic evaluation against a snapshot getter. The timer is
4049
4779
  * unref'd so it never keeps the process alive. Returns a disposer.
@@ -4085,6 +4815,7 @@ export {
4085
4815
  HqCommandAuditLog,
4086
4816
  HqEventLog,
4087
4817
  HqPublisher,
4818
+ HqSimpleLog,
4088
4819
  HqSnapshotStore,
4089
4820
  HqTimeseriesStore,
4090
4821
  buildTranscriptFromEvents,
@@ -4099,6 +4830,7 @@ export {
4099
4830
  discoverLocalHqEndpoint,
4100
4831
  emptyHqAuthFile,
4101
4832
  ensureHqFirstRunAuthFile,
4833
+ hashHqPassword,
4102
4834
  hqAuthFilePath,
4103
4835
  hqRuntimeFilePath,
4104
4836
  mapMailboxAgentToHqSummary,
@@ -4106,6 +4838,7 @@ export {
4106
4838
  mapSessionEventToEntries,
4107
4839
  mergeToolResults,
4108
4840
  mintHqBrowserToken,
4841
+ mintHqCookieSecret,
4109
4842
  mintHqToken,
4110
4843
  mutateHqAuthFile,
4111
4844
  parseHqEventPayload,
@@ -4119,7 +4852,6 @@ export {
4119
4852
  resolveHqDataDir,
4120
4853
  resolveHqRedactionPolicy,
4121
4854
  scrubAndTruncateHqPreview,
4122
- startAgentMonitorEventBridge,
4123
4855
  startBrainTelemetryBridge,
4124
4856
  startCostTelemetryBridge,
4125
4857
  startFleetTelemetryBridge,
@@ -4131,6 +4863,7 @@ export {
4131
4863
  toAlertMessage,
4132
4864
  tokenHasCapability,
4133
4865
  validateHqCommand,
4866
+ verifyHqPassword,
4134
4867
  watchHqAuthFile,
4135
4868
  writeHqAuthFile,
4136
4869
  writeHqRuntimeFile