@swarmclawai/swarmclaw 0.8.4 → 0.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (394) hide show
  1. package/README.md +9 -9
  2. package/bin/swarmclaw.js +5 -1
  3. package/bin/worker-cmd.js +73 -0
  4. package/package.json +2 -1
  5. package/src/app/api/agents/[id]/route.ts +17 -7
  6. package/src/app/api/agents/route.ts +21 -8
  7. package/src/app/api/approvals/route.test.ts +6 -6
  8. package/src/app/api/approvals/route.ts +2 -1
  9. package/src/app/api/auth/route.ts +2 -3
  10. package/src/app/api/chatrooms/[id]/chat/route.test.ts +299 -0
  11. package/src/app/api/chatrooms/[id]/chat/route.ts +3 -2
  12. package/src/app/api/chatrooms/[id]/route.ts +7 -6
  13. package/src/app/api/chats/[id]/chat/route.test.ts +496 -0
  14. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  15. package/src/app/api/chats/[id]/clear/route.ts +9 -9
  16. package/src/app/api/chats/[id]/devserver/route.ts +2 -1
  17. package/src/app/api/chats/[id]/edit-resend/route.ts +3 -4
  18. package/src/app/api/chats/[id]/fork/route.ts +3 -5
  19. package/src/app/api/chats/[id]/restore/route.ts +6 -7
  20. package/src/app/api/chats/[id]/retry/route.ts +3 -4
  21. package/src/app/api/chats/[id]/route.ts +61 -62
  22. package/src/app/api/chats/route.ts +7 -1
  23. package/src/app/api/connectors/[id]/route.ts +7 -8
  24. package/src/app/api/connectors/route.ts +5 -4
  25. package/src/app/api/eval/run/route.ts +2 -1
  26. package/src/app/api/eval/suite/route.ts +2 -1
  27. package/src/app/api/external-agents/route.test.ts +1 -1
  28. package/src/app/api/external-agents/route.ts +2 -2
  29. package/src/app/api/files/serve/route.ts +1 -1
  30. package/src/app/api/gateways/[id]/route.ts +7 -5
  31. package/src/app/api/gateways/route.ts +1 -1
  32. package/src/app/api/knowledge/upload/route.ts +1 -1
  33. package/src/app/api/logs/route.ts +5 -7
  34. package/src/app/api/memory-images/[filename]/route.ts +2 -3
  35. package/src/app/api/openclaw/agent-files/route.ts +4 -3
  36. package/src/app/api/openclaw/approvals/route.ts +3 -4
  37. package/src/app/api/openclaw/config-sync/route.ts +3 -2
  38. package/src/app/api/openclaw/cron/route.ts +3 -2
  39. package/src/app/api/openclaw/dotenv-keys/route.ts +2 -1
  40. package/src/app/api/openclaw/exec-config/route.ts +3 -2
  41. package/src/app/api/openclaw/gateway/route.ts +5 -4
  42. package/src/app/api/openclaw/history/route.ts +3 -2
  43. package/src/app/api/openclaw/media/route.ts +2 -1
  44. package/src/app/api/openclaw/permissions/route.ts +3 -2
  45. package/src/app/api/openclaw/sandbox-env/route.ts +3 -2
  46. package/src/app/api/openclaw/skills/install/route.ts +2 -1
  47. package/src/app/api/openclaw/skills/remove/route.ts +2 -1
  48. package/src/app/api/openclaw/skills/route.ts +3 -2
  49. package/src/app/api/orchestrator/run/route.ts +5 -14
  50. package/src/app/api/perf/route.ts +43 -0
  51. package/src/app/api/plugins/dependencies/route.ts +2 -1
  52. package/src/app/api/plugins/install/route.ts +2 -1
  53. package/src/app/api/plugins/marketplace/route.ts +3 -2
  54. package/src/app/api/plugins/settings/route.ts +2 -1
  55. package/src/app/api/preview-server/route.ts +11 -10
  56. package/src/app/api/projects/[id]/route.ts +1 -1
  57. package/src/app/api/schedules/[id]/route.test.ts +128 -0
  58. package/src/app/api/schedules/[id]/route.ts +43 -43
  59. package/src/app/api/schedules/[id]/run/route.ts +11 -62
  60. package/src/app/api/schedules/route.ts +21 -87
  61. package/src/app/api/settings/route.ts +2 -0
  62. package/src/app/api/setup/doctor/route.ts +9 -8
  63. package/src/app/api/tasks/[id]/approve/route.ts +33 -30
  64. package/src/app/api/tasks/[id]/route.ts +12 -35
  65. package/src/app/api/tasks/import/github/route.ts +2 -1
  66. package/src/app/api/tasks/route.ts +79 -91
  67. package/src/app/api/wallets/[id]/approve/route.ts +2 -1
  68. package/src/app/api/wallets/[id]/route.ts +13 -19
  69. package/src/app/api/wallets/[id]/send/route.ts +2 -1
  70. package/src/app/api/wallets/route.ts +2 -1
  71. package/src/app/api/webhooks/[id]/route.ts +2 -1
  72. package/src/app/api/webhooks/route.test.ts +3 -1
  73. package/src/app/page.tsx +23 -331
  74. package/src/cli/index.js +19 -0
  75. package/src/cli/index.ts +38 -7
  76. package/src/cli/spec.js +9 -0
  77. package/src/components/activity/activity-feed.tsx +7 -4
  78. package/src/components/agents/agent-card.tsx +32 -6
  79. package/src/components/agents/agent-chat-list.tsx +55 -22
  80. package/src/components/agents/agent-files-editor.tsx +3 -2
  81. package/src/components/agents/agent-sheet.tsx +123 -22
  82. package/src/components/agents/inspector-panel.tsx +1 -1
  83. package/src/components/agents/openclaw-skills-panel.tsx +2 -1
  84. package/src/components/agents/trash-list.tsx +1 -1
  85. package/src/components/auth/access-key-gate.tsx +8 -2
  86. package/src/components/auth/setup-wizard.tsx +10 -9
  87. package/src/components/auth/user-picker.tsx +3 -2
  88. package/src/components/chat/chat-area.tsx +20 -1
  89. package/src/components/chat/chat-card.tsx +18 -3
  90. package/src/components/chat/chat-header.tsx +24 -4
  91. package/src/components/chat/chat-list.tsx +2 -11
  92. package/src/components/chat/heartbeat-history-panel.tsx +2 -1
  93. package/src/components/chat/message-bubble.tsx +45 -6
  94. package/src/components/chat/message-list.tsx +280 -145
  95. package/src/components/chat/streaming-bubble.tsx +217 -60
  96. package/src/components/chat/swarm-panel.test.ts +274 -0
  97. package/src/components/chat/swarm-panel.tsx +410 -0
  98. package/src/components/chat/swarm-status-card.tsx +346 -0
  99. package/src/components/chat/tool-call-bubble.tsx +48 -23
  100. package/src/components/chatrooms/chatroom-list.tsx +8 -5
  101. package/src/components/chatrooms/chatroom-message.tsx +10 -7
  102. package/src/components/chatrooms/chatroom-view.tsx +12 -9
  103. package/src/components/connectors/connector-health.tsx +6 -4
  104. package/src/components/connectors/connector-list.tsx +16 -11
  105. package/src/components/connectors/connector-sheet.tsx +12 -6
  106. package/src/components/home/home-view.tsx +38 -24
  107. package/src/components/input/chat-input.tsx +10 -1
  108. package/src/components/layout/app-layout.tsx +2 -38
  109. package/src/components/layout/sheet-layer.tsx +50 -0
  110. package/src/components/mcp-servers/mcp-server-list.tsx +37 -5
  111. package/src/components/mcp-servers/mcp-server-sheet.tsx +12 -2
  112. package/src/components/plugins/plugin-list.tsx +8 -4
  113. package/src/components/plugins/plugin-sheet.tsx +2 -1
  114. package/src/components/providers/provider-list.tsx +3 -2
  115. package/src/components/providers/provider-sheet.tsx +2 -1
  116. package/src/components/runs/run-list.tsx +11 -7
  117. package/src/components/schedules/schedule-card.tsx +5 -3
  118. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  119. package/src/components/shared/attachment-chip.tsx +19 -3
  120. package/src/components/shared/notification-center.tsx +6 -3
  121. package/src/components/shared/settings/plugin-manager.tsx +3 -2
  122. package/src/components/shared/settings/section-embedding.tsx +2 -1
  123. package/src/components/shared/settings/section-orchestrator.tsx +2 -1
  124. package/src/components/shared/settings/section-user-preferences.tsx +107 -0
  125. package/src/components/shared/settings/settings-page.tsx +13 -9
  126. package/src/components/skills/clawhub-browser.tsx +15 -4
  127. package/src/components/skills/skill-list.tsx +15 -4
  128. package/src/components/tasks/approvals-panel.tsx +2 -1
  129. package/src/components/tasks/task-board.tsx +35 -37
  130. package/src/components/tasks/task-sheet.tsx +4 -3
  131. package/src/components/ui/full-screen-loader.tsx +164 -0
  132. package/src/components/wallets/wallet-approval-dialog.tsx +2 -1
  133. package/src/components/wallets/wallet-panel.tsx +6 -5
  134. package/src/components/wallets/wallet-section.tsx +3 -2
  135. package/src/components/webhooks/webhook-list.tsx +4 -5
  136. package/src/components/webhooks/webhook-sheet.tsx +6 -6
  137. package/src/hooks/use-app-bootstrap.ts +202 -0
  138. package/src/hooks/use-mounted-ref.ts +14 -0
  139. package/src/hooks/use-now.ts +31 -0
  140. package/src/hooks/use-openclaw-gateway.ts +2 -1
  141. package/src/instrumentation.ts +20 -8
  142. package/src/lib/agent-default-tools.test.ts +52 -0
  143. package/src/lib/agent-default-tools.ts +40 -0
  144. package/src/lib/api-client.test.ts +21 -0
  145. package/src/lib/api-client.ts +6 -11
  146. package/src/lib/canvas-content.test.ts +360 -0
  147. package/src/lib/chat-streaming-state.test.ts +49 -2
  148. package/src/lib/chat-streaming-state.ts +26 -10
  149. package/src/lib/fetch-timeout.test.ts +54 -0
  150. package/src/lib/fetch-timeout.ts +60 -3
  151. package/src/lib/live-tool-events.test.ts +77 -0
  152. package/src/lib/live-tool-events.ts +73 -0
  153. package/src/lib/local-observability.test.ts +2 -2
  154. package/src/lib/openclaw-endpoint.test.ts +1 -1
  155. package/src/lib/providers/anthropic.ts +12 -16
  156. package/src/lib/providers/index.ts +4 -2
  157. package/src/lib/providers/ollama.ts +9 -6
  158. package/src/lib/providers/openai.ts +11 -14
  159. package/src/lib/runtime-env.test.ts +8 -8
  160. package/src/lib/schedule-dedupe-advanced.test.ts +2 -2
  161. package/src/lib/schedule-dedupe.test.ts +1 -1
  162. package/src/lib/schedule-dedupe.ts +3 -2
  163. package/src/lib/server/agent-thread-session.test.ts +6 -6
  164. package/src/lib/server/agent-thread-session.ts +6 -9
  165. package/src/lib/server/alert-dispatch.ts +2 -1
  166. package/src/lib/server/api-routes.test.ts +6 -6
  167. package/src/lib/server/approval-connector-notify.test.ts +4 -4
  168. package/src/lib/server/approvals-auto-approve.test.ts +29 -29
  169. package/src/lib/server/approvals.test.ts +317 -0
  170. package/src/lib/server/approvals.ts +5 -4
  171. package/src/lib/server/autonomy-runtime.test.ts +11 -11
  172. package/src/lib/server/browser-state.ts +2 -2
  173. package/src/lib/server/capability-router.test.ts +1 -1
  174. package/src/lib/server/capability-router.ts +3 -2
  175. package/src/lib/server/chat-execution-advanced.test.ts +15 -2
  176. package/src/lib/server/chat-execution-connector-delivery.ts +67 -0
  177. package/src/lib/server/chat-execution-disabled.test.ts +3 -3
  178. package/src/lib/server/chat-execution-eval-history.test.ts +3 -3
  179. package/src/lib/server/chat-execution-heartbeat.test.ts +42 -1
  180. package/src/lib/server/chat-execution-session-sync.test.ts +119 -0
  181. package/src/lib/server/chat-execution-tool-events.ts +116 -0
  182. package/src/lib/server/chat-execution-utils.test.ts +479 -0
  183. package/src/lib/server/chat-execution-utils.ts +533 -0
  184. package/src/lib/server/chat-execution.ts +153 -748
  185. package/src/lib/server/chat-streaming-utils.ts +174 -0
  186. package/src/lib/server/chat-turn-tool-routing.ts +310 -0
  187. package/src/lib/server/chatroom-session-persistence.test.ts +2 -2
  188. package/src/lib/server/clawhub-client.ts +2 -1
  189. package/src/lib/server/collection-helpers.test.ts +92 -0
  190. package/src/lib/server/collection-helpers.ts +25 -3
  191. package/src/lib/server/connectors/access.ts +146 -0
  192. package/src/lib/server/connectors/bluebubbles.test.ts +1 -1
  193. package/src/lib/server/connectors/bluebubbles.ts +4 -4
  194. package/src/lib/server/connectors/commands.ts +367 -0
  195. package/src/lib/server/connectors/connector-routing.test.ts +4 -4
  196. package/src/lib/server/connectors/delivery.ts +142 -0
  197. package/src/lib/server/connectors/discord.ts +37 -40
  198. package/src/lib/server/connectors/email.ts +11 -10
  199. package/src/lib/server/connectors/googlechat.ts +4 -4
  200. package/src/lib/server/connectors/inbound-audio-transcription.ts +2 -1
  201. package/src/lib/server/connectors/ingress-delivery.ts +23 -0
  202. package/src/lib/server/connectors/manager-roundtrip.test.ts +300 -0
  203. package/src/lib/server/connectors/manager.test.ts +352 -77
  204. package/src/lib/server/connectors/manager.ts +134 -673
  205. package/src/lib/server/connectors/matrix.ts +4 -4
  206. package/src/lib/server/connectors/message-sentinel.ts +7 -0
  207. package/src/lib/server/connectors/openclaw.test.ts +1 -1
  208. package/src/lib/server/connectors/openclaw.ts +8 -10
  209. package/src/lib/server/connectors/outbox.test.ts +192 -0
  210. package/src/lib/server/connectors/outbox.ts +369 -0
  211. package/src/lib/server/connectors/pairing.test.ts +18 -1
  212. package/src/lib/server/connectors/pairing.ts +49 -4
  213. package/src/lib/server/connectors/policy.ts +9 -3
  214. package/src/lib/server/connectors/reconnect-state.ts +71 -0
  215. package/src/lib/server/connectors/response-media.ts +256 -0
  216. package/src/lib/server/connectors/runtime-state.ts +67 -0
  217. package/src/lib/server/connectors/session.test.ts +357 -0
  218. package/src/lib/server/connectors/session.ts +422 -0
  219. package/src/lib/server/connectors/signal.ts +7 -7
  220. package/src/lib/server/connectors/slack.ts +43 -43
  221. package/src/lib/server/connectors/teams.ts +4 -4
  222. package/src/lib/server/connectors/telegram.ts +37 -43
  223. package/src/lib/server/connectors/types.ts +31 -1
  224. package/src/lib/server/connectors/whatsapp.test.ts +108 -0
  225. package/src/lib/server/connectors/whatsapp.ts +106 -34
  226. package/src/lib/server/context-manager.test.ts +409 -0
  227. package/src/lib/server/cost.test.ts +1 -1
  228. package/src/lib/server/daemon-policy.ts +78 -0
  229. package/src/lib/server/daemon-state-connectors.test.ts +167 -0
  230. package/src/lib/server/daemon-state.test.ts +283 -55
  231. package/src/lib/server/daemon-state.ts +106 -109
  232. package/src/lib/server/data-dir.test.ts +5 -5
  233. package/src/lib/server/data-dir.ts +4 -0
  234. package/src/lib/server/delegation-jobs-advanced.test.ts +1 -1
  235. package/src/lib/server/delegation-jobs.test.ts +87 -0
  236. package/src/lib/server/delegation-jobs.ts +42 -48
  237. package/src/lib/server/devserver-launch.ts +1 -1
  238. package/src/lib/server/document-utils.ts +7 -9
  239. package/src/lib/server/elevenlabs.ts +2 -1
  240. package/src/lib/server/embeddings.test.ts +105 -0
  241. package/src/lib/server/ethereum.ts +3 -2
  242. package/src/lib/server/eval/agent-regression.ts +3 -2
  243. package/src/lib/server/eval/runner.ts +2 -1
  244. package/src/lib/server/eval/scorer.ts +2 -1
  245. package/src/lib/server/evm-swap.ts +2 -1
  246. package/src/lib/server/gateway/protocol.test.ts +1 -1
  247. package/src/lib/server/guardian.ts +2 -1
  248. package/src/lib/server/heartbeat-blocked-suppression.test.ts +151 -0
  249. package/src/lib/server/heartbeat-service-timer.test.ts +6 -6
  250. package/src/lib/server/heartbeat-service.test.ts +406 -0
  251. package/src/lib/server/heartbeat-service.ts +54 -7
  252. package/src/lib/server/heartbeat-wake.test.ts +19 -0
  253. package/src/lib/server/heartbeat-wake.ts +17 -16
  254. package/src/lib/server/integrity-monitor.test.ts +149 -0
  255. package/src/lib/server/json-utils.ts +22 -0
  256. package/src/lib/server/knowledge-db.test.ts +13 -13
  257. package/src/lib/server/link-understanding.ts +2 -1
  258. package/src/lib/server/llm-response-cache.test.ts +1 -1
  259. package/src/lib/server/main-agent-loop-advanced.test.ts +65 -3
  260. package/src/lib/server/main-agent-loop.test.ts +6 -6
  261. package/src/lib/server/main-agent-loop.ts +21 -7
  262. package/src/lib/server/mcp-client.test.ts +1 -1
  263. package/src/lib/server/mcp-conformance.test.ts +1 -1
  264. package/src/lib/server/mcp-conformance.ts +3 -2
  265. package/src/lib/server/memory-consolidation.ts +2 -1
  266. package/src/lib/server/memory-db.test.ts +485 -0
  267. package/src/lib/server/memory-db.ts +39 -26
  268. package/src/lib/server/memory-graph.test.ts +2 -2
  269. package/src/lib/server/memory-policy.test.ts +7 -7
  270. package/src/lib/server/memory-retrieval.test.ts +1 -1
  271. package/src/lib/server/openclaw-config-sync.ts +2 -1
  272. package/src/lib/server/openclaw-deploy.test.ts +1 -1
  273. package/src/lib/server/openclaw-deploy.ts +8 -12
  274. package/src/lib/server/openclaw-exec-config.ts +2 -1
  275. package/src/lib/server/openclaw-gateway.ts +6 -7
  276. package/src/lib/server/openclaw-skills-normalize.ts +2 -1
  277. package/src/lib/server/openclaw-sync.ts +7 -5
  278. package/src/lib/server/orchestrator-lg-structure.test.ts +17 -0
  279. package/src/lib/server/orchestrator-lg.ts +199 -327
  280. package/src/lib/server/path-utils.ts +31 -0
  281. package/src/lib/server/perf.ts +161 -0
  282. package/src/lib/server/plugins-approval-guidance.ts +115 -0
  283. package/src/lib/server/plugins.test.ts +1 -1
  284. package/src/lib/server/plugins.ts +22 -132
  285. package/src/lib/server/process-manager.ts +5 -8
  286. package/src/lib/server/provider-health.test.ts +137 -0
  287. package/src/lib/server/provider-health.ts +3 -3
  288. package/src/lib/server/provider-model-discovery.ts +3 -12
  289. package/src/lib/server/queue-followups.test.ts +9 -9
  290. package/src/lib/server/queue-reconcile.test.ts +2 -2
  291. package/src/lib/server/queue-recovery.test.ts +269 -0
  292. package/src/lib/server/queue.test.ts +570 -0
  293. package/src/lib/server/queue.ts +62 -455
  294. package/src/lib/server/resolve-image.ts +30 -0
  295. package/src/lib/server/runtime-settings.test.ts +4 -4
  296. package/src/lib/server/runtime-storage-write-paths.test.ts +60 -0
  297. package/src/lib/server/schedule-normalization.test.ts +279 -0
  298. package/src/lib/server/schedule-service.ts +263 -0
  299. package/src/lib/server/scheduler.ts +17 -74
  300. package/src/lib/server/session-mailbox.test.ts +191 -0
  301. package/src/lib/server/session-run-manager.test.ts +640 -0
  302. package/src/lib/server/session-run-manager.ts +59 -15
  303. package/src/lib/server/session-tools/autonomy-tools.test.ts +20 -20
  304. package/src/lib/server/session-tools/calendar.ts +2 -1
  305. package/src/lib/server/session-tools/canvas.ts +2 -1
  306. package/src/lib/server/session-tools/chatroom.ts +2 -1
  307. package/src/lib/server/session-tools/connector.ts +26 -28
  308. package/src/lib/server/session-tools/context-mgmt.ts +3 -2
  309. package/src/lib/server/session-tools/crawl.ts +4 -3
  310. package/src/lib/server/session-tools/crud.ts +105 -324
  311. package/src/lib/server/session-tools/delegate-fallback.test.ts +9 -9
  312. package/src/lib/server/session-tools/delegate.ts +6 -8
  313. package/src/lib/server/session-tools/discovery-approvals.test.ts +15 -15
  314. package/src/lib/server/session-tools/discovery.ts +4 -3
  315. package/src/lib/server/session-tools/document.ts +2 -1
  316. package/src/lib/server/session-tools/email.ts +2 -1
  317. package/src/lib/server/session-tools/extract.ts +2 -1
  318. package/src/lib/server/session-tools/file.ts +4 -3
  319. package/src/lib/server/session-tools/http.ts +2 -1
  320. package/src/lib/server/session-tools/human-loop.ts +2 -1
  321. package/src/lib/server/session-tools/image-gen.ts +4 -3
  322. package/src/lib/server/session-tools/index.ts +26 -30
  323. package/src/lib/server/session-tools/mailbox.ts +2 -1
  324. package/src/lib/server/session-tools/manage-connectors.test.ts +4 -4
  325. package/src/lib/server/session-tools/manage-schedules.test.ts +12 -12
  326. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +5 -5
  327. package/src/lib/server/session-tools/manage-tasks.test.ts +2 -2
  328. package/src/lib/server/session-tools/monitor.ts +2 -1
  329. package/src/lib/server/session-tools/platform.ts +2 -1
  330. package/src/lib/server/session-tools/plugin-creator.ts +2 -1
  331. package/src/lib/server/session-tools/replicate.ts +3 -2
  332. package/src/lib/server/session-tools/session-tools-wiring.test.ts +6 -6
  333. package/src/lib/server/session-tools/shell.ts +4 -9
  334. package/src/lib/server/session-tools/subagent.ts +322 -170
  335. package/src/lib/server/session-tools/table.ts +6 -5
  336. package/src/lib/server/session-tools/wallet-tool.test.ts +3 -3
  337. package/src/lib/server/session-tools/wallet.ts +7 -6
  338. package/src/lib/server/session-tools/web-browser-config.test.ts +1 -0
  339. package/src/lib/server/session-tools/web-utils.ts +317 -0
  340. package/src/lib/server/session-tools/web.ts +62 -328
  341. package/src/lib/server/skill-prompt-budget.test.ts +1 -1
  342. package/src/lib/server/skills-normalize.ts +2 -1
  343. package/src/lib/server/storage-item-access.test.ts +302 -0
  344. package/src/lib/server/storage.ts +366 -314
  345. package/src/lib/server/stream-agent-chat.test.ts +82 -3
  346. package/src/lib/server/stream-agent-chat.ts +146 -510
  347. package/src/lib/server/stream-continuation.ts +412 -0
  348. package/src/lib/server/subagent-lineage.test.ts +647 -0
  349. package/src/lib/server/subagent-lineage.ts +435 -0
  350. package/src/lib/server/subagent-runtime.test.ts +484 -0
  351. package/src/lib/server/subagent-runtime.ts +419 -0
  352. package/src/lib/server/subagent-swarm.test.ts +391 -0
  353. package/src/lib/server/subagent-swarm.ts +564 -0
  354. package/src/lib/server/system-events.ts +3 -3
  355. package/src/lib/server/task-followups.test.ts +491 -0
  356. package/src/lib/server/task-followups.ts +391 -0
  357. package/src/lib/server/task-lifecycle.test.ts +205 -0
  358. package/src/lib/server/task-lifecycle.ts +200 -0
  359. package/src/lib/server/task-quality-gate.test.ts +1 -1
  360. package/src/lib/server/task-resume.ts +208 -0
  361. package/src/lib/server/task-service.test.ts +108 -0
  362. package/src/lib/server/task-service.ts +264 -0
  363. package/src/lib/server/task-validation.test.ts +1 -1
  364. package/src/lib/server/test-utils/run-with-temp-data-dir.ts +42 -0
  365. package/src/lib/server/tool-capability-policy.test.ts +2 -2
  366. package/src/lib/server/tool-capability-policy.ts +3 -2
  367. package/src/lib/server/tool-planning.ts +2 -1
  368. package/src/lib/server/tool-retry.ts +2 -3
  369. package/src/lib/server/wake-dispatcher.test.ts +303 -0
  370. package/src/lib/server/wake-dispatcher.ts +318 -0
  371. package/src/lib/server/wake-mode.test.ts +161 -0
  372. package/src/lib/server/wake-mode.ts +174 -0
  373. package/src/lib/server/wallet-service.ts +8 -9
  374. package/src/lib/server/watch-jobs.ts +2 -1
  375. package/src/lib/server/workspace-context.ts +2 -2
  376. package/src/lib/shared-utils.test.ts +142 -0
  377. package/src/lib/shared-utils.ts +62 -0
  378. package/src/lib/tool-event-summary.ts +2 -1
  379. package/src/lib/view-routes.test.ts +100 -0
  380. package/src/lib/wallet.test.ts +322 -6
  381. package/src/proxy.test.ts +4 -4
  382. package/src/proxy.ts +2 -3
  383. package/src/stores/set-if-changed.ts +40 -0
  384. package/src/stores/slices/agent-slice.ts +111 -0
  385. package/src/stores/slices/auth-slice.ts +25 -0
  386. package/src/stores/slices/data-slice.ts +301 -0
  387. package/src/stores/slices/index.ts +7 -0
  388. package/src/stores/slices/session-slice.ts +112 -0
  389. package/src/stores/slices/task-slice.ts +63 -0
  390. package/src/stores/slices/ui-slice.ts +192 -0
  391. package/src/stores/use-app-store.ts +17 -822
  392. package/src/stores/use-approval-store.ts +2 -1
  393. package/src/stores/use-chat-store.ts +8 -1
  394. package/src/types/index.ts +10 -0
