@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,137 @@
1
+ import assert from 'node:assert/strict'
2
+ import { before, describe, it } from 'node:test'
3
+
4
+ // provider-health uses globalThis to store state — we can import directly
5
+ // since the pure logic functions don't need DATA_DIR. But spawnSync is used
6
+ // by commandExists/delegateToolReady, so rankDelegatesByHealth will hit
7
+ // the real filesystem. We test pure state functions here.
8
+
9
+ let providerHealth: typeof import('./provider-health')
10
+
11
+ before(async () => {
12
+ process.env.SWARMCLAW_BUILD_MODE = '1'
13
+ providerHealth = await import('./provider-health')
14
+ })
15
+
16
+ describe('provider-health', () => {
17
+ // -------------------------------------------------------------------------
18
+ // markProviderFailure / markProviderSuccess / isProviderCoolingDown
19
+ // -------------------------------------------------------------------------
20
+
21
+ it('fresh provider is not cooling down', () => {
22
+ assert.equal(providerHealth.isProviderCoolingDown('fresh-provider-xyz'), false)
23
+ })
24
+
25
+ it('markProviderFailure puts provider into cooldown', () => {
26
+ providerHealth.markProviderFailure('test-fail-1', 'connection refused')
27
+ assert.equal(providerHealth.isProviderCoolingDown('test-fail-1'), true)
28
+ })
29
+
30
+ it('markProviderSuccess clears cooldown', () => {
31
+ providerHealth.markProviderFailure('test-recover-1', 'timeout')
32
+ assert.equal(providerHealth.isProviderCoolingDown('test-recover-1'), true)
33
+
34
+ providerHealth.markProviderSuccess('test-recover-1')
35
+ assert.equal(providerHealth.isProviderCoolingDown('test-recover-1'), false)
36
+ })
37
+
38
+ it('multiple failures increase cooldown (exponential backoff)', () => {
39
+ const id = 'test-backoff-1'
40
+ providerHealth.markProviderFailure(id, 'err')
41
+ const snap1 = providerHealth.getProviderHealthSnapshot()[id]
42
+
43
+ providerHealth.markProviderFailure(id, 'err')
44
+ const snap2 = providerHealth.getProviderHealthSnapshot()[id]
45
+
46
+ providerHealth.markProviderFailure(id, 'err')
47
+ const snap3 = providerHealth.getProviderHealthSnapshot()[id]
48
+
49
+ assert.equal(snap1.failures, 1)
50
+ assert.equal(snap2.failures, 2)
51
+ assert.equal(snap3.failures, 3)
52
+
53
+ // Cooldown should increase with more failures
54
+ const cooldown1 = (snap1.cooldownUntil ?? 0) - (snap1.lastFailureAt ?? 0)
55
+ const cooldown2 = (snap2.cooldownUntil ?? 0) - (snap2.lastFailureAt ?? 0)
56
+ const cooldown3 = (snap3.cooldownUntil ?? 0) - (snap3.lastFailureAt ?? 0)
57
+ assert.ok(cooldown2 > cooldown1, 'cooldown2 > cooldown1')
58
+ assert.ok(cooldown3 > cooldown2, 'cooldown3 > cooldown2')
59
+ })
60
+
61
+ it('failure count is capped at 50', () => {
62
+ const id = 'test-cap-1'
63
+ for (let i = 0; i < 60; i++) {
64
+ providerHealth.markProviderFailure(id, `err-${i}`)
65
+ }
66
+ const snap = providerHealth.getProviderHealthSnapshot()[id]
67
+ assert.equal(snap.failures, 50)
68
+ })
69
+
70
+ it('error message is truncated to 500 chars', () => {
71
+ const id = 'test-trunc-1'
72
+ const longError = 'x'.repeat(1000)
73
+ providerHealth.markProviderFailure(id, longError)
74
+ const snap = providerHealth.getProviderHealthSnapshot()[id]
75
+ assert.equal(snap.lastError?.length, 500)
76
+ })
77
+
78
+ it('success resets failure count to 0', () => {
79
+ const id = 'test-reset-1'
80
+ providerHealth.markProviderFailure(id, 'err')
81
+ providerHealth.markProviderFailure(id, 'err')
82
+ providerHealth.markProviderFailure(id, 'err')
83
+ providerHealth.markProviderSuccess(id)
84
+ const snap = providerHealth.getProviderHealthSnapshot()[id]
85
+ assert.equal(snap.failures, 0)
86
+ assert.equal(snap.cooldownUntil, undefined)
87
+ })
88
+
89
+ it('success preserves lastError and lastFailureAt from previous failures', () => {
90
+ const id = 'test-preserve-1'
91
+ providerHealth.markProviderFailure(id, 'original error')
92
+ const afterFail = providerHealth.getProviderHealthSnapshot()[id]
93
+ providerHealth.markProviderSuccess(id)
94
+ const afterSuccess = providerHealth.getProviderHealthSnapshot()[id]
95
+
96
+ assert.equal(afterSuccess.lastError, 'original error')
97
+ assert.equal(afterSuccess.lastFailureAt, afterFail.lastFailureAt)
98
+ assert.ok(afterSuccess.lastSuccessAt! > 0)
99
+ })
100
+
101
+ // -------------------------------------------------------------------------
102
+ // getProviderHealthSnapshot
103
+ // -------------------------------------------------------------------------
104
+
105
+ it('snapshot includes coolingDown boolean', () => {
106
+ const id = 'test-snapshot-cool'
107
+ providerHealth.markProviderFailure(id, 'err')
108
+ const snap = providerHealth.getProviderHealthSnapshot()
109
+ assert.equal(snap[id].coolingDown, true)
110
+
111
+ providerHealth.markProviderSuccess(id)
112
+ const snap2 = providerHealth.getProviderHealthSnapshot()
113
+ assert.equal(snap2[id].coolingDown, false)
114
+ })
115
+
116
+ // -------------------------------------------------------------------------
117
+ // OPENAI_COMPATIBLE_DEFAULTS
118
+ // -------------------------------------------------------------------------
119
+
120
+ it('OPENAI_COMPATIBLE_DEFAULTS has expected providers', () => {
121
+ const defaults = providerHealth.OPENAI_COMPATIBLE_DEFAULTS
122
+ assert.ok(defaults.openai)
123
+ assert.ok(defaults.google)
124
+ assert.ok(defaults.deepseek)
125
+ assert.ok(defaults.groq)
126
+ assert.ok(defaults.together)
127
+ assert.ok(defaults.mistral)
128
+ assert.ok(defaults.xai)
129
+ assert.ok(defaults.fireworks)
130
+
131
+ // Each entry has name and defaultEndpoint
132
+ for (const [, val] of Object.entries(defaults)) {
133
+ assert.ok(typeof val.name === 'string' && val.name.length > 0)
134
+ assert.ok(typeof val.defaultEndpoint === 'string' && val.defaultEndpoint.startsWith('https://'))
135
+ }
136
+ })
137
+ })
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from 'child_process'
2
+ import { errorMessage, hmrSingleton } from '@/lib/shared-utils'
2
3
 
