macro-agent 0.1.0 → 0.1.1

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 (337) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/.sudocode/specs.jsonl +4 -0
  3. package/CLAUDE.md +16 -14
  4. package/README.md +11 -29
  5. package/dist/acp/macro-agent.d.ts +15 -0
  6. package/dist/acp/macro-agent.d.ts.map +1 -1
  7. package/dist/acp/macro-agent.js +131 -35
  8. package/dist/acp/macro-agent.js.map +1 -1
  9. package/dist/acp/types.d.ts +32 -1
  10. package/dist/acp/types.d.ts.map +1 -1
  11. package/dist/acp/types.js.map +1 -1
  12. package/dist/agent/agent-manager.d.ts +65 -1
  13. package/dist/agent/agent-manager.d.ts.map +1 -1
  14. package/dist/agent/agent-manager.js +464 -183
  15. package/dist/agent/agent-manager.js.map +1 -1
  16. package/dist/agent/types.d.ts +1 -1
  17. package/dist/agent/types.d.ts.map +1 -1
  18. package/dist/api/server.d.ts +3 -0
  19. package/dist/api/server.d.ts.map +1 -1
  20. package/dist/api/server.js +37 -6
  21. package/dist/api/server.js.map +1 -1
  22. package/dist/auth/index.d.ts +2 -0
  23. package/dist/auth/index.d.ts.map +1 -0
  24. package/dist/auth/index.js +2 -0
  25. package/dist/auth/index.js.map +1 -0
  26. package/dist/auth/token.d.ts +41 -0
  27. package/dist/auth/token.d.ts.map +1 -0
  28. package/dist/auth/token.js +73 -0
  29. package/dist/auth/token.js.map +1 -0
  30. package/dist/cli/acp.d.ts +2 -23
  31. package/dist/cli/acp.d.ts.map +1 -1
  32. package/dist/cli/acp.js +127 -61
  33. package/dist/cli/acp.js.map +1 -1
  34. package/dist/cli/index.js +147 -15
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/cli/mcp.d.ts +6 -0
  37. package/dist/cli/mcp.d.ts.map +1 -1
  38. package/dist/cli/mcp.js +268 -181
  39. package/dist/cli/mcp.js.map +1 -1
  40. package/dist/cli/parse-args.d.ts +20 -0
  41. package/dist/cli/parse-args.d.ts.map +1 -0
  42. package/dist/cli/parse-args.js +43 -0
  43. package/dist/cli/parse-args.js.map +1 -0
  44. package/dist/cli/stable-instance-id.d.ts +8 -0
  45. package/dist/cli/stable-instance-id.d.ts.map +1 -0
  46. package/dist/cli/stable-instance-id.js +14 -0
  47. package/dist/cli/stable-instance-id.js.map +1 -0
  48. package/dist/config/project-config.d.ts +74 -7
  49. package/dist/config/project-config.d.ts.map +1 -1
  50. package/dist/config/project-config.js +123 -20
  51. package/dist/config/project-config.js.map +1 -1
  52. package/dist/map/adapter/acp-over-map.d.ts +17 -0
  53. package/dist/map/adapter/acp-over-map.d.ts.map +1 -1
  54. package/dist/map/adapter/acp-over-map.js +384 -23
  55. package/dist/map/adapter/acp-over-map.js.map +1 -1
  56. package/dist/map/adapter/connection-manager.d.ts.map +1 -1
  57. package/dist/map/adapter/connection-manager.js +3 -0
  58. package/dist/map/adapter/connection-manager.js.map +1 -1
  59. package/dist/map/adapter/event-log.d.ts +87 -0
  60. package/dist/map/adapter/event-log.d.ts.map +1 -0
  61. package/dist/map/adapter/event-log.js +122 -0
  62. package/dist/map/adapter/event-log.js.map +1 -0
  63. package/dist/map/adapter/event-translator.js +6 -6
  64. package/dist/map/adapter/event-translator.js.map +1 -1
  65. package/dist/map/adapter/extensions/agent-lifecycle.d.ts +82 -0
  66. package/dist/map/adapter/extensions/agent-lifecycle.d.ts.map +1 -0
  67. package/dist/map/adapter/extensions/agent-lifecycle.js +164 -0
  68. package/dist/map/adapter/extensions/agent-lifecycle.js.map +1 -0
  69. package/dist/map/adapter/extensions/index.d.ts +10 -1
  70. package/dist/map/adapter/extensions/index.d.ts.map +1 -1
  71. package/dist/map/adapter/extensions/index.js +34 -0
  72. package/dist/map/adapter/extensions/index.js.map +1 -1
  73. package/dist/map/adapter/extensions/mcp-bridge.d.ts +57 -0
  74. package/dist/map/adapter/extensions/mcp-bridge.d.ts.map +1 -0
  75. package/dist/map/adapter/extensions/mcp-bridge.js +745 -0
  76. package/dist/map/adapter/extensions/mcp-bridge.js.map +1 -0
  77. package/dist/map/adapter/extensions/rename.d.ts +29 -0
  78. package/dist/map/adapter/extensions/rename.d.ts.map +1 -0
  79. package/dist/map/adapter/extensions/rename.js +49 -0
  80. package/dist/map/adapter/extensions/rename.js.map +1 -0
  81. package/dist/map/adapter/extensions/task.d.ts.map +1 -1
  82. package/dist/map/adapter/extensions/task.js +10 -0
  83. package/dist/map/adapter/extensions/task.js.map +1 -1
  84. package/dist/map/adapter/extensions/update-metadata.d.ts +29 -0
  85. package/dist/map/adapter/extensions/update-metadata.d.ts.map +1 -0
  86. package/dist/map/adapter/extensions/update-metadata.js +67 -0
  87. package/dist/map/adapter/extensions/update-metadata.js.map +1 -0
  88. package/dist/map/adapter/index.d.ts +2 -1
  89. package/dist/map/adapter/index.d.ts.map +1 -1
  90. package/dist/map/adapter/index.js +8 -2
  91. package/dist/map/adapter/index.js.map +1 -1
  92. package/dist/map/adapter/interface.d.ts +2 -0
  93. package/dist/map/adapter/interface.d.ts.map +1 -1
  94. package/dist/map/adapter/map-adapter.d.ts +3 -0
  95. package/dist/map/adapter/map-adapter.d.ts.map +1 -1
  96. package/dist/map/adapter/map-adapter.js +258 -35
  97. package/dist/map/adapter/map-adapter.js.map +1 -1
  98. package/dist/map/adapter/subscription-manager.d.ts.map +1 -1
  99. package/dist/map/adapter/subscription-manager.js +5 -1
  100. package/dist/map/adapter/subscription-manager.js.map +1 -1
  101. package/dist/map/adapter/types.d.ts +2 -0
  102. package/dist/map/adapter/types.d.ts.map +1 -1
  103. package/dist/mcp/map-client.d.ts +39 -0
  104. package/dist/mcp/map-client.d.ts.map +1 -0
  105. package/dist/mcp/map-client.js +129 -0
  106. package/dist/mcp/map-client.js.map +1 -0
  107. package/dist/mcp/mcp-server.d.ts +14 -0
  108. package/dist/mcp/mcp-server.d.ts.map +1 -1
  109. package/dist/mcp/mcp-server.js +113 -85
  110. package/dist/mcp/mcp-server.js.map +1 -1
  111. package/dist/mcp/types.d.ts +9 -1
  112. package/dist/mcp/types.d.ts.map +1 -1
  113. package/dist/mcp/types.js.map +1 -1
  114. package/dist/metrics/metrics.js +1 -1
  115. package/dist/metrics/metrics.js.map +1 -1
  116. package/dist/roles/capabilities.d.ts +3 -1
  117. package/dist/roles/capabilities.d.ts.map +1 -1
  118. package/dist/roles/capabilities.js +17 -7
  119. package/dist/roles/capabilities.js.map +1 -1
  120. package/dist/roles/config-loader.d.ts +6 -6
  121. package/dist/roles/config-loader.d.ts.map +1 -1
  122. package/dist/roles/config-loader.js +6 -6
  123. package/dist/roles/config-loader.js.map +1 -1
  124. package/dist/roles/registry.d.ts +2 -2
  125. package/dist/roles/registry.js +2 -2
  126. package/dist/server/combined-server.d.ts +20 -0
  127. package/dist/server/combined-server.d.ts.map +1 -1
  128. package/dist/server/combined-server.js +107 -8
  129. package/dist/server/combined-server.js.map +1 -1
  130. package/dist/store/event-store.d.ts +2 -1
  131. package/dist/store/event-store.d.ts.map +1 -1
  132. package/dist/store/event-store.js +69 -20
  133. package/dist/store/event-store.js.map +1 -1
  134. package/dist/store/types/agents.d.ts +18 -0
  135. package/dist/store/types/agents.d.ts.map +1 -1
  136. package/dist/store/types/events.d.ts +1 -1
  137. package/dist/store/types/events.d.ts.map +1 -1
  138. package/dist/task/backend/index.d.ts +47 -29
  139. package/dist/task/backend/index.d.ts.map +1 -1
  140. package/dist/task/backend/index.js +109 -71
  141. package/dist/task/backend/index.js.map +1 -1
  142. package/dist/task/backend/memory.d.ts +1 -0
  143. package/dist/task/backend/memory.d.ts.map +1 -1
  144. package/dist/task/backend/memory.js +3 -0
  145. package/dist/task/backend/memory.js.map +1 -1
  146. package/dist/task/backend/opentasks/backend.d.ts +140 -0
  147. package/dist/task/backend/opentasks/backend.d.ts.map +1 -0
  148. package/dist/task/backend/opentasks/backend.js +1023 -0
  149. package/dist/task/backend/opentasks/backend.js.map +1 -0
  150. package/dist/task/backend/opentasks/client.d.ts +337 -0
  151. package/dist/task/backend/opentasks/client.d.ts.map +1 -0
  152. package/dist/task/backend/opentasks/client.js +225 -0
  153. package/dist/task/backend/opentasks/client.js.map +1 -0
  154. package/dist/task/backend/opentasks/daemon-manager.d.ts +89 -0
  155. package/dist/task/backend/opentasks/daemon-manager.d.ts.map +1 -0
  156. package/dist/task/backend/opentasks/daemon-manager.js +195 -0
  157. package/dist/task/backend/opentasks/daemon-manager.js.map +1 -0
  158. package/dist/task/backend/opentasks/index.d.ts +21 -0
  159. package/dist/task/backend/opentasks/index.d.ts.map +1 -0
  160. package/dist/task/backend/opentasks/index.js +21 -0
  161. package/dist/task/backend/opentasks/index.js.map +1 -0
  162. package/dist/task/backend/opentasks/mapping.d.ts +48 -0
  163. package/dist/task/backend/opentasks/mapping.d.ts.map +1 -0
  164. package/dist/task/backend/opentasks/mapping.js +77 -0
  165. package/dist/task/backend/opentasks/mapping.js.map +1 -0
  166. package/dist/task/backend/types.d.ts +33 -53
  167. package/dist/task/backend/types.d.ts.map +1 -1
  168. package/dist/task/backend/types.js +7 -11
  169. package/dist/task/backend/types.js.map +1 -1
  170. package/dist/task/backend/unified-tool-provider.d.ts +57 -0
  171. package/dist/task/backend/unified-tool-provider.d.ts.map +1 -0
  172. package/dist/task/backend/unified-tool-provider.js +623 -0
  173. package/dist/task/backend/unified-tool-provider.js.map +1 -0
  174. package/dist/teams/team-loader.d.ts +2 -2
  175. package/dist/teams/team-loader.js +3 -3
  176. package/dist/teams/team-loader.js.map +1 -1
  177. package/dist/teams/team-runtime.d.ts.map +1 -1
  178. package/dist/teams/team-runtime.js +2 -0
  179. package/dist/teams/team-runtime.js.map +1 -1
  180. package/docs/architecture.md +7 -6
  181. package/docs/configuration.md +26 -62
  182. package/docs/implementation-details.md +5 -5
  183. package/docs/implementation-summary.md +17 -17
  184. package/docs/plan-self-driving-support.md +4 -4
  185. package/docs/spec-self-driving-support.md +10 -10
  186. package/docs/team-templates.md +2 -2
  187. package/docs/teams.md +3 -3
  188. package/docs/troubleshooting.md +10 -11
  189. package/package.json +6 -4
  190. package/src/__tests__/e2e/agent-spawn-visibility.e2e.test.ts +761 -0
  191. package/src/__tests__/e2e/full-agent-conflict-resolution.e2e.test.ts +2 -2
  192. package/src/__tests__/e2e/mcp-thin-client-bridge.e2e.test.ts +304 -0
  193. package/src/__tests__/e2e/mcp-tools-available.e2e.test.ts +324 -0
  194. package/src/__tests__/e2e/multi-agent.e2e.test.ts +5 -5
  195. package/src/__tests__/e2e/spawn-session-streaming.e2e.test.ts +563 -0
  196. package/src/acp/__tests__/integration.test.ts +56 -31
  197. package/src/acp/__tests__/macro-agent.test.ts +16 -7
  198. package/src/acp/macro-agent.ts +170 -36
  199. package/src/acp/types.ts +46 -1
  200. package/src/agent/__tests__/agent-manager.test.ts +228 -2
  201. package/src/agent/agent-manager.ts +714 -261
  202. package/src/agent/types.ts +3 -1
  203. package/src/api/server.ts +41 -7
  204. package/src/auth/__tests__/token.test.ts +100 -0
  205. package/src/auth/index.ts +1 -0
  206. package/src/auth/token.ts +82 -0
  207. package/src/cli/__tests__/acp.test.ts +1 -1
  208. package/src/cli/__tests__/stable-instance-id.test.ts +1 -1
  209. package/src/cli/acp.ts +130 -72
  210. package/src/cli/index.ts +120 -14
  211. package/src/cli/mcp.ts +311 -207
  212. package/src/cli/parse-args.ts +54 -0
  213. package/src/cli/stable-instance-id.ts +14 -0
  214. package/src/config/project-config.ts +190 -27
  215. package/src/lifecycle/__tests__/cascade-termination.test.ts +1 -1
  216. package/src/map/adapter/__tests__/acp-over-map-cancel.test.ts +22 -4
  217. package/src/map/adapter/__tests__/acp-over-map-getmodels.test.ts +355 -0
  218. package/src/map/adapter/__tests__/acp-over-map-history.test.ts +263 -0
  219. package/src/map/adapter/__tests__/acp-over-map-persistence.e2e.test.ts +1 -1
  220. package/src/map/adapter/__tests__/event-broadcast.test.ts +420 -0
  221. package/src/map/adapter/__tests__/event-log.test.ts +527 -0
  222. package/src/map/adapter/__tests__/event-translator.test.ts +3 -3
  223. package/src/map/adapter/__tests__/extensions.test.ts +408 -0
  224. package/src/map/adapter/__tests__/map-adapter.test.ts +99 -0
  225. package/src/map/adapter/__tests__/mcp-bridge.test.ts +1187 -0
  226. package/src/map/adapter/__tests__/multi-client-broadcast.test.ts +711 -0
  227. package/src/map/adapter/__tests__/websocket-integration.test.ts +218 -0
  228. package/src/map/adapter/acp-over-map.ts +678 -66
  229. package/src/map/adapter/connection-manager.ts +3 -0
  230. package/src/map/adapter/event-log.ts +208 -0
  231. package/src/map/adapter/event-translator.ts +6 -6
  232. package/src/map/adapter/extensions/agent-lifecycle.ts +267 -0
  233. package/src/map/adapter/extensions/index.ts +60 -0
  234. package/src/map/adapter/extensions/mcp-bridge.ts +995 -0
  235. package/src/map/adapter/extensions/task.ts +11 -0
  236. package/src/map/adapter/extensions/update-metadata.ts +126 -0
  237. package/src/map/adapter/index.ts +28 -0
  238. package/src/map/adapter/interface.ts +2 -0
  239. package/src/map/adapter/map-adapter.ts +312 -47
  240. package/src/map/adapter/subscription-manager.ts +5 -1
  241. package/src/map/adapter/types.ts +2 -0
  242. package/src/mcp/__tests__/map-client.test.ts +386 -0
  243. package/src/mcp/__tests__/mcp-server-thin-client.test.ts +368 -0
  244. package/src/mcp/__tests__/mcp-server.test.ts +100 -1
  245. package/src/mcp/map-client.ts +177 -0
  246. package/src/mcp/mcp-server.ts +191 -100
  247. package/src/mcp/types.ts +6 -1
  248. package/src/metrics/metrics.ts +1 -1
  249. package/src/monitor/__tests__/stale-agent-flow.integration.test.ts +1 -1
  250. package/src/roles/__tests__/config-loader.test.ts +7 -7
  251. package/src/roles/capabilities.ts +17 -7
  252. package/src/roles/config-loader.ts +6 -6
  253. package/src/roles/registry.ts +2 -2
  254. package/src/server/__tests__/combined-server.test.ts +94 -21
  255. package/src/server/combined-server.ts +189 -33
  256. package/src/steering/__tests__/steering-integration.test.ts +1 -1
  257. package/src/store/__tests__/event-store.test.ts +196 -1
  258. package/src/store/__tests__/instance.test.ts +3 -3
  259. package/src/store/event-store.ts +80 -21
  260. package/src/store/types/agents.ts +15 -0
  261. package/src/store/types/events.ts +1 -1
  262. package/src/task/backend/__tests__/create-task-backend.test.ts +225 -0
  263. package/src/task/backend/__tests__/e2e/unified-tool-provider-opentasks.e2e.test.ts +524 -0
  264. package/src/task/backend/__tests__/unified-tool-provider.test.ts +579 -0
  265. package/src/task/backend/index.ts +156 -106
  266. package/src/task/backend/memory.ts +4 -0
  267. package/src/task/backend/opentasks/__tests__/backend.test.ts +968 -0
  268. package/src/task/backend/opentasks/__tests__/daemon-manager.test.ts +406 -0
  269. package/src/task/backend/opentasks/__tests__/mapping.test.ts +84 -0
  270. package/src/task/backend/opentasks/__tests__/opentasks-backend.e2e.test.ts +1338 -0
  271. package/src/task/backend/opentasks/backend.ts +1323 -0
  272. package/src/task/backend/opentasks/client.ts +652 -0
  273. package/src/task/backend/opentasks/daemon-manager.ts +253 -0
  274. package/src/task/backend/opentasks/index.ts +69 -0
  275. package/src/task/backend/opentasks/mapping.ts +94 -0
  276. package/src/task/backend/types.ts +42 -66
  277. package/src/task/backend/unified-tool-provider.ts +779 -0
  278. package/src/teams/__tests__/cross-subsystem.integration.test.ts +1 -1
  279. package/src/teams/team-loader.ts +3 -3
  280. package/src/teams/team-runtime.ts +2 -0
  281. package/test_fixtures/README.md +2 -3
  282. package/test_fixtures/fixtures/index.ts +0 -3
  283. package/test_fixtures/fixtures/projects/project-with-specs.ts +7 -149
  284. package/test_fixtures/fixtures/repos/index.ts +1 -3
  285. package/test_fixtures/fixtures/repos/temp-repo-factory.ts +0 -116
  286. package/test_fixtures/fixtures/repos/types.ts +0 -11
  287. package/test_fixtures/harness/__tests__/fixtures.test.ts +10 -102
  288. package/test_fixtures/harness/__tests__/temp-repo-and-simulator.test.ts +0 -33
  289. package/test_fixtures/harness/simulator/agent-simulator.ts +4 -4
  290. package/vitest.config.ts +1 -1
  291. package/vitest.e2e.config.ts +1 -1
  292. package/vitest.setup.ts +1 -30
  293. package/.macro-agent/teams/self-driving/prompts/grinder.md +0 -27
  294. package/.macro-agent/teams/self-driving/prompts/judge.md +0 -27
  295. package/.macro-agent/teams/self-driving/prompts/planner.md +0 -33
  296. package/.macro-agent/teams/self-driving/roles/grinder.yaml +0 -17
  297. package/.macro-agent/teams/self-driving/roles/judge.yaml +0 -24
  298. package/.macro-agent/teams/self-driving/roles/planner.yaml +0 -18
  299. package/.macro-agent/teams/self-driving/team.yaml +0 -103
  300. package/.macro-agent/teams/structured/prompts/developer.md +0 -26
  301. package/.macro-agent/teams/structured/prompts/lead.md +0 -25
  302. package/.macro-agent/teams/structured/prompts/reviewer.md +0 -24
  303. package/.macro-agent/teams/structured/roles/developer.yaml +0 -12
  304. package/.macro-agent/teams/structured/roles/lead.yaml +0 -11
  305. package/.macro-agent/teams/structured/roles/reviewer.yaml +0 -19
  306. package/.macro-agent/teams/structured/team.yaml +0 -89
  307. package/docs/sudocode-integration.md +0 -383
  308. package/src/task/backend/__tests__/backend-parity.test.ts +0 -451
  309. package/src/task/backend/__tests__/tool-provider-edge-cases.test.ts +0 -430
  310. package/src/task/backend/__tests__/tool-provider.test.ts +0 -983
  311. package/src/task/backend/sudocode/__tests__/backend-edge-cases.test.ts +0 -575
  312. package/src/task/backend/sudocode/__tests__/backend.test.ts +0 -1194
  313. package/src/task/backend/sudocode/__tests__/client-integration.test.ts +0 -418
  314. package/src/task/backend/sudocode/__tests__/client.test.ts +0 -345
  315. package/src/task/backend/sudocode/__tests__/e2e/backend.e2e.test.ts +0 -753
  316. package/src/task/backend/sudocode/__tests__/e2e/server-client.e2e.test.ts +0 -680
  317. package/src/task/backend/sudocode/__tests__/e2e-workflow.test.ts +0 -666
  318. package/src/task/backend/sudocode/__tests__/integration/standalone-client.integration.test.ts +0 -396
  319. package/src/task/backend/sudocode/__tests__/integration/sudocode-cli.integration.test.ts +0 -328
  320. package/src/task/backend/sudocode/__tests__/integration/test-utils.ts +0 -175
  321. package/src/task/backend/sudocode/__tests__/mapping-edge-cases.test.ts +0 -265
  322. package/src/task/backend/sudocode/__tests__/server-client.test.ts +0 -675
  323. package/src/task/backend/sudocode/__tests__/sync-policy-edge-cases.test.ts +0 -521
  324. package/src/task/backend/sudocode/__tests__/sync-policy.test.ts +0 -519
  325. package/src/task/backend/sudocode/__tests__/tools.test.ts +0 -471
  326. package/src/task/backend/sudocode/backend.ts +0 -1237
  327. package/src/task/backend/sudocode/client.ts +0 -515
  328. package/src/task/backend/sudocode/index.ts +0 -120
  329. package/src/task/backend/sudocode/mapping.ts +0 -93
  330. package/src/task/backend/sudocode/server-client.ts +0 -522
  331. package/src/task/backend/sudocode/standalone-client.ts +0 -623
  332. package/src/task/backend/sudocode/sync-policy.ts +0 -387
  333. package/src/task/backend/sudocode/tools.ts +0 -896
  334. package/src/task/backend/tool-provider.ts +0 -506
  335. package/test_fixtures/fixtures/sudocode/index.ts +0 -29
  336. package/test_fixtures/fixtures/sudocode/issues.ts +0 -185
  337. package/test_fixtures/fixtures/sudocode/specs.ts +0 -159
