@swarmclawai/swarmclaw 0.2.0

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 (319) hide show
  1. package/README.md +577 -0
  2. package/bin/server-cmd.js +359 -0
  3. package/bin/swarmclaw.js +29 -0
  4. package/bin/swarmclaw.mjs +1504 -0
  5. package/next.config.ts +33 -0
  6. package/package.json +112 -0
  7. package/postcss.config.mjs +7 -0
  8. package/public/branding/swarmclaw-org-avatar.png +0 -0
  9. package/public/branding/swarmclaw-org-avatar.svg +58 -0
  10. package/public/file.svg +1 -0
  11. package/public/globe.svg +1 -0
  12. package/public/next.svg +1 -0
  13. package/public/screenshots/agents.png +0 -0
  14. package/public/screenshots/connectors.png +0 -0
  15. package/public/screenshots/dashboard.png +0 -0
  16. package/public/screenshots/new-session-openclaw.png +0 -0
  17. package/public/screenshots/providers.png +0 -0
  18. package/public/screenshots/schedules.png +0 -0
  19. package/public/screenshots/tasks.png +0 -0
  20. package/public/vercel.svg +1 -0
  21. package/public/window.svg +1 -0
  22. package/src/app/api/agents/[id]/route.ts +30 -0
  23. package/src/app/api/agents/[id]/thread/route.ts +66 -0
  24. package/src/app/api/agents/generate/route.ts +42 -0
  25. package/src/app/api/agents/route.ts +33 -0
  26. package/src/app/api/auth/route.ts +25 -0
  27. package/src/app/api/claude-skills/route.ts +42 -0
  28. package/src/app/api/clawhub/install/route.ts +39 -0
  29. package/src/app/api/clawhub/search/route.ts +11 -0
  30. package/src/app/api/connectors/[id]/route.ts +79 -0
  31. package/src/app/api/connectors/route.ts +60 -0
  32. package/src/app/api/credentials/[id]/route.ts +14 -0
  33. package/src/app/api/credentials/route.ts +31 -0
  34. package/src/app/api/daemon/health-check/route.ts +11 -0
  35. package/src/app/api/daemon/route.ts +22 -0
  36. package/src/app/api/dirs/pick/route.ts +60 -0
  37. package/src/app/api/dirs/route.ts +29 -0
  38. package/src/app/api/documents/[id]/route.ts +47 -0
  39. package/src/app/api/documents/route.ts +93 -0
  40. package/src/app/api/files/serve/route.ts +69 -0
  41. package/src/app/api/generate/info/route.ts +12 -0
  42. package/src/app/api/generate/route.ts +106 -0
  43. package/src/app/api/ip/route.ts +6 -0
  44. package/src/app/api/knowledge/[id]/route.ts +61 -0
  45. package/src/app/api/knowledge/route.ts +48 -0
  46. package/src/app/api/knowledge/upload/route.ts +86 -0
  47. package/src/app/api/logs/route.ts +65 -0
  48. package/src/app/api/mcp-servers/[id]/route.ts +32 -0
  49. package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
  50. package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
  51. package/src/app/api/mcp-servers/route.ts +27 -0
  52. package/src/app/api/memory/[id]/route.ts +126 -0
  53. package/src/app/api/memory/maintenance/route.ts +63 -0
  54. package/src/app/api/memory/route.ts +111 -0
  55. package/src/app/api/memory-images/[filename]/route.ts +36 -0
  56. package/src/app/api/orchestrator/run/route.ts +43 -0
  57. package/src/app/api/plugins/install/route.ts +58 -0
  58. package/src/app/api/plugins/marketplace/route.ts +33 -0
  59. package/src/app/api/plugins/route.ts +21 -0
  60. package/src/app/api/preview-server/route.ts +339 -0
  61. package/src/app/api/providers/[id]/models/route.ts +29 -0
  62. package/src/app/api/providers/[id]/route.ts +34 -0
  63. package/src/app/api/providers/configs/route.ts +7 -0
  64. package/src/app/api/providers/ollama/route.ts +30 -0
  65. package/src/app/api/providers/openclaw/health/route.ts +23 -0
  66. package/src/app/api/providers/route.ts +28 -0
  67. package/src/app/api/runs/[id]/route.ts +9 -0
  68. package/src/app/api/runs/route.ts +13 -0
  69. package/src/app/api/schedules/[id]/route.ts +28 -0
  70. package/src/app/api/schedules/[id]/run/route.ts +104 -0
  71. package/src/app/api/schedules/route.ts +78 -0
  72. package/src/app/api/secrets/[id]/route.ts +29 -0
  73. package/src/app/api/secrets/route.ts +42 -0
  74. package/src/app/api/sessions/[id]/browser/route.ts +13 -0
  75. package/src/app/api/sessions/[id]/chat/route.ts +96 -0
  76. package/src/app/api/sessions/[id]/clear/route.ts +19 -0
  77. package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
  78. package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
  79. package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
  80. package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
  81. package/src/app/api/sessions/[id]/messages/route.ts +9 -0
  82. package/src/app/api/sessions/[id]/retry/route.ts +28 -0
  83. package/src/app/api/sessions/[id]/route.ts +103 -0
  84. package/src/app/api/sessions/[id]/stop/route.ts +13 -0
  85. package/src/app/api/sessions/heartbeat/route.ts +26 -0
  86. package/src/app/api/sessions/route.ts +85 -0
  87. package/src/app/api/settings/route.ts +58 -0
  88. package/src/app/api/setup/check-provider/route.ts +326 -0
  89. package/src/app/api/setup/doctor/route.ts +250 -0
  90. package/src/app/api/skills/[id]/route.ts +40 -0
  91. package/src/app/api/skills/import/route.ts +69 -0
  92. package/src/app/api/skills/route.ts +28 -0
  93. package/src/app/api/tasks/[id]/route.ts +102 -0
  94. package/src/app/api/tasks/route.ts +115 -0
  95. package/src/app/api/tts/route.ts +40 -0
  96. package/src/app/api/upload/route.ts +18 -0
  97. package/src/app/api/uploads/[filename]/route.ts +59 -0
  98. package/src/app/api/usage/route.ts +35 -0
  99. package/src/app/api/version/route.ts +81 -0
  100. package/src/app/api/version/update/route.ts +95 -0
  101. package/src/app/api/webhooks/[id]/history/route.ts +13 -0
  102. package/src/app/api/webhooks/[id]/route.ts +204 -0
  103. package/src/app/api/webhooks/route.ts +37 -0
  104. package/src/app/favicon.ico +0 -0
  105. package/src/app/globals.css +370 -0
  106. package/src/app/layout.tsx +52 -0
  107. package/src/app/page.tsx +172 -0
  108. package/src/cli/index.js +1232 -0
  109. package/src/cli/index.test.js +281 -0
  110. package/src/cli/index.ts +1158 -0
  111. package/src/cli/spec.js +284 -0
  112. package/src/components/agents/agent-card.tsx +219 -0
  113. package/src/components/agents/agent-chat-list.tsx +165 -0
  114. package/src/components/agents/agent-list.tsx +110 -0
  115. package/src/components/agents/agent-sheet.tsx +1220 -0
  116. package/src/components/auth/access-key-gate.tsx +248 -0
  117. package/src/components/auth/setup-wizard.tsx +940 -0
  118. package/src/components/auth/user-picker.tsx +88 -0
  119. package/src/components/chat/chat-area.tsx +406 -0
  120. package/src/components/chat/chat-header.tsx +491 -0
  121. package/src/components/chat/chat-tool-toggles.tsx +161 -0
  122. package/src/components/chat/code-block.tsx +146 -0
  123. package/src/components/chat/dev-server-bar.tsx +39 -0
  124. package/src/components/chat/message-bubble.tsx +486 -0
  125. package/src/components/chat/message-list.tsx +299 -0
  126. package/src/components/chat/session-debug-panel.tsx +196 -0
  127. package/src/components/chat/streaming-bubble.tsx +85 -0
  128. package/src/components/chat/thinking-indicator.tsx +26 -0
  129. package/src/components/chat/tool-call-bubble.tsx +438 -0
  130. package/src/components/chat/tool-request-banner.tsx +103 -0
  131. package/src/components/connectors/connector-list.tsx +196 -0
  132. package/src/components/connectors/connector-sheet.tsx +804 -0
  133. package/src/components/input/chat-input.tsx +235 -0
  134. package/src/components/knowledge/knowledge-list.tsx +206 -0
  135. package/src/components/knowledge/knowledge-sheet.tsx +316 -0
  136. package/src/components/layout/app-layout.tsx +1016 -0
  137. package/src/components/layout/daemon-indicator.tsx +56 -0
  138. package/src/components/layout/mobile-header.tsx +31 -0
  139. package/src/components/layout/network-banner.tsx +17 -0
  140. package/src/components/layout/update-banner.tsx +130 -0
  141. package/src/components/logs/log-list.tsx +358 -0
  142. package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
  143. package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
  144. package/src/components/memory/memory-card.tsx +63 -0
  145. package/src/components/memory/memory-detail.tsx +339 -0
  146. package/src/components/memory/memory-list.tsx +198 -0
  147. package/src/components/memory/memory-sheet.tsx +70 -0
  148. package/src/components/plugins/plugin-list.tsx +60 -0
  149. package/src/components/plugins/plugin-sheet.tsx +311 -0
  150. package/src/components/providers/provider-list.tsx +96 -0
  151. package/src/components/providers/provider-sheet.tsx +542 -0
  152. package/src/components/runs/run-list.tsx +231 -0
  153. package/src/components/schedules/schedule-card.tsx +63 -0
  154. package/src/components/schedules/schedule-list.tsx +76 -0
  155. package/src/components/schedules/schedule-sheet.tsx +336 -0
  156. package/src/components/secrets/secret-sheet.tsx +180 -0
  157. package/src/components/secrets/secrets-list.tsx +91 -0
  158. package/src/components/sessions/new-session-sheet.tsx +478 -0
  159. package/src/components/sessions/session-card.tsx +144 -0
  160. package/src/components/sessions/session-list.tsx +202 -0
  161. package/src/components/shared/ai-gen-block.tsx +77 -0
  162. package/src/components/shared/avatar.tsx +48 -0
  163. package/src/components/shared/bottom-sheet.tsx +30 -0
  164. package/src/components/shared/confirm-dialog.tsx +47 -0
  165. package/src/components/shared/connector-platform-icon.tsx +113 -0
  166. package/src/components/shared/dir-browser.tsx +285 -0
  167. package/src/components/shared/dropdown.tsx +55 -0
  168. package/src/components/shared/icon-button.tsx +25 -0
  169. package/src/components/shared/settings/plugin-manager.tsx +207 -0
  170. package/src/components/shared/settings/section-capability-policy.tsx +93 -0
  171. package/src/components/shared/settings/section-embedding.tsx +99 -0
  172. package/src/components/shared/settings/section-heartbeat.tsx +168 -0
  173. package/src/components/shared/settings/section-memory.tsx +77 -0
  174. package/src/components/shared/settings/section-orchestrator.tsx +108 -0
  175. package/src/components/shared/settings/section-providers.tsx +181 -0
  176. package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
  177. package/src/components/shared/settings/section-secrets.tsx +132 -0
  178. package/src/components/shared/settings/section-user-preferences.tsx +24 -0
  179. package/src/components/shared/settings/section-voice.tsx +53 -0
  180. package/src/components/shared/settings/settings-sheet.tsx +88 -0
  181. package/src/components/shared/settings/types.ts +7 -0
  182. package/src/components/shared/settings/utils.ts +13 -0
  183. package/src/components/shared/settings-sheet.tsx +1 -0
  184. package/src/components/shared/skeleton.tsx +19 -0
  185. package/src/components/shared/usage-badge.tsx +28 -0
  186. package/src/components/skills/clawhub-browser.tsx +225 -0
  187. package/src/components/skills/skill-list.tsx +70 -0
  188. package/src/components/skills/skill-sheet.tsx +254 -0
  189. package/src/components/tasks/task-board.tsx +96 -0
  190. package/src/components/tasks/task-card.tsx +179 -0
  191. package/src/components/tasks/task-column.tsx +73 -0
  192. package/src/components/tasks/task-list.tsx +118 -0
  193. package/src/components/tasks/task-sheet.tsx +415 -0
  194. package/src/components/ui/avatar.tsx +109 -0
  195. package/src/components/ui/badge.tsx +48 -0
  196. package/src/components/ui/button.tsx +64 -0
  197. package/src/components/ui/card.tsx +92 -0
  198. package/src/components/ui/dialog.tsx +158 -0
  199. package/src/components/ui/dropdown-menu.tsx +257 -0
  200. package/src/components/ui/input.tsx +21 -0
  201. package/src/components/ui/scroll-area.tsx +58 -0
  202. package/src/components/ui/select.tsx +190 -0
  203. package/src/components/ui/separator.tsx +28 -0
  204. package/src/components/ui/sheet.tsx +143 -0
  205. package/src/components/ui/sonner.tsx +22 -0
  206. package/src/components/ui/textarea.tsx +18 -0
  207. package/src/components/ui/tooltip.tsx +56 -0
  208. package/src/components/usage/usage-list.tsx +105 -0
  209. package/src/components/webhooks/webhook-list.tsx +166 -0
  210. package/src/components/webhooks/webhook-sheet.tsx +402 -0
  211. package/src/hooks/use-auto-resize.ts +20 -0
  212. package/src/hooks/use-media-query.ts +21 -0
  213. package/src/hooks/use-speech-recognition.ts +83 -0
  214. package/src/instrumentation.ts +8 -0
  215. package/src/lib/agents.ts +13 -0
  216. package/src/lib/api-client.ts +100 -0
  217. package/src/lib/chat.ts +60 -0
  218. package/src/lib/memory.ts +42 -0
  219. package/src/lib/openclaw-endpoint.test.ts +48 -0
  220. package/src/lib/openclaw-endpoint.ts +67 -0
  221. package/src/lib/provider-config.ts +13 -0
  222. package/src/lib/providers/anthropic.ts +135 -0
  223. package/src/lib/providers/claude-cli.ts +202 -0
  224. package/src/lib/providers/codex-cli.ts +260 -0
  225. package/src/lib/providers/index.ts +351 -0
  226. package/src/lib/providers/ollama.ts +131 -0
  227. package/src/lib/providers/openai.ts +164 -0
  228. package/src/lib/providers/openclaw.ts +330 -0
  229. package/src/lib/providers/opencode-cli.ts +164 -0
  230. package/src/lib/runtime-loop.ts +15 -0
  231. package/src/lib/schedule-dedupe.test.ts +84 -0
  232. package/src/lib/schedule-dedupe.ts +174 -0
  233. package/src/lib/schedule-name.ts +62 -0
  234. package/src/lib/schedules.ts +16 -0
  235. package/src/lib/server/agent-registry.ts +70 -0
  236. package/src/lib/server/api-routes.test.ts +362 -0
  237. package/src/lib/server/autonomy-contract.ts +200 -0
  238. package/src/lib/server/build-llm.ts +155 -0
  239. package/src/lib/server/capability-router.test.ts +21 -0
  240. package/src/lib/server/capability-router.ts +172 -0
  241. package/src/lib/server/chat-execution.ts +894 -0
  242. package/src/lib/server/clawhub-client.test.ts +161 -0
  243. package/src/lib/server/clawhub-client.ts +26 -0
  244. package/src/lib/server/connectors/connector-routing.test.ts +243 -0
  245. package/src/lib/server/connectors/discord.ts +116 -0
  246. package/src/lib/server/connectors/googlechat.ts +66 -0
  247. package/src/lib/server/connectors/manager.ts +559 -0
  248. package/src/lib/server/connectors/matrix.ts +78 -0
  249. package/src/lib/server/connectors/media.ts +149 -0
  250. package/src/lib/server/connectors/openclaw.test.ts +375 -0
  251. package/src/lib/server/connectors/openclaw.ts +1132 -0
  252. package/src/lib/server/connectors/signal.ts +183 -0
  253. package/src/lib/server/connectors/slack.ts +258 -0
  254. package/src/lib/server/connectors/teams.ts +94 -0
  255. package/src/lib/server/connectors/telegram.ts +221 -0
  256. package/src/lib/server/connectors/types.ts +62 -0
  257. package/src/lib/server/connectors/whatsapp.ts +349 -0
  258. package/src/lib/server/context-manager.ts +232 -0
  259. package/src/lib/server/cost.ts +31 -0
  260. package/src/lib/server/daemon-state.ts +354 -0
  261. package/src/lib/server/data-dir.ts +3 -0
  262. package/src/lib/server/embeddings.ts +111 -0
  263. package/src/lib/server/execution-log.ts +257 -0
  264. package/src/lib/server/gateway/protocol.test.ts +54 -0
  265. package/src/lib/server/gateway/protocol.ts +114 -0
  266. package/src/lib/server/heartbeat-service.ts +366 -0
  267. package/src/lib/server/knowledge-db.test.ts +441 -0
  268. package/src/lib/server/logger.ts +47 -0
  269. package/src/lib/server/main-agent-loop.ts +1017 -0
  270. package/src/lib/server/mcp-client.test.ts +342 -0
  271. package/src/lib/server/mcp-client.ts +130 -0
  272. package/src/lib/server/memory-db.ts +1078 -0
  273. package/src/lib/server/memory-graph.test.ts +153 -0
  274. package/src/lib/server/memory-graph.ts +138 -0
  275. package/src/lib/server/openclaw-health.ts +245 -0
  276. package/src/lib/server/orchestrator-lg.ts +431 -0
  277. package/src/lib/server/orchestrator.ts +364 -0
  278. package/src/lib/server/playwright-proxy.mjs +70 -0
  279. package/src/lib/server/plugins.ts +229 -0
  280. package/src/lib/server/process-manager.ts +327 -0
  281. package/src/lib/server/provider-health.ts +113 -0
  282. package/src/lib/server/queue.ts +859 -0
  283. package/src/lib/server/runtime-settings.ts +119 -0
  284. package/src/lib/server/scheduler.ts +196 -0
  285. package/src/lib/server/session-mailbox.ts +129 -0
  286. package/src/lib/server/session-run-manager.ts +512 -0
  287. package/src/lib/server/session-tools/connector.ts +124 -0
  288. package/src/lib/server/session-tools/context-mgmt.ts +103 -0
  289. package/src/lib/server/session-tools/context.ts +114 -0
  290. package/src/lib/server/session-tools/crud.ts +673 -0
  291. package/src/lib/server/session-tools/delegate.ts +708 -0
  292. package/src/lib/server/session-tools/file.ts +264 -0
  293. package/src/lib/server/session-tools/index.ts +164 -0
  294. package/src/lib/server/session-tools/memory.ts +230 -0
  295. package/src/lib/server/session-tools/session-info.ts +422 -0
  296. package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
  297. package/src/lib/server/session-tools/shell.ts +171 -0
  298. package/src/lib/server/session-tools/web.ts +408 -0
  299. package/src/lib/server/session-tools.ts +9 -0
  300. package/src/lib/server/skills-normalize.ts +130 -0
  301. package/src/lib/server/storage-mcp.test.ts +161 -0
  302. package/src/lib/server/storage.ts +670 -0
  303. package/src/lib/server/stream-agent-chat.ts +571 -0
  304. package/src/lib/server/task-reports.ts +122 -0
  305. package/src/lib/server/task-result.ts +161 -0
  306. package/src/lib/server/task-validation.test.ts +27 -0
  307. package/src/lib/server/task-validation.ts +90 -0
  308. package/src/lib/server/tool-capability-policy.test.ts +58 -0
  309. package/src/lib/server/tool-capability-policy.ts +262 -0
  310. package/src/lib/sessions.ts +68 -0
  311. package/src/lib/tasks.ts +20 -0
  312. package/src/lib/tts.ts +42 -0
  313. package/src/lib/upload.ts +10 -0
  314. package/src/lib/utils.ts +6 -0
  315. package/src/proxy.ts +43 -0
  316. package/src/stores/use-app-store.ts +468 -0
  317. package/src/stores/use-chat-store.ts +323 -0
  318. package/src/types/index.ts +621 -0
  319. package/tsconfig.json +34 -0