3
4
  type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli'
4
5
 
@@ -10,9 +11,8 @@ interface ProviderHealthState {
10
11
  cooldownUntil?: number
11
12
  }
12
13
 
13
- const gk = '__swarmclaw_provider_health__' as const
14
14
  const states: Map<string, ProviderHealthState> =
15
- (globalThis as any)[gk] ?? ((globalThis as any)[gk] = new Map<string, ProviderHealthState>())
15
+ hmrSingleton('__swarmclaw_provider_health__', () => new Map<string, ProviderHealthState>())
16
16
 
17
17
  const cliCheckCache = new Map<string, { at: number; ok: boolean }>()
18
18
  const delegateReadyCache = new Map<string, { at: number; ok: boolean }>()
@@ -264,7 +264,7 @@ export async function pingProvider(
264
264
  } catch (err: unknown) {
265
265
  const msg = err instanceof Error && err.name === 'TimeoutError'
266
266
  ? 'Connection timed out.'
267
- : (err instanceof Error ? err.message : String(err))
267
+ : errorMessage(err)
268
268
  return { ok: false, message: msg }
269
269
  }
270
270
  }
@@ -1,4 +1,5 @@
1
1
  import crypto from 'crypto'
2
+ import { hmrSingleton } from '@/lib/shared-utils'
2
3
  import { getProviderList } from '@/lib/providers'
