@swarmclawai/swarmclaw 0.8.4 → 0.8.7

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 (394) hide show
  1. package/README.md +9 -9
  2. package/bin/swarmclaw.js +5 -1
  3. package/bin/worker-cmd.js +73 -0
  4. package/package.json +2 -1
  5. package/src/app/api/agents/[id]/route.ts +17 -7
  6. package/src/app/api/agents/route.ts +21 -8
  7. package/src/app/api/approvals/route.test.ts +6 -6
  8. package/src/app/api/approvals/route.ts +2 -1
  9. package/src/app/api/auth/route.ts +2 -3
  10. package/src/app/api/chatrooms/[id]/chat/route.test.ts +299 -0
  11. package/src/app/api/chatrooms/[id]/chat/route.ts +3 -2
  12. package/src/app/api/chatrooms/[id]/route.ts +7 -6
  13. package/src/app/api/chats/[id]/chat/route.test.ts +496 -0
  14. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  15. package/src/app/api/chats/[id]/clear/route.ts +9 -9
  16. package/src/app/api/chats/[id]/devserver/route.ts +2 -1
  17. package/src/app/api/chats/[id]/edit-resend/route.ts +3 -4
  18. package/src/app/api/chats/[id]/fork/route.ts +3 -5
  19. package/src/app/api/chats/[id]/restore/route.ts +6 -7
  20. package/src/app/api/chats/[id]/retry/route.ts +3 -4
  21. package/src/app/api/chats/[id]/route.ts +61 -62
  22. package/src/app/api/chats/route.ts +7 -1
  23. package/src/app/api/connectors/[id]/route.ts +7 -8
  24. package/src/app/api/connectors/route.ts +5 -4
  25. package/src/app/api/eval/run/route.ts +2 -1
  26. package/src/app/api/eval/suite/route.ts +2 -1
  27. package/src/app/api/external-agents/route.test.ts +1 -1
  28. package/src/app/api/external-agents/route.ts +2 -2
  29. package/src/app/api/files/serve/route.ts +1 -1
  30. package/src/app/api/gateways/[id]/route.ts +7 -5
  31. package/src/app/api/gateways/route.ts +1 -1
  32. package/src/app/api/knowledge/upload/route.ts +1 -1
  33. package/src/app/api/logs/route.ts +5 -7
  34. package/src/app/api/memory-images/[filename]/route.ts +2 -3
  35. package/src/app/api/openclaw/agent-files/route.ts +4 -3
  36. package/src/app/api/openclaw/approvals/route.ts +3 -4
  37. package/src/app/api/openclaw/config-sync/route.ts +3 -2
  38. package/src/app/api/openclaw/cron/route.ts +3 -2
  39. package/src/app/api/openclaw/dotenv-keys/route.ts +2 -1
  40. package/src/app/api/openclaw/exec-config/route.ts +3 -2
  41. package/src/app/api/openclaw/gateway/route.ts +5 -4
  42. package/src/app/api/openclaw/history/route.ts +3 -2
  43. package/src/app/api/openclaw/media/route.ts +2 -1
  44. package/src/app/api/openclaw/permissions/route.ts +3 -2
  45. package/src/app/api/openclaw/sandbox-env/route.ts +3 -2
  46. package/src/app/api/openclaw/skills/install/route.ts +2 -1
  47. package/src/app/api/openclaw/skills/remove/route.ts +2 -1
  48. package/src/app/api/openclaw/skills/route.ts +3 -2
  49. package/src/app/api/orchestrator/run/route.ts +5 -14
  50. package/src/app/api/perf/route.ts +43 -0
  51. package/src/app/api/plugins/dependencies/route.ts +2 -1
  52. package/src/app/api/plugins/install/route.ts +2 -1
  53. package/src/app/api/plugins/marketplace/route.ts +3 -2
  54. package/src/app/api/plugins/settings/route.ts +2 -1
  55. package/src/app/api/preview-server/route.ts +11 -10
  56. package/src/app/api/projects/[id]/route.ts +1 -1
  57. package/src/app/api/schedules/[id]/route.test.ts +128 -0
  58. package/src/app/api/schedules/[id]/route.ts +43 -43
  59. package/src/app/api/schedules/[id]/run/route.ts +11 -62
  60. package/src/app/api/schedules/route.ts +21 -87
  61. package/src/app/api/settings/route.ts +2 -0
  62. package/src/app/api/setup/doctor/route.ts +9 -8
  63. package/src/app/api/tasks/[id]/approve/route.ts +33 -30
  64. package/src/app/api/tasks/[id]/route.ts +12 -35
  65. package/src/app/api/tasks/import/github/route.ts +2 -1
  66. package/src/app/api/tasks/route.ts +79 -91
  67. package/src/app/api/wallets/[id]/approve/route.ts +2 -1
  68. package/src/app/api/wallets/[id]/route.ts +13 -19
  69. package/src/app/api/wallets/[id]/send/route.ts +2 -1
  70. package/src/app/api/wallets/route.ts +2 -1
  71. package/src/app/api/webhooks/[id]/route.ts +2 -1
  72. package/src/app/api/webhooks/route.test.ts +3 -1
  73. package/src/app/page.tsx +23 -331
  74. package/src/cli/index.js +19 -0
  75. package/src/cli/index.ts +38 -7
  76. package/src/cli/spec.js +9 -0
  77. package/src/components/activity/activity-feed.tsx +7 -4
  78. package/src/components/agents/agent-card.tsx +32 -6
  79. package/src/components/agents/agent-chat-list.tsx +55 -22
  80. package/src/components/agents/agent-files-editor.tsx +3 -2
  81. package/src/components/agents/agent-sheet.tsx +123 -22
  82. package/src/components/agents/inspector-panel.tsx +1 -1
  83. package/src/components/agents/openclaw-skills-panel.tsx +2 -1
  84. package/src/components/agents/trash-list.tsx +1 -1
  85. package/src/components/auth/access-key-gate.tsx +8 -2
  86. package/src/components/auth/setup-wizard.tsx +10 -9
  87. package/src/components/auth/user-picker.tsx +3 -2
  88. package/src/components/chat/chat-area.tsx +20 -1
  89. package/src/components/chat/chat-card.tsx +18 -3
  90. package/src/components/chat/chat-header.tsx +24 -4
  91. package/src/components/chat/chat-list.tsx +2 -11
  92. package/src/components/chat/heartbeat-history-panel.tsx +2 -1
  93. package/src/components/chat/message-bubble.tsx +45 -6
  94. package/src/components/chat/message-list.tsx +280 -145
  95. package/src/components/chat/streaming-bubble.tsx +217 -60
  96. package/src/components/chat/swarm-panel.test.ts +274 -0
  97. package/src/components/chat/swarm-panel.tsx +410 -0
  98. package/src/components/chat/swarm-status-card.tsx +346 -0
  99. package/src/components/chat/tool-call-bubble.tsx +48 -23
  100. package/src/components/chatrooms/chatroom-list.tsx +8 -5
  101. package/src/components/chatrooms/chatroom-message.tsx +10 -7
  102. package/src/components/chatrooms/chatroom-view.tsx +12 -9
  103. package/src/components/connectors/connector-health.tsx +6 -4
  104. package/src/components/connectors/connector-list.tsx +16 -11
  105. package/src/components/connectors/connector-sheet.tsx +12 -6
  106. package/src/components/home/home-view.tsx +38 -24
  107. package/src/components/input/chat-input.tsx +10 -1
  108. package/src/components/layout/app-layout.tsx +2 -38
  109. package/src/components/layout/sheet-layer.tsx +50 -0
  110. package/src/components/mcp-servers/mcp-server-list.tsx +37 -5
  111. package/src/components/mcp-servers/mcp-server-sheet.tsx +12 -2
  112. package/src/components/plugins/plugin-list.tsx +8 -4
  113. package/src/components/plugins/plugin-sheet.tsx +2 -1
  114. package/src/components/providers/provider-list.tsx +3 -2
  115. package/src/components/providers/provider-sheet.tsx +2 -1
  116. package/src/components/runs/run-list.tsx +11 -7
  117. package/src/components/schedules/schedule-card.tsx +5 -3
  118. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  119. package/src/components/shared/attachment-chip.tsx +19 -3
  120. package/src/components/shared/notification-center.tsx +6 -3
  121. package/src/components/shared/settings/plugin-manager.tsx +3 -2
  122. package/src/components/shared/settings/section-embedding.tsx +2 -1
  123. package/src/components/shared/settings/section-orchestrator.tsx +2 -1
  124. package/src/components/shared/settings/section-user-preferences.tsx +107 -0
  125. package/src/components/shared/settings/settings-page.tsx +13 -9
  126. package/src/components/skills/clawhub-browser.tsx +15 -4
  127. package/src/components/skills/skill-list.tsx +15 -4
  128. package/src/components/tasks/approvals-panel.tsx +2 -1
  129. package/src/components/tasks/task-board.tsx +35 -37
  130. package/src/components/tasks/task-sheet.tsx +4 -3
  131. package/src/components/ui/full-screen-loader.tsx +164 -0
  132. package/src/components/wallets/wallet-approval-dialog.tsx +2 -1
  133. package/src/components/wallets/wallet-panel.tsx +6 -5
  134. package/src/components/wallets/wallet-section.tsx +3 -2
  135. package/src/components/webhooks/webhook-list.tsx +4 -5
  136. package/src/components/webhooks/webhook-sheet.tsx +6 -6
  137. package/src/hooks/use-app-bootstrap.ts +202 -0
  138. package/src/hooks/use-mounted-ref.ts +14 -0
  139. package/src/hooks/use-now.ts +31 -0
  140. package/src/hooks/use-openclaw-gateway.ts +2 -1
  141. package/src/instrumentation.ts +20 -8
  142. package/src/lib/agent-default-tools.test.ts +52 -0
  143. package/src/lib/agent-default-tools.ts +40 -0
  144. package/src/lib/api-client.test.ts +21 -0
  145. package/src/lib/api-client.ts +6 -11
  146. package/src/lib/canvas-content.test.ts +360 -0
  147. package/src/lib/chat-streaming-state.test.ts +49 -2
  148. package/src/lib/chat-streaming-state.ts +26 -10
  149. package/src/lib/fetch-timeout.test.ts +54 -0
  150. package/src/lib/fetch-timeout.ts +60 -3
  151. package/src/lib/live-tool-events.test.ts +77 -0
  152. package/src/lib/live-tool-events.ts +73 -0
  153. package/src/lib/local-observability.test.ts +2 -2
  154. package/src/lib/openclaw-endpoint.test.ts +1 -1
  155. package/src/lib/providers/anthropic.ts +12 -16
  156. package/src/lib/providers/index.ts +4 -2
  157. package/src/lib/providers/ollama.ts +9 -6
  158. package/src/lib/providers/openai.ts +11 -14
  159. package/src/lib/runtime-env.test.ts +8 -8
  160. package/src/lib/schedule-dedupe-advanced.test.ts +2 -2
  161. package/src/lib/schedule-dedupe.test.ts +1 -1
  162. package/src/lib/schedule-dedupe.ts +3 -2
  163. package/src/lib/server/agent-thread-session.test.ts +6 -6
  164. package/src/lib/server/agent-thread-session.ts +6 -9
  165. package/src/lib/server/alert-dispatch.ts +2 -1
  166. package/src/lib/server/api-routes.test.ts +6 -6
  167. package/src/lib/server/approval-connector-notify.test.ts +4 -4
  168. package/src/lib/server/approvals-auto-approve.test.ts +29 -29
  169. package/src/lib/server/approvals.test.ts +317 -0
  170. package/src/lib/server/approvals.ts +5 -4
  171. package/src/lib/server/autonomy-runtime.test.ts +11 -11
  172. package/src/lib/server/browser-state.ts +2 -2
  173. package/src/lib/server/capability-router.test.ts +1 -1
  174. package/src/lib/server/capability-router.ts +3 -2
  175. package/src/lib/server/chat-execution-advanced.test.ts +15 -2
  176. package/src/lib/server/chat-execution-connector-delivery.ts +67 -0
  177. package/src/lib/server/chat-execution-disabled.test.ts +3 -3
  178. package/src/lib/server/chat-execution-eval-history.test.ts +3 -3
  179. package/src/lib/server/chat-execution-heartbeat.test.ts +42 -1
  180. package/src/lib/server/chat-execution-session-sync.test.ts +119 -0
  181. package/src/lib/server/chat-execution-tool-events.ts +116 -0
  182. package/src/lib/server/chat-execution-utils.test.ts +479 -0
  183. package/src/lib/server/chat-execution-utils.ts +533 -0
  184. package/src/lib/server/chat-execution.ts +153 -748
  185. package/src/lib/server/chat-streaming-utils.ts +174 -0
  186. package/src/lib/server/chat-turn-tool-routing.ts +310 -0
  187. package/src/lib/server/chatroom-session-persistence.test.ts +2 -2
  188. package/src/lib/server/clawhub-client.ts +2 -1
  189. package/src/lib/server/collection-helpers.test.ts +92 -0
  190. package/src/lib/server/collection-helpers.ts +25 -3
  191. package/src/lib/server/connectors/access.ts +146 -0
  192. package/src/lib/server/connectors/bluebubbles.test.ts +1 -1
  193. package/src/lib/server/connectors/bluebubbles.ts +4 -4
  194. package/src/lib/server/connectors/commands.ts +367 -0
  195. package/src/lib/server/connectors/connector-routing.test.ts +4 -4
  196. package/src/lib/server/connectors/delivery.ts +142 -0
  197. package/src/lib/server/connectors/discord.ts +37 -40
  198. package/src/lib/server/connectors/email.ts +11 -10
  199. package/src/lib/server/connectors/googlechat.ts +4 -4
  200. package/src/lib/server/connectors/inbound-audio-transcription.ts +2 -1
  201. package/src/lib/server/connectors/ingress-delivery.ts +23 -0
  202. package/src/lib/server/connectors/manager-roundtrip.test.ts +300 -0
  203. package/src/lib/server/connectors/manager.test.ts +352 -77
  204. package/src/lib/server/connectors/manager.ts +134 -673
  205. package/src/lib/server/connectors/matrix.ts +4 -4
  206. package/src/lib/server/connectors/message-sentinel.ts +7 -0
  207. package/src/lib/server/connectors/openclaw.test.ts +1 -1
  208. package/src/lib/server/connectors/openclaw.ts +8 -10
  209. package/src/lib/server/connectors/outbox.test.ts +192 -0
  210. package/src/lib/server/connectors/outbox.ts +369 -0
  211. package/src/lib/server/connectors/pairing.test.ts +18 -1
  212. package/src/lib/server/connectors/pairing.ts +49 -4
  213. package/src/lib/server/connectors/policy.ts +9 -3
  214. package/src/lib/server/connectors/reconnect-state.ts +71 -0
  215. package/src/lib/server/connectors/response-media.ts +256 -0
  216. package/src/lib/server/connectors/runtime-state.ts +67 -0
  217. package/src/lib/server/connectors/session.test.ts +357 -0
  218. package/src/lib/server/connectors/session.ts +422 -0
  219. package/src/lib/server/connectors/signal.ts +7 -7
  220. package/src/lib/server/connectors/slack.ts +43 -43
  221. package/src/lib/server/connectors/teams.ts +4 -4
  222. package/src/lib/server/connectors/telegram.ts +37 -43
  223. package/src/lib/server/connectors/types.ts +31 -1
  224. package/src/lib/server/connectors/whatsapp.test.ts +108 -0
  225. package/src/lib/server/connectors/whatsapp.ts +106 -34
  226. package/src/lib/server/context-manager.test.ts +409 -0
  227. package/src/lib/server/cost.test.ts +1 -1
  228. package/src/lib/server/daemon-policy.ts +78 -0
  229. package/src/lib/server/daemon-state-connectors.test.ts +167 -0
  230. package/src/lib/server/daemon-state.test.ts +283 -55
  231. package/src/lib/server/daemon-state.ts +106 -109
  232. package/src/lib/server/data-dir.test.ts +5 -5
  233. package/src/lib/server/data-dir.ts +4 -0
  234. package/src/lib/server/delegation-jobs-advanced.test.ts +1 -1
  235. package/src/lib/server/delegation-jobs.test.ts +87 -0
  236. package/src/lib/server/delegation-jobs.ts +42 -48
  237. package/src/lib/server/devserver-launch.ts +1 -1
  238. package/src/lib/server/document-utils.ts +7 -9
  239. package/src/lib/server/elevenlabs.ts +2 -1
  240. package/src/lib/server/embeddings.test.ts +105 -0
  241. package/src/lib/server/ethereum.ts +3 -2
  242. package/src/lib/server/eval/agent-regression.ts +3 -2
  243. package/src/lib/server/eval/runner.ts +2 -1
  244. package/src/lib/server/eval/scorer.ts +2 -1
  245. package/src/lib/server/evm-swap.ts +2 -1
  246. package/src/lib/server/gateway/protocol.test.ts +1 -1
  247. package/src/lib/server/guardian.ts +2 -1
  248. package/src/lib/server/heartbeat-blocked-suppression.test.ts +151 -0
  249. package/src/lib/server/heartbeat-service-timer.test.ts +6 -6
  250. package/src/lib/server/heartbeat-service.test.ts +406 -0
  251. package/src/lib/server/heartbeat-service.ts +54 -7
  252. package/src/lib/server/heartbeat-wake.test.ts +19 -0
  253. package/src/lib/server/heartbeat-wake.ts +17 -16
  254. package/src/lib/server/integrity-monitor.test.ts +149 -0
  255. package/src/lib/server/json-utils.ts +22 -0
  256. package/src/lib/server/knowledge-db.test.ts +13 -13
  257. package/src/lib/server/link-understanding.ts +2 -1
  258. package/src/lib/server/llm-response-cache.test.ts +1 -1
  259. package/src/lib/server/main-agent-loop-advanced.test.ts +65 -3
  260. package/src/lib/server/main-agent-loop.test.ts +6 -6
  261. package/src/lib/server/main-agent-loop.ts +21 -7
  262. package/src/lib/server/mcp-client.test.ts +1 -1
  263. package/src/lib/server/mcp-conformance.test.ts +1 -1
  264. package/src/lib/server/mcp-conformance.ts +3 -2
  265. package/src/lib/server/memory-consolidation.ts +2 -1
  266. package/src/lib/server/memory-db.test.ts +485 -0
  267. package/src/lib/server/memory-db.ts +39 -26
  268. package/src/lib/server/memory-graph.test.ts +2 -2
  269. package/src/lib/server/memory-policy.test.ts +7 -7
  270. package/src/lib/server/memory-retrieval.test.ts +1 -1
  271. package/src/lib/server/openclaw-config-sync.ts +2 -1
  272. package/src/lib/server/openclaw-deploy.test.ts +1 -1
  273. package/src/lib/server/openclaw-deploy.ts +8 -12
  274. package/src/lib/server/openclaw-exec-config.ts +2 -1
  275. package/src/lib/server/openclaw-gateway.ts +6 -7
  276. package/src/lib/server/openclaw-skills-normalize.ts +2 -1
  277. package/src/lib/server/openclaw-sync.ts +7 -5
  278. package/src/lib/server/orchestrator-lg-structure.test.ts +17 -0
  279. package/src/lib/server/orchestrator-lg.ts +199 -327
  280. package/src/lib/server/path-utils.ts +31 -0
  281. package/src/lib/server/perf.ts +161 -0
  282. package/src/lib/server/plugins-approval-guidance.ts +115 -0
  283. package/src/lib/server/plugins.test.ts +1 -1
  284. package/src/lib/server/plugins.ts +22 -132
  285. package/src/lib/server/process-manager.ts +5 -8
  286. package/src/lib/server/provider-health.test.ts +137 -0
  287. package/src/lib/server/provider-health.ts +3 -3
  288. package/src/lib/server/provider-model-discovery.ts +3 -12
  289. package/src/lib/server/queue-followups.test.ts +9 -9
  290. package/src/lib/server/queue-reconcile.test.ts +2 -2
  291. package/src/lib/server/queue-recovery.test.ts +269 -0
  292. package/src/lib/server/queue.test.ts +570 -0
  293. package/src/lib/server/queue.ts +62 -455
  294. package/src/lib/server/resolve-image.ts +30 -0
  295. package/src/lib/server/runtime-settings.test.ts +4 -4
  296. package/src/lib/server/runtime-storage-write-paths.test.ts +60 -0
  297. package/src/lib/server/schedule-normalization.test.ts +279 -0
  298. package/src/lib/server/schedule-service.ts +263 -0
  299. package/src/lib/server/scheduler.ts +17 -74
  300. package/src/lib/server/session-mailbox.test.ts +191 -0
  301. package/src/lib/server/session-run-manager.test.ts +640 -0
  302. package/src/lib/server/session-run-manager.ts +59 -15
  303. package/src/lib/server/session-tools/autonomy-tools.test.ts +20 -20
  304. package/src/lib/server/session-tools/calendar.ts +2 -1
  305. package/src/lib/server/session-tools/canvas.ts +2 -1
  306. package/src/lib/server/session-tools/chatroom.ts +2 -1
  307. package/src/lib/server/session-tools/connector.ts +26 -28
  308. package/src/lib/server/session-tools/context-mgmt.ts +3 -2
  309. package/src/lib/server/session-tools/crawl.ts +4 -3
  310. package/src/lib/server/session-tools/crud.ts +105 -324
  311. package/src/lib/server/session-tools/delegate-fallback.test.ts +9 -9
  312. package/src/lib/server/session-tools/delegate.ts +6 -8
  313. package/src/lib/server/session-tools/discovery-approvals.test.ts +15 -15
  314. package/src/lib/server/session-tools/discovery.ts +4 -3
  315. package/src/lib/server/session-tools/document.ts +2 -1
  316. package/src/lib/server/session-tools/email.ts +2 -1
  317. package/src/lib/server/session-tools/extract.ts +2 -1
  318. package/src/lib/server/session-tools/file.ts +4 -3
  319. package/src/lib/server/session-tools/http.ts +2 -1
  320. package/src/lib/server/session-tools/human-loop.ts +2 -1
  321. package/src/lib/server/session-tools/image-gen.ts +4 -3
  322. package/src/lib/server/session-tools/index.ts +26 -30
  323. package/src/lib/server/session-tools/mailbox.ts +2 -1
  324. package/src/lib/server/session-tools/manage-connectors.test.ts +4 -4
  325. package/src/lib/server/session-tools/manage-schedules.test.ts +12 -12
  326. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +5 -5
  327. package/src/lib/server/session-tools/manage-tasks.test.ts +2 -2
  328. package/src/lib/server/session-tools/monitor.ts +2 -1
  329. package/src/lib/server/session-tools/platform.ts +2 -1
  330. package/src/lib/server/session-tools/plugin-creator.ts +2 -1
  331. package/src/lib/server/session-tools/replicate.ts +3 -2
  332. package/src/lib/server/session-tools/session-tools-wiring.test.ts +6 -6
  333. package/src/lib/server/session-tools/shell.ts +4 -9
  334. package/src/lib/server/session-tools/subagent.ts +322 -170
  335. package/src/lib/server/session-tools/table.ts +6 -5
  336. package/src/lib/server/session-tools/wallet-tool.test.ts +3 -3
  337. package/src/lib/server/session-tools/wallet.ts +7 -6
  338. package/src/lib/server/session-tools/web-browser-config.test.ts +1 -0
  339. package/src/lib/server/session-tools/web-utils.ts +317 -0
  340. package/src/lib/server/session-tools/web.ts +62 -328
  341. package/src/lib/server/skill-prompt-budget.test.ts +1 -1
  342. package/src/lib/server/skills-normalize.ts +2 -1
  343. package/src/lib/server/storage-item-access.test.ts +302 -0
  344. package/src/lib/server/storage.ts +366 -314
  345. package/src/lib/server/stream-agent-chat.test.ts +82 -3
  346. package/src/lib/server/stream-agent-chat.ts +146 -510
  347. package/src/lib/server/stream-continuation.ts +412 -0
  348. package/src/lib/server/subagent-lineage.test.ts +647 -0
  349. package/src/lib/server/subagent-lineage.ts +435 -0
  350. package/src/lib/server/subagent-runtime.test.ts +484 -0
  351. package/src/lib/server/subagent-runtime.ts +419 -0
  352. package/src/lib/server/subagent-swarm.test.ts +391 -0
  353. package/src/lib/server/subagent-swarm.ts +564 -0
  354. package/src/lib/server/system-events.ts +3 -3
  355. package/src/lib/server/task-followups.test.ts +491 -0
  356. package/src/lib/server/task-followups.ts +391 -0
  357. package/src/lib/server/task-lifecycle.test.ts +205 -0
  358. package/src/lib/server/task-lifecycle.ts +200 -0
  359. package/src/lib/server/task-quality-gate.test.ts +1 -1
  360. package/src/lib/server/task-resume.ts +208 -0
  361. package/src/lib/server/task-service.test.ts +108 -0
  362. package/src/lib/server/task-service.ts +264 -0
  363. package/src/lib/server/task-validation.test.ts +1 -1
  364. package/src/lib/server/test-utils/run-with-temp-data-dir.ts +42 -0
  365. package/src/lib/server/tool-capability-policy.test.ts +2 -2
  366. package/src/lib/server/tool-capability-policy.ts +3 -2
  367. package/src/lib/server/tool-planning.ts +2 -1
  368. package/src/lib/server/tool-retry.ts +2 -3
  369. package/src/lib/server/wake-dispatcher.test.ts +303 -0
  370. package/src/lib/server/wake-dispatcher.ts +318 -0
  371. package/src/lib/server/wake-mode.test.ts +161 -0
  372. package/src/lib/server/wake-mode.ts +174 -0
  373. package/src/lib/server/wallet-service.ts +8 -9
  374. package/src/lib/server/watch-jobs.ts +2 -1
  375. package/src/lib/server/workspace-context.ts +2 -2
  376. package/src/lib/shared-utils.test.ts +142 -0
  377. package/src/lib/shared-utils.ts +62 -0
  378. package/src/lib/tool-event-summary.ts +2 -1
  379. package/src/lib/view-routes.test.ts +100 -0
  380. package/src/lib/wallet.test.ts +322 -6
  381. package/src/proxy.test.ts +4 -4
  382. package/src/proxy.ts +2 -3
  383. package/src/stores/set-if-changed.ts +40 -0
  384. package/src/stores/slices/agent-slice.ts +111 -0
  385. package/src/stores/slices/auth-slice.ts +25 -0
  386. package/src/stores/slices/data-slice.ts +301 -0
  387. package/src/stores/slices/index.ts +7 -0
  388. package/src/stores/slices/session-slice.ts +112 -0
  389. package/src/stores/slices/task-slice.ts +63 -0
  390. package/src/stores/slices/ui-slice.ts +192 -0
  391. package/src/stores/use-app-store.ts +17 -822
  392. package/src/stores/use-approval-store.ts +2 -1
  393. package/src/stores/use-chat-store.ts +8 -1
  394. package/src/types/index.ts +10 -0
