@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
@@ -3,8 +3,10 @@ import fs from 'fs'
3
3
  import path from 'path'
4
4
  import type { Connector } from '@/types'
5
5
  import type { PlatformConnector, ConnectorInstance, InboundMessage, InboundMediaType } from './types'
6
+ import { resolveConnectorIngressReply } from './ingress-delivery'
7
+ import { deliverChunkedConnectorText } from './delivery'
6
8
  import { downloadInboundMediaToUpload, inferInboundMediaType, mimeFromPath, isImageMime, isAudioMime } from './media'
7
- import { getConnectorReplySendOptions, isNoMessage, recordConnectorOutboundDelivery } from './manager'
9
+ import { errorMessage } from '@/lib/shared-utils'
8
10
 
9
11
  const telegram: PlatformConnector = {
10
12
  async start(connector, botToken, onMessage): Promise<ConnectorInstance> {
@@ -152,37 +154,25 @@ const telegram: PlatformConnector = {
152
154
 
153
155
  try {
154
156
  await ctx.api.sendChatAction(ctx.chat.id, 'typing')
155
- const response = await onMessage(inbound)
156
-
157
- if (isNoMessage(response)) return
158
-
159
- const replyOptions = getConnectorReplySendOptions({ connectorId: connector.id, inbound })
160
- const baseOptions: Record<string, unknown> = {}
161
- if (replyOptions.replyToMessageId) {
162
- baseOptions.reply_parameters = { message_id: Number(replyOptions.replyToMessageId) }
163
- }
164
- if (replyOptions.threadId) {
165
- baseOptions.message_thread_id = Number(replyOptions.threadId)
166
- }
167
-
168
- let lastMessageId: string | undefined
169
-
170
- // Telegram has a 4096 char limit
171
- if (response.length <= 4096) {
172
- const sent = await ctx.api.sendMessage(ctx.chat.id, response, baseOptions as any)
173
- lastMessageId = String(sent.message_id)
174
- } else {
175
- const chunks = response.match(/[\s\S]{1,4090}/g) || [response]
176
- for (let i = 0; i < chunks.length; i += 1) {
177
- const sent = await ctx.api.sendMessage(ctx.chat.id, chunks[i], (i === 0 ? baseOptions : {}) as any)
178
- lastMessageId = String(sent.message_id)
179
- }
180
- }
181
- await recordConnectorOutboundDelivery({
157
+ const reply = await resolveConnectorIngressReply(onMessage, inbound)
158
+ if (!reply) return
159
+ await deliverChunkedConnectorText({
182
160
  connectorId: connector.id,
183
161
  inbound,
184
- messageId: lastMessageId,
185
- state: 'sent',
162
+ text: reply.visibleText,
163
+ maxSingleMessageLength: 4096,
164
+ chunkLength: 4090,
165
+ sendChunk: async (chunk, meta) => {
166
+ const options: Record<string, unknown> = {}
167
+ if (meta.isFirstChunk && meta.replyToMessageId) {
168
+ options.reply_parameters = { message_id: Number(meta.replyToMessageId) }
169
+ }
170
+ if (meta.threadId) {
171
+ options.message_thread_id = Number(meta.threadId)
172
+ }
173
+ const sent = await ctx.api.sendMessage(ctx.chat.id, chunk, options as any)
174
+ return String(sent.message_id)
175
+ },
186
176
  })
187
177
  } catch (err: any) {
188
178
  console.error(`[telegram] Error handling message:`, err.message)
@@ -195,19 +185,7 @@ const telegram: PlatformConnector = {
195
185
  // Track whether the bot is actively polling
196
186
  let botRunning = true
197
187
 
198
- // Start polling not awaited (runs in background)
199
- bot.start({
200
- allowed_updates: ['message', 'edited_message'],
201
- onStart: (botInfo) => {
202
- botUsername = botInfo.username || ''
203
- console.log(`[telegram] Bot started as @${botInfo.username} — polling for updates`)
204
- },
205
- }).catch((err) => {
206
- botRunning = false
207
- console.error(`[telegram] Polling stopped with error:`, err.message || err)
208
- })
209
-
210
- return {
188
+ const instance: ConnectorInstance = {
211
189
  connector,
212
190
  isAlive() {
213
191
  return botRunning
@@ -293,6 +271,22 @@ const telegram: PlatformConnector = {
293
271
  console.log(`[telegram] Bot stopped`)
294
272
  },
295
273
  }
274
+
275
+ // Start polling — not awaited (runs in background)
276
+ bot.start({
277
+ allowed_updates: ['message', 'edited_message'],
278
+ onStart: (botInfo) => {
279
+ botUsername = botInfo.username || ''
280
+ console.log(`[telegram] Bot started as @${botInfo.username} — polling for updates`)
281
+ },
282
+ }).catch((err: unknown) => {
283
+ botRunning = false
284
+ const errMsg = errorMessage(err)
285
+ console.error(`[telegram] Polling stopped with error:`, errMsg)
286
+ instance.onCrash?.(`Polling stopped: ${errMsg}`)
287
+ })
288
+
289
+ return instance
296
290
  },
297
291
  }
298
292
 
@@ -68,6 +68,34 @@ export interface OutboundTypingOptions {
68
68
  threadId?: string
69
69
  }
70
70
 
71
+ export interface ConnectorRouteResult {
72
+ managerHandled: boolean
73
+ visibleText: string
74
+ delivery: 'sent' | 'silent'
75
+ messageId?: string
76
+ }
77
+
78
+ export type ConnectorIngressResult = string | ConnectorRouteResult
79
+
80
+ export function isConnectorRouteResult(value: unknown): value is ConnectorRouteResult {
81
+ return !!value
82
+ && typeof value === 'object'
83
+ && !Array.isArray(value)
84
+ && typeof (value as ConnectorRouteResult).managerHandled === 'boolean'
85
+ && typeof (value as ConnectorRouteResult).visibleText === 'string'
86
+ }
87
+
88
+ export function normalizeConnectorIngressResult(value: ConnectorIngressResult): ConnectorRouteResult {
89
+ if (isConnectorRouteResult(value)) return value
90
+ const visibleText = typeof value === 'string' ? value : ''
91
+ const normalized = visibleText.trim().toUpperCase()
92
+ return {
93
+ managerHandled: false,
94
+ visibleText,
95
+ delivery: normalized && normalized !== 'NO_MESSAGE' ? 'sent' : 'silent',
96
+ }
97
+ }
98
+
71
99
  /** A running connector instance */
72
100
  export interface ConnectorInstance {
73
101
  connector: Connector
@@ -96,6 +124,8 @@ export interface ConnectorInstance {
96
124
  sendTyping?: (channelId: string, options?: OutboundTypingOptions) => Promise<void>
97
125
  /** Health check: returns true if the underlying connection is alive */
98
126
  isAlive?: () => boolean
127
+ /** Called by the platform when the connection dies terminally (no internal retry) */
128
+ onCrash?: (error: string) => void
99
129
  }
100
130
 
101
131
  /** Platform-specific connector implementation */
@@ -103,6 +133,6 @@ export interface PlatformConnector {
103
133
  start(
104
134
  connector: Connector,
105
135
  botToken: string,
106
- onMessage: (msg: InboundMessage) => Promise<string>,
136
+ onMessage: (msg: InboundMessage) => Promise<ConnectorIngressResult>,
107
137
  ): Promise<ConnectorInstance>
108
138
  }
@@ -3,10 +3,13 @@ import test from 'node:test'
3
3
  import {
4
4
  buildWhatsAppTextPayloads,
5
5
  buildWhatsAppInboundMessage,
6
+ isWhatsAppSocketAlive,
6
7
  isWhatsAppInboundAllowed,
7
8
  normalizeWhatsAppAudioForSend,
8
9
  normalizeWhatsAppIdentifier,
10
+ resolveWhatsAppAllowedIdentifiers,
9
11
  } from './whatsapp'
12
+ import { normalizeE164, normalizeWhatsappTarget } from './response-media'
10
13
 
11
14
  test('buildWhatsAppTextPayloads disables link previews for text sends', () => {
12
15
  const payloads = buildWhatsAppTextPayloads('See https://example.com for details')
@@ -32,6 +35,19 @@ test('normalizeWhatsAppIdentifier strips jid wrappers and device suffixes', () =
32
35
  assert.equal(normalizeWhatsAppIdentifier('199900000001@lid'), '199900000001')
33
36
  })
34
37
 
38
+ test('normalizeWhatsAppIdentifier handles international numbers', () => {
39
+ // Swiss
40
+ assert.equal(normalizeWhatsAppIdentifier('+41 79 666 68 64@s.whatsapp.net'), '41796666864')
41
+ // German
42
+ assert.equal(normalizeWhatsAppIdentifier('491711234567@s.whatsapp.net'), '491711234567')
43
+ // Brazilian
44
+ assert.equal(normalizeWhatsAppIdentifier('+55 11 99999-8888'), '5511999998888')
45
+ // Indian
46
+ assert.equal(normalizeWhatsAppIdentifier('919876543210:0@s.whatsapp.net'), '919876543210')
47
+ // Plain number with whatsapp: prefix
48
+ assert.equal(normalizeWhatsAppIdentifier('whatsapp:+447911123456'), '447911123456')
49
+ })
50
+
35
51
  test('isWhatsAppInboundAllowed matches allow-list entries against alt phone JIDs', () => {
36
52
  const allowed = ['15550001111']
37
53
  const msg = {
@@ -45,6 +61,28 @@ test('isWhatsAppInboundAllowed matches allow-list entries against alt phone JIDs
45
61
  assert.equal(isWhatsAppInboundAllowed({ allowedJids: ['15559990000'], msg }), false)
46
62
  })
47
63
 
64
+ test('resolveWhatsAppAllowedIdentifiers merges connector and settings approvals', () => {
65
+ const allowed = resolveWhatsAppAllowedIdentifiers({
66
+ configuredAllowedJids: '15550001111',
67
+ settingsContacts: [
68
+ { id: 'family', label: 'Family', phone: '+1 (666) 000-2222' },
69
+ { id: 'dup', label: 'Family JID', phone: '16660002222@s.whatsapp.net' },
70
+ ],
71
+ })
72
+
73
+ assert.deepEqual(allowed, ['15550001111', '16660002222'])
74
+ })
75
+
76
+ test('resolveWhatsAppAllowedIdentifiers keeps the connector open when no allowedJids are configured', () => {
77
+ const allowed = resolveWhatsAppAllowedIdentifiers({
78
+ settingsContacts: [
79
+ { id: 'family', label: 'Family', phone: '+1 (666) 000-2222' },
80
+ ],
81
+ })
82
+
83
+ assert.equal(allowed, null)
84
+ })
85
+
48
86
  test('buildWhatsAppInboundMessage includes modern WhatsApp metadata', () => {
49
87
  const inbound = buildWhatsAppInboundMessage({
50
88
  msg: {
@@ -79,6 +117,34 @@ test('buildWhatsAppInboundMessage includes modern WhatsApp metadata', () => {
79
117
  assert.equal(inbound?.text, 'Hey there')
80
118
  })
81
119
 
120
+ test('isWhatsAppSocketAlive reports disconnected sockets as dead so daemon restarts can run', () => {
121
+ assert.equal(isWhatsAppSocketAlive({
122
+ stopped: false,
123
+ socket: { ws: { isClosed: true } },
124
+ connectionState: 'close',
125
+ }), false)
126
+
127
+ assert.equal(isWhatsAppSocketAlive({
128
+ stopped: false,
129
+ socket: { ws: { isClosing: true } },
130
+ connectionState: 'connecting',
131
+ }), false)
132
+ })
133
+
134
+ test('isWhatsAppSocketAlive keeps QR and active sessions marked live', () => {
135
+ assert.equal(isWhatsAppSocketAlive({
136
+ stopped: false,
137
+ socket: { ws: { isConnecting: true } },
138
+ connectionState: 'connecting',
139
+ }), true)
140
+
141
+ assert.equal(isWhatsAppSocketAlive({
142
+ stopped: false,
143
+ socket: { ws: { isOpen: true } },
144
+ connectionState: 'open',
145
+ }), true)
146
+ })
147
+
82
148
  test('normalizeWhatsAppAudioForSend transcodes mp3 voice notes to Android-safe opus/ogg', () => {
83
149
  let transcodeCalls = 0
84
150
  const converted = normalizeWhatsAppAudioForSend({
@@ -132,3 +198,45 @@ test('normalizeWhatsAppAudioForSend leaves normal audio attachments alone when p
132
198
  assert.equal(converted.buffer.toString(), 'music')
133
199
  assert.equal(converted.mimeType, 'audio/mpeg')
134
200
  })
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // normalizeE164 — E.164 formatting for all country codes
204
+ // ---------------------------------------------------------------------------
205
+
206
+ test('normalizeE164 strips formatting and ensures + prefix', () => {
207
+ assert.equal(normalizeE164('+41 79 666 68 64'), '+41796666864')
208
+ assert.equal(normalizeE164('(555) 123-4567'), '+5551234567')
209
+ assert.equal(normalizeE164('whatsapp:+447911123456'), '+447911123456')
210
+ assert.equal(normalizeE164('491711234567'), '+491711234567')
211
+ assert.equal(normalizeE164('+55 11 99999-8888'), '+5511999998888')
212
+ })
213
+
214
+ // ---------------------------------------------------------------------------
215
+ // normalizeWhatsappTarget — JID normalization for outbound messages
216
+ // ---------------------------------------------------------------------------
217
+
218
+ test('normalizeWhatsappTarget builds user JIDs from plain numbers', () => {
219
+ assert.equal(normalizeWhatsappTarget('+41796666864'), '41796666864@s.whatsapp.net')
220
+ assert.equal(normalizeWhatsappTarget('447911123456'), '447911123456@s.whatsapp.net')
221
+ assert.equal(normalizeWhatsappTarget('+55 11 99999-8888'), '5511999998888@s.whatsapp.net')
222
+ assert.equal(normalizeWhatsappTarget('(555) 123-4567'), '5551234567@s.whatsapp.net')
223
+ })
224
+
225
+ test('normalizeWhatsappTarget preserves group JIDs', () => {
226
+ assert.equal(normalizeWhatsappTarget('120363401234567890@g.us'), '120363401234567890@g.us')
227
+ assert.equal(normalizeWhatsappTarget('120363-401234567890@g.us'), '120363-401234567890@g.us')
228
+ })
229
+
230
+ test('normalizeWhatsappTarget extracts phone from user JIDs', () => {
231
+ assert.equal(normalizeWhatsappTarget('41796666864:0@s.whatsapp.net'), '41796666864@s.whatsapp.net')
232
+ assert.equal(normalizeWhatsappTarget('15550001111:7@s.whatsapp.net'), '15550001111@s.whatsapp.net')
233
+ })
234
+
235
+ test('normalizeWhatsappTarget passes through LID JIDs unchanged', () => {
236
+ assert.equal(normalizeWhatsappTarget('199900000001@lid'), '199900000001@lid')
237
+ })
238
+
239
+ test('normalizeWhatsappTarget strips whatsapp: prefix', () => {
240
+ assert.equal(normalizeWhatsappTarget('whatsapp:+447911123456'), '447911123456@s.whatsapp.net')
241
+ assert.equal(normalizeWhatsappTarget('whatsapp:120363401234567890@g.us'), '120363401234567890@g.us')
242
+ })
@@ -12,13 +12,16 @@ import fs from 'fs'
12
12
  import os from 'os'
13
13
  import { spawnSync } from 'child_process'
14
14
  import type { Connector } from '@/types'
15
+ import { dedup, errorMessage } from '@/lib/shared-utils'
15
16
  import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
17
+ import { resolveConnectorIngressReply } from './ingress-delivery'
16
18
  import { saveInboundMediaBuffer, mimeFromPath, isImageMime, isAudioMime } from './media'
17
- import { isNoMessage, recordConnectorOutboundDelivery } from './manager'
19
+ import { recordConnectorOutboundDelivery } from './delivery'
18
20
  import { formatTextForWhatsApp } from './whatsapp-text'
21
+ import { getWhatsAppApprovedSenderIds } from './pairing'
19
22
 
20
23
  import { DATA_DIR } from '../data-dir'
21
- import { loadConnectors } from '../storage'
24
+ import { loadConnectors, loadSettings } from '../storage'
22
25
 
23
26
  const AUTH_DIR = path.join(DATA_DIR, 'whatsapp-auth')
24
27
  const INBOUND_DEDUPE_TTL_MS = 2 * 60 * 1000
@@ -29,6 +32,15 @@ const WHATSAPP_VOICE_NOTE_EXTS = new Set(['.ogg', '.opus'])
29
32
 
30
33
  let cachedFfmpegBinary: string | null | undefined
31
34
 
35
+ type WhatsAppSocketState = {
36
+ ws?: {
37
+ isOpen?: boolean
38
+ isClosed?: boolean
39
+ isClosing?: boolean
40
+ isConnecting?: boolean
41
+ } | null
42
+ } | null
43
+
32
44
  export function buildWhatsAppTextPayloads(text: string): Array<{ text: string; linkPreview: null }> {
33
45
  const chunks = text.length <= WHATSAPP_SINGLE_MESSAGE_MAX
34
46
  ? [text]
@@ -36,6 +48,26 @@ export function buildWhatsAppTextPayloads(text: string): Array<{ text: string; l
36
48
  return chunks.map((chunk) => ({ text: chunk, linkPreview: null }))
37
49
  }
38
50
 
51
+ export function isWhatsAppSocketAlive(params: {
52
+ stopped: boolean
53
+ socket: WhatsAppSocketState
54
+ connectionState?: string | null
55
+ }): boolean {
56
+ if (params.stopped) return false
57
+ if (!params.socket) return false
58
+
59
+ const ws = params.socket.ws
60
+ if (!ws) return false
61
+ if (params.connectionState === 'close') return false
62
+ if (ws.isClosed || ws.isClosing) return false
63
+ if (ws.isOpen || ws.isConnecting) return true
64
+ if (params.connectionState === 'open' || params.connectionState === 'connecting') return true
65
+
66
+ // Treat an existing socket with no explicit close signal as live while QR/auth
67
+ // negotiation is still in progress.
68
+ return params.connectionState == null
69
+ }
70
+
39
71
  function normalizeMimeType(mimeType?: string): string {
40
72
  return String(mimeType || '').toLowerCase().split(';')[0].trim()
41
73
  }
@@ -141,6 +173,7 @@ export function normalizeWhatsAppAudioForSend(params: {
141
173
  return converted || { buffer: params.buffer, mimeType }
142
174
  }
143
175
 
176
+ /** Extract the user part from a JID, stripping the server and device suffix */
144
177
  function jidUserPart(raw: string): string {
145
178
  const trimmed = String(raw || '').trim().toLowerCase()
146
179
  if (!trimmed) return ''
@@ -148,16 +181,15 @@ function jidUserPart(raw: string): string {
148
181
  return withoutServer.split(':')[0]
149
182
  }
150
183
 
151
- /** Normalize a phone number or JID user part for inbound matching */
184
+ /**
185
+ * Normalize a phone number or JID to a bare-digit identifier for matching.
186
+ * Works for all country codes — strips formatting, `whatsapp:` prefixes,
187
+ * JID suffixes (`@s.whatsapp.net`, `@lid`), and device suffixes (`:0`).
188
+ * Returns bare digits (no `+` prefix) for comparison.
189
+ */
152
190
  export function normalizeWhatsAppIdentifier(raw: string): string {
153
- let n = jidUserPart(raw).replace(/[\s\-()]/g, '')
154
- // UK local: 07xxx → 447xxx
155
- if (n.startsWith('0') && n.length >= 10) {
156
- n = '44' + n.slice(1)
157
- }
158
- // Strip leading +
159
- if (n.startsWith('+')) n = n.slice(1)
160
- return n.replace(/[^a-z0-9]/g, '')
191
+ const withoutPrefix = String(raw || '').replace(/^whatsapp:/i, '').trim()
192
+ return jidUserPart(withoutPrefix).replace(/[^\da-z]/g, '')
161
193
  }
162
194
 
163
195
  function parseAllowedIdentifiers(raw: unknown): string[] | null {
@@ -169,6 +201,19 @@ function parseAllowedIdentifiers(raw: unknown): string[] | null {
169
201
  return out.length ? out : null
170
202
  }
171
203
 
204
+ export function resolveWhatsAppAllowedIdentifiers(params: {
205
+ configuredAllowedJids?: unknown
206
+ settingsContacts?: unknown
207
+ }): string[] | null {
208
+ const configured = parseAllowedIdentifiers(params.configuredAllowedJids)
209
+ if (!configured?.length) return null
210
+ const settings = getWhatsAppApprovedSenderIds(params.settingsContacts)
211
+ .map((entry) => normalizeWhatsAppIdentifier(entry))
212
+ .filter(Boolean)
213
+ const merged = dedup([...configured, ...settings])
214
+ return merged.length ? merged : null
215
+ }
216
+
172
217
  function messageContextInfo(content: any): any {
173
218
  return content?.extendedTextMessage?.contextInfo
174
219
  || content?.imageMessage?.contextInfo
@@ -190,7 +235,7 @@ export function collectWhatsAppAddressCandidates(msg: Pick<WAMessage, 'key'>): s
190
235
  const normalized = raw
191
236
  .map((entry) => normalizeWhatsAppIdentifier(String(entry || '')))
192
237
  .filter(Boolean)
193
- return Array.from(new Set(normalized))
238
+ return dedup(normalized)
194
239
  }
195
240
 
196
241
  export function isWhatsAppInboundAllowed(params: {
@@ -292,21 +337,38 @@ const whatsapp: PlatformConnector = {
292
337
  let sock: ReturnType<typeof makeWASocket> | null = null
293
338
  let stopped = false
294
339
  let socketGen = 0 // Track socket generation to ignore stale events
340
+ let connectionState: string | null = 'connecting'
341
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null
295
342
  const seenInboundMessageIds = new Map<string, number>()
296
343
 
344
+ const clearReconnectTimer = () => {
345
+ if (!reconnectTimer) return
346
+ clearTimeout(reconnectTimer)
347
+ reconnectTimer = null
348
+ }
349
+
350
+ const scheduleReconnect = (delayMs: number) => {
351
+ if (stopped) return
352
+ clearReconnectTimer()
353
+ reconnectTimer = setTimeout(() => {
354
+ reconnectTimer = null
355
+ if (stopped) return
356
+ startSocket()
357
+ }, delayMs)
358
+ reconnectTimer.unref?.()
359
+ }
360
+
297
361
  const instance: ConnectorInstance = {
298
362
  connector,
299
363
  qrDataUrl: null,
300
364
  authenticated: false,
301
365
  hasCredentials: hasStoredCreds(authDir),
302
366
  isAlive() {
303
- if (stopped || !sock) return false
304
- // Check the underlying WebSocket connection state
305
- const ws = sock.ws
306
- if (!ws) return false
307
- // If authenticated, the connection is alive
308
- // If we have a socket but not yet authenticated (QR phase), still considered alive
309
- return !stopped
367
+ return isWhatsAppSocketAlive({
368
+ stopped,
369
+ socket: sock,
370
+ connectionState,
371
+ })
310
372
  },
311
373
  async sendMessage(channelId, text, options) {
312
374
  if (!sock) throw new Error('WhatsApp connector is not connected')
@@ -324,7 +386,7 @@ const whatsapp: PlatformConnector = {
324
386
  try {
325
387
  sent = await sock.sendMessage(channelId, { image: buf, caption, mimetype: mime })
326
388
  } catch (err: unknown) {
327
- const errMsg = err instanceof Error ? err.message : String(err)
389
+ const errMsg = errorMessage(err)
328
390
  console.warn(`[whatsapp] Image send failed (${errMsg}); retrying as document: ${fName}`)
329
391
  sent = await sock.sendMessage(channelId, { document: buf, fileName: fName, mimetype: mime, caption })
330
392
  }
@@ -378,6 +440,8 @@ const whatsapp: PlatformConnector = {
378
440
  },
379
441
  async stop() {
380
442
  stopped = true
443
+ connectionState = 'close'
444
+ clearReconnectTimer()
381
445
  try { sock?.end(undefined) } catch { /* ignore */ }
382
446
  sock = null
383
447
  console.log(`[whatsapp] Stopped connector: ${connector.name}`)
@@ -388,6 +452,9 @@ const whatsapp: PlatformConnector = {
388
452
  const sentMessageIds = new Set<string>()
389
453
 
390
454
  const startSocket = () => {
455
+ if (stopped) return
456
+ clearReconnectTimer()
457
+
391
458
  // Close previous socket to prevent stale event handlers
392
459
  if (sock) {
393
460
  try { sock.ev.removeAllListeners('connection.update') } catch { /* ignore */ }
@@ -398,6 +465,7 @@ const whatsapp: PlatformConnector = {
398
465
  }
399
466
 
400
467
  const gen = ++socketGen // Capture generation for stale detection
468
+ connectionState = 'connecting'
401
469
  console.log(`[whatsapp] Starting socket gen=${gen} for ${connector.name} (hasCreds=${instance.hasCredentials})`)
402
470
 
403
471
  sock = makeWASocket({
@@ -416,6 +484,7 @@ const whatsapp: PlatformConnector = {
416
484
  if (gen !== socketGen) return // Ignore events from stale sockets
417
485
 
418
486
  const { connection, lastDisconnect, qr } = update
487
+ if (typeof connection === 'string' && connection) connectionState = connection
419
488
  console.log(`[whatsapp] Connection update gen=${gen}: connection=${connection}, hasQR=${!!qr}`)
420
489
 
421
490
  if (qr) {
@@ -444,16 +513,17 @@ const whatsapp: PlatformConnector = {
444
513
  if (!stopped) {
445
514
  // Recreate auth dir and state for fresh start
446
515
  fs.mkdirSync(authDir, { recursive: true })
447
- setTimeout(startSocket, 1000)
516
+ scheduleReconnect(1000)
448
517
  }
449
518
  } else if (reason === 440) {
450
519
  // Conflict — another session replaced this one. Do NOT reconnect
451
520
  // (reconnecting would create a ping-pong loop with the other session)
452
521
  console.log(`[whatsapp] Session conflict (replaced by another connection) — stopping`)
453
522
  instance.authenticated = false
523
+ instance.onCrash?.('Session conflict — replaced by another connection')
454
524
  } else if (!stopped) {
455
525
  console.log(`[whatsapp] Reconnecting in 3s...`)
456
- setTimeout(startSocket, 3000)
526
+ scheduleReconnect(3000)
457
527
  } else {
458
528
  console.log(`[whatsapp] Disconnected permanently`)
459
529
  }
@@ -518,7 +588,10 @@ const whatsapp: PlatformConnector = {
518
588
 
519
589
  const jid = msg.key.remoteJid || ''
520
590
  const latestConnector = (loadConnectors()[connector.id] as Connector | undefined) || connector
521
- const allowedJids = parseAllowedIdentifiers(latestConnector.config?.allowedJids)
591
+ const allowedJids = resolveWhatsAppAllowedIdentifiers({
592
+ configuredAllowedJids: latestConnector.config?.allowedJids,
593
+ settingsContacts: loadSettings().whatsappApprovedContacts,
594
+ })
522
595
 
523
596
  // Match allowed JIDs using normalized numbers
524
597
  // Self-chat always passes the filter (it's the bot's own account)
@@ -580,18 +653,17 @@ const whatsapp: PlatformConnector = {
580
653
 
581
654
  try {
582
655
  await sock!.sendPresenceUpdate('composing', jid)
583
- const response = await onMessage(inbound)
656
+ const reply = await resolveConnectorIngressReply(onMessage, inbound)
584
657
  await sock!.sendPresenceUpdate('paused', jid)
585
-
586
- if (!isNoMessage(response)) {
587
- const sent = await instance.sendMessage?.(jid, response)
588
- await recordConnectorOutboundDelivery({
589
- connectorId: connector.id,
590
- inbound,
591
- messageId: sent?.messageId,
592
- state: 'sent',
593
- })
594
- }
658
+ if (!reply) continue
659
+
660
+ const sent = await instance.sendMessage?.(jid, reply.visibleText)
661
+ await recordConnectorOutboundDelivery({
662
+ connectorId: connector.id,
663
+ inbound,
664
+ messageId: sent?.messageId,
665
+ state: 'sent',
666
+ })
595
667
  } catch (err: any) {
596
668
  console.error(`[whatsapp] Error handling message:`, err.message)
597
669
  try {