@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,570 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { after, before, describe, it } from 'node:test'
6
+ import type { BoardTask, Session } from '@/types'
7
+ import type { SessionLike } from './task-followups'
8
+
9
+ const originalEnv = {
10
+ DATA_DIR: process.env.DATA_DIR,
11
+ WORKSPACE_DIR: process.env.WORKSPACE_DIR,
12
+ SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
13
+ }
14
+
15
+ let tempDir = ''
16
+ let queue: typeof import('./queue')
17
+
18
+ before(async () => {
19
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-queue-test-'))
20
+ process.env.DATA_DIR = path.join(tempDir, 'data')
21
+ process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
22
+ process.env.SWARMCLAW_BUILD_MODE = '1'
23
+ fs.mkdirSync(process.env.DATA_DIR, { recursive: true })
24
+ fs.mkdirSync(process.env.WORKSPACE_DIR, { recursive: true })
25
+ queue = await import('./queue')
26
+ })
27
+
28
+ after(() => {
29
+ if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
30
+ else process.env.DATA_DIR = originalEnv.DATA_DIR
31
+ if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
32
+ else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
33
+ if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
34
+ else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
35
+ fs.rmSync(tempDir, { recursive: true, force: true })
36
+ })
37
+
38
+ function makeTask(overrides: Partial<import('@/types').BoardTask> = {}): import('@/types').BoardTask {
39
+ return {
40
+ id: `task-${Math.random().toString(36).slice(2, 8)}`,
41
+ title: 'Test task',
42
+ description: 'A test task',
43
+ status: 'queued',
44
+ agentId: 'agent-1',
45
+ createdAt: Date.now(),
46
+ updatedAt: Date.now(),
47
+ ...overrides,
48
+ }
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // dequeueNextRunnableTask
53
+ // ---------------------------------------------------------------------------
54
+ describe('dequeueNextRunnableTask', () => {
55
+ it('returns the first queued task from the queue', () => {
56
+ const t1 = makeTask({ id: 'a', status: 'queued' })
57
+ const t2 = makeTask({ id: 'b', status: 'queued' })
58
+ const tasks = { a: t1, b: t2 }
59
+ const q = ['a', 'b']
60
+ const result = queue.dequeueNextRunnableTask(q, tasks)
61
+ assert.equal(result, 'a')
62
+ assert.deepEqual(q, ['b'])
63
+ })
64
+
65
+ it('returns null for empty queue', () => {
66
+ const result = queue.dequeueNextRunnableTask([], {})
67
+ assert.equal(result, null)
68
+ })
69
+
70
+ it('strips stale non-queued entries before dequeuing', () => {
71
+ const t1 = makeTask({ id: 'a', status: 'completed' })
72
+ const t2 = makeTask({ id: 'b', status: 'queued' })
73
+ const tasks = { a: t1, b: t2 }
74
+ const q = ['a', 'b']
75
+ const result = queue.dequeueNextRunnableTask(q, tasks)
76
+ assert.equal(result, 'b')
77
+ assert.deepEqual(q, [])
78
+ })
79
+
80
+ it('strips entries for missing tasks', () => {
81
+ const t2 = makeTask({ id: 'b', status: 'queued' })
82
+ const tasks = { b: t2 }
83
+ const q = ['nonexistent', 'b']
84
+ const result = queue.dequeueNextRunnableTask(q, tasks)
85
+ assert.equal(result, 'b')
86
+ assert.deepEqual(q, [])
87
+ })
88
+
89
+ it('skips tasks blocked by incomplete dependencies', () => {
90
+ const blocker = makeTask({ id: 'dep', status: 'running' })
91
+ const blocked = makeTask({ id: 'child', status: 'queued', blockedBy: ['dep'] })
92
+ const unblocked = makeTask({ id: 'free', status: 'queued' })
93
+ const tasks = { dep: blocker, child: blocked, free: unblocked }
94
+ const q = ['child', 'free']
95
+ const result = queue.dequeueNextRunnableTask(q, tasks)
96
+ assert.equal(result, 'free')
97
+ assert.deepEqual(q, ['child'])
98
+ })
99
+
100
+ it('dequeues blocked task when all blockers are completed', () => {
101
+ const blocker = makeTask({ id: 'dep', status: 'completed' })
102
+ const blocked = makeTask({ id: 'child', status: 'queued', blockedBy: ['dep'] })
103
+ const tasks = { dep: blocker, child: blocked }
104
+ const q = ['child']
105
+ const result = queue.dequeueNextRunnableTask(q, tasks)
106
+ assert.equal(result, 'child')
107
+ assert.deepEqual(q, [])
108
+ })
109
+
110
+ it('skips tasks with future retryScheduledAt', () => {
111
+ const t = makeTask({ id: 'retry', status: 'queued', retryScheduledAt: Date.now() + 60_000 })
112
+ const tasks = { retry: t }
113
+ const q = ['retry']
114
+ const result = queue.dequeueNextRunnableTask(q, tasks)
115
+ assert.equal(result, null)
116
+ assert.deepEqual(q, ['retry'])
117
+ })
118
+
119
+ it('dequeues tasks with past retryScheduledAt', () => {
120
+ const t = makeTask({ id: 'retry', status: 'queued', retryScheduledAt: Date.now() - 1000 })
121
+ const tasks = { retry: t }
122
+ const q = ['retry']
123
+ const result = queue.dequeueNextRunnableTask(q, tasks)
124
+ assert.equal(result, 'retry')
125
+ assert.deepEqual(q, [])
126
+ })
127
+
128
+ it('respects FIFO ordering for tasks at same priority', () => {
129
+ const t1 = makeTask({ id: 'first', status: 'queued' })
130
+ const t2 = makeTask({ id: 'second', status: 'queued' })
131
+ const t3 = makeTask({ id: 'third', status: 'queued' })
132
+ const tasks = { first: t1, second: t2, third: t3 }
133
+ const q = ['first', 'second', 'third']
134
+ assert.equal(queue.dequeueNextRunnableTask(q, tasks), 'first')
135
+ assert.equal(queue.dequeueNextRunnableTask(q, tasks), 'second')
136
+ assert.equal(queue.dequeueNextRunnableTask(q, tasks), 'third')
137
+ assert.equal(queue.dequeueNextRunnableTask(q, tasks), null)
138
+ })
139
+
140
+ it('skips multiple blocked tasks and finds a runnable one deeper in the queue', () => {
141
+ const dep1 = makeTask({ id: 'dep1', status: 'running' })
142
+ const dep2 = makeTask({ id: 'dep2', status: 'running' })
143
+ const b1 = makeTask({ id: 'b1', status: 'queued', blockedBy: ['dep1'] })
144
+ const b2 = makeTask({ id: 'b2', status: 'queued', blockedBy: ['dep2'] })
145
+ const free = makeTask({ id: 'free', status: 'queued' })
146
+ const tasks = { dep1, dep2, b1, b2, free }
147
+ const q = ['b1', 'b2', 'free']
148
+ const result = queue.dequeueNextRunnableTask(q, tasks)
149
+ assert.equal(result, 'free')
150
+ assert.deepEqual(q, ['b1', 'b2'])
151
+ })
152
+ })
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // extractTaskResumeState
156
+ // ---------------------------------------------------------------------------
157
+ describe('extractTaskResumeState', () => {
158
+ it('returns null for null/undefined input', () => {
159
+ assert.equal(queue.extractTaskResumeState(null), null)
160
+ assert.equal(queue.extractTaskResumeState(undefined), null)
161
+ })
162
+
163
+ it('returns null when no resume IDs are present', () => {
164
+ const result = queue.extractTaskResumeState({ id: 'x', title: 'test' })
165
+ assert.equal(result, null)
166
+ })
167
+
168
+ it('extracts claudeResumeId', () => {
169
+ const result = queue.extractTaskResumeState({ claudeResumeId: 'claude-123' })
170
+ assert.ok(result)
171
+ assert.equal(result!.claudeSessionId, 'claude-123')
172
+ assert.equal(result!.delegateResumeIds.claudeCode, 'claude-123')
173
+ })
174
+
175
+ it('extracts codexResumeId', () => {
176
+ const result = queue.extractTaskResumeState({ codexResumeId: 'codex-abc' })
177
+ assert.ok(result)
178
+ assert.equal(result!.codexThreadId, 'codex-abc')
179
+ assert.equal(result!.delegateResumeIds.codex, 'codex-abc')
180
+ })
181
+
182
+ it('extracts opencodeResumeId', () => {
183
+ const result = queue.extractTaskResumeState({ opencodeResumeId: 'oc-456' })
184
+ assert.ok(result)
185
+ assert.equal(result!.opencodeSessionId, 'oc-456')
186
+ assert.equal(result!.delegateResumeIds.opencode, 'oc-456')
187
+ })
188
+
189
+ it('falls back to legacy cliResumeId with claude-cli provider', () => {
190
+ const result = queue.extractTaskResumeState({
191
+ cliResumeId: 'legacy-id',
192
+ cliProvider: 'claude-cli',
193
+ })
194
+ assert.ok(result)
195
+ assert.equal(result!.claudeSessionId, 'legacy-id')
196
+ assert.equal(result!.delegateResumeIds.claudeCode, 'legacy-id')
197
+ })
198
+
199
+ it('falls back to legacy cliResumeId with codex-cli provider', () => {
200
+ const result = queue.extractTaskResumeState({
201
+ cliResumeId: 'legacy-codex',
202
+ cliProvider: 'codex-cli',
203
+ })
204
+ assert.ok(result)
205
+ assert.equal(result!.codexThreadId, 'legacy-codex')
206
+ assert.equal(result!.delegateResumeIds.codex, 'legacy-codex')
207
+ })
208
+
209
+ it('ignores whitespace-only resume IDs', () => {
210
+ const result = queue.extractTaskResumeState({ claudeResumeId: ' ' })
211
+ assert.equal(result, null)
212
+ })
213
+
214
+ it('trims resume IDs', () => {
215
+ const result = queue.extractTaskResumeState({ claudeResumeId: ' abc ' })
216
+ assert.ok(result)
217
+ assert.equal(result!.claudeSessionId, 'abc')
218
+ })
219
+ })
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // extractSessionResumeState
223
+ // ---------------------------------------------------------------------------
224
+ describe('extractSessionResumeState', () => {
225
+ it('returns null for null/undefined', () => {
226
+ assert.equal(queue.extractSessionResumeState(null), null)
227
+ assert.equal(queue.extractSessionResumeState(undefined), null)
228
+ })
229
+
230
+ it('extracts session-level resume IDs', () => {
231
+ const result = queue.extractSessionResumeState({
232
+ claudeSessionId: 'cs-1',
233
+ codexThreadId: 'ct-2',
234
+ })
235
+ assert.ok(result)
236
+ assert.equal(result!.claudeSessionId, 'cs-1')
237
+ assert.equal(result!.codexThreadId, 'ct-2')
238
+ assert.equal(result!.delegateResumeIds.claudeCode, 'cs-1')
239
+ assert.equal(result!.delegateResumeIds.codex, 'ct-2')
240
+ })
241
+
242
+ it('returns null when session has no resume IDs', () => {
243
+ const result = queue.extractSessionResumeState({ id: 'sess-empty' })
244
+ assert.equal(result, null)
245
+ })
246
+
247
+ it('prefers delegateResumeIds over direct fields', () => {
248
+ const result = queue.extractSessionResumeState({
249
+ claudeSessionId: 'direct',
250
+ delegateResumeIds: { claudeCode: 'delegate-override', codex: null, opencode: null, gemini: null },
251
+ })
252
+ assert.ok(result)
253
+ assert.equal(result!.delegateResumeIds.claudeCode, 'delegate-override')
254
+ })
255
+ })
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // resolveTaskResumeContext
259
+ // ---------------------------------------------------------------------------
260
+ describe('resolveTaskResumeContext', () => {
261
+ it('returns null when no resume state is available', () => {
262
+ const task = makeTask({ id: 't1' })
263
+ const result = queue.resolveTaskResumeContext(task, { t1: task })
264
+ assert.equal(result, null)
265
+ })
266
+
267
+ it('finds resume state from self', () => {
268
+ const task = makeTask({ id: 't1', claudeResumeId: 'cr-self' })
269
+ const result = queue.resolveTaskResumeContext(task, { t1: task })
270
+ assert.ok(result)
271
+ assert.equal(result!.source, 'self')
272
+ assert.equal(result!.sourceTaskId, 't1')
273
+ assert.equal(result!.resume.claudeSessionId, 'cr-self')
274
+ })
275
+
276
+ it('finds resume state from delegatedFromTaskId', () => {
277
+ const parent = makeTask({ id: 'parent', claudeResumeId: 'cr-parent' })
278
+ const child = makeTask({ id: 'child', delegatedFromTaskId: 'parent' })
279
+ const result = queue.resolveTaskResumeContext(child, { parent, child })
280
+ assert.ok(result)
281
+ assert.equal(result!.source, 'delegated_from_task')
282
+ assert.equal(result!.sourceTaskId, 'parent')
283
+ })
284
+
285
+ it('finds resume state from blockedBy tasks', () => {
286
+ const blocker = makeTask({ id: 'blocker', codexResumeId: 'codex-b' })
287
+ const blocked = makeTask({ id: 'blocked', blockedBy: ['blocker'] })
288
+ const result = queue.resolveTaskResumeContext(blocked, { blocker, blocked })
289
+ assert.ok(result)
290
+ assert.equal(result!.source, 'blocked_by')
291
+ assert.equal(result!.sourceTaskId, 'blocker')
292
+ assert.equal(result!.resume.codexThreadId, 'codex-b')
293
+ })
294
+
295
+ it('prefers self over delegated_from_task', () => {
296
+ const parent = makeTask({ id: 'parent', claudeResumeId: 'cr-parent' })
297
+ const child = makeTask({ id: 'child', claudeResumeId: 'cr-self', delegatedFromTaskId: 'parent' })
298
+ const result = queue.resolveTaskResumeContext(child, { parent, child })
299
+ assert.ok(result)
300
+ assert.equal(result!.source, 'self')
301
+ assert.equal(result!.resume.claudeSessionId, 'cr-self')
302
+ })
303
+ })
304
+
305
+ // ---------------------------------------------------------------------------
306
+ // applyTaskResumeStateToSession
307
+ // ---------------------------------------------------------------------------
308
+ describe('applyTaskResumeStateToSession', () => {
309
+ function makeSession(overrides: Record<string, unknown> = {}): Session {
310
+ return {
311
+ id: 'sess-1',
312
+ agentId: 'agent-1',
313
+ messages: [],
314
+ createdAt: Date.now(),
315
+ lastActiveAt: Date.now(),
316
+ active: true,
317
+ ...overrides,
318
+ } as unknown as Session
319
+ }
320
+
321
+ it('returns false for null/undefined resume state', () => {
322
+ const session = makeSession()
323
+ assert.equal(queue.applyTaskResumeStateToSession(session, null), false)
324
+ assert.equal(queue.applyTaskResumeStateToSession(session, undefined), false)
325
+ })
326
+
327
+ it('applies claudeSessionId to session', () => {
328
+ const session = makeSession()
329
+ const resume = {
330
+ claudeSessionId: 'cs-1',
331
+ codexThreadId: null,
332
+ opencodeSessionId: null,
333
+ delegateResumeIds: { claudeCode: 'cs-1', codex: null, opencode: null, gemini: null },
334
+ }
335
+ const changed = queue.applyTaskResumeStateToSession(session, resume)
336
+ assert.equal(changed, true)
337
+ assert.equal(session.claudeSessionId, 'cs-1')
338
+ })
339
+
340
+ it('returns false when session already has the same values', () => {
341
+ const session = makeSession({
342
+ claudeSessionId: 'cs-1',
343
+ delegateResumeIds: { claudeCode: 'cs-1', codex: null, opencode: null, gemini: null },
344
+ })
345
+ const resume = {
346
+ claudeSessionId: 'cs-1',
347
+ codexThreadId: null,
348
+ opencodeSessionId: null,
349
+ delegateResumeIds: { claudeCode: 'cs-1', codex: null, opencode: null, gemini: null },
350
+ }
351
+ const changed = queue.applyTaskResumeStateToSession(session, resume)
352
+ assert.equal(changed, false)
353
+ })
354
+
355
+ it('returns false for empty resume state (no IDs set)', () => {
356
+ const session = makeSession()
357
+ const resume = {
358
+ claudeSessionId: null,
359
+ codexThreadId: null,
360
+ opencodeSessionId: null,
361
+ delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
362
+ }
363
+ const changed = queue.applyTaskResumeStateToSession(session, resume)
364
+ assert.equal(changed, false)
365
+ })
366
+ })
367
+
368
+ // ---------------------------------------------------------------------------
369
+ // resolveReusableTaskSessionId
370
+ // ---------------------------------------------------------------------------
371
+ describe('resolveReusableTaskSessionId', () => {
372
+ it('returns empty string when no session candidates exist', () => {
373
+ const task = makeTask({ id: 't1' })
374
+ const result = queue.resolveReusableTaskSessionId(task, { t1: task }, {})
375
+ assert.equal(result, '')
376
+ })
377
+
378
+ it('returns session from task checkpoint', () => {
379
+ const task = makeTask({
380
+ id: 't1',
381
+ checkpoint: { lastSessionId: 'sess-abc', updatedAt: Date.now() },
382
+ })
383
+ const sessions = { 'sess-abc': { id: 'sess-abc', cwd: '/tmp', messages: [], user: '' } }
384
+ const result = queue.resolveReusableTaskSessionId(task, { t1: task }, sessions as Record<string, SessionLike>)
385
+ assert.equal(result, 'sess-abc')
386
+ })
387
+
388
+ it('returns session from task sessionId', () => {
389
+ const task = makeTask({ id: 't1', sessionId: 'sess-direct' })
390
+ const sessions = { 'sess-direct': { id: 'sess-direct', cwd: '/tmp', messages: [], user: '' } }
391
+ const result = queue.resolveReusableTaskSessionId(task, { t1: task }, sessions as Record<string, SessionLike>)
392
+ assert.equal(result, 'sess-direct')
393
+ })
394
+
395
+ it('prefers checkpoint over sessionId', () => {
396
+ const task = makeTask({
397
+ id: 't1',
398
+ sessionId: 'sess-old',
399
+ checkpoint: { lastSessionId: 'sess-new', updatedAt: Date.now() },
400
+ })
401
+ const sessions = {
402
+ 'sess-old': { id: 'sess-old', cwd: '/tmp', messages: [], user: '' },
403
+ 'sess-new': { id: 'sess-new', cwd: '/tmp', messages: [], user: '' },
404
+ }
405
+ const result = queue.resolveReusableTaskSessionId(task, { t1: task }, sessions as Record<string, SessionLike>)
406
+ assert.equal(result, 'sess-new')
407
+ })
408
+
409
+ it('checks delegatedFromTaskId for sessions', () => {
410
+ const parent = makeTask({ id: 'parent', sessionId: 'sess-parent' })
411
+ const child = makeTask({ id: 'child', delegatedFromTaskId: 'parent' })
412
+ const sessions = { 'sess-parent': { id: 'sess-parent', cwd: '/tmp', messages: [], user: '' } }
413
+ const result = queue.resolveReusableTaskSessionId(child, { parent, child }, sessions as Record<string, SessionLike>)
414
+ assert.equal(result, 'sess-parent')
415
+ })
416
+
417
+ it('checks blockedBy tasks for sessions', () => {
418
+ const blocker = makeTask({ id: 'blocker', sessionId: 'sess-blocker' })
419
+ const blocked = makeTask({ id: 'blocked', blockedBy: ['blocker'] })
420
+ const sessions = { 'sess-blocker': { id: 'sess-blocker', cwd: '/tmp', messages: [], user: '' } }
421
+ const result = queue.resolveReusableTaskSessionId(blocked, { blocker, blocked }, sessions as Record<string, SessionLike>)
422
+ assert.equal(result, 'sess-blocker')
423
+ })
424
+
425
+ it('returns empty when referenced session does not exist', () => {
426
+ const task = makeTask({ id: 't1', sessionId: 'gone' })
427
+ const result = queue.resolveReusableTaskSessionId(task, { t1: task }, {})
428
+ assert.equal(result, '')
429
+ })
430
+ })
431
+
432
+ // ---------------------------------------------------------------------------
433
+ // enqueueTask (integration with storage)
434
+ // ---------------------------------------------------------------------------
435
+ describe('enqueueTask', () => {
436
+ it('sets task to queued status and adds to queue', async () => {
437
+ const storage = await import('./storage')
438
+ const taskId = 'enq-test-1'
439
+ const tasks: Record<string, BoardTask> = {}
440
+ tasks[taskId] = makeTask({ id: taskId, status: 'backlog' })
441
+ storage.saveTasks(tasks)
442
+ storage.saveQueue([])
443
+
444
+ queue.enqueueTask(taskId)
445
+
446
+ const updatedTasks = storage.loadTasks()
447
+ const updatedQueue = storage.loadQueue()
448
+ assert.equal(updatedTasks[taskId].status, 'queued')
449
+ assert.ok(updatedTasks[taskId].queuedAt)
450
+ assert.ok(updatedQueue.includes(taskId))
451
+ })
452
+
453
+ it('does not duplicate task in queue on double enqueue', async () => {
454
+ const storage = await import('./storage')
455
+ const taskId = 'enq-test-2'
456
+ const tasks: Record<string, BoardTask> = {}
457
+ tasks[taskId] = makeTask({ id: taskId, status: 'backlog' })
458
+ storage.saveTasks(tasks)
459
+ storage.saveQueue([])
460
+
461
+ queue.enqueueTask(taskId)
462
+ queue.enqueueTask(taskId)
463
+
464
+ const updatedQueue = storage.loadQueue()
465
+ const count = updatedQueue.filter((id: string) => id === taskId).length
466
+ assert.equal(count, 1)
467
+ })
468
+
469
+ it('no-ops for nonexistent task', async () => {
470
+ const storage = await import('./storage')
471
+ storage.saveQueue([])
472
+ queue.enqueueTask('does-not-exist')
473
+ const updatedQueue = storage.loadQueue()
474
+ assert.equal(updatedQueue.length, 0)
475
+ })
476
+ })
477
+
478
+ // ---------------------------------------------------------------------------
479
+ // cleanupFinishedTaskSessions (integration)
480
+ // ---------------------------------------------------------------------------
481
+ describe('cleanupFinishedTaskSessions', () => {
482
+ it('disables heartbeat on sessions for completed tasks', async () => {
483
+ const storage = await import('./storage')
484
+ const sessionId = 'sess-cleanup-1'
485
+ const tasks: Record<string, BoardTask> = {}
486
+ tasks['t-done'] = makeTask({ id: 't-done', status: 'completed', sessionId })
487
+ storage.saveTasks(tasks)
488
+
489
+ const sessions: Record<string, unknown> = {}
490
+ sessions[sessionId] = {
491
+ id: sessionId,
492
+ agentId: 'a1',
493
+ messages: [],
494
+ createdAt: Date.now(),
495
+ lastActiveAt: Date.now(),
496
+ active: false,
497
+ heartbeatEnabled: true,
498
+ }
499
+ storage.saveSessions(sessions)
500
+
501
+ queue.cleanupFinishedTaskSessions()
502
+
503
+ const updated = storage.loadSessions()
504
+ assert.equal(updated[sessionId].heartbeatEnabled, false)
505
+ })
506
+
507
+ it('skips sessions already disabled', async () => {
508
+ const storage = await import('./storage')
509
+ const sessionId = 'sess-cleanup-2'
510
+ const tasks: Record<string, BoardTask> = {}
511
+ tasks['t-done2'] = makeTask({ id: 't-done2', status: 'completed', sessionId })
512
+ storage.saveTasks(tasks)
513
+
514
+ const sessions: Record<string, unknown> = {}
515
+ sessions[sessionId] = {
516
+ id: sessionId,
517
+ agentId: 'a1',
518
+ messages: [],
519
+ createdAt: Date.now(),
520
+ lastActiveAt: Date.now() - 10_000,
521
+ active: false,
522
+ heartbeatEnabled: false,
523
+ }
524
+ storage.saveSessions(sessions)
525
+
526
+ queue.cleanupFinishedTaskSessions()
527
+
528
+ const updated = storage.loadSessions()
529
+ // lastActiveAt should not have been updated since heartbeat was already disabled
530
+ assert.ok(updated[sessionId].lastActiveAt <= Date.now() - 5_000)
531
+ })
532
+ })
533
+
534
+ // ---------------------------------------------------------------------------
535
+ // disableSessionHeartbeat (integration)
536
+ // ---------------------------------------------------------------------------
537
+ describe('disableSessionHeartbeat', () => {
538
+ it('disables heartbeat on existing session', async () => {
539
+ const storage = await import('./storage')
540
+ const sessionId = 'sess-hb-1'
541
+ const sessions: Record<string, unknown> = {}
542
+ sessions[sessionId] = {
543
+ id: sessionId,
544
+ agentId: 'a1',
545
+ messages: [],
546
+ createdAt: Date.now(),
547
+ lastActiveAt: Date.now() - 60_000,
548
+ active: true,
549
+ heartbeatEnabled: true,
550
+ }
551
+ storage.saveSessions(sessions)
552
+
553
+ queue.disableSessionHeartbeat(sessionId)
554
+
555
+ const updated = storage.loadSessions()
556
+ assert.equal(updated[sessionId].heartbeatEnabled, false)
557
+ assert.ok(updated[sessionId].lastActiveAt > Date.now() - 5_000)
558
+ })
559
+
560
+ it('no-ops for null/undefined session ID', async () => {
561
+ queue.disableSessionHeartbeat(null)
562
+ queue.disableSessionHeartbeat(undefined)
563
+ // Should not throw
564
+ })
565
+
566
+ it('no-ops for nonexistent session', async () => {
567
+ queue.disableSessionHeartbeat('does-not-exist-session')
568
+ // Should not throw
569
+ })
570
+ })