@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,422 @@
1
+ import { genId } from '@/lib/id'
2
+ import { getProvider } from '@/lib/providers'
3
+ import type { Agent, Connector, MessageSource, Session } from '@/types'
4
+ import { WORKSPACE_DIR } from '../data-dir'
5
+ import { ensureAgentThreadSession } from '../agent-thread-session'
6
+ import { syncSessionArchiveMemory } from '../session-archive-memory'
7
+ import { loadAgents, loadSessions, loadStoredItem, upsertStoredItem } from '../storage'
8
+ import { notify } from '../ws-hub'
9
+ import {
10
+ buildConnectorConversationKey,
11
+ getConnectorSessionStaleness,
12
+ resetConnectorSessionRuntime,
13
+ resolveConnectorSessionPolicy,
14
+ } from './policy'
15
+ import { resolveThreadPersonaLabel } from './thread-context'
16
+ import type { InboundMessage } from './types'
17
+
18
+ export type ConnectorSession = Session
19
+ export type ConnectorAgent = Agent
20
+
21
+ export interface ConnectorRuntimeDefaults {
22
+ provider: Session['provider']
23
+ model: string
24
+ apiEndpoint: string | null
25
+ thinkingLevel: Session['connectorThinkLevel']
26
+ }
27
+
28
+ export interface ResolvedDirectSession {
29
+ session: ConnectorSession
30
+ sessionKey: string
31
+ wasCreated: boolean
32
+ staleReason?: string | null
33
+ clearedMessages?: number
34
+ }
35
+
36
+ export function findDirectSessionForInbound(connector: Connector, msg: InboundMessage): ConnectorSession | null {
37
+ if (connector.chatroomId) return null
38
+ const effectiveAgentId = msg.agentIdOverride || connector.agentId
39
+ const channelIds = new Set([msg.channelId, msg.channelIdAlt].filter(Boolean))
40
+ const senderIds = new Set([msg.senderId, msg.senderIdAlt].filter(Boolean))
41
+ const sessions = Object.values(loadSessions() as Record<string, ConnectorSession>)
42
+ const candidates = sessions.filter((session) =>
43
+ session?.agentId === effectiveAgentId
44
+ && session?.connectorContext?.connectorId === connector.id
45
+ && (
46
+ channelIds.has(session?.connectorContext?.channelId || '')
47
+ || channelIds.has(session?.connectorContext?.channelIdAlt || '')
48
+ ),
49
+ )
50
+ if (msg.threadId) {
51
+ const threadExact = candidates.find((session) => session?.connectorContext?.threadId === msg.threadId)
52
+ if (threadExact) return threadExact
53
+ }
54
+ const senderExact = candidates.find((session) =>
55
+ senderIds.has(session?.connectorContext?.senderId || '')
56
+ || senderIds.has(session?.connectorContext?.senderIdAlt || ''),
57
+ )
58
+ if (senderExact) return senderExact
59
+ return candidates[0] || null
60
+ }
61
+
62
+ export function persistSessionRecord(session: ConnectorSession): void {
63
+ session.updatedAt = Date.now()
64
+ upsertStoredItem('sessions', session.id, session)
65
+ notify('sessions')
66
+ }
67
+
68
+ export function updateSessionConnectorContext(
69
+ session: ConnectorSession,
70
+ connector: Connector,
71
+ msg: InboundMessage,
72
+ sessionKey: string,
73
+ ): void {
74
+ const policy = resolveConnectorSessionPolicy(connector, msg, session)
75
+ session.connectorContext = {
76
+ ...(session.connectorContext || {}),
77
+ connectorId: connector.id,
78
+ platform: connector.platform,
79
+ channelId: msg.channelId,
80
+ channelIdAlt: msg.channelIdAlt || session.connectorContext?.channelIdAlt || null,
81
+ senderId: msg.senderId,
82
+ senderIdAlt: msg.senderIdAlt || session.connectorContext?.senderIdAlt || null,
83
+ senderName: msg.senderName,
84
+ sessionKey,
85
+ peerKey: msg.senderIdAlt || msg.senderId,
86
+ scope: policy.scope,
87
+ replyMode: policy.replyMode,
88
+ threadBinding: policy.threadBinding,
89
+ groupPolicy: policy.groupPolicy,
90
+ threadId: msg.threadId || session.connectorContext?.threadId || null,
91
+ threadTitle: msg.threadTitle || session.connectorContext?.threadTitle || null,
92
+ threadPersonaLabel: resolveThreadPersonaLabel(msg) || session.connectorContext?.threadPersonaLabel || null,
93
+ threadParentChannelId: msg.threadParentChannelId || session.connectorContext?.threadParentChannelId || null,
94
+ threadParentChannelName: msg.threadParentChannelName || session.connectorContext?.threadParentChannelName || null,
95
+ isGroup: !!msg.isGroup,
96
+ lastInboundAt: Date.now(),
97
+ lastInboundMessageId: msg.messageId || null,
98
+ lastInboundReplyToMessageId: msg.replyToMessageId || null,
99
+ lastInboundThreadId: msg.threadId || null,
100
+ lastOutboundAt: session.connectorContext?.lastOutboundAt ?? null,
101
+ lastOutboundMessageId: session.connectorContext?.lastOutboundMessageId ?? null,
102
+ lastResetAt: session.connectorContext?.lastResetAt ?? null,
103
+ lastResetReason: session.connectorContext?.lastResetReason ?? null,
104
+ }
105
+ }
106
+
107
+ export function describeSessionControls(
108
+ session: ConnectorSession,
109
+ connector: Connector,
110
+ msg: InboundMessage,
111
+ ): string {
112
+ const policy = resolveConnectorSessionPolicy(connector, msg, session)
113
+ const context = session.connectorContext || {}
114
+ const sessionAgeSec = Math.max(0, Math.round((Date.now() - (session.createdAt || Date.now())) / 1000))
115
+ const idleSec = Math.max(0, Math.round((Date.now() - (session.lastActiveAt || Date.now())) / 1000))
116
+ return [
117
+ `Session controls for ${connector.platform}/${connector.name}:`,
118
+ `- Session: ${session.id}`,
119
+ `- Scope: ${policy.scope}`,
120
+ `- Reply mode: ${policy.replyMode}`,
121
+ `- Thread binding: ${policy.threadBinding}`,
122
+ `- Group policy: ${policy.groupPolicy}`,
123
+ `- Reset mode: ${policy.resetMode}`,
124
+ `- Idle timeout: ${policy.idleTimeoutSec ?? 0}s`,
125
+ `- Max age: ${policy.maxAgeSec ?? 0}s`,
126
+ `- Daily reset: ${policy.dailyResetAt || 'off'}`,
127
+ `- Reset timezone: ${policy.resetTimezone || 'local'}`,
128
+ `- Debounce: ${policy.inboundDebounceMs}ms`,
129
+ `- Typing indicators: ${policy.typingIndicators ? 'on' : 'off'}`,
130
+ `- Thinking: ${policy.thinkingLevel || session.thinkingLevel || 'inherit'}`,
131
+ `- Model: ${session.provider}/${session.model}`,
132
+ `- Last outbound message: ${context.lastOutboundMessageId || 'none'}`,
133
+ `- Thread: ${context.threadId || 'none'}`,
134
+ `- Thread title: ${context.threadTitle || 'none'}`,
135
+ `- Thread persona: ${context.threadPersonaLabel || 'none'}`,
136
+ `- Session age: ${sessionAgeSec}s`,
137
+ `- Idle for: ${idleSec}s`,
138
+ ].join('\n')
139
+ }
140
+
141
+ function normalizeSessionSettingKey(raw: string): string {
142
+ return raw.trim().toLowerCase().replace(/[_-]+/g, '')
143
+ }
144
+
145
+ export function applySessionSetting(
146
+ session: ConnectorSession,
147
+ keyRaw: string,
148
+ valueRaw: string,
149
+ msg: InboundMessage,
150
+ ): string {
151
+ const key = normalizeSessionSettingKey(keyRaw)
152
+ const value = valueRaw.trim()
153
+ const asInt = () => {
154
+ const parsed = Number.parseInt(value, 10)
155
+ if (!Number.isFinite(parsed) || parsed < 0) {
156
+ throw new Error(`Invalid numeric value for ${keyRaw}: ${valueRaw}`)
157
+ }
158
+ return parsed
159
+ }
160
+ const asEnum = <T extends string>(allowed: readonly T[], label: string): T | null => {
161
+ if (!value) return null
162
+ const normalized = value.toLowerCase()
163
+ if ((allowed as readonly string[]).includes(normalized)) return normalized as T
164
+ throw new Error(`Invalid ${label}. Use one of: ${allowed.join(', ')}.`)
165
+ }
166
+
167
+ switch (key) {
168
+ case 'think':
169
+ case 'thinkinglevel':
170
+ session.connectorThinkLevel = asEnum(['minimal', 'low', 'medium', 'high'] as const, '/think level')
171
+ return `Connector thinking level set to ${session.connectorThinkLevel || 'inherit'}.`
172
+ case 'reply':
173
+ case 'replymode':
174
+ session.connectorReplyMode = asEnum(['off', 'first', 'all'] as const, 'reply mode')
175
+ return `Reply mode set to ${session.connectorReplyMode || 'inherit'}.`
176
+ case 'scope':
177
+ case 'sessionscope':
178
+ session.connectorSessionScope = asEnum(['main', 'channel', 'peer', 'channel-peer', 'thread'] as const, 'session scope')
179
+ return `Session scope set to ${session.connectorSessionScope || 'inherit'}.`
180
+ case 'thread':
181
+ case 'threadbinding':
182
+ session.connectorThreadBinding = asEnum(['off', 'prefer', 'strict'] as const, 'thread binding')
183
+ if (!value) {
184
+ session.connectorContext = { ...(session.connectorContext || {}), threadId: null }
185
+ } else if (session.connectorThreadBinding === 'strict' && msg.threadId) {
186
+ session.connectorContext = { ...(session.connectorContext || {}), threadId: msg.threadId }
187
+ }
188
+ return `Thread binding set to ${session.connectorThreadBinding || 'inherit'}.`
189
+ case 'group':
190
+ case 'grouppolicy':
191
+ session.connectorGroupPolicy = asEnum(['open', 'mention', 'reply-or-mention', 'disabled'] as const, 'group policy')
192
+ return `Group policy set to ${session.connectorGroupPolicy || 'inherit'}.`
193
+ case 'idle':
194
+ case 'idletimeout':
195
+ session.connectorIdleTimeoutSec = asInt()
196
+ return `Idle timeout set to ${session.connectorIdleTimeoutSec}s.`
197
+ case 'maxage':
198
+ session.connectorMaxAgeSec = asInt()
199
+ return `Max age set to ${session.connectorMaxAgeSec}s.`
200
+ case 'reset':
201
+ case 'resetmode': {
202
+ const normalized = value.toLowerCase()
203
+ if (!value) {
204
+ session.sessionResetMode = null
205
+ return 'Reset mode set to inherit.'
206
+ }
207
+ if (normalized !== 'idle' && normalized !== 'daily') {
208
+ throw new Error('Reset mode must be "idle" or "daily".')
209
+ }
210
+ session.sessionResetMode = normalized
211
+ return `Reset mode set to ${session.sessionResetMode}.`
212
+ }
213
+ case 'daily':
214
+ case 'dailyreset':
215
+ case 'dailyresetat':
216
+ if (!value) {
217
+ session.sessionDailyResetAt = null
218
+ return 'Daily reset time cleared.'
219
+ }
220
+ if (!/^\d{1,2}:\d{2}$/.test(value)) {
221
+ throw new Error('Daily reset time must be in HH:MM format.')
222
+ }
223
+ session.sessionDailyResetAt = value
224
+ return `Daily reset time set to ${session.sessionDailyResetAt}.`
225
+ case 'timezone':
226
+ case 'resettimezone':
227
+ session.sessionResetTimezone = value || null
228
+ return `Reset timezone set to ${session.sessionResetTimezone || 'inherit/local'}.`
229
+ case 'model':
230
+ session.model = value
231
+ return `Model set to ${session.model}.`
232
+ case 'provider': {
233
+ const provider = getProvider(value)
234
+ if (!provider) {
235
+ throw new Error(`Unknown provider "${value}".`)
236
+ }
237
+ session.provider = provider.id as Session['provider']
238
+ session.apiEndpoint = provider.defaultEndpoint || session.apiEndpoint || null
239
+ return `Provider set to ${session.provider}.`
240
+ }
241
+ default:
242
+ throw new Error(`Unknown session setting "${keyRaw}".`)
243
+ }
244
+ }
245
+
246
+ export function applyConnectorRuntimeDefaults(
247
+ session: ConnectorSession,
248
+ defaults: ConnectorRuntimeDefaults,
249
+ ): void {
250
+ session.provider = defaults.provider
251
+ session.model = defaults.model
252
+ session.apiEndpoint = defaults.apiEndpoint
253
+ session.connectorThinkLevel = defaults.thinkingLevel
254
+ }
255
+
256
+ export function resolveDirectSession(params: {
257
+ connector: Connector
258
+ msg: InboundMessage
259
+ agent: ConnectorAgent
260
+ }): ResolvedDirectSession {
261
+ const { connector, msg, agent } = params
262
+ const policySeed = resolveConnectorSessionPolicy(connector, msg)
263
+ const providerInfo = policySeed.providerOverride ? getProvider(policySeed.providerOverride) : null
264
+ const defaultProvider: Session['provider'] = providerInfo?.id || (agent.provider === 'claude-cli' ? 'anthropic' : agent.provider)
265
+ const defaultModel = policySeed.modelOverride || agent.model
266
+ const defaultApiEndpoint = agent.apiEndpoint || providerInfo?.defaultEndpoint || null
267
+ const runtimeDefaults: ConnectorRuntimeDefaults = {
268
+ provider: defaultProvider,
269
+ model: defaultModel,
270
+ apiEndpoint: defaultApiEndpoint,
271
+ thinkingLevel: policySeed.thinkingLevel || null,
272
+ }
273
+ const sessionKey = buildConnectorConversationKey({
274
+ connector,
275
+ msg,
276
+ agentId: agent.id,
277
+ policy: policySeed,
278
+ })
279
+ const sessions = loadSessions()
280
+ let session = Object.values(sessions as Record<string, ConnectorSession>).find((item) => item?.name === sessionKey)
281
+ if (!session) {
282
+ session = findDirectSessionForInbound(connector, msg) || undefined
283
+ }
284
+ let wasCreated = false
285
+ if (!session) {
286
+ const id = genId()
287
+ session = {
288
+ id,
289
+ name: sessionKey,
290
+ cwd: WORKSPACE_DIR,
291
+ user: 'connector',
292
+ provider: defaultProvider,
293
+ model: defaultModel,
294
+ credentialId: agent.credentialId || null,
295
+ fallbackCredentialIds: Array.isArray(agent.fallbackCredentialIds) ? [...agent.fallbackCredentialIds] : [],
296
+ apiEndpoint: defaultApiEndpoint,
297
+ claudeSessionId: null,
298
+ codexThreadId: null,
299
+ opencodeSessionId: null,
300
+ delegateResumeIds: {
301
+ claudeCode: null,
302
+ codex: null,
303
+ opencode: null,
304
+ gemini: null,
305
+ },
306
+ messages: [],
307
+ createdAt: Date.now(),
308
+ lastActiveAt: Date.now(),
309
+ sessionType: 'human' as const,
310
+ agentId: agent.id,
311
+ plugins: agent.plugins || agent.tools || [],
312
+ thinkingLevel: agent.thinkingLevel || null,
313
+ connectorThinkLevel: policySeed.thinkingLevel || null,
314
+ }
315
+ wasCreated = true
316
+ }
317
+ session.name = sessionKey
318
+ session.agentId = agent.id
319
+ session.plugins = Array.isArray(session.plugins) ? session.plugins : (agent.plugins || agent.tools || [])
320
+ session.provider = defaultProvider
321
+ session.model = defaultModel
322
+ if (session.credentialId === undefined) session.credentialId = agent.credentialId || null
323
+ if (!Array.isArray(session.fallbackCredentialIds) && Array.isArray(agent.fallbackCredentialIds)) {
324
+ session.fallbackCredentialIds = [...agent.fallbackCredentialIds]
325
+ }
326
+ if (session.apiEndpoint === undefined || session.apiEndpoint === null) session.apiEndpoint = defaultApiEndpoint
327
+ if ((session.connectorThinkLevel === undefined || session.connectorThinkLevel === null) && policySeed.thinkingLevel) {
328
+ session.connectorThinkLevel = policySeed.thinkingLevel
329
+ }
330
+
331
+ const policy = resolveConnectorSessionPolicy(connector, msg, session)
332
+ const staleness = getConnectorSessionStaleness(session, policy)
333
+ let clearedMessages = 0
334
+ if (staleness.stale) {
335
+ try { syncSessionArchiveMemory(session, { agent }) } catch { /* archive sync is best-effort */ }
336
+ clearedMessages = resetConnectorSessionRuntime(session, staleness.reason || 'session_refresh')
337
+ applyConnectorRuntimeDefaults(session, {
338
+ ...runtimeDefaults,
339
+ thinkingLevel: policySeed.thinkingLevel || session.connectorThinkLevel || null,
340
+ })
341
+ }
342
+ updateSessionConnectorContext(session, connector, msg, sessionKey)
343
+ upsertStoredItem('sessions', session.id, session)
344
+ return {
345
+ session,
346
+ sessionKey,
347
+ wasCreated,
348
+ staleReason: staleness.reason || null,
349
+ clearedMessages,
350
+ }
351
+ }
352
+
353
+ function mirrorConnectorMessageToAgentThread(
354
+ session: ConnectorSession,
355
+ message: Record<string, unknown>,
356
+ ): void {
357
+ if (!session.agentId) return
358
+ if (typeof session.name !== 'string' || !session.name.startsWith('connector:')) return
359
+
360
+ const agents = loadAgents()
361
+ const agent = agents[session.agentId]
362
+ const threadSession = agent?.threadSessionId
363
+ ? loadStoredItem('sessions', agent.threadSessionId) as ConnectorSession | null
364
+ : ensureAgentThreadSession(session.agentId)
365
+ if (!threadSession || threadSession.id === session.id) return
366
+
367
+ const last = Array.isArray(threadSession.messages) ? threadSession.messages[threadSession.messages.length - 1] : null
368
+ const source = message.source as MessageSource | undefined
369
+ const lastSource = (last?.source || null) as MessageSource | null
370
+ if (
371
+ last
372
+ && last.role === message.role
373
+ && last.text === message.text
374
+ && lastSource?.platform === source?.platform
375
+ && lastSource?.connectorId === source?.connectorId
376
+ && lastSource?.channelId === source?.channelId
377
+ && lastSource?.messageId === source?.messageId
378
+ ) {
379
+ return
380
+ }
381
+
382
+ if (!Array.isArray(threadSession.messages)) threadSession.messages = []
383
+ threadSession.messages.push({
384
+ ...message,
385
+ time: typeof message.time === 'number' ? message.time : Date.now(),
386
+ historyExcluded: true,
387
+ } as Session['messages'][number])
388
+ threadSession.lastActiveAt = Date.now()
389
+
390
+ upsertStoredItem('sessions', threadSession.id, threadSession)
391
+ notify('sessions')
392
+ notify(`messages:${threadSession.id}`)
393
+ }
394
+
395
+ export function pushSessionMessage(
396
+ session: ConnectorSession,
397
+ role: 'user' | 'assistant',
398
+ text: string,
399
+ extra: Record<string, unknown> = {},
400
+ ): void {
401
+ if (!text.trim()) return
402
+ if (!Array.isArray(session.messages)) session.messages = []
403
+ const message = { role, text: text.trim(), time: Date.now(), ...extra }
404
+ session.messages.push(message)
405
+ session.lastActiveAt = Date.now()
406
+ mirrorConnectorMessageToAgentThread(session, message)
407
+ }
408
+
409
+ export function modelHistoryTail(
410
+ messages: Session['messages'] | null | undefined,
411
+ limit = 20,
412
+ ): Session['messages'] {
413
+ const filtered = (Array.isArray(messages) ? messages : []).filter((message) => message?.historyExcluded !== true)
414
+ return filtered.slice(-limit)
415
+ }
416
+
417
+ export function persistSession(session: ConnectorSession): void {
418
+ session.updatedAt = Date.now()
419
+ upsertStoredItem('sessions', session.id, session)
420
+ notify('sessions')
421
+ notify(`messages:${session.id}`)
422
+ }
@@ -1,8 +1,8 @@
1
1
  import { spawn, execSync } from 'child_process'