package/README.md CHANGED
@@ -148,7 +148,7 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
148
148
  ```
149
149
 
150
150
  The installer resolves the latest stable release tag and installs that version by default.
151
- To pin a version: `SWARMCLAW_VERSION=v0.8.4 curl ... | bash`
151
+ To pin a version: `SWARMCLAW_VERSION=v0.8.7 curl ... | bash`
152
152
 
153
153
  Or run locally from the repo (friendly for non-technical users):
154
154
 
@@ -701,8 +701,8 @@ npm run update:easy # safe update helper for local installs
701
701
  SwarmClaw uses tag-based releases (`vX.Y.Z`) as the stable channel.
702
702
 
703
703
  ```bash
704
- # example minor release (v0.8.4 style)
705
- npm version minor
704
+ # example patch release (v0.8.7 style)
705
+ npm version patch
706
706
  git push origin main --follow-tags
707
707
  ```
708
708
 
@@ -711,14 +711,14 @@ On `v*` tags, GitHub Actions will:
711
711
  2. Create a GitHub Release
712
712
  3. Build and publish Docker images to `ghcr.io/swarmclawai/swarmclaw` (`:vX.Y.Z`, `:latest`, `:sha-*`)
713
713
 
714
- #### v0.8.4 Release Readiness Notes
714
+ #### v0.8.7 Release Readiness Notes
715
715
 
716
- Before shipping `v0.8.4`, confirm the following user-facing changes are reflected in docs:
716
+ Before shipping `v0.8.7`, confirm the following user-facing changes are reflected in docs:
717
717
 
718
- 1. Connector/runtime docs note that the current patch line includes the connector-manager follow-up fix already shipped in the latest app commit.
719
- 2. Chat/session docs still note that the chat index serves lightweight session summaries instead of full transcript payloads, and full messages are loaded from per-chat endpoints.
720
- 3. Operator/runtime docs still note that the daemon owns scheduler/queue startup; background services should be described from the daemon controls rather than as unconditional boot behavior.
721
- 4. Site and README install/version strings are updated to `v0.8.4`, including install snippets, release notes index text, and sidebar/footer labels.
718
+ 1. Install/update docs note that `v0.8.7` repairs the committed npm lockfile so `npm ci` succeeds again on clean GitHub Actions and operator installs.
719
+ 2. Site and README install/version strings are updated to `v0.8.7`, including install snippets, release notes index text, and sidebar/footer labels.
720
+ 3. Release notes make it explicit that this patch is a packaging/install integrity fix on top of the `v0.8.6` runtime changes, not a new behavior rollout.
721
+ 4. The release branch and `main` stay aligned so the shipped tag points at the same commit users see on the default branch.
722
722
 
723
723
  ## CLI
724
724
 
package/bin/swarmclaw.js CHANGED
@@ -110,11 +110,15 @@ async function main() {
110
110
  const argv = process.argv.slice(2)
111
111
  const top = argv[0]
112
112
 
113
- // Route 'server' and 'update' subcommands to CJS scripts (no TS dependency).
113
+ // Route 'server', 'worker', and 'update' subcommands to CJS scripts (no TS dependency).
114
114
  if (top === 'server') {
115
115
  require('./server-cmd.js').main()
116
116
  return
117
117
  }
118
+ if (top === 'worker') {
119
+ require('./worker-cmd.js').main()
120
+ return
121
+ }
118
122
  if (top === 'update') {
119
123
  require('./update-cmd.js').main()
120
124
  return
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+
4
+ const fs = require('node:fs')
5
+ const path = require('node:path')
6
+ const os = require('node:os')
7
+ const { spawn } = require('node:child_process')
8
+
9
+ function printHelp() {
10
+ const help = `
11
+ Usage: swarmclaw worker [options]
12
+
13
+ Starts a dedicated background worker process for SwarmClaw to process background
14
+ queues and tasks independently of the Next.js web application.
15
+
16
+ Options:
17
+ -h, --help Show this help message
18
+ `.trim()
19
+ console.log(help)
20
+ }
21
+
22
+ function main() {
23
+ const args = process.argv.slice(3)
24
+ for (const arg of args) {
25
+ if (arg === '-h' || arg === '--help') {
26
+ printHelp()
27
+ process.exit(0)
28
+ } else {
29
+ console.error(`[swarmclaw] Unknown argument: ${arg}`)
30
+ printHelp()
31
+ process.exit(1)
32
+ }
33
+ }
34
+
35
+ const SWARMCLAW_HOME = process.env.SWARMCLAW_HOME || path.join(os.homedir(), '.swarmclaw')
36
+ const DATA_DIR = path.join(SWARMCLAW_HOME, 'data')
37
+
38
+ process.env.DATA_DIR = DATA_DIR
39
+ process.env.SWARMCLAW_DAEMON_BACKGROUND_SERVICES = '1'
40
+ // Flag that tells Next.js NOT to start the HTTP/Websocket listener, just boot the daemon.
41
+ process.env.SWARMCLAW_WORKER_ONLY = '1'
42
+
43
+ console.log(`[swarmclaw] Starting dedicated background worker...`)
44
+ console.log(`[swarmclaw] Data directory: ${DATA_DIR}`)
45
+
46
+ // We reuse the built server.js but signal it to only run the daemon
47
+ const standaloneBase = path.join(SWARMCLAW_HOME, '.next', 'standalone')
48
+ let serverJs = path.join(standaloneBase, 'server.js')
49
+
50
+ if (!fs.existsSync(serverJs)) {
51
+ console.error('Standalone server.js not found. Try running: swarmclaw server --build')
52
+ process.exit(1)
53
+ }
54
+
55
+ const child = spawn(process.execPath, [serverJs], {
56
+ env: process.env,
57
+ stdio: 'inherit',
58
+ })
59
+
60
+ child.on('exit', (code) => {
61
+ process.exit(code || 0)
62
+ })
63
+
64
+ for (const sig of ['SIGINT', 'SIGTERM']) {
65
+ process.on(sig, () => child.kill(sig))
66
+ }
67
+ }
68
+
69
+ if (require.main === module) {
70
+ main()
71
+ }
72
+
73
+ module.exports = { main }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "0.8.4",
3
+ "version": "0.8.7",
4
4
  "description": "Self-hosted AI agent orchestration dashboard — manage LLM providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -50,6 +50,7 @@
50
50
  "start": "node .next/standalone/server.js",
51
51
  "start:standalone": "node .next/standalone/server.js",
52
52
  "smoke:browser": "node ./scripts/browser-route-smoke.mjs",
53
+ "smoke:browser:workbench": "node ./scripts/browser-workbench-smoke.mjs",
53
54
  "benchmark:autonomy": "node ./scripts/benchmark-autonomy-harness.mjs",
54
55
  "benchmark:agent-regression": "node --import tsx ./scripts/run-agent-regression-suite.ts",
55
56
  "lint": "eslint",
@@ -1,16 +1,21 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadAgents, saveAgents, loadSessions, saveSessions, logActivity } from '@/lib/server/storage'
2
+ import { loadAgents, saveAgents, loadSessions, logActivity, upsertStoredItem, upsertStoredItems } from '@/lib/server/storage'
3
3
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
4
4
  import { mutateItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
5
+ import { ensureAgentThreadSession } from '@/lib/server/agent-thread-session'
5
6
 
6
7
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
- const ops: CollectionOps<any> = { load: () => loadAgents({ includeTrashed: true }), save: saveAgents, topic: 'agents' }
8
+ const ops: CollectionOps<any> = { load: () => loadAgents({ includeTrashed: true }), save: saveAgents, topic: 'agents', table: 'agents' }
8
9
 
9
10
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
10
11
  const { id } = await params
11
12
  const body = await req.json()
12
13
  const result = mutateItem(ops, id, (agent) => {
13
14
  Object.assign(agent, body, { updatedAt: Date.now() })
15
+ if (Array.isArray(body.plugins) || Array.isArray(body.tools)) {
16
+ agent.plugins = Array.isArray(body.plugins) ? body.plugins : body.tools
17
+ delete (agent as Record<string, unknown>).tools
18
+ }
14
19
  if (body.platformAssignScope === 'all' || body.platformAssignScope === 'self') {
15
20
  agent.platformAssignScope = body.platformAssignScope
16
21
  agent.isOrchestrator = body.platformAssignScope === 'all'
@@ -64,6 +69,10 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
64
69
  })
65
70
  if (!result) return notFound()
66
71
 
72
+ if (result.threadSessionId) {
73
+ ensureAgentThreadSession(id)
74
+ }
75
+
67
76
  if (result.threadSessionId) {
68
77
  const sessions = loadSessions()
69
78
  const shortcut = sessions[result.threadSessionId]
@@ -77,7 +86,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
77
86
  shortcut.shortcutForAgentId = id
78
87
  changed = true
79
88
  }
80
- if (changed) saveSessions(sessions)
89
+ if (changed) upsertStoredItem('sessions', shortcut.id, shortcut)
81
90
  }
82
91
  }
83
92
 
@@ -97,15 +106,16 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
97
106
 
98
107
  // Detach sessions from the trashed agent
99
108
  const sessions = loadSessions()
100
- let detachedSessions = 0
109
+ const detached: Array<[string, unknown]> = []
101
110
  for (const session of Object.values(sessions) as Array<Record<string, unknown>>) {
102
111
  if (!session || session.agentId !== id) continue
103
112
  session.agentId = null
104
- detachedSessions += 1
113
+ detached.push([session.id as string, session])
105
114
  }
106
- if (detachedSessions > 0) {
107
- saveSessions(sessions)
115
+ if (detached.length > 0) {
116
+ upsertStoredItems('sessions', detached)
108
117
  }
118
+ const detachedSessions = detached.length
109
119
 
110
120
  return NextResponse.json({ ok: true, detachedSessions })
111
121
  }
@@ -1,9 +1,11 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { genId } from '@/lib/id'
3
- import { loadAgents, loadSessions, loadUsage, saveAgents, logActivity } from '@/lib/server/storage'
3
+ import { perf } from '@/lib/server/perf'
4
+ import { loadAgents, loadSessions, loadUsage, logActivity, upsertStoredItem } from '@/lib/server/storage'
4
5
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
5
6
  import { notify } from '@/lib/server/ws-hub'
6
7
  import { getAgentSpendWindows } from '@/lib/server/cost'
8
+ import { resolveAgentPluginSelection } from '@/lib/agent-default-tools'
7
9
  import { AgentCreateSchema, formatZodError } from '@/lib/validation/schemas'
8
10
  import { z } from 'zod'
9
11
  export const dynamic = 'force-dynamic'
@@ -15,6 +17,7 @@ async function ensureDaemonIfNeeded(source: string) {
15
17
 
16
18
 
17
19
  export async function GET(req: Request) {
20
+ const endPerf = perf.start('api', 'GET /api/agents')
18
21
  const agents = loadAgents()
19
22
  const sessions = loadSessions()
20
23
  const usage = loadUsage()
@@ -37,28 +40,38 @@ export async function GET(req: Request) {
37
40
 
38
41
  const { searchParams } = new URL(req.url)
39
42
  const limitParam = searchParams.get('limit')
40
- if (!limitParam) return NextResponse.json(agents)
43
+ if (!limitParam) {
44
+ endPerf({ count: Object.keys(agents).length })
45
+ return NextResponse.json(agents)
46
+ }
41
47
 
42
48
  const limit = Math.max(1, Number(limitParam) || 50)
43
49
  const offset = Math.max(0, Number(searchParams.get('offset')) || 0)
44
50
  const all = Object.values(agents).sort((a, b) => b.updatedAt - a.updatedAt)
45
51
  const items = all.slice(offset, offset + limit)
52
+ endPerf({ count: items.length, total: all.length })
46
53
  return NextResponse.json({ items, total: all.length, hasMore: offset + limit < all.length })
47
54
  }
48
55
 
49
56
  export async function POST(req: Request) {
50
57
  await ensureDaemonIfNeeded('api/agents:post')
51
58
  const raw = await req.json()
59
+ const rawRecord = raw && typeof raw === 'object' ? raw as Record<string, unknown> : null
52
60
  const parsed = AgentCreateSchema.safeParse(raw)
53
61
  if (!parsed.success) {
54
62
  return NextResponse.json(formatZodError(parsed.error as z.ZodError), { status: 400 })
55
63
  }
56
64
  const body = parsed.data
65
+ const plugins = resolveAgentPluginSelection({
66
+ hasExplicitPlugins: Boolean(rawRecord && Object.prototype.hasOwnProperty.call(rawRecord, 'plugins')),
67
+ hasExplicitTools: Boolean(rawRecord && Object.prototype.hasOwnProperty.call(rawRecord, 'tools')),
68
+ plugins: body.plugins,
69
+ tools: body.tools,
70
+ })
57
71
  const id = genId()
58
72
  const now = Date.now()
59
- const agents = loadAgents()
60
73
  const platformAssignScope = body.platformAssignScope
61
- agents[id] = {
74
+ const agent = {
62
75
  id,
63
76
  name: body.name,
64
77
  description: body.description,
@@ -80,7 +93,7 @@ export async function POST(req: Request) {
80
93
  isOrchestrator: platformAssignScope === 'all',
81
94
  platformAssignScope,
82
95
  subAgentIds: body.subAgentIds,
83
- plugins: body.plugins?.length ? body.plugins : (body.tools || []),
96
+ plugins,
84
97
  skills: body.skills,
85
98
  skillIds: body.skillIds,
86
99
  mcpServerIds: body.mcpServerIds,
@@ -113,8 +126,8 @@ export async function POST(req: Request) {
113
126
  createdAt: now,
114
127
  updatedAt: now,
115
128
  }
116
- saveAgents(agents)
117
- logActivity({ entityType: 'agent', entityId: id, action: 'created', actor: 'user', summary: `Agent created: "${agents[id].name}"` })
129
+ upsertStoredItem('agents', id, agent)
130
+ logActivity({ entityType: 'agent', entityId: id, action: 'created', actor: 'user', summary: `Agent created: "${agent.name}"` })
118
131
  notify('agents')
119
- return NextResponse.json(agents[id])
132
+ return NextResponse.json(agent)
120
133
  }
@@ -34,9 +34,9 @@ function runWithTempDataDir(script: string) {
34
34
 
35
35
  test('GET and POST /api/approvals smoke the pending approval flow end-to-end', () => {
36
36
  const output = runWithTempDataDir(`
