macro-agent 0.0.17 → 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 (338) 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 +17 -0
  6. package/dist/acp/macro-agent.d.ts.map +1 -1
  7. package/dist/acp/macro-agent.js +183 -55
  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 +23 -0
  53. package/dist/map/adapter/acp-over-map.d.ts.map +1 -1
  54. package/dist/map/adapter/acp-over-map.js +482 -55
  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 +4 -0
  95. package/dist/map/adapter/map-adapter.d.ts.map +1 -1
  96. package/dist/map/adapter/map-adapter.js +302 -30
  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 +7 -1
  131. package/dist/store/event-store.d.ts.map +1 -1
  132. package/dist/store/event-store.js +91 -8
  133. package/dist/store/event-store.js.map +1 -1
  134. package/dist/store/types/agents.d.ts +23 -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__/history.test.ts +8 -4
  197. package/src/acp/__tests__/integration.test.ts +56 -31
  198. package/src/acp/__tests__/macro-agent.test.ts +16 -7
  199. package/src/acp/macro-agent.ts +230 -62
  200. package/src/acp/types.ts +46 -1
  201. package/src/agent/__tests__/agent-manager.test.ts +228 -2
  202. package/src/agent/agent-manager.ts +714 -261
  203. package/src/agent/types.ts +3 -1
  204. package/src/api/server.ts +41 -7
  205. package/src/auth/__tests__/token.test.ts +100 -0
  206. package/src/auth/index.ts +1 -0
  207. package/src/auth/token.ts +82 -0
  208. package/src/cli/__tests__/acp.test.ts +1 -1
  209. package/src/cli/__tests__/stable-instance-id.test.ts +1 -1
  210. package/src/cli/acp.ts +130 -72
  211. package/src/cli/index.ts +120 -14
  212. package/src/cli/mcp.ts +311 -207
  213. package/src/cli/parse-args.ts +54 -0
  214. package/src/cli/stable-instance-id.ts +14 -0
  215. package/src/config/project-config.ts +190 -27
  216. package/src/lifecycle/__tests__/cascade-termination.test.ts +1 -1
  217. package/src/map/adapter/__tests__/acp-over-map-cancel.test.ts +820 -0
  218. package/src/map/adapter/__tests__/acp-over-map-getmodels.test.ts +355 -0
  219. package/src/map/adapter/__tests__/acp-over-map-history.test.ts +724 -2
  220. package/src/map/adapter/__tests__/acp-over-map-persistence.e2e.test.ts +1 -1
  221. package/src/map/adapter/__tests__/event-broadcast.test.ts +420 -0
  222. package/src/map/adapter/__tests__/event-log.test.ts +527 -0
  223. package/src/map/adapter/__tests__/event-translator.test.ts +3 -3
  224. package/src/map/adapter/__tests__/extensions.test.ts +408 -0
  225. package/src/map/adapter/__tests__/map-adapter.test.ts +99 -0
  226. package/src/map/adapter/__tests__/mcp-bridge.test.ts +1187 -0
  227. package/src/map/adapter/__tests__/multi-client-broadcast.test.ts +711 -0
  228. package/src/map/adapter/__tests__/websocket-integration.test.ts +218 -0
  229. package/src/map/adapter/acp-over-map.ts +777 -92
  230. package/src/map/adapter/connection-manager.ts +3 -0
  231. package/src/map/adapter/event-log.ts +208 -0
  232. package/src/map/adapter/event-translator.ts +6 -6
  233. package/src/map/adapter/extensions/agent-lifecycle.ts +267 -0
  234. package/src/map/adapter/extensions/index.ts +60 -0
  235. package/src/map/adapter/extensions/mcp-bridge.ts +995 -0
  236. package/src/map/adapter/extensions/task.ts +11 -0
  237. package/src/map/adapter/extensions/update-metadata.ts +126 -0
  238. package/src/map/adapter/index.ts +28 -0
  239. package/src/map/adapter/interface.ts +2 -0
  240. package/src/map/adapter/map-adapter.ts +373 -38
  241. package/src/map/adapter/subscription-manager.ts +5 -1
  242. package/src/map/adapter/types.ts +2 -0
  243. package/src/mcp/__tests__/map-client.test.ts +386 -0
  244. package/src/mcp/__tests__/mcp-server-thin-client.test.ts +368 -0
  245. package/src/mcp/__tests__/mcp-server.test.ts +100 -1
  246. package/src/mcp/map-client.ts +177 -0
  247. package/src/mcp/mcp-server.ts +191 -100
  248. package/src/mcp/types.ts +6 -1
  249. package/src/metrics/metrics.ts +1 -1
  250. package/src/monitor/__tests__/stale-agent-flow.integration.test.ts +1 -1
  251. package/src/roles/__tests__/config-loader.test.ts +7 -7
  252. package/src/roles/capabilities.ts +17 -7
  253. package/src/roles/config-loader.ts +6 -6
  254. package/src/roles/registry.ts +2 -2
  255. package/src/server/__tests__/combined-server.test.ts +94 -21
  256. package/src/server/combined-server.ts +189 -33
  257. package/src/steering/__tests__/steering-integration.test.ts +1 -1
  258. package/src/store/__tests__/event-store.test.ts +236 -1
  259. package/src/store/__tests__/instance.test.ts +3 -3
  260. package/src/store/event-store.ts +109 -8
  261. package/src/store/types/agents.ts +16 -0
  262. package/src/store/types/events.ts +1 -1
  263. package/src/task/backend/__tests__/create-task-backend.test.ts +225 -0
  264. package/src/task/backend/__tests__/e2e/unified-tool-provider-opentasks.e2e.test.ts +524 -0
  265. package/src/task/backend/__tests__/unified-tool-provider.test.ts +579 -0
  266. package/src/task/backend/index.ts +156 -106
  267. package/src/task/backend/memory.ts +4 -0
  268. package/src/task/backend/opentasks/__tests__/backend.test.ts +968 -0
  269. package/src/task/backend/opentasks/__tests__/daemon-manager.test.ts +406 -0
  270. package/src/task/backend/opentasks/__tests__/mapping.test.ts +84 -0
  271. package/src/task/backend/opentasks/__tests__/opentasks-backend.e2e.test.ts +1338 -0
  272. package/src/task/backend/opentasks/backend.ts +1323 -0
  273. package/src/task/backend/opentasks/client.ts +652 -0
  274. package/src/task/backend/opentasks/daemon-manager.ts +253 -0
  275. package/src/task/backend/opentasks/index.ts +69 -0
  276. package/src/task/backend/opentasks/mapping.ts +94 -0
  277. package/src/task/backend/types.ts +42 -66
  278. package/src/task/backend/unified-tool-provider.ts +779 -0
  279. package/src/teams/__tests__/cross-subsystem.integration.test.ts +1 -1
  280. package/src/teams/team-loader.ts +3 -3
  281. package/src/teams/team-runtime.ts +2 -0
  282. package/test_fixtures/README.md +2 -3
  283. package/test_fixtures/fixtures/index.ts +0 -3
  284. package/test_fixtures/fixtures/projects/project-with-specs.ts +7 -149
  285. package/test_fixtures/fixtures/repos/index.ts +1 -3
  286. package/test_fixtures/fixtures/repos/temp-repo-factory.ts +0 -116
  287. package/test_fixtures/fixtures/repos/types.ts +0 -11
  288. package/test_fixtures/harness/__tests__/fixtures.test.ts +10 -102
  289. package/test_fixtures/harness/__tests__/temp-repo-and-simulator.test.ts +0 -33
  290. package/test_fixtures/harness/simulator/agent-simulator.ts +4 -4
  291. package/vitest.config.ts +1 -1
  292. package/vitest.e2e.config.ts +1 -1
  293. package/vitest.setup.ts +1 -30
  294. package/.macro-agent/teams/self-driving/prompts/grinder.md +0 -27
  295. package/.macro-agent/teams/self-driving/prompts/judge.md +0 -27
  296. package/.macro-agent/teams/self-driving/prompts/planner.md +0 -33
  297. package/.macro-agent/teams/self-driving/roles/grinder.yaml +0 -17
  298. package/.macro-agent/teams/self-driving/roles/judge.yaml +0 -24
  299. package/.macro-agent/teams/self-driving/roles/planner.yaml +0 -18
  300. package/.macro-agent/teams/self-driving/team.yaml +0 -103
  301. package/.macro-agent/teams/structured/prompts/developer.md +0 -26
  302. package/.macro-agent/teams/structured/prompts/lead.md +0 -25
  303. package/.macro-agent/teams/structured/prompts/reviewer.md +0 -24
  304. package/.macro-agent/teams/structured/roles/developer.yaml +0 -12
  305. package/.macro-agent/teams/structured/roles/lead.yaml +0 -11
  306. package/.macro-agent/teams/structured/roles/reviewer.yaml +0 -19
  307. package/.macro-agent/teams/structured/team.yaml +0 -89
  308. package/docs/sudocode-integration.md +0 -383
  309. package/src/task/backend/__tests__/backend-parity.test.ts +0 -451
  310. package/src/task/backend/__tests__/tool-provider-edge-cases.test.ts +0 -430
  311. package/src/task/backend/__tests__/tool-provider.test.ts +0 -983
  312. package/src/task/backend/sudocode/__tests__/backend-edge-cases.test.ts +0 -575
  313. package/src/task/backend/sudocode/__tests__/backend.test.ts +0 -1194
  314. package/src/task/backend/sudocode/__tests__/client-integration.test.ts +0 -418
  315. package/src/task/backend/sudocode/__tests__/client.test.ts +0 -345
  316. package/src/task/backend/sudocode/__tests__/e2e/backend.e2e.test.ts +0 -753
  317. package/src/task/backend/sudocode/__tests__/e2e/server-client.e2e.test.ts +0 -680
  318. package/src/task/backend/sudocode/__tests__/e2e-workflow.test.ts +0 -666
  319. package/src/task/backend/sudocode/__tests__/integration/standalone-client.integration.test.ts +0 -396
  320. package/src/task/backend/sudocode/__tests__/integration/sudocode-cli.integration.test.ts +0 -328
  321. package/src/task/backend/sudocode/__tests__/integration/test-utils.ts +0 -175
  322. package/src/task/backend/sudocode/__tests__/mapping-edge-cases.test.ts +0 -265
  323. package/src/task/backend/sudocode/__tests__/server-client.test.ts +0 -675
  324. package/src/task/backend/sudocode/__tests__/sync-policy-edge-cases.test.ts +0 -521
  325. package/src/task/backend/sudocode/__tests__/sync-policy.test.ts +0 -519
  326. package/src/task/backend/sudocode/__tests__/tools.test.ts +0 -471
  327. package/src/task/backend/sudocode/backend.ts +0 -1237
  328. package/src/task/backend/sudocode/client.ts +0 -515
  329. package/src/task/backend/sudocode/index.ts +0 -120
  330. package/src/task/backend/sudocode/mapping.ts +0 -93
  331. package/src/task/backend/sudocode/server-client.ts +0 -522
  332. package/src/task/backend/sudocode/standalone-client.ts +0 -623
  333. package/src/task/backend/sudocode/sync-policy.ts +0 -387
  334. package/src/task/backend/sudocode/tools.ts +0 -896
  335. package/src/task/backend/tool-provider.ts +0 -506
  336. package/test_fixtures/fixtures/sudocode/index.ts +0 -29
  337. package/test_fixtures/fixtures/sudocode/issues.ts +0 -185
  338. 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
+ });