@@ -0,0 +1,54 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { fetchWithTimeout } from './fetch-timeout'
5
+
6
+ const originalFetch = global.fetch
7
+ const originalSetTimeout = global.setTimeout
8
+ const originalClearTimeout = global.clearTimeout
9
+
10
+ test.afterEach(() => {
11
+ global.fetch = originalFetch
12
+ global.setTimeout = originalSetTimeout
13
+ global.clearTimeout = originalClearTimeout
14
+ })
15
+
16
+ test('fetchWithTimeout throws TimeoutError with a clear message on timeout', async () => {
17
+ global.setTimeout = (((callback: (...args: unknown[]) => void) => {
18
+ queueMicrotask(() => callback())
19
+ return 1 as unknown as ReturnType<typeof setTimeout>
20
+ }) as typeof setTimeout)
21
+ global.clearTimeout = (() => {}) as typeof clearTimeout
22
+ global.fetch = (((_input: RequestInfo | URL, init?: RequestInit) => new Promise<Response>((_resolve, reject) => {
23
+ const onAbort = () => reject(init?.signal?.reason ?? new DOMException('Aborted', 'AbortError'))
24
+ if (init?.signal?.aborted) onAbort()
25
+ else init?.signal?.addEventListener('abort', onAbort, { once: true })
26
+ })) as typeof fetch)
27
+
28
+ await assert.rejects(
29
+ () => fetchWithTimeout('/slow', {}, 5_000),
30
+ (err: unknown) => {
31
+ assert.ok(err instanceof Error)
32
+ assert.equal(err.name, 'TimeoutError')
33
+ assert.match(err.message, /5000ms/)
34
+ return true
35
+ },
36
+ )
37
+ })
38
+
39
+ test('fetchWithTimeout preserves caller abort signals', async () => {
40
+ const controller = new AbortController()
41
+ const expectedError = new DOMException('Manual cancel', 'AbortError')
42
+ controller.abort(expectedError)
43
+ global.fetch = (((_input: RequestInfo | URL, init?: RequestInit) => {
44
+ return Promise.reject(init?.signal?.reason ?? expectedError)
45
+ }) as typeof fetch)
46
+
47
+ await assert.rejects(
48
+ () => fetchWithTimeout('/aborted', { signal: controller.signal }, 5_000),
49
+ (err: unknown) => {
50
+ assert.strictEqual(err, expectedError)
51
+ return true
52
+ },
53
+ )
54
+ })
@@ -1,16 +1,73 @@
1
1
  const MIN_TIMEOUT_MS = 1_000
