@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
@@ -25,6 +25,7 @@ import { applyAgentReactionsFromText } from '@/lib/server/chatroom-orchestration
25
25
  import { resolvePrimaryAgentRoute } from '@/lib/server/agent-runtime-config'
26
26
  import { shouldSuppressHiddenControlText, stripHiddenControlTokens } from '@/lib/server/assistant-control'
27
27
  import type { Chatroom, ChatroomMessage, Agent } from '@/types'
28
+ import { errorMessage } from '@/lib/shared-utils'
28
29
 
29
30
  export const dynamic = 'force-dynamic'
30
31
  export const maxDuration = 300
@@ -283,7 +284,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
283
284
  writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
284
285
  return []
285
286
  } catch (err: unknown) {
286
- const msg = err instanceof Error ? err.message : String(err)
287
+ const msg = errorMessage(err)
287
288
  markProviderFailure(agent.provider, msg)
288
289
  writeEvent({ t: 'err', text: `Agent ${agent.name} error: ${msg}`, agentId: agent.id })
289
290
  writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
@@ -336,7 +337,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
336
337
  }
337
338
 
338
339
  processAgents().catch((err) => {
339
- const msg = err instanceof Error ? err.message : String(err)
340
+ const msg = errorMessage(err)
340
341
  writeEvent({ t: 'err', text: msg })
341
342
  writeEvent({ t: 'done' })
342
343
  if (!closed) {
@@ -103,12 +103,13 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
103
103
  // Cascade: null out chatroomId on any connectors that reference this chatroom
104
104
  const connectors = loadConnectors()
105
105
  let connectorsDirty = false
106
- for (const connector of Object.values(connectors)) {
107
- if (connector.chatroomId === id) {
108
- connector.chatroomId = null
109
- connector.updatedAt = Date.now()
110
- connectorsDirty = true
111
- }
106
+ for (const rawConnector of Object.values(connectors)) {
107
+ if (!rawConnector || typeof rawConnector !== 'object') continue
108
+ const connector = rawConnector as { chatroomId?: string | null; updatedAt?: number }
109
+ if (connector.chatroomId !== id) continue
110
+ connector.chatroomId = null
111
+ connector.updatedAt = Date.now()
112
+ connectorsDirty = true
112
113
  }
113
114
  if (connectorsDirty) {
114
115
  saveConnectors(connectors)
@@ -0,0 +1,496 @@
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('chat route keeps long-lived user runs alive after stream disconnect and records perf', () => {
7
+ const output = runWithTempDataDir<{
8
+ responseReturnedBeforeProviderFinished: boolean
9
+ firstChunk: string
10
+ assistantReplies: string[]
11
+ runStatuses: string[]
12
+ queueLength: number
13
+ perfLabels: string[]
14
+ }>(`
15
+ const storageMod = await import('./src/lib/server/storage')
16
+ const providersMod = await import('@/lib/providers')
17
+ const routeMod = await import('./src/app/api/chats/[id]/chat/route')
18
+ const runsMod = await import('./src/lib/server/session-run-manager')
19
+ const perfMod = await import('./src/lib/server/perf')
20
+ const storage = storageMod.default || storageMod
21
+ const providers = providersMod.default || providersMod
22
+ const route = routeMod.default || routeMod
23
+ const runs = runsMod.default || runsMod
24
+ const perf = perfMod.perf || perfMod.default?.perf || perfMod.default || perfMod
25
+
26
+ let providerFinishedAt = 0
27
+ providers.PROVIDERS['workbench-provider'] = {
28
+ id: 'workbench-provider',
29
+ name: 'Workbench Provider',
30
+ models: ['wb-model'],
31
+ requiresApiKey: false,
32
+ requiresEndpoint: false,
33
+ handler: {
34
+ streamChat: async (opts) => {
35
+ await new Promise((resolve) => setTimeout(resolve, 120))
36
+ const reply = 'Long-lived work finished cleanly.'
37
+ opts.write('data: ' + JSON.stringify({ t: 'r', text: reply }) + '\\n')
38
+ providerFinishedAt = Date.now()
39
+ return reply
40
+ },
41
+ },
42
+ }
43
+
44
+ const now = Date.now()
45
+ storage.saveAgents({
46
+ agent_1: {
47
+ id: 'agent_1',
48
+ name: 'Workbench Agent',
49
+ provider: 'workbench-provider',
50
+ model: 'wb-model',
51
+ plugins: [],
52
+ createdAt: now,
53
+ updatedAt: now,
54
+ },
55
+ })
56
+ storage.saveSessions({
57
+ sess_1: {
58
+ id: 'sess_1',
59
+ name: 'Workbench Session',
60
+ cwd: process.env.WORKSPACE_DIR,
61
+ user: 'workbench',
62
+ provider: 'workbench-provider',
63
+ model: 'wb-model',
64
+ claudeSessionId: null,
65
+ messages: [],
66
+ createdAt: now,
67
+ lastActiveAt: now,
68
+ sessionType: 'human',
69
+ agentId: 'agent_1',
70
+ plugins: [],
71
+ },
72
+ })
73
+
74
+ perf.setEnabled(true)
75
+ perf.clearRecentEntries()
76
+
77
+ const startedAt = Date.now()
78
+ const response = await route.POST(
79
+ new Request('http://local/api/chats/sess_1/chat', {
80
+ method: 'POST',
81
+ headers: { 'content-type': 'application/json' },
82
+ body: JSON.stringify({ message: 'Please run the long-lived task.' }),
83
+ }),
84
+ { params: Promise.resolve({ id: 'sess_1' }) },
85
+ )
86
+ const returnedAt = Date.now()
87
+
88
+ const reader = response.body.getReader()
89
+ const decoder = new TextDecoder()
90
+ const firstRead = await reader.read()
91
+ const firstChunk = decoder.decode(firstRead.value || new Uint8Array())
92
+ await reader.cancel()
93
+
94
+ await new Promise((resolve) => setTimeout(resolve, 220))
95
+
96
+ const session = storage.loadSessions().sess_1
97
+ const assistantReplies = (session.messages || [])
98
+ .filter((entry) => entry.role === 'assistant')
99
+ .map((entry) => entry.text)
100
+ const runStatuses = runs.listRuns({ sessionId: 'sess_1' }).map((entry) => entry.status)
101
+ const queueState = runs.getSessionExecutionState('sess_1')
102
+ const perfLabels = perf.getRecentEntries()
103
+ .filter((entry) => entry.category === 'chat-execution' || entry.category === 'queue')
104
+ .map((entry) => entry.category + '/' + entry.label)
105
+
106
+ console.log(JSON.stringify({
107
+ responseReturnedBeforeProviderFinished: providerFinishedAt > 0 && returnedAt < providerFinishedAt,
108
+ firstChunk,
109
+ assistantReplies,
110
+ runStatuses,
111
+ queueLength: queueState.queueLength,
112
+ perfLabels,
113
+ responseLatencyMs: returnedAt - startedAt,
114
+ }))
115
+ `, { prefix: 'swarmclaw-chat-route-test-' })
116
+
117
+ assert.equal(output.responseReturnedBeforeProviderFinished, true)
118
+ assert.match(output.firstChunk, /\\"status\\":\\"queued\\"/)
119
+ assert.deepEqual(output.assistantReplies, ['Long-lived work finished cleanly.'])
120
+ assert.ok(output.runStatuses.includes('completed'))
121
+ assert.equal(output.queueLength, 0)
122
+ assert.ok(output.perfLabels.includes('chat-execution/executeSessionChatTurn'))
123
+ assert.ok(output.perfLabels.includes('chat-execution/llm-round-trip'))
124
+ })
125
+
126
+ test('chat route heartbeat runs stay internal and do not persist terminal ack text', () => {
127
+ const output = runWithTempDataDir<{
128
+ events: Array<{ t?: string; text?: string }>
129
+ assistantReplies: string[]
130
+ queueLength: number
131
+ }>(`
132
+ const storageMod = await import('./src/lib/server/storage')
133
+ const providersMod = await import('@/lib/providers')
134
+ const routeMod = await import('./src/app/api/chats/[id]/chat/route')
135
+ const runsMod = await import('./src/lib/server/session-run-manager')
136
+ const storage = storageMod.default || storageMod
137
+ const providers = providersMod.default || providersMod
138
+ const route = routeMod.default || routeMod
139
+ const runs = runsMod.default || runsMod
140
+
141
+ providers.PROVIDERS['heartbeat-provider'] = {
142
+ id: 'heartbeat-provider',
143
+ name: 'Heartbeat Provider',
144
+ models: ['hb-model'],
145
+ requiresApiKey: false,
146
+ requiresEndpoint: false,
147
+ handler: {
148
+ streamChat: async (opts) => {
149
+ opts.write('data: ' + JSON.stringify({ t: 'r', text: 'HEARTBEAT_OK' }) + '\\n')
150
+ return 'HEARTBEAT_OK'
151
+ },
152
+ },
153
+ }
154
+
155
+ const now = Date.now()
156
+ storage.saveAgents({
157
+ agent_1: {
158
+ id: 'agent_1',
159
+ name: 'Heartbeat Agent',
160
+ provider: 'heartbeat-provider',
161
+ model: 'hb-model',
162
+ plugins: [],
163
+ heartbeatEnabled: true,
164
+ createdAt: now,
165
+ updatedAt: now,
166
+ },
167
+ })
168
+ storage.saveSessions({
169
+ sess_1: {
170
+ id: 'sess_1',
171
+ name: 'Heartbeat Session',
172
+ cwd: process.env.WORKSPACE_DIR,
173
+ user: 'workbench',
174
+ provider: 'heartbeat-provider',
175
+ model: 'hb-model',
176
+ claudeSessionId: null,
177
+ messages: [],
178
+ createdAt: now,
179
+ lastActiveAt: now,
180
+ sessionType: 'human',
181
+ agentId: 'agent_1',
182
+ heartbeatEnabled: true,
183
+ plugins: [],
184
+ },
185
+ })
186
+
187
+ async function readSse(response) {
188
+ const reader = response.body.getReader()
189
+ const decoder = new TextDecoder()
190
+ let buffer = ''
191
+ const events = []
192
+ while (true) {
193
+ const { done, value } = await reader.read()
194
+ if (done) break
195
+ buffer += decoder.decode(value, { stream: true })
196
+ let idx = buffer.indexOf('\\n\\n')
197
+ while (idx !== -1) {
198
+ const chunk = buffer.slice(0, idx)
199
+ buffer = buffer.slice(idx + 2)
200
+ const line = chunk
201
+ .split('\\n')
202
+ .map((entry) => entry.trim())
203
+ .find((entry) => entry.startsWith('data: '))
204
+ if (line) {
205
+ events.push(JSON.parse(line.slice(6)))
206
+ }
207
+ idx = buffer.indexOf('\\n\\n')
208
+ }
209
+ }
210
+ return events
211
+ }
212
+
213
+ const response = await route.POST(
214
+ new Request('http://local/api/chats/sess_1/chat', {
215
+ method: 'POST',
216
+ headers: { 'content-type': 'application/json' },
217
+ body: JSON.stringify({ message: 'SWARM_HEARTBEAT_CHECK', internal: true }),
218
+ }),
219
+ { params: Promise.resolve({ id: 'sess_1' }) },
220
+ )
221
+
222
+ const events = await readSse(response)
223
+ const session = storage.loadSessions().sess_1
224
+ const assistantReplies = (session.messages || [])
225
+ .filter((entry) => entry.role === 'assistant')
226
+ .map((entry) => entry.text)
227
+
228
+ console.log(JSON.stringify({
229
+ events,
230
+ assistantReplies,
231
+ queueLength: runs.getSessionExecutionState('sess_1').queueLength,
232
+ }))
233
+ `, { prefix: 'swarmclaw-chat-route-heartbeat-' })
234
+
235
+ assert.equal(output.events[0]?.t, 'md')
236
+ assert.match(String(output.events[0]?.text || ''), /"internal":true/)
237
+ assert.match(String(output.events[0]?.text || ''), /"source":"heartbeat"/)
238
+ assert.equal(output.events.at(-1)?.t, 'done')
239
+ assert.equal(output.assistantReplies.some((text) => /HEARTBEAT_OK/i.test(text)), false)
240
+ assert.equal(output.queueLength, 0)
241
+ })
242
+
243
+ test('chat route queues a second user message behind the first run and completes both in order', () => {
244
+ const output = runWithTempDataDir<{
245
+ firstRunMeta: string
246
+ secondRunMeta: string
247
+ assistantReplies: string[]
248
+ runStatuses: string[]
249
+ }>(`
250
+ const storageMod = await import('./src/lib/server/storage')
251
+ const providersMod = await import('@/lib/providers')
252
+ const routeMod = await import('./src/app/api/chats/[id]/chat/route')
253
+ const runsMod = await import('./src/lib/server/session-run-manager')
254
+ const storage = storageMod.default || storageMod
255
+ const providers = providersMod.default || providersMod
256
+ const route = routeMod.default || routeMod
257
+ const runs = runsMod.default || runsMod
258
+
259
+ providers.PROVIDERS['queue-provider'] = {
260
+ id: 'queue-provider',
261
+ name: 'Queue Provider',
262
+ models: ['queue-model'],
263
+ requiresApiKey: false,
264
+ requiresEndpoint: false,
265
+ handler: {
266
+ streamChat: async (opts) => {
267
+ await new Promise((resolve) => setTimeout(resolve, 80))
268
+ const reply = 'Completed: ' + opts.message
269
+ opts.write('data: ' + JSON.stringify({ t: 'r', text: reply }) + '\\n')
270
+ return reply
271
+ },
272
+ },
273
+ }
274
+
275
+ const now = Date.now()
276
+ storage.saveAgents({
277
+ agent_1: {
278
+ id: 'agent_1',
279
+ name: 'Queue Agent',
280
+ provider: 'queue-provider',
281
+ model: 'queue-model',
282
+ plugins: [],
283
+ createdAt: now,
284
+ updatedAt: now,
285
+ },
286
+ })
287
+ storage.saveSessions({
288
+ sess_1: {
289
+ id: 'sess_1',
290
+ name: 'Queued Session',
291
+ cwd: process.env.WORKSPACE_DIR,
292
+ user: 'workbench',
293
+ provider: 'queue-provider',
294
+ model: 'queue-model',
295
+ claudeSessionId: null,
296
+ messages: [],
297
+ createdAt: now,
298
+ lastActiveAt: now,
299
+ sessionType: 'human',
300
+ agentId: 'agent_1',
301
+ plugins: [],
302
+ },
303
+ })
304
+
305
+ async function readSse(response) {
306
+ const reader = response.body.getReader()
307
+ const decoder = new TextDecoder()
308
+ let buffer = ''
309
+ const events = []
310
+ while (true) {
311
+ const { done, value } = await reader.read()
312
+ if (done) break
313
+ buffer += decoder.decode(value, { stream: true })
314
+ let idx = buffer.indexOf('\\n\\n')
315
+ while (idx !== -1) {
316
+ const chunk = buffer.slice(0, idx)
317
+ buffer = buffer.slice(idx + 2)
318
+ const line = chunk
319
+ .split('\\n')
320
+ .map((entry) => entry.trim())
321
+ .find((entry) => entry.startsWith('data: '))
322
+ if (line) {
323
+ events.push(JSON.parse(line.slice(6)))
324
+ }
325
+ idx = buffer.indexOf('\\n\\n')
326
+ }
327
+ }
328
+ return events
329
+ }
330
+
331
+ const response1 = await route.POST(
332
+ new Request('http://local/api/chats/sess_1/chat', {
333
+ method: 'POST',
334
+ headers: { 'content-type': 'application/json' },
335
+ body: JSON.stringify({ message: 'first queued message' }),
336
+ }),
337
+ { params: Promise.resolve({ id: 'sess_1' }) },
338
+ )
339
+ const response2 = await route.POST(
340
+ new Request('http://local/api/chats/sess_1/chat', {
341
+ method: 'POST',
342
+ headers: { 'content-type': 'application/json' },
343
+ body: JSON.stringify({ message: 'second queued message' }),
344
+ }),
345
+ { params: Promise.resolve({ id: 'sess_1' }) },
346
+ )
347
+
348
+ const [events1, events2] = await Promise.all([readSse(response1), readSse(response2)])
349
+ const session = storage.loadSessions().sess_1
350
+ const assistantReplies = (session.messages || [])
351
+ .filter((entry) => entry.role === 'assistant')
352
+ .map((entry) => entry.text)
353
+ console.log(JSON.stringify({
354
+ firstRunMeta: events1.find((entry) => entry.t === 'md')?.text || '',
355
+ secondRunMeta: events2.find((entry) => entry.t === 'md')?.text || '',
356
+ assistantReplies,
357
+ runStatuses: runs.listRuns({ sessionId: 'sess_1' }).map((entry) => entry.status),
358
+ }))
359
+ `, { prefix: 'swarmclaw-chat-route-plugins-' })
360
+
361
+ assert.match(output.firstRunMeta, /"position":0/)
362
+ assert.match(output.secondRunMeta, /"position":1/)
363
+ assert.deepEqual(output.assistantReplies, [
364
+ 'Completed: first queued message',
365
+ 'Completed: second queued message',
366
+ ])
367
+ assert.deepEqual(output.runStatuses, ['completed', 'completed'])
368
+ })
369
+
370
+ test('chat route forwards plugin-path tool activity when a plugin-enabled run uses streamAgentChat', () => {
371
+ const output = runWithTempDataDir<{
372
+ toolCalls: string[]
373
+ toolResults: string[]
374
+ assistantReplies: string[]
375
+ }>(`
376
+ const storageMod = await import('./src/lib/server/storage')
377
+ const providersMod = await import('@/lib/providers')
378
+ const routeMod = await import('./src/app/api/chats/[id]/chat/route')
379
+ const streamMod = await import('./src/lib/server/stream-agent-chat')
380
+ const storage = storageMod.default || storageMod
381
+ const providers = providersMod.default || providersMod
382
+ const route = routeMod.default || routeMod
383
+ const stream = streamMod.default || streamMod
384
+
385
+ providers.PROVIDERS['plugin-provider'] = {
386
+ id: 'plugin-provider',
387
+ name: 'Plugin Provider',
388
+ models: ['plugin-model'],
389
+ requiresApiKey: false,
390
+ requiresEndpoint: false,
391
+ handler: { streamChat: async () => '' },
392
+ }
393
+
394
+ const now = Date.now()
395
+ storage.saveAgents({
396
+ agent_1: {
397
+ id: 'agent_1',
398
+ name: 'Plugin Agent',
399
+ provider: 'plugin-provider',
400
+ model: 'plugin-model',
401
+ plugins: ['web'],
402
+ createdAt: now,
403
+ updatedAt: now,
404
+ },
405
+ })
406
+ storage.saveSessions({
407
+ sess_1: {
408
+ id: 'sess_1',
409
+ name: 'Plugin Session',
410
+ cwd: process.env.WORKSPACE_DIR,
411
+ user: 'workbench',
412
+ provider: 'plugin-provider',
413
+ model: 'plugin-model',
414
+ claudeSessionId: null,
415
+ messages: [],
416
+ createdAt: now,
417
+ lastActiveAt: now,
418
+ sessionType: 'human',
419
+ agentId: 'agent_1',
420
+ plugins: ['web'],
421
+ },
422
+ })
423
+
424
+ async function readSse(response) {
425
+ const reader = response.body.getReader()
426
+ const decoder = new TextDecoder()
427
+ let buffer = ''
428
+ const events = []
429
+ while (true) {
430
+ const { done, value } = await reader.read()
431
+ if (done) break
432
+ buffer += decoder.decode(value, { stream: true })
433
+ let idx = buffer.indexOf('\\n\\n')
434
+ while (idx !== -1) {
435
+ const chunk = buffer.slice(0, idx)
436
+ buffer = buffer.slice(idx + 2)
437
+ const line = chunk
438
+ .split('\\n')
439
+ .map((entry) => entry.trim())
440
+ .find((entry) => entry.startsWith('data: '))
441
+ if (line) {
442
+ events.push(JSON.parse(line.slice(6)))
443
+ }
444
+ idx = buffer.indexOf('\\n\\n')
445
+ }
446
+ }
447
+ return events
448
+ }
449
+
450
+ stream.setStreamAgentChatForTest(async (opts) => {
451
+ opts.write('data: ' + JSON.stringify({
452
+ t: 'tool_call',
453
+ toolName: 'web',
454
+ toolInput: JSON.stringify({ q: 'queue health' }),
455
+ toolCallId: 'tool-1',
456
+ }) + '\\n')
457
+ opts.write('data: ' + JSON.stringify({
458
+ t: 'tool_result',
459
+ toolName: 'web',
460
+ toolOutput: 'Fetched queue health summary',
461
+ toolCallId: 'tool-1',
462
+ }) + '\\n')
463
+ const reply = 'Queue looks healthy and no plugin errors were observed.'
464
+ opts.write('data: ' + JSON.stringify({ t: 'r', text: reply }) + '\\n')
465
+ return { fullText: reply, finalResponse: reply }
466
+ })
467
+
468
+ try {
469
+ const response = await route.POST(
470
+ new Request('http://local/api/chats/sess_1/chat', {
471
+ method: 'POST',
472
+ headers: { 'content-type': 'application/json' },
473
+ body: JSON.stringify({ message: 'Check queue health with tools.' }),
474
+ }),
475
+ { params: Promise.resolve({ id: 'sess_1' }) },
476
+ )
477
+
478
+ const events = await readSse(response)
479
+ const session = storage.loadSessions().sess_1
480
+ const assistantReplies = (session.messages || [])
481
+ .filter((entry) => entry.role === 'assistant')
482
+ .map((entry) => entry.text)
483
+ console.log(JSON.stringify({
484
+ toolCalls: events.filter((entry) => entry.t === 'tool_call').map((entry) => entry.toolName),
485
+ toolResults: events.filter((entry) => entry.t === 'tool_result').map((entry) => entry.toolOutput),
486
+ assistantReplies,
487
+ }))
488
+ } finally {
489
+ stream.setStreamAgentChatForTest(null)
490
+ }
491
+ `, { prefix: 'swarmclaw-chat-route-plugin-events-' })
492
+
493
+ assert.deepEqual(output.toolCalls, ['web'])
494
+ assert.deepEqual(output.toolResults, ['Fetched queue health summary'])
495
+ assert.deepEqual(output.assistantReplies, ['Queue looks healthy and no plugin errors were observed.'])
496
+ })
@@ -26,6 +26,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
26
26
 
27
27
  const encoder = new TextEncoder()
28
28
  let abortRun: (() => void) | null = null
29
+ let unsubscribeRun: (() => void) | null = null
29
30
  const stream = new ReadableStream({
30
31
  start(controller) {
31
32
  let closed = false
@@ -54,6 +55,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
54
55
  callerSignal: internal ? req.signal : undefined,
55
56
  })
56
57
  abortRun = run.abort
58
+ unsubscribeRun = run.unsubscribe
57
59
 
58
60
  log.info('chat', `Enqueued session run ${run.runId}`, {
59
61
  sessionId: id,
@@ -91,9 +93,11 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
91
93
  })
92
94
  },
93
95
  cancel() {
94
- // Client disconnected. User-facing runs continue in the background so
95
- // they can persist results even when the transport drops. Explicit stop
96
- // controls still cancel the run through the session run manager.
96
+ // Client disconnected always remove this subscriber's listener to
97
+ // prevent writes to a closed stream (and free the closure).
98
+ unsubscribeRun?.()
99
+ // User-facing runs continue in the background so they can persist
100
+ // results even when the transport drops. Internal runs are aborted.
97
101
  if (internal) abortRun?.()
98
102
  },
99
103
  })
@@ -1,20 +1,20 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadSessions, saveSessions } from '@/lib/server/storage'
2
+ import { loadSession, upsertSession } from '@/lib/server/storage'
3
3
  import { notFound } from '@/lib/server/collection-helpers'
4
4
 
5
5
  export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
6
6
  const { id } = await params
7
- const sessions = loadSessions()
8
- if (!sessions[id]) return notFound()
9
- sessions[id].messages = []
10
- sessions[id].claudeSessionId = null
11
- sessions[id].codexThreadId = null
12
- sessions[id].opencodeSessionId = null
13
- sessions[id].delegateResumeIds = {
7
+ const session = loadSession(id)
8
+ if (!session) return notFound()
9
+ session.messages = []
10
+ session.claudeSessionId = null
11
+ session.codexThreadId = null
12
+ session.opencodeSessionId = null
13
+ session.delegateResumeIds = {
14
14
  claudeCode: null,
15
15
  codex: null,
16
16
  opencode: null,
17
17
  }
18
- saveSessions(sessions)
18
+ upsertSession(id, session)
19
19
  return new NextResponse('OK')
20
20
  }
@@ -3,6 +3,7 @@ import { spawn } from 'child_process'
3
3
  import { loadSessions, devServers, localIP } from '@/lib/server/storage'
4
4
  import { notFound } from '@/lib/server/collection-helpers'
5
5
  import { resolveDevServerLaunchDir } from '@/lib/server/devserver-launch'
6
+ import { sleep } from '@/lib/shared-utils'
6
7
  import net from 'net'
7
8
 
8
9
  interface DevServerStartResult {
@@ -65,7 +66,7 @@ async function startDevServer(id: string, session: { cwd: string }): Promise<Dev
65
66
  devServers.set(id, { proc, url: `http://${localIP()}:${port}` })
66
67
  console.log(`[${id}] starting dev server in ${launch.launchDir} (session cwd=${session.cwd})`)
67
68
 
68
- await new Promise((resolve) => setTimeout(resolve, 4000))
69
+ await sleep(4000)
69
70
  const ds = devServers.get(id)
70
71
  if (!ds) {
71
72
  return {
@@ -1,12 +1,11 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadSessions, saveSessions } from '@/lib/server/storage'
2
+ import { loadSession, upsertSession } from '@/lib/server/storage'
3
3
  import { notFound } from '@/lib/server/collection-helpers'
4
4
 
5
5
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
6
6
  const { id } = await params
7
7
  const body = await req.json().catch(() => ({})) as { messageIndex: number; newText: string }
8
- const sessions = loadSessions()
9
- const session = sessions[id]
8
+ const session = loadSession(id)
10
9
  if (!session) return notFound()
11
10
 
12
11
  const { messageIndex, newText } = body
@@ -16,7 +15,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
16
15
 
17
16
  // Truncate messages to messageIndex (discard that msg + everything after)
18
17
  session.messages = session.messages.slice(0, messageIndex)
19
- saveSessions(sessions)
18
+ upsertSession(id, session)
20
19
 
21
20
  return NextResponse.json({ message: newText })
22
21
  }
@@ -1,13 +1,12 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { randomUUID } from 'node:crypto'
3
- import { loadSessions, saveSessions } from '@/lib/server/storage'
3
+ import { loadSession, upsertSession } from '@/lib/server/storage'
4
4
  import { notFound } from '@/lib/server/collection-helpers'
5
5
 
6
6
  export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
7
7
  const { id } = await params
8
8
  const body = await req.json().catch(() => ({})) as { messageIndex: number }
9
- const sessions = loadSessions()
10
- const source = sessions[id]
9
+ const source = loadSession(id)
11
10
  if (!source) return notFound()
12
11
 
13
12
  const { messageIndex } = body
@@ -37,8 +36,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
37
36
  conversationTone: source.conversationTone,
38
37
  }
39
38
 
40
- sessions[newId] = forked
41
- saveSessions(sessions)
39
+ upsertSession(newId, forked)
42
40
 
43
41
  return NextResponse.json(forked)
44
42
  }