@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,406 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { after, before, describe, it } from 'node:test'
6
+
7
+ const originalEnv = {
8
+ DATA_DIR: process.env.DATA_DIR,
9
+ WORKSPACE_DIR: process.env.WORKSPACE_DIR,
10
+ SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
11
+ }
12
+
13
+ let tempDir = ''
14
+ let workspaceDir = ''
15
+ let mod: typeof import('./heartbeat-service')
16
+
17
+ before(async () => {
18
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-heartbeat-svc-'))
19
+ workspaceDir = path.join(tempDir, 'workspace')
20
+ fs.mkdirSync(workspaceDir, { recursive: true })
21
+ process.env.DATA_DIR = path.join(tempDir, 'data')
22
+ process.env.WORKSPACE_DIR = workspaceDir
23
+ process.env.SWARMCLAW_BUILD_MODE = '1'
24
+ mod = await import('./heartbeat-service')
25
+ })
26
+
27
+ after(() => {
28
+ // Stop the service in case any test started it
29
+ try { mod.stopHeartbeatService() } catch { /* ignore */ }
30
+ if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
31
+ else process.env.DATA_DIR = originalEnv.DATA_DIR
32
+ if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
33
+ else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
34
+ if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
35
+ else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
36
+ fs.rmSync(tempDir, { recursive: true, force: true })
37
+ })
38
+
39
+ // ── stripBlockedItems ───────────────────────────────────────────────────
40
+
41
+ describe('stripBlockedItems', () => {
42
+ it('returns empty string for empty input', () => {
43
+ assert.equal(mod.stripBlockedItems(''), '')
44
+ })
45
+
46
+ it('removes list items with (blocked) marker', () => {
47
+ const input = '- Task A\n- Task B (blocked, no update)\n- Task C'
48
+ const result = mod.stripBlockedItems(input)
49
+ assert.ok(result.includes('Task A'))
50
+ assert.ok(!result.includes('Task B'))
51
+ assert.ok(result.includes('Task C'))
52
+ })
53
+
54
+ it('removes items with various blocked formats', () => {
55
+ const input = [
56
+ '- Normal item',
57
+ '* Blocked one (blocked: awaiting approval)',
58
+ '+ Another (blocked by dependency)',
59
+ '- Keep this',
60
+ ].join('\n')
61
+ const result = mod.stripBlockedItems(input)
62
+ assert.ok(!result.includes('Blocked one'))
63
+ assert.ok(!result.includes('Another (blocked'))
64
+ assert.ok(result.includes('Normal item'))
65
+ assert.ok(result.includes('Keep this'))
66
+ })
67
+
68
+ it('preserves headers even if they mention blocked', () => {
69
+ const input = '## Blocked Tasks\n- Item (blocked)'
70
+ const result = mod.stripBlockedItems(input)
71
+ assert.ok(result.includes('## Blocked Tasks'))
72
+ assert.ok(!result.includes('- Item'))
73
+ })
74
+
75
+ it('preserves non-list lines mentioning blocked', () => {
76
+ const input = 'Some blocked context text\n- Real blocked item (blocked)'
77
+ const result = mod.stripBlockedItems(input)
78
+ assert.ok(result.includes('Some blocked context'))
79
+ assert.ok(!result.includes('Real blocked'))
80
+ })
81
+ })
82
+
83
+ // ── isHeartbeatContentEffectivelyEmpty ───────────────────────────────────
84
+
85
+ describe('isHeartbeatContentEffectivelyEmpty', () => {
86
+ it('returns true for null/undefined/empty', () => {
87
+ assert.equal(mod.isHeartbeatContentEffectivelyEmpty(null), true)
88
+ assert.equal(mod.isHeartbeatContentEffectivelyEmpty(undefined), true)
89
+ assert.equal(mod.isHeartbeatContentEffectivelyEmpty(''), true)
90
+ })
91
+
92
+ it('returns true for headers only', () => {
93
+ assert.equal(mod.isHeartbeatContentEffectivelyEmpty('# Title\n## Subtitle\n### H3'), true)
94
+ })
95
+
96
+ it('returns true for empty list items', () => {
97
+ assert.equal(mod.isHeartbeatContentEffectivelyEmpty('- \n* \n+ '), true)
98
+ })
99
+
100
+ it('returns true for empty checkboxes', () => {
101
+ assert.equal(mod.isHeartbeatContentEffectivelyEmpty('- [ ] \n- [x] '), true)
102
+ })
103
+
104
+ it('returns false for content with real text', () => {
105
+ assert.equal(mod.isHeartbeatContentEffectivelyEmpty('# Title\n- Do something useful'), false)
106
+ })
107
+
108
+ it('returns false for plain text', () => {
109
+ assert.equal(mod.isHeartbeatContentEffectivelyEmpty('Run the backup job'), false)
110
+ })
111
+ })
112
+
113
+ // ── buildIdentityContext ────────────────────────────────────────────────
114
+
115
+ describe('buildIdentityContext', () => {
116
+ it('returns empty string when no identity fields', () => {
117
+ assert.equal(mod.buildIdentityContext({}, {}), '')
118
+ })
119
+
120
+ it('builds context from agent fields', () => {
121
+ const result = mod.buildIdentityContext(null, { name: 'Bot', emoji: '🤖' })
122
+ assert.ok(result.includes('## Your Identity'))
123
+ assert.ok(result.includes('Name: Bot'))
124
+ })
125
+
126
+ it('prefers file identity fields over agent fields', () => {
127
+ // Without an actual IDENTITY.md on disk, file fields will be empty,
128
+ // so agent fields should be used as fallback
129
+ const result = mod.buildIdentityContext({ cwd: workspaceDir }, { name: 'Agent', vibe: 'chill' })
130
+ assert.ok(result.includes('Name: Agent'))
131
+ assert.ok(result.includes('Vibe: chill'))
132
+ })
133
+
134
+ it('reads IDENTITY.md from session cwd when present', () => {
135
+ const identityPath = path.join(workspaceDir, 'IDENTITY.md')
136
+ fs.writeFileSync(identityPath, '- **Name**: FileBot\n- **Emoji**: 🐛\n')
137
+ try {
138
+ const result = mod.buildIdentityContext({ cwd: workspaceDir }, { name: 'Agent' })
139
+ assert.ok(result.includes('Name: FileBot'))
140
+ } finally {
141
+ fs.unlinkSync(identityPath)
142
+ }
143
+ })
144
+ })
145
+
146
+ // ── readHeartbeatFile ───────────────────────────────────────────────────
147
+
148
+ describe('readHeartbeatFile', () => {
149
+ it('returns empty string when no HEARTBEAT.md', () => {
150
+ assert.equal(mod.readHeartbeatFile({ cwd: workspaceDir }), '')
151
+ })
152
+
153
+ it('reads HEARTBEAT.md from session cwd', () => {
154
+ const hbPath = path.join(workspaceDir, 'HEARTBEAT.md')
155
+ fs.writeFileSync(hbPath, '# Tasks\n- Check logs')
156
+ try {
157
+ const result = mod.readHeartbeatFile({ cwd: workspaceDir })
158
+ assert.ok(result.includes('Check logs'))
159
+ } finally {
160
+ fs.unlinkSync(hbPath)
161
+ }
162
+ })
163
+ })
164
+
165
+ // ── heartbeatConfigForSession ───────────────────────────────────────────
166
+
167
+ describe('heartbeatConfigForSession', () => {
168
+ it('uses global defaults when no overrides', () => {
169
+ const cfg = mod.heartbeatConfigForSession(
170
+ { id: 's1' },
171
+ { heartbeatIntervalSec: 60 },
172
+ {},
173
+ )
174
+ assert.equal(cfg.intervalSec, 60)
175
+ assert.equal(cfg.enabled, true)
176
+ })
177
+
178
+ it('disables when interval is 0', () => {
179
+ const cfg = mod.heartbeatConfigForSession(
180
+ { id: 's1' },
181
+ { heartbeatIntervalSec: 0 },
182
+ {},
183
+ )
184
+ assert.equal(cfg.enabled, false)
185
+ })
186
+
187
+ it('agent layer overrides global settings', () => {
188
+ const agents: Record<string, Record<string, unknown>> = {
189
+ 'a1': { heartbeatIntervalSec: 120, heartbeatEnabled: true, heartbeatPrompt: 'Custom agent prompt' },
190
+ }
191
+ const cfg = mod.heartbeatConfigForSession(
192
+ { id: 's1', agentId: 'a1' },
193
+ { heartbeatIntervalSec: 60 },
194
+ agents,
195
+ )
196
+ assert.equal(cfg.intervalSec, 120)
197
+ assert.equal(cfg.prompt, 'Custom agent prompt')
198
+ })
199
+
200
+ it('agent can disable heartbeat', () => {
201
+ const agents: Record<string, Record<string, unknown>> = {
202
+ 'a1': { heartbeatEnabled: false },
203
+ }
204
+ const cfg = mod.heartbeatConfigForSession(
205
+ { id: 's1', agentId: 'a1' },
206
+ { heartbeatIntervalSec: 60 },
207
+ agents,
208
+ )
209
+ assert.equal(cfg.enabled, false)
210
+ })
211
+
212
+ it('session layer overrides agent settings', () => {
213
+ const agents: Record<string, Record<string, unknown>> = {
214
+ 'a1': { heartbeatEnabled: true },
215
+ }
216
+ const cfg = mod.heartbeatConfigForSession(
217
+ { id: 's1', agentId: 'a1', heartbeatEnabled: false },
218
+ { heartbeatIntervalSec: 60 },
219
+ agents,
220
+ )
221
+ assert.equal(cfg.enabled, false)
222
+ })
223
+
224
+ it('session interval overrides agent interval', () => {
225
+ const agents: Record<string, Record<string, unknown>> = {
226
+ 'a1': { heartbeatIntervalSec: 120 },
227
+ }
228
+ const cfg = mod.heartbeatConfigForSession(
229
+ { id: 's1', agentId: 'a1', heartbeatIntervalSec: 300 },
230
+ { heartbeatIntervalSec: 60 },
231
+ agents,
232
+ )
233
+ assert.equal(cfg.intervalSec, 300)
234
+ })
235
+
236
+ it('supports duration string format for interval', () => {
237
+ const cfg = mod.heartbeatConfigForSession(
238
+ { id: 's1' },
239
+ { heartbeatInterval: '1h30m' },
240
+ {},
241
+ )
242
+ assert.equal(cfg.intervalSec, 5400) // 1h30m = 5400s
243
+ })
244
+
245
+ it('resolves model from settings and agent layers', () => {
246
+ const agents: Record<string, Record<string, unknown>> = {
247
+ 'a1': { heartbeatModel: 'gpt-4' },
248
+ }
249
+ const cfg = mod.heartbeatConfigForSession(
250
+ { id: 's1', agentId: 'a1' },
251
+ { heartbeatIntervalSec: 60, heartbeatModel: 'gpt-3.5' },
252
+ agents,
253
+ )
254
+ assert.equal(cfg.model, 'gpt-4')
255
+ })
256
+
257
+ it('returns showOk and showAlerts defaults', () => {
258
+ const cfg = mod.heartbeatConfigForSession(
259
+ { id: 's1' },
260
+ { heartbeatIntervalSec: 60 },
261
+ {},
262
+ )
263
+ assert.equal(cfg.showOk, false)
264
+ assert.equal(cfg.showAlerts, true)
265
+ })
266
+ })
267
+
268
+ // ── start/stop/status service lifecycle ─────────────────────────────────
269
+
270
+ describe('heartbeat service lifecycle', () => {
271
+ it('reports not running initially', () => {
272
+ // Stop in case a previous test left it running
273
+ mod.stopHeartbeatService()
274
+ const status = mod.getHeartbeatServiceStatus()
275
+ assert.equal(status.running, false)
276
+ })
277
+
278
+ it('start sets running to true', () => {
279
+ mod.startHeartbeatService()
280
+ try {
281
+ const status = mod.getHeartbeatServiceStatus()
282
+ assert.equal(status.running, true)
283
+ } finally {
284
+ mod.stopHeartbeatService()
285
+ }
286
+ })
287
+
288
+ it('stop sets running to false', () => {
289
+ mod.startHeartbeatService()
290
+ mod.stopHeartbeatService()
291
+ const status = mod.getHeartbeatServiceStatus()
292
+ assert.equal(status.running, false)
293
+ })
294
+
295
+ it('restart clears tracked sessions and restarts', () => {
296
+ mod.startHeartbeatService()
297
+ mod.restartHeartbeatService()
298
+ try {
299
+ const status = mod.getHeartbeatServiceStatus()
300
+ assert.equal(status.running, true)
301
+ assert.equal(status.trackedSessions, 0)
302
+ } finally {
303
+ mod.stopHeartbeatService()
304
+ }
305
+ })
306
+
307
+ it('double start replaces the timer (no duplicate intervals)', () => {
308
+ mod.startHeartbeatService()
309
+ mod.startHeartbeatService()
310
+ try {
311
+ const status = mod.getHeartbeatServiceStatus()
312
+ assert.equal(status.running, true)
313
+ } finally {
314
+ mod.stopHeartbeatService()
315
+ }
316
+ })
317
+ })
318
+
319
+ // ── buildAgentHeartbeatPrompt ───────────────────────────────────────────
320
+
321
+ describe('buildAgentHeartbeatPrompt', () => {
322
+ it('returns fallback prompt when agent is null', () => {
323
+ const result = mod.buildAgentHeartbeatPrompt({ id: 's1' }, null, 'fallback', '')
324
+ assert.equal(result, 'fallback')
325
+ })
326
+
327
+ it('includes AGENT_HEARTBEAT_TICK header', () => {
328
+ const result = mod.buildAgentHeartbeatPrompt(
329
+ { id: 's1', messages: [] },
330
+ { name: 'Bot' },
331
+ 'Check status',
332
+ '',
333
+ )
334
+ assert.ok(result.includes('AGENT_HEARTBEAT_TICK'))
335
+ })
336
+
337
+ it('includes heartbeat file content when provided', () => {
338
+ const result = mod.buildAgentHeartbeatPrompt(
339
+ { id: 's1', messages: [] },
340
+ { name: 'Bot' },
341
+ 'Check status',
342
+ '# Tasks\n- Do the thing',
343
+ )
344
+ assert.ok(result.includes('HEARTBEAT.md contents'))
345
+ assert.ok(result.includes('Do the thing'))
346
+ })
347
+
348
+ it('excludes HEARTBEAT.md section when content is effectively empty', () => {
349
+ const result = mod.buildAgentHeartbeatPrompt(
350
+ { id: 's1', messages: [] },
351
+ { name: 'Bot' },
352
+ 'Check status',
353
+ '# Title\n- [ ] ',
354
+ )
355
+ assert.ok(!result.includes('HEARTBEAT.md contents'))
356
+ })
357
+
358
+ it('includes dynamic goal when set on agent', () => {
359
+ const result = mod.buildAgentHeartbeatPrompt(
360
+ { id: 's1', messages: [] },
361
+ { name: 'Bot', heartbeatGoal: 'Monitor CI pipeline' },
362
+ 'Check status',
363
+ '',
364
+ )
365
+ assert.ok(result.includes('Monitor CI pipeline'))
366
+ })
367
+
368
+ it('includes recent conversation context', () => {
369
+ const result = mod.buildAgentHeartbeatPrompt(
370
+ {
371
+ id: 's1',
372
+ messages: [
373
+ { role: 'user', text: 'Check the logs', toolEvents: [] },
374
+ { role: 'assistant', text: 'Logs look clean', toolEvents: [] },
375
+ ],
376
+ },
377
+ { name: 'Bot' },
378
+ 'Check status',
379
+ '',
380
+ )
381
+ assert.ok(result.includes('[user]: Check the logs'))
382
+ assert.ok(result.includes('[assistant]: Logs look clean'))
383
+ })
384
+
385
+ it('includes agent soul in prompt', () => {
386
+ const result = mod.buildAgentHeartbeatPrompt(
387
+ { id: 's1', messages: [] },
388
+ { name: 'Bot', soul: 'You are a cheerful monitoring assistant' },
389
+ 'Check status',
390
+ '',
391
+ )
392
+ assert.ok(result.includes('Persona: You are a cheerful'))
393
+ })
394
+
395
+ it('strips blocked items from heartbeat file content', () => {
396
+ const result = mod.buildAgentHeartbeatPrompt(
397
+ { id: 's1', messages: [] },
398
+ { name: 'Bot' },
399
+ 'Check status',
400
+ '- Active task\n- Blocked task (blocked, waiting)\n- Another active task',
401
+ )
402
+ assert.ok(result.includes('Active task'))
403
+ assert.ok(!result.includes('Blocked task'))
404
+ assert.ok(result.includes('Another active task'))
405
+ })
406
+ })
@@ -15,6 +15,7 @@ import { buildIdentityContinuityContext } from './identity-continuity'
15
15
  import { buildMainLoopHeartbeatPrompt, isMainSession } from './main-agent-loop'
