@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
@@ -7,6 +7,7 @@ import { getScheduleSignatureKey } from '@/lib/schedule-dedupe'
7
7
  import { enqueueSystemEvent } from './system-events'
8
8
  import { requestHeartbeatNow } from './heartbeat-wake'
9
9
  import { processDueWatchJobs } from './watch-jobs'
10
+ import { isAgentDisabled } from './agent-availability'
10
11
 
11
12
  const TICK_INTERVAL = 60_000 // 60 seconds
12
13
  let intervalId: ReturnType<typeof setInterval> | null = null
@@ -32,6 +33,11 @@ interface SchedulerScheduleLike {
32
33
  runNumber?: number
33
34
  createdInSessionId?: string | null
34
35
  createdByAgentId?: string | null
36
+ followupConnectorId?: string | null
37
+ followupChannelId?: string | null
38
+ followupThreadId?: string | null
39
+ followupSenderId?: string | null
40
+ followupSenderName?: string | null
35
41
  }
36
42
 
37
43
  export function startScheduler() {
@@ -123,6 +129,16 @@ async function tick() {
123
129
  })
124
130
  continue
125
131
  }
132
+ if (isAgentDisabled(agent)) {
133
+ console.warn(`[scheduler] Skipping schedule "${schedule.name}" (${schedule.id}) because agent ${schedule.agentId} is disabled`)
134
+ advanceSchedule(schedule)
135
+ saveSchedules(schedules)
136
+ pushMainLoopEventToMainSessions({
137
+ type: 'schedule_skipped',
138
+ text: `Schedule skipped: "${schedule.name}" (${schedule.id}) — agent ${schedule.agentId} is disabled.`,
139
+ })
140
+ continue
141
+ }
126
142
 
127
143
  console.log(`[scheduler] Firing schedule "${schedule.name}" (${schedule.id})`)
128
144
  schedule.lastRunAt = now
@@ -185,6 +201,11 @@ async function tick() {
185
201
  sourceScheduleKey: scheduleSignature || null,
186
202
  createdInSessionId: schedule.createdInSessionId || null,
187
203
  createdByAgentId: schedule.createdByAgentId || null,
204
+ followupConnectorId: schedule.followupConnectorId || null,
205
+ followupChannelId: schedule.followupChannelId || null,
206
+ followupThreadId: schedule.followupThreadId || null,
207
+ followupSenderId: schedule.followupSenderId || null,
208
+ followupSenderName: schedule.followupSenderName || null,
188
209
  runNumber: schedule.runNumber,
189
210
  }
190
211
  schedule.linkedTaskId = taskId
@@ -204,6 +225,13 @@ async function tick() {
204
225
  if (schedule.createdInSessionId) {
205
226
  enqueueSystemEvent(schedule.createdInSessionId, `Schedule triggered: ${schedule.name}`)
206
227
  }
207
- requestHeartbeatNow({ agentId: schedule.agentId, reason: 'schedule' })
228
+ requestHeartbeatNow({
229
+ agentId: schedule.agentId,
230
+ eventId: `${schedule.id}:${schedule.runNumber}`,
231
+ reason: 'schedule',
232
+ source: `schedule:${schedule.id}`,
233
+ resumeMessage: `Schedule triggered: ${schedule.name}`,
234
+ detail: `Run #${schedule.runNumber} queued task ${taskId}.`,
235
+ })
208
236
  }
209
237
  }