3
4
  import { OPENAI_COMPATIBLE_DEFAULTS } from '@/lib/server/provider-health'
4
5
  import { decryptKey, loadCredentials } from '@/lib/server/storage'
@@ -33,20 +34,10 @@ const CLOUD_CACHE_TTL_MS = 15 * 60_000
33
34
  const LOCAL_CACHE_TTL_MS = 60_000
34
35
  const ERROR_CACHE_TTL_MS = 30_000
35
36
  const DISCOVERY_TIMEOUT_MS = 10_000
36
- const gk = '__swarmclaw_provider_model_discovery__' as const
37
-
38
- type DiscoveryGlobals = typeof globalThis & {
39
- [gk]?: {
40
- cache: Map<string, DiscoveryCacheEntry>
41
- pending: Map<string, Promise<ProviderModelDiscoveryResult>>
42
- }
43
- }
44
-
45
- const discoveryGlobals = globalThis as DiscoveryGlobals
46
- const discoveryState = discoveryGlobals[gk] ?? (discoveryGlobals[gk] = {
37
+ const discoveryState = hmrSingleton('__swarmclaw_provider_model_discovery__', () => ({
47
38
  cache: new Map<string, DiscoveryCacheEntry>(),
48
39
  pending: new Map<string, Promise<ProviderModelDiscoveryResult>>(),
49
- })
40
+ }))
50
41
 