2
2
  import type { ChildProcess } from 'child_process'
3
3
  import type { Connector } from '@/types'
4
- import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
5
- import { isNoMessage } from './manager'
4
+ import type { PlatformConnector, ConnectorInstance, InboundMessage, ConnectorIngressResult } from './types'
5
+ import { resolveConnectorIngressReply } from './ingress-delivery'
6
6
 
7
7
  const signal: PlatformConnector = {
8
8
  async start(connector, _botToken, onMessage): Promise<ConnectorInstance> {
@@ -129,7 +129,7 @@ const signal: PlatformConnector = {
129
129
  export async function handleSignalEvent(
130
130
  event: any,
131
131
  connector: Connector,
132
- onMessage: (msg: InboundMessage) => Promise<string>,
132
+ onMessage: (msg: InboundMessage) => Promise<ConnectorIngressResult>,
133
133
  ) {
134
134
  // signal-cli JSON output structure varies; handle the common envelope format
135
135
  const envelope = event.envelope || event
@@ -151,8 +151,8 @@ export async function handleSignalEvent(
151
151
  }
152
152
 
153
153
  try {
154
- const response = await onMessage(inbound)
155
- if (isNoMessage(response)) return
154
+ const reply = await resolveConnectorIngressReply(onMessage, inbound)
155
+ if (!reply) return
156
156
 
157
157
  // Send reply back
158
158
  const cliPath = connector.config.signalCliPath || 'signal-cli'
@@ -165,14 +165,14 @@ export async function handleSignalEvent(
165
165
  method: 'POST',
166
166
  headers: { 'Content-Type': 'application/json' },
167
167
  body: JSON.stringify({
168
- message: response,
168
+ message: reply.visibleText,
169
169
  number: phoneNumber,
170
170
  recipients: [inbound.channelId],
171
171
  }),
172
172
  })
173
173
  } else {
174
174
  execSync(
175
- `${cliPath} -u ${phoneNumber} send -m ${JSON.stringify(response)} ${inbound.channelId}`,
175
+ `${cliPath} -u ${phoneNumber} send -m ${JSON.stringify(reply.visibleText)} ${inbound.channelId}`,
176
176
  { timeout: 15_000 },
177
177
  )
178
178
  }
@@ -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, InboundThreadHistoryEntry } from './types'
6
+ import { resolveConnectorIngressReply } from './ingress-delivery'
7
+ import { deliverChunkedConnectorText } from './delivery'
6
8
  import { downloadInboundMediaToUpload, inferInboundMediaType, mimeFromPath, isImageMime } from './media'
7
- import { getConnectorReplySendOptions, isNoMessage, recordConnectorOutboundDelivery } from './manager'
9
+ import { dedup, errorMessage } from '@/lib/shared-utils'
8
10
 
9
11
  function normalizeSlackEmoji(input: string): string {
10
12
  const raw = input.trim().replace(/^:|:$/g, '')
@@ -54,7 +56,7 @@ async function hydrateSlackThreadContext(params: {
54
56
  const messages = Array.isArray((result as any)?.messages) ? (result as any).messages as any[] : []
55
57
  if (!messages.length) return
56
58
 
57
- const userIds = [...new Set(messages.map((message) => typeof message?.user === 'string' ? message.user : '').filter(Boolean))]
59
+ const userIds = dedup(messages.map((message) => typeof message?.user === 'string' ? message.user : '').filter(Boolean))
58
60
  const nameMap = new Map<string, string>()
59
61
  await Promise.all(userIds.map(async (userId) => {
60
62
  const name = await resolveSlackUserDisplayName(params.client, userId)
@@ -97,7 +99,7 @@ async function hydrateSlackThreadContext(params: {
97
99
  params.inbound.threadPersonaLabel = params.inbound.threadTitle
98
100
  params.inbound.threadHistory = history.length ? history : undefined
99
101
  } catch (err: unknown) {
100
- console.warn(`[slack] Thread context bootstrap failed: ${err instanceof Error ? err.message : String(err)}`)
102
+ console.warn(`[slack] Thread context bootstrap failed: ${errorMessage(err)}`)
101
103
  }
102
104
  }
103
105
 
@@ -242,38 +244,22 @@ const slack: PlatformConnector = {
242
244
  await hydrateSlackThreadContext({ client, inbound, currentTs: msg.ts || undefined, botUserId })
243
245
 
244
246
  try {
245
- const response = await onMessage(inbound)
246
-
247
- if (isNoMessage(response)) return
248
-
249
- const replyOptions = getConnectorReplySendOptions({ connectorId: connector.id, inbound })
250
- const threadTs = replyOptions.threadId || replyOptions.replyToMessageId
251
- let lastMessageId: string | undefined
252
-
253
- // Slack has a 4000 char limit for messages
254
- if (response.length <= 4000) {
255
- const sent = await client.chat.postMessage({
256
- channel: channelId,
257
- text: response,
258
- thread_ts: threadTs,
259
- })
260
- lastMessageId = sent.ts || undefined
261
- } else {
262
- const chunks = response.match(/[\s\S]{1,3990}/g) || [response]
263
- for (const chunk of chunks) {
247
+ const reply = await resolveConnectorIngressReply(onMessage, inbound)
248
+ if (!reply) return
249
+ await deliverChunkedConnectorText({
250
+ connectorId: connector.id,
251
+ inbound,
252
+ text: reply.visibleText,
253
+ maxSingleMessageLength: 4000,
254
+ chunkLength: 3990,
255
+ sendChunk: async (chunk, meta) => {
264
256
  const sent = await client.chat.postMessage({
265
257
  channel: channelId,
266
258
  text: chunk,
267
- thread_ts: threadTs,
259
+ thread_ts: meta.threadId || meta.replyToMessageId,
268
260
  })
269
- lastMessageId = sent.ts || undefined
270
- }
271
- }
272
- await recordConnectorOutboundDelivery({
273
- connectorId: connector.id,
274
- inbound,
275
- messageId: lastMessageId,
276
- state: 'sent',
261
+ return sent.ts || undefined
262
+ },
277
263
  })
278
264
  } catch (err: any) {
279
265
  console.error(`[slack] Error handling message:`, err.message)
@@ -316,19 +302,22 @@ const slack: PlatformConnector = {
316
302
  })
317
303
 
318
304
  try {
319
- const response = await onMessage(inbound)
320
- if (isNoMessage(response)) return
321
- const replyOptions = getConnectorReplySendOptions({ connectorId: connector.id, inbound })
322
- const sent = await client.chat.postMessage({
323
- channel: event.channel,
324
- text: response,
325
- thread_ts: replyOptions.threadId || replyOptions.replyToMessageId,
326
- })
327
- await recordConnectorOutboundDelivery({
305
+ const reply = await resolveConnectorIngressReply(onMessage, inbound)
306
+ if (!reply) return
307
+ await deliverChunkedConnectorText({
328
308
  connectorId: connector.id,
329
309
  inbound,
330
- messageId: sent.ts || undefined,
331
- state: 'sent',
310
+ text: reply.visibleText,
311
+ maxSingleMessageLength: 4000,
312
+ chunkLength: 3990,
313
+ sendChunk: async (chunk, meta) => {
314
+ const sent = await client.chat.postMessage({
315
+ channel: event.channel,
316
+ text: chunk,
317
+ thread_ts: meta.threadId || meta.replyToMessageId,
318
+ })
319
+ return sent.ts || undefined
320
+ },
332
321
  })
333
322
  } catch (err: any) {
334
323
  console.error(`[slack] Error handling mention:`, err.message)
@@ -340,7 +329,7 @@ const slack: PlatformConnector = {
340
329
 
341
330
  let appStopped = false
342
331
 
343
- return {
332
+ const instance: ConnectorInstance = {
344
333
  connector,
345
334
  isAlive() {
346
335
  return !appStopped && !!app.client
@@ -437,6 +426,17 @@ const slack: PlatformConnector = {
437
426
  console.log(`[slack] Bot disconnected`)
438
427
  },
439
428
  }
429
+
430
+ // Bolt emits 'error' on unrecoverable failures (auth revoked, socket closed permanently)
431
+ app.error(async (error) => {
432
+ const errMsg = error.original?.message || error.message || String(error)
433
+ console.error(`[slack] App error:`, errMsg)
434
+ if (appStopped) return
435
+ appStopped = true
436
+ instance.onCrash?.(`Slack error: ${errMsg}`)
437
+ })
438
+
439
+ return instance
440
440
  },
441
441
  }
442
442
 
@@ -1,5 +1,5 @@
1
1
  import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
2
- import { isNoMessage } from './manager'
2
+ import { resolveConnectorIngressReply } from './ingress-delivery'
3
3
 
4
4
  const teams: PlatformConnector = {
5
5
  async start(connector, botToken, onMessage): Promise<ConnectorInstance> {
@@ -45,9 +45,9 @@ const teams: PlatformConnector = {
45
45
  }
46
46
 
47
47
  try {
48
- const response = await onMessage(inbound)
49
- if (isNoMessage(response)) return
50
- await context.sendActivity(response)
48
+ const reply = await resolveConnectorIngressReply(onMessage, inbound)
49
+ if (!reply) return
50
+ await context.sendActivity(reply.visibleText)
51
51
  } catch (err: any) {
52
52
  console.error(`[teams] Error handling message:`, err.message)
53
53
  try {