16
16
  import { ensureAgentThreadSession } from './agent-thread-session'
17
17
  import { isAgentDisabled } from './agent-availability'
18
+ import { errorMessage, hmrSingleton } from '@/lib/shared-utils'
18
19
 
19
20
  const HEARTBEAT_TICK_MS = 60_000
20
21
  const MAX_CONCURRENT_HEARTBEATS = 5
@@ -33,14 +34,12 @@ interface HeartbeatState {
33
34
  failures: Map<string, FailureRecord>
34
35
  }
35
36
 
36
- const globalKey = '__swarmclaw_heartbeat_service__' as const
37
- const globalScope = globalThis as typeof globalThis & { [globalKey]?: HeartbeatState }
38
- const state: HeartbeatState = globalScope[globalKey] ?? (globalScope[globalKey] = {
37
+ const state: HeartbeatState = hmrSingleton<HeartbeatState>('__swarmclaw_heartbeat_service__', () => ({
39
38
  timer: null,
40
39
  running: false,
41
40
  lastBySession: new Map<string, number>(),
42
41
  failures: new Map<string, FailureRecord>(),
43
- })
42
+ }))
44
43
 
45
44
  function parseIntBounded(value: unknown, fallback: number, min: number, max: number): number {
46
45
  const parsed = typeof value === 'number'
@@ -194,6 +193,31 @@ export function buildIdentityContext(session: Record<string, unknown> | undefine
194
193
  return `## Your Identity\n${lines.join('\n')}`
195
194
  }
196
195
 
196
+ // ── Blocked-item suppression ────────────────────────────────────────────
197
+ // Ported from OpenClaw's duplicate-suppression pattern: instead of letting
198
+ // the LLM see blocked tasks every tick (and parrot "still blocked"), we
199
+ // strip those lines before they ever reach the prompt. A line is
200
+ // considered blocked if it contains "(blocked" anywhere (case-insensitive),
201
+ // which covers "(blocked, no update)", "(blocked: awaiting …)", etc.
202
+ const BLOCKED_MARKER_RE = /\(blocked\b/i
203
+
204
+ /**
205
+ * Remove blocked checklist items from HEARTBEAT.md content so the LLM
206
+ * doesn't keep surfacing them. Headers and non-list lines pass through
207
+ * unchanged.
208
+ */
209
+ export function stripBlockedItems(content: string): string {
210
+ if (!content) return ''
211
+ const lines = content.split('\n')
212
+ const filtered = lines.filter((line) => {
213
+ const trimmed = line.trim()
214
+ // Only filter checklist / list items that are explicitly marked blocked
215
+ if (/^[-*+]\s/.test(trimmed) && BLOCKED_MARKER_RE.test(trimmed)) return false
216
+ return true
217
+ })
218
+ return filtered.join('\n')
219
+ }
220
+
197
221
  /** Detect HEARTBEAT.md files that contain only skeleton structure (headers, empty list items) but no real content. */
198
222
  export function isHeartbeatContentEffectivelyEmpty(content: string | undefined | null): boolean {
199
223
  if (!content || typeof content !== 'string') return true
@@ -236,8 +260,9 @@ export function buildAgentHeartbeatPrompt(session: any, agent: any, fallbackProm
236
260
  })
237
261
  .join('\n')
238
262
 
239
- // Don't inject effectively-empty HEARTBEAT.md content
240
- const effectiveFileContent = isHeartbeatContentEffectivelyEmpty(heartbeatFileContent) ? '' : heartbeatFileContent
263
+ // Strip blocked items, then check if anything meaningful remains
264
+ const strippedContent = stripBlockedItems(heartbeatFileContent)
265
+ const effectiveFileContent = isHeartbeatContentEffectivelyEmpty(strippedContent) ? '' : strippedContent
241
266
 
242
267
  return [
243
268
  'AGENT_HEARTBEAT_TICK',
@@ -507,7 +532,7 @@ async function tickHeartbeats() {
507
532
  count: (prev?.count ?? 0) + 1,
508
533
  lastFailedAt: Date.now(),
509
534
  })
510
- const msg = err instanceof Error ? err.message : String(err)
535
+ const msg = errorMessage(err)
511
536
  log.warn('heartbeat', `Heartbeat run failed for session ${sid}`, msg)
512
537
  })
