@swarmclawai/swarmclaw 0.8.4 → 0.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (394) hide show
  1. package/README.md +9 -9
  2. package/bin/swarmclaw.js +5 -1
  3. package/bin/worker-cmd.js +73 -0
  4. package/package.json +2 -1
  5. package/src/app/api/agents/[id]/route.ts +17 -7
  6. package/src/app/api/agents/route.ts +21 -8
  7. package/src/app/api/approvals/route.test.ts +6 -6
  8. package/src/app/api/approvals/route.ts +2 -1
  9. package/src/app/api/auth/route.ts +2 -3
  10. package/src/app/api/chatrooms/[id]/chat/route.test.ts +299 -0
  11. package/src/app/api/chatrooms/[id]/chat/route.ts +3 -2
  12. package/src/app/api/chatrooms/[id]/route.ts +7 -6
  13. package/src/app/api/chats/[id]/chat/route.test.ts +496 -0
  14. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  15. package/src/app/api/chats/[id]/clear/route.ts +9 -9
  16. package/src/app/api/chats/[id]/devserver/route.ts +2 -1
  17. package/src/app/api/chats/[id]/edit-resend/route.ts +3 -4
  18. package/src/app/api/chats/[id]/fork/route.ts +3 -5
  19. package/src/app/api/chats/[id]/restore/route.ts +6 -7
  20. package/src/app/api/chats/[id]/retry/route.ts +3 -4
  21. package/src/app/api/chats/[id]/route.ts +61 -62
  22. package/src/app/api/chats/route.ts +7 -1
  23. package/src/app/api/connectors/[id]/route.ts +7 -8
  24. package/src/app/api/connectors/route.ts +5 -4
  25. package/src/app/api/eval/run/route.ts +2 -1
  26. package/src/app/api/eval/suite/route.ts +2 -1
  27. package/src/app/api/external-agents/route.test.ts +1 -1
  28. package/src/app/api/external-agents/route.ts +2 -2
  29. package/src/app/api/files/serve/route.ts +1 -1
  30. package/src/app/api/gateways/[id]/route.ts +7 -5
  31. package/src/app/api/gateways/route.ts +1 -1
  32. package/src/app/api/knowledge/upload/route.ts +1 -1
  33. package/src/app/api/logs/route.ts +5 -7
  34. package/src/app/api/memory-images/[filename]/route.ts +2 -3
  35. package/src/app/api/openclaw/agent-files/route.ts +4 -3
  36. package/src/app/api/openclaw/approvals/route.ts +3 -4
  37. package/src/app/api/openclaw/config-sync/route.ts +3 -2
  38. package/src/app/api/openclaw/cron/route.ts +3 -2
  39. package/src/app/api/openclaw/dotenv-keys/route.ts +2 -1
  40. package/src/app/api/openclaw/exec-config/route.ts +3 -2
  41. package/src/app/api/openclaw/gateway/route.ts +5 -4
  42. package/src/app/api/openclaw/history/route.ts +3 -2
  43. package/src/app/api/openclaw/media/route.ts +2 -1
  44. package/src/app/api/openclaw/permissions/route.ts +3 -2
  45. package/src/app/api/openclaw/sandbox-env/route.ts +3 -2
  46. package/src/app/api/openclaw/skills/install/route.ts +2 -1
  47. package/src/app/api/openclaw/skills/remove/route.ts +2 -1
  48. package/src/app/api/openclaw/skills/route.ts +3 -2
  49. package/src/app/api/orchestrator/run/route.ts +5 -14
  50. package/src/app/api/perf/route.ts +43 -0
  51. package/src/app/api/plugins/dependencies/route.ts +2 -1
  52. package/src/app/api/plugins/install/route.ts +2 -1
  53. package/src/app/api/plugins/marketplace/route.ts +3 -2
  54. package/src/app/api/plugins/settings/route.ts +2 -1
  55. package/src/app/api/preview-server/route.ts +11 -10
  56. package/src/app/api/projects/[id]/route.ts +1 -1
  57. package/src/app/api/schedules/[id]/route.test.ts +128 -0
  58. package/src/app/api/schedules/[id]/route.ts +43 -43
  59. package/src/app/api/schedules/[id]/run/route.ts +11 -62
  60. package/src/app/api/schedules/route.ts +21 -87
  61. package/src/app/api/settings/route.ts +2 -0
  62. package/src/app/api/setup/doctor/route.ts +9 -8
  63. package/src/app/api/tasks/[id]/approve/route.ts +33 -30
  64. package/src/app/api/tasks/[id]/route.ts +12 -35
  65. package/src/app/api/tasks/import/github/route.ts +2 -1
  66. package/src/app/api/tasks/route.ts +79 -91
  67. package/src/app/api/wallets/[id]/approve/route.ts +2 -1
  68. package/src/app/api/wallets/[id]/route.ts +13 -19
  69. package/src/app/api/wallets/[id]/send/route.ts +2 -1
  70. package/src/app/api/wallets/route.ts +2 -1
  71. package/src/app/api/webhooks/[id]/route.ts +2 -1
  72. package/src/app/api/webhooks/route.test.ts +3 -1
  73. package/src/app/page.tsx +23 -331
  74. package/src/cli/index.js +19 -0
  75. package/src/cli/index.ts +38 -7
  76. package/src/cli/spec.js +9 -0
  77. package/src/components/activity/activity-feed.tsx +7 -4
  78. package/src/components/agents/agent-card.tsx +32 -6
  79. package/src/components/agents/agent-chat-list.tsx +55 -22
  80. package/src/components/agents/agent-files-editor.tsx +3 -2
  81. package/src/components/agents/agent-sheet.tsx +123 -22
  82. package/src/components/agents/inspector-panel.tsx +1 -1
  83. package/src/components/agents/openclaw-skills-panel.tsx +2 -1
  84. package/src/components/agents/trash-list.tsx +1 -1
  85. package/src/components/auth/access-key-gate.tsx +8 -2
  86. package/src/components/auth/setup-wizard.tsx +10 -9
  87. package/src/components/auth/user-picker.tsx +3 -2
  88. package/src/components/chat/chat-area.tsx +20 -1
  89. package/src/components/chat/chat-card.tsx +18 -3
  90. package/src/components/chat/chat-header.tsx +24 -4
  91. package/src/components/chat/chat-list.tsx +2 -11
  92. package/src/components/chat/heartbeat-history-panel.tsx +2 -1
  93. package/src/components/chat/message-bubble.tsx +45 -6
  94. package/src/components/chat/message-list.tsx +280 -145
  95. package/src/components/chat/streaming-bubble.tsx +217 -60
  96. package/src/components/chat/swarm-panel.test.ts +274 -0
  97. package/src/components/chat/swarm-panel.tsx +410 -0
  98. package/src/components/chat/swarm-status-card.tsx +346 -0
  99. package/src/components/chat/tool-call-bubble.tsx +48 -23
  100. package/src/components/chatrooms/chatroom-list.tsx +8 -5
  101. package/src/components/chatrooms/chatroom-message.tsx +10 -7
  102. package/src/components/chatrooms/chatroom-view.tsx +12 -9
  103. package/src/components/connectors/connector-health.tsx +6 -4
  104. package/src/components/connectors/connector-list.tsx +16 -11
  105. package/src/components/connectors/connector-sheet.tsx +12 -6
  106. package/src/components/home/home-view.tsx +38 -24
  107. package/src/components/input/chat-input.tsx +10 -1
  108. package/src/components/layout/app-layout.tsx +2 -38
  109. package/src/components/layout/sheet-layer.tsx +50 -0
  110. package/src/components/mcp-servers/mcp-server-list.tsx +37 -5
  111. package/src/components/mcp-servers/mcp-server-sheet.tsx +12 -2
  112. package/src/components/plugins/plugin-list.tsx +8 -4
  113. package/src/components/plugins/plugin-sheet.tsx +2 -1
  114. package/src/components/providers/provider-list.tsx +3 -2
  115. package/src/components/providers/provider-sheet.tsx +2 -1
  116. package/src/components/runs/run-list.tsx +11 -7
  117. package/src/components/schedules/schedule-card.tsx +5 -3
  118. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  119. package/src/components/shared/attachment-chip.tsx +19 -3
  120. package/src/components/shared/notification-center.tsx +6 -3
  121. package/src/components/shared/settings/plugin-manager.tsx +3 -2
  122. package/src/components/shared/settings/section-embedding.tsx +2 -1
  123. package/src/components/shared/settings/section-orchestrator.tsx +2 -1
  124. package/src/components/shared/settings/section-user-preferences.tsx +107 -0
  125. package/src/components/shared/settings/settings-page.tsx +13 -9
  126. package/src/components/skills/clawhub-browser.tsx +15 -4
  127. package/src/components/skills/skill-list.tsx +15 -4
  128. package/src/components/tasks/approvals-panel.tsx +2 -1
  129. package/src/components/tasks/task-board.tsx +35 -37
  130. package/src/components/tasks/task-sheet.tsx +4 -3
  131. package/src/components/ui/full-screen-loader.tsx +164 -0
  132. package/src/components/wallets/wallet-approval-dialog.tsx +2 -1
  133. package/src/components/wallets/wallet-panel.tsx +6 -5
  134. package/src/components/wallets/wallet-section.tsx +3 -2
  135. package/src/components/webhooks/webhook-list.tsx +4 -5
  136. package/src/components/webhooks/webhook-sheet.tsx +6 -6
  137. package/src/hooks/use-app-bootstrap.ts +202 -0
  138. package/src/hooks/use-mounted-ref.ts +14 -0
  139. package/src/hooks/use-now.ts +31 -0
  140. package/src/hooks/use-openclaw-gateway.ts +2 -1
  141. package/src/instrumentation.ts +20 -8
  142. package/src/lib/agent-default-tools.test.ts +52 -0
  143. package/src/lib/agent-default-tools.ts +40 -0
  144. package/src/lib/api-client.test.ts +21 -0
  145. package/src/lib/api-client.ts +6 -11
  146. package/src/lib/canvas-content.test.ts +360 -0
  147. package/src/lib/chat-streaming-state.test.ts +49 -2
  148. package/src/lib/chat-streaming-state.ts +26 -10
  149. package/src/lib/fetch-timeout.test.ts +54 -0
  150. package/src/lib/fetch-timeout.ts +60 -3
  151. package/src/lib/live-tool-events.test.ts +77 -0
  152. package/src/lib/live-tool-events.ts +73 -0
  153. package/src/lib/local-observability.test.ts +2 -2
  154. package/src/lib/openclaw-endpoint.test.ts +1 -1
  155. package/src/lib/providers/anthropic.ts +12 -16
  156. package/src/lib/providers/index.ts +4 -2
  157. package/src/lib/providers/ollama.ts +9 -6
  158. package/src/lib/providers/openai.ts +11 -14
  159. package/src/lib/runtime-env.test.ts +8 -8
  160. package/src/lib/schedule-dedupe-advanced.test.ts +2 -2
  161. package/src/lib/schedule-dedupe.test.ts +1 -1
  162. package/src/lib/schedule-dedupe.ts +3 -2
  163. package/src/lib/server/agent-thread-session.test.ts +6 -6
  164. package/src/lib/server/agent-thread-session.ts +6 -9
  165. package/src/lib/server/alert-dispatch.ts +2 -1
  166. package/src/lib/server/api-routes.test.ts +6 -6
  167. package/src/lib/server/approval-connector-notify.test.ts +4 -4
  168. package/src/lib/server/approvals-auto-approve.test.ts +29 -29
  169. package/src/lib/server/approvals.test.ts +317 -0
  170. package/src/lib/server/approvals.ts +5 -4
  171. package/src/lib/server/autonomy-runtime.test.ts +11 -11
  172. package/src/lib/server/browser-state.ts +2 -2
  173. package/src/lib/server/capability-router.test.ts +1 -1
  174. package/src/lib/server/capability-router.ts +3 -2
  175. package/src/lib/server/chat-execution-advanced.test.ts +15 -2
  176. package/src/lib/server/chat-execution-connector-delivery.ts +67 -0
  177. package/src/lib/server/chat-execution-disabled.test.ts +3 -3
  178. package/src/lib/server/chat-execution-eval-history.test.ts +3 -3
  179. package/src/lib/server/chat-execution-heartbeat.test.ts +42 -1
  180. package/src/lib/server/chat-execution-session-sync.test.ts +119 -0
  181. package/src/lib/server/chat-execution-tool-events.ts +116 -0
  182. package/src/lib/server/chat-execution-utils.test.ts +479 -0
  183. package/src/lib/server/chat-execution-utils.ts +533 -0
  184. package/src/lib/server/chat-execution.ts +153 -748
  185. package/src/lib/server/chat-streaming-utils.ts +174 -0
  186. package/src/lib/server/chat-turn-tool-routing.ts +310 -0
  187. package/src/lib/server/chatroom-session-persistence.test.ts +2 -2
  188. package/src/lib/server/clawhub-client.ts +2 -1
  189. package/src/lib/server/collection-helpers.test.ts +92 -0
  190. package/src/lib/server/collection-helpers.ts +25 -3
  191. package/src/lib/server/connectors/access.ts +146 -0
  192. package/src/lib/server/connectors/bluebubbles.test.ts +1 -1
  193. package/src/lib/server/connectors/bluebubbles.ts +4 -4
  194. package/src/lib/server/connectors/commands.ts +367 -0
  195. package/src/lib/server/connectors/connector-routing.test.ts +4 -4
  196. package/src/lib/server/connectors/delivery.ts +142 -0
  197. package/src/lib/server/connectors/discord.ts +37 -40
  198. package/src/lib/server/connectors/email.ts +11 -10
  199. package/src/lib/server/connectors/googlechat.ts +4 -4
  200. package/src/lib/server/connectors/inbound-audio-transcription.ts +2 -1
  201. package/src/lib/server/connectors/ingress-delivery.ts +23 -0
  202. package/src/lib/server/connectors/manager-roundtrip.test.ts +300 -0
  203. package/src/lib/server/connectors/manager.test.ts +352 -77
  204. package/src/lib/server/connectors/manager.ts +134 -673
  205. package/src/lib/server/connectors/matrix.ts +4 -4
  206. package/src/lib/server/connectors/message-sentinel.ts +7 -0
  207. package/src/lib/server/connectors/openclaw.test.ts +1 -1
  208. package/src/lib/server/connectors/openclaw.ts +8 -10
  209. package/src/lib/server/connectors/outbox.test.ts +192 -0
  210. package/src/lib/server/connectors/outbox.ts +369 -0
  211. package/src/lib/server/connectors/pairing.test.ts +18 -1
  212. package/src/lib/server/connectors/pairing.ts +49 -4
  213. package/src/lib/server/connectors/policy.ts +9 -3
  214. package/src/lib/server/connectors/reconnect-state.ts +71 -0
  215. package/src/lib/server/connectors/response-media.ts +256 -0
  216. package/src/lib/server/connectors/runtime-state.ts +67 -0
  217. package/src/lib/server/connectors/session.test.ts +357 -0
  218. package/src/lib/server/connectors/session.ts +422 -0
  219. package/src/lib/server/connectors/signal.ts +7 -7
  220. package/src/lib/server/connectors/slack.ts +43 -43
  221. package/src/lib/server/connectors/teams.ts +4 -4
  222. package/src/lib/server/connectors/telegram.ts +37 -43
  223. package/src/lib/server/connectors/types.ts +31 -1
  224. package/src/lib/server/connectors/whatsapp.test.ts +108 -0
  225. package/src/lib/server/connectors/whatsapp.ts +106 -34
  226. package/src/lib/server/context-manager.test.ts +409 -0
  227. package/src/lib/server/cost.test.ts +1 -1
  228. package/src/lib/server/daemon-policy.ts +78 -0
  229. package/src/lib/server/daemon-state-connectors.test.ts +167 -0
  230. package/src/lib/server/daemon-state.test.ts +283 -55
  231. package/src/lib/server/daemon-state.ts +106 -109
  232. package/src/lib/server/data-dir.test.ts +5 -5
  233. package/src/lib/server/data-dir.ts +4 -0
  234. package/src/lib/server/delegation-jobs-advanced.test.ts +1 -1
  235. package/src/lib/server/delegation-jobs.test.ts +87 -0
  236. package/src/lib/server/delegation-jobs.ts +42 -48
  237. package/src/lib/server/devserver-launch.ts +1 -1
  238. package/src/lib/server/document-utils.ts +7 -9
  239. package/src/lib/server/elevenlabs.ts +2 -1
  240. package/src/lib/server/embeddings.test.ts +105 -0
  241. package/src/lib/server/ethereum.ts +3 -2
  242. package/src/lib/server/eval/agent-regression.ts +3 -2
  243. package/src/lib/server/eval/runner.ts +2 -1
  244. package/src/lib/server/eval/scorer.ts +2 -1
  245. package/src/lib/server/evm-swap.ts +2 -1
  246. package/src/lib/server/gateway/protocol.test.ts +1 -1
  247. package/src/lib/server/guardian.ts +2 -1
  248. package/src/lib/server/heartbeat-blocked-suppression.test.ts +151 -0
  249. package/src/lib/server/heartbeat-service-timer.test.ts +6 -6
  250. package/src/lib/server/heartbeat-service.test.ts +406 -0
  251. package/src/lib/server/heartbeat-service.ts +54 -7
  252. package/src/lib/server/heartbeat-wake.test.ts +19 -0
  253. package/src/lib/server/heartbeat-wake.ts +17 -16
  254. package/src/lib/server/integrity-monitor.test.ts +149 -0
  255. package/src/lib/server/json-utils.ts +22 -0
  256. package/src/lib/server/knowledge-db.test.ts +13 -13
  257. package/src/lib/server/link-understanding.ts +2 -1
  258. package/src/lib/server/llm-response-cache.test.ts +1 -1
  259. package/src/lib/server/main-agent-loop-advanced.test.ts +65 -3
  260. package/src/lib/server/main-agent-loop.test.ts +6 -6
  261. package/src/lib/server/main-agent-loop.ts +21 -7
  262. package/src/lib/server/mcp-client.test.ts +1 -1
  263. package/src/lib/server/mcp-conformance.test.ts +1 -1
  264. package/src/lib/server/mcp-conformance.ts +3 -2
  265. package/src/lib/server/memory-consolidation.ts +2 -1
  266. package/src/lib/server/memory-db.test.ts +485 -0
  267. package/src/lib/server/memory-db.ts +39 -26
  268. package/src/lib/server/memory-graph.test.ts +2 -2
  269. package/src/lib/server/memory-policy.test.ts +7 -7
  270. package/src/lib/server/memory-retrieval.test.ts +1 -1
  271. package/src/lib/server/openclaw-config-sync.ts +2 -1
  272. package/src/lib/server/openclaw-deploy.test.ts +1 -1
  273. package/src/lib/server/openclaw-deploy.ts +8 -12
  274. package/src/lib/server/openclaw-exec-config.ts +2 -1
  275. package/src/lib/server/openclaw-gateway.ts +6 -7
  276. package/src/lib/server/openclaw-skills-normalize.ts +2 -1
  277. package/src/lib/server/openclaw-sync.ts +7 -5
  278. package/src/lib/server/orchestrator-lg-structure.test.ts +17 -0
  279. package/src/lib/server/orchestrator-lg.ts +199 -327
  280. package/src/lib/server/path-utils.ts +31 -0
  281. package/src/lib/server/perf.ts +161 -0
  282. package/src/lib/server/plugins-approval-guidance.ts +115 -0
  283. package/src/lib/server/plugins.test.ts +1 -1
  284. package/src/lib/server/plugins.ts +22 -132
  285. package/src/lib/server/process-manager.ts +5 -8
  286. package/src/lib/server/provider-health.test.ts +137 -0
  287. package/src/lib/server/provider-health.ts +3 -3
  288. package/src/lib/server/provider-model-discovery.ts +3 -12
  289. package/src/lib/server/queue-followups.test.ts +9 -9
  290. package/src/lib/server/queue-reconcile.test.ts +2 -2
  291. package/src/lib/server/queue-recovery.test.ts +269 -0
  292. package/src/lib/server/queue.test.ts +570 -0
  293. package/src/lib/server/queue.ts +62 -455
  294. package/src/lib/server/resolve-image.ts +30 -0
  295. package/src/lib/server/runtime-settings.test.ts +4 -4
  296. package/src/lib/server/runtime-storage-write-paths.test.ts +60 -0
  297. package/src/lib/server/schedule-normalization.test.ts +279 -0
  298. package/src/lib/server/schedule-service.ts +263 -0
  299. package/src/lib/server/scheduler.ts +17 -74
  300. package/src/lib/server/session-mailbox.test.ts +191 -0
  301. package/src/lib/server/session-run-manager.test.ts +640 -0
  302. package/src/lib/server/session-run-manager.ts +59 -15
  303. package/src/lib/server/session-tools/autonomy-tools.test.ts +20 -20
  304. package/src/lib/server/session-tools/calendar.ts +2 -1
  305. package/src/lib/server/session-tools/canvas.ts +2 -1
  306. package/src/lib/server/session-tools/chatroom.ts +2 -1
  307. package/src/lib/server/session-tools/connector.ts +26 -28
  308. package/src/lib/server/session-tools/context-mgmt.ts +3 -2
  309. package/src/lib/server/session-tools/crawl.ts +4 -3
  310. package/src/lib/server/session-tools/crud.ts +105 -324
  311. package/src/lib/server/session-tools/delegate-fallback.test.ts +9 -9
  312. package/src/lib/server/session-tools/delegate.ts +6 -8
  313. package/src/lib/server/session-tools/discovery-approvals.test.ts +15 -15
  314. package/src/lib/server/session-tools/discovery.ts +4 -3
  315. package/src/lib/server/session-tools/document.ts +2 -1
  316. package/src/lib/server/session-tools/email.ts +2 -1
  317. package/src/lib/server/session-tools/extract.ts +2 -1
  318. package/src/lib/server/session-tools/file.ts +4 -3
  319. package/src/lib/server/session-tools/http.ts +2 -1
  320. package/src/lib/server/session-tools/human-loop.ts +2 -1
  321. package/src/lib/server/session-tools/image-gen.ts +4 -3
  322. package/src/lib/server/session-tools/index.ts +26 -30
  323. package/src/lib/server/session-tools/mailbox.ts +2 -1
  324. package/src/lib/server/session-tools/manage-connectors.test.ts +4 -4
  325. package/src/lib/server/session-tools/manage-schedules.test.ts +12 -12
  326. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +5 -5
  327. package/src/lib/server/session-tools/manage-tasks.test.ts +2 -2
  328. package/src/lib/server/session-tools/monitor.ts +2 -1
  329. package/src/lib/server/session-tools/platform.ts +2 -1
  330. package/src/lib/server/session-tools/plugin-creator.ts +2 -1
  331. package/src/lib/server/session-tools/replicate.ts +3 -2
  332. package/src/lib/server/session-tools/session-tools-wiring.test.ts +6 -6
  333. package/src/lib/server/session-tools/shell.ts +4 -9
  334. package/src/lib/server/session-tools/subagent.ts +322 -170
  335. package/src/lib/server/session-tools/table.ts +6 -5
  336. package/src/lib/server/session-tools/wallet-tool.test.ts +3 -3
  337. package/src/lib/server/session-tools/wallet.ts +7 -6
  338. package/src/lib/server/session-tools/web-browser-config.test.ts +1 -0
  339. package/src/lib/server/session-tools/web-utils.ts +317 -0
  340. package/src/lib/server/session-tools/web.ts +62 -328
  341. package/src/lib/server/skill-prompt-budget.test.ts +1 -1
  342. package/src/lib/server/skills-normalize.ts +2 -1
  343. package/src/lib/server/storage-item-access.test.ts +302 -0
  344. package/src/lib/server/storage.ts +366 -314
  345. package/src/lib/server/stream-agent-chat.test.ts +82 -3
  346. package/src/lib/server/stream-agent-chat.ts +146 -510
  347. package/src/lib/server/stream-continuation.ts +412 -0
  348. package/src/lib/server/subagent-lineage.test.ts +647 -0
  349. package/src/lib/server/subagent-lineage.ts +435 -0
  350. package/src/lib/server/subagent-runtime.test.ts +484 -0
  351. package/src/lib/server/subagent-runtime.ts +419 -0
  352. package/src/lib/server/subagent-swarm.test.ts +391 -0
  353. package/src/lib/server/subagent-swarm.ts +564 -0
  354. package/src/lib/server/system-events.ts +3 -3
  355. package/src/lib/server/task-followups.test.ts +491 -0
  356. package/src/lib/server/task-followups.ts +391 -0
  357. package/src/lib/server/task-lifecycle.test.ts +205 -0
  358. package/src/lib/server/task-lifecycle.ts +200 -0
  359. package/src/lib/server/task-quality-gate.test.ts +1 -1
  360. package/src/lib/server/task-resume.ts +208 -0
  361. package/src/lib/server/task-service.test.ts +108 -0
  362. package/src/lib/server/task-service.ts +264 -0
  363. package/src/lib/server/task-validation.test.ts +1 -1
  364. package/src/lib/server/test-utils/run-with-temp-data-dir.ts +42 -0
  365. package/src/lib/server/tool-capability-policy.test.ts +2 -2
  366. package/src/lib/server/tool-capability-policy.ts +3 -2
  367. package/src/lib/server/tool-planning.ts +2 -1
  368. package/src/lib/server/tool-retry.ts +2 -3
  369. package/src/lib/server/wake-dispatcher.test.ts +303 -0
  370. package/src/lib/server/wake-dispatcher.ts +318 -0
  371. package/src/lib/server/wake-mode.test.ts +161 -0
  372. package/src/lib/server/wake-mode.ts +174 -0
  373. package/src/lib/server/wallet-service.ts +8 -9
  374. package/src/lib/server/watch-jobs.ts +2 -1
  375. package/src/lib/server/workspace-context.ts +2 -2
  376. package/src/lib/shared-utils.test.ts +142 -0
  377. package/src/lib/shared-utils.ts +62 -0
  378. package/src/lib/tool-event-summary.ts +2 -1
  379. package/src/lib/view-routes.test.ts +100 -0
  380. package/src/lib/wallet.test.ts +322 -6
  381. package/src/proxy.test.ts +4 -4
  382. package/src/proxy.ts +2 -3
  383. package/src/stores/set-if-changed.ts +40 -0
  384. package/src/stores/slices/agent-slice.ts +111 -0
  385. package/src/stores/slices/auth-slice.ts +25 -0
  386. package/src/stores/slices/data-slice.ts +301 -0
  387. package/src/stores/slices/index.ts +7 -0
  388. package/src/stores/slices/session-slice.ts +112 -0
  389. package/src/stores/slices/task-slice.ts +63 -0
  390. package/src/stores/slices/ui-slice.ts +192 -0
  391. package/src/stores/use-app-store.ts +17 -822
  392. package/src/stores/use-approval-store.ts +2 -1
  393. package/src/stores/use-chat-store.ts +8 -1
  394. package/src/types/index.ts +10 -0
