@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
@@ -0,0 +1,409 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { after, before, describe, it } from 'node:test'
6
+
7
+ import type { Message } from '@/types'
8
+
9
+ const originalEnv = {
10
+ DATA_DIR: process.env.DATA_DIR,
11
+ WORKSPACE_DIR: process.env.WORKSPACE_DIR,
12
+ SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
13
+ }
14
+
15
+ let tempDir = ''
16
+ let cm: typeof import('./context-manager')
17
+
18
+ before(async () => {
19
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-context-manager-'))
20
+ process.env.DATA_DIR = path.join(tempDir, 'data')
21
+ process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
22
+ process.env.SWARMCLAW_BUILD_MODE = '1'
23
+ cm = await import('./context-manager')
24
+ })
25
+
26
+ after(() => {
27
+ if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
28
+ else process.env.DATA_DIR = originalEnv.DATA_DIR
29
+ if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
30
+ else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
31
+ if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
32
+ else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
33
+ fs.rmSync(tempDir, { recursive: true, force: true })
34
+ })
35
+
36
+ function makeMsg(role: 'user' | 'assistant', text: string, toolEvents?: Message['toolEvents']): Message {
37
+ return { role, text, time: Date.now(), toolEvents }
38
+ }
39
+
40
+ describe('context-manager', () => {
41
+ // --- estimateTokens ---
42
+
43
+ describe('estimateTokens', () => {
44
+ it('returns 0 for empty string', () => {
45
+ assert.equal(cm.estimateTokens(''), 0)
46
+ })
47
+
48
+ it('returns 0 for falsy input', () => {
49
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
+ assert.equal(cm.estimateTokens(null as any), 0)
51
+ })
52
+
53
+ it('estimates ~1 token per 4 chars', () => {
54
+ const tokens = cm.estimateTokens('abcdefghijklmnop') // 16 chars
55
+ assert.equal(tokens, 4)
56
+ })
57
+
58
+ it('rounds up fractional token counts', () => {
59
+ const tokens = cm.estimateTokens('abcde') // 5 chars -> ceil(5/4) = 2
60
+ assert.equal(tokens, 2)
61
+ })
62
+ })
63
+
64
+ // --- estimateMessagesTokens ---
65
+
66
+ describe('estimateMessagesTokens', () => {
67
+ it('returns 0 for empty array', () => {
68
+ assert.equal(cm.estimateMessagesTokens([]), 0)
69
+ })
70
+
71
+ it('includes per-message overhead', () => {
72
+ const msgs = [makeMsg('user', 'hi')]
73
+ const tokens = cm.estimateMessagesTokens(msgs)
74
+ // 4 overhead + ceil(2/4) = 4 + 1 = 5
75
+ assert.equal(tokens, 5)
76
+ })
77
+
78
+ it('includes tool event tokens', () => {
79
+ const msgs = [makeMsg('assistant', 'ok', [
80
+ { name: 'web_search', input: '{"q":"test query string here"}', output: 'result data here' },
81
+ ])]
82
+ const tokens = cm.estimateMessagesTokens(msgs)
83
+ // Should be > just the text tokens
84
+ assert.ok(tokens > 5, `Expected more than 5 tokens with tool events, got ${tokens}`)
85
+ })
86
+ })
87
+
88
+ // --- getContextWindowSize ---
89
+
90
+ describe('getContextWindowSize', () => {
91
+ it('returns known model window size', () => {
92
+ assert.equal(cm.getContextWindowSize('anthropic', 'claude-opus-4-6'), 200_000)
93
+ })
94
+
95
+ it('falls back to provider default for unknown model', () => {
96
+ assert.equal(cm.getContextWindowSize('anthropic', 'claude-unknown-model'), 200_000)
97
+ })
98
+
99
+ it('falls back to 8192 for unknown provider and model', () => {
100
+ assert.equal(cm.getContextWindowSize('unknown-provider', 'unknown-model'), 8_192)
101
+ })
102
+
103
+ it('returns openai model sizes', () => {
104
+ assert.equal(cm.getContextWindowSize('openai', 'gpt-4o'), 128_000)
105
+ })
106
+ })
107
+
108
+ // --- getContextStatus ---
109
+
110
+ describe('getContextStatus', () => {
111
+ it('returns ok for small context usage', () => {
112
+ const msgs = [makeMsg('user', 'hello')]
113
+ const status = cm.getContextStatus(msgs, 100, 'anthropic', 'claude-opus-4-6')
114
+ assert.equal(status.strategy, 'ok')
115
+ assert.ok(status.percentUsed < 70)
116
+ assert.equal(status.contextWindow, 200_000)
117
+ assert.equal(status.messageCount, 1)
118
+ })
119
+
120
+ it('returns warning at 70%+ usage', () => {
121
+ // 200k window, need ~140k tokens. 140000 tokens * 4 chars = 560000 chars
122
+ const bigText = 'x'.repeat(560_000)
123
+ const msgs = [makeMsg('user', bigText)]
124
+ const status = cm.getContextStatus(msgs, 0, 'anthropic', 'claude-opus-4-6')
125
+ assert.equal(status.strategy, 'warning')
126
+ })
127
+
128
+ it('returns critical at 90%+ usage', () => {
129
+ const bigText = 'x'.repeat(720_000) // 180k tokens
130
+ const msgs = [makeMsg('user', bigText)]
131
+ const status = cm.getContextStatus(msgs, 0, 'anthropic', 'claude-opus-4-6')
132
+ assert.equal(status.strategy, 'critical')
133
+ })
134
+ })
135
+
136
+ // --- getContextDegradationWarning ---
137
+
138
+ describe('getContextDegradationWarning', () => {
139
+ it('returns null below 60%', () => {
140
+ const msgs = [makeMsg('user', 'short message')]
141
+ const warning = cm.getContextDegradationWarning(msgs, 100, 'anthropic', 'claude-opus-4-6')
142
+ assert.equal(warning, null)
143
+ })
144
+
145
+ it('returns warning at 85%+', () => {
146
+ const bigText = 'x'.repeat(680_000) // ~170k tokens
147
+ const msgs = [makeMsg('user', bigText)]
148
+ const warning = cm.getContextDegradationWarning(msgs, 0, 'anthropic', 'claude-opus-4-6')
149
+ assert.ok(warning !== null)
150
+ assert.ok(warning!.includes('CRITICAL'))
151
+ })
152
+
153
+ it('returns softer warning between 60-70%', () => {
154
+ // Need 60-70% of 200k = 120k-140k tokens = 480k-560k chars
155
+ const text = 'x'.repeat(500_000)
156
+ const msgs = [makeMsg('user', text)]
157
+ const warning = cm.getContextDegradationWarning(msgs, 0, 'anthropic', 'claude-opus-4-6')
158
+ assert.ok(warning !== null)
159
+ assert.ok(warning!.includes('Consider saving'))
160
+ })
161
+ })
162
+
163
+ // --- shouldAutoCompact ---
164
+
165
+ describe('shouldAutoCompact', () => {
166
+ it('returns false for small context', () => {
167
+ const msgs = [makeMsg('user', 'hello')]
168
+ assert.equal(cm.shouldAutoCompact(msgs, 100, 'anthropic', 'claude-opus-4-6'), false)
169
+ })
170
+
171
+ it('returns true when context exceeds threshold', () => {
172
+ const bigText = 'x'.repeat(660_000) // ~165k tokens -> 82.5% of 200k
173
+ const msgs = [makeMsg('user', bigText)]
174
+ assert.equal(cm.shouldAutoCompact(msgs, 0, 'anthropic', 'claude-opus-4-6'), true)
175
+ })
176
+
177
+ it('respects custom trigger percent', () => {
178
+ const bigText = 'x'.repeat(400_000) // ~100k tokens -> 50% of 200k
179
+ const msgs = [makeMsg('user', bigText)]
180
+ assert.equal(cm.shouldAutoCompact(msgs, 0, 'anthropic', 'claude-opus-4-6', 40), true)
181
+ assert.equal(cm.shouldAutoCompact(msgs, 0, 'anthropic', 'claude-opus-4-6', 60), false)
182
+ })
183
+ })
184
+
185
+ // --- slidingWindowCompact ---
186
+
187
+ describe('slidingWindowCompact', () => {
188
+ it('returns all messages when under limit', () => {
189
+ const msgs = [makeMsg('user', 'a'), makeMsg('assistant', 'b')]
190
+ const result = cm.slidingWindowCompact(msgs, 5)
191
+ assert.equal(result.length, 2)
192
+ })
193
+
194
+ it('keeps only last N messages', () => {
195
+ const msgs = Array.from({ length: 20 }, (_, i) => makeMsg('user', `msg-${i}`))
196
+ const result = cm.slidingWindowCompact(msgs, 5)
197
+ assert.equal(result.length, 5)
198
+ assert.equal(result[0].text, 'msg-15')
199
+ assert.equal(result[4].text, 'msg-19')
200
+ })
201
+ })
202
+
203
+ // --- splitMessagesByTokenBudget ---
204
+
205
+ describe('splitMessagesByTokenBudget', () => {
206
+ it('returns empty array for empty messages', () => {
207
+ assert.deepEqual(cm.splitMessagesByTokenBudget([], 1000), [])
208
+ })
209
+
210
+ it('keeps all in one chunk when within budget', () => {
211
+ const msgs = [makeMsg('user', 'hi'), makeMsg('assistant', 'hello')]
212
+ const chunks = cm.splitMessagesByTokenBudget(msgs, 10000)
213
+ assert.equal(chunks.length, 1)
214
+ assert.equal(chunks[0].length, 2)
215
+ })
216
+
217
+ it('splits messages across multiple chunks', () => {
218
+ const msgs = Array.from({ length: 10 }, () =>
219
+ makeMsg('user', 'x'.repeat(400)), // ~100 tokens + 4 overhead each
220
+ )
221
+ // Budget of 210 tokens should fit ~2 messages per chunk
222
+ const chunks = cm.splitMessagesByTokenBudget(msgs, 210)
223
+ assert.ok(chunks.length > 1, `Expected multiple chunks, got ${chunks.length}`)
224
+ // All messages should be accounted for
225
+ const totalMsgs = chunks.reduce((sum, c) => sum + c.length, 0)
226
+ assert.equal(totalMsgs, 10)
227
+ })
228
+ })
229
+
230
+ // --- computeAdaptiveChunkRatio ---
231
+
232
+ describe('computeAdaptiveChunkRatio', () => {
233
+ it('returns base ratio for empty messages', () => {
234
+ assert.equal(cm.computeAdaptiveChunkRatio([], 200_000), 0.4)
235
+ })
236
+
237
+ it('returns base ratio for small messages', () => {
238
+ const msgs = [makeMsg('user', 'short')]
239
+ const ratio = cm.computeAdaptiveChunkRatio(msgs, 200_000)
240
+ assert.equal(ratio, 0.4)
241
+ })
242
+
243
+ it('reduces ratio for large average messages', () => {
244
+ // Need avgRatio > 0.1: safeAvgTokens / contextWindow > 0.1
245
+ // For 200k window: safeAvgTokens > 20k -> avgTokens > 20k/1.2 ~ 16667 -> chars > 66668
246
+ const msgs = Array.from({ length: 4 }, () => makeMsg('user', 'x'.repeat(80_000)))
247
+ const ratio = cm.computeAdaptiveChunkRatio(msgs, 200_000)
248
+ assert.ok(ratio < 0.4, `Expected ratio < 0.4, got ${ratio}`)
249
+ assert.ok(ratio >= 0.15, `Expected ratio >= 0.15, got ${ratio}`)
250
+ })
251
+ })
252
+
253
+ // --- extractToolFailures ---
254
+
255
+ describe('extractToolFailures', () => {
256
+ it('returns empty array when no failures', () => {
257
+ const msgs = [makeMsg('assistant', 'ok', [
258
+ { name: 'web', input: '{}', output: 'data' },
259
+ ])]
260
+ assert.deepEqual(cm.extractToolFailures(msgs), [])
261
+ })
262
+
263
+ it('extracts tool failures', () => {
264
+ const msgs = [makeMsg('assistant', 'fail', [
265
+ { name: 'shell', input: 'ls', output: 'command not found', error: true },
266
+ ])]
267
+ const failures = cm.extractToolFailures(msgs)
268
+ assert.equal(failures.length, 1)
269
+ assert.ok(failures[0].includes('[shell]'))
270
+ assert.ok(failures[0].includes('error'))
271
+ })
272
+
273
+ it('limits to MAX_TOOL_FAILURES (8)', () => {
274
+ const events = Array.from({ length: 20 }, (_, i) => ({
275
+ name: `tool-${i}`, input: '{}', output: `err-${i}`, error: true as const,
276
+ }))
277
+ const msgs = [makeMsg('assistant', 'many errors', events)]
278
+ const failures = cm.extractToolFailures(msgs)
279
+ assert.equal(failures.length, 8)
280
+ })
281
+ })
282
+
283
+ // --- extractFileOperations ---
284
+
285
+ describe('extractFileOperations', () => {
286
+ it('returns empty sets when no file ops', () => {
287
+ const msgs = [makeMsg('user', 'hello')]
288
+ const ops = cm.extractFileOperations(msgs)
289
+ assert.deepEqual(ops, { read: [], modified: [] })
290
+ })
291
+
292
+ it('extracts read operations', () => {
293
+ const msgs = [makeMsg('assistant', 'reading', [
294
+ { name: 'read_file', input: JSON.stringify({ filePath: '/tmp/test.ts' }) },
295
+ ])]
296
+ const ops = cm.extractFileOperations(msgs)
297
+ assert.deepEqual(ops.read, ['/tmp/test.ts'])
298
+ assert.deepEqual(ops.modified, [])
299
+ })
300
+
301
+ it('extracts write operations', () => {
302
+ const msgs = [makeMsg('assistant', 'writing', [
303
+ { name: 'write_file', input: JSON.stringify({ filePath: '/tmp/out.ts' }) },
304
+ { name: 'edit_file', input: JSON.stringify({ filePath: '/tmp/edit.ts' }) },
305
+ ])]
306
+ const ops = cm.extractFileOperations(msgs)
307
+ assert.deepEqual(ops.read, [])
308
+ assert.equal(ops.modified.length, 2)
309
+ assert.ok(ops.modified.includes('/tmp/out.ts'))
310
+ assert.ok(ops.modified.includes('/tmp/edit.ts'))
311
+ })
312
+
313
+ it('deduplicates paths', () => {
314
+ const msgs = [
315
+ makeMsg('assistant', 'op1', [
316
+ { name: 'read_file', input: JSON.stringify({ filePath: '/tmp/same.ts' }) },
317
+ ]),
318
+ makeMsg('assistant', 'op2', [
319
+ { name: 'read_file', input: JSON.stringify({ filePath: '/tmp/same.ts' }) },
320
+ ]),
321
+ ]
322
+ const ops = cm.extractFileOperations(msgs)
323
+ assert.equal(ops.read.length, 1)
324
+ })
325
+ })
326
+
327
+ // --- llmCompact ---
328
+
329
+ describe('llmCompact', () => {
330
+ it('returns original messages when under keepLastN', async () => {
331
+ const msgs = [makeMsg('user', 'hi'), makeMsg('assistant', 'hey')]
332
+ const result = await cm.llmCompact({
333
+ messages: msgs,
334
+ provider: 'anthropic',
335
+ model: 'claude-opus-4-6',
336
+ agentId: null,
337
+ sessionId: 'test-session',
338
+ summarize: async () => 'summary',
339
+ })
340
+ assert.equal(result.prunedCount, 0)
341
+ assert.equal(result.summaryAdded, false)
342
+ assert.equal(result.messages.length, 2)
343
+ })
344
+
345
+ it('summarizes old messages and keeps recent ones', async () => {
346
+ const msgs = Array.from({ length: 15 }, (_, i) =>
347
+ makeMsg(i % 2 === 0 ? 'user' : 'assistant', `message-${i}`),
348
+ )
349
+ const result = await cm.llmCompact({
350
+ messages: msgs,
351
+ provider: 'anthropic',
352
+ model: 'claude-opus-4-6',
353
+ agentId: null,
354
+ sessionId: 'test-session',
355
+ summarize: async () => 'This is a test summary of the conversation.',
356
+ keepLastN: 5,
357
+ })
358
+ assert.equal(result.prunedCount, 10)
359
+ assert.equal(result.summaryAdded, true)
360
+ // summary message + 5 recent
361
+ assert.equal(result.messages.length, 6)
362
+ assert.ok(result.messages[0].text.includes('[Context Summary]'))
363
+ })
364
+
365
+ it('falls back to sliding window when summarizer fails', async () => {
366
+ const msgs = Array.from({ length: 15 }, (_, i) =>
367
+ makeMsg(i % 2 === 0 ? 'user' : 'assistant', `message-${i}`),
368
+ )
369
+ const result = await cm.llmCompact({
370
+ messages: msgs,
371
+ provider: 'anthropic',
372
+ model: 'claude-opus-4-6',
373
+ agentId: null,
374
+ sessionId: 'test-session',
375
+ summarize: async () => { throw new Error('LLM unavailable') },
376
+ keepLastN: 5,
377
+ })
378
+ assert.equal(result.summaryAdded, false)
379
+ assert.equal(result.messages.length, 5)
380
+ })
381
+ })
382
+
383
+ // --- consolidateToMemory ---
384
+
385
+ describe('consolidateToMemory', () => {
386
+ it('returns 0 when agentId is null', () => {
387
+ const msgs = [makeMsg('assistant', 'We decided to use Rust.')]
388
+ assert.equal(cm.consolidateToMemory(msgs, null, 'session-1'), 0)
389
+ })
390
+
391
+ it('stores memories for decision-containing messages', () => {
392
+ const msgs = [makeMsg('assistant', 'We decided to refactor the module using a new approach.')]
393
+ const stored = cm.consolidateToMemory(msgs, 'agent-test', 'session-test')
394
+ assert.ok(stored >= 1, `Expected at least 1 memory stored, got ${stored}`)
395
+ })
396
+
397
+ it('skips user messages', () => {
398
+ const msgs = [makeMsg('user', 'We decided to use TypeScript.')]
399
+ const stored = cm.consolidateToMemory(msgs, 'agent-test', 'session-test')
400
+ assert.equal(stored, 0)
401
+ })
402
+
403
+ it('skips messages without decision/fact/result keywords', () => {
404
+ const msgs = [makeMsg('assistant', 'Hello there, how are you?')]
405
+ const stored = cm.consolidateToMemory(msgs, 'agent-test', 'session-test')
406
+ assert.equal(stored, 0)
407
+ })
408
+ })
409
+ })
@@ -1,7 +1,7 @@
1
1
  import assert from 'node:assert/strict'