2
2
 
3
+ function createTimeoutError(timeoutMs: number): Error {
4
+ const error = new Error(`Request timed out after ${timeoutMs}ms`)
5
+ error.name = 'TimeoutError'
6
+ return error
7
+ }
8
+
9
+ function abortWithReason(controller: AbortController, reason: unknown): void {
10
+ try {
11
+ controller.abort(reason)
12
+ } catch {
13
+ controller.abort()
14
+ }
15
+ }
16
+
17
+ function combineAbortSignals(signals: AbortSignal[]): AbortSignal {
18
+ if (signals.length === 1) return signals[0]
19
+ if (typeof AbortSignal.any === 'function') return AbortSignal.any(signals)
20
+
21
+ const controller = new AbortController()
22
+ const listeners = new Map<AbortSignal, () => void>()
23
+ const abortFrom = (signal: AbortSignal) => {
24
+ for (const [candidate, listener] of listeners.entries()) {
25
+ candidate.removeEventListener('abort', listener)
26
+ }
27
+ abortWithReason(controller, signal.reason)
28
+ }
29
+
30
+ for (const signal of signals) {
31
+ if (signal.aborted) {
32
+ abortFrom(signal)
33
+ break
34
+ }
35
+ const listener = () => abortFrom(signal)
36
+ listeners.set(signal, listener)
37
+ signal.addEventListener('abort', listener, { once: true })
38
+ }
39
+
40
+ return controller.signal
41
+ }
42
+
43
+ export function isAbortError(err: unknown): boolean {
44
+ return Boolean(err) && typeof err === 'object' && (err as { name?: string }).name === 'AbortError'
45
+ }
46
+
47
+ export function isTimeoutError(err: unknown): boolean {
48
+ return Boolean(err) && typeof err === 'object' && (err as { name?: string }).name === 'TimeoutError'
49
+ }
50
+
3
51
  export async function fetchWithTimeout(
4
52
  input: RequestInfo | URL,
5
53
  init: RequestInit = {},
6
54
  timeoutMs: number,
7
55
  ): Promise<Response> {
8
56
  const boundedTimeout = Math.max(MIN_TIMEOUT_MS, Math.trunc(timeoutMs))
9
- const controller = new AbortController()
10
- const timer = setTimeout(() => controller.abort(), boundedTimeout)
57
+ const timeoutController = new AbortController()
58
+ const timeoutError = createTimeoutError(boundedTimeout)
59
+ const signal = init.signal
60
+ ? combineAbortSignals([init.signal, timeoutController.signal])
61
+ : timeoutController.signal
62
+ const timer = setTimeout(() => abortWithReason(timeoutController, timeoutError), boundedTimeout)
11
63
 
12
64
  try {
13
- return await fetch(input, { ...init, signal: controller.signal })
65
+ return await fetch(input, { ...init, signal })
66
+ } catch (err) {
67
+ if (timeoutController.signal.aborted && isTimeoutError(timeoutController.signal.reason)) {
68
+ throw timeoutController.signal.reason
69
+ }
70
+ throw err
14
71
  } finally {
15
72
  clearTimeout(timer)
16
73
  }
@@ -0,0 +1,77 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { applyStreamingToolCall, applyStreamingToolResult, isLikelyToolErrorOutput } from './live-tool-events'
5
+
6
+ describe('live tool events', () => {
7
+ it('dedupes consecutive identical pending tool calls', () => {
8
+ const first = applyStreamingToolCall([], {
9
+ toolName: 'browser',
10
+ toolInput: '{"action":"navigate","url":"https://example.com"}',
11
+ toolCallId: 'call-1',
12
+ }, 'fallback-1')
13
+
14
+ const second = applyStreamingToolCall(first, {
15
+ toolName: 'browser',
16
+ toolInput: '{"action":"navigate","url":"https://example.com"}',
17
+ toolCallId: 'call-1',
18
+ }, 'fallback-2')
19
+
20
+ assert.equal(second.length, 1)
21
+ assert.equal(second[0]?.id, 'call-1')
22
+ })
23
+
24
+ it('matches parallel same-tool results by toolCallId', () => {
25
+ const withCalls = applyStreamingToolCall(
26
+ applyStreamingToolCall([], {
27
+ toolName: 'read_file',
28
+ toolInput: '{"path":"a.txt"}',
29
+ toolCallId: 'call-a',
30
+ }, 'fallback-a'),
31
+ {
32
+ toolName: 'read_file',
33
+ toolInput: '{"path":"b.txt"}',
34
+ toolCallId: 'call-b',
35
+ },
36
+ 'fallback-b',
37
+ )
38
+
39
+ const withFirstResult = applyStreamingToolResult(withCalls, {
40
+ toolName: 'read_file',
41
+ toolOutput: 'contents of b',
42
+ toolCallId: 'call-b',
43
+ })
44
+ const withBothResults = applyStreamingToolResult(withFirstResult, {
45
+ toolName: 'read_file',
46
+ toolOutput: 'contents of a',
47
+ toolCallId: 'call-a',
48
+ })
49
+
50
+ assert.equal(withBothResults[0]?.output, 'contents of a')
51
+ assert.equal(withBothResults[0]?.status, 'done')
52
+ assert.equal(withBothResults[1]?.output, 'contents of b')
53
+ assert.equal(withBothResults[1]?.status, 'done')
54
+ })
55
+
56
+ it('uses fallback matching when toolCallId is missing', () => {
57
+ const withCall = applyStreamingToolCall([], {
58
+ toolName: 'browser',
59
+ toolInput: '{"action":"navigate","url":"https://example.com"}',
60
+ }, 'fallback-nav')
61
+
62
+ const withResult = applyStreamingToolResult(withCall, {
63
+ toolName: 'browser',
64
+ toolOutput: 'navigated successfully',
65
+ })
66
+
67
+ assert.equal(withResult[0]?.id, 'fallback-nav')
68
+ assert.equal(withResult[0]?.status, 'done')
69
+ assert.equal(withResult[0]?.output, 'navigated successfully')
70
+ })
71
+
72
+ it('detects common tool error outputs', () => {
73
+ assert.equal(isLikelyToolErrorOutput('Error: command failed'), true)
74
+ assert.equal(isLikelyToolErrorOutput('timeout waiting for response'), true)
75
+ assert.equal(isLikelyToolErrorOutput('File written successfully'), false)
76
+ })
77
+ })
@@ -0,0 +1,73 @@
1
+ import type { SSEEvent } from '@/types'
2
+
3
+ export interface StreamingToolEvent {
4
+ id: string
5
+ name: string
6
+ input: string
7
+ output?: string
8
+ status: 'running' | 'done' | 'error'
9
+ toolCallId?: string
10
+ }
11
+
12
+ export function isLikelyToolErrorOutput(output: string): boolean {
13
+ const trimmed = output.trim()
14
+ return /^(Error:|error:|ECONNREFUSED|ETIMEDOUT|timeout|failed)/i.test(trimmed)
15
+ || output.includes('ECONNREFUSED')
16
+ || output.includes('ETIMEDOUT')
17
+ || output.includes('Error:')
18
+ }
19
+
20
+ export function applyStreamingToolCall(
21
+ events: StreamingToolEvent[],
22
+ ev: Pick<SSEEvent, 'toolName' | 'toolInput' | 'toolCallId'>,
23
+ fallbackId: string,
24
+ ): StreamingToolEvent[] {
25
+ const previous = events[events.length - 1]
26
+ const name = ev.toolName || 'unknown'
27
+ const input = ev.toolInput || ''
28
+
29
+ if (
30
+ previous
31
+ && previous.name === name
32
+ && previous.input === input
33
+ && previous.status === 'running'
34
+ && previous.toolCallId === (ev.toolCallId || previous.toolCallId)
35
+ ) {
36
+ return events
37
+ }
38
+
39
+ return [...events, {
40
+ id: ev.toolCallId || fallbackId,
41
+ name,
42
+ input,
43
+ status: 'running',
44
+ toolCallId: ev.toolCallId,
45
+ }]
46
+ }
47
+
48
+ export function applyStreamingToolResult(
49
+ events: StreamingToolEvent[],
50
+ ev: Pick<SSEEvent, 'toolName' | 'toolOutput' | 'toolCallId'>,
51
+ ): StreamingToolEvent[] {
52
+ const index = ev.toolCallId
53
+ ? events.findLastIndex((entry) => entry.toolCallId === ev.toolCallId && entry.status === 'running')
54
+ : events.findLastIndex((entry) => entry.name === (ev.toolName || 'unknown') && entry.status === 'running')
55
+
56
+ if (index === -1) return events
57
+
58
+ const output = ev.toolOutput || ''
59
+ const nextStatus = isLikelyToolErrorOutput(output) ? 'error' : 'done'
60
+ const current = events[index]
61
+
62
+ if (current.output === output && current.status === nextStatus) {
63
+ return events
64
+ }
65
+
66
+ const next = [...events]
67
+ next[index] = {
68
+ ...current,
69
+ output,
70
+ status: nextStatus,
71
+ }
72
+ return next
73
+ }
@@ -64,10 +64,10 @@ describe('local observability', () => {
64
64
  })
65
65
 
66
66
  it('detects localhost browser hosts', () => {
67
- globalThis.window = { location: { hostname: 'localhost' } } as Window
67
+ globalThis.window = { location: { hostname: "localhost" } } as any
68
68
  assert.equal(isLocalhostBrowser(), true)
69
69
 
70
- globalThis.window = { location: { hostname: 'swarmclaw.ai' } } as Window
70
+ globalThis.window = { location: { hostname: "swarmclaw.ai" } } as any
71
71
  assert.equal(isLocalhostBrowser(), false)
72
72
  })
73
73
  })