@@ -1,26 +1,26 @@
1
1
  import { genId } from '@/lib/id'
2
+ import { hmrSingleton } from '@/lib/shared-utils'
2
3
  import type { DelegationJobArtifact, DelegationJobCheckpoint, DelegationJobRecord, DelegationJobStatus } from '@/types'
3
- import { loadDelegationJobs, upsertDelegationJob } from './storage'
4
+ import { loadDelegationJobs, upsertDelegationJob, patchDelegationJob } from './storage'
4
5
  import { notify } from './ws-hub'
5
6
 
6
7
  interface DelegationRuntimeHandle {
7
8
  cancel?: () => void
8
9
  }
9
10
 
10
- const runtimeKey = '__swarmclaw_delegation_job_runtime__' as const
11
- const runtimeScope = globalThis as typeof globalThis & {
12
- [runtimeKey]?: Map<string, DelegationRuntimeHandle>
13
- }
14
- const runtimeHandles = runtimeScope[runtimeKey] ?? (runtimeScope[runtimeKey] = new Map())
15
-
16
- function now() {
17
- return Date.now()
18
- }
11
+ const runtimeHandles = hmrSingleton('__swarmclaw_delegation_job_runtime__', () => new Map<string, DelegationRuntimeHandle>())
19
12
 
20
13
  function isTerminalStatus(status: DelegationJobStatus | null | undefined): boolean {
21
14
  return status === 'completed' || status === 'failed' || status === 'cancelled'
22
15
  }