2
2
  import { test } from 'node:test'
3
3
  import type { Agent } from '@/types'
4
- import { checkAgentBudgetLimits, getAgentSpendWindows } from './cost.ts'
4
+ import { checkAgentBudgetLimits, getAgentSpendWindows } from './cost'
5
5
 
6
6
  function buildNowTs(): number {
7
7
  const d = new Date()
@@ -0,0 +1,78 @@
1
+ import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/heartbeat-defaults'
2
+ import { isProductionRuntime } from '@/lib/runtime-env'
3
+ import type { Session } from '@/types'
4
+
5
+ const SYNTHETIC_HEALTH_SESSION_USERS = new Set(['workbench', 'comparison-bench'])
6
+ const SYNTHETIC_HEALTH_SESSION_PREFIXES = ['wb-', 'cmp-']
7
+
8
+ function parseBoolish(value: unknown, fallback: boolean): boolean {
9
+ if (typeof value === 'boolean') return value
10
+ if (typeof value !== 'string') return fallback
11
+ const normalized = value.trim().toLowerCase()
12
+ if (!normalized) return fallback
13
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) return true
14
+ if (['0', 'false', 'no', 'off'].includes(normalized)) return false
15
+ return fallback
16
+ }
17
+
18
+ export function daemonAutostartEnvEnabled(): boolean {
19
+ return parseBoolish(process.env.SWARMCLAW_DAEMON_AUTOSTART, isProductionRuntime())
20
+ }
21
+
22
+ export function isDaemonBackgroundServicesEnabled(): boolean {
23
+ return parseBoolish(process.env.SWARMCLAW_DAEMON_BACKGROUND_SERVICES, true)
24
+ }
25
+
26
+ export function parseHeartbeatIntervalSec(
27
+ value: unknown,
28
+ fallback = DEFAULT_HEARTBEAT_INTERVAL_SEC,
29
+ ): number {
30
+ const parsed = typeof value === 'number'
31
+ ? value
32
+ : typeof value === 'string'
33
+ ? Number.parseInt(value, 10)
34
+ : Number.NaN
35
+ if (!Number.isFinite(parsed)) return fallback
36
+ return Math.max(0, Math.min(3600, Math.trunc(parsed)))
37
+ }
38
+
39
+ export function shouldNotifyProviderReachabilityIssue(provider: string): boolean {
40
+ return provider !== 'openclaw'
41
+ }
42
+
43
+ function hasSyntheticHealthPrefix(value: unknown): boolean {
44
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
45
+ return SYNTHETIC_HEALTH_SESSION_PREFIXES.some((prefix) => normalized.startsWith(prefix))
46
+ }
47
+
48
+ export function shouldSuppressSessionHeartbeatHealthAlert(
49
+ session: Pick<Session, 'id' | 'name' | 'user' | 'shortcutForAgentId'>,
50
+ ): boolean {
51
+ const user = typeof session.user === 'string' ? session.user.trim().toLowerCase() : ''
52
+ if (SYNTHETIC_HEALTH_SESSION_USERS.has(user)) return true
53
+ if (hasSyntheticHealthPrefix(session.id)) return true
54
+ if (hasSyntheticHealthPrefix(session.shortcutForAgentId)) return true
55
+
56
+ const name = typeof session.name === 'string' ? session.name.trim().toLowerCase() : ''
57
+ return name.startsWith('workbench ')
58
+ || name.startsWith('assistant benchmark ')
59
+ || name.startsWith('comparison ')
60
+ }
61
+
62
+ export function shouldSuppressSyntheticAgentHealthAlert(agentId: string): boolean {
63
+ return hasSyntheticHealthPrefix(agentId)
64
+ }
65
+
66
+ export function buildSessionHeartbeatHealthDedupKey(
67
+ sessionId: string,
68
+ state: 'stale' | 'auto-disabled',
69
+ ): string {
70
+ return `health-alert:session-heartbeat:${state}:${sessionId}`
71
+ }
72
+
73
+ export function parseCronToMs(cron: string | null | undefined, fallbackMs: number): number | null {
74
+ if (!cron || typeof cron !== 'string') return null
75
+ const hourMatch = cron.match(/\*\/(\d+)/)
76
+ if (hourMatch) return parseInt(hourMatch[1], 10) * 3600_000
77
+ return fallbackMs
78
+ }
@@ -0,0 +1,167 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+ import { runWithTempDataDir } from './test-utils/run-with-temp-data-dir'
4
+
5
+ describe('connector lifecycle for daemon recovery', () => {
6
+ it('preserves enabled connectors across runtime stop/start and auto-starts them again', () => {
7
+ const output = runWithTempDataDir(`
8
+ const storageMod = await import('./src/lib/server/storage.ts')
9
+ const managerMod = await import('./src/lib/server/connectors/manager.ts')
10
+ const pluginsMod = await import('./src/lib/server/plugins.ts')
11
+ const storage = storageMod.default || storageMod
12
+ const manager = managerMod.default || managerMod
13
+ const plugins = pluginsMod.default || pluginsMod
14
+
15
+ let startCount = 0
16
+ plugins.getPluginManager().registerBuiltin('test-daemon-autostart-plugin', {
17
+ name: 'Test Daemon Autostart Plugin',
18
+ connectors: [{
19
+ id: 'test-daemon-autostart',
20
+ name: 'Test Daemon Autostart',
21
+ description: 'Connector started by runtime autostart',
22
+ startListener: async () => {
23
+ startCount += 1
24
+ return async () => {}
25
+ },
26
+ }],
27
+ })
28
+
29
+ const now = Date.now()
30
+ storage.saveSettings({})
31
+ storage.saveConnectors({
32
+ conn_auto: {
33
+ id: 'conn_auto',
34
+ name: 'Autostart Connector',
35
+ platform: 'test-daemon-autostart',
36
+ agentId: null,
37
+ credentialId: null,
38
+ config: { botToken: 'test-token' },
39
+ isEnabled: true,
40
+ status: 'stopped',
41
+ createdAt: now,
42
+ updatedAt: now,
43
+ },
44
+ })
45
+
46
+ await manager.startConnector('conn_auto')
47
+ const firstStart = {
48
+ running: manager.listRunningConnectors(),
49
+ connector: storage.loadConnectors().conn_auto,
50
+ }
51
+
52
+ await manager.stopAllConnectors({ disable: false })
53
+ const afterStop = storage.loadConnectors().conn_auto
54
+
55
+ await manager.autoStartConnectors()
56
+ const secondStart = {
57
+ running: manager.listRunningConnectors(),
58
+ connector: storage.loadConnectors().conn_auto,
59
+ }
60
+
61
+ await manager.stopAllConnectors()
62
+
63
+ console.log(JSON.stringify({
64
+ startCount,
65
+ firstStart,
66
+ afterStop,
67
+ secondStart,
68
+ }))
69
+ `, { prefix: 'swarmclaw-daemon-test-' })
70
+
71
+ assert.equal(output.startCount, 2)
72
+ assert.equal(output.firstStart.running.some((entry: { id: string }) => entry.id === 'conn_auto'), true)
73
+ assert.equal(output.firstStart.connector.status, 'running')
74
+ assert.equal(output.afterStop.isEnabled, true)
75
+ assert.equal(output.afterStop.status, 'stopped')
76
+ assert.equal(output.secondStart.running.some((entry: { id: string }) => entry.id === 'conn_auto'), true)
77
+ assert.equal(output.secondStart.connector.status, 'running')
78
+ })
79
+
80
+ it('restarts unhealthy connectors through the daemon recovery path', () => {
81
+ const output = runWithTempDataDir(`
82
+ const storageMod = await import('./src/lib/server/storage.ts')
83
+ const managerMod = await import('./src/lib/server/connectors/manager.ts')
84
+ const pluginsMod = await import('./src/lib/server/plugins.ts')
85
+ const storage = storageMod.default || storageMod
86
+ const manager = managerMod.default || managerMod
87
+ const plugins = pluginsMod.default || pluginsMod
88
+
89
+ let startCount = 0
90
+ let stopCount = 0
91
+ plugins.getPluginManager().registerBuiltin('test-daemon-restart-plugin', {
92
+ name: 'Test Daemon Restart Plugin',
93
+ connectors: [{
94
+ id: 'test-daemon-restart',
95
+ name: 'Test Daemon Restart',
96
+ description: 'Connector restarted by daemon-style recovery',
97
+ startListener: async () => {
98
+ startCount += 1
99
+ return async () => {}
100
+ },
101
+ }],
102
+ })
103
+
104
+ const now = Date.now()
105
+ storage.saveSettings({})
106
+ storage.saveConnectors({
107
+ conn_restart: {
108
+ id: 'conn_restart',
109
+ name: 'Restart Connector',
110
+ platform: 'test-daemon-restart',
111
+ agentId: null,
112
+ credentialId: null,
113
+ config: { botToken: 'test-token' },
114
+ isEnabled: true,
115
+ status: 'running',
116
+ lastError: null,
117
+ createdAt: now,
118
+ updatedAt: now,
119
+ },
120
+ })
121
+
122
+ const running = globalThis.__swarmclaw_running_connectors__ || new Map()
123
+ globalThis.__swarmclaw_running_connectors__ = running
124
+ running.set('conn_restart', {
125
+ connector: storage.loadConnectors().conn_restart,
126
+ authenticated: true,
127
+ isAlive: () => false,
128
+ stop: async () => {
129
+ stopCount += 1
130
+ },
131
+ })
132
+
133
+ await manager.checkConnectorHealth()
134
+ const reconnectState = manager.getReconnectState('conn_restart')
135
+ if (reconnectState && !reconnectState.exhausted) {
136
+ await manager.startConnector('conn_restart')
137
+ manager.clearReconnectState('conn_restart')
138
+ }
139
+
140
+ const health = Object.values(storage.loadConnectorHealth())
141
+ .filter((entry) => entry.connectorId === 'conn_restart')
142
+ .map((entry) => entry.event)
143
+
144
+ console.log(JSON.stringify({
145
+ startCount,
146
+ stopCount,
147
+ status: manager.getConnectorStatus('conn_restart'),
148
+ reconnectState: manager.getReconnectState('conn_restart'),
149
+ connector: storage.loadConnectors().conn_restart,
150
+ health,
151
+ running: manager.listRunningConnectors(),
152
+ }))
153
+
154
+ await manager.stopAllConnectors()
155
+ `, { prefix: 'swarmclaw-daemon-test-' })
156
+
157
+ assert.equal(output.startCount, 1)
158
+ assert.equal(output.stopCount, 1)
159
+ assert.equal(output.status, 'running')
160
+ assert.equal(output.reconnectState, null)
161
+ assert.equal(output.connector.status, 'running')
162
+ assert.equal(output.connector.lastError, null)
163
+ assert.equal(output.health.includes('disconnected'), true)
164
+ assert.equal(output.health.includes('started'), true)
165
+ assert.equal(output.running.some((entry: { id: string }) => entry.id === 'conn_restart'), true)
166
+ })
167
+ })