@@ -0,0 +1,512 @@
1
+ import crypto from 'crypto'
2
+ import type { SSEEvent } from '@/types'
3
+ import { active, loadSessions } from './storage'
4
+ import { executeSessionChatTurn, type ExecuteChatTurnResult } from './chat-execution'
5
+ import { loadRuntimeSettings } from './runtime-settings'
6
+ import { log } from './logger'
7
+ import { handleMainLoopRunResult, type MainLoopFollowupRequest } from './main-agent-loop'
8
+
9
+ export type SessionRunStatus = 'queued' | 'running' | 'completed' | 'failed' | 'cancelled'
10
+ export type SessionQueueMode = 'followup' | 'steer' | 'collect'
11
+
12
+ export interface SessionRunRecord {
13
+ id: string
14
+ sessionId: string
15
+ source: string
16
+ internal: boolean
17
+ mode: SessionQueueMode
18
+ status: SessionRunStatus
19
+ messagePreview: string
20
+ dedupeKey?: string
21
+ queuedAt: number
22
+ startedAt?: number
23
+ endedAt?: number
24
+ error?: string
25
+ resultPreview?: string
26
+ }
27
+
28
+ interface QueueEntry {
29
+ executionKey: string
30
+ run: SessionRunRecord
31
+ message: string
32
+ imagePath?: string
33
+ imageUrl?: string
34
+ onEvents: Array<(event: SSEEvent) => void>
35
+ signalController: AbortController
36
+ maxRuntimeMs?: number
37
+ modelOverride?: string
38
+ heartbeatConfig?: { ackMaxChars: number; showOk: boolean; showAlerts: boolean; target: string | null }
39
+ resolve: (value: ExecuteChatTurnResult) => void
40
+ reject: (error: Error) => void
41
+ promise: Promise<ExecuteChatTurnResult>
42
+ }
43
+
44
+ interface RuntimeState {
45
+ runningByExecution: Map<string, QueueEntry>
46
+ queueByExecution: Map<string, QueueEntry[]>
47
+ runs: Map<string, SessionRunRecord>
48
+ recentRunIds: string[]
49
+ promises: Map<string, Promise<ExecuteChatTurnResult>>
50
+ }
51
+
52
+ const MAX_RECENT_RUNS = 500
53
+ const COLLECT_COALESCE_WINDOW_MS = 1500
54
+ const globalKey = '__swarmclaw_session_run_manager__' as const
55
+ const state: RuntimeState = (globalThis as any)[globalKey] ?? ((globalThis as any)[globalKey] = {
56
+ runningByExecution: new Map<string, QueueEntry>(),
57
+ queueByExecution: new Map<string, QueueEntry[]>(),
58
+ runs: new Map<string, SessionRunRecord>(),
59
+ recentRunIds: [],
60
+ promises: new Map<string, Promise<ExecuteChatTurnResult>>(),
61
+ })
62
+
63
+ function now() {
64
+ return Date.now()
65
+ }
66
+
67
+ function messagePreview(text: string): string {
68
+ return (text || '').replace(/\s+/g, ' ').trim().slice(0, 140)
69
+ }
70
+
71
+ function trimRecentRuns() {
72
+ while (state.recentRunIds.length > MAX_RECENT_RUNS) {
73
+ const id = state.recentRunIds.shift()
74
+ if (!id) continue
75
+ state.runs.delete(id)
76
+ state.promises.delete(id)
77
+ }
78
+ }
79
+
80
+ function registerRun(run: SessionRunRecord) {
81
+ state.runs.set(run.id, run)
82
+ state.recentRunIds.push(run.id)
83
+ trimRecentRuns()
84
+ }
85
+
86
+ function emitToSubscribers(entry: QueueEntry, event: SSEEvent) {
87
+ for (const send of entry.onEvents) {
88
+ try {
89
+ send(event)
90
+ } catch {
91
+ // Subscriber stream can be closed by the client.
92
+ }
93
+ }
94
+ }
95
+
96
+ function emitRunMeta(entry: QueueEntry, status: SessionRunStatus, extra?: Record<string, unknown>) {
97
+ emitToSubscribers(entry, {
98
+ t: 'md',
99
+ text: JSON.stringify({
100
+ run: {
101
+ id: entry.run.id,
102
+ sessionId: entry.run.sessionId,
103
+ status,
104
+ source: entry.run.source,
105
+ internal: entry.run.internal,
106
+ ...extra,
107
+ },
108
+ }),
109
+ })
110
+ }
111
+
112
+ function executionKeyForSession(sessionId: string): string {
113
+ return `session:${sessionId}`
114
+ }
115
+
116
+ function queueForExecution(executionKey: string): QueueEntry[] {
117
+ const existing = state.queueByExecution.get(executionKey)
118
+ if (existing) return existing
119
+ const created: QueueEntry[] = []
120
+ state.queueByExecution.set(executionKey, created)
121
+ return created
122
+ }
123
+
124
+ function normalizeMode(mode: string | undefined, internal: boolean): SessionQueueMode {
125
+ if (mode === 'steer' || mode === 'collect' || mode === 'followup') return mode
126
+ return internal ? 'collect' : 'followup'
127
+ }
128
+
129
+ function cancelPendingForSession(sessionId: string, reason: string): number {
130
+ let cancelled = 0
131
+ for (const [key, queue] of state.queueByExecution.entries()) {
132
+ if (!queue.length) continue
133
+ const keep: QueueEntry[] = []
134
+ for (const entry of queue) {
135
+ if (entry.run.sessionId !== sessionId) {
136
+ keep.push(entry)
137
+ continue
138
+ }
139
+ entry.run.status = 'cancelled'
140
+ entry.run.endedAt = now()
141
+ entry.run.error = reason
142
+ emitRunMeta(entry, 'cancelled', { reason })
143
+ entry.reject(new Error(reason))
144
+ cancelled++
145
+ }
146
+ if (keep.length > 0) state.queueByExecution.set(key, keep)
147
+ else state.queueByExecution.delete(key)
148
+ }
149
+ return cancelled
150
+ }
151
+
152
+ export function cancelAllHeartbeatRuns(reason = 'Heartbeat disabled globally'): { cancelledQueued: number; abortedRunning: number } {
153
+ let cancelledQueued = 0
154
+ let abortedRunning = 0
155
+
156
+ for (const [key, queue] of state.queueByExecution.entries()) {
157
+ if (!queue.length) continue
158
+ const keep: QueueEntry[] = []
159
+ for (const entry of queue) {
160
+ const isHeartbeat = entry.run.internal === true && entry.run.source === 'heartbeat'
161
+ if (!isHeartbeat) {
162
+ keep.push(entry)
163
+ continue
164
+ }
165
+ entry.run.status = 'cancelled'
166
+ entry.run.endedAt = now()
167
+ entry.run.error = reason
168
+ emitRunMeta(entry, 'cancelled', { reason })
169
+ entry.reject(new Error(reason))
170
+ cancelledQueued += 1
171
+ }
172
+ if (keep.length > 0) state.queueByExecution.set(key, keep)
173
+ else state.queueByExecution.delete(key)
174
+ }
175
+
176
+ for (const entry of state.runningByExecution.values()) {
177
+ const isHeartbeat = entry.run.internal === true && entry.run.source === 'heartbeat'
178
+ if (!isHeartbeat) continue
179
+ abortedRunning += 1
180
+ entry.signalController.abort()
181
+ try { active.get(entry.run.sessionId)?.kill?.() } catch { /* noop */ }
182
+ }
183
+
184
+ return { cancelledQueued, abortedRunning }
185
+ }
186
+
187
+ function scheduleMainLoopFollowup(sessionId: string, followup: MainLoopFollowupRequest) {
188
+ const delayMs = Math.max(0, Math.trunc(followup.delayMs || 0))
189
+ setTimeout(() => {
190
+ try {
191
+ const sessions = loadSessions()
192
+ const session = sessions[sessionId]
193
+ if (!session || session.name !== '__main__') return
194
+ enqueueSessionRun({
195
+ sessionId,
196
+ message: followup.message,
197
+ internal: true,
198
+ source: 'main-loop-followup',
199
+ mode: 'collect',
200
+ dedupeKey: followup.dedupeKey,
201
+ })
202
+ } catch (err: any) {
203
+ log.warn('session-run', `Failed to enqueue main-loop followup for ${sessionId}`, err?.message || String(err))
204
+ }
205
+ }, delayMs)
206
+ }
207
+
208
+ async function drainExecution(executionKey: string): Promise<void> {
209
+ if (state.runningByExecution.has(executionKey)) return
210
+ const q = queueForExecution(executionKey)
211
+ const next = q.shift()
212
+ if (!next) return
213
+
214
+ state.runningByExecution.set(executionKey, next)
215
+ next.run.status = 'running'
216
+ next.run.startedAt = now()
217
+ emitRunMeta(next, 'running')
218
+ log.info('session-run', `Run started ${next.run.id}`, {
219
+ sessionId: next.run.sessionId,
220
+ source: next.run.source,
221
+ internal: next.run.internal,
222
+ mode: next.run.mode,
223
+ timeoutMs: next.maxRuntimeMs || null,
224
+ })
225
+
226
+ let runtimeTimer: ReturnType<typeof setTimeout> | null = null
227
+ if (next.maxRuntimeMs && next.maxRuntimeMs > 0) {
228
+ runtimeTimer = setTimeout(() => {
229
+ next.signalController.abort()
230
+ }, next.maxRuntimeMs)
231
+ }
232
+
233
+ try {
234
+ const result = await executeSessionChatTurn({
235
+ sessionId: next.run.sessionId,
236
+ message: next.message,
237
+ imagePath: next.imagePath,
238
+ imageUrl: next.imageUrl,
239
+ internal: next.run.internal,
240
+ source: next.run.source,
241
+ runId: next.run.id,
242
+ signal: next.signalController.signal,
243
+ onEvent: (event) => emitToSubscribers(next, event),
244
+ modelOverride: next.modelOverride,
245
+ heartbeatConfig: next.heartbeatConfig,
246
+ })
247
+
248
+ const failed = !!result.error
249
+ let followup: MainLoopFollowupRequest | null = null
250
+ try {
251
+ followup = handleMainLoopRunResult({
252
+ sessionId: next.run.sessionId,
253
+ message: next.message,
254
+ internal: next.run.internal,
255
+ source: next.run.source,
256
+ resultText: result.text,
257
+ error: result.error,
258
+ toolEvents: result.toolEvents,
259
+ })
260
+ } catch (mainLoopErr: any) {
261
+ log.warn('session-run', `Main-loop update failed for ${next.run.id}`, mainLoopErr?.message || String(mainLoopErr))
262
+ }
263
+
264
+ next.run.status = failed ? 'failed' : 'completed'
265
+ next.run.endedAt = now()
266
+ next.run.error = result.error
267
+ next.run.resultPreview = result.text?.slice(0, 280)
268
+ emitRunMeta(next, next.run.status, {
269
+ persisted: result.persisted,
270
+ hasText: !!result.text,
271
+ error: result.error || null,
272
+ })
273
+ log.info('session-run', `Run finished ${next.run.id}`, {
274
+ sessionId: next.run.sessionId,
275
+ status: next.run.status,
276
+ persisted: result.persisted,
277
+ hasText: !!result.text,
278
+ error: result.error || null,
279
+ durationMs: (next.run.endedAt || now()) - (next.run.startedAt || now()),
280
+ })
281
+ next.resolve(result)
282
+ if (!failed && followup) {
283
+ scheduleMainLoopFollowup(next.run.sessionId, followup)
284
+ log.info('session-run', `Queued main-loop followup after ${next.run.id}`, {
285
+ sessionId: next.run.sessionId,
286
+ delayMs: followup.delayMs,
287
+ })
288
+ }
289
+ } catch (err: any) {
290
+ const aborted = next.signalController.signal.aborted
291
+ next.run.status = aborted ? 'cancelled' : 'failed'
292
+ next.run.endedAt = now()
293
+ next.run.error = err?.message || String(err)
294
+ emitRunMeta(next, next.run.status, { error: next.run.error })
295
+ log.error('session-run', `Run failed ${next.run.id}`, {
296
+ sessionId: next.run.sessionId,
297
+ status: next.run.status,
298
+ error: next.run.error,
299
+ durationMs: (next.run.endedAt || now()) - (next.run.startedAt || now()),
300
+ })
301
+ try {
302
+ handleMainLoopRunResult({
303
+ sessionId: next.run.sessionId,
304
+ message: next.message,
305
+ internal: next.run.internal,
306
+ source: next.run.source,
307
+ resultText: '',
308
+ error: next.run.error,
309
+ toolEvents: [],
310
+ })
311
+ } catch {
312
+ // Main-loop bookkeeping failures should not affect queue execution.
313
+ }
314
+ next.reject(err instanceof Error ? err : new Error(next.run.error))
315
+ } finally {
316
+ if (runtimeTimer) clearTimeout(runtimeTimer)
317
+ state.runningByExecution.delete(executionKey)
318
+ void drainExecution(executionKey)
319
+ }
320
+ }
321
+
322
+ function findDedupeMatch(sessionId: string, dedupeKey?: string): QueueEntry | null {
323
+ if (!dedupeKey) return null
324
+ const executionKey = executionKeyForSession(sessionId)
325
+ const running = state.runningByExecution.get(executionKey)
326
+ if (running?.run.sessionId === sessionId && running?.run.dedupeKey === dedupeKey) return running
327
+ const q = queueForExecution(executionKey)
328
+ return q.find((e) => e.run.sessionId === sessionId && e.run.dedupeKey === dedupeKey) || null
329
+ }
330
+
331
+ export interface EnqueueSessionRunInput {
332
+ sessionId: string
333
+ message: string
334
+ imagePath?: string
335
+ imageUrl?: string
336
+ internal?: boolean
337
+ source?: string
338
+ mode?: SessionQueueMode
339
+ onEvent?: (event: SSEEvent) => void
340
+ dedupeKey?: string
341
+ maxRuntimeMs?: number
342
+ modelOverride?: string
343
+ heartbeatConfig?: { ackMaxChars: number; showOk: boolean; showAlerts: boolean; target: string | null }
344
+ }
345
+
346
+ export interface EnqueueSessionRunResult {
347
+ runId: string
348
+ position: number
349
+ deduped?: boolean
350
+ coalesced?: boolean
351
+ promise: Promise<ExecuteChatTurnResult>
352
+ }
353
+
354
+ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSessionRunResult {
355
+ const internal = input.internal === true
356
+ const mode = normalizeMode(input.mode, internal)
357
+ const source = input.source || 'chat'
358
+ const executionKey = executionKeyForSession(input.sessionId)
359
+ const runtime = loadRuntimeSettings()
360
+ const defaultMaxRuntimeMs = runtime.ongoingLoopMaxRuntimeMs ?? (10 * 60_000)
361
+ const effectiveMaxRuntimeMs = typeof input.maxRuntimeMs === 'number'
362
+ ? input.maxRuntimeMs
363
+ : defaultMaxRuntimeMs
364
+
365
+ const dedupe = findDedupeMatch(input.sessionId, input.dedupeKey)
366
+ if (dedupe) {
367
+ if (input.onEvent) dedupe.onEvents.push(input.onEvent)
368
+ return {
369
+ runId: dedupe.run.id,
370
+ position: 0,
371
+ deduped: true,
372
+ promise: dedupe.promise,
373
+ }
374
+ }
375
+
376
+ if (mode === 'steer') {
377
+ const running = state.runningByExecution.get(executionKey)
378
+ if (running && running.run.sessionId === input.sessionId) {
379
+ running.signalController.abort()
380
+ try { active.get(input.sessionId)?.kill?.() } catch { /* noop */ }
381
+ }
382
+ cancelPendingForSession(input.sessionId, 'Cancelled by steer mode')
383
+ }
384
+
385
+ const running = state.runningByExecution.get(executionKey)
386
+ const q = queueForExecution(executionKey)
387
+ if (mode === 'collect' && !input.imagePath && !input.imageUrl) {
388
+ const nowMs = now()
389
+ const candidate = q.at(-1)
390
+ const canCoalesce = !!candidate
391
+ && candidate.run.mode === 'collect'
392
+ && candidate.run.internal === internal
393
+ && candidate.run.source === source
394
+ && !candidate.imagePath
395
+ && !candidate.imageUrl
396
+ && (nowMs - candidate.run.queuedAt) <= COLLECT_COALESCE_WINDOW_MS
397
+
398
+ if (candidate && canCoalesce) {
399
+ const nextChunk = input.message.trim()
400
+ if (nextChunk) {
401
+ const current = candidate.message.trim()
402
+ candidate.message = current
403
+ ? `${current}\n\n[Collected follow-up]\n${nextChunk}`
404
+ : nextChunk
405
+ candidate.run.messagePreview = messagePreview(candidate.message)
406
+ candidate.run.queuedAt = nowMs
407
+ }
408
+ if (input.onEvent) candidate.onEvents.push(input.onEvent)
409
+ emitRunMeta(candidate, 'queued', { position: 0, coalesced: true, mergedIntoRunId: candidate.run.id })
410
+ return {
411
+ runId: candidate.run.id,
412
+ position: 0,
413
+ coalesced: true,
414
+ promise: candidate.promise,
415
+ }
416
+ }
417
+ }
418
+
419
+ const runId = crypto.randomBytes(8).toString('hex')
420
+ const run: SessionRunRecord = {
421
+ id: runId,
422
+ sessionId: input.sessionId,
423
+ source,
424
+ internal,
425
+ mode,
426
+ status: 'queued',
427
+ messagePreview: messagePreview(input.message),
428
+ dedupeKey: input.dedupeKey,
429
+ queuedAt: now(),
430
+ }
431
+ registerRun(run)
432
+
433
+ let resolve!: (value: ExecuteChatTurnResult) => void
434
+ let reject!: (error: Error) => void
435
+ const promise = new Promise<ExecuteChatTurnResult>((res, rej) => {
436
+ resolve = res
437
+ reject = rej
438
+ })
439
+ state.promises.set(runId, promise)
440
+
441
+ const entry: QueueEntry = {
442
+ executionKey,
443
+ run,
444
+ message: input.message,
445
+ imagePath: input.imagePath,
446
+ imageUrl: input.imageUrl,
447
+ onEvents: input.onEvent ? [input.onEvent] : [],
448
+ signalController: new AbortController(),
449
+ maxRuntimeMs: effectiveMaxRuntimeMs > 0 ? effectiveMaxRuntimeMs : undefined,
450
+ modelOverride: input.modelOverride,
451
+ heartbeatConfig: input.heartbeatConfig,
452
+ resolve,
453
+ reject,
454
+ promise,
455
+ }
456
+
457
+ q.push(entry)
458
+ const position = (running ? 1 : 0) + q.length - 1
459
+ emitRunMeta(entry, 'queued', { position })
460
+ void drainExecution(executionKey)
461
+
462
+ return { runId, position, promise }
463
+ }
464
+
465
+ export function getSessionRunState(sessionId: string): {
466
+ runningRunId?: string
467
+ queueLength: number
468
+ } {
469
+ const executionKey = executionKeyForSession(sessionId)
470
+ const running = state.runningByExecution.get(executionKey)
471
+ const queued = queueForExecution(executionKey).filter((entry) => entry.run.sessionId === sessionId).length
472
+ return {
473
+ runningRunId: running?.run.sessionId === sessionId ? running.run.id : undefined,
474
+ queueLength: queued,
475
+ }
476
+ }
477
+
478
+ export function getRunById(runId: string): SessionRunRecord | null {
479
+ return state.runs.get(runId) || null
480
+ }
481
+
482
+ export function listRuns(params?: {
483
+ sessionId?: string
484
+ status?: SessionRunStatus
485
+ limit?: number
486
+ }): SessionRunRecord[] {
487
+ const limit = Math.max(1, Math.min(1000, params?.limit ?? 200))
488
+ const ordered = [...state.recentRunIds].reverse()
489
+ const out: SessionRunRecord[] = []
490
+ for (const id of ordered) {
491
+ const run = state.runs.get(id)
492
+ if (!run) continue
493
+ if (params?.sessionId && run.sessionId !== params.sessionId) continue
494
+ if (params?.status && run.status !== params.status) continue
495
+ out.push(run)
496
+ if (out.length >= limit) break
497
+ }
498
+ return out
499
+ }
500
+
501
+ export function cancelSessionRuns(sessionId: string, reason = 'Cancelled'): { cancelledQueued: number; cancelledRunning: boolean } {
502
+ const executionKey = executionKeyForSession(sessionId)
503
+ const running = state.runningByExecution.get(executionKey)
504
+ let cancelledRunning = false
505
+ if (running && running.run.sessionId === sessionId) {
506
+ cancelledRunning = true
507
+ running.signalController.abort()
508
+ try { active.get(sessionId)?.kill?.() } catch { /* noop */ }
509
+ }
510
+ const cancelledQueued = cancelPendingForSession(sessionId, reason)
511
+ return { cancelledQueued, cancelledRunning }
512
+ }
@@ -0,0 +1,124 @@
1
+ import { z } from 'zod'
2
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import { loadConnectors, loadSettings } from '../storage'
4
+ import type { ToolBuildContext } from './context'
5
+
6
+ export function buildConnectorTools(bctx: ToolBuildContext): StructuredToolInterface[] {
7
+ const tools: StructuredToolInterface[] = []
8
+ const { ctx, hasTool } = bctx
9
+
10
+ if (hasTool('manage_connectors')) {
11
+ tools.push(
12
+ tool(
13
+ async ({ action, connectorId, platform, to, message, imageUrl, fileUrl, mediaPath, mimeType, fileName, caption, approved }) => {
14
+ try {
15
+ const normalizeWhatsAppTarget = (input: string): string => {
16
+ const raw = input.trim()
17
+ if (!raw) return raw
18
+ if (raw.includes('@')) return raw
19
+ let cleaned = raw.replace(/[^\d+]/g, '')
20
+ if (cleaned.startsWith('+')) cleaned = cleaned.slice(1)
21
+ if (cleaned.startsWith('0') && cleaned.length >= 10) {
22
+ cleaned = '44' + cleaned.slice(1)
23
+ }
24
+ cleaned = cleaned.replace(/[^\d]/g, '')
25
+ return cleaned ? `${cleaned}@s.whatsapp.net` : raw
26
+ }
27
+
28
+ const { listRunningConnectors, sendConnectorMessage, getConnectorRecentChannelId } = await import('../connectors/manager')
29
+ const running = listRunningConnectors(platform || undefined)
30
+
31
+ if (action === 'list_running' || action === 'list_targets') {
32
+ return JSON.stringify(running)
33
+ }
34
+
35
+ if (action === 'send') {
36
+ const settings = loadSettings()
37
+ if (settings.safetyRequireApprovalForOutbound === true && approved !== true) {
38
+ return 'Error: outbound connector sends require explicit approval. Re-run with approved=true after user confirmation.'
39
+ }
40
+ const hasText = !!message?.trim()
41
+ const hasMedia = !!imageUrl?.trim() || !!fileUrl?.trim()
42
+ if (!hasText && !hasMedia) return 'Error: message or media URL is required for send action.'
43
+ if (!running.length) {
44
+ return `Error: no running connectors${platform ? ` for platform "${platform}"` : ''}.`
45
+ }
46
+
47
+ const selected = connectorId
48
+ ? running.find((c) => c.id === connectorId)
49
+ : running[0]
50
+ if (!selected) return `Error: running connector not found: ${connectorId}`
51
+
52
+ const connectors = loadConnectors()
53
+ const connector = connectors[selected.id]
54
+ if (!connector) return `Error: connector not found: ${selected.id}`
55
+
56
+ let channelId = to?.trim() || ''
57
+ if (!channelId) {
58
+ const outbound = connector.config?.outboundJid?.trim()
59
+ if (outbound) channelId = outbound
60
+ }
61
+ if (!channelId) {
62
+ const recentChannelId = getConnectorRecentChannelId(selected.id)
63
+ if (recentChannelId) channelId = recentChannelId
64
+ }
65
+ if (!channelId) {
66
+ const allowed = connector.config?.allowedJids?.split(',').map((s: string) => s.trim()).filter(Boolean) || []
67
+ if (allowed.length) channelId = allowed[0]
68
+ }
69
+ if (!channelId) {
70
+ return `Error: no target recipient configured. Provide "to", or set connector config "outboundJid"/"allowedJids".`
71
+ }
72
+ if (connector.platform === 'whatsapp') {
73
+ channelId = normalizeWhatsAppTarget(channelId)
74
+ }
75
+
76
+ const sent = await sendConnectorMessage({
77
+ connectorId: selected.id,
78
+ channelId,
79
+ text: message?.trim() || '',
80
+ imageUrl: imageUrl?.trim() || undefined,
81
+ fileUrl: fileUrl?.trim() || undefined,
82
+ mediaPath: mediaPath?.trim() || undefined,
83
+ mimeType: mimeType?.trim() || undefined,
84
+ fileName: fileName?.trim() || undefined,
85
+ caption: caption?.trim() || undefined,
86
+ })
87
+ return JSON.stringify({
88
+ status: 'sent',
89
+ connectorId: sent.connectorId,
90
+ platform: sent.platform,
91
+ to: sent.channelId,
92
+ messageId: sent.messageId || null,
93
+ })
94
+ }
95
+
96
+ return 'Unknown action. Use list_running, list_targets, or send.'
97
+ } catch (err: any) {
98
+ return `Error: ${err.message || String(err)}`
99
+ }
100
+ },
101
+ {
102
+ name: 'connector_message_tool',
103
+ description: 'Send proactive outbound messages through running connectors (for example WhatsApp status updates). Supports listing running connectors/targets and sending text plus optional media (URLs or local file paths).',
104
+ schema: z.object({
105
+ action: z.enum(['list_running', 'list_targets', 'send']).describe('connector messaging action'),
106
+ connectorId: z.string().optional().describe('Optional connector id. Defaults to the first running connector (or first for selected platform).'),
107
+ platform: z.string().optional().describe('Optional platform filter (whatsapp, telegram, slack, discord).'),
108
+ to: z.string().optional().describe('Target channel id / recipient. For WhatsApp, phone number or full JID.'),
109
+ message: z.string().optional().describe('Message text to send (required for send action).'),
110
+ imageUrl: z.string().optional().describe('Optional public image URL to attach/send where platform supports media.'),
111
+ fileUrl: z.string().optional().describe('Optional public file URL to attach/send where platform supports documents.'),
112
+ mediaPath: z.string().optional().describe('Absolute local file path to send (e.g. a screenshot). Auto-detects mime type from extension. Takes priority over imageUrl/fileUrl.'),
113
+ mimeType: z.string().optional().describe('Optional MIME type for mediaPath or fileUrl.'),
114
+ fileName: z.string().optional().describe('Optional display file name for mediaPath or fileUrl.'),
115
+ caption: z.string().optional().describe('Optional caption used with image/file sends.'),
116
+ approved: z.boolean().optional().describe('Set true to explicitly confirm outbound send when safetyRequireApprovalForOutbound is enabled.'),
117
+ }),
118
+ },
119
+ ),
120
+ )
121
+ }
122
+
123
+ return tools
124
+ }