23
16
 
17
+ function appendCheckpoint(
18
+ existing: DelegationJobCheckpoint[] | undefined,
19
+ checkpoint: DelegationJobCheckpoint,
20
+ ): DelegationJobCheckpoint[] {
21
+ return [...(existing || []), checkpoint].slice(-24)
22
+ }
23
+
24
24
  function notifyDelegationJobsChanged() {
25
25
  notify('delegation_jobs')
26
26
  }
@@ -37,7 +37,7 @@ export interface CreateDelegationJobInput {
37
37
  }
38
38
 
39
39
  export function createDelegationJob(input: CreateDelegationJobInput): DelegationJobRecord {
40
- const createdAt = now()
40
+ const createdAt = Date.now()
41
41
  const job: DelegationJobRecord = {
42
42
  id: genId(10),
43
43
  kind: input.kind,
@@ -92,16 +92,16 @@ export function updateDelegationJob(
92
92
  id: string,
93
93
  patch: Partial<DelegationJobRecord>,
94
94
  ): DelegationJobRecord | null {
95
- const current = getDelegationJob(id)
96
- if (!current) return null
97
- const next: DelegationJobRecord = {
98
- ...current,
99
- ...patch,
100
- updatedAt: now(),
101
- }
102
- upsertDelegationJob(id, next)
103
- notifyDelegationJobsChanged()
104
- return next
95
+ const result = patchDelegationJob(id, (current) => {
96
+ if (!current) return null
97
+ return {
98
+ ...current,
99
+ ...patch,
100
+ updatedAt: Date.now(),
101
+ }
102
+ })
103
+ if (result) notifyDelegationJobsChanged()
104
+ return result
105
105
  }
106
106
 
107
107
  export function appendDelegationCheckpoint(
@@ -114,10 +114,9 @@ export function appendDelegationCheckpoint(
114
114
  if (isTerminalStatus(current.status) && status && status !== current.status) {
115
115
  return current
116
116
  }
117
- const checkpoints = [...(current.checkpoints || []), { at: now(), note, status }]
118
117
  return updateDelegationJob(id, {
119
118
  status: isTerminalStatus(current.status) ? current.status : (status ?? current.status),
120
- checkpoints: checkpoints.slice(-24),
119
+ checkpoints: appendCheckpoint(current.checkpoints, { at: Date.now(), note, status }),
121
120
  })
122
121
  }
123
122
 
@@ -128,7 +127,7 @@ export function startDelegationJob(id: string, patch?: Partial<DelegationJobReco
128
127
  return updateDelegationJob(id, {
129
128
  ...patch,
130
129
  status: 'running',
131
- startedAt: now(),
130
+ startedAt: Date.now(),
132
131
  })
133
132
  }
134
133
 
@@ -147,7 +146,7 @@ export function completeDelegationJob(
147
146
  result,
148
147
  resultPreview: result.slice(0, 1000),
149
148
  error: null,
150
- completedAt: now(),
149
+ completedAt: Date.now(),
151
150
  })
152
151
  }
153
152
 
@@ -160,7 +159,7 @@ export function failDelegationJob(id: string, error: string, patch?: Partial<Del
160
159
  ...patch,
161
160
  status: 'failed',
162
161
  error,
163
- completedAt: now(),
162
+ completedAt: Date.now(),
164
163
  })