@@ -0,0 +1,36 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+
4
+ import { buildSessionNoteMessage } from './session-note'
5
+
6
+ test('buildSessionNoteMessage defaults to assistant/system note metadata', () => {
7
+ const result = buildSessionNoteMessage({
8
+ text: 'Live test passed',
9
+ })
10
+
11
+ assert.ok(result)
12
+ assert.equal(result?.role, 'assistant')
13
+ assert.equal(result?.kind, 'system')
14
+ assert.equal(result?.text, 'Live test passed')
15
+ assert.equal(typeof result?.time, 'number')
16
+ })
17
+
18
+ test('buildSessionNoteMessage trims text and preserves explicit role/kind', () => {
19
+ const result = buildSessionNoteMessage({
20
+ text: ' Visible smoke report ',
21
+ role: 'user',
22
+ kind: 'chat',
23
+ time: 123,
24
+ })
25
+
26
+ assert.deepEqual(result, {
27
+ role: 'user',
28
+ kind: 'chat',
29
+ text: 'Visible smoke report',
30
+ time: 123,
31
+ })
32
+ })
33
+
34
+ test('buildSessionNoteMessage returns null for empty text', () => {
35
+ assert.equal(buildSessionNoteMessage({ text: ' ' }), null)
36
+ })
@@ -0,0 +1,42 @@
1
+ import type { Message, MessageToolEvent } from '@/types'
2
+ import { loadSessions, saveSessions } from './storage'
3
+ import { notify } from './ws-hub'
4
+
5
+ export interface SessionNoteInput {
6
+ sessionId: string
7
+ text: string
8
+ role?: Message['role']
9
+ kind?: Message['kind']
10
+ toolEvents?: MessageToolEvent[]
11
+ time?: number
12
+ }
13
+
14
+ export function buildSessionNoteMessage(input: Omit<SessionNoteInput, 'sessionId'>): Message | null {
15
+ const trimmed = String(input.text || '').trim()
16
+ if (!trimmed) return null
17
+ return {
18
+ role: input.role || 'assistant',
19
+ kind: input.kind || 'system',
20
+ text: trimmed,
21
+ time: typeof input.time === 'number' && Number.isFinite(input.time) ? input.time : Date.now(),
22
+ ...(Array.isArray(input.toolEvents) && input.toolEvents.length ? { toolEvents: input.toolEvents } : {}),
23
+ }
24
+ }
25
+
26
+ export function appendSessionNote(input: SessionNoteInput): Message | null {
27
+ const sessions = loadSessions()
28
+ const session = sessions[input.sessionId]
29
+ if (!session) return null
30
+ if (!Array.isArray(session.messages)) session.messages = []
31
+
32
+ const next = buildSessionNoteMessage(input)
33
+ if (!next) return null
34
+
35
+ session.messages.push(next)
36
+ session.lastActiveAt = next.time
37
+ sessions[input.sessionId] = session
38
+ saveSessions(sessions)
39
+ notify('sessions')
40
+ notify(`messages:${input.sessionId}`)
41
+ return next
42
+ }
@@ -7,6 +7,7 @@ import { log } from './logger'
7
7
  import { isInternalHeartbeatRun } from './heartbeat-source'
8
8
  import { cleanupSessionBrowser } from './session-tools/web'
9
9
  import { cancelDelegationJobsForParentSession } from './delegation-jobs'
10
+ import { handleMainLoopRunResult } from './main-agent-loop'
10
11
 
11
12
  export type SessionRunStatus = 'queued' | 'running' | 'completed' | 'failed' | 'cancelled'
12
13
  export type SessionQueueMode = 'followup' | 'steer' | 'collect'
@@ -218,7 +219,10 @@ export function cancelAllHeartbeatRuns(reason = 'Heartbeat disabled globally'):
218
219
  async function drainExecution(executionKey: string): Promise<void> {
219
220
  if (state.runningByExecution.has(executionKey)) return
220
221
  const q = queueForExecution(executionKey)
221
- const next = q.shift()
222
+ // Priority: user (non-heartbeat) runs go first. If a heartbeat is queued
223
+ // behind a user run, the user run takes priority.
224
+ const userIdx = q.findIndex(e => !isInternalHeartbeatRun(e.run.internal, e.run.source))
225
+ const next = userIdx >= 0 ? q.splice(userIdx, 1)[0] : q.shift()
222
226
  if (!next) return
223
227
 
224
228
  state.runningByExecution.set(executionKey, next)
@@ -276,6 +280,36 @@ async function drainExecution(executionKey: string): Promise<void> {
276
280
  error: next.run.error || null,
277
281
  durationMs: (next.run.endedAt || now()) - (next.run.startedAt || now()),
278
282
  })
