@swarmclawai/swarmclaw 0.7.7 → 0.8.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 (281) hide show
  1. package/README.md +12 -14
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -14
@@ -0,0 +1,1147 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+ import { describe, it } from 'node:test'
7
+
8
+ import { sanitizeConnectorOutboundContent } from './manager'
9
+
10
+ const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../..')
11
+
12
+ function runWithTempDataDir(script: string) {
13
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-manager-test-'))
14
+ try {
15
+ const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
16
+ cwd: repoRoot,
17
+ env: {
18
+ ...process.env,
19
+ DATA_DIR: tempDir,
20
+ WORKSPACE_DIR: path.join(tempDir, 'workspace'),
21
+ },
22
+ encoding: 'utf-8',
23
+ })
24
+ assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
25
+ const lines = (result.stdout || '')
26
+ .trim()
27
+ .split('\n')
28
+ .map((line) => line.trim())
29
+ .filter(Boolean)
30
+ const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
31
+ return JSON.parse(jsonLine || '{}')
32
+ } finally {
33
+ fs.rmSync(tempDir, { recursive: true, force: true })
34
+ }
35
+ }
36
+
37
+ describe('sanitizeConnectorOutboundContent', () => {
38
+ it('strips hidden control tokens from captions without suppressing the media send itself', () => {
39
+ assert.deepEqual(
40
+ sanitizeConnectorOutboundContent({
41
+ text: 'Here is the attachment',
42
+ caption: 'NO_MESSAGE',
43
+ }),
44
+ {
45
+ sanitizedText: 'Here is the attachment',
46
+ suppressHiddenText: false,
47
+ sanitizedCaptionText: '',
48
+ sanitizedCaption: undefined,
49
+ },
50
+ )
51
+ })
52
+
53
+ it('suppresses pure hidden-control text payloads', () => {
54
+ assert.deepEqual(
55
+ sanitizeConnectorOutboundContent({
56
+ text: 'HEARTBEAT_OK',
57
+ caption: 'Looks good',
58
+ }),
59
+ {
60
+ sanitizedText: '',
61
+ suppressHiddenText: true,
62
+ sanitizedCaptionText: 'Looks good',
63
+ sanitizedCaption: 'Looks good',
64
+ },
65
+ )
66
+ })
67
+
68
+ it('mirrors direct WhatsApp inbound and assistant replies into the session transcript', () => {
69
+ const output = runWithTempDataDir(`
70
+ const storageMod = await import('./src/lib/server/storage.ts')
71
+ const managerMod = await import('./src/lib/server/connectors/manager.ts')
72
+ const providersMod = await import('./src/lib/providers/index.ts')
73
+ const storage = storageMod.default || storageMod
74
+ const manager = managerMod.default || managerMod
75
+ const providers = providersMod.default || providersMod
76
+
77
+ const now = Date.now()
78
+ providers.PROVIDERS['test-provider'] = {
79
+ id: 'test-provider',
80
+ name: 'Test Provider',
81
+ models: ['test-model'],
82
+ requiresApiKey: false,
83
+ requiresEndpoint: false,
84
+ handler: {
85
+ streamChat: async (opts) => {
86
+ opts.write('data: ' + JSON.stringify({ t: 'r', text: 'Roger that via WhatsApp' }) + '\\n')
87
+ return ''
88
+ },
89
+ },
90
+ }
91
+
92
+ storage.saveSettings({})
93
+ storage.saveAgents({
94
+ agent_1: {
95
+ id: 'agent_1',
96
+ name: 'Molly',
97
+ provider: 'test-provider',
98
+ model: 'test-model',
99
+ plugins: [],
100
+ threadSessionId: 'agent_thread',
101
+ createdAt: now,
102
+ updatedAt: now,
103
+ },
104
+ })
105
+ storage.saveConnectors({
106
+ conn_1: {
107
+ id: 'conn_1',
108
+ name: 'WhatsApp',
109
+ platform: 'whatsapp',
110
+ agentId: 'agent_1',
111
+ credentialId: null,
112
+ config: { inboundDebounceMs: 0 },
113
+ isEnabled: true,
114
+ status: 'running',
115
+ createdAt: now,
116
+ updatedAt: now,
117
+ },
118
+ })
119
+ storage.saveSessions({
120
+ agent_thread: {
121
+ id: 'agent_thread',
122
+ name: 'Molly',
123
+ cwd: process.env.WORKSPACE_DIR,
124
+ user: 'default',
125
+ provider: 'test-provider',
126
+ model: 'test-model',
127
+ claudeSessionId: null,
128
+ messages: [],
129
+ createdAt: now,
130
+ lastActiveAt: now,
131
+ sessionType: 'human',
132
+ agentId: 'agent_1',
133
+ plugins: [],
134
+ },
135
+ })
136
+
137
+ const connector = storage.loadConnectors().conn_1
138
+ const response = await manager.routeConnectorMessageForTest(connector, {
139
+ platform: 'whatsapp',
140
+ channelId: '15550001111@s.whatsapp.net',
141
+ senderId: '15550001111@s.whatsapp.net',
142
+ senderName: 'Alice',
143
+ text: 'Hello from WhatsApp',
144
+ messageId: 'in-1',
145
+ isGroup: false,
146
+ })
147
+
148
+ const sessions = storage.loadSessions()
149
+ const directSession = Object.values(sessions).find((entry) => entry.id !== 'agent_thread')
150
+ const mainSession = sessions.agent_thread
151
+ console.log(JSON.stringify({ response, directSession, mainSession }))
152
+ `)
153
+
154
+ assert.equal(output.response, 'Roger that via WhatsApp')
155
+ assert.equal(output.directSession.messages.length, 2)
156
+ assert.equal(output.directSession.messages[0].role, 'user')
157
+ assert.equal(output.directSession.messages[0].source.platform, 'whatsapp')
158
+ assert.equal(output.directSession.messages[0].source.messageId, 'in-1')
159
+ assert.equal(output.directSession.messages[1].role, 'assistant')
160
+ assert.equal(output.directSession.messages[1].text, 'Roger that via WhatsApp')
161
+ assert.equal(output.directSession.messages[1].source.platform, 'whatsapp')
162
+ assert.equal(output.directSession.messages[1].source.replyToMessageId, 'in-1')
163
+ assert.equal(output.mainSession.messages.length, 2)
164
+ assert.equal(output.mainSession.messages[0].source.platform, 'whatsapp')
165
+ assert.equal(output.mainSession.messages[0].source.senderName, 'Alice')
166
+ assert.equal(output.mainSession.messages[0].historyExcluded, true)
167
+ assert.equal(output.mainSession.messages[1].source.platform, 'whatsapp')
168
+ assert.equal(output.mainSession.messages[1].text, 'Roger that via WhatsApp')
169
+ assert.equal(output.mainSession.messages[1].historyExcluded, true)
170
+ })
171
+
172
+ it('mirrors same-channel connector_message_tool sends when the agent suppresses visible text', () => {
173
+ const output = runWithTempDataDir(`
174
+ const storageMod = await import('./src/lib/server/storage.ts')
175
+ const managerMod = await import('./src/lib/server/connectors/manager.ts')
176
+ const providersMod = await import('./src/lib/providers/index.ts')
177
+ const storage = storageMod.default || storageMod
178
+ const manager = managerMod.default || managerMod
179
+ const providers = providersMod.default || providersMod
180
+
181
+ const now = Date.now()
182
+ providers.PROVIDERS['test-provider'] = {
183
+ id: 'test-provider',
184
+ name: 'Test Provider',
185
+ models: ['test-model'],
186
+ requiresApiKey: false,
187
+ requiresEndpoint: false,
188
+ handler: {
189
+ streamChat: async (opts) => {
190
+ opts.write('data: ' + JSON.stringify({ t: 'r', text: 'NO_MESSAGE' }) + '\\n')
191
+ return ''
192
+ },
193
+ },
194
+ }
195
+ storage.saveSettings({})
196
+ storage.saveAgents({
197
+ agent_1: {
198
+ id: 'agent_1',
199
+ name: 'Molly',
200
+ provider: 'test-provider',
201
+ model: 'test-model',
202
+ plugins: ['connector_message_tool'],
203
+ createdAt: now,
204
+ updatedAt: now,
205
+ },
206
+ })
207
+ storage.saveConnectors({
208
+ conn_1: {
209
+ id: 'conn_1',
210
+ name: 'WhatsApp',
211
+ platform: 'whatsapp',
212
+ agentId: 'agent_1',
213
+ credentialId: null,
214
+ config: { inboundDebounceMs: 0 },
215
+ isEnabled: true,
216
+ status: 'running',
217
+ createdAt: now,
218
+ updatedAt: now,
219
+ },
220
+ })
221
+ storage.saveSessions({})
222
+
223
+ manager.setStreamAgentChatForTest(async (opts) => {
224
+ opts.write('data: ' + JSON.stringify({
225
+ t: 'tool_call',
226
+ toolName: 'connector_message_tool',
227
+ toolInput: JSON.stringify({
228
+ action: 'send',
229
+ to: '15550001111@s.whatsapp.net',
230
+ message: 'Sent from tool path',
231
+ }),
232
+ toolCallId: 'call-1',
233
+ }) + '\\n')
234
+ opts.write('data: ' + JSON.stringify({
235
+ t: 'tool_result',
236
+ toolName: 'connector_message_tool',
237
+ toolOutput: JSON.stringify({
238
+ status: 'sent',
239
+ to: '15550001111@s.whatsapp.net',
240
+ messageId: 'wa-out-1',
241
+ }),
242
+ toolCallId: 'call-1',
243
+ }) + '\\n')
244
+ return {
245
+ fullText: 'NO_MESSAGE',
246
+ finalResponse: 'NO_MESSAGE',
247
+ toolEvents: [],
248
+ }
249
+ })
250
+
251
+ try {
252
+ const connector = storage.loadConnectors().conn_1
253
+ const response = await manager.routeConnectorMessageForTest(connector, {
254
+ platform: 'whatsapp',
255
+ channelId: '15550001111@s.whatsapp.net',
256
+ senderId: '15550001111@s.whatsapp.net',
257
+ senderName: 'Alice',
258
+ text: 'Send it on WhatsApp',
259
+ messageId: 'in-2',
260
+ isGroup: false,
261
+ })
262
+ const sessions = storage.loadSessions()
263
+ const directSession = Object.values(sessions).find((entry) => String(entry.name || '').startsWith('connector:'))
264
+ const mainSession = Object.values(sessions).find((entry) => entry.id !== directSession?.id)
265
+ console.log(JSON.stringify({ response, directSession, mainSession }))
266
+ } finally {
267
+ manager.setStreamAgentChatForTest(null)
268
+ }
269
+ `)
270
+
271
+ assert.equal(output.response, 'NO_MESSAGE')
272
+ assert.equal(output.directSession.messages.length, 2)
273
+ assert.equal(output.directSession.messages[1].role, 'assistant')
274
+ assert.equal(output.directSession.messages[1].text, 'Sent from tool path')
275
+ assert.equal(output.directSession.messages[1].source.platform, 'whatsapp')
276
+ assert.equal(output.directSession.messages[1].source.messageId, 'wa-out-1')
277
+ assert.equal(output.directSession.connectorContext.lastOutboundMessageId, 'wa-out-1')
278
+ assert.equal(output.mainSession.messages.length, 2)
279
+ assert.equal(output.mainSession.messages.every((entry) => entry.historyExcluded === true), true)
280
+ })
281
+
282
+ it('accepts WhatsApp allowlist matches through senderIdAlt when the primary sender id is a lid', () => {
283
+ const output = runWithTempDataDir(`
284
+ const storageMod = await import('./src/lib/server/storage.ts')
285
+ const managerMod = await import('./src/lib/server/connectors/manager.ts')
286
+ const providersMod = await import('./src/lib/providers/index.ts')
287
+ const storage = storageMod.default || storageMod
288
+ const manager = managerMod.default || managerMod
289
+ const providers = providersMod.default || providersMod
290
+
291
+ const now = Date.now()
292
+ providers.PROVIDERS['test-provider'] = {
293
+ id: 'test-provider',
294
+ name: 'Test Provider',
295
+ models: ['test-model'],
296
+ requiresApiKey: false,
297
+ requiresEndpoint: false,
298
+ handler: {
299
+ streamChat: async (opts) => {
300
+ opts.write('data: ' + JSON.stringify({ t: 'r', text: 'Allowlist matched' }) + '\\n')
301
+ return ''
302
+ },
303
+ },
304
+ }
305
+
306
+ storage.saveSettings({})
307
+ storage.saveAgents({
308
+ agent_1: {
309
+ id: 'agent_1',
310
+ name: 'Molly',
311
+ provider: 'test-provider',
312
+ model: 'test-model',
313
+ plugins: [],
314
+ createdAt: now,
315
+ updatedAt: now,
316
+ },
317
+ })
318
+ storage.saveConnectors({
319
+ conn_1: {
320
+ id: 'conn_1',
321
+ name: 'WhatsApp',
322
+ platform: 'whatsapp',
323
+ agentId: 'agent_1',
324
+ credentialId: null,
325
+ config: {
326
+ inboundDebounceMs: 0,
327
+ dmPolicy: 'allowlist',
328
+ allowFrom: '15550001111',
329
+ },
330
+ isEnabled: true,
331
+ status: 'running',
332
+ createdAt: now,
333
+ updatedAt: now,
334
+ },
335
+ })
336
+ storage.saveSessions({})
337
+
338
+ const connector = storage.loadConnectors().conn_1
339
+ const response = await manager.routeConnectorMessageForTest(connector, {
340
+ platform: 'whatsapp',
341
+ channelId: '199900000001@lid',
342
+ channelIdAlt: '15550001111@s.whatsapp.net',
343
+ senderId: '199900000001@lid',
344
+ senderIdAlt: '15550001111@s.whatsapp.net',
345
+ senderName: 'Alice',
346
+ text: 'Hello from a lid sender',
347
+ messageId: 'in-3',
348
+ isGroup: false,
349
+ })
350
+
351
+ const session = Object.values(storage.loadSessions())[0]
352
+ console.log(JSON.stringify({ response, session }))
353
+ `)
354
+
355
+ assert.equal(output.response, 'Allowlist matched')
356
+ assert.equal(output.session.messages[0].source.senderId, '199900000001@lid')
357
+ assert.equal(output.session.messages[0].source.messageId, 'in-3')
358
+ })
359
+
360
+ it('routes send_voice_note to the current connector conversation when an audio file already exists', () => {
361
+ const output = runWithTempDataDir(`
362
+ const fs = await import('node:fs')
363
+ const path = await import('node:path')
364
+ const storageMod = await import('./src/lib/server/storage.ts')
365
+ const managerMod = await import('./src/lib/server/connectors/manager.ts')
366
+ const pluginsMod = await import('./src/lib/server/plugins.ts')
367
+ const toolsMod = await import('./src/lib/server/session-tools/index.ts')
368
+ const storage = storageMod.default || storageMod
369
+ const manager = managerMod.default || managerMod
370
+ const plugins = pluginsMod.default || pluginsMod
371
+ const toolsApi = toolsMod.default || toolsMod
372
+
373
+ const now = Date.now()
374
+ const sent = []
375
+ plugins.getPluginManager().registerBuiltin('test-voice-connector-plugin', {
376
+ name: 'Test Voice Connector Plugin',
377
+ connectors: [{
378
+ id: 'test-voice',
379
+ name: 'Test Voice',
380
+ description: 'Test voice connector',
381
+ startListener: async () => async () => {},
382
+ sendMessage: async (channelId, text, options) => {
383
+ sent.push({ channelId, text, options })
384
+ return { messageId: 'voice-out-1' }
385
+ },
386
+ }],
387
+ })
388
+
389
+ storage.saveSettings({})
390
+ storage.saveAgents({
391
+ agent_1: {
392
+ id: 'agent_1',
393
+ name: 'Molly',
394
+ provider: 'anthropic',
395
+ model: 'claude-test',
396
+ plugins: ['manage_connectors'],
397
+ createdAt: now,
398
+ updatedAt: now,
399
+ },
400
+ })
401
+ storage.saveConnectors({
402
+ conn_voice: {
403
+ id: 'conn_voice',
404
+ name: 'Test Voice Connector',
405
+ platform: 'test-voice',
406
+ agentId: 'agent_1',
407
+ credentialId: null,
408
+ config: { botToken: 'test-token' },
409
+ isEnabled: true,
410
+ status: 'stopped',
411
+ createdAt: now,
412
+ updatedAt: now,
413
+ },
414
+ })
415
+
416
+ const voicePath = path.join(process.env.DATA_DIR, 'gran-voice.mp3')
417
+ fs.writeFileSync(voicePath, Buffer.from('fake-mp3'))
418
+
419
+ storage.saveSessions({
420
+ session_1: {
421
+ id: 'session_1',
422
+ name: 'Molly',
423
+ cwd: process.env.WORKSPACE_DIR,
424
+ user: 'default',
425
+ provider: 'anthropic',
426
+ model: 'claude-test',
427
+ claudeSessionId: null,
428
+ messages: [{
429
+ role: 'user',
430
+ text: 'Send my gran a voice note',
431
+ time: now,
432
+ source: {
433
+ platform: 'whatsapp',
434
+ connectorId: 'conn_voice',
435
+ connectorName: 'Test Voice Connector',
436
+ channelId: '278200000001@s.whatsapp.net',
437
+ senderId: '278200000001@s.whatsapp.net',
438
+ senderName: 'Gran',
439
+ },
440
+ }],
441
+ createdAt: now,
442
+ lastActiveAt: now,
443
+ sessionType: 'human',
444
+ agentId: 'agent_1',
445
+ plugins: ['manage_connectors'],
446
+ connectorContext: {
447
+ connectorId: 'conn_voice',
448
+ platform: 'whatsapp',
449
+ channelId: '278200000001@s.whatsapp.net',
450
+ senderId: '278200000001@s.whatsapp.net',
451
+ senderName: 'Gran',
452
+ },
453
+ },
454
+ })
455
+
456
+ await manager.startConnector('conn_voice')
457
+ const built = await toolsApi.buildSessionTools(process.cwd(), ['manage_connectors'], {
458
+ sessionId: 'session_1',
459
+ agentId: 'agent_1',
460
+ platformAssignScope: 'self',
461
+ })
462
+
463
+ try {
464
+ const connectorTool = built.tools.find((tool) => tool.name === 'connector_message_tool')
465
+ const raw = await connectorTool.invoke({
466
+ action: 'send_voice_note',
467
+ connectorId: 'conn_voice',
468
+ mediaPath: voicePath,
469
+ })
470
+ console.log(JSON.stringify({ result: JSON.parse(String(raw)), sent }))
471
+ } finally {
472
+ await built.cleanup()
473
+ await manager.stopConnector('conn_voice')
474
+ }
475
+ `)
476
+
477
+ assert.equal(output.result.status, 'voice_sent')
478
+ assert.equal(output.result.to, '278200000001@s.whatsapp.net')
479
+ assert.equal(output.sent.length, 1)
480
+ assert.equal(output.sent[0].channelId, '278200000001@s.whatsapp.net')
481
+ assert.match(output.sent[0].text, /gran-voice\.mp3/)
482
+ })
483
+
484
+ it('restarts a stale connector automatically when an outbound send fails with connection closed', () => {
485
+ const output = runWithTempDataDir(`
486
+ const storageMod = await import('./src/lib/server/storage.ts')
487
+ const managerMod = await import('./src/lib/server/connectors/manager.ts')
488
+ const pluginsMod = await import('./src/lib/server/plugins.ts')
489
+ const storage = storageMod.default || storageMod
490
+ const manager = managerMod.default || managerMod
491
+ const plugins = pluginsMod.default || pluginsMod
492
+
493
+ const now = Date.now()
494
+ let startCount = 0
495
+ const attempts = []
496
+ plugins.getPluginManager().registerBuiltin('test-recover-connector-plugin', {
497
+ name: 'Test Recover Connector Plugin',
498
+ connectors: [{
499
+ id: 'test-recover',
500
+ name: 'Test Recover',
501
+ description: 'Test connector with recoverable send failure',
502
+ startListener: async () => {
503
+ startCount += 1
504
+ return async () => {}
505
+ },
506
+ sendMessage: async (channelId, text, options) => {
507
+ attempts.push({ channelId, text, options })
508
+ if (attempts.length === 1) throw new Error('Connection Closed')
509
+ return { messageId: 'recover-1' }
510
+ },
511
+ }],
512
+ })
513
+
514
+ storage.saveSettings({})
515
+ storage.saveConnectors({
516
+ conn_recover: {
517
+ id: 'conn_recover',
518
+ name: 'Recover Connector',
519
+ platform: 'test-recover',
520
+ agentId: 'agent_1',
521
+ credentialId: null,
522
+ config: { botToken: 'test-token' },
523
+ isEnabled: true,
524
+ status: 'stopped',
525
+ createdAt: now,
526
+ updatedAt: now,
527
+ },
528
+ })
529
+
530
+ try {
531
+ await manager.startConnector('conn_recover')
532
+ const result = await manager.sendConnectorMessage({
533
+ connectorId: 'conn_recover',
534
+ channelId: '15550001111',
535
+ text: 'hello after restart',
536
+ })
537
+ const health = Object.values(storage.loadConnectorHealth()).filter((entry) => entry.connectorId === 'conn_recover')
538
+ console.log(JSON.stringify({ result, attempts, startCount, health }))
539
+ } finally {
540
+ await manager.stopConnector('conn_recover')
541
+ }
542
+ `)
543
+
544
+ assert.equal(output.result.messageId, 'recover-1')
545
+ assert.equal(output.attempts.length, 2)
546
+ assert.equal(output.startCount, 2)
547
+ assert.equal(output.health.some((entry: { event?: string }) => entry.event === 'disconnected'), true)
548
+ })
549
+
550
+ it('blocks ambiguous connector sends when a thread references multiple people on the same connector', () => {
551
+ const output = runWithTempDataDir(`
552
+ const fs = await import('node:fs')
553
+ const path = await import('node:path')
554
+ const storageMod = await import('./src/lib/server/storage.ts')
555
+ const managerMod = await import('./src/lib/server/connectors/manager.ts')
556
+ const pluginsMod = await import('./src/lib/server/plugins.ts')
557
+ const toolsMod = await import('./src/lib/server/session-tools/index.ts')
558
+ const storage = storageMod.default || storageMod
559
+ const manager = managerMod.default || managerMod
560
+ const plugins = pluginsMod.default || pluginsMod
561
+ const toolsApi = toolsMod.default || toolsMod
562
+
563
+ const now = Date.now()
564
+ plugins.getPluginManager().registerBuiltin('test-ambiguous-connector-plugin', {
565
+ name: 'Test Ambiguous Connector Plugin',
566
+ connectors: [{
567
+ id: 'test-voice',
568
+ name: 'Test Voice',
569
+ description: 'Test voice connector',
570
+ startListener: async () => async () => {},
571
+ sendMessage: async () => ({ messageId: 'should-not-send' }),
572
+ }],
573
+ })
574
+
575
+ storage.saveSettings({})
576
+ storage.saveAgents({
577
+ agent_1: {
578
+ id: 'agent_1',
579
+ name: 'Molly',
580
+ provider: 'anthropic',
581
+ model: 'claude-test',
582
+ plugins: ['manage_connectors'],
583
+ createdAt: now,
584
+ updatedAt: now,
585
+ },
586
+ })
587
+ storage.saveConnectors({
588
+ conn_voice: {
589
+ id: 'conn_voice',
590
+ name: 'Test Voice Connector',
591
+ platform: 'test-voice',
592
+ agentId: 'agent_1',
593
+ credentialId: null,
594
+ config: { botToken: 'test-token' },
595
+ isEnabled: true,
596
+ status: 'stopped',
597
+ createdAt: now,
598
+ updatedAt: now,
599
+ },
600
+ })
601
+
602
+ const voicePath = path.join(process.env.DATA_DIR, 'gran-voice.mp3')
603
+ fs.writeFileSync(voicePath, Buffer.from('fake-mp3'))
604
+
605
+ storage.saveSessions({
606
+ session_1: {
607
+ id: 'session_1',
608
+ name: 'Molly',
609
+ cwd: process.env.WORKSPACE_DIR,
610
+ user: 'default',
611
+ provider: 'anthropic',
612
+ model: 'claude-test',
613
+ claudeSessionId: null,
614
+ messages: [
615
+ {
616
+ role: 'user',
617
+ text: 'Alice said hello',
618
+ time: now - 1000,
619
+ source: {
620
+ platform: 'whatsapp',
621
+ connectorId: 'conn_voice',
622
+ connectorName: 'Test Voice Connector',
623
+ channelId: '15550001111@s.whatsapp.net',
624
+ senderId: '15550001111@s.whatsapp.net',
625
+ senderName: 'Alice',
626
+ },
627
+ },
628
+ {
629
+ role: 'user',
630
+ text: 'Gran replied after that',
631
+ time: now,
632
+ source: {
633
+ platform: 'whatsapp',
634
+ connectorId: 'conn_voice',
635
+ connectorName: 'Test Voice Connector',
636
+ channelId: '278200000001@s.whatsapp.net',
637
+ senderId: '278200000001@s.whatsapp.net',
638
+ senderName: 'Gran',
639
+ },
640
+ },
641
+ ],
642
+ createdAt: now,
643
+ lastActiveAt: now,
644
+ sessionType: 'human',
645
+ agentId: 'agent_1',
646
+ plugins: ['manage_connectors'],
647
+ },
648
+ })
649
+
650
+ await manager.startConnector('conn_voice')
651
+ const built = await toolsApi.buildSessionTools(process.cwd(), ['manage_connectors'], {
652
+ sessionId: 'session_1',
653
+ agentId: 'agent_1',
654
+ platformAssignScope: 'self',
655
+ })
656
+
657
+ try {
658
+ const connectorTool = built.tools.find((tool) => tool.name === 'connector_message_tool')
659
+ const raw = await connectorTool.invoke({
660
+ action: 'send_voice_note',
661
+ connectorId: 'conn_voice',
662
+ mediaPath: voicePath,
663
+ })
664
+ console.log(JSON.stringify({ raw: String(raw) }))
665
+ } finally {
666
+ await built.cleanup()
667
+ await manager.stopConnector('conn_voice')
668
+ }
669
+ `)
670
+
671
+ assert.match(output.raw, /multiple connector recipients/)
672
+ assert.match(output.raw, /Alice/)
673
+ assert.match(output.raw, /Gran/)
674
+ })
675
+
676
+ it('keeps direct connector sessions isolated across four inbound senders for the same agent and mirrors their metadata into the main thread', () => {
677
+ const output = runWithTempDataDir(`
678
+ const storageMod = await import('./src/lib/server/storage.ts')
679
+ const managerMod = await import('./src/lib/server/connectors/manager.ts')
680
+ const providersMod = await import('./src/lib/providers/index.ts')
681
+ const storage = storageMod.default || storageMod
682
+ const manager = managerMod.default || managerMod
683
+ const providers = providersMod.default || providersMod
684
+
685
+ const now = Date.now()
686
+ providers.PROVIDERS['test-provider'] = {
687
+ id: 'test-provider',
688
+ name: 'Test Provider',
689
+ models: ['test-model'],
690
+ requiresApiKey: false,
691
+ requiresEndpoint: false,
692
+ handler: {
693
+ streamChat: async (opts) => {
694
+ const text = String(opts.message || '')
695
+ const match = text.match(/\\[(.*?)\\]/)
696
+ const name = match?.[1] || 'Unknown'
697
+ opts.write('data: ' + JSON.stringify({ t: 'r', text: 'Replying to ' + name }) + '\\n')
698
+ return ''
699
+ },
700
+ },
701
+ }
702
+
703
+ storage.saveSettings({})
704
+ storage.saveAgents({
705
+ agent_1: {
706
+ id: 'agent_1',
707
+ name: 'Molly',
708
+ provider: 'test-provider',
709
+ model: 'test-model',
710
+ plugins: [],
711
+ threadSessionId: 'agent_thread',
712
+ createdAt: now,
713
+ updatedAt: now,
714
+ },
715
+ })
716
+ storage.saveConnectors({
717
+ conn_1: {
718
+ id: 'conn_1',
719
+ name: 'WhatsApp',
720
+ platform: 'whatsapp',
721
+ agentId: 'agent_1',
722
+ credentialId: null,
723
+ config: { inboundDebounceMs: 0 },
724
+ isEnabled: true,
725
+ status: 'running',
726
+ createdAt: now,
727
+ updatedAt: now,
728
+ },
729
+ })
730
+ storage.saveSessions({
731
+ agent_thread: {
732
+ id: 'agent_thread',
733
+ name: 'Molly',
734
+ cwd: process.env.WORKSPACE_DIR,
735
+ user: 'default',
736
+ provider: 'test-provider',
737
+ model: 'test-model',
738
+ claudeSessionId: null,
739
+ messages: [],
740
+ createdAt: now,
741
+ lastActiveAt: now,
742
+ sessionType: 'human',
743
+ agentId: 'agent_1',
744
+ plugins: [],
745
+ },
746
+ })
747
+
748
+ const connector = storage.loadConnectors().conn_1
749
+ const inbound = async (senderId, senderName, messageId, text) => manager.routeConnectorMessageForTest(connector, {
750
+ platform: 'whatsapp',
751
+ channelId: senderId,
752
+ senderId,
753
+ senderName,
754
+ text,
755
+ messageId,
756
+ isGroup: false,
757
+ })
758
+
759
+ await inbound('15550001111@s.whatsapp.net', 'Alice', 'in-a', 'Hello from Alice')
760
+ await inbound('16660002222@s.whatsapp.net', 'Bob', 'in-b', 'Hello from Bob')
761
+ await inbound('278200000001@s.whatsapp.net', 'Gran', 'in-c', 'Hello from Gran')
762
+ await inbound('447700900123@s.whatsapp.net', 'Wayde', 'in-d', 'Hello from Wayde')
763
+
764
+ const sessions = storage.loadSessions()
765
+ const directSessions = Object.values(sessions)
766
+ .filter((entry) => String(entry.name || '').startsWith('connector:'))
767
+ .map((entry) => ({
768
+ id: entry.id,
769
+ name: entry.name,
770
+ senderName: entry.connectorContext?.senderName || null,
771
+ channelId: entry.connectorContext?.channelId || null,
772
+ texts: (entry.messages || []).map((m) => ({ role: m.role, text: m.text, source: m.source || null })),
773
+ }))
774
+ .sort((a, b) => String(a.senderName).localeCompare(String(b.senderName)))
775
+ const thread = sessions.agent_thread
776
+ console.log(JSON.stringify({ directSessions, threadMessages: thread.messages }))
777
+ `)
778
+
779
+ assert.equal(output.directSessions.length, 4)
780
+ assert.deepEqual(output.directSessions.map((entry) => entry.senderName), ['Alice', 'Bob', 'Gran', 'Wayde'])
781
+ assert.equal(output.directSessions.every((entry) => entry.texts.length === 2), true)
782
+ assert.equal(output.directSessions.every((entry) => entry.texts[0].source?.senderName === entry.senderName), true)
783
+ assert.equal(output.directSessions.every((entry) => entry.texts[1].text === `Replying to ${entry.senderName}`), true)
784
+ assert.equal(output.threadMessages.length, 8)
785
+ assert.deepEqual(
786
+ output.threadMessages.filter((msg) => msg.role === 'user').map((msg) => msg.source?.senderName),
787
+ ['Alice', 'Bob', 'Gran', 'Wayde'],
788
+ )
789
+ assert.deepEqual(
790
+ output.threadMessages.filter((msg) => msg.role === 'assistant').map((msg) => ({
791
+ text: msg.text,
792
+ senderName: msg.source?.senderName,
793
+ connectorId: msg.source?.connectorId,
794
+ })),
795
+ [
796
+ { text: 'Replying to Alice', senderName: 'Alice', connectorId: 'conn_1' },
797
+ { text: 'Replying to Bob', senderName: 'Bob', connectorId: 'conn_1' },
798
+ { text: 'Replying to Gran', senderName: 'Gran', connectorId: 'conn_1' },
799
+ { text: 'Replying to Wayde', senderName: 'Wayde', connectorId: 'conn_1' },
800
+ ],
801
+ )
802
+ assert.equal(output.threadMessages.every((msg) => msg.historyExcluded === true), true)
803
+ })
804
+
805
+ it('excludes mirrored connector transcript entries from direct agent-thread history', () => {
806
+ const output = runWithTempDataDir(`
807
+ const storageMod = await import('./src/lib/server/storage.ts')
808
+ const managerMod = await import('./src/lib/server/connectors/manager.ts')
809
+ const chatExecMod = await import('./src/lib/server/chat-execution.ts')
810
+ const providersMod = await import('./src/lib/providers/index.ts')
811
+ const storage = storageMod.default || storageMod
812
+ const manager = managerMod.default || managerMod
813
+ const chatExec = chatExecMod.default || chatExecMod
814
+ const providers = providersMod.default || providersMod
815
+
816
+ const now = Date.now()
817
+ providers.PROVIDERS['test-provider'] = {
818
+ id: 'test-provider',
819
+ name: 'Test Provider',
820
+ models: ['test-model'],
821
+ requiresApiKey: false,
822
+ requiresEndpoint: false,
823
+ handler: {
824
+ streamChat: async (opts) => {
825
+ const history = typeof opts.loadHistory === 'function' ? opts.loadHistory(opts.session.id) : []
826
+ return JSON.stringify({
827
+ historyCount: history.length,
828
+ texts: history.map((entry) => entry.text),
829
+ senderNames: history.map((entry) => entry.source?.senderName || null),
830
+ })
831
+ },
832
+ },
833
+ }
834
+
835
+ storage.saveSettings({})
836
+ storage.saveAgents({
837
+ agent_1: {
838
+ id: 'agent_1',
839
+ name: 'Molly',
840
+ provider: 'test-provider',
841
+ model: 'test-model',
842
+ plugins: [],
843
+ threadSessionId: 'agent_thread',
844
+ createdAt: now,
845
+ updatedAt: now,
846
+ },
847
+ })
848
+ storage.saveConnectors({
849
+ conn_1: {
850
+ id: 'conn_1',
851
+ name: 'WhatsApp',
852
+ platform: 'whatsapp',
853
+ agentId: 'agent_1',
854
+ credentialId: null,
855
+ config: { inboundDebounceMs: 0 },
856
+ isEnabled: true,
857
+ status: 'running',
858
+ createdAt: now,
859
+ updatedAt: now,
860
+ },
861
+ })
862
+ storage.saveSessions({
863
+ agent_thread: {
864
+ id: 'agent_thread',
865
+ name: 'Molly',
866
+ cwd: process.env.WORKSPACE_DIR,
867
+ user: 'default',
868
+ provider: 'test-provider',
869
+ model: 'test-model',
870
+ claudeSessionId: null,
871
+ messages: [],
872
+ createdAt: now,
873
+ lastActiveAt: now,
874
+ sessionType: 'human',
875
+ agentId: 'agent_1',
876
+ plugins: [],
877
+ },
878
+ })
879
+
880
+ const connector = storage.loadConnectors().conn_1
881
+ await manager.routeConnectorMessageForTest(connector, {
882
+ platform: 'whatsapp',
883
+ channelId: '15550001111@s.whatsapp.net',
884
+ senderId: '15550001111@s.whatsapp.net',
885
+ senderName: 'Alice',
886
+ text: 'Hello from Alice',
887
+ messageId: 'in-a',
888
+ isGroup: false,
889
+ })
890
+ await manager.routeConnectorMessageForTest(connector, {
891
+ platform: 'whatsapp',
892
+ channelId: '278200000001@s.whatsapp.net',
893
+ senderId: '278200000001@s.whatsapp.net',
894
+ senderName: 'Gran',
895
+ text: 'Hello from Gran',
896
+ messageId: 'in-g',
897
+ isGroup: false,
898
+ })
899
+
900
+ const result = await chatExec.executeSessionChatTurn({
901
+ sessionId: 'agent_thread',
902
+ message: 'This is Wayde in the app.',
903
+ })
904
+
905
+ const thread = storage.loadSessions().agent_thread
906
+ console.log(JSON.stringify({
907
+ reply: JSON.parse(result.text),
908
+ threadMessages: thread.messages,
909
+ }))
910
+ `)
911
+
912
+ assert.equal(output.reply.senderNames.includes('Alice'), false)
913
+ assert.equal(output.reply.senderNames.includes('Gran'), false)
914
+ assert.equal(output.reply.texts.some((entry) => /Alice|Gran/.test(String(entry))), false)
915
+ assert.equal(output.reply.historyCount >= 1, true)
916
+ assert.equal(output.threadMessages.some((msg) => msg.historyExcluded === true && msg.source?.connectorId === 'conn_1'), true)
917
+ })
918
+
919
+ it('creates one reusable connector-sender approval for unknown allowlist senders and allows them after approval', () => {
920
+ const output = runWithTempDataDir(`
921
+ const storageMod = await import('./src/lib/server/storage.ts')
922
+ const managerMod = await import('./src/lib/server/connectors/manager.ts')
923
+ const approvalsMod = await import('./src/lib/server/approvals.ts')
924
+ const pairingMod = await import('./src/lib/server/connectors/pairing.ts')
925
+ const providersMod = await import('./src/lib/providers/index.ts')
926
+ const storage = storageMod.default || storageMod
927
+ const manager = managerMod.default || managerMod
928
+ const approvals = approvalsMod.default || approvalsMod
929
+ const pairing = pairingMod.default || pairingMod
930
+ const providers = providersMod.default || providersMod
931
+
932
+ const now = Date.now()
933
+ providers.PROVIDERS['test-provider'] = {
934
+ id: 'test-provider',
935
+ name: 'Test Provider',
936
+ models: ['test-model'],
937
+ requiresApiKey: false,
938
+ requiresEndpoint: false,
939
+ handler: {
940
+ streamChat: async (opts) => {
941
+ const match = String(opts.message || '').match(/\\[(.*?)\\]/)
942
+ const name = match?.[1] || 'Unknown'
943
+ opts.write('data: ' + JSON.stringify({ t: 'r', text: 'Approved hello to ' + name }) + '\\n')
944
+ return ''
945
+ },
946
+ },
947
+ }
948
+
949
+ storage.saveSettings({ approvalsEnabled: true })
950
+ storage.saveAgents({
951
+ agent_1: {
952
+ id: 'agent_1',
953
+ name: 'Molly',
954
+ provider: 'test-provider',
955
+ model: 'test-model',
956
+ plugins: [],
957
+ threadSessionId: 'agent_thread',
958
+ createdAt: now,
959
+ updatedAt: now,
960
+ },
961
+ })
962
+ storage.saveConnectors({
963
+ conn_1: {
964
+ id: 'conn_1',
965
+ name: 'WhatsApp',
966
+ platform: 'whatsapp',
967
+ agentId: 'agent_1',
968
+ credentialId: null,
969
+ config: {
970
+ inboundDebounceMs: 0,
971
+ dmPolicy: 'allowlist',
972
+ allowFrom: '15550001111',
973
+ },
974
+ isEnabled: true,
975
+ status: 'running',
976
+ createdAt: now,
977
+ updatedAt: now,
978
+ },
979
+ })
980
+ storage.saveSessions({
981
+ agent_thread: {
982
+ id: 'agent_thread',
983
+ name: 'Molly',
984
+ cwd: process.env.WORKSPACE_DIR,
985
+ user: 'default',
986
+ provider: 'test-provider',
987
+ model: 'test-model',
988
+ claudeSessionId: null,
989
+ messages: [],
990
+ createdAt: now,
991
+ lastActiveAt: now,
992
+ sessionType: 'human',
993
+ agentId: 'agent_1',
994
+ plugins: [],
995
+ },
996
+ })
997
+
998
+ const connector = storage.loadConnectors().conn_1
999
+ const inbound = async (messageId, text) => manager.routeConnectorMessageForTest(connector, {
1000
+ platform: 'whatsapp',
1001
+ channelId: '16660002222@s.whatsapp.net',
1002
+ senderId: '16660002222@s.whatsapp.net',
1003
+ senderName: 'Bob',
1004
+ text,
1005
+ messageId,
1006
+ isGroup: false,
1007
+ })
1008
+
1009
+ const first = await inbound('in-b1', 'Hello from Bob')
1010
+ const second = await inbound('in-b2', 'Following up before approval')
1011
+ const pendingBefore = Object.values(storage.loadApprovals())
1012
+ await approvals.submitDecision(pendingBefore[0].id, true)
1013
+ const allowed = pairing.listStoredAllowedSenders('conn_1')
1014
+ const third = await inbound('in-b3', 'Hello after approval')
1015
+ const approvalsAfter = Object.values(storage.loadApprovals())
1016
+ const sessions = storage.loadSessions()
1017
+ const thread = sessions.agent_thread
1018
+ console.log(JSON.stringify({
1019
+ first,
1020
+ second,
1021
+ pendingBefore: pendingBefore.map((entry) => ({
1022
+ id: entry.id,
1023
+ category: entry.category,
1024
+ status: entry.status,
1025
+ title: entry.title,
1026
+ data: entry.data,
1027
+ })),
1028
+ allowed,
1029
+ third,
1030
+ approvalsAfter: approvalsAfter.map((entry) => ({ id: entry.id, status: entry.status })),
1031
+ threadMessages: thread.messages,
1032
+ }))
1033
+ `)
1034
+
1035
+ assert.match(output.first, /pending approval/i)
1036
+ assert.match(output.second, /pending approval/i)
1037
+ assert.equal(output.pendingBefore.length, 1)
1038
+ assert.equal(output.pendingBefore[0].category, 'connector_sender')
1039
+ assert.equal(output.pendingBefore[0].data.senderId, '16660002222@s.whatsapp.net')
1040
+ assert.deepEqual(output.allowed, ['16660002222@s.whatsapp.net'])
1041
+ assert.equal(output.third, 'Approved hello to Bob')
1042
+ assert.equal(output.approvalsAfter.length, 1)
1043
+ assert.equal(output.approvalsAfter[0].status, 'approved')
1044
+ assert.equal(output.threadMessages.some((msg) => msg.source?.senderName === 'Bob' && msg.historyExcluded === true), true)
1045
+ })
1046
+
1047
+ it('returns a friendly retry message instead of blank no-response when connector chat aborts', () => {
1048
+ const output = runWithTempDataDir(`
1049
+ const storageMod = await import('./src/lib/server/storage.ts')
1050
+ const managerMod = await import('./src/lib/server/connectors/manager.ts')
1051
+ const providersMod = await import('./src/lib/providers/index.ts')
1052
+ const storage = storageMod.default || storageMod
1053
+ const manager = managerMod.default || managerMod
1054
+ const providers = providersMod.default || providersMod
1055
+
1056
+ const now = Date.now()
1057
+ providers.PROVIDERS['test-provider'] = {
1058
+ id: 'test-provider',
1059
+ name: 'Test Provider',
1060
+ models: ['test-model'],
1061
+ requiresApiKey: false,
1062
+ requiresEndpoint: false,
1063
+ handler: {
1064
+ streamChat: async () => '',
1065
+ },
1066
+ }
1067
+ storage.saveSettings({})
1068
+ storage.saveAgents({
1069
+ agent_1: {
1070
+ id: 'agent_1',
1071
+ name: 'Molly',
1072
+ provider: 'test-provider',
1073
+ model: 'test-model',
1074
+ plugins: ['manage_connectors'],
1075
+ threadSessionId: 'agent_thread',
1076
+ createdAt: now,
1077
+ updatedAt: now,
1078
+ },
1079
+ })
1080
+ storage.saveConnectors({
1081
+ conn_1: {
1082
+ id: 'conn_1',
1083
+ name: 'WhatsApp',
1084
+ platform: 'whatsapp',
1085
+ agentId: 'agent_1',
1086
+ credentialId: null,
1087
+ config: { inboundDebounceMs: 0 },
1088
+ isEnabled: true,
1089
+ status: 'running',
1090
+ createdAt: now,
1091
+ updatedAt: now,
1092
+ },
1093
+ })
1094
+ storage.saveSessions({
1095
+ agent_thread: {
1096
+ id: 'agent_thread',
1097
+ name: 'Molly',
1098
+ cwd: process.env.WORKSPACE_DIR,
1099
+ user: 'default',
1100
+ provider: 'test-provider',
1101
+ model: 'test-model',
1102
+ claudeSessionId: null,
1103
+ messages: [],
1104
+ createdAt: now,
1105
+ lastActiveAt: now,
1106
+ sessionType: 'human',
1107
+ agentId: 'agent_1',
1108
+ plugins: ['manage_connectors'],
1109
+ },
1110
+ })
1111
+
1112
+ manager.setStreamAgentChatForTest(async (opts) => {
1113
+ opts.write('data: ' + JSON.stringify({ t: 'err', text: 'Abort' }) + '\\n')
1114
+ return {
1115
+ fullText: '',
1116
+ finalResponse: '',
1117
+ toolEvents: [],
1118
+ }
1119
+ })
1120
+
1121
+ try {
1122
+ const connector = storage.loadConnectors().conn_1
1123
+ const response = await manager.routeConnectorMessageForTest(connector, {
1124
+ platform: 'whatsapp',
1125
+ channelId: '15550001111@s.whatsapp.net',
1126
+ senderId: '15550001111@s.whatsapp.net',
1127
+ senderName: 'Alice',
1128
+ text: 'Hello?',
1129
+ messageId: 'in-hello',
1130
+ isGroup: false,
1131
+ })
1132
+ const sessions = storage.loadSessions()
1133
+ const directSession = Object.values(sessions).find((entry) => String(entry.name || '').startsWith('connector:'))
1134
+ const mainSession = sessions.agent_thread
1135
+ console.log(JSON.stringify({ response, directSession, mainSession }))
1136
+ } finally {
1137
+ manager.setStreamAgentChatForTest(null)
1138
+ }
1139
+ `)
1140
+
1141
+ assert.equal(output.response, 'Sorry, I hit a temporary issue while responding. Please try again.')
1142
+ assert.equal(output.directSession.messages.at(-1).role, 'assistant')
1143
+ assert.equal(output.directSession.messages.at(-1).text, 'Sorry, I hit a temporary issue while responding. Please try again.')
1144
+ assert.equal(output.mainSession.messages.at(-1).historyExcluded, true)
1145
+ assert.equal(output.mainSession.messages.at(-1).text, 'Sorry, I hit a temporary issue while responding. Please try again.')
1146
+ })
1147
+ })