513
538
  }
@@ -577,3 +602,25 @@ export function getHeartbeatServiceStatus() {
577
602
  trackedSessions: state.lastBySession.size,
578
603
  }
579
604
  }
605
+
606
+ /**
607
+ * Remove tracking entries for sessions that no longer exist.
608
+ * Called periodically by the daemon health sweep.
609
+ */
610
+ export function pruneHeartbeatState(liveSessionIds: Set<string>): number {
611
+ let removed = 0
612
+ for (const id of state.lastBySession.keys()) {
613
+ if (!liveSessionIds.has(id)) {
614
+ state.lastBySession.delete(id)
615
+ state.failures.delete(id)
616
+ removed++
617
+ }
618
+ }
619
+ // Also clean up orphaned failure entries
620
+ for (const id of state.failures.keys()) {
621
+ if (!liveSessionIds.has(id)) {
622
+ state.failures.delete(id)
623
+ }
624
+ }
625
+ return removed
626
+ }
@@ -4,6 +4,7 @@ import { afterEach, describe, it } from 'node:test'
4
4
  import {
5
5
  buildHeartbeatWakePrompt,
6
6
  buildWakeTriggerContext,
7
+ deriveHeartbeatWakeDeliveryMode,
7
8
  hasPendingHeartbeatWake,
8
9
  mergeHeartbeatWakeRequest,
9
10
  requestHeartbeatNow,
@@ -109,4 +110,22 @@ describe('heartbeat-wake helpers', () => {
109
110
  ['connector-message', 'watch_job'],
110
111
  )
111
112
  })
