@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
@@ -2,18 +2,19 @@ import { z } from 'zod'
2
2
  import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
3
  import fs from 'fs'
4
4
  import path from 'path'
5
- import { fileURLToPath, pathToFileURL } from 'url'
6
5
  import * as cheerio from 'cheerio'
7
6
  import { UPLOAD_DIR } from '../storage'
8
7
  import type { ToolBuildContext } from './context'
9
8
  import { spawnSync } from 'child_process'
10
9
  import { safePath, truncate, MAX_OUTPUT, findBinaryOnPath } from './context'
11
- import { getSearchProvider, type SearchResult } from './search-providers'
10
+ import { getSearchProvider } from './search-providers'
12
11
  import { dedupeScreenshotMarkdownLines } from './web-output'
13
12
  import { withRetry } from '../tool-retry'
14
13
  import type { Plugin, PluginHooks } from '@/types'
15
14
  import { getPluginManager } from '../plugins'
16
15
  import { normalizeToolInputArgs } from './normalize-tool-args'
16
+ import { dedup, errorMessage, hmrSingleton, sleep } from '@/lib/shared-utils'
17
+ import { resolvePathWithinBaseDir } from '../path-utils'
17
18
  import {
18
19
  ensureSessionBrowserProfileId,
19
20
  getBrowserProfileDir,
@@ -23,49 +24,27 @@ import {
23
24
  removeBrowserSessionRecord,
24
25
  upsertBrowserSessionRecord,
25
26
  } from '../browser-state'
26
-
27
- function cleanSearchField(value: string | undefined): string {
28
- return (value || '').replace(/\s+/g, ' ').trim()
29
- }
30
-
31
- export function formatWebSearchResults(query: string, results: SearchResult[], maxChars = MAX_OUTPUT): string {
32
- const cleanedQuery = cleanSearchField(query)
33
- const header = cleanedQuery ? `Search results for: ${cleanedQuery}` : 'Search results'
34
- const sections: string[] = [header]
35
- const joinSections = (items: string[]) => items.filter(Boolean).join('\n\n')
36
-
37
- for (let index = 0; index < results.length; index++) {
38
- const result = results[index]
39
- const title = cleanSearchField(result?.title) || cleanSearchField(result?.url) || `Result ${index + 1}`
40
- const url = cleanSearchField(result?.url)
41
- const snippet = cleanSearchField(result?.snippet)
42
- const lines = [`${index + 1}. ${title}`]
43
- if (url) lines.push(`URL: ${url}`)
44
- if (snippet) lines.push(`Snippet: ${snippet}`)
45
- const candidate = joinSections([...sections, lines.join('\n')])
46
- if (candidate.length <= maxChars) {
47
- sections.push(lines.join('\n'))
48
- continue
49
- }
50
-
51
- if (url) {
52
- const minimalLines = [`${index + 1}. ${title}`, `URL: ${url}`]
53
- const minimalCandidate = joinSections([...sections, minimalLines.join('\n')])
54
- if (minimalCandidate.length <= maxChars) {
55
- sections.push(minimalLines.join('\n'))
56
- }
57
- }
58
-
59
- const omitted = results.length - index
60
- if (omitted > 0) {
61
- const remainingNotice = `(${omitted} additional result${omitted === 1 ? '' : 's'} omitted for brevity)`
62
- const withNotice = joinSections([...sections, remainingNotice])
63
- if (withNotice.length <= maxChars) sections.push(remainingNotice)
64
- }
65
- return truncate(joinSections(sections), maxChars)
66
- }
67
-
68
- return truncate(joinSections(sections), maxChars)
27
+ import {
28
+ buildBrowserConnectionOptions,
29
+ buildBrowserStdioServerParams,
30
+ formatWebSearchResults,
31
+ inferWebActionFromArgs,
32
+ normalizeBrowserActionParams,
33
+ parseJsonArrayValue,
34
+ pickBrowserTargetFromParams,
35
+ pickNonEmptyBrowserString,
36
+ resolveBrowserNavigationTarget,
37
+ sanitizePlaywrightMcpEnv,
38
+ } from './web-utils'
39
+
40
+ export {
41
+ buildBrowserConnectionOptions,
42
+ buildBrowserStdioServerParams,
43
+ formatWebSearchResults,
44
+ inferWebActionFromArgs,
45
+ normalizeBrowserActionParams,
46
+ resolveBrowserNavigationTarget,
47
+ sanitizePlaywrightMcpEnv,
69
48
  }
70
49
 
71
50
  type BrowserRuntimeEntry = {
@@ -77,74 +56,25 @@ type BrowserRuntimeEntry = {
77
56
  refCount: number
78
57
  }
79
58
 
80
- export const activeBrowsers = new Map<string, BrowserRuntimeEntry>()
81
- const pendingBrowserInitializations = new Map<string, Promise<BrowserRuntimeEntry>>()
82
-
83
- export function buildBrowserConnectionOptions(profileDir: string) {
84
- return {
85
- browser: {
86
- userDataDir: profileDir,
87
- launchOptions: { headless: true },
88
- contextOptions: {
89
- viewport: { width: 1440, height: 900 },
90
- },
91
- },
92
- imageResponses: 'allow' as const,
93
- capabilities: ['core', 'pdf', 'vision', 'network', 'storage'],
94
- // Keep browser state isolated per session/profile. The upstream shared
95
- // context mode is process-global and causes unrelated agent sessions to
96
- // contend with each other.
97
- sharedBrowserContext: false,
98
- timeouts: {
99
- action: 15_000,
100
- navigation: 60_000,
101
- },
102
- }
103
- }
104
-
105
- export function buildBrowserStdioServerParams(profileDir: string) {
106
- const cliCandidates = [
107
- path.join(process.cwd(), 'node_modules', '@playwright', 'mcp', 'cli.js'),
108
- path.join(process.cwd(), '[project]', 'node_modules', '@playwright', 'mcp', 'cli.js'),
109
- ]
110
- const cliPath = cliCandidates.find((candidate) => fs.existsSync(candidate)) || cliCandidates[0]
111
- const outputDir = path.join(profileDir, 'mcp-output')
112
- const env = sanitizePlaywrightMcpEnv()
113
- return {
114
- command: process.execPath,
115
- args: [
116
- cliPath,
117
- '--headless',
118
- '--user-data-dir', profileDir,
119
- '--output-dir', outputDir,
120
- '--caps', 'vision,pdf',
121
- '--image-responses', 'allow',
122
- '--output-mode', 'file',
123
- '--timeout-action', '15000',
124
- '--timeout-navigation', '60000',
125
- ],
126
- env: {
127
- ...env,
128
- PLAYWRIGHT_MCP_USER_DATA_DIR: profileDir,
129
- PLAYWRIGHT_MCP_HEADLESS: '1',
130
- PLAYWRIGHT_MCP_IMAGE_RESPONSES: 'allow',
131
- PLAYWRIGHT_MCP_OUTPUT_DIR: outputDir,
132
- PLAYWRIGHT_MCP_OUTPUT_MODE: 'file',
133
- PLAYWRIGHT_MCP_TIMEOUT_ACTION: '15000',
134
- PLAYWRIGHT_MCP_TIMEOUT_NAVIGATION: '60000',
135
- },
136
- stderr: 'inherit' as const,
137
- }
138
- }
59
+ // Stored on globalThis to survive HMR reloads in dev mode —
60
+ // prevents orphaned Chromium processes when web.ts is edited.
61
+ export const activeBrowsers: Map<string, BrowserRuntimeEntry> =
62
+ hmrSingleton('__swarmclaw_active_browsers__', () => new Map<string, BrowserRuntimeEntry>())
63
+ const pendingBrowserInitializations: Map<string, Promise<BrowserRuntimeEntry>> =
64
+ hmrSingleton('__swarmclaw_pending_browser_inits__', () => new Map<string, Promise<BrowserRuntimeEntry>>())
65
+
66
+ // Kill all browsers on process exit to prevent orphaned Chromium processes
67
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
68
+ const _shutdownRegistered = hmrSingleton('__swarmclaw_browser_shutdown_registered__', () => {
69
+ process.on('exit', () => {
70
+ for (const [, entry] of activeBrowsers) {
71
+ try { entry.client?.close?.() } catch { /* ignore */ }
72
+ try { entry.server?.close?.() } catch { /* ignore */ }
73
+ }
74
+ })
75
+ return true
76
+ })
139
77
 
140
- export function sanitizePlaywrightMcpEnv(baseEnv: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
141
- const env: NodeJS.ProcessEnv = { ...baseEnv }
142
- for (const key of Object.keys(env)) {
143
- if (!key.toUpperCase().startsWith('PLAYWRIGHT_MCP_')) continue
144
- delete env[key]
145
- }
146
- return env
147
- }
148
78
  export function sweepOrphanedBrowsers(maxAgeMs = 30 * 60 * 1000): number {
149
79
  const now = Date.now(); let cleaned = 0
150
80
  for (const [key, entry] of activeBrowsers) {
@@ -171,213 +101,10 @@ export function cleanupSessionBrowser(sessionId: string): void {
171
101
  export function getActiveBrowserCount(): number { return activeBrowsers.size }
172
102
  export function hasActiveBrowser(sessionId: string): boolean { return activeBrowsers.has(sessionId) }
173
103
 
174
- export function inferWebActionFromArgs(params: {
175
- action?: string
176
- query?: string
177
- url?: string
178
- }): 'search' | 'fetch' | undefined {
179
- if (params.action === 'search' || params.action === 'fetch') return params.action
180
- if (typeof params.url === 'string' && /^https?:\/\//i.test(params.url.trim())) return 'fetch'
181
- if (typeof params.query === 'string' && params.query.trim()) return 'search'
182
- if (typeof params.url === 'string' && params.url.trim()) return 'search'
183
- return undefined
184
- }
185
-
186
- function parseStructuredJsonValue(value: unknown): unknown {
187
- if (typeof value !== 'string') return value
188
- const trimmed = value.trim()
189
- if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) return value
190
- try {
191
- return JSON.parse(trimmed)
192
- } catch {
193
- return value
194
- }
195
- }
196
-
197
- function parseJsonObjectValue(value: unknown): Record<string, unknown> | null {
198
- const parsed = parseStructuredJsonValue(value)
199
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
200
- ? parsed as Record<string, unknown>
201
- : null
202
- }
203
-
204
- function parseJsonArrayValue(value: unknown): unknown[] | null {
205
- const parsed = parseStructuredJsonValue(value)
206
- return Array.isArray(parsed) ? parsed : null
207
- }
208
-
209
- function pickNonEmptyBrowserString(...values: unknown[]): string | undefined {
210
- for (const value of values) {
211
- if (typeof value !== 'string') continue
212
- const trimmed = value.trim()
213
- if (trimmed) return trimmed
214
- }
215
- return undefined
216
- }
217
-
218
- function wrapBrowserEvaluateFunction(code: string): string {
219
- const trimmed = code.trim()
220
- if (!trimmed) return trimmed
221
- if (/^(?:async\s+)?function\b/.test(trimmed)) return trimmed
222
- if (/^(?:async\s*)?\([^)]*\)\s*=>/.test(trimmed)) return trimmed
223
- return /[;{}]/.test(trimmed)
224
- ? `() => { ${trimmed} }`
225
- : `() => (${trimmed})`
226
- }
227
-
228
- function wrapBrowserRunCodeFunction(code: string): string {
229
- const trimmed = code.trim()
230
- if (!trimmed) return trimmed
231
- if (/^(?:async\s+)?function\b/.test(trimmed)) return trimmed
232
- if (/^(?:async\s*)?\([^)]*\)\s*=>/.test(trimmed)) return trimmed
233
- return /[;{}]/.test(trimmed)
234
- ? `async (page) => { ${trimmed} }`
235
- : `async (page) => (${trimmed})`
236
- }
237
-
238
- export function normalizeBrowserActionParams(rawParams: Record<string, unknown>): Record<string, unknown> {
239
- const normalized = normalizeToolInputArgs(rawParams)
240
- const action = String(normalized.action || '').trim().toLowerCase()
241
- const params: Record<string, unknown> = { ...normalized }
242
-
243
- const parsedFields = parseJsonArrayValue(params.fields)
244
- if (parsedFields) params.fields = parsedFields
245
-
246
- const parsedForm = parseJsonObjectValue(params.form)
247
- if (parsedForm) params.form = parsedForm
248
-
249
- if (typeof params.selector === 'string' && !pickNonEmptyBrowserString(params.element)) {
250
- params.element = params.selector
251
- }
252
-
253
- if (action === 'submit_form' && typeof params.selector === 'string' && !pickNonEmptyBrowserString(params.submitElement)) {
254
- params.submitElement = params.selector
255
- }
256
-
257
- if (action === 'select') {
258
- const parsedValues = parseJsonArrayValue(params.values ?? params.option ?? params.value)
259
- if (parsedValues) params.values = parsedValues
260
- else if (params.values === undefined) {
261
- const scalar = pickNonEmptyBrowserString(params.option, params.value, params.text)
262
- if (scalar) params.values = [scalar]
263
- }
264
- }
265
-
266
- if (action === 'evaluate' && !pickNonEmptyBrowserString(params.function)) {
267
- const code = pickNonEmptyBrowserString(params.code, params.script, params.javascript, params.js)
268
- if (code) params.function = wrapBrowserEvaluateFunction(code)
269
- }
270
-
271
- if (action === 'run_code') {
272
- const code = pickNonEmptyBrowserString(params.code, params.function, params.script, params.javascript, params.js)
273
- if (code) params.code = wrapBrowserRunCodeFunction(code)
274
- }
275
-
276
- return params
277
- }
278
-
279
- function pickBrowserTargetFromParams(params: Record<string, unknown>): string | null {
280
- for (const value of [
281
- params.url,
282
- params.filePath,
283
- params.path,
284
- params.href,
285
- params.link,
286
- params.target,
287
- params.page,
288
- ]) {
289
- if (typeof value !== 'string') continue
290
- const trimmed = value.trim()
291
- if (trimmed) return trimmed
292
- }
293
- return null
294
- }
295
-
296
- function resolveUploadFilePath(target: string): string | null {
297
- const normalized = target.replace(/^sandbox:/, '')
298
- const match = normalized.match(/^\/api\/uploads\/([^?#]+)/)
299
- if (!match) return null
300
- let decoded = match[1]
301
- try {
302
- decoded = decodeURIComponent(decoded)
303
- } catch {
304
- // keep raw segment
305
- }
306
- const safeName = decoded.replace(/[^a-zA-Z0-9._-]/g, '')
307
- const resolved = path.join(UPLOAD_DIR, safeName)
308
- return fs.existsSync(resolved) ? resolved : null
309
- }
310
-
311
- function resolveBrowserFileUrlPath(target: string): string | null {
312
- if (!/^file:/i.test(target)) return null
313
- try {
314
- const resolved = fileURLToPath(target)
315
- return fs.existsSync(resolved) ? resolved : null
316
- } catch {
317
- return null
318
- }
319
- }
320
-
321
- function tryResolveBrowserLocalPath(cwd: string, target: string): string | null {
322
- const uploadPath = resolveUploadFilePath(target)
323
- if (uploadPath) return uploadPath
324
-
325
- const fileUrlPath = resolveBrowserFileUrlPath(target)
326
- if (fileUrlPath) return fileUrlPath
327
-
328
- if (/^(?:https?:|about:|data:)/i.test(target)) return null
329
-
330
- const normalized = target.replace(/^sandbox:/, '')
331
- const looksLikePath = normalized.startsWith('/')
332
- || normalized.startsWith('./')
333
- || normalized.startsWith('../')
334
- || normalized.includes('/')
335
- || /\.(?:html?|xhtml|txt|md|json|ya?ml|csv|ts|tsx|js|jsx|mjs|cjs|css|png|jpe?g|gif|webp|svg|pdf)$/i.test(normalized)
336
- if (!looksLikePath) return null
337
-
338
- const candidates = new Set<string>()
339
- if (path.isAbsolute(normalized)) candidates.add(normalized)
340
- try { candidates.add(safePath(cwd, normalized)) } catch { /* ignore */ }
341
- try { candidates.add(path.resolve(cwd, normalized)) } catch { /* ignore */ }
342
-
343
- for (const candidate of candidates) {
344
- if (!candidate || !fs.existsSync(candidate)) continue
345
- const stat = fs.statSync(candidate)
346
- if (stat.isDirectory()) {
347
- const indexPath = path.join(candidate, 'index.html')
348
- if (fs.existsSync(indexPath)) return indexPath
349
- return null
350
- }
351
- return candidate
352
- }
353
- return null
354
- }
355
-
356
- function localHtmlFileToDataUrl(filePath: string): string | null {
357
- const ext = path.extname(filePath).toLowerCase()
358
- if (ext !== '.html' && ext !== '.htm') return null
359
- try {
360
- const html = fs.readFileSync(filePath, 'utf8')
361
- const hasRelativeAssetReferences = /<(?:script|img|source|video|audio)\b[^>]+\b(?:src|poster)\s*=\s*["'](?![a-z]+:|\/\/|#|\/)([^"']+)["']|<link\b[^>]+\bhref\s*=\s*["'](?![a-z]+:|\/\/|#|\/)([^"']+)["']/i.test(html)
362
- if (hasRelativeAssetReferences) return null
363
- return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`
364
- } catch {
365
- return null
366
- }
367
- }
368
-
369
- export function resolveBrowserNavigationTarget(cwd: string, target: string): string {
370
- const trimmed = target.trim()
371
- if (!trimmed) return trimmed
372
- const localPath = tryResolveBrowserLocalPath(cwd, trimmed)
373
- if (localPath) return localHtmlFileToDataUrl(localPath) || pathToFileURL(localPath).toString()
374
- return trimmed.replace(/^sandbox:/, '')
375
- }
376
-
377
104
  /**
378
105
  * Unified Web Execution Logic
379
106
  */
380
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
107
+
381
108
  async function executeWebAction(args: Record<string, unknown>) {
382
109
  const normalized = normalizeToolInputArgs(args)
383
110
  const { query, url, maxResults } = normalized as { query?: string; url?: string; maxResults?: number }
@@ -414,7 +141,7 @@ async function executeWebAction(args: Record<string, unknown>) {
414
141
  const result = await pdfParse(Buffer.from(arrayBuffer))
415
142
  return truncate(result.text, MAX_OUTPUT)
416
143
  } catch (err: unknown) {
417
- return `Error parsing PDF: ${err instanceof Error ? err.message : String(err)}`
144
+ return `Error parsing PDF: ${errorMessage(err)}`
418
145
  }
419
146
  }
420
147
  const html = await res.text()
@@ -426,7 +153,7 @@ async function executeWebAction(args: Record<string, unknown>) {
426
153
  }
427
154
  return `Error: Unknown action "${action}"`
428
155
  } catch (err: unknown) {
429
- return `Error: ${err instanceof Error ? err.message : String(err)}`
156
+ return `Error: ${errorMessage(err)}`
430
157
  }
431
158
  }
432
159
 
@@ -557,7 +284,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
557
284
  inheritedFromSessionId: profileInfo.inheritedFromSessionId,
558
285
  status: 'error',
559
286
  lastAction: 'browser_restore',
560
- lastError: err instanceof Error ? err.message : String(err),
287
+ lastError: errorMessage(err),
561
288
  })
562
289
  }
563
290
  }
@@ -877,7 +604,14 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
877
604
  const fileMatch = text.match(/\]\((\.\.\/[^\s)]+|\/[^\s)]+\.(pdf|png|jpg|jpeg|gif|webp|html|mp4|webm))\)/)
878
605
  if (fileMatch) {
879
606
  const rawPath = fileMatch[1]
880
- const srcPath = rawPath.startsWith('/') ? rawPath : path.resolve(process.cwd(), rawPath)
607
+ let srcPath = rawPath
608
+ if (!rawPath.startsWith('/')) {
609
+ try {
610
+ srcPath = resolvePathWithinBaseDir(cwd, rawPath)
611
+ } catch {
612
+ continue
613
+ }
614
+ }
881
615
  if (fs.existsSync(srcPath)) {
882
616
  const ext = path.extname(srcPath).slice(1).toLowerCase()
883
617
  const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'gif', 'webp']
@@ -913,7 +647,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
913
647
  }
914
648
  if (isScreenshotTool) parts = dedupeScreenshotMarkdownLines(parts)
915
649
  if (savedPaths.length > 0) {
916
- const unique = Array.from(new Set(savedPaths))
650
+ const unique = dedup(savedPaths)
917
651
  parts.push(`Saved to: ${unique.map((p) => path.relative(cwd, p) || '.').join(', ')}`)
918
652
  }
919
653
  upsertBrowserSessionRecord({
@@ -938,7 +672,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
938
672
  })
939
673
  return fallback
940
674
  } catch (err: unknown) {
941
- const message = err instanceof Error ? err.message : String(err)
675
+ const message = errorMessage(err)
942
676
  upsertBrowserSessionRecord({
943
677
  sessionId: sessionKey,
944
678
  profileId: profileInfo.profileId,
@@ -981,7 +715,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
981
715
  }
982
716
 
983
717
  const dismissCookieBanners = async (mcpCall: (toolName: string, args: Record<string, unknown>) => Promise<string>) => {
984
- await new Promise((r) => setTimeout(r, 1200))
718
+ await sleep(1200)
985
719
  const js = `() => {
986
720
  const docs = [document];
987
721
  const roots = [document];
@@ -1250,7 +984,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
1250
984
  try {
1251
985
  await callBrowserEvaluate(`async () => { await new Promise(resolve => setTimeout(resolve, ${Math.min(waitMs, 5000)})); }`)
1252
986
  } catch {
1253
- await new Promise((resolve) => setTimeout(resolve, waitMs))
987
+ await sleep(waitMs)
1254
988
  }
1255
989
 
1256
990
  return {
@@ -1654,7 +1388,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
1654
1388
  }
1655
1389
  }); }`)
1656
1390
  } catch {
1657
- await new Promise((r) => setTimeout(r, 1200))
1391
+ await sleep(1200)
1658
1392
  }
1659
1393
  try { await dismissCookieBanners(callMcpTool) } catch { /* ignore */ }
1660
1394
  }
@@ -1669,7 +1403,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
1669
1403
 
1670
1404
  let result = await callMcpTool(mcpTool, args, { saveTo: typeof params.saveTo === 'string' ? params.saveTo : undefined })
1671
1405
  if (action === 'navigate' && result.includes('ERR_ABORTED')) {
1672
- await new Promise((r) => setTimeout(r, 1000))
1406
+ await sleep(1000)
1673
1407
  result = await callMcpTool('browser_snapshot', {})
1674
1408
  }
1675
1409
  if (action === 'navigate') {
@@ -1686,7 +1420,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
1686
1420
 
1687
1421
  return result
1688
1422
  } catch (err: unknown) {
1689
- return `Error: ${err instanceof Error ? err.message : String(err)}`
1423
+ return `Error: ${errorMessage(err)}`
1690
1424
  }
1691
1425
  },
1692
1426
  {
@@ -1775,7 +1509,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
1775
1509
  const result = runBrowserCommand(command, parsedArgs)
1776
1510
  if (result.status !== 0) return `Error (exit ${result.status}): ${result.stderr || result.stdout || 'unknown'}`
1777
1511
  return truncate(result.stdout || '(no output)', MAX_OUTPUT)
1778
- } catch (err: unknown) { return `Error: ${err instanceof Error ? err.message : String(err)}` }
1512
+ } catch (err: unknown) { return `Error: ${errorMessage(err)}` }
1779
1513
  },
1780
1514
  {
1781
1515
  name: 'openclaw_browser',
@@ -4,7 +4,7 @@ import { budgetSkillsForPrompt, buildSkillPromptText, MAX_SKILLS_IN_PROMPT, MAX_
4
4
  import type { Skill } from '@/types'
5
5
 
6
6
  function makeSkill(id: string, overrides: Partial<Skill> = {}): Skill {
7
- return {
7
+ return { id: "test-skill-1",
8
8
  name: id,
9
9
  filename: `${id}.md`,
10
10
  content: overrides.content ?? `Instructions for ${id} skill.`,
@@ -1,5 +1,6 @@
1
1
  import path from 'path'
2
2
  import type { SkillInstallOption, SkillRequirements, SkillSecuritySummary } from '@/types'
3
+ import { dedup } from '@/lib/shared-utils'
3
4
 
4
5
  export type SkillSourceFormat = 'openclaw' | 'plain'
5
6
 
@@ -292,7 +293,7 @@ function pickRuntimeMetadata(frontmatter: Record<string, unknown>): Record<strin
292
293
  }
293
294
 
294
295
  function uniqueStrings(values: string[]): string[] {
295
- return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)))
296
+ return dedup(values.map((value) => value.trim()).filter(Boolean))
296
297
  }
297
298
 
298
299
  function extractDetectedEnvVars(rawContent: string): string[] {