51
42
  function clean(value: string | null | undefined): string {
52
43
  return typeof value === 'string' ? value.trim() : ''
@@ -82,7 +82,7 @@ describe('resolveTaskOriginConnectorFollowupTarget', () => {
82
82
  const target = resolveTaskOriginConnectorFollowupTarget({
83
83
  task,
84
84
  sessions: sessions as SessionFixtureMap,
85
- connectors,
85
+ connectors: connectors as any,
86
86
  running,
87
87
  })
88
88
 
@@ -129,7 +129,7 @@ describe('resolveTaskOriginConnectorFollowupTarget', () => {
129
129
  const target = resolveTaskOriginConnectorFollowupTarget({
130
130
  task,
131
131
  sessions: sessions as SessionFixtureMap,
132
- connectors,
132
+ connectors: connectors as any,
133
133
  running,
134
134
  })
135
135
 
@@ -177,7 +177,7 @@ describe('resolveTaskOriginConnectorFollowupTarget', () => {
177
177
  const target = resolveTaskOriginConnectorFollowupTarget({
178
178
  task,
179
179
  sessions: sessions as SessionFixtureMap,
180
- connectors,
180
+ connectors: connectors as any,
181
181
  running,
182
182
  })
183
183
 
@@ -226,7 +226,7 @@ describe('resolveTaskOriginConnectorFollowupTarget', () => {
226
226
  const target = resolveTaskOriginConnectorFollowupTarget({
227
227
  task,
228
228
  sessions: sessions as SessionFixtureMap,
229
- connectors,
229
+ connectors: connectors as any,
230
230
  running,
231
231
  })
232
232
 
@@ -280,7 +280,7 @@ describe('resolveTaskOriginConnectorFollowupTarget', () => {
280
280
  const target = resolveTaskOriginConnectorFollowupTarget({
281
281
  task,
282
282
  sessions: sessions as SessionFixtureMap,
283
- connectors,
283
+ connectors: connectors as any,
284
284
  running,
285
285
  })
286
286
 
@@ -341,7 +341,7 @@ describe('resolveTaskOriginConnectorFollowupTarget', () => {
341
341
  const target = resolveTaskOriginConnectorFollowupTarget({
342
342
  task,
343
343
  sessions: sessions as SessionFixtureMap,
344
- connectors,
344
+ connectors: connectors as any,
345
345
  running,
346
346
  })
347
347
 
@@ -391,7 +391,7 @@ describe('collectTaskConnectorFollowupTargets', () => {
391
391
  const targets = collectTaskConnectorFollowupTargets({
392
392
  task,
393
393
  sessions: sessions as SessionFixtureMap,
394
- connectors,
394
+ connectors: connectors as any,
395
395
  running,
396
396
  })
397
397
 
@@ -434,7 +434,7 @@ describe('collectTaskConnectorFollowupTargets', () => {
434
434
  const targets = collectTaskConnectorFollowupTargets({
435
435
  task,
436
436
  sessions: sessions as SessionFixtureMap,
437
- connectors,
437
+ connectors: connectors as any,
438
438
  running,
439
439
  })
440
440
 
@@ -478,7 +478,7 @@ describe('collectTaskConnectorFollowupTargets', () => {
478
478
  const targets = collectTaskConnectorFollowupTargets({
479
479
  task,
480
480
  sessions: sessions as SessionFixtureMap,
481
- connectors,
481
+ connectors: connectors as any,
482
482
  running,
483
483
  })
484
484
 
@@ -36,8 +36,8 @@ function runWithTempDataDir(script: string) {
36
36
  describe('reconcileFinishedRunningTasks', () => {
37
37
  it('finalizes a completed one-off scheduled task from its finished session and deletes the schedule', () => {
38
38
  const output = runWithTempDataDir(`
39
- const storageMod = await import('./src/lib/server/storage.ts')
40
- const queueMod = await import('./src/lib/server/queue.ts')
39
+ const storageMod = await import('./src/lib/server/storage')
40
+ const queueMod = await import('./src/lib/server/queue')
41
41
  const storage = storageMod.default || storageMod
42
42
  const queue = queueMod.default || queueMod
43
43
 
@@ -0,0 +1,269 @@
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 { spawnSync } from 'node:child_process'
6
+ import { describe, it } from 'node:test'
7
+
8
+ const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..')
9
+
10
+ function runWithTempDataDir(script: string) {
11
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-queue-recovery-'))
12
+ try {
13
+ const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
14
+ cwd: repoRoot,
15
+ env: {
16
+ ...process.env,
17
+ DATA_DIR: path.join(tempDir, 'data'),
18
+ WORKSPACE_DIR: path.join(tempDir, 'workspace'),
19
+ SWARMCLAW_BUILD_MODE: '1',
20
+ },
21
+ encoding: 'utf-8',
22
+ timeout: 15000,
23
+ })
24
+ assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
25
+ const lines = (result.stdout || '')
26
+ .trim()
27
+ .split('\n')
28
+ .map((line) => line.trim())
29
+ .filter(Boolean)
30
+ const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
31
+ return JSON.parse(jsonLine || '{}') as Record<string, any>
32
+ } finally {
33
+ fs.rmSync(tempDir, { recursive: true, force: true })
34
+ }
35
+ }
36
+
37
+ describe('queue recovery', () => {
38
+ it('processNext recovers orphaned queued tasks and defers them when the agent is disabled', () => {
39
+ const output = runWithTempDataDir(`
40
+ const storageMod = await import('./src/lib/server/storage')
41
+ const queueMod = await import('./src/lib/server/queue')
42
+ const storage = storageMod.default || storageMod
43
+ const queue = queueMod.default || queueMod
44
+
45
+ const now = Date.now()
46
+ storage.saveAgents({
47
+ 'agent-disabled': {
48
+ id: 'agent-disabled',
49
+ name: 'Disabled Agent',
50
+ provider: 'openai',
51
+ model: 'gpt-test',
52
+ disabled: true,
53
+ createdAt: now,
54
+ updatedAt: now,
55
+ },
56
+ })
57
+ storage.saveTasks({
58
+ orphaned: {
59
+ id: 'orphaned',
60
+ title: 'Recover me',
61
+ description: 'Queued task missing from the queue array',
62
+ status: 'queued',
63
+ agentId: 'agent-disabled',
64
+ createdAt: now - 5_000,
65
+ updatedAt: now - 5_000,
66
+ },
67
+ })
68
+ storage.saveQueue([])
69
+
70
+ await queue.processNext()
71
+
72
+ const task = storage.loadTasks().orphaned
73
+ const queueItems = storage.loadQueue()
74
+ console.log(JSON.stringify({
75
+ status: task?.status ?? null,
76
+ queued: queueItems,
77
+ retryDelayMs: typeof task?.retryScheduledAt === 'number' ? task.retryScheduledAt - now : null,
78
+ error: task?.error ?? null,
79
+ }))
80
+ `)
81
+
82
+ assert.equal(output.status, 'queued')
83
+ assert.deepEqual(output.queued, ['orphaned'])
84
+ assert.equal(typeof output.retryDelayMs, 'number')
85
+ assert.ok(output.retryDelayMs >= 55_000 && output.retryDelayMs <= 65_000)
86
+ assert.match(output.error, /disabled/i)
87
+ })
88
+
89
+ it('recoverStalledRunningTasks requeues tasks missing startedAt and records the recovery', () => {
90
+ const output = runWithTempDataDir(`
91
+ const storageMod = await import('./src/lib/server/storage')
92
+ const queueMod = await import('./src/lib/server/queue')
93
+ const storage = storageMod.default || storageMod
94
+ const queue = queueMod.default || queueMod
95
+
96
+ const now = Date.now()
97
+ storage.saveTasks({
98
+ broken: {
99
+ id: 'broken',
100
+ title: 'Broken running task',
101
+ description: 'Missing startedAt should be recovered',
102
+ status: 'running',
103
+ agentId: 'agent-a',
104
+ createdAt: now - 20_000,
105
+ updatedAt: now - 15_000,
106
+ },
107
+ })
108
+ storage.saveQueue([])
109
+
110
+ const originalSetTimeout = globalThis.setTimeout
111
+ const scheduled = []
112
+ globalThis.setTimeout = (fn, delay, ...args) => {
113
+ scheduled.push(delay)
114
+ return 0
115
+ }
116
+ try {
117
+ const result = queue.recoverStalledRunningTasks()
118
+ const task = storage.loadTasks().broken
119
+ console.log(JSON.stringify({
120
+ result,
121
+ status: task?.status ?? null,
122
+ queued: storage.loadQueue(),
123
+ retryDelayMs: typeof task?.retryScheduledAt === 'number' ? task.retryScheduledAt - now : null,
124
+ error: task?.error ?? null,
125
+ comment: task?.comments?.at(-1)?.text ?? null,
126
+ scheduledCalls: scheduled.length,
127
+ }))
128
+ } finally {
129
+ globalThis.setTimeout = originalSetTimeout
130
+ }
131
+ `)
132
+
133
+ assert.equal(output.result.recovered, 1)
134
+ assert.equal(output.result.deadLettered, 0)
135
+ assert.equal(output.status, 'queued')
136
+ assert.deepEqual(output.queued, ['broken'])
137
+ assert.equal(typeof output.retryDelayMs, 'number')
138
+ assert.ok(output.retryDelayMs >= 25_000 && output.retryDelayMs <= 35_000)
139
+ assert.match(output.error, /missing startedAt/i)
140
+ assert.match(output.comment, /missing startedAt/i)
141
+ assert.equal(output.scheduledCalls, 1)
142
+ })
143
+
144
+ it('recoverStalledRunningTasks preserves retry policy backoff for stalled tasks', () => {
145
+ const output = runWithTempDataDir(`
146
+ const storageMod = await import('./src/lib/server/storage')
147
+ const queueMod = await import('./src/lib/server/queue')
148
+ const storage = storageMod.default || storageMod
149
+ const queue = queueMod.default || queueMod
150
+
151
+ const now = Date.now()
152
+ storage.saveSettings({
153
+ ...storage.loadSettings(),
154
+ taskStallTimeoutMin: 5,
155
+ taskRetryBackoffSec: 90,
156
+ })
157
+ storage.saveSessions({
158
+ 'sess-stalled': {
159
+ id: 'sess-stalled',
160
+ agentId: 'agent-a',
161
+ messages: [],
162
+ createdAt: now - 100_000,
163
+ lastActiveAt: now - 5_000,
164
+ heartbeatEnabled: true,
165
+ },
166
+ })
167
+ storage.saveTasks({
168
+ stalled: {
169
+ id: 'stalled',
170
+ title: 'Stalled task',
171
+ description: 'Should use configured backoff when recovered',
172
+ status: 'running',
173
+ agentId: 'agent-a',
174
+ sessionId: 'sess-stalled',
175
+ createdAt: now - 200_000,
176
+ updatedAt: now - 420_000,
177
+ startedAt: now - 420_000,
178
+ maxAttempts: 3,
179
+ attempts: 0,
180
+ },
181
+ })
182
+ storage.saveQueue([])
183
+
184
+ const originalSetTimeout = globalThis.setTimeout
185
+ const scheduled = []
186
+ globalThis.setTimeout = (fn, delay, ...args) => {
187
+ scheduled.push(delay)
188
+ return 0
189
+ }
190
+ try {
191
+ const result = queue.recoverStalledRunningTasks()
192
+ const task = storage.loadTasks().stalled
193
+ const session = storage.loadSessions()['sess-stalled']
194
+ console.log(JSON.stringify({
195
+ result,
196
+ status: task?.status ?? null,
197
+ attempts: task?.attempts ?? null,
198
+ queued: storage.loadQueue(),
199
+ retryDelayMs: typeof task?.retryScheduledAt === 'number' ? task.retryScheduledAt - now : null,
200
+ error: task?.error ?? null,
201
+ heartbeatEnabled: session?.heartbeatEnabled ?? null,
202
+ scheduledCalls: scheduled.length,
203
+ }))
204
+ } finally {
205
+ globalThis.setTimeout = originalSetTimeout
206
+ }
207
+ `)
208
+
209
+ assert.equal(output.result.recovered, 1)
210
+ assert.equal(output.result.deadLettered, 0)
211
+ assert.equal(output.status, 'queued')
212
+ assert.equal(output.attempts, 1)
213
+ assert.deepEqual(output.queued, ['stalled'])
214
+ assert.equal(typeof output.retryDelayMs, 'number')
215
+ assert.ok(output.retryDelayMs >= 85_000 && output.retryDelayMs <= 95_000)
216
+ assert.match(output.error, /Retry scheduled after failure/i)
217
+ assert.equal(output.heartbeatEnabled, false)
218
+ assert.equal(output.scheduledCalls, 1)
219
+ })
220
+
221
+ it('resumeQueue restores blocked queued tasks without clobbering their queuedAt timestamp', () => {
222
+ const output = runWithTempDataDir(`
223
+ const storageMod = await import('./src/lib/server/storage')
224
+ const queueMod = await import('./src/lib/server/queue')
225
+ const storage = storageMod.default || storageMod
226
+ const queue = queueMod.default || queueMod
227
+
228
+ const originalQueuedAt = Date.now() - 45_000
229
+ storage.saveTasks({
230
+ dep: {
231
+ id: 'dep',
232
+ title: 'Dependency',
233
+ description: 'Still running',
234
+ status: 'running',
235
+ agentId: 'agent-a',
236
+ createdAt: originalQueuedAt - 10_000,
237
+ updatedAt: originalQueuedAt - 10_000,
238
+ startedAt: originalQueuedAt - 10_000,
239
+ },
240
+ blocked: {
241
+ id: 'blocked',
242
+ title: 'Blocked task',
243
+ description: 'Should be re-added to the queue on boot',
244
+ status: 'queued',
245
+ agentId: 'agent-a',
246
+ blockedBy: ['dep'],
247
+ queuedAt: originalQueuedAt,
248
+ createdAt: originalQueuedAt - 20_000,
249
+ updatedAt: originalQueuedAt - 5_000,
250
+ },
251
+ })
252
+ storage.saveQueue([])
253
+
254
+ queue.resumeQueue()
255
+
256
+ const task = storage.loadTasks().blocked
257
+ console.log(JSON.stringify({
258
+ queued: storage.loadQueue(),
259
+ queuedAt: task?.queuedAt ?? null,
260
+ status: task?.status ?? null,
261
+ }))
262
+ `)
263
+
264
+ assert.deepEqual(output.queued, ['blocked'])
265
+ assert.equal(output.status, 'queued')
266
+ assert.equal(typeof output.queuedAt, 'number')
267
+ assert.ok(output.queuedAt < Date.now() - 30_000)
268
+ })
269
+ })