113
+
114
+ it('forces connector-triggered wakes into tool-only delivery mode', () => {
115
+ const connectorWake = mergeHeartbeatWakeRequest(undefined, {
116
+ sessionId: 'sess-3',
117
+ reason: 'connector-message',
118
+ source: 'connector:whatsapp',
119
+ requestedAt: 1,
120
+ })
121
+ const scheduleWake = mergeHeartbeatWakeRequest(undefined, {
122
+ sessionId: 'sess-4',
123
+ reason: 'schedule',
124
+ source: 'schedule:daily',
125
+ requestedAt: 1,
126
+ })
127
+
128
+ assert.equal(deriveHeartbeatWakeDeliveryMode(connectorWake.events), 'tool_only')
129
+ assert.equal(deriveHeartbeatWakeDeliveryMode(scheduleWake.events), 'default')
130
+ })
112
131
  })
@@ -16,6 +16,7 @@ import { loadSessions, loadAgents, loadSettings } from './storage'
16
16
  import { enqueueSessionRun, getSessionExecutionState } from './session-run-manager'
17
17
  import { log } from './logger'
18
18
  import { isAgentDisabled } from './agent-availability'
19
+ import { errorMessage, hmrSingleton } from '@/lib/shared-utils'
19
20
 
20
21
  export interface WakeRequestInput {
21
22
  eventId?: string
@@ -56,21 +57,12 @@ const MAX_RESUME_CHARS = 280
56
57
  const MAX_DETAIL_CHARS = 800
57
58
  type WakeTimerKind = 'normal' | 'retry'
58
59
 
59
- const globalKey = '__swarmclaw_heartbeat_wake__' as const
60
- const globalScope = globalThis as typeof globalThis & {
61
- [globalKey]?: {
62
- pending: Map<string, WakeRequest>
63
- timer: ReturnType<typeof setTimeout> | null
64
- timerDueAt: number | null
65
- timerKind: WakeTimerKind | null
66
- }
67
- }
68
- const state = globalScope[globalKey] ?? (globalScope[globalKey] = {
69
- pending: new Map(),
70
- timer: null,
71
- timerDueAt: null,
72
- timerKind: null,
73
- })
60
+ const state = hmrSingleton('__swarmclaw_heartbeat_wake__', () => ({
61
+ pending: new Map<string, WakeRequest>(),
62
+ timer: null as ReturnType<typeof setTimeout> | null,
63
+ timerDueAt: null as number | null,
64
+ timerKind: null as WakeTimerKind | null,
65
+ }))
74
66
 
75
67
  function trimText(value: unknown, maxChars: number): string | undefined {
76
68
  if (typeof value !== 'string') return undefined
@@ -235,6 +227,14 @@ export function buildWakeTriggerContext(events: WakeEvent[], nowIso?: string): s
235
227
  return lines.join('\n')
236
228
  }
237
229
 
230
+ export function deriveHeartbeatWakeDeliveryMode(
231
+ events: WakeEvent[],
232
+ ): 'default' | 'tool_only' {
233
+ return events.some((event) => event.reason.toLowerCase() === 'connector-message')
234
+ ? 'tool_only'
235
+ : 'default'
236
+ }
237
+
238
238
  export function buildHeartbeatWakePrompt(input: {
239
239
  wake: WakeRequest
240
240
  basePrompt?: string
@@ -347,6 +347,7 @@ function flushWakes(): void {
347
347
  showOk: cfg.showOk,
348
348
  showAlerts: cfg.showAlerts,
349
349
  target: cfg.target,
350
+ deliveryMode: deriveHeartbeatWakeDeliveryMode(wake.events),
350
351
  },
351
352
  })
352
353
 
@@ -360,7 +361,7 @@ function flushWakes(): void {
360
361
  retryCount: wake.retryCount + 1,
361
362
  })
362
363
  delayedForRetry = true
363
- log.warn('heartbeat-wake', `Wake failed: ${err instanceof Error ? err.message : String(err)}`)
364
+ log.warn('heartbeat-wake', `Wake failed: ${errorMessage(err)}`)
364
365
  }
365
366
  }
366
367