@@ -0,0 +1,711 @@
1
+ /**
2
+ * Multi-Client Event Broadcast E2E Test
3
+ *
4
+ * Spins up a REAL CombinedServer with in-memory EventStore, connects
5
+ * two WebSocket clients, and verifies that events emitted by one client's
6
+ * ACP activity are broadcast to the other client's subscription.
7
+ *
8
+ * This tests the full pipeline:
9
+ * Client B sends map/send (ACP envelope)
10
+ * → handleSend → handleACPOverMAP
11
+ * → emitEvent(message_sent)
12
+ * → processRequest → emitNotification → emitEvent(message_delivered)
13
+ * → emitEvent(message_delivered) [final response]
14
+ * → subscription match → sendToSession → WebSocket.send()
15
+ * → Client A receives map/event notification
16
+ */
17
+
18
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
19
+ import { WebSocket } from "ws";
20
+ import { createEventStore, type EventStore } from "../../../store/event-store.js";
21
+ import {
22
+ createAgentManager,
23
+ type AgentManager,
24
+ } from "../../../agent/agent-manager.js";
25
+ import { createTaskManager, type TaskManager } from "../../../task/task-manager.js";
26
+ import {
27
+ createMessageRouter,
28
+ type MessageRouter,
29
+ } from "../../../router/message-router.js";
30
+ import {
31
+ createCombinedServer,
32
+ type CombinedServer,
33
+ type CombinedServerServices,
34
+ } from "../../../server/combined-server.js";
35
+
36
+ // =============================================================================
37
+ // Helpers
38
+ // =============================================================================
39
+
40
+ function getPortFromUrl(url: string): number {
41
+ return parseInt(new URL(url).port, 10);
42
+ }
43
+
44
+ interface JsonRpcMessage {
45
+ jsonrpc: "2.0";
46
+ id?: number;
47
+ method?: string;
48
+ params?: unknown;
49
+ result?: unknown;
50
+ error?: { code: number; message: string; data?: unknown };
51
+ }
52
+
53
+ /**
54
+ * WebSocket MAP client that handles both RPC responses AND notifications.
55
+ */
56
+ class MAPTestClient {
57
+ private ws!: WebSocket;
58
+ private waiters: Map<
59
+ number,
60
+ { resolve: (r: JsonRpcMessage) => void; reject: (e: Error) => void }
61
+ > = new Map();
62
+ private nextId = 1;
63
+ private url: string;
64
+
65
+ /** All received notifications (no `id` field) */
66
+ readonly notifications: JsonRpcMessage[] = [];
67
+
68
+ /** Resolvers waiting for a notification matching some predicate */
69
+ private notificationWaiters: Array<{
70
+ check: (msg: JsonRpcMessage) => boolean;
71
+ resolve: (msg: JsonRpcMessage) => void;
72
+ reject: (e: Error) => void;
73
+ timeout: ReturnType<typeof setTimeout>;
74
+ }> = [];
75
+
76
+ constructor(url: string) {
77
+ this.url = url;
78
+ }
79
+
80
+ async connect(): Promise<void> {
81
+ this.ws = new WebSocket(this.url);
82
+ return new Promise((resolve, reject) => {
83
+ const timeout = setTimeout(
84
+ () => reject(new Error("Connection timeout")),
85
+ 5000,
86
+ );
87
+ this.ws.on("open", () => {
88
+ clearTimeout(timeout);
89
+ resolve();
90
+ });
91
+ this.ws.on("error", (err) => {
92
+ clearTimeout(timeout);
93
+ reject(err);
94
+ });
95
+ this.ws.on("message", (data: Buffer) => {
96
+ try {
97
+ const msg = JSON.parse(data.toString()) as JsonRpcMessage;
98
+
99
+ if (msg.id != null) {
100
+ // RPC response
101
+ const waiter = this.waiters.get(msg.id);
102
+ if (waiter) {
103
+ this.waiters.delete(msg.id);
104
+ waiter.resolve(msg);
105
+ }
106
+ } else if (msg.method) {
107
+ // Notification (no id)
108
+ this.notifications.push(msg);
109
+
110
+ // Check pending notification waiters
111
+ for (let i = this.notificationWaiters.length - 1; i >= 0; i--) {
112
+ const w = this.notificationWaiters[i];
113
+ if (w.check(msg)) {
114
+ clearTimeout(w.timeout);
115
+ this.notificationWaiters.splice(i, 1);
116
+ w.resolve(msg);
117
+ }
118
+ }
119
+ }
120
+ } catch {
121
+ // ignore parse errors
122
+ }
123
+ });
124
+ });
125
+ }
126
+
127
+ async request(method: string, params?: unknown): Promise<JsonRpcMessage> {
128
+ const id = this.nextId++;
129
+ return new Promise((resolve, reject) => {
130
+ const timeout = setTimeout(() => {
131
+ this.waiters.delete(id);
132
+ reject(new Error(`Request timeout: ${method}`));
133
+ }, 30000);
134
+ this.waiters.set(id, {
135
+ resolve: (r) => {
136
+ clearTimeout(timeout);
137
+ resolve(r);
138
+ },
139
+ reject: (e) => {
140
+ clearTimeout(timeout);
141
+ reject(e);
142
+ },
143
+ });
144
+ this.ws.send(JSON.stringify({ jsonrpc: "2.0", method, params, id }));
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Wait for a notification matching the predicate.
150
+ * Checks already-received notifications first.
151
+ */
152
+ waitForNotification(
153
+ check: (msg: JsonRpcMessage) => boolean,
154
+ timeoutMs = 15000,
155
+ ): Promise<JsonRpcMessage> {
156
+ const existing = this.notifications.find(check);
157
+ if (existing) return Promise.resolve(existing);
158
+
159
+ return new Promise((resolve, reject) => {
160
+ const timeout = setTimeout(() => {
161
+ const idx = this.notificationWaiters.findIndex(
162
+ (w) => w.resolve === resolve,
163
+ );
164
+ if (idx >= 0) this.notificationWaiters.splice(idx, 1);
165
+ reject(
166
+ new Error(
167
+ `Timeout waiting for notification. Received ${this.notifications.length} notifications:\n` +
168
+ this.notifications
169
+ .map((n) => {
170
+ const p = n.params as Record<string, unknown> | undefined;
171
+ const evt = p?.event as Record<string, unknown> | undefined;
172
+ return ` ${n.method} → type=${evt?.type}`;
173
+ })
174
+ .join("\n"),
175
+ ),
176
+ );
177
+ }, timeoutMs);
178
+ this.notificationWaiters.push({ check, resolve, reject, timeout });
179
+ });
180
+ }
181
+
182
+ close(): void {
183
+ // Clean up pending waiters
184
+ for (const w of this.notificationWaiters) {
185
+ clearTimeout(w.timeout);
186
+ }
187
+ this.notificationWaiters.length = 0;
188
+
189
+ try {
190
+ this.ws?.close();
191
+ } catch {
192
+ // ignore
193
+ }
194
+ }
195
+ }
196
+
197
+ // =============================================================================
198
+ // Tests
199
+ // =============================================================================
200
+
201
+ describe("Multi-client event broadcast E2E", () => {
202
+ let eventStore: EventStore;
203
+ let agentManager: AgentManager;
204
+ let taskManager: TaskManager;
205
+ let messageRouter: MessageRouter;
206
+ let server: CombinedServer;
207
+ let port: number;
208
+ const clients: MAPTestClient[] = [];
209
+
210
+ beforeEach(async () => {
211
+ eventStore = await createEventStore({ inMemory: true });
212
+ messageRouter = createMessageRouter(eventStore);
213
+ taskManager = createTaskManager(eventStore);
214
+ agentManager = createAgentManager(eventStore, messageRouter, {
215
+ defaultPermissionMode: "auto-approve",
216
+ defaultCwd: process.cwd(),
217
+ });
218
+
219
+ const services: CombinedServerServices = {
220
+ eventStore,
221
+ agentManager,
222
+ taskManager,
223
+ messageRouter,
224
+ };
225
+
226
+ server = createCombinedServer(services, { port: 0, host: "localhost" });
227
+ await server.start();
228
+ port = getPortFromUrl(server.getUrl());
229
+ });
230
+
231
+ afterEach(async () => {
232
+ for (const c of clients) {
233
+ c.close();
234
+ }
235
+ clients.length = 0;
236
+ await server.stop().catch(() => {});
237
+ await agentManager.close();
238
+ await eventStore.close();
239
+ });
240
+
241
+ function createClient(): MAPTestClient {
242
+ const client = new MAPTestClient(`ws://localhost:${port}/map?token=${server.serverToken}`);
243
+ clients.push(client);
244
+ return client;
245
+ }
246
+
247
+ // Helper to check if a notification is a map/event with a specific type
248
+ function isEventOfType(type: string) {
249
+ return (msg: JsonRpcMessage) => {
250
+ if (msg.method !== "map/event") return false;
251
+ const params = msg.params as Record<string, unknown> | undefined;
252
+ const event = params?.event as Record<string, unknown> | undefined;
253
+ return event?.type === type;
254
+ };
255
+ }
256
+
257
+ /** Send ACP initialize + session/new via map/send, return the response. */
258
+ async function initializeAndCreateSession(
259
+ client: MAPTestClient,
260
+ streamId: string,
261
+ ): Promise<JsonRpcMessage> {
262
+ // ACP requires initialize before session/new
263
+ await client.request("map/send", {
264
+ to: { agent: "default" },
265
+ payload: {
266
+ acp: {
267
+ jsonrpc: "2.0",
268
+ id: 1,
269
+ method: "initialize",
270
+ params: {
271
+ clientInfo: { name: "test-client", version: "0.1.0" },
272
+ },
273
+ },
274
+ acpContext: { streamId, direction: "client-to-agent" },
275
+ },
276
+ });
277
+
278
+ return client.request("map/send", {
279
+ to: { agent: "default" },
280
+ payload: {
281
+ acp: {
282
+ jsonrpc: "2.0",
283
+ id: 2,
284
+ method: "session/new",
285
+ params: {},
286
+ },
287
+ acpContext: { streamId, direction: "client-to-agent" },
288
+ },
289
+ });
290
+ }
291
+
292
+ it("Client A receives agent_registered when Client B creates a session", { timeout: 30_000 }, async () => {
293
+ // Connect Client A and subscribe
294
+ const clientA = createClient();
295
+ await clientA.connect();
296
+ const subRes = await clientA.request("map/subscribe", {
297
+ filter: {
298
+ eventTypes: [
299
+ "agent_registered",
300
+ "message_sent",
301
+ "message_delivered",
302
+ "agent_state_changed",
303
+ ],
304
+ },
305
+ });
306
+ expect(subRes.error).toBeUndefined();
307
+ expect(subRes.result).toHaveProperty("subscriptionId");
308
+
309
+ // Connect Client B
310
+ const clientB = createClient();
311
+ await clientB.connect();
312
+
313
+ // Client B initializes ACP stream and creates a session
314
+ const streamId = `stream-${Date.now()}`;
315
+ const sessionNewRes = await initializeAndCreateSession(clientB, streamId);
316
+ console.log("session/new response:", JSON.stringify(sessionNewRes, null, 2));
317
+
318
+ // Client A should receive agent_registered notification
319
+ const agentEvent = await clientA.waitForNotification(
320
+ isEventOfType("agent_registered"),
321
+ 10000,
322
+ );
323
+
324
+ expect(agentEvent.method).toBe("map/event");
325
+ const params = agentEvent.params as Record<string, unknown>;
326
+ const event = params.event as Record<string, unknown>;
327
+ expect(event.type).toBe("agent_registered");
328
+
329
+ const data = event.data as Record<string, unknown>;
330
+ expect(data.agentId).toBeDefined();
331
+ console.log("Agent registered event data:", JSON.stringify(data, null, 2));
332
+ });
333
+
334
+ it("Client A receives message_sent when Client B sends ACP request", { timeout: 30_000 }, async () => {
335
+ // Connect and subscribe Client A
336
+ const clientA = createClient();
337
+ await clientA.connect();
338
+ await clientA.request("map/subscribe", {
339
+ filter: {
340
+ eventTypes: [
341
+ "agent_registered",
342
+ "message_sent",
343
+ "message_delivered",
344
+ ],
345
+ },
346
+ });
347
+
348
+ // Connect Client B and create a session first
349
+ const clientB = createClient();
350
+ await clientB.connect();
351
+
352
+ const streamId = `stream-${Date.now()}`;
353
+
354
+ // Initialize ACP stream
355
+ await clientB.request("map/send", {
356
+ to: { agent: "default" },
357
+ payload: {
358
+ acp: {
359
+ jsonrpc: "2.0",
360
+ id: 1,
361
+ method: "initialize",
362
+ params: {
363
+ clientInfo: { name: "test-client-b", version: "0.1.0" },
364
+ },
365
+ },
366
+ acpContext: {
367
+ streamId,
368
+ direction: "client-to-agent",
369
+ },
370
+ },
371
+ });
372
+
373
+ // Create new session (this also triggers agent_registered)
374
+ const sessionNewRes = await clientB.request("map/send", {
375
+ to: { agent: "default" },
376
+ payload: {
377
+ acp: {
378
+ jsonrpc: "2.0",
379
+ id: 2,
380
+ method: "session/new",
381
+ params: {},
382
+ },
383
+ acpContext: {
384
+ streamId,
385
+ direction: "client-to-agent",
386
+ },
387
+ },
388
+ });
389
+
390
+ console.log("session/new response:", JSON.stringify(sessionNewRes, null, 2));
391
+
392
+ // Wait for agent_registered first (confirms agent was created)
393
+ const agentEvent = await clientA.waitForNotification(
394
+ isEventOfType("agent_registered"),
395
+ 10000,
396
+ );
397
+ const agentData = (
398
+ (agentEvent.params as Record<string, unknown>).event as Record<
399
+ string,
400
+ unknown
401
+ >
402
+ ).data as Record<string, unknown>;
403
+ const agentId = agentData.agentId as string;
404
+ console.log("Created agent:", agentId);
405
+
406
+ // Client A should have received message_sent events for the ACP requests
407
+ // (initialize and session/new both emit message_sent)
408
+ const messageSentEvents = clientA.notifications.filter(
409
+ isEventOfType("message_sent"),
410
+ );
411
+ console.log(
412
+ `Client A received ${messageSentEvents.length} message_sent events`,
413
+ );
414
+ expect(messageSentEvents.length).toBeGreaterThanOrEqual(1);
415
+
416
+ // Verify the message_sent event has the ACP envelope in the data
417
+ const sentParams = messageSentEvents[0].params as Record<string, unknown>;
418
+ const sentEvent = sentParams.event as Record<string, unknown>;
419
+ const sentData = sentEvent.data as Record<string, unknown>;
420
+ expect(sentData.from).toBeDefined();
421
+ expect(sentData.to).toBeDefined();
422
+ expect(sentData.message).toBeDefined();
423
+
424
+ const message = sentData.message as Record<string, unknown>;
425
+ expect(message.payload).toBeDefined();
426
+
427
+ // The payload should be a valid ACP envelope
428
+ const payload = message.payload as Record<string, unknown>;
429
+ expect(payload.acp).toBeDefined();
430
+ expect(payload.acpContext).toBeDefined();
431
+
432
+ console.log("message_sent event data:", JSON.stringify(sentData, null, 2));
433
+ });
434
+
435
+ it("Client A receives message_delivered for ACP streaming updates", { timeout: 30_000 }, async () => {
436
+ // Connect and subscribe Client A
437
+ const clientA = createClient();
438
+ await clientA.connect();
439
+ await clientA.request("map/subscribe", {
440
+ filter: {
441
+ eventTypes: [
442
+ "agent_registered",
443
+ "message_sent",
444
+ "message_delivered",
445
+ ],
446
+ },
447
+ });
448
+
449
+ // Connect Client B
450
+ const clientB = createClient();
451
+ await clientB.connect();
452
+
453
+ const streamId = `stream-${Date.now()}`;
454
+
455
+ // Initialize
456
+ await clientB.request("map/send", {
457
+ to: { agent: "default" },
458
+ payload: {
459
+ acp: {
460
+ jsonrpc: "2.0",
461
+ id: 1,
462
+ method: "initialize",
463
+ params: {
464
+ clientInfo: { name: "test-client-b", version: "0.1.0" },
465
+ },
466
+ },
467
+ acpContext: {
468
+ streamId,
469
+ direction: "client-to-agent",
470
+ },
471
+ },
472
+ });
473
+
474
+ // Create session
475
+ const sessionRes = await clientB.request("map/send", {
476
+ to: { agent: "default" },
477
+ payload: {
478
+ acp: {
479
+ jsonrpc: "2.0",
480
+ id: 2,
481
+ method: "session/new",
482
+ params: {},
483
+ },
484
+ acpContext: {
485
+ streamId,
486
+ direction: "client-to-agent",
487
+ },
488
+ },
489
+ });
490
+
491
+ console.log("session/new response:", JSON.stringify(sessionRes, null, 2));
492
+
493
+ // Wait for agent_registered first
494
+ const agentEvent = await clientA.waitForNotification(
495
+ isEventOfType("agent_registered"),
496
+ 10000,
497
+ );
498
+ const agentData = (
499
+ (agentEvent.params as Record<string, unknown>).event as Record<
500
+ string,
501
+ unknown
502
+ >
503
+ ).data as Record<string, unknown>;
504
+ const agentId = agentData.agentId as string;
505
+
506
+ // Extract session ID from the session/new response
507
+ const sessionResult =
508
+ sessionRes.result as Record<string, unknown> | undefined;
509
+ let sessionId: string | undefined;
510
+ if (sessionResult?.delivered) {
511
+ // The response comes from sendMessage — extract sessionId from
512
+ // message_delivered events that should be in Client A's notifications
513
+ }
514
+
515
+ // Check message_delivered events — session/new should emit session info
516
+ // Wait a moment for streaming updates
517
+ await new Promise((r) => setTimeout(r, 1000));
518
+
519
+ const deliveredEvents = clientA.notifications.filter(
520
+ isEventOfType("message_delivered"),
521
+ );
522
+ console.log(
523
+ `Client A received ${deliveredEvents.length} message_delivered events`,
524
+ );
525
+
526
+ // There should be at least one message_delivered event
527
+ // (session info notification emitted during session/new processing)
528
+ expect(deliveredEvents.length).toBeGreaterThanOrEqual(1);
529
+
530
+ // Verify the delivered event has the proper structure
531
+ for (const evt of deliveredEvents) {
532
+ const p = evt.params as Record<string, unknown>;
533
+ const e = p.event as Record<string, unknown>;
534
+ const d = e.data as Record<string, unknown>;
535
+ expect(d.from).toBeDefined();
536
+ expect(d.to).toBeDefined();
537
+ expect(d.message).toBeDefined();
538
+
539
+ const msg = d.message as Record<string, unknown>;
540
+ expect(msg.payload).toBeDefined();
541
+
542
+ const payload = msg.payload as Record<string, unknown>;
543
+ expect(payload.acp).toBeDefined();
544
+ expect(payload.acpContext).toBeDefined();
545
+
546
+ console.log(
547
+ ` message_delivered: method=${(payload.acp as Record<string, unknown>).method ?? "(response)"}`,
548
+ );
549
+ }
550
+ });
551
+
552
+ it("Client A does NOT receive events for non-subscribed types", { timeout: 30_000 }, async () => {
553
+ // Client A subscribes only to agent_registered
554
+ const clientA = createClient();
555
+ await clientA.connect();
556
+ await clientA.request("map/subscribe", {
557
+ filter: { eventTypes: ["agent_registered"] },
558
+ });
559
+
560
+ // Client B sends ACP messages
561
+ const clientB = createClient();
562
+ await clientB.connect();
563
+
564
+ const streamId = `stream-${Date.now()}`;
565
+ await clientB.request("map/send", {
566
+ to: { agent: "default" },
567
+ payload: {
568
+ acp: {
569
+ jsonrpc: "2.0",
570
+ id: 1,
571
+ method: "initialize",
572
+ params: {
573
+ clientInfo: { name: "test-client-b", version: "0.1.0" },
574
+ },
575
+ },
576
+ acpContext: {
577
+ streamId,
578
+ direction: "client-to-agent",
579
+ },
580
+ },
581
+ });
582
+
583
+ await clientB.request("map/send", {
584
+ to: { agent: "default" },
585
+ payload: {
586
+ acp: {
587
+ jsonrpc: "2.0",
588
+ id: 2,
589
+ method: "session/new",
590
+ params: {},
591
+ },
592
+ acpContext: {
593
+ streamId,
594
+ direction: "client-to-agent",
595
+ },
596
+ },
597
+ });
598
+
599
+ // Wait for agent_registered (should arrive)
600
+ await clientA.waitForNotification(
601
+ isEventOfType("agent_registered"),
602
+ 10000,
603
+ );
604
+
605
+ // Give time for any other events to arrive
606
+ await new Promise((r) => setTimeout(r, 2000));
607
+
608
+ // Should NOT have received message_sent or message_delivered
609
+ const messageSent = clientA.notifications.filter(
610
+ isEventOfType("message_sent"),
611
+ );
612
+ const messageDelivered = clientA.notifications.filter(
613
+ isEventOfType("message_delivered"),
614
+ );
615
+
616
+ expect(messageSent).toHaveLength(0);
617
+ expect(messageDelivered).toHaveLength(0);
618
+ });
619
+
620
+ it("Both clients receive events when both are subscribed", { timeout: 30_000 }, async () => {
621
+ const clientA = createClient();
622
+ await clientA.connect();
623
+ await clientA.request("map/subscribe", {
624
+ filter: { eventTypes: ["agent_registered", "message_sent"] },
625
+ });
626
+
627
+ const clientB = createClient();
628
+ await clientB.connect();
629
+ await clientB.request("map/subscribe", {
630
+ filter: { eventTypes: ["agent_registered", "message_sent"] },
631
+ });
632
+
633
+ const streamId = `stream-${Date.now()}`;
634
+
635
+ // Client B initializes and creates session — both should see the events
636
+ await initializeAndCreateSession(clientB, streamId);
637
+
638
+ // Both should receive agent_registered
639
+ const eventA = await clientA.waitForNotification(
640
+ isEventOfType("agent_registered"),
641
+ 10000,
642
+ );
643
+ const eventB = await clientB.waitForNotification(
644
+ isEventOfType("agent_registered"),
645
+ 10000,
646
+ );
647
+
648
+ expect(eventA.method).toBe("map/event");
649
+ expect(eventB.method).toBe("map/event");
650
+
651
+ console.log(
652
+ `Client A notifications: ${clientA.notifications.length}`,
653
+ clientA.notifications.map((n) => {
654
+ const p = n.params as Record<string, unknown>;
655
+ const e = p?.event as Record<string, unknown>;
656
+ return e?.type;
657
+ }),
658
+ );
659
+ console.log(
660
+ `Client B notifications: ${clientB.notifications.length}`,
661
+ clientB.notifications.map((n) => {
662
+ const p = n.params as Record<string, unknown>;
663
+ const e = p?.event as Record<string, unknown>;
664
+ return e?.type;
665
+ }),
666
+ );
667
+ });
668
+
669
+ it("map/replay returns historical events", { timeout: 30_000 }, async () => {
670
+ // Client B creates an agent (generates events)
671
+ const clientB = createClient();
672
+ await clientB.connect();
673
+
674
+ const streamId = `stream-${Date.now()}`;
675
+ await initializeAndCreateSession(clientB, streamId);
676
+
677
+ // Wait for the request to be processed
678
+ await new Promise((r) => setTimeout(r, 2000));
679
+
680
+ // NOW Client A connects and replays history
681
+ const clientA = createClient();
682
+ await clientA.connect();
683
+
684
+ const replayRes = await clientA.request("map/replay", {
685
+ filter: {
686
+ eventTypes: [
687
+ "agent_registered",
688
+ "message_sent",
689
+ "message_delivered",
690
+ ],
691
+ },
692
+ limit: 100,
693
+ });
694
+
695
+ expect(replayRes.error).toBeUndefined();
696
+ const result = replayRes.result as {
697
+ events: Array<{ event: { type: string } }>;
698
+ hasMore: boolean;
699
+ };
700
+ expect(result.events).toBeDefined();
701
+ expect(result.events.length).toBeGreaterThan(0);
702
+
703
+ const eventTypes = result.events.map((e) => e.event.type);
704
+ console.log("Replayed event types:", eventTypes);
705
+
706
+ // Should include agent_registered from the session/new
707
+ expect(eventTypes).toContain("agent_registered");
708
+ // Should include message_sent (for the session/new ACP request)
709
+ expect(eventTypes).toContain("message_sent");
710
+ });
711
+ });