@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
@@ -1,5 +1,6 @@
1
1
  import { loadSettings } from './storage'
2
2
  import type { AppNotification } from '@/types'
3
+ import { errorMessage } from '@/lib/shared-utils'
3
4
 
4
5
  /** In-memory rate limiter: dedupKey → last dispatch timestamp */
5
6
  const recentDispatches = new Map<string, number>()
@@ -59,6 +60,6 @@ export async function dispatchAlert(notification: AppNotification): Promise<void
59
60
  signal: AbortSignal.timeout(5000),
60
61
  })
61
62
  } catch (err: unknown) {
62
- console.warn('[alert-dispatch] Webhook delivery failed:', err instanceof Error ? err.message : String(err))
63
+ console.warn('[alert-dispatch] Webhook delivery failed:', errorMessage(err))
63
64
  }
64
65
  }
@@ -230,13 +230,13 @@ describe('Knowledge API contract', () => {
230
230
  // --- Route file structure -----------------------------------------------
231
231
  describe('route file exports', () => {
232
232
  it('knowledge/route.ts exports GET and POST', () => {
233
- const src = readRoute('knowledge', 'route.ts')
233
+ const src = readRoute('knowledge', 'route')
234
234
  assert.match(src, /export\s+async\s+function\s+GET/)
235
235
  assert.match(src, /export\s+async\s+function\s+POST/)
236
236
  })
237
237
 
238
238
  it('knowledge/[id]/route.ts exports GET, PUT, DELETE', () => {
239
- const src = readRoute('knowledge', '[id]', 'route.ts')
239
+ const src = readRoute('knowledge', '[id]', 'route')
240
240
  assert.match(src, /export\s+async\s+function\s+GET/)
241
241
  assert.match(src, /export\s+async\s+function\s+PUT/)
242
242
  assert.match(src, /export\s+async\s+function\s+DELETE/)
@@ -334,25 +334,25 @@ describe('MCP Server API contract', () => {
334
334
  // --- Route file structure -----------------------------------------------
335
335
  describe('route file exports', () => {
336
336
  it('mcp-servers/route.ts exports GET and POST', () => {
337
- const src = readRoute('mcp-servers', 'route.ts')
337
+ const src = readRoute('mcp-servers', 'route')
338
338
  assert.match(src, /export\s+async\s+function\s+GET/)
339
339
  assert.match(src, /export\s+async\s+function\s+POST/)
340
340
  })
341
341
 
342
342
  it('mcp-servers/[id]/route.ts exports GET, PUT, DELETE', () => {
343
- const src = readRoute('mcp-servers', '[id]', 'route.ts')
343
+ const src = readRoute('mcp-servers', '[id]', 'route')
344
344
  assert.match(src, /export\s+async\s+function\s+GET/)
345
345
  assert.match(src, /export\s+async\s+function\s+PUT/)
346
346
  assert.match(src, /export\s+async\s+function\s+DELETE/)
347
347
  })
348
348
 
349
349
  it('MCP POST route assigns an id via genId helper', () => {
350
- const src = readRoute('mcp-servers', 'route.ts')
350
+ const src = readRoute('mcp-servers', 'route')
351
351
  assert.match(src, /const\s+id\s*=\s*genId\(/)
352
352
  })
353
353
 
354
354
  it('MCP PUT route preserves id and sets updatedAt via mutateItem', () => {
355
- const src = readRoute('mcp-servers', '[id]', 'route.ts')
355
+ const src = readRoute('mcp-servers', '[id]', 'route')
356
356
  assert.match(src, /mutateItem\(/)
357
357
  assert.match(src, /updatedAt:\s*Date\.now\(\)/)
358
358
  assert.match(src, /\.\.\.server,\s*\.\.\.body,\s*id,/)
@@ -35,8 +35,8 @@ function runWithTempDataDir(script: string) {
35
35
  describe('approval connector reminders', () => {
36
36
  it('resolves a due approval to the session connector target and records one-shot delivery state', () => {
37
37
  const output = runWithTempDataDir(`
38
- const storageMod = await import('./src/lib/server/storage.ts')
39
- const approvalsMod = await import('./src/lib/server/approvals.ts')
38
+ const storageMod = await import('./src/lib/server/storage')
39
+ const approvalsMod = await import('./src/lib/server/approvals')
40
40
  const storage = storageMod.default || storageMod
41
41
  const approvals = approvalsMod.default || approvalsMod
42
42
 
@@ -148,8 +148,8 @@ describe('approval connector reminders', () => {
148
148
 
149
149
  it('falls back to a running owned connector and respects retry cooldowns after failed sends', () => {
150
150
  const output = runWithTempDataDir(`
151
- const storageMod = await import('./src/lib/server/storage.ts')
152
- const approvalsMod = await import('./src/lib/server/approvals.ts')
151
+ const storageMod = await import('./src/lib/server/storage')
152
+ const approvalsMod = await import('./src/lib/server/approvals')
153
153
  const storage = storageMod.default || storageMod
154
154
  const approvals = approvalsMod.default || approvalsMod
155
155
 
@@ -35,8 +35,8 @@ function runWithTempDataDir(script: string) {
35
35
  describe('approval auto-approve', () => {
36
36
  it('defaults new installs to approvals auto-run', () => {
37
37
  const output = runWithTempDataDir(`
38
- const storageMod = await import('./src/lib/server/storage.ts')
39
- const approvalsMod = await import('./src/lib/server/approvals.ts')
38
+ const storageMod = await import('./src/lib/server/storage')
39
+ const approvalsMod = await import('./src/lib/server/approvals')
40
40
  const storage = storageMod.default || storageMod
41
41
  const approvals = approvalsMod.default || approvalsMod
42
42
 
@@ -86,9 +86,9 @@ describe('approval auto-approve', () => {
86
86
 
87
87
  it('auto-approves tool access and plugin scaffolds when configured', () => {
88
88
  const output = runWithTempDataDir(`
89
- const storageMod = await import('./src/lib/server/storage.ts')
90
- const approvalsMod = await import('./src/lib/server/approvals.ts')
91
- const dataDirMod = await import('./src/lib/server/data-dir.ts')
89
+ const storageMod = await import('./src/lib/server/storage')
90
+ const approvalsMod = await import('./src/lib/server/approvals')
91
+ const dataDirMod = await import('./src/lib/server/data-dir')
92
92
  const storage = storageMod.default || storageMod
93
93
  const approvals = approvalsMod.default || approvalsMod
94
94
  const dataDir = dataDirMod.DATA_DIR || dataDirMod.default?.DATA_DIR || dataDirMod['module.exports']?.DATA_DIR
@@ -164,9 +164,9 @@ describe('approval auto-approve', () => {
164
164
 
165
165
  it('can disable approvals platform-wide for fully autonomous execution', () => {
166
166
  const output = runWithTempDataDir(`
167
- const storageMod = await import('./src/lib/server/storage.ts')
168
- const approvalsMod = await import('./src/lib/server/approvals.ts')
169
- const sessionRunsMod = await import('./src/lib/server/session-run-manager.ts')
167
+ const storageMod = await import('./src/lib/server/storage')
168
+ const approvalsMod = await import('./src/lib/server/approvals')
169
+ const sessionRunsMod = await import('./src/lib/server/session-run-manager')
170
170
  const storage = storageMod.default || storageMod
171
171
  const approvals = approvalsMod.default || approvalsMod
172
172
  const sessionRuns = sessionRunsMod.default || sessionRunsMod
@@ -225,8 +225,8 @@ describe('approval auto-approve', () => {
225
225
 
226
226
  it('adds a pending approval request message to the chat session when approvals are enabled', () => {
227
227
  const output = runWithTempDataDir(`
228
- const storageMod = await import('./src/lib/server/storage.ts')
229
- const approvalsMod = await import('./src/lib/server/approvals.ts')
228
+ const storageMod = await import('./src/lib/server/storage')
229
+ const approvalsMod = await import('./src/lib/server/approvals')
230
230
  const storage = storageMod.default || storageMod
231
231
  const approvals = approvalsMod.default || approvalsMod
232
232
 
@@ -288,9 +288,9 @@ describe('approval auto-approve', () => {
288
288
  it('injects approval guidance only from enabled plugins', () => {
289
289
  const output = runWithTempDataDir(`
290
290
  process.on('unhandledRejection', () => {})
291
- await import('./src/lib/server/session-tools/wallet.ts')
292
- const storageMod = await import('./src/lib/server/storage.ts')
293
- const approvalsMod = await import('./src/lib/server/approvals.ts')
291
+ await import('./src/lib/server/session-tools/wallet')
292
+ const storageMod = await import('./src/lib/server/storage')
293
+ const approvalsMod = await import('./src/lib/server/approvals')
294
294
  const storage = storageMod.default || storageMod
295
295
  const approvals = approvalsMod.default || approvalsMod
296
296
 
@@ -424,9 +424,9 @@ describe('approval auto-approve', () => {
424
424
  it('derives tool-access approval guidance from the requested plugin metadata', () => {
425
425
  const output = runWithTempDataDir(`
426
426
  process.on('unhandledRejection', () => {})
427
- await import('./src/lib/server/session-tools/http.ts')
428
- const storageMod = await import('./src/lib/server/storage.ts')
429
- const approvalsMod = await import('./src/lib/server/approvals.ts')
427
+ await import('./src/lib/server/session-tools/http')
428
+ const storageMod = await import('./src/lib/server/storage')
429
+ const approvalsMod = await import('./src/lib/server/approvals')
430
430
  const storage = storageMod.default || storageMod
431
431
  const approvals = approvalsMod.default || approvalsMod
432
432
 
@@ -478,9 +478,9 @@ describe('approval auto-approve', () => {
478
478
  it('injects plugin-owned scaffold guidance for plugin creator approvals', () => {
479
479
  const output = runWithTempDataDir(`
480
480
  process.on('unhandledRejection', () => {})
481
- await import('./src/lib/server/session-tools/plugin-creator.ts')
482
- const storageMod = await import('./src/lib/server/storage.ts')
483
- const approvalsMod = await import('./src/lib/server/approvals.ts')
481
+ await import('./src/lib/server/session-tools/plugin-creator')
482
+ const storageMod = await import('./src/lib/server/storage')
483
+ const approvalsMod = await import('./src/lib/server/approvals')
484
484
  const storage = storageMod.default || storageMod
485
485
  const approvals = approvalsMod.default || approvalsMod
486
486
 
@@ -549,8 +549,8 @@ describe('approval auto-approve', () => {
549
549
  it('applies tool access after a manual approval decision', () => {
550
550
  const output = runWithTempDataDir(`
551
551
  process.on('unhandledRejection', () => {})
552
- const storageMod = await import('./src/lib/server/storage.ts')
553
- const approvalsMod = await import('./src/lib/server/approvals.ts')
552
+ const storageMod = await import('./src/lib/server/storage')
553
+ const approvalsMod = await import('./src/lib/server/approvals')
554
554
  const storage = storageMod.default || storageMod
555
555
  const approvals = approvalsMod.default || approvalsMod
556
556
 
@@ -609,9 +609,9 @@ describe('approval auto-approve', () => {
609
609
  it('wakes the blocked session after a manual approval decision', () => {
610
610
  const output = runWithTempDataDir(`
611
611
  process.on('unhandledRejection', () => {})
612
- const storageMod = await import('./src/lib/server/storage.ts')
613
- const approvalsMod = await import('./src/lib/server/approvals.ts')
614
- const sessionRunsMod = await import('./src/lib/server/session-run-manager.ts')
612
+ const storageMod = await import('./src/lib/server/storage')
613
+ const approvalsMod = await import('./src/lib/server/approvals')
614
+ const sessionRunsMod = await import('./src/lib/server/session-run-manager')
615
615
  const storage = storageMod.default || storageMod
616
616
  const approvals = approvalsMod.default || approvalsMod
617
617
  const sessionRuns = sessionRunsMod.default || sessionRunsMod
@@ -672,14 +672,14 @@ describe('approval auto-approve', () => {
672
672
 
673
673
  assert.equal(output.finalStatus, 'approved')
674
674
  assert.equal(output.runCount >= 1, true)
675
- assert.equal(output.runSources.filter((source) => source === 'approval-decision').length, 1)
675
+ assert.equal(output.runSources.filter((source: any) => source === "approval-decision").length, 1)
676
676
  })
677
677
 
678
678
  it('reuses equivalent wallet approvals instead of creating duplicates', () => {
679
679
  const output = runWithTempDataDir(`
680
680
  process.on('unhandledRejection', () => {})
681
- const storageMod = await import('./src/lib/server/storage.ts')
682
- const approvalsMod = await import('./src/lib/server/approvals.ts')
681
+ const storageMod = await import('./src/lib/server/storage')
682
+ const approvalsMod = await import('./src/lib/server/approvals')
683
683
  const storage = storageMod.default || storageMod
684
684
  const approvals = approvalsMod.default || approvalsMod
685
685
 
@@ -754,8 +754,8 @@ describe('approval auto-approve', () => {
754
754
 
755
755
  it('reuses approved tool-access decisions across sessions for the same agent', () => {
756
756
  const output = runWithTempDataDir(`
757
- const storageMod = await import('./src/lib/server/storage.ts')
758
- const approvalsMod = await import('./src/lib/server/approvals.ts')
757
+ const storageMod = await import('./src/lib/server/storage')
758
+ const approvalsMod = await import('./src/lib/server/approvals')
759
759
  const storage = storageMod.default || storageMod
760
760
  const approvals = approvalsMod.default || approvalsMod
761
761
 
@@ -0,0 +1,317 @@
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
+ const originalEnv = {
8
+ DATA_DIR: process.env.DATA_DIR,
9
+ WORKSPACE_DIR: process.env.WORKSPACE_DIR,
10
+ SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
11
+ }
12
+
13
+ let tempDir = ''
14
+ let approvals: typeof import('./approvals')
15
+ let storage: typeof import('./storage')
16
+
17
+ before(async () => {
18
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-approvals-'))
19
+ process.env.DATA_DIR = path.join(tempDir, 'data')
20
+ process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
21
+ process.env.SWARMCLAW_BUILD_MODE = '1'
22
+ storage = await import('./storage')
23
+ approvals = await import('./approvals')
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
+ describe('approvals', () => {
37
+ // ---- requestApproval ----
38
+
39
+ it('creates a pending approval with correct fields', () => {
40
+ const result = approvals.requestApproval({
41
+ category: 'tool_access',
42
+ title: 'Enable web',
43
+ data: { toolId: 'web' },
44
+ agentId: 'agent-1',
45
+ sessionId: 'session-1',
46
+ })
47
+
48
+ assert.equal(result.status, 'pending')
49
+ assert.equal(result.category, 'tool_access')
50
+ assert.equal(result.agentId, 'agent-1')
51
+ assert.equal(result.sessionId, 'session-1')
52
+ assert.ok(result.id.length > 0)
53
+ assert.ok(result.createdAt > 0)
54
+ assert.equal(result.createdAt, result.updatedAt)
55
+ })
56
+
57
+ it('normalizes tool_access title to "Enable Plugin: <id>"', () => {
58
+ const result = approvals.requestApproval({
59
+ category: 'tool_access',
60
+ title: 'Whatever title',
61
+ data: { toolId: 'shell' },
62
+ })
63
+ assert.equal(result.title, 'Enable Plugin: shell')
64
+ })
65
+
66
+ it('copies toolId to both toolId and pluginId for tool_access', () => {
67
+ const result = approvals.requestApproval({
68
+ category: 'tool_access',
69
+ title: 'test',
70
+ data: { toolId: 'browser' },
71
+ })
72
+ assert.equal(result.data.toolId, 'browser')
73
+ assert.equal(result.data.pluginId, 'browser')
74
+ })
75
+
76
+ it('throws when tool_access has no toolId or pluginId', () => {
77
+ assert.throws(() => {
78
+ approvals.requestApproval({
79
+ category: 'tool_access',
80
+ title: 'bad',
81
+ data: {},
82
+ })
83
+ }, /toolId or pluginId/)
84
+ })
85
+
86
+ it('allows non-tool_access categories without toolId', () => {
87
+ const result = approvals.requestApproval({
88
+ category: 'wallet_transfer',
89
+ title: 'Send 1 SOL',
90
+ data: { toAddress: 'abc', amountSol: 1 },
91
+ })
92
+ assert.equal(result.status, 'pending')
93
+ assert.equal(result.category, 'wallet_transfer')
94
+ })
95
+
96
+ // ---- listAutoApprovableApprovalCategories ----
97
+
98
+ it('returns a copy of auto-approvable categories', () => {
99
+ const cats = approvals.listAutoApprovableApprovalCategories()
100
+ assert.ok(cats.includes('tool_access'))
101
+ assert.ok(cats.includes('wallet_transfer'))
102
+ assert.ok(cats.includes('connector_sender'))
103
+ // Mutating the returned array shouldn't affect future calls
104
+ cats.push('fake' as never)
105
+ const cats2 = approvals.listAutoApprovableApprovalCategories()
106
+ assert.ok(!cats2.includes('fake' as never))
107
+ })
108
+
109
+ // ---- isApprovalCategoryAutoApproved ----
110
+
111
+ it('returns false when no auto-approve categories configured', () => {
112
+ assert.equal(approvals.isApprovalCategoryAutoApproved('tool_access'), false)
113
+ })
114
+
115
+ it('returns true when category is in the auto-approve list', () => {
116
+ // Configure auto-approve categories in settings
117
+ const settings = storage.loadSettings()
118
+ settings.approvalAutoApproveCategories = ['tool_access', 'wallet_transfer']
119
+ storage.saveSettings(settings)
120
+
121
+ assert.equal(approvals.isApprovalCategoryAutoApproved('tool_access'), true)
122
+ assert.equal(approvals.isApprovalCategoryAutoApproved('wallet_transfer'), true)
123
+ assert.equal(approvals.isApprovalCategoryAutoApproved('plugin_scaffold'), false)
124
+
125
+ // Clean up
126
+ settings.approvalAutoApproveCategories = []
127
+ storage.saveSettings(settings)
128
+ })
129
+
130
+ // ---- listPendingApprovals ----
131
+
132
+ it('lists only pending approvals sorted by updatedAt desc', () => {
133
+ // Create several approvals
134
+ const a1 = approvals.requestApproval({
135
+ category: 'tool_access',
136
+ title: 'A',
137
+ data: { toolId: 'a1tool' },
138
+ })
139
+ const a2 = approvals.requestApproval({
140
+ category: 'tool_access',
141
+ title: 'B',
142
+ data: { toolId: 'a2tool' },
143
+ })
144
+
145
+ const pending = approvals.listPendingApprovals()
146
+ const ids = pending.map((p) => p.id)
147
+ assert.ok(ids.includes(a1.id))
148
+ assert.ok(ids.includes(a2.id))
149
+ // All returned should be pending
150
+ for (const p of pending) {
151
+ assert.equal(p.status, 'pending')
152
+ }
153
+ // Sorted desc by updatedAt
154
+ for (let i = 1; i < pending.length; i++) {
155
+ assert.ok(pending[i - 1].updatedAt >= pending[i].updatedAt)
156
+ }
157
+ })
158
+
159
+ // ---- submitDecision ----
160
+
161
+ it('approves a pending request', async () => {
162
+ const req = approvals.requestApproval({
163
+ category: 'human_loop',
164
+ title: 'Confirm deployment',
165
+ data: { question: 'Deploy to prod?' },
166
+ sessionId: null,
167
+ agentId: null,
168
+ })
169
+ assert.equal(req.status, 'pending')
170
+
171
+ await approvals.submitDecision(req.id, true)
172
+
173
+ // Check it's no longer pending
174
+ const pending = approvals.listPendingApprovals()
175
+ assert.ok(!pending.some((p) => p.id === req.id))
176
+ })
177
+
178
+ it('rejects a pending request', async () => {
179
+ const req = approvals.requestApproval({
180
+ category: 'human_loop',
181
+ title: 'Confirm deletion',
182
+ data: { question: 'Delete everything?' },
183
+ sessionId: null,
184
+ agentId: null,
185
+ })
186
+
187
+ await approvals.submitDecision(req.id, false)
188
+
189
+ const pending = approvals.listPendingApprovals()
190
+ assert.ok(!pending.some((p) => p.id === req.id))
191
+ })
192
+
193
+ it('throws for non-existent approval id', async () => {
194
+ await assert.rejects(
195
+ () => approvals.submitDecision('nonexistent-xyz', true),
196
+ /not found/i,
197
+ )
198
+ })
199
+
200
+ it('is idempotent — approving an already-approved request is a no-op', async () => {
201
+ const req = approvals.requestApproval({
202
+ category: 'human_loop',
203
+ title: 'Idempotent test',
204
+ data: { question: 'yes?' },
205
+ })
206
+ await approvals.submitDecision(req.id, true)
207
+ // Should not throw
208
+ await approvals.submitDecision(req.id, true)
209
+ })
210
+
211
+ // ---- requestApprovalMaybeAutoApprove ----
212
+
213
+ it('auto-approves when approvalsEnabled is false', async () => {
214
+ const settings = storage.loadSettings()
215
+ settings.approvalsEnabled = false
216
+ storage.saveSettings(settings)
217
+
218
+ const result = await approvals.requestApprovalMaybeAutoApprove({
219
+ category: 'tool_access',
220
+ title: 'Auto test',
221
+ data: { toolId: 'auto_tool_1' },
222
+ })
223
+ assert.equal(result.status, 'approved')
224
+
225
+ settings.approvalsEnabled = true
226
+ storage.saveSettings(settings)
227
+ })
228
+
229
+ it('auto-approves when category is in auto-approve list', async () => {
230
+ const settings = storage.loadSettings()
231
+ settings.approvalAutoApproveCategories = ['wallet_transfer']
232
+ storage.saveSettings(settings)
233
+
234
+ const result = await approvals.requestApprovalMaybeAutoApprove({
235
+ category: 'wallet_transfer',
236
+ title: 'Auto transfer',
237
+ data: { toAddress: 'xyz_auto', amountSol: 0.5 },
238
+ })
239
+ assert.equal(result.status, 'approved')
240
+
241
+ settings.approvalAutoApproveCategories = []
242
+ storage.saveSettings(settings)
243
+ })
244
+
245
+ it('stays pending when approvals enabled and category not auto-approved', async () => {
246
+ const settings = storage.loadSettings()
247
+ settings.approvalsEnabled = true
248
+ settings.approvalAutoApproveCategories = []
249
+ storage.saveSettings(settings)
250
+
251
+ const result = await approvals.requestApprovalMaybeAutoApprove({
252
+ category: 'plugin_scaffold',
253
+ title: 'Manual scaffold',
254
+ data: { filename: 'test.js', code: 'module.exports = {}' },
255
+ })
256
+ assert.equal(result.status, 'pending')
257
+ })
258
+
259
+ // ---- markApprovalConnectorNotificationAttempt / Sent ----
260
+
261
+ it('records connector notification attempt', () => {
262
+ const req = approvals.requestApproval({
263
+ category: 'human_loop',
264
+ title: 'Notify test',
265
+ data: { question: 'hello?' },
266
+ })
267
+
268
+ const updated = approvals.markApprovalConnectorNotificationAttempt(req.id, {
269
+ at: 1000,
270
+ connectorId: 'conn-1',
271
+ channelId: 'ch-1',
272
+ })
273
+ assert.ok(updated)
274
+ assert.equal(updated!.connectorNotification?.attemptedAt, 1000)
275
+ assert.equal(updated!.connectorNotification?.connectorId, 'conn-1')
276
+ assert.equal(updated!.connectorNotification?.sentAt, undefined)
277
+ })
278
+
279
+ it('records connector notification sent', () => {
280
+ const req = approvals.requestApproval({
281
+ category: 'human_loop',
282
+ title: 'Sent test',
283
+ data: { question: 'done?' },
284
+ })
285
+
286
+ const updated = approvals.markApprovalConnectorNotificationSent(req.id, {
287
+ at: 2000,
288
+ connectorId: 'conn-2',
289
+ channelId: 'ch-2',
290
+ messageId: 'msg-123',
291
+ })
292
+ assert.ok(updated)
293
+ assert.equal(updated!.connectorNotification?.sentAt, 2000)
294
+ assert.equal(updated!.connectorNotification?.connectorId, 'conn-2')
295
+ assert.equal(updated!.connectorNotification?.messageId, 'msg-123')
296
+ assert.equal(updated!.connectorNotification?.lastError, null)
297
+ })
298
+
299
+ it('returns null for notification attempt on non-existent id', () => {
300
+ const result = approvals.markApprovalConnectorNotificationAttempt('ghost-id', { at: 1 })
301
+ assert.equal(result, null)
302
+ })
303
+
304
+ // ---- listPendingApprovalsNeedingConnectorNotification ----
305
+
306
+ it('returns empty when notification is disabled', () => {
307
+ const settings = storage.loadSettings()
308
+ settings.approvalConnectorNotifyEnabled = false
309
+ storage.saveSettings(settings)
310
+
311
+ const result = approvals.listPendingApprovalsNeedingConnectorNotification()
312
+ assert.deepEqual(result, [])
313
+
314
+ settings.approvalConnectorNotifyEnabled = true
315
+ storage.saveSettings(settings)
316
+ })
317
+ })
@@ -1,6 +1,7 @@
1
1
  import { genId } from '@/lib/id'
2
2
  import { loadApprovals, upsertApproval, loadSessions, saveSessions, loadSettings, loadAgents } from './storage'
3
3
  import type { ApprovalRequest, ApprovalCategory, Message } from '@/types'
4
+ import { dedup, errorMessage } from '@/lib/shared-utils'
4
5
  import { notify } from './ws-hub'
5
6
  import { log } from './logger'
6
7
  import { requestHeartbeatNow } from './heartbeat-wake'
@@ -67,7 +68,7 @@ function getEnabledPluginsForApproval(request: ApprovalRequest): string[] {
67
68
  const sessionPlugins = request.sessionId ? normalizePluginList(sessions[request.sessionId]?.plugins) : []
68
69
  const agentPlugins = request.agentId ? normalizePluginList(agents[request.agentId]?.plugins) : []
69
70
  const targetPlugins = getApprovalTargetPlugins(request)
70
- return Array.from(new Set([...sessionPlugins, ...agentPlugins, ...targetPlugins]))
71
+ return dedup([...sessionPlugins, ...agentPlugins, ...targetPlugins])
71
72
  }
72
73
 
73
74
  function getApprovalGuidance(
@@ -533,7 +534,7 @@ async function applyApprovedSideEffects(request: ApprovalRequest): Promise<void>
533
534
  } catch (err: unknown) {
534
535
  log.error('approvals', 'Plugin scaffold dependency setup failed', {
535
536
  filename,
536
- error: err instanceof Error ? err.message : String(err),
537
+ error: errorMessage(err),
537
538
  })
538
539
  await manager.savePluginSource(filename, code, {
539
540
  meta: createdByAgentId ? { createdByAgentId } : undefined,
@@ -573,7 +574,7 @@ async function applyApprovedSideEffects(request: ApprovalRequest): Promise<void>
573
574
  } catch (err: unknown) {
574
575
  log.error('approvals', 'Plugin install failed after approval', {
575
576
  url,
576
- error: err instanceof Error ? err.message : String(err),
577
+ error: errorMessage(err),
577
578
  })
578
579
  }
579
580
  } else if (filename) {
@@ -596,7 +597,7 @@ async function applyApprovedSideEffects(request: ApprovalRequest): Promise<void>
596
597
  } catch (err: unknown) {
597
598
  log.error('approvals', 'Plugin dependency install failed after approval', {
598
599
  filename,
599
- error: err instanceof Error ? err.message : String(err),
600
+ error: errorMessage(err),
600
601
  })
601
602
  }
602
603
  }
@@ -35,8 +35,8 @@ function runWithTempDataDir(script: string) {
35
35
  describe('browser session persistence', () => {
36
36
  it('isolates browser profiles by default and stores observations', () => {
37
37
  const output = runWithTempDataDir(`
38
- const storage = (await import('./src/lib/server/storage.ts')).default
39
- const browserState = (await import('./src/lib/server/browser-state.ts')).default
38
+ const storage = (await import('./src/lib/server/storage')).default
39
+ const browserState = (await import('./src/lib/server/browser-state')).default
40
40
 
41
41
  const now = Date.now()
42
42
  storage.saveSessions({
@@ -99,7 +99,7 @@ describe('browser session persistence', () => {
99
99
 
100
100
  it('isolates subagent browser profiles by default unless sharing is explicitly requested', () => {
101
101
  const output = runWithTempDataDir(`
102
- const mod = await import('./src/lib/server/session-tools/subagent.ts')
102
+ const mod = await import('./src/lib/server/session-tools/subagent')
103
103
  const { resolveSubagentBrowserProfileId } = mod.default || mod['module.exports'] || mod
104
104
 
105
105
  const parent = {
@@ -123,8 +123,8 @@ describe('durable watch jobs', () => {
123
123
  const output = runWithTempDataDir(`
124
124
  import fs from 'node:fs'
125
125
  import path from 'node:path'
126
- const storage = (await import('./src/lib/server/storage.ts')).default
127
- const watchJobs = (await import('./src/lib/server/watch-jobs.ts')).default
126
+ const storage = (await import('./src/lib/server/storage')).default
127
+ const watchJobs = (await import('./src/lib/server/watch-jobs')).default
128
128
 
129
129
  const watchFile = path.join(process.env.DATA_DIR, 'watch.txt')
130
130
  fs.writeFileSync(watchFile, 'build succeeded')
@@ -204,10 +204,10 @@ describe('durable watch jobs', () => {
204
204
 
205
205
  it('triggers mailbox and approval waits from human-loop events', () => {
206
206
  const output = runWithTempDataDir(`
207
- const storage = (await import('./src/lib/server/storage.ts')).default
208
- const watchJobs = (await import('./src/lib/server/watch-jobs.ts')).default
209
- const mailboxMod = await import('./src/lib/server/session-mailbox.ts')
210
- const approvalsMod = await import('./src/lib/server/approvals.ts')
207
+ const storage = (await import('./src/lib/server/storage')).default
208
+ const watchJobs = (await import('./src/lib/server/watch-jobs')).default
209
+ const mailboxMod = await import('./src/lib/server/session-mailbox')
210
+ const approvalsMod = await import('./src/lib/server/approvals')
211
211
  const mailbox = mailboxMod.default || mailboxMod
212
212
  const approvals = approvalsMod.default || approvalsMod
213
213
 
@@ -282,8 +282,8 @@ describe('durable watch jobs', () => {
282
282
  describe('delegation jobs', () => {
283
283
  it('preserves cancellation and recovers stale jobs', () => {
284
284
  const output = runWithTempDataDir(`
285
- const delegationJobs = (await import('./src/lib/server/delegation-jobs.ts')).default
286
- const storage = (await import('./src/lib/server/storage.ts')).default
285
+ const delegationJobs = (await import('./src/lib/server/delegation-jobs')).default
286
+ const storage = (await import('./src/lib/server/storage')).default
287
287
 
288
288
  let cancelledCalls = 0
289
289
  const cancelledJob = delegationJobs.createDelegationJob({
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs'
2
- import path from 'path'
3
2
  import type { BrowserObservation, BrowserSessionRecord, Session } from '@/types'
4
3
  import { BROWSER_PROFILES_DIR } from './data-dir'
4
+ import { resolvePathWithinBaseDir } from './path-utils'
5
5
  import {
6
6
  deleteBrowserSession,
7
7
  loadBrowserSessions,
@@ -22,7 +22,7 @@ export function normalizeBrowserProfileId(value: unknown): string {
22
22
 
23
23
  export function getBrowserProfileDir(profileId: string): string {
24
24
  if (!fs.existsSync(BROWSER_PROFILES_DIR)) fs.mkdirSync(BROWSER_PROFILES_DIR, { recursive: true })
25
- const dir = path.join(BROWSER_PROFILES_DIR, sanitizeToken(profileId))
25
+ const dir = resolvePathWithinBaseDir(BROWSER_PROFILES_DIR, sanitizeToken(profileId))
26
26
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
27
27
  return dir
28
28
  }
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict'
2
2
  import { test } from 'node:test'
3
- import { routeTaskIntent } from './capability-router.ts'
3
+ import { routeTaskIntent } from './capability-router'
4
4
 
5
5
  test('routeTaskIntent keeps recall-style prompts as general intent', () => {
6
6
  const decision = routeTaskIntent(