@@ -5,7 +5,7 @@ import {
5
5
  isLocalOpenClawEndpoint,
6
6
  normalizeOpenClawEndpoint,
7
7
  normalizeProviderEndpoint,
8
- } from './openclaw-endpoint.ts'
8
+ } from './openclaw-endpoint'
9
9
 
10
10
  test('normalizeOpenClawEndpoint handles ws/http/path variants', () => {
11
11
  assert.equal(
@@ -2,6 +2,7 @@ import fs from 'fs'
2
2
  import https from 'https'
3
3
  import type { StreamChatOptions } from './index'
4
4
  import { PROVIDER_DEFAULTS } from './provider-defaults'
5
+ import { resolveImagePath } from '@/lib/server/resolve-image'
5
6
 
6
7
  const IMAGE_EXTS = /\.(png|jpg|jpeg|gif|webp|bmp)$/i
7
8
  const TEXT_EXTS = /\.(txt|md|csv|json|xml|html|js|ts|tsx|jsx|py|go|rs|java|c|cpp|h|yml|yaml|toml|env|log|sh|sql|css|scss)$/i
@@ -24,9 +25,9 @@ function fileToContentBlocks(filePath: string): any[] {
24
25
  return [{ type: 'text', text: `[Attached file: ${filePath.split('/').pop()}]` }]
25
26
  }
26
27
 
27
- export function streamAnthropicChat({ session, message, imagePath, imageUrl, apiKey, systemPrompt, write, active, loadHistory, onUsage, signal }: StreamChatOptions): Promise<string> {
28
+ export function streamAnthropicChat({ session, message, imagePath, apiKey, systemPrompt, write, active, loadHistory, onUsage, signal }: StreamChatOptions): Promise<string> {
28
29
  return new Promise((resolve) => {
29
- const messages = buildMessages(session, message, imagePath, loadHistory, imageUrl)
30
+ const messages = buildMessages(session, message, imagePath, loadHistory)
30
31
  const model = session.model || 'claude-sonnet-4-6'
31
32
  let usageInput = 0
32
33
  let usageOutput = 0
@@ -136,30 +137,25 @@ export function streamAnthropicChat({ session, message, imagePath, imageUrl, api
136
137
  })
137
138
  }
138
139
 
139
- function urlToImageBlock(url: string): { type: string; source: { type: string; url: string } } {
140
- return { type: 'image', source: { type: 'url', url } }
141
- }
142
-
143
- function buildMessages(session: any, message: string, imagePath: string | undefined, loadHistory: (id: string) => any[], imageUrl?: string) {
144
- const msgs: Array<{ role: string; content: any }> = []
140
+ function buildMessages(session: Record<string, unknown> & { id: string }, message: string, imagePath: string | undefined, loadHistory: (id: string) => Record<string, unknown>[]) {
141
+ const msgs: Array<{ role: string; content: unknown }> = []
145
142
 
146
143
  if (loadHistory) {
147
144
  const history = loadHistory(session.id).slice(-40)
148
145
  for (const m of history) {
149
- if (m.role === 'user' && (m.imagePath || m.imageUrl)) {
150
- const blocks = m.imagePath ? fileToContentBlocks(m.imagePath) : []
151
- if (m.imageUrl) blocks.push(urlToImageBlock(m.imageUrl))
146
+ const histImagePath = resolveImagePath(m.imagePath as string | undefined, m.imageUrl as string | undefined)
147
+ if (m.role === 'user' && histImagePath) {
148
+ const blocks = fileToContentBlocks(histImagePath)
152
149
  msgs.push({ role: 'user', content: [...blocks, { type: 'text', text: m.text }] })
153
150
  } else {
154
- msgs.push({ role: m.role, content: m.text })
151
+ msgs.push({ role: m.role as string, content: m.text })
155
152
  }
156
153
  }
157
154
  }
158
155
 
159
- // Current message with optional attachment
160
- if (imagePath || imageUrl) {
161
- const blocks = imagePath ? fileToContentBlocks(imagePath) : []
162
- if (imageUrl) blocks.push(urlToImageBlock(imageUrl))
156
+ // Current message with optional attachment (imagePath already resolved by caller)
157
+ if (imagePath) {
158
+ const blocks = fileToContentBlocks(imagePath)
163
159
  msgs.push({ role: 'user', content: [...blocks, { type: 'text', text: message }] })
164
160
  } else {
165
161
  msgs.push({ role: 'user', content: message })
@@ -5,6 +5,8 @@ import { streamOpenAiChat } from './openai'
5
5
  import { streamOllamaChat } from './ollama'
6
6
  import { streamAnthropicChat } from './anthropic'
7
7
  import { streamOpenClawChat } from './openclaw'
8
+ import { errorMessage } from '../shared-utils'
9
+ import { sleep } from '@/lib/shared-utils'
8
10
  import type { ProviderInfo, ProviderConfig as CustomProviderConfig, ProviderType } from '../../types'
9
11
 
10
12
  const RETRYABLE_STATUS_CODES = [401, 429, 500, 502, 503]
@@ -392,7 +394,7 @@ export async function streamChatWithFailover(
392
394
  lastError = err
393
395
  const errObj = err as Record<string, unknown>
394
396
  const statusCode = (typeof errObj?.status === 'number' ? errObj.status : typeof errObj?.statusCode === 'number' ? errObj.statusCode : 0) as number
395
- const errMessage = err instanceof Error ? err.message : String(err)
397
+ const errMessage = errorMessage(err)
396
398
  const isRetryable = RETRYABLE_STATUS_CODES.includes(statusCode)
397
399
  || errMessage?.includes('rate limit')
398
400
  || errMessage?.includes('Rate limit')
@@ -409,7 +411,7 @@ export async function streamChatWithFailover(
409
411
  // Exponential backoff for rate-limit / server errors (skip for auth rotation)
410
412
  if (statusCode !== 401) {
411
413
  const delay = Math.min(500 * Math.pow(2, i), 8000)
412
- await new Promise((r) => setTimeout(r, delay))
414
+ await sleep(delay)
413
415
  }
414
416
  continue
415
417
  }
@@ -3,6 +3,7 @@ import http from 'http'
3
3
  import https from 'https'
4
4
  import type { StreamChatOptions } from './index'
5
5
  import { resolveOllamaRuntimeConfig } from '@/lib/server/ollama-runtime'
6
+ import { resolveImagePath } from '@/lib/server/resolve-image'
6
7
 
7
8
  const IMAGE_EXTS = /\.(png|jpg|jpeg|gif|webp|bmp)$/i
8
9
  const TEXT_EXTS = /\.(txt|md|csv|json|xml|html|js|ts|tsx|jsx|py|go|rs|java|c|cpp|h|yml|yaml|toml|env|log|sh|sql|css|scss)$/i
@@ -144,20 +145,22 @@ function fileToOllamaMsg(text: string, filePath?: string): { content: string; im
144
145
  return { content: `[Attached file: ${filePath.split('/').pop()}]\n\n${text}` }
145
146
  }
146
147
 
147
- function buildMessages(session: any, message: string, imagePath: string | undefined, loadHistory: (id: string) => any[]) {
148
+ function buildMessages(session: Record<string, unknown>, message: string, imagePath: string | undefined, loadHistory: (id: string) => Record<string, unknown>[]) {
148
149
  const msgs: Array<{ role: string; content: string; images?: string[] }> = []
149
150
 
150
151
  if (loadHistory) {
151
- const history = loadHistory(session.id).slice(-40)
152
+ const history = loadHistory(session.id as string).slice(-40)
152
153
  for (const m of history) {
153
- if (m.role === 'user' && m.imagePath) {
154
- msgs.push({ role: 'user', ...fileToOllamaMsg(m.text, m.imagePath) })
154
+ const histImagePath = resolveImagePath(m.imagePath as string | undefined, m.imageUrl as string | undefined)
155
+ if (m.role === 'user' && histImagePath) {
156
+ msgs.push({ role: 'user', ...fileToOllamaMsg(m.text as string, histImagePath) })
155
157
  } else {
156
- msgs.push({ role: m.role, content: m.text })
158
+ msgs.push({ role: m.role as string, content: m.text as string })
157
159
  }
158
160
  }
159
161
  }
160
162
 
161
- msgs.push({ role: 'user', ...fileToOllamaMsg(message, imagePath) })
163
+ const resolvedPath = resolveImagePath(imagePath)
164
+ msgs.push({ role: 'user', ...fileToOllamaMsg(message, resolvedPath ?? undefined) })
162
165
  return msgs
163
166
  }
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs'
2
2
  import type { StreamChatOptions } from './index'
3
3
  import { PROVIDER_DEFAULTS } from './provider-defaults'
4
+ import { resolveImagePath } from '@/lib/server/resolve-image'
4
5
 
5
6
  const IMAGE_EXTS = /\.(png|jpg|jpeg|gif|webp|bmp)$/i
6
7
  const TEXT_EXTS = /\.(txt|md|csv|json|xml|html|js|ts|tsx|jsx|py|go|rs|java|c|cpp|h|yml|yaml|toml|env|log|sh|sql|css|scss)$/i
@@ -168,34 +169,30 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
168
169
  })
169
170
  }
170
171
 
171
- function urlToImagePart(url: string): { type: string; image_url: { url: string; detail: string } } {
172
- return { type: 'image_url', image_url: { url, detail: 'auto' } }
173
- }
174
-
175
- async function buildMessages(session: any, message: string, imagePath: string | undefined, systemPrompt: string | undefined, loadHistory: (id: string) => any[], imageUrl?: string) {
176
- const msgs: Array<{ role: string; content: any }> = []
172
+ async function buildMessages(session: Record<string, unknown>, message: string, imagePath: string | undefined, systemPrompt: string | undefined, loadHistory: (id: string) => Record<string, unknown>[], imageUrl?: string) {
173
+ const msgs: Array<{ role: string; content: unknown }> = []
177
174
 
178
175
  if (systemPrompt) {
179
176
  msgs.push({ role: 'system', content: systemPrompt })
180
177
  }
181
178
 
182
179
  if (loadHistory) {
183
- const history = loadHistory(session.id).slice(-40)
180
+ const history = loadHistory(session.id as string).slice(-40)
184
181
  for (const m of history) {
185
- if (m.role === 'user' && (m.imagePath || m.imageUrl)) {
186
- const parts = m.imagePath ? await fileToContentParts(m.imagePath) : []
187
- if (m.imageUrl) parts.push(urlToImagePart(m.imageUrl))
182
+ const histImagePath = resolveImagePath(m.imagePath as string | undefined, m.imageUrl as string | undefined)
183
+ if (m.role === 'user' && histImagePath) {
184
+ const parts = await fileToContentParts(histImagePath)
188
185
  msgs.push({ role: 'user', content: [...parts, { type: 'text', text: m.text }] })
189
186
  } else {
190
- msgs.push({ role: m.role, content: m.text })
187
+ msgs.push({ role: m.role as string, content: m.text })
191
188
  }
192
189
  }
193
190
  }
194
191
 
195
192
  // Current message with optional attachment
196
- if (imagePath || imageUrl) {
197
- const parts = imagePath ? await fileToContentParts(imagePath) : []
198
- if (imageUrl) parts.push(urlToImagePart(imageUrl))
193
+ const resolvedPath = resolveImagePath(imagePath, imageUrl)
194
+ if (resolvedPath) {
195
+ const parts = await fileToContentParts(resolvedPath)
199
196
  msgs.push({ role: 'user', content: [...parts, { type: 'text', text: message }] })
200
197
  } else {
201
198
  msgs.push({ role: 'user', content: message })
@@ -5,24 +5,24 @@ import { isDevelopmentLikeRuntime, isProductionRuntime } from './runtime-env'
5
5
 
6
6
  describe('runtime env helpers', () => {
7
7
  it('treats missing NODE_ENV as development-like', () => {
8
- const previousNodeEnv = process.env.NODE_ENV
9
- delete process.env.NODE_ENV
8
+ const previousNodeEnv = process.env.NODE_ENV;
9
+ delete (process.env as any).NODE_ENV
10
10
 
11
11
  assert.equal(isDevelopmentLikeRuntime(), true)
12
12
  assert.equal(isProductionRuntime(), false)
13
13
 
14
- if (previousNodeEnv === undefined) delete process.env.NODE_ENV
15
- else process.env.NODE_ENV = previousNodeEnv
14
+ if (previousNodeEnv === undefined) delete (process.env as any).NODE_ENV
15
+ else (process.env as any).NODE_ENV = previousNodeEnv
16
16
  })
17
17
 
18
18
  it('detects explicit production mode', () => {
19
- const previousNodeEnv = process.env.NODE_ENV
20
- process.env.NODE_ENV = 'production'
19
+ const previousNodeEnv = process.env.NODE_ENV;
20
+ (process.env as any).NODE_ENV = 'production'
21
21
 
22
22
  assert.equal(isDevelopmentLikeRuntime(), false)
23
23
  assert.equal(isProductionRuntime(), true)
24
24
 
25
- if (previousNodeEnv === undefined) delete process.env.NODE_ENV
26
- else process.env.NODE_ENV = previousNodeEnv
25
+ if (previousNodeEnv === undefined) delete (process.env as any).NODE_ENV
26
+ else (process.env as any).NODE_ENV = previousNodeEnv
27
27
  })
28
28
  })
@@ -5,11 +5,11 @@ import {
5
5
  findEquivalentSchedules,
6
6
  getScheduleSignatureKey,
7
7
  type ScheduleLike,
8
- } from './schedule-dedupe.ts'
8
+ } from './schedule-dedupe'
9
9
  import {
10
10
  isAgentCreatedSchedule,
11
11
  shouldAutoDeleteScheduleAfterTerminalRun,
12
- } from './schedule-origin.ts'
12
+ } from './schedule-origin'
13
13
  import type { Schedule } from '@/types'
14
14
 
15
15
  function makeSchedule(partial?: Partial<ScheduleLike>): ScheduleLike {
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict'
2
2
  import { test } from 'node:test'
3
- import { findDuplicateSchedule, findEquivalentSchedules, getScheduleSignatureKey, type ScheduleLike } from './schedule-dedupe.ts'
3
+ import { findDuplicateSchedule, findEquivalentSchedules, getScheduleSignatureKey, type ScheduleLike } from './schedule-dedupe'
4
4
 
5
5
  test('findDuplicateSchedule matches active interval schedules with normalized prompts', () => {
6
6
  const schedules: Record<string, ScheduleLike> = {
@@ -1,5 +1,6 @@
1
1
  import { CronExpressionParser } from 'cron-parser'
2
2
  import type { ScheduleType } from '@/types'
3
+ import { dedup } from '@/lib/shared-utils'
3
4
 
4
5
  export type ScheduleLike = {
5
6
  id?: string
@@ -242,8 +243,8 @@ function countTokenOverlap(a: string[], b: string[]): number {
242
243
 
243
244
  function hasFuzzyPromptMatch(a: ScheduleSignature, b: ScheduleSignature): boolean {
244
245
  if (!a.promptTokens.length || !b.promptTokens.length) return false
245
- const uniqueA = [...new Set(a.promptTokens)]
246
- const uniqueB = [...new Set(b.promptTokens)]
246
+ const uniqueA = dedup(a.promptTokens)
247
+ const uniqueB = dedup(b.promptTokens)
247
248
  const overlap = countTokenOverlap(uniqueA, uniqueB)
248
249
  if (overlap === 0) return false
249
250
  const smallerSize = Math.min(uniqueA.length, uniqueB.length)
@@ -36,9 +36,9 @@ function runWithTempDataDir(script: string) {
36
36
  describe('ensureAgentThreadSession', () => {
37
37
  it('creates and reuses an agent shortcut chat for heartbeat-enabled agents', () => {
38
38
  const output = runWithTempDataDir(`
39
- const storageMod = await import('./src/lib/server/storage.ts')
39
+ const storageMod = await import('./src/lib/server/storage')
40
40
  const storage = storageMod.default || storageMod['module.exports'] || storageMod
41
- const helperMod = await import('./src/lib/server/agent-thread-session.ts')
41
+ const helperMod = await import('./src/lib/server/agent-thread-session')
42
42
  const ensureAgentThreadSession = helperMod.ensureAgentThreadSession
43
43
  || helperMod.default?.ensureAgentThreadSession
44
44
  || helperMod['module.exports']?.ensureAgentThreadSession
@@ -91,9 +91,9 @@ describe('ensureAgentThreadSession', () => {
91
91
 
92
92
  it('does not create a new shortcut chat when the agent is disabled', () => {
93
93
  const output = runWithTempDataDir(`
94
- const storageMod = await import('./src/lib/server/storage.ts')
94
+ const storageMod = await import('./src/lib/server/storage')
95
95
  const storage = storageMod.default || storageMod['module.exports'] || storageMod
96
- const helperMod = await import('./src/lib/server/agent-thread-session.ts')
96
+ const helperMod = await import('./src/lib/server/agent-thread-session')
97
97
  const ensureAgentThreadSession = helperMod.ensureAgentThreadSession
98
98
  || helperMod.default?.ensureAgentThreadSession
99
99
  || helperMod['module.exports']?.ensureAgentThreadSession
@@ -136,9 +136,9 @@ describe('ensureAgentThreadSession', () => {
136
136
 
137
137
  it('propagates explicit OpenClaw gateway agent ids into the shortcut session', () => {
138
138
  const output = runWithTempDataDir(`
139
- const storageMod = await import('./src/lib/server/storage.ts')
139
+ const storageMod = await import('./src/lib/server/storage')
140
140
  const storage = storageMod.default || storageMod['module.exports'] || storageMod
141
- const helperMod = await import('./src/lib/server/agent-thread-session.ts')
141
+ const helperMod = await import('./src/lib/server/agent-thread-session')
142
142
  const ensureAgentThreadSession = helperMod.ensureAgentThreadSession
143
143
  || helperMod.default?.ensureAgentThreadSession
144
144
  || helperMod['module.exports']?.ensureAgentThreadSession
@@ -3,7 +3,7 @@ import type { Agent, Session } from '@/types'
3
3
  import { applyResolvedRoute, resolvePrimaryAgentRoute } from './agent-runtime-config'
4
4
  import { isAgentDisabled } from './agent-availability'
5
5
  import { WORKSPACE_DIR } from './data-dir'
6
- import { loadAgents, loadSessions, saveAgents, saveSessions } from './storage'
6
+ import { loadAgents, loadSessions, upsertStoredItem } from './storage'
7
7
 
8
8
  function buildEmptyDelegateResumeIds(): NonNullable<Session['delegateResumeIds']> {
9
9
  return {
@@ -105,8 +105,7 @@ export function ensureAgentThreadSession(agentId: string, user = 'default'): Ses
105
105
  const existingId = typeof agent.threadSessionId === 'string' ? agent.threadSessionId : ''
106
106
  if (existingId && sessions[existingId]) {
107
107
  const session = buildThreadSession(agent, existingId, user, now, sessions[existingId] as Session)
108
- sessions[existingId] = session
109
- saveSessions(sessions)
108
+ upsertStoredItem('sessions', existingId, session)
110
109
  return session
111
110
  }
112
111
 
@@ -118,10 +117,9 @@ export function ensureAgentThreadSession(agentId: string, user = 'default'): Ses
118
117
  if (legacySession) {
119
118
  agent.threadSessionId = legacySession.id
120
119
  agent.updatedAt = now
121
- saveAgents(agents)
120
+ upsertStoredItem('agents', agentId, agent)
122
121
  const session = buildThreadSession(agent, legacySession.id, user, now, legacySession)
123
- sessions[legacySession.id] = session
124
- saveSessions(sessions)
122
+ upsertStoredItem('sessions', legacySession.id, session)
125
123
  return session
126
124
  }
127
125
 
@@ -129,11 +127,10 @@ export function ensureAgentThreadSession(agentId: string, user = 'default'): Ses
129
127
 
130
128
  const sessionId = `agent-chat-${agentId}-${genId()}`
131
129
  const session = buildThreadSession(agent, sessionId, user, now)
132
- sessions[sessionId] = session
133
- saveSessions(sessions)
130
+ upsertStoredItem('sessions', sessionId, session)
134
131
 
135
132
  agent.threadSessionId = sessionId
136
133
  agent.updatedAt = now
137
- saveAgents(agents)
134
+ upsertStoredItem('agents', agentId, agent)
138
135
  return session
139
136
  }