283
+ const followup = handleMainLoopRunResult({
284
+ sessionId: next.run.sessionId,
285
+ message: next.message,
286
+ internal: next.run.internal,
287
+ source: next.run.source,
288
+ resultText: result.text,
289
+ error: next.run.error,
290
+ toolEvents: result.toolEvents,
291
+ inputTokens: result.inputTokens,
292
+ outputTokens: result.outputTokens,
293
+ estimatedCost: result.estimatedCost,
294
+ })
295
+ if (followup) {
296
+ setTimeout(() => {
297
+ try {
298
+ enqueueSessionRun({
299
+ sessionId: next.run.sessionId,
300
+ message: followup.message,
301
+ internal: true,
302
+ source: 'main-loop-followup',
303
+ mode: 'followup',
304
+ dedupeKey: followup.dedupeKey,
305
+ })
306
+ } catch (err: unknown) {
307
+ log.warn('session-run', `Main loop follow-up enqueue failed for ${next.run.sessionId}`, {
308
+ error: err instanceof Error ? err.message : String(err),
309
+ })
310
+ }
311
+ }, Math.max(0, followup.delayMs || 0))
312
+ }
279
313
  next.resolve(result)
280
314
  } catch (err: any) {
281
315
  const aborted = next.signalController.signal.aborted
@@ -385,6 +419,18 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
385
419
  cancelPendingForSession(input.sessionId, 'Cancelled by steer mode')
386
420
  }
387
421
 
422
+ // Heartbeat preemption: if a user chat arrives while a heartbeat is running,
423
+ // abort the heartbeat so the user doesn't wait. The heartbeat will retry
424
+ // on the next tick.
425
+ if (!internal && source === 'chat') {
426
+ const running = state.runningByExecution.get(executionKey)
427
+ if (running && isInternalHeartbeatRun(running.run.internal, running.run.source)) {
428
+ log.info('session-run', `Preempting heartbeat ${running.run.id} for user chat on ${input.sessionId}`)
429
+ abortSessionRuntime(running, 'Preempted by user chat')
430
+ state.runningByExecution.delete(executionKey)
431
+ }
432
+ }
433
+
388
434
  const running = state.runningByExecution.get(executionKey)
389
435
  const q = queueForExecution(executionKey)