37
- const storageMod = await import('./src/lib/server/storage.ts')
38
- const approvalsMod = await import('./src/lib/server/approvals.ts')
39
- const routeMod = await import('./src/app/api/approvals/route.ts')
37
+ const storageMod = await import('./src/lib/server/storage')
38
+ const approvalsMod = await import('./src/lib/server/approvals')
39
+ const routeMod = await import('./src/app/api/approvals/route')
40
40
  const storage = storageMod.default || storageMod
41
41
  const approvals = approvalsMod.default || approvalsMod
42
42
  const route = routeMod.default || routeMod
@@ -88,9 +88,9 @@ test('GET and POST /api/approvals smoke the pending approval flow end-to-end', (
88
88
 
89
89
  test('POST /api/approvals rejects invalid payloads and remains idempotent for repeated decisions', () => {
90
90
  const output = runWithTempDataDir(`
91
- const storageMod = await import('./src/lib/server/storage.ts')
92
- const approvalsMod = await import('./src/lib/server/approvals.ts')
93
- const routeMod = await import('./src/app/api/approvals/route.ts')
91
+ const storageMod = await import('./src/lib/server/storage')
92
+ const approvalsMod = await import('./src/lib/server/approvals')
93
+ const routeMod = await import('./src/app/api/approvals/route')
94
94
  const storage = storageMod.default || storageMod
95
95
  const approvals = approvalsMod.default || approvalsMod
96
96
  const route = routeMod.default || routeMod
@@ -1,5 +1,6 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { listPendingApprovals, submitDecision } from '@/lib/server/approvals'
3
+ import { errorMessage } from '@/lib/shared-utils'
3
4
 
4
5
  export const dynamic = 'force-dynamic'
5
6
 
@@ -17,6 +18,6 @@ export async function POST(req: Request) {
17
18
  await submitDecision(id, approved)
18
19
  return NextResponse.json({ ok: true })
19
20
  } catch (err: unknown) {
20
- return NextResponse.json({ error: err instanceof Error ? err.message : String(err) }, { status: 500 })
21
+ return NextResponse.json({ error: errorMessage(err) }, { status: 500 })
21
22
  }
22
23
  }
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
2
2
  import { validateAccessKey, isFirstTimeSetup, markSetupComplete } from '@/lib/server/storage'
3
3
  import { AUTH_COOKIE_NAME, getCookieValue } from '@/lib/auth'
4
4
  import { isProductionRuntime } from '@/lib/runtime-env'
5
+ import { hmrSingleton } from '@/lib/shared-utils'
5
6
  export const dynamic = 'force-dynamic'
6
7
 
7
8
  interface AuthAttemptEntry {
@@ -9,9 +10,7 @@ interface AuthAttemptEntry {
9
10
  lockedUntil: number
10
11
  }
11
12
 
12
- const authRateLimitMap = (
13
- (globalThis as Record<string, unknown>).__swarmclaw_auth_rate_limit__ ??= new Map()
14
- ) as Map<string, AuthAttemptEntry>
13
+ const authRateLimitMap = hmrSingleton('__swarmclaw_auth_rate_limit__', () => new Map<string, AuthAttemptEntry>())
15
14
 
16
15
  const MAX_ATTEMPTS = 5
17
16
  const LOCKOUT_MS = 15 * 60 * 1000
@@ -0,0 +1,299 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+
4
+ import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
5
+
6
+ test('chatroom route prevents duplicate chained replies when an already-queued agent is re-mentioned', () => {
7
+ const output = runWithTempDataDir<{
8
+ assistantCounts: Record<string, number>
9
+ startCounts: Record<string, number>
10
+ doneCounts: Record<string, number>
11
+ messageTexts: string[]
12
+ }>(`
13
+ const storageMod = await import('./src/lib/server/storage')
14
+ const providersMod = await import('@/lib/providers')
15
+ const routeMod = await import('./src/app/api/chatrooms/[id]/chat/route')
16
+ const streamMod = await import('./src/lib/server/stream-agent-chat')
17
+ const storage = storageMod.default || storageMod
18
+ const providers = providersMod.default || providersMod
19
+ const route = routeMod.default || routeMod
20
+ const stream = streamMod.default || streamMod
21
+
22
+ providers.PROVIDERS['chatroom-provider'] = {
23
+ id: 'chatroom-provider',
24
+ name: 'Chatroom Provider',
25
+ models: ['room-model'],
26
+ requiresApiKey: false,
27
+ requiresEndpoint: false,
28
+ handler: { streamChat: async () => '' },
29
+ }
30
+
31
+ const now = Date.now()
32
+ storage.saveAgents({
33
+ alpha: {
34
+ id: 'alpha',
35
+ name: 'Alpha',
36
+ provider: 'chatroom-provider',
37
+ model: 'room-model',
38
+ plugins: [],
39
+ createdAt: now,
40
+ updatedAt: now,
41
+ },
42
+ beta: {
43
+ id: 'beta',
44
+ name: 'Beta',
45
+ provider: 'chatroom-provider',
46
+ model: 'room-model',
47
+ plugins: [],
48
+ createdAt: now,
49
+ updatedAt: now,
50
+ },
51
+ })
52
+ storage.saveChatrooms({
53
+ room_1: {
54
+ id: 'room_1',
55
+ name: 'Workbench Room',
56
+ agentIds: ['alpha', 'beta'],
57
+ messages: [],
58
+ createdAt: now,
59
+ updatedAt: now,
60
+ chatMode: 'sequential',
61
+ autoAddress: false,
62
+ },
63
+ })
64
+
65
+ async function readSse(response) {
66
+ const reader = response.body.getReader()
67
+ const decoder = new TextDecoder()
68
+ let buffer = ''
69
+ const events = []
70
+ while (true) {
71
+ const { done, value } = await reader.read()
72
+ if (done) break
73
+ buffer += decoder.decode(value, { stream: true })
74
+ let idx = buffer.indexOf('\\n\\n')
75
+ while (idx !== -1) {
76
+ const chunk = buffer.slice(0, idx)
77
+ buffer = buffer.slice(idx + 2)
78
+ const line = chunk
79
+ .split('\\n')
80
+ .map((entry) => entry.trim())
81
+ .find((entry) => entry.startsWith('data: '))
82
+ if (line) {
83
+ events.push(JSON.parse(line.slice(6)))
84
+ }
85
+ idx = buffer.indexOf('\\n\\n')
86
+ }
87
+ }
88
+ return events
89
+ }
90
+
91
+ stream.setStreamAgentChatForTest(async (opts) => {
92
+ const agentId = opts.session?.agentId
93
+ if (agentId === 'alpha') {
94
+ const reply = '@Beta please double-check this. Alpha found the first issue.'
95
+ opts.write('data: ' + JSON.stringify({ t: 'r', text: reply }) + '\\n')
96
+ return { fullText: reply, finalResponse: reply }
97
+ }
98
+ if (agentId === 'beta') {
99
+ const reply = 'Beta checked it and confirmed the fix.'
100
+ opts.write('data: ' + JSON.stringify({ t: 'r', text: reply }) + '\\n')
101
+ return { fullText: reply, finalResponse: reply }
102
+ }
103
+ return { fullText: '', finalResponse: '' }
104
+ })
105
+
106
+ try {
107
+ const response = await route.POST(
108
+ new Request('http://local/api/chatrooms/room_1/chat', {
109
+ method: 'POST',
110
+ headers: { 'content-type': 'application/json' },
111
+ body: JSON.stringify({ senderId: 'user', text: '@Alpha @Beta coordinate on this fix.' }),
112
+ }),
113
+ { params: Promise.resolve({ id: 'room_1' }) },
114
+ )
115
+
116
+ const events = await readSse(response)
117
+ const chatroom = storage.loadChatrooms().room_1
118
+ const assistantMessages = chatroom.messages.filter((entry) => entry.role === 'assistant')
119
+ const assistantCounts = assistantMessages.reduce((acc, entry) => {
120
+ acc[entry.senderId] = (acc[entry.senderId] || 0) + 1
121
+ return acc
122
+ }, {})
123
+ const startCounts = events
124
+ .filter((entry) => entry.t === 'cr_agent_start')
125
+ .reduce((acc, entry) => {
126
+ acc[entry.agentId] = (acc[entry.agentId] || 0) + 1
127
+ return acc
128
+ }, {})
129
+ const doneCounts = events
130
+ .filter((entry) => entry.t === 'cr_agent_done')
131
+ .reduce((acc, entry) => {
132
+ acc[entry.agentId] = (acc[entry.agentId] || 0) + 1
133
+ return acc
134
+ }, {})
135
+
136
+ console.log(JSON.stringify({
137
+ assistantCounts,
138
+ startCounts,
139
+ doneCounts,
140
+ messageTexts: assistantMessages.map((entry) => entry.text),
141
+ }))
142
+ } finally {
143
+ stream.setStreamAgentChatForTest(null)
144
+ }
145
+ `, { prefix: 'swarmclaw-chatroom-route-test-' })
146
+
147
+ assert.deepEqual(output.assistantCounts, { alpha: 1, beta: 1 })
148
+ assert.deepEqual(output.startCounts, { alpha: 1, beta: 1 })
149
+ assert.deepEqual(output.doneCounts, { alpha: 1, beta: 1 })
150
+ assert.equal(output.messageTexts.some((text) => /double-check/i.test(text)), true)
151
+ assert.equal(output.messageTexts.some((text) => /confirmed the fix/i.test(text)), true)
152
+ })
153
+
154
+ test('chatroom route forwards tool activity and records one reply per participating agent', () => {
155
+ const output = runWithTempDataDir<{
156
+ toolCalls: string[]
157
+ toolResults: string[]
158
+ assistantCounts: Record<string, number>
159
+ agentOrder: string[]
160
+ }>(`
161
+ const storageMod = await import('./src/lib/server/storage')
162
+ const providersMod = await import('@/lib/providers')
163
+ const routeMod = await import('./src/app/api/chatrooms/[id]/chat/route')
164
+ const streamMod = await import('./src/lib/server/stream-agent-chat')
165
+ const storage = storageMod.default || storageMod
166
+ const providers = providersMod.default || providersMod
167
+ const route = routeMod.default || routeMod
168
+ const stream = streamMod.default || streamMod
169
+
170
+ providers.PROVIDERS['chatroom-tool-provider'] = {
171
+ id: 'chatroom-tool-provider',
172
+ name: 'Chatroom Tool Provider',
173
+ models: ['room-tool-model'],
174
+ requiresApiKey: false,
175
+ requiresEndpoint: false,
176
+ handler: { streamChat: async () => '' },
177
+ }
178
+
179
+ const now = Date.now()
180
+ storage.saveAgents({
181
+ alpha: {
182
+ id: 'alpha',
183
+ name: 'Alpha',
184
+ provider: 'chatroom-tool-provider',
185
+ model: 'room-tool-model',
186
+ plugins: ['shell'],
187
+ createdAt: now,
188
+ updatedAt: now,
189
+ },
190
+ beta: {
191
+ id: 'beta',
192
+ name: 'Beta',
193
+ provider: 'chatroom-tool-provider',
194
+ model: 'room-tool-model',
195
+ plugins: ['shell'],
196
+ createdAt: now,
197
+ updatedAt: now,
198
+ },
199
+ })
200
+ storage.saveChatrooms({
201
+ room_1: {
202
+ id: 'room_1',
203
+ name: 'Parallel Workbench Room',
204
+ agentIds: ['alpha', 'beta'],
205
+ messages: [],
206
+ createdAt: now,
207
+ updatedAt: now,
208
+ chatMode: 'parallel',
209
+ autoAddress: true,
210
+ },
211
+ })
212
+
213
+ async function readSse(response) {
214
+ const reader = response.body.getReader()
215
+ const decoder = new TextDecoder()
216
+ let buffer = ''
217
+ const events = []
218
+ while (true) {
219
+ const { done, value } = await reader.read()
220
+ if (done) break
221
+ buffer += decoder.decode(value, { stream: true })
222
+ let idx = buffer.indexOf('\\n\\n')
223
+ while (idx !== -1) {
224
+ const chunk = buffer.slice(0, idx)
225
+ buffer = buffer.slice(idx + 2)
226
+ const line = chunk
227
+ .split('\\n')
228
+ .map((entry) => entry.trim())
229
+ .find((entry) => entry.startsWith('data: '))
230
+ if (line) {
231
+ events.push(JSON.parse(line.slice(6)))
232
+ }
233
+ idx = buffer.indexOf('\\n\\n')
234
+ }
235
+ }
236
+ return events
237
+ }
238
+
239
+ stream.setStreamAgentChatForTest(async (opts) => {
240
+ const agentId = opts.session?.agentId
241
+ if (agentId === 'alpha') {
242
+ opts.write('data: ' + JSON.stringify({
243
+ t: 'tool_call',
244
+ toolName: 'shell',
245
+ toolInput: 'pwd',
246
+ toolCallId: 'alpha-shell',
247
+ }) + '\\n')
248
+ opts.write('data: ' + JSON.stringify({
249
+ t: 'tool_result',
250
+ toolName: 'shell',
251
+ toolOutput: process.env.WORKSPACE_DIR,
252
+ toolCallId: 'alpha-shell',
253
+ }) + '\\n')
254
+ const reply = 'Alpha inspected the workspace root and found the repo is ready.'
255
+ opts.write('data: ' + JSON.stringify({ t: 'r', text: reply }) + '\\n')
256
+ return { fullText: reply, finalResponse: reply }
257
+ }
258
+ if (agentId === 'beta') {
259
+ const reply = 'Beta reviewed the plugin path and agrees with Alpha.'
260
+ opts.write('data: ' + JSON.stringify({ t: 'r', text: reply }) + '\\n')
261
+ return { fullText: reply, finalResponse: reply }
262
+ }
263
+ return { fullText: '', finalResponse: '' }
264
+ })
265
+
266
+ try {
267
+ const response = await route.POST(
268
+ new Request('http://local/api/chatrooms/room_1/chat', {
269
+ method: 'POST',
270
+ headers: { 'content-type': 'application/json' },
271
+ body: JSON.stringify({ senderId: 'user', text: 'Please inspect the workspace and plugin path.' }),
272
+ }),
273
+ { params: Promise.resolve({ id: 'room_1' }) },
274
+ )
275
+
276
+ const events = await readSse(response)
277
+ const chatroom = storage.loadChatrooms().room_1
278
+ const assistantMessages = chatroom.messages.filter((entry) => entry.role === 'assistant')
279
+ const assistantCounts = assistantMessages.reduce((acc, entry) => {
280
+ acc[entry.senderId] = (acc[entry.senderId] || 0) + 1
281
+ return acc
282
+ }, {})
283
+
284
+ console.log(JSON.stringify({
285
+ toolCalls: events.filter((entry) => entry.t === 'tool_call').map((entry) => entry.toolName),
286
+ toolResults: events.filter((entry) => entry.t === 'tool_result').map((entry) => entry.toolOutput),
287
+ assistantCounts,
288
+ agentOrder: assistantMessages.map((entry) => entry.senderId),
289
+ }))
290
+ } finally {
291
+ stream.setStreamAgentChatForTest(null)
292
+ }
293
+ `, { prefix: 'swarmclaw-chatroom-route-tools-' })
294
+
295
+ assert.deepEqual(output.toolCalls, ['shell'])
296
+ assert.equal(output.toolResults.length, 1)
297
+ assert.deepEqual(output.assistantCounts, { alpha: 1, beta: 1 })
298
+ assert.deepEqual([...new Set(output.agentOrder)].sort(), ['alpha', 'beta'])
299
+ })