165
164
  }
166
165
 
@@ -174,21 +173,20 @@ export function cancelDelegationJob(id: string): DelegationJobRecord | null {
174
173
  } catch {
175
174
  // best-effort cancel
176
175
  }
177
- runtimeHandles.delete(id)
178
- const checkpoint: DelegationJobCheckpoint = {
179
- at: now(),
180
- note: 'Job cancelled',
181
- status: 'cancelled',
182
- }
183
- return updateDelegationJob(id, {
176
+ // Commit state before deleting handle — if the update fails, the handle
177
+ // stays intact for a future retry rather than being orphaned.
178
+ const result = updateDelegationJob(id, {
184
179
  status: 'cancelled',
185
- completedAt: now(),
180
+ completedAt: Date.now(),
186
181
  error: null,
187
- checkpoints: [
188
- ...(current.checkpoints || []),
189
- checkpoint,
190
- ].slice(-24),
182
+ checkpoints: appendCheckpoint(current.checkpoints, {
183
+ at: Date.now(),
184
+ note: 'Job cancelled',
185
+ status: 'cancelled',
186
+ }),
191
187
  })
188
+ runtimeHandles.delete(id)
189
+ return result
192
190
  }
193
191
 
194
192
  export function cancelDelegationJobsForParentSession(
@@ -206,16 +204,12 @@ export function cancelDelegationJobsForParentSession(
206
204
  const checkpoints = Array.isArray(next.checkpoints) ? next.checkpoints : []
207
205
  const last = checkpoints[checkpoints.length - 1]
208
206
  if (!last || last.note !== note) {
209
- const checkpoint: DelegationJobCheckpoint = {
210
- at: now(),
211
- note,
212
- status: 'cancelled',
213
- }
214
207
  updateDelegationJob(job.id, {
215
- checkpoints: [
216
- ...checkpoints,
217
- checkpoint,
218
- ].slice(-24),
208
+ checkpoints: appendCheckpoint(next.checkpoints, {
209
+ at: Date.now(),
210
+ note,
211
+ status: 'cancelled',
212
+ }),
219
213
  })
220
214
  }
221
215
  }
@@ -235,7 +229,7 @@ export function appendDelegationArtifacts(id: string, artifacts: DelegationJobAr
235
229
  }
236
230
 
237
231
  export function recoverStaleDelegationJobs(maxAgeMs = 15 * 60_000): number {
238
- const threshold = now() - maxAgeMs
232
+ const threshold = Date.now() - maxAgeMs
239
233
  const stale = listDelegationJobs().filter((job) =>
240
234
  (job.status === 'queued' || job.status === 'running')
241
235
  && !runtimeHandles.has(job.id)
@@ -19,7 +19,7 @@ export interface DevServerLaunchResolution {
19
19
  const NEXT_CONFIG_FILES = [
20
20
  'next.config.js',
21
21
  'next.config.mjs',
22
- 'next.config.ts',
22
+ 'next.config',
23
23
  ]
24
24
 
25
25
  function readPackageJson(dir: string): PackageJsonLike | null {
@@ -2,11 +2,13 @@ import fs from 'fs'
2
2
  import path from 'path'
3
3
  import { spawnSync } from 'child_process'
4
4
  import * as cheerio from 'cheerio'
5
+ import { dedup } from '@/lib/shared-utils'
5
6
  import { findBinaryOnPath } from './session-tools/context'
7
+ import { safeJsonParse } from './json-utils'
6
8
 
7
9
  const TEXT_EXTENSIONS = new Set([
8
10
  '.txt', '.md', '.markdown', '.json', '.jsonl', '.csv', '.tsv',
9
- '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs',
11
+ '', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs',
10
12
  '.java', '.yaml', '.yml', '.sql', '.xml', '.css', '.scss', '.html', '.htm',
11
13
  ])
12
14
  const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp', '.tif', '.tiff'])
@@ -113,7 +115,7 @@ function matrixToTable(name: string, matrix: string[][]): StructuredTable {
113
115
  }
114
116
 
115
117
  function objectsToTable(name: string, rows: Array<Record<string, unknown>>): StructuredTable {
116
- const headers = Array.from(new Set(rows.flatMap((row) => Object.keys(row))))
118
+ const headers = dedup(rows.flatMap((row) => Object.keys(row)))
117
119
  const normalizedRows = rows.map((row) => {
118
120
  const out: Record<string, unknown> = {}
119
121
  for (const header of headers) out[header] = normalizeScalar(row[header])
@@ -264,13 +266,9 @@ export async function extractDocumentArtifact(filePath: string, options?: { maxC
264
266
  const raw = fs.readFileSync(resolved, 'utf-8')
265
267
  text = raw
266
268
  method = 'json'
267
- try {
268
- const parsed = JSON.parse(raw)
269
- if (Array.isArray(parsed) && parsed.every((row) => row && typeof row === 'object' && !Array.isArray(row))) {
270
- tables = [objectsToTable(path.basename(resolved), parsed as Array<Record<string, unknown>>)]
271
- }
272
- } catch {
273
- // keep raw json text only
269
+ const parsed = safeJsonParse<unknown>(raw, null)
270
+ if (Array.isArray(parsed) && parsed.every((row) => row && typeof row === 'object' && !Array.isArray(row))) {
271
+ tables = [objectsToTable(path.basename(resolved), parsed as Array<Record<string, unknown>>)]
274
272
  }
275
273
  } else if (ext === '.html' || ext === '.htm') {
276
274
  const html = fs.readFileSync(resolved, 'utf-8')
@@ -1,4 +1,5 @@
1
1
  import { decryptKey, loadSettings, loadSecrets } from './storage'
2
+ import { dedup } from '@/lib/shared-utils'
2
3
 
3
4
  const DEFAULT_VOICE_ID = 'JBFqnCBsd6RMkjVDRZzb'
4
5
  const DEFAULT_MODEL_ID = 'eleven_multilingual_v2'
@@ -56,7 +57,7 @@ function resolveElevenLabsApiKeyCandidates(): string[] {
56
57
  }
57
58
  }
58
59
 
59
- return [...new Set(candidates)]
60
+ return dedup(candidates)
60
61
  }
61
62
 
62
63
  function shouldRetryWithFallbackVoice(voiceId: string, errBody: string): boolean {
@@ -0,0 +1,105 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ // embeddings.ts exports pure utility functions (cosineSimilarity, serialize/deserialize)
5
+ // that don't need DATA_DIR. getEmbedding needs storage — skip that, test pure logic.
6
+
7
+ // Direct import is safe for the pure math/serialization functions since the module
8
+ // only touches storage lazily in getEmbedding().
9
+ // However the top-level import of ./storage will trigger DB init,
10
+ // so we use dynamic import with SWARMCLAW_BUILD_MODE.
11
+
12
+ let embeddings: typeof import('./embeddings')
13
+
14
+ import { before } from 'node:test'
15
+
16
+ before(async () => {
17
+ process.env.SWARMCLAW_BUILD_MODE = '1'
18
+ embeddings = await import('./embeddings')
19
+ })
20
+
21
+ describe('embeddings - cosineSimilarity', () => {
22
+ it('identical vectors have similarity 1', () => {
23
+ const v = [1, 2, 3, 4, 5]
24
+ const sim = embeddings.cosineSimilarity(v, v)
25
+ assert.ok(Math.abs(sim - 1) < 1e-10, `expected ~1, got ${sim}`)
26
+ })
27
+
28
+ it('orthogonal vectors have similarity 0', () => {
29
+ const a = [1, 0, 0]
30
+ const b = [0, 1, 0]
31
+ const sim = embeddings.cosineSimilarity(a, b)
32
+ assert.ok(Math.abs(sim) < 1e-10, `expected ~0, got ${sim}`)
33
+ })
34
+
35
+ it('opposite vectors have similarity -1', () => {
36
+ const a = [1, 2, 3]
37
+ const b = [-1, -2, -3]
38
+ const sim = embeddings.cosineSimilarity(a, b)
39
+ assert.ok(Math.abs(sim + 1) < 1e-10, `expected ~-1, got ${sim}`)
40
+ })
41
+
42
+ it('different-length vectors return 0', () => {
43
+ const sim = embeddings.cosineSimilarity([1, 2], [1, 2, 3])
44
+ assert.equal(sim, 0)
45
+ })
46
+
47
+ it('zero vector returns 0', () => {
48
+ const sim = embeddings.cosineSimilarity([0, 0, 0], [1, 2, 3])
49
+ assert.equal(sim, 0)
50
+ })
51
+
52
+ it('both zero vectors return 0', () => {
53
+ const sim = embeddings.cosineSimilarity([0, 0], [0, 0])
54
+ assert.equal(sim, 0)
55
+ })
56
+
57
+ it('empty vectors return 0', () => {
58
+ const sim = embeddings.cosineSimilarity([], [])
59
+ assert.equal(sim, 0)
60
+ })
61
+ })
62
+
63
+ describe('embeddings - serialize/deserialize roundtrip', () => {
64
+ it('roundtrips a simple embedding', () => {
65
+ const original = [0.1, 0.2, 0.3, -0.5, 1.0]
66
+ const buf = embeddings.serializeEmbedding(original)
67
+ const restored = embeddings.deserializeEmbedding(buf)
68
+
69
+ assert.equal(restored.length, original.length)
70
+ for (let i = 0; i < original.length; i++) {
71
+ assert.ok(Math.abs(restored[i] - original[i]) < 1e-6, `index ${i}: ${restored[i]} vs ${original[i]}`)
72
+ }
73
+ })
74
+
75
+ it('roundtrips an empty embedding', () => {
76
+ const buf = embeddings.serializeEmbedding([])
77
+ const restored = embeddings.deserializeEmbedding(buf)
78
+ assert.equal(restored.length, 0)
79
+ })
80
+
81
+ it('roundtrips a large embedding (384 dimensions)', () => {
82
+ const original = Array.from({ length: 384 }, (_, i) => Math.sin(i) * 0.5)
83
+ const buf = embeddings.serializeEmbedding(original)
84
+ assert.equal(buf.byteLength, 384 * 4) // Float32 = 4 bytes each
85
+ const restored = embeddings.deserializeEmbedding(buf)
86
+ assert.equal(restored.length, 384)
87
+ for (let i = 0; i < original.length; i++) {
88
+ assert.ok(Math.abs(restored[i] - original[i]) < 1e-6)
89
+ }
90
+ })
91
+
92
+ it('serialize produces a Buffer', () => {
93
+ const buf = embeddings.serializeEmbedding([1, 2, 3])
94
+ assert.ok(Buffer.isBuffer(buf))
95
+ })
96
+
97
+ it('preserves special float values', () => {
98
+ const original = [0, -0, Infinity, -Infinity]
99
+ const buf = embeddings.serializeEmbedding(original)
100
+ const restored = embeddings.deserializeEmbedding(buf)
101
+ assert.equal(restored[0], 0)
102
+ assert.equal(restored[2], Infinity)
103
+ assert.equal(restored[3], -Infinity)
104
+ })
105
+ })
@@ -15,6 +15,7 @@ import {
15
15
  } from 'ethers'
16
16
 
17
17
  import { decryptKey, encryptKey } from './storage'
18
+ import { errorMessage } from '@/lib/shared-utils'
18
19
 
19
20
  export type EvmNetworkId = 'ethereum' | 'arbitrum' | 'base'
20
21
 
@@ -490,7 +491,7 @@ export async function simulateEthereumTransaction(
490
491
  `estimate gas on ${network.label}`,
491
492
  )).toString()
492
493
  } catch (err: unknown) {
493
- callError = err instanceof Error ? err.message : String(err)
494
+ callError = errorMessage(err)
494
495
  }
495
496
 
496
497
  try {
@@ -499,7 +500,7 @@ export async function simulateEthereumTransaction(
499
500
  `simulate transaction call on ${network.label}`,
500
501
  )
501
502
  } catch (err: unknown) {
502
- if (!callError) callError = err instanceof Error ? err.message : String(err)
503
+ if (!callError) callError = errorMessage(err)
503
504
  }
504
505
 
505
506
  return {
@@ -5,6 +5,7 @@ import { createHash } from 'node:crypto'
5
5
  import path from 'node:path'
6
6
  import { genId } from '@/lib/id'
7
7
  import type { ApprovalRequest, MessageToolEvent, Session } from '@/types'
8
+ import { dedup } from '@/lib/shared-utils'
8
9
  import { submitDecision } from '../approvals'
9
10
  import { executeSessionChatTurn, type ExecuteChatTurnResult } from '../chat-execution'
10
11
  import { WORKSPACE_DIR } from '../data-dir'
@@ -850,11 +851,11 @@ export function resolveRegressionPlugins(
850
851
  agent: Record<string, unknown>,
851
852
  pluginMode: RegressionPluginMode,
852
853
  ): RegressionPluginResolution {
853
- const requiredCanonical = Array.from(new Set(
854
+ const requiredCanonical = dedup(
854
855
  normalizePluginList(requiredPlugins)
855
856
  .map((plugin) => canonicalizePluginId(plugin))
856
857
  .filter(Boolean),
857
- ))
858
+ )
858
859
  if (pluginMode === 'scenario') {
859
860
  return {
860
861
  requiredPlugins: requiredCanonical,
@@ -9,6 +9,7 @@ import { loadSessions, saveSessions, loadAgents, loadCredentials, decryptKey } f
9
9
  import { executeSessionChatTurn } from '../chat-execution'
10
10
  import { WORKSPACE_DIR } from '../data-dir'
11
11
  import type { Session } from '@/types'
12
+ import { errorMessage } from '@/lib/shared-utils'
12
13
 
13
14
  export function resolveEvalSessionCwd(runId: string): string {
14
15
  const dir = path.join(WORKSPACE_DIR, 'evals', runId)
@@ -98,7 +99,7 @@ export async function runEvalScenario(scenarioId: string, agentId: string): Prom
98
99
  run.endedAt = Date.now()
99
100
  } catch (err: unknown) {
100
101
  run.status = 'failed'
101
- run.error = err instanceof Error ? err.message : String(err)
102
+ run.error = errorMessage(err)
102
103
  run.endedAt = Date.now()
103
104
  } finally {
104
105
  // Clean up eval session
@@ -1,5 +1,6 @@
1
1
  import type { ScoringCriterion, EvalCriterionResult } from './types'
2
2
  import type { MessageToolEvent } from '@/types'
3
+ import { errorMessage } from '@/lib/shared-utils'
3
4
 
4
5
  export async function scoreCriteria(
5
6
  criteria: ScoringCriterion[],
@@ -84,7 +85,7 @@ export async function scoreCriteria(
84
85
  criterion: criterion.name,
85
86
  score: 0,
86
87
  maxScore: criterion.weight,
87
- evidence: `LLM judge error: ${err instanceof Error ? err.message : String(err)}`,
88
+ evidence: `LLM judge error: ${errorMessage(err)}`,
88
89
  })
89
90
  }
90
91
  break
@@ -5,6 +5,7 @@ import type { AgentWallet, WalletAssetBalance } from '@/types'
5
5
 
6
6
  import { getEvmNetworkConfig, getProviderForNetwork, type EvmNetworkId } from './ethereum'
7
7
  import { getWalletPortfolioSnapshot } from './wallet-service'
8
+ import { errorMessage } from '@/lib/shared-utils'
8
9
 
9
10
  const PARASWAP_API_BASE = 'https://api.paraswap.io'
10
11
  const PARASWAP_VERSION = '6.2'
@@ -470,6 +471,6 @@ export async function prepareEvmSwapPlan(input: PrepareEvmSwapPlanInput): Promis
470
471
  }
471
472
 
472
473
  export function isLikelyRetryableSwapError(err: unknown): boolean {
473
- const message = err instanceof Error ? err.message : String(err)
474
+ const message = errorMessage(err)
474
475
  return /rate|price|slippage|expired|call exception|execution reverted|insufficient output/i.test(message)
475
476
  }
@@ -4,7 +4,7 @@ import {
4
4
  createGatewayRequestFrame,
5
5
  parseGatewayFrame,
6
6
  serializeGatewayFrame,
7
- } from './protocol.ts'
7
+ } from './protocol'
8
8
 
9
9
  test('gateway protocol parses request/response/event frames', () => {
10
10
  const req = parseGatewayFrame('{"type":"req","id":"1","method":"connect","params":{"foo":"bar"}}')
@@ -1,6 +1,7 @@
1
1
  import { execSync } from 'child_process'
2
2
  import path from 'path'
3
3
  import fs from 'fs'
4
+ import { errorMessage } from '@/lib/shared-utils'
4
5
 
5
6
  /**
6
7
  * OpenClaw Guardian — Auto-Rollback capability.
@@ -29,6 +30,6 @@ export function performGuardianRollback(cwd: string): { ok: boolean; reason?: st
29
30
  return { ok: true }
30
31
  } catch (err: unknown) {
31
32
  console.error('[guardian] Auto-rollback failed:', err)
32
- return { ok: false, reason: `Git operation failed: ${err instanceof Error ? err.message : String(err)}` }
33
+ return { ok: false, reason: `Git operation failed: ${errorMessage(err)}` }
33
34
  }
34
35
  }
@@ -0,0 +1,151 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+ import {
4
+ stripBlockedItems,
5
+ isHeartbeatContentEffectivelyEmpty,
6
+ buildAgentHeartbeatPrompt,
7
+ } from './heartbeat-service'
8
+
9
+ describe('heartbeat blocked-item suppression', () => {
10
+ describe('stripBlockedItems', () => {
11
+ it('removes checklist items marked (blocked, no update)', () => {
12
+ const input = [
13
+ '# Heartbeat Tasks',
14
+ '## Active',
15
+ '- [ ] Pull SWGOH roster data (blocked, no update)',
16
+ '- [ ] Send daily summary',
17
+ '## Completed',
18
+ '- [x] Do laundry',
19
+ ].join('\n')
20
+
21
+ const result = stripBlockedItems(input)
22
+
23
+ assert.ok(!result.includes('SWGOH'), 'blocked item should be stripped')
24
+ assert.ok(result.includes('Send daily summary'), 'non-blocked item should remain')
25
+ assert.ok(result.includes('Do laundry'), 'completed item should remain')
26
+ assert.ok(result.includes('# Heartbeat Tasks'), 'headers should remain')
27
+ })
28
+
29
+ it('removes items with various blocked markers', () => {
30
+ const input = [
31
+ '- [ ] Task A (blocked: awaiting input)',
32
+ '- [ ] Task B (Blocked, pending user decision)',
33
+ '- [ ] Task C (BLOCKED)',
34
+ '- [ ] Task D - normal task',
35
+ '* [ ] Task E (blocked, no update)',
36
+ ].join('\n')
37
+
38
+ const result = stripBlockedItems(input)
39
+
40
+ assert.ok(!result.includes('Task A'), 'blocked: variant stripped')
41
+ assert.ok(!result.includes('Task B'), 'Blocked, variant stripped')
42
+ assert.ok(!result.includes('Task C'), 'BLOCKED variant stripped')
43
+ assert.ok(result.includes('Task D'), 'non-blocked task preserved')
44
+ assert.ok(!result.includes('Task E'), 'asterisk list item stripped')
45
+ })
46
+
47
+ it('preserves non-list lines that mention "blocked"', () => {
48
+ const input = [
49
+ '## Notes',
50
+ 'Some items are blocked until user responds.',
51
+ '- [ ] Actual blocked task (blocked, no update)',
52
+ ].join('\n')
53
+
54
+ const result = stripBlockedItems(input)
55
+
56
+ assert.ok(result.includes('Some items are blocked'), 'prose mentioning blocked should stay')
57
+ assert.ok(!result.includes('Actual blocked task'), 'blocked list item should be stripped')
58
+ })
59
+
60
+ it('returns empty string for empty input', () => {
61
+ assert.equal(stripBlockedItems(''), '')
62
+ assert.equal(stripBlockedItems(null as unknown as string), '')
63
+ })
64
+
65
+ it('returns content unchanged when no blocked items', () => {
66
+ const input = '- [ ] Task A\n- [ ] Task B\n'
67
+ assert.equal(stripBlockedItems(input), input)
68
+ })
69
+ })
70
+
71
+ describe('blocked items + effectively empty', () => {
72
+ it('treats content with only blocked items as effectively empty', () => {
73
+ const input = [
74
+ '# Heartbeat Tasks',
75
+ '## Active',
76
+ '- [ ] Pull SWGOH data (blocked, no update)',
77
+ '## Completed',
78
+ ].join('\n')
79
+
80
+ const stripped = stripBlockedItems(input)
81
+ // After stripping, only headers remain — effectively empty
82
+ assert.equal(isHeartbeatContentEffectivelyEmpty(stripped), true)
83
+ })
84
+
85
+ it('treats content with blocked + active items as not empty', () => {
86
+ const input = [
87
+ '# Heartbeat Tasks',
88
+ '## Active',
89
+ '- [ ] Pull SWGOH data (blocked, no update)',
90
+ '- [ ] Send daily summary',
91
+ ].join('\n')
92
+
93
+ const stripped = stripBlockedItems(input)
94
+ assert.equal(isHeartbeatContentEffectivelyEmpty(stripped), false)
95
+ })
96
+ })
97
+
98
+ describe('buildAgentHeartbeatPrompt integration', () => {
99
+ it('does not include blocked items in the prompt sent to the LLM', () => {
100
+ const session = {
101
+ id: 'test-session',
102
+ cwd: '/tmp',
103
+ messages: [],
104
+ }
105
+ const agent = {
106
+ id: 'test-agent',
107
+ name: 'Test',
108
+ description: 'Test agent',
109
+ }
110
+
111
+ const heartbeatFileContent = [
112
+ '# Heartbeat Tasks',
113
+ '## Active',
114
+ '- [ ] Pull SWGOH roster data (blocked, no update)',
115
+ '- [ ] Check weather forecast',
116
+ '## Completed',
117
+ '- [x] Laundry done',
118
+ ].join('\n')
119
+
120
+ const prompt = buildAgentHeartbeatPrompt(session, agent, 'default prompt', heartbeatFileContent)
121
+
122
+ assert.ok(!prompt.includes('SWGOH'), 'blocked SWGOH task should not appear in prompt')
123
+ assert.ok(prompt.includes('Check weather forecast'), 'non-blocked task should appear')
124
+ assert.ok(prompt.includes('Laundry done'), 'completed task should appear')
125
+ })
126
+
127
+ it('produces no HEARTBEAT.md section when all active items are blocked', () => {
128
+ const session = {
129
+ id: 'test-session',
130
+ cwd: '/tmp',
131
+ messages: [],
132
+ }
133
+ const agent = {
134
+ id: 'test-agent',
135
+ name: 'Test',
136
+ description: 'Test agent',
137
+ }
138
+
139
+ const heartbeatFileContent = [
140
+ '# Heartbeat Tasks',
141
+ '## Active',
142
+ '- [ ] Task A (blocked, no update)',
143
+ '- [ ] Task B (blocked: awaiting user)',
144
+ ].join('\n')
145
+
146
+ const prompt = buildAgentHeartbeatPrompt(session, agent, 'default prompt', heartbeatFileContent)
147
+
148
+ assert.ok(!prompt.includes('HEARTBEAT.md contents:'), 'should not include HEARTBEAT.md section when all items are blocked')
149
+ })
150
+ })
151
+ })
@@ -37,9 +37,9 @@ describe('heartbeat-service scheduling', () => {
37
37
  it('does not fire periodic heartbeats for agents that are explicitly off', () => {
38
38
  const output = runWithTempDataDir(`
39
39
  const { setTimeout: delay } = await import('node:timers/promises')
40
- const storageMod = await import('./src/lib/server/storage.ts')
41
- const heartbeatMod = await import('./src/lib/server/heartbeat-service.ts')
42
- const runsMod = await import('./src/lib/server/session-run-manager.ts')
40
+ const storageMod = await import('./src/lib/server/storage')
41
+ const heartbeatMod = await import('./src/lib/server/heartbeat-service')
42
+ const runsMod = await import('./src/lib/server/session-run-manager')
43
43
  const storage = storageMod.default || storageMod['module.exports'] || storageMod
44
44
  const heartbeat = heartbeatMod.default || heartbeatMod['module.exports'] || heartbeatMod
45
45
  const runs = runsMod.default || runsMod['module.exports'] || runsMod
@@ -100,9 +100,9 @@ describe('heartbeat-service scheduling', () => {
100
100
  it('fires periodic heartbeats only after the service tick window when enabled', () => {
101
101
  const output = runWithTempDataDir(`
102
102
  const { setTimeout: delay } = await import('node:timers/promises')
103
- const storageMod = await import('./src/lib/server/storage.ts')
104
- const heartbeatMod = await import('./src/lib/server/heartbeat-service.ts')
105
- const runsMod = await import('./src/lib/server/session-run-manager.ts')
103
+ const storageMod = await import('./src/lib/server/storage')
104
+ const heartbeatMod = await import('./src/lib/server/heartbeat-service')
105
+ const runsMod = await import('./src/lib/server/session-run-manager')
106
106
  const storage = storageMod.default || storageMod['module.exports'] || storageMod
107
107
  const heartbeat = heartbeatMod.default || heartbeatMod['module.exports'] || heartbeatMod
108
108
  const runs = runsMod.default || runsMod['module.exports'] || runsMod