390
436
  if (mode === 'collect' && !input.imagePath && !input.imageUrl && !input.attachedFiles?.length) {
@@ -475,15 +521,48 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
475
521
  export function getSessionRunState(sessionId: string): {
476
522
  runningRunId?: string
477
523
  queueLength: number
524
+ } {
525
+ const summary = getSessionExecutionState(sessionId)
526
+ return {
527
+ runningRunId: summary.runningRunId,
528
+ queueLength: summary.queueLength,
529
+ }
530
+ }
531
+
532
+ export function getSessionExecutionState(sessionId: string): {
533
+ runningRunId?: string
534
+ queueLength: number
535
+ hasRunning: boolean
536
+ hasQueued: boolean
537
+ hasRunningHeartbeat: boolean
538
+ hasQueuedHeartbeat: boolean
539
+ hasRunningNonHeartbeat: boolean
540
+ hasQueuedNonHeartbeat: boolean
478
541
  } {
479
542
  const executionKey = executionKeyForSession(sessionId)
480
543
  const running = state.runningByExecution.get(executionKey)
481
- const queued = queueForExecution(executionKey).filter((entry) => entry.run.sessionId === sessionId).length
544
+ const runningMatchesSession = running?.run.sessionId === sessionId
545
+ const runningHeartbeat = Boolean(
546
+ runningMatchesSession
547
+ && isInternalHeartbeatRun(running.run.internal, running.run.source),
548
+ )
549
+ const runningNonHeartbeat = Boolean(runningMatchesSession && !runningHeartbeat)
550
+ const queuedEntries = queueForExecution(executionKey).filter((entry) => entry.run.sessionId === sessionId)
551
+ const queuedHeartbeat = queuedEntries.filter((entry) =>
552
+ isInternalHeartbeatRun(entry.run.internal, entry.run.source),
553
+ ).length
554
+ const queuedNonHeartbeat = queuedEntries.length - queuedHeartbeat
482
555
  return {
483
- runningRunId: (running?.run.sessionId === sessionId && running.run.status === 'running')
556
+ runningRunId: (runningMatchesSession && running?.run.status === 'running')
484
557
  ? running.run.id
485
558
  : undefined,
486
- queueLength: queued,
559
+ queueLength: queuedEntries.length,
560
+ hasRunning: Boolean(runningMatchesSession),
561
+ hasQueued: queuedEntries.length > 0,
562
+ hasRunningHeartbeat: runningHeartbeat,
563
+ hasQueuedHeartbeat: queuedHeartbeat > 0,
564
+ hasRunningNonHeartbeat: runningNonHeartbeat,
565
+ hasQueuedNonHeartbeat: queuedNonHeartbeat > 0,
487
566
  }
488
567
  }
489
568
 
@@ -6,6 +6,7 @@ import type { ToolBuildContext } from './context'
6
6
  import type { Plugin, PluginHooks } from '@/types'
7
7
  import { getPluginManager } from '../plugins'
8
8
  import { normalizeToolInputArgs } from './normalize-tool-args'
9
+ import { normalizeCanvasContent, summarizeCanvasContent } from '@/lib/canvas-content'
9
10
 
10
11
  /**
11
12
  * Core Canvas Execution Logic
@@ -14,6 +15,7 @@ async function executeCanvasAction(args: Record<string, unknown>, context: { ses
14
15
  const normalized = normalizeToolInputArgs(args)
15
16
  const action = normalized.action as string
16
17
  const content = normalized.content as string | undefined
18
+ const document = normalized.document
17
19
  try {
18
20
  const sessionId = context.sessionId
19
21
  if (!sessionId) return 'Error: no active session for canvas.'
@@ -23,13 +25,18 @@ async function executeCanvasAction(args: Record<string, unknown>, context: { ses
23
25
  if (!session) return 'Error: session not found.'
24
26
 
25
27
  if (action === 'present') {
26
- if (!content) return 'Error: content is required for present action.'
27
- ;(session as Record<string, unknown>).canvasContent = content
28
+ const nextContent = normalizeCanvasContent(document ?? content)
29
+ if (!nextContent) return 'Error: content or document is required for present action.'
30
+ ;(session as Record<string, unknown>).canvasContent = nextContent
28
31
  session.lastActiveAt = Date.now()
29
32
  sessions[sessionId] = session
30
33
  saveSessions(sessions)
31
34
  notify(`canvas:${sessionId}`)
32
- return JSON.stringify({ ok: true, action: 'present', contentLength: content.length })
35
+ return JSON.stringify({
36
+ ok: true,
37
+ action: 'present',
38
+ ...summarizeCanvasContent(nextContent),
39
+ })
33
40
  }
34
41
 
35
42
  if (action === 'hide') {
@@ -42,14 +49,8 @@ async function executeCanvasAction(args: Record<string, unknown>, context: { ses
42
49
  }
43
50
 
44
51
  if (action === 'snapshot') {
45
- const current = (session as Record<string, unknown>).canvasContent
46
- return JSON.stringify({
47
- ok: true,
48
- action: 'snapshot',
49
- hasContent: !!current,
50
- contentLength: typeof current === 'string' ? current.length : 0,
51
- preview: typeof current === 'string' ? current.slice(0, 500) : null,
52
- })
52
+ const current = normalizeCanvasContent((session as Record<string, unknown>).canvasContent)
53
+ return JSON.stringify({ ok: true, action: 'snapshot', ...summarizeCanvasContent(current) })
53
54
  }
54
55
 
55
56
  return `Unknown canvas action "${action}".`
@@ -73,7 +74,8 @@ const CanvasPlugin: Plugin = {
73
74
  type: 'object',
74
75
  properties: {
75
76
  action: { type: 'string', enum: ['present', 'hide', 'snapshot'] },
76
- content: { type: 'string' }
77
+ content: { type: 'string' },
78
+ document: { type: 'object', additionalProperties: true },
77
79
  },
78
80
  required: ['action']
79
81
  },
@@ -0,0 +1,37 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { describe, it } from 'node:test'
5
+ import { UPLOAD_DIR } from '../storage'
6
+ import { resolveConnectorMediaInput } from './connector'
7
+
8
+ describe('resolveConnectorMediaInput', () => {
9
+ it('resolves /api/uploads urls passed via mediaPath back to disk', () => {
10
+ const filename = `screenshot-test-${Date.now()}.png`
11
+ const uploadPath = path.join(UPLOAD_DIR, filename)
12
+ fs.mkdirSync(UPLOAD_DIR, { recursive: true })
13
+ fs.writeFileSync(uploadPath, 'png')
14
+
15
+ try {
16
+ const resolved = resolveConnectorMediaInput({
17
+ cwd: process.cwd(),
18
+ mediaPath: `/api/uploads/${filename}`,
19
+ })
20
+ assert.equal(resolved.error, undefined)
21
+ assert.equal(resolved.mediaPath, uploadPath)
22
+ } finally {
23
+ fs.rmSync(uploadPath, { force: true })
24
+ }
25
+ })
26
+
27
+ it('treats remote urls passed via mediaPath as sendable urls instead of local files', () => {
28
+ const resolved = resolveConnectorMediaInput({
29
+ cwd: process.cwd(),
30
+ mediaPath: 'https://example.com/report.pdf',
31
+ })
32
+
33
+ assert.equal(resolved.error, undefined)
34
+ assert.equal(resolved.mediaPath, undefined)
35
+ assert.equal(resolved.fileUrl, 'https://example.com/report.pdf')
36
+ })
37
+ })
@@ -0,0 +1,138 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ import {
5
+ CONNECTOR_MESSAGE_TOOL_ACTIONS,
6
+ CONNECTOR_MESSAGE_TOOL_PARAMETERS,
7
+ inferConnectorActionName,
8
+ normalizeConnectorActionInputAliases,
9
+ normalizeConnectorActionName,
10
+ } from './connector'
11
+ import { getPluginManager } from '../plugins'
12
+ import { buildSessionTools } from './index'
13
+
14
+ describe('connector_message_tool contract', () => {
15
+ it('exposes the connector actions and voice-note fields through the plugin schema', () => {
16
+ const entry = getPluginManager()
17
+ .getTools(['manage_connectors'])
18
+ .find((tool) => tool.tool.name === 'connector_message_tool')
19
+
20
+ assert.ok(entry, 'connector_message_tool should be registered for manage_connectors')
21
+
22
+ const props = (entry!.tool.parameters?.properties ?? {}) as Record<string, { type?: string; enum?: string[] }>
23
+ assert.deepEqual(props.action?.enum, [...CONNECTOR_MESSAGE_TOOL_ACTIONS])
24
+ assert.equal(props.approved?.type, 'boolean')
25
+ assert.equal(props.ptt?.type, 'boolean')
26
+ assert.equal(props.voiceText?.type, 'string')
27
+ assert.equal(props.recipientId?.type, 'string')
28
+ assert.equal(props.channel?.type, 'string')
29
+ assert.equal(Array.isArray(entry!.tool.parameters?.required), false)
30
+ assert.equal(Array.isArray((CONNECTOR_MESSAGE_TOOL_PARAMETERS as { required?: unknown }).required), false)
31
+ })
32
+
33
+ it('normalizes legacy rich-message aliases to the current connector actions', () => {
34
+ assert.equal(normalizeConnectorActionName('message_react'), 'react')
35
+ assert.equal(normalizeConnectorActionName('message_edit'), 'edit')
36
+ assert.equal(normalizeConnectorActionName('message_delete'), 'delete')
37
+ assert.equal(normalizeConnectorActionName('message_pin'), 'pin')
38
+ assert.equal(normalizeConnectorActionName('send_voice_note'), 'send_voice_note')
39
+ })
40
+
41
+ it('infers send-style actions from partial connector payloads', () => {
42
+ assert.equal(inferConnectorActionName({ voiceText: 'hello there' }), 'send_voice_note')
43
+ assert.equal(inferConnectorActionName({ followUpMessage: 'check back later', delaySec: 60 }), 'schedule_followup')
44
+ assert.equal(inferConnectorActionName({ message: 'plain text message' }), 'send')
45
+ assert.equal(inferConnectorActionName({}), null)
46
+ })
47
+
48
+ it('normalizes connector and target aliases from model-generated delivery calls', () => {
49
+ const running = [{ id: 'd81cd63b', name: 'Main Whatsapp connection' }]
50
+
51
+ assert.deepEqual(
52
+ normalizeConnectorActionInputAliases({
53
+ action: 'send_voice_note',
54
+ channel: 'Main Whatsapp connection',
55
+ recipientId: '07958148127',
56
+ }, running),
57
+ {
58
+ action: 'send_voice_note',
59
+ channel: 'Main Whatsapp connection',
60
+ recipientId: '07958148127',
61
+ connectorId: 'd81cd63b',
62
+ to: '07958148127',
63
+ },
64
+ )
65
+
66
+ assert.deepEqual(
67
+ normalizeConnectorActionInputAliases({
68
+ action: 'send_voice_note',
69
+ id: 'd81cd63b',
70
+ target: '199900000001@lid',
71
+ }, running),
72
+ {
73
+ action: 'send_voice_note',
74
+ id: 'd81cd63b',
75
+ target: '199900000001@lid',
76
+ connectorId: 'd81cd63b',
77
+ to: '199900000001@lid',
78
+ },
79
+ )
80
+ })
81
+
82
+ it('treats raw id as messageId for message actions instead of as a target alias', () => {
83
+ assert.deepEqual(
84
+ normalizeConnectorActionInputAliases({
85
+ action: 'react',
86
+ id: 'msg-123',
87
+ emoji: '👍',
88
+ }, [{ id: 'conn-1', name: 'Primary connector' }]),
89
+ {
90
+ action: 'react',
91
+ id: 'msg-123',
92
+ emoji: '👍',
93
+ messageId: 'msg-123',
94
+ },
95
+ )
96
+ })
97
+
98
+ it('buildSessionTools exposes the native connector schema instead of the legacy passthrough bridge', async () => {
99
+ const built = await buildSessionTools(process.cwd(), ['manage_connectors'], {
100
+ sessionId: 'connector-native-schema-test',
101
+ agentId: 'default',
102
+ platformAssignScope: 'self',
103
+ })
104
+
105
+ try {
106
+ const connectorTool = built.tools.find((tool) => tool.name === 'connector_message_tool')
107
+ assert.ok(connectorTool, 'connector_message_tool should be available when manage_connectors is enabled')
108
+
109
+ const schema = (connectorTool as { schema?: { safeParse: (value: unknown) => { success: boolean } } }).schema
110
+ assert.ok(schema, 'connector_message_tool should expose a validation schema')
111
+ assert.equal(schema.safeParse({ action: 'send_voice_note', approved: true, ptt: true }).success, true)
112
+ assert.equal(schema.safeParse({ voiceText: 'hello', recipientId: '07958148127', channel: 'Main Whatsapp connection' }).success, true)
113
+ assert.equal(schema.safeParse({ action: 'message_react' }).success, true)
114
+ assert.equal(schema.safeParse({}).success, true)
115
+ assert.equal(schema.safeParse({ action: 'bogus_action' }).success, false)
116
+ } finally {
117
+ await built.cleanup()
118
+ }
119
+ })
120
+
121
+ it('loads connector_message_tool when a session only has the tool-level grant alias', async () => {
122
+ const built = await buildSessionTools(process.cwd(), ['connector_message_tool'], {
123
+ sessionId: 'connector-tool-alias-test',
124
+ agentId: 'default',
125
+ platformAssignScope: 'self',
126
+ })
127
+
128
+ try {
129
+ assert.equal(
130
+ built.tools.some((tool) => tool.name === 'connector_message_tool'),
131
+ true,
132
+ 'connector_message_tool should load from its persisted approval alias',
133
+ )
134
+ } finally {
135
+ await built.cleanup()
136
+ }
137
+ })
138
+ })