@swarmclawai/swarmclaw 0.7.2 → 0.7.4

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 (274) hide show
  1. package/README.md +116 -50
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +43 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +39 -8
  12. package/src/app/api/agents/route.ts +35 -2
  13. package/src/app/api/auth/route.ts +77 -8
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  19. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  20. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  21. package/src/app/api/chats/[id]/route.ts +30 -0
  22. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  23. package/src/app/api/chats/heartbeat/route.ts +2 -1
  24. package/src/app/api/chats/route.ts +23 -1
  25. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  26. package/src/app/api/connectors/doctor/route.ts +13 -0
  27. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  28. package/src/app/api/external-agents/[id]/route.ts +31 -0
  29. package/src/app/api/external-agents/register/route.ts +3 -0
  30. package/src/app/api/external-agents/route.ts +66 -0
  31. package/src/app/api/files/open/route.ts +16 -14
  32. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  33. package/src/app/api/gateways/[id]/route.ts +79 -0
  34. package/src/app/api/gateways/route.ts +57 -0
  35. package/src/app/api/memory/maintenance/route.ts +11 -1
  36. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  37. package/src/app/api/openclaw/gateway/route.ts +10 -7
  38. package/src/app/api/openclaw/skills/route.ts +12 -4
  39. package/src/app/api/plugins/dependencies/route.ts +24 -0
  40. package/src/app/api/plugins/install/route.ts +15 -92
  41. package/src/app/api/plugins/route.ts +3 -26
  42. package/src/app/api/plugins/settings/route.ts +17 -12
  43. package/src/app/api/plugins/ui/route.ts +1 -0
  44. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  45. package/src/app/api/schedules/[id]/route.ts +38 -9
  46. package/src/app/api/schedules/route.ts +51 -28
  47. package/src/app/api/settings/route.ts +55 -17
  48. package/src/app/api/setup/doctor/route.ts +6 -4
  49. package/src/app/api/tasks/[id]/route.ts +16 -6
  50. package/src/app/api/tasks/bulk/route.ts +3 -3
  51. package/src/app/api/tasks/route.ts +9 -4
  52. package/src/app/api/webhooks/[id]/route.ts +8 -1
  53. package/src/app/page.tsx +135 -17
  54. package/src/cli/binary.test.js +142 -0
  55. package/src/cli/index.js +38 -11
  56. package/src/cli/index.test.js +195 -0
  57. package/src/cli/index.ts +21 -12
  58. package/src/cli/server-cmd.test.js +59 -0
  59. package/src/cli/spec.js +20 -2
  60. package/src/components/agents/agent-card.tsx +15 -12
  61. package/src/components/agents/agent-chat-list.tsx +101 -1
  62. package/src/components/agents/agent-list.tsx +46 -9
  63. package/src/components/agents/agent-sheet.tsx +456 -23
  64. package/src/components/agents/inspector-panel.tsx +110 -49
  65. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  66. package/src/components/auth/access-key-gate.tsx +36 -97
  67. package/src/components/auth/setup-wizard.tsx +970 -275
  68. package/src/components/chat/chat-area.tsx +70 -27
  69. package/src/components/chat/chat-card.tsx +6 -21
  70. package/src/components/chat/chat-header.tsx +263 -366
  71. package/src/components/chat/chat-list.tsx +62 -26
  72. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  73. package/src/components/chat/message-list.tsx +145 -19
  74. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  75. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  76. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  77. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  78. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  79. package/src/components/chatrooms/chatroom-view.tsx +422 -209
  80. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  81. package/src/components/connectors/connector-list.tsx +265 -127
  82. package/src/components/connectors/connector-sheet.tsx +217 -0
  83. package/src/components/gateways/gateway-sheet.tsx +567 -0
  84. package/src/components/home/home-view.tsx +128 -4
  85. package/src/components/input/chat-input.tsx +135 -86
  86. package/src/components/layout/app-layout.tsx +385 -194
  87. package/src/components/layout/mobile-header.tsx +26 -8
  88. package/src/components/memory/memory-browser.tsx +71 -6
  89. package/src/components/memory/memory-card.tsx +18 -0
  90. package/src/components/memory/memory-detail.tsx +58 -31
  91. package/src/components/memory/memory-sheet.tsx +32 -4
  92. package/src/components/plugins/plugin-list.tsx +15 -3
  93. package/src/components/plugins/plugin-sheet.tsx +118 -9
  94. package/src/components/projects/project-detail.tsx +189 -1
  95. package/src/components/providers/provider-list.tsx +158 -2
  96. package/src/components/providers/provider-sheet.tsx +81 -70
  97. package/src/components/shared/agent-picker-list.tsx +2 -2
  98. package/src/components/shared/bottom-sheet.tsx +31 -15
  99. package/src/components/shared/command-palette.tsx +111 -24
  100. package/src/components/shared/confirm-dialog.tsx +45 -30
  101. package/src/components/shared/model-combobox.tsx +90 -8
  102. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  103. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  104. package/src/components/shared/settings/section-heartbeat.tsx +88 -6
  105. package/src/components/shared/settings/section-orchestrator.tsx +6 -3
  106. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  107. package/src/components/shared/settings/section-secrets.tsx +6 -6
  108. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  109. package/src/components/shared/settings/section-voice.tsx +5 -1
  110. package/src/components/shared/settings/section-web-search.tsx +10 -2
  111. package/src/components/shared/settings/settings-page.tsx +248 -47
  112. package/src/components/tasks/approvals-panel.tsx +211 -18
  113. package/src/components/tasks/task-board.tsx +242 -46
  114. package/src/components/ui/dialog.tsx +2 -2
  115. package/src/components/usage/metrics-dashboard.tsx +74 -1
  116. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  117. package/src/components/wallets/wallet-panel.tsx +17 -5
  118. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  119. package/src/lib/auth.ts +17 -0
  120. package/src/lib/chat-streaming-state.test.ts +108 -0
  121. package/src/lib/chat-streaming-state.ts +108 -0
  122. package/src/lib/heartbeat-defaults.ts +48 -0
  123. package/src/lib/memory-presentation.ts +59 -0
  124. package/src/lib/openclaw-agent-id.test.ts +14 -0
  125. package/src/lib/openclaw-agent-id.ts +31 -0
  126. package/src/lib/provider-model-discovery-client.ts +29 -0
  127. package/src/lib/providers/index.ts +12 -5
  128. package/src/lib/runtime-loop.ts +105 -3
  129. package/src/lib/safe-storage.ts +6 -1
  130. package/src/lib/server/agent-assignment.test.ts +112 -0
  131. package/src/lib/server/agent-assignment.ts +169 -0
  132. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  133. package/src/lib/server/agent-runtime-config.ts +277 -0
  134. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  135. package/src/lib/server/approvals-auto-approve.test.ts +264 -0
  136. package/src/lib/server/approvals.ts +483 -75
  137. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  138. package/src/lib/server/browser-state.test.ts +118 -0
  139. package/src/lib/server/browser-state.ts +123 -0
  140. package/src/lib/server/build-llm.test.ts +44 -0
  141. package/src/lib/server/build-llm.ts +11 -4
  142. package/src/lib/server/builtin-plugins.ts +34 -0
  143. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  144. package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
  145. package/src/lib/server/chat-execution.ts +402 -125
  146. package/src/lib/server/chatroom-health.test.ts +26 -0
  147. package/src/lib/server/chatroom-health.ts +2 -3
  148. package/src/lib/server/chatroom-helpers.test.ts +74 -2
  149. package/src/lib/server/chatroom-helpers.ts +144 -11
  150. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  151. package/src/lib/server/connectors/discord.ts +175 -11
  152. package/src/lib/server/connectors/doctor.test.ts +80 -0
  153. package/src/lib/server/connectors/doctor.ts +116 -0
  154. package/src/lib/server/connectors/manager.ts +994 -130
  155. package/src/lib/server/connectors/policy.test.ts +222 -0
  156. package/src/lib/server/connectors/policy.ts +452 -0
  157. package/src/lib/server/connectors/slack.ts +189 -10
  158. package/src/lib/server/connectors/telegram.ts +65 -15
  159. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  160. package/src/lib/server/connectors/thread-context.ts +72 -0
  161. package/src/lib/server/connectors/types.ts +41 -11
  162. package/src/lib/server/daemon-state.ts +62 -3
  163. package/src/lib/server/data-dir.ts +13 -0
  164. package/src/lib/server/delegation-jobs.test.ts +140 -0
  165. package/src/lib/server/delegation-jobs.ts +248 -0
  166. package/src/lib/server/document-utils.test.ts +47 -0
  167. package/src/lib/server/document-utils.ts +397 -0
  168. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  169. package/src/lib/server/eval/agent-regression.ts +1742 -0
  170. package/src/lib/server/eval/runner.ts +11 -1
  171. package/src/lib/server/eval/store.ts +2 -1
  172. package/src/lib/server/heartbeat-service.ts +23 -43
  173. package/src/lib/server/heartbeat-source.test.ts +22 -0
  174. package/src/lib/server/heartbeat-source.ts +7 -0
  175. package/src/lib/server/identity-continuity.test.ts +77 -0
  176. package/src/lib/server/identity-continuity.ts +127 -0
  177. package/src/lib/server/mailbox-utils.ts +347 -0
  178. package/src/lib/server/main-agent-loop.ts +31 -964
  179. package/src/lib/server/memory-db.ts +4 -6
  180. package/src/lib/server/memory-tiers.ts +40 -0
  181. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  182. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  183. package/src/lib/server/openclaw-exec-config.ts +6 -5
  184. package/src/lib/server/openclaw-gateway.ts +123 -36
  185. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  186. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  187. package/src/lib/server/openclaw-sync.ts +3 -2
  188. package/src/lib/server/orchestrator-lg.ts +18 -8
  189. package/src/lib/server/orchestrator.ts +5 -4
  190. package/src/lib/server/playwright-proxy.mjs +27 -3
  191. package/src/lib/server/plugins.test.ts +215 -0
  192. package/src/lib/server/plugins.ts +832 -69
  193. package/src/lib/server/provider-health.ts +33 -3
  194. package/src/lib/server/provider-model-discovery.ts +481 -0
  195. package/src/lib/server/queue.ts +4 -21
  196. package/src/lib/server/runtime-settings.test.ts +119 -0
  197. package/src/lib/server/runtime-settings.ts +12 -92
  198. package/src/lib/server/schedule-normalization.ts +187 -0
  199. package/src/lib/server/scheduler.ts +2 -0
  200. package/src/lib/server/session-archive-memory.test.ts +85 -0
  201. package/src/lib/server/session-archive-memory.ts +230 -0
  202. package/src/lib/server/session-mailbox.ts +8 -18
  203. package/src/lib/server/session-reset-policy.test.ts +99 -0
  204. package/src/lib/server/session-reset-policy.ts +311 -0
  205. package/src/lib/server/session-run-manager.ts +33 -80
  206. package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
  207. package/src/lib/server/session-tools/calendar.ts +2 -12
  208. package/src/lib/server/session-tools/connector.ts +109 -8
  209. package/src/lib/server/session-tools/context.ts +14 -2
  210. package/src/lib/server/session-tools/crawl.ts +447 -0
  211. package/src/lib/server/session-tools/crud.ts +96 -34
  212. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  213. package/src/lib/server/session-tools/delegate.ts +406 -20
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  215. package/src/lib/server/session-tools/discovery.ts +40 -12
  216. package/src/lib/server/session-tools/document.ts +283 -0
  217. package/src/lib/server/session-tools/email.ts +1 -3
  218. package/src/lib/server/session-tools/extract.ts +137 -0
  219. package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
  220. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  221. package/src/lib/server/session-tools/file.ts +243 -24
  222. package/src/lib/server/session-tools/http.ts +9 -3
  223. package/src/lib/server/session-tools/human-loop.ts +227 -0
  224. package/src/lib/server/session-tools/image-gen.ts +1 -3
  225. package/src/lib/server/session-tools/index.ts +87 -2
  226. package/src/lib/server/session-tools/mailbox.ts +276 -0
  227. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  228. package/src/lib/server/session-tools/memory.ts +35 -3
  229. package/src/lib/server/session-tools/monitor.ts +162 -12
  230. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  231. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  232. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  233. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  234. package/src/lib/server/session-tools/platform.ts +142 -4
  235. package/src/lib/server/session-tools/plugin-creator.ts +95 -25
  236. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  237. package/src/lib/server/session-tools/replicate.ts +1 -3
  238. package/src/lib/server/session-tools/sandbox.ts +51 -92
  239. package/src/lib/server/session-tools/schedule.ts +20 -10
  240. package/src/lib/server/session-tools/session-info.ts +58 -4
  241. package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
  242. package/src/lib/server/session-tools/shell.ts +2 -2
  243. package/src/lib/server/session-tools/subagent.ts +195 -27
  244. package/src/lib/server/session-tools/table.ts +587 -0
  245. package/src/lib/server/session-tools/wallet.ts +13 -10
  246. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  247. package/src/lib/server/session-tools/web.ts +947 -108
  248. package/src/lib/server/storage.ts +255 -10
  249. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  250. package/src/lib/server/stream-agent-chat.ts +185 -25
  251. package/src/lib/server/structured-extract.test.ts +72 -0
  252. package/src/lib/server/structured-extract.ts +373 -0
  253. package/src/lib/server/task-mention.test.ts +16 -2
  254. package/src/lib/server/task-mention.ts +61 -11
  255. package/src/lib/server/tool-aliases.ts +80 -12
  256. package/src/lib/server/tool-capability-policy.ts +7 -1
  257. package/src/lib/server/tool-retry.ts +2 -0
  258. package/src/lib/server/watch-jobs.test.ts +173 -0
  259. package/src/lib/server/watch-jobs.ts +532 -0
  260. package/src/lib/server/ws-hub.ts +5 -3
  261. package/src/lib/setup-defaults.ts +352 -11
  262. package/src/lib/tool-definitions.ts +3 -4
  263. package/src/lib/validation/schemas.test.ts +26 -0
  264. package/src/lib/validation/schemas.ts +62 -1
  265. package/src/lib/ws-client.ts +14 -12
  266. package/src/proxy.ts +5 -5
  267. package/src/stores/use-app-store.ts +43 -7
  268. package/src/stores/use-chat-store.ts +31 -2
  269. package/src/stores/use-chatroom-store.ts +153 -26
  270. package/src/types/index.ts +470 -44
  271. package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
  272. package/src/components/chat/new-chat-sheet.tsx +0 -253
  273. package/src/lib/server/main-session.ts +0 -17
  274. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -4,7 +4,9 @@ import { active, loadSessions } from './storage'
4
4
  import { executeSessionChatTurn, type ExecuteChatTurnResult } from './chat-execution'
5
5
  import { loadRuntimeSettings } from './runtime-settings'
6
6
  import { log } from './logger'
7
- import { handleMainLoopRunResult, type MainLoopFollowupRequest } from './main-agent-loop'
7
+ import { isInternalHeartbeatRun } from './heartbeat-source'
8
+ import { cleanupSessionBrowser } from './session-tools/web'
9
+ import { cancelDelegationJobsForParentSession } from './delegation-jobs'
8
10
 
9
11
  export type SessionRunStatus = 'queued' | 'running' | 'completed' | 'failed' | 'cancelled'
10
12
  export type SessionQueueMode = 'followup' | 'steer' | 'collect'
@@ -122,6 +124,23 @@ function emitRunMeta(entry: QueueEntry, status: SessionRunStatus, extra?: Record
122
124
  })
123
125
  }
124
126
 
127
+ function markRunningEntryCancelled(entry: QueueEntry, reason: string) {
128
+ if (entry.run.status === 'cancelled') return
129
+ entry.run.status = 'cancelled'
130
+ entry.run.endedAt = now()
131
+ entry.run.error = reason
132
+ emitRunMeta(entry, 'cancelled', { reason })
133
+ }
134
+
135
+ function abortSessionRuntime(entry: QueueEntry, reason: string) {
136
+ markRunningEntryCancelled(entry, reason)
137
+ entry.signalController.abort()
138
+ try { active.get(entry.run.sessionId)?.kill?.() } catch { /* noop */ }
139
+ active.delete(entry.run.sessionId)
140
+ try { cleanupSessionBrowser(entry.run.sessionId) } catch { /* noop */ }
141
+ try { cancelDelegationJobsForParentSession(entry.run.sessionId, reason) } catch { /* noop */ }
142
+ }
143
+
125
144
  function executionKeyForSession(sessionId: string): string {
126
145
  return `session:${sessionId}`
127
146
  }
@@ -170,7 +189,7 @@ export function cancelAllHeartbeatRuns(reason = 'Heartbeat disabled globally'):
170
189
  if (!queue.length) continue
171
190
  const keep: QueueEntry[] = []
172
191
  for (const entry of queue) {
173
- const isHeartbeat = entry.run.internal === true && entry.run.source === 'heartbeat'
192
+ const isHeartbeat = isInternalHeartbeatRun(entry.run.internal, entry.run.source)
174
193
  if (!isHeartbeat) {
175
194
  keep.push(entry)
176
195
  continue
@@ -187,45 +206,15 @@ export function cancelAllHeartbeatRuns(reason = 'Heartbeat disabled globally'):
187
206
  }
188
207
 
189
208
  for (const entry of state.runningByExecution.values()) {
190
- const isHeartbeat = entry.run.internal === true && entry.run.source === 'heartbeat'
209
+ const isHeartbeat = isInternalHeartbeatRun(entry.run.internal, entry.run.source)
191
210
  if (!isHeartbeat) continue
192
211
  abortedRunning += 1
193
- entry.signalController.abort()
194
- try { active.get(entry.run.sessionId)?.kill?.() } catch { /* noop */ }
212
+ abortSessionRuntime(entry, reason)
195
213
  }
196
214
 
197
215
  return { cancelledQueued, abortedRunning }
198
216
  }
199
217
 
200
- function scheduleMainLoopFollowup(sessionId: string, followup: MainLoopFollowupRequest) {
201
- const delayMs = Math.max(0, Math.trunc(followup.delayMs || 0))
202
- setTimeout(() => {
203
- try {
204
- const sessions = loadSessions()
205
- const session = sessions[sessionId]
206
- if (!session || !isMainMissionSession(session)) return
207
- enqueueSessionRun({
208
- sessionId,
209
- message: followup.message,
210
- internal: true,
211
- source: 'main-loop-followup',
212
- mode: 'collect',
213
- dedupeKey: followup.dedupeKey,
214
- })
215
- } catch (err: any) {
216
- log.warn('session-run', `Failed to enqueue main-loop followup for ${sessionId}`, err?.message || String(err))
217
- }
218
- }, delayMs)
219
- }
220
-
221
- export function isMainMissionSession(session: Record<string, unknown>): boolean {
222
- const id = typeof session.id === 'string' ? session.id.trim() : ''
223
- const sessionType = typeof session.sessionType === 'string' ? session.sessionType : ''
224
- if (id.startsWith('agent-thread-')) return true
225
- if (sessionType === 'orchestrated') return true
226
- return false
227
- }
228
-
229
218
  async function drainExecution(executionKey: string): Promise<void> {
230
219
  if (state.runningByExecution.has(executionKey)) return
231
220
  const q = queueForExecution(executionKey)
@@ -269,49 +258,25 @@ async function drainExecution(executionKey: string): Promise<void> {
269
258
  })
270
259
 
271
260
  const failed = !!result.error
272
- let followup: MainLoopFollowupRequest | null = null
273
- try {
274
- followup = handleMainLoopRunResult({
275
- sessionId: next.run.sessionId,
276
- message: next.message,
277
- internal: next.run.internal,
278
- source: next.run.source,
279
- resultText: result.text,
280
- error: result.error,
281
- toolEvents: result.toolEvents,
282
- inputTokens: result.inputTokens,
283
- outputTokens: result.outputTokens,
284
- estimatedCost: result.estimatedCost,
285
- })
286
- } catch (mainLoopErr: any) {
287
- log.warn('session-run', `Main-loop update failed for ${next.run.id}`, mainLoopErr?.message || String(mainLoopErr))
288
- }
289
-
290
- next.run.status = failed ? 'failed' : 'completed'
291
- next.run.endedAt = now()
292
- next.run.error = result.error
261
+ const aborted = next.signalController.signal.aborted
262
+ next.run.status = aborted ? 'cancelled' : (failed ? 'failed' : 'completed')
263
+ next.run.endedAt = next.run.endedAt || now()
264
+ next.run.error = aborted ? (next.run.error || 'Cancelled') : result.error
293
265
  next.run.resultPreview = result.text?.slice(0, 280)
294
266
  emitRunMeta(next, next.run.status, {
295
267
  persisted: result.persisted,
296
268
  hasText: !!result.text,
297
- error: result.error || null,
269
+ error: next.run.error || null,
298
270
  })
299
271
  log.info('session-run', `Run finished ${next.run.id}`, {
300
272
  sessionId: next.run.sessionId,
301
273
  status: next.run.status,
302
274
  persisted: result.persisted,
303
275
  hasText: !!result.text,
304
- error: result.error || null,
276
+ error: next.run.error || null,
305
277
  durationMs: (next.run.endedAt || now()) - (next.run.startedAt || now()),
306
278
  })
307
279
  next.resolve(result)
308
- if (!failed && followup) {
309
- scheduleMainLoopFollowup(next.run.sessionId, followup)
310
- log.info('session-run', `Queued main-loop followup after ${next.run.id}`, {
311
- sessionId: next.run.sessionId,
312
- delayMs: followup.delayMs,
313
- })
314
- }
315
280
  } catch (err: any) {
316
281
  const aborted = next.signalController.signal.aborted
317
282
  next.run.status = aborted ? 'cancelled' : 'failed'
@@ -324,19 +289,6 @@ async function drainExecution(executionKey: string): Promise<void> {
324
289
  error: next.run.error,
325
290
  durationMs: (next.run.endedAt || now()) - (next.run.startedAt || now()),
326
291
  })
327
- try {
328
- handleMainLoopRunResult({
329
- sessionId: next.run.sessionId,
330
- message: next.message,
331
- internal: next.run.internal,
332
- source: next.run.source,
333
- resultText: '',
334
- error: next.run.error,
335
- toolEvents: [],
336
- })
337
- } catch {
338
- // Main-loop bookkeeping failures should not affect queue execution.
339
- }
340
292
  next.reject(err instanceof Error ? err : new Error(next.run.error))
341
293
  } finally {
342
294
  if (runtimeTimer) clearTimeout(runtimeTimer)
@@ -528,7 +480,9 @@ export function getSessionRunState(sessionId: string): {
528
480
  const running = state.runningByExecution.get(executionKey)
529
481
  const queued = queueForExecution(executionKey).filter((entry) => entry.run.sessionId === sessionId).length
530
482
  return {
531
- runningRunId: running?.run.sessionId === sessionId ? running.run.id : undefined,
483
+ runningRunId: (running?.run.sessionId === sessionId && running.run.status === 'running')
484
+ ? running.run.id
485
+ : undefined,
532
486
  queueLength: queued,
533
487
  }
534
488
  }
@@ -562,8 +516,7 @@ export function cancelSessionRuns(sessionId: string, reason = 'Cancelled'): { ca
562
516
  let cancelledRunning = false
563
517
  if (running && running.run.sessionId === sessionId) {
564
518
  cancelledRunning = true
565
- running.signalController.abort()
566
- try { active.get(sessionId)?.kill?.() } catch { /* noop */ }
519
+ abortSessionRuntime(running, reason)
567
520
  }
568
521
  const cancelledQueued = cancelPendingForSession(sessionId, reason)
569
522
  return { cancelledQueued, cancelledRunning }
@@ -0,0 +1,128 @@
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
+
6
+ const thisFile = new URL(import.meta.url).pathname
7
+ const toolsDir = path.dirname(thisFile)
8
+ const serverDir = path.resolve(toolsDir, '..')
9
+
10
+ function readToolSource(fileName: string): string {
11
+ return fs.readFileSync(path.join(toolsDir, fileName), 'utf-8')
12
+ }
13
+
14
+ function readServerSource(fileName: string): string {
15
+ return fs.readFileSync(path.join(serverDir, fileName), 'utf-8')
16
+ }
17
+
18
+ describe('browser workflow surface', () => {
19
+ it('advertises the higher-level browser actions in web.ts', () => {
20
+ const src = readToolSource('web.ts')
21
+ for (const action of ['read_page', 'extract_links', 'extract_form_fields', 'extract_table', 'fill_form', 'submit_form', 'scroll_until', 'download_file', 'complete_web_task']) {
22
+ assert.equal(src.includes(`'${action}'`), true, `web.ts should expose ${action}`)
23
+ }
24
+ })
25
+
26
+ it('supports the shorthand form-map path for fill_form', () => {
27
+ const src = readToolSource('web.ts')
28
+ assert.equal(src.includes('params.form'), true)
29
+ assert.equal(src.includes('fields is required for fill_form.'), true)
30
+ })
31
+
32
+ it('flags pages that require human-provided input', () => {
33
+ const src = readToolSource('web.ts')
34
+ assert.equal(src.includes("type: 'human_input_required'"), true)
35
+ assert.equal(src.includes('Ask the human instead of guessing'), true)
36
+ })
37
+ })
38
+
39
+ describe('durable wait surface', () => {
40
+ it('advertises the durable wait actions in monitor.ts', () => {
41
+ const src = readToolSource('monitor.ts')
42
+ for (const action of ['wait_until', 'wait_for_http', 'wait_for_file', 'wait_for_task', 'wait_for_webhook', 'wait_for_page_change']) {
43
+ assert.equal(src.includes(`'${action}'`), true, `monitor.ts should expose ${action}`)
44
+ }
45
+ assert.equal(src.includes('createDurableWatch'), true)
46
+ })
47
+
48
+ it('routes schedule_wake through durable watch storage', () => {
49
+ const src = readToolSource('schedule.ts')
50
+ assert.equal(src.includes('createWatchJob'), true)
51
+ assert.equal(src.includes("type: 'time'"), true)
52
+ })
53
+ })
54
+
55
+ describe('sandbox surface', () => {
56
+ it('advertises a Deno-only sandbox and steers simple APIs to http_request', () => {
57
+ const src = readToolSource('sandbox.ts')
58
+ assert.equal(src.includes("enum: ['javascript', 'typescript']"), true)
59
+ assert.equal(src.includes('http_request'), true)
60
+ assert.equal(src.includes('plugin_creator'), true)
61
+ assert.equal(src.includes('manage_schedules'), true)
62
+ assert.equal(src.includes('openclaw_sandbox'), false)
63
+ })
64
+ })
65
+
66
+ describe('delegation job handles', () => {
67
+ it('exposes subagent control actions', () => {
68
+ const src = readToolSource('subagent.ts')
69
+ for (const action of ['status', 'list', 'wait', 'cancel']) {
70
+ assert.equal(src.includes(`action === '${action}'`), true, `subagent.ts should handle ${action}`)
71
+ }
72
+ assert.equal(src.includes('createDelegationJob'), true)
73
+ })
74
+
75
+ it('builds delegate context from the invoking session and uses job records', () => {
76
+ const src = readToolSource('delegate.ts')
77
+ assert.equal(src.includes('buildDelegateContextFromSessionish'), true)
78
+ assert.equal(src.includes('createDelegationJob'), true)
79
+ assert.equal(src.includes('waitForDelegateJob'), true)
80
+ })
81
+
82
+ it('scheduler and daemon recover the durable autonomy jobs', () => {
83
+ const schedulerSrc = readServerSource('scheduler.ts')
84
+ const daemonSrc = readServerSource('daemon-state.ts')
85
+ assert.equal(schedulerSrc.includes('processDueWatchJobs'), true)
86
+ assert.equal(daemonSrc.includes('recoverStaleDelegationJobs'), true)
87
+ })
88
+ })
89
+
90
+ describe('primitive plugin surfaces', () => {
91
+ it('advertises mailbox and human-loop actions', () => {
92
+ const mailboxSrc = readToolSource('mailbox.ts')
93
+ const humanSrc = readToolSource('human-loop.ts')
94
+ for (const action of ['list_messages', 'list_threads', 'search_messages', 'read_message', 'download_attachment', 'reply', 'wait_for_email']) {
95
+ assert.equal(mailboxSrc.includes(`'${action}'`), true, `mailbox.ts should expose ${action}`)
96
+ }
97
+ for (const action of ['request_input', 'request_approval', 'wait_for_reply', 'wait_for_approval', 'list_mailbox', 'ack_mailbox', 'status']) {
98
+ assert.equal(humanSrc.includes(`'${action}'`), true, `human-loop.ts should expose ${action}`)
99
+ }
100
+ })
101
+
102
+ it('advertises document, extract, table, and crawl actions', () => {
103
+ const documentSrc = readToolSource('document.ts')
104
+ const extractSrc = readToolSource('extract.ts')
105
+ const tableSrc = readToolSource('table.ts')
106
+ const crawlSrc = readToolSource('crawl.ts')
107
+
108
+ for (const action of ['read', 'metadata', 'ocr', 'extract_tables', 'store', 'list', 'search', 'get', 'delete']) {
109
+ assert.equal(documentSrc.includes(`'${action}'`), true, `document.ts should expose ${action}`)
110
+ }
111
+ for (const action of ['extract_structured', 'summarize', 'status']) {
112
+ assert.equal(extractSrc.includes(`'${action}'`), true, `extract.ts should expose ${action}`)
113
+ }
114
+ for (const action of ['read', 'load_csv', 'load_xlsx', 'summarize', 'filter', 'sort', 'group', 'pivot', 'dedupe', 'join', 'write']) {
115
+ assert.equal(tableSrc.includes(`'${action}'`), true, `table.ts should expose ${action}`)
116
+ }
117
+ for (const action of ['crawl_site', 'follow_pagination', 'extract_sitemap', 'dedupe_pages', 'batch_extract']) {
118
+ assert.equal(crawlSrc.includes(`'${action}'`), true, `crawl.ts should expose ${action}`)
119
+ }
120
+ })
121
+
122
+ it('registers the primitive plugins in builtin-plugins.ts', () => {
123
+ const src = readServerSource('builtin-plugins.ts')
124
+ for (const moduleName of ['mailbox', 'human-loop', 'document', 'extract', 'table', 'crawl']) {
125
+ assert.equal(src.includes(`session-tools/${moduleName}`), true, `builtin-plugins.ts should import ${moduleName}`)
126
+ }
127
+ })
128
+ })
@@ -3,7 +3,6 @@ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
3
  import type { Plugin, PluginHooks } from '@/types'
4
4
  import { getPluginManager } from '../plugins'
5
5
  import { normalizeToolInputArgs } from './normalize-tool-args'
6
- import { loadSettings } from '../storage'
7
6
  import type { ToolBuildContext } from './context'
8
7
 
9
8
  type CalendarProvider = 'google' | 'outlook'
@@ -18,8 +17,7 @@ interface CalendarConfig {
18
17
  }
19
18
 
20
19
  function getConfig(): CalendarConfig {
21
- const settings = loadSettings()
22
- const ps = (settings.pluginSettings as Record<string, Record<string, unknown>> | undefined)?.calendar ?? {}
20
+ const ps = getPluginManager().getPluginSettings('calendar')
23
21
  return {
24
22
  provider: (ps.provider as CalendarProvider) || 'google',
25
23
  accessToken: (ps.accessToken as string) || '',
@@ -49,15 +47,7 @@ async function refreshGoogleToken(cfg: CalendarConfig): Promise<string | null> {
49
47
  const data = await res.json()
50
48
  const newToken = data?.access_token as string | undefined
51
49
  if (newToken) {
52
- // Persist the refreshed token
53
- const settings = loadSettings()
54
- const pluginSettings = (settings.pluginSettings as Record<string, Record<string, unknown>> | undefined) ?? {}
55
- const calSettings = pluginSettings.calendar ?? {}
56
- calSettings.accessToken = newToken
57
- pluginSettings.calendar = calSettings
58
- settings.pluginSettings = pluginSettings
59
- const { saveSettings } = await import('../storage')
60
- saveSettings(settings)
50
+ getPluginManager().setPluginSettings('calendar', { ...cfg, accessToken: newToken })
61
51
  }
62
52
  return newToken || null
63
53
  } catch {
@@ -60,8 +60,6 @@ function isAutonomousSystemTurn(userText: string): boolean {
60
60
  if (!userText) return false
61
61
  const text = userText.toUpperCase()
62
62
  return text.includes('AGENT_HEARTBEAT_WAKE')
63
- || text.includes('SWARM_MAIN_MISSION_TICK')
64
- || text.includes('SWARM_MAIN_AUTO_FOLLOWUP')
65
63
  || text.includes('SWARM_HEARTBEAT_CHECK')
66
64
  }
67
65
 
@@ -247,6 +245,9 @@ interface ConnectorActionInput {
247
245
  platform?: string
248
246
  to?: string
249
247
  message?: string
248
+ messageId?: string
249
+ targetMessage?: 'last_inbound' | 'last_outbound'
250
+ emoji?: string
250
251
  voiceText?: string
251
252
  voiceId?: string
252
253
  imageUrl?: string
@@ -255,9 +256,12 @@ interface ConnectorActionInput {
255
256
  mimeType?: string
256
257
  fileName?: string
257
258
  caption?: string
259
+ replyToMessageId?: string
260
+ threadId?: string
258
261
  delaySec?: number
259
262
  followUpMessage?: string
260
263
  followUpDelaySec?: number
264
+ dedupeKey?: string
261
265
  approved?: boolean
262
266
  ptt?: boolean
263
267
  }
@@ -284,13 +288,25 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
284
288
  mimeType,
285
289
  fileName,
286
290
  caption,
291
+ messageId,
292
+ targetMessage,
293
+ emoji,
294
+ replyToMessageId,
295
+ threadId,
296
+ dedupeKey,
287
297
  approved,
288
298
  ptt,
289
299
  } = normalized as ConnectorActionInput
290
300
 
291
301
  try {
292
302
  const actionName = String(action)
293
- const { listRunningConnectors, sendConnectorMessage, getConnectorRecentChannelId } = await import('../connectors/manager')
303
+ const {
304
+ listRunningConnectors,
305
+ sendConnectorMessage,
306
+ getConnectorRecentChannelId,
307
+ scheduleConnectorFollowUp,
308
+ performConnectorMessageAction,
309
+ } = await import('../connectors/manager')
294
310
  const running = listRunningConnectors(platform || undefined)
295
311
 
296
312
  if (actionName === 'list_running' || actionName === 'list_targets') {
@@ -342,6 +358,9 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
342
358
  return { selected, connector }
343
359
  }
344
360
 
361
+ const currentSession = bctx.resolveCurrentSession?.()
362
+ const sessionId = bctx.ctx?.sessionId || currentSession?.id || undefined
363
+
345
364
  if (actionName === 'send' || actionName === 'send_voice_note' || actionName === 'schedule_followup') {
346
365
  const settings = loadSettings()
347
366
  if (settings.safetyRequireApprovalForOutbound === true && approved !== true) {
@@ -363,9 +382,7 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
363
382
  let channelId = target.channelId
364
383
  if (connector.platform === 'whatsapp') channelId = normalizeWhatsAppTarget(channelId)
365
384
 
366
- const currentSession = bctx.resolveCurrentSession?.()
367
385
  const latestUserTurn = parseLatestUserTurn(currentSession)
368
- const sessionId = bctx.ctx?.sessionId || currentSession?.id || 'unknown-session'
369
386
  const turnKey = buildConnectorActionKey([sessionId, latestUserTurn.time || 'no-user-turn'])
370
387
  const multiOutboundAllowed = userExplicitlyWantsMultipleOutbound(latestUserTurn.text)
371
388
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -392,6 +409,9 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
392
409
  const sent = await sendConnectorMessage({
393
410
  connectorId: selected.id, channelId, text: '', mediaPath: voicePath, mimeType: 'audio/mpeg',
394
411
  fileName: fileName?.trim() || 'voicenote.mp3', caption: caption?.trim() || undefined, ptt: ptt ?? true,
412
+ sessionId,
413
+ replyToMessageId: replyToMessageId?.trim() || undefined,
414
+ threadId: threadId?.trim() || undefined,
395
415
  })
396
416
  const result = JSON.stringify({ status: 'voice_sent', connectorId: sent.connectorId, platform: sent.platform, to: sent.channelId, voiceFile: voicePath })
397
417
  connectorTurnSendBudget.set(turnKey, { count: (existingBudget?.count || 0) + 1, at: now, lastResult: result })
@@ -405,11 +425,54 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
405
425
  return 'Error: message or media required.'
406
426
  }
407
427
 
428
+ if (actionName === 'schedule_followup') {
429
+ const followupText = (normalized.followUpMessage as string | undefined)?.trim() || message?.trim() || ''
430
+ if (!followupText && !media.mediaPath && !media.imageUrl && !media.fileUrl) {
431
+ return 'Error: follow-up message or media required.'
432
+ }
433
+ const followupDelay = (() => {
434
+ const direct = Number(normalized.followUpDelaySec)
435
+ if (Number.isFinite(direct) && direct >= 0) return direct
436
+ const fallback = Number(normalized.delaySec)
437
+ if (Number.isFinite(fallback) && fallback >= 0) return fallback
438
+ return 300
439
+ })()
440
+ const scheduled = scheduleConnectorFollowUp({
441
+ connectorId: selected.id,
442
+ channelId,
443
+ text: followupText,
444
+ sessionId,
445
+ delaySec: followupDelay,
446
+ dedupeKey: dedupeKey?.trim() || undefined,
447
+ imageUrl: media.imageUrl,
448
+ fileUrl: media.fileUrl,
449
+ mediaPath: media.mediaPath,
450
+ mimeType: mimeType?.trim() || undefined,
451
+ fileName: fileName?.trim() || undefined,
452
+ caption: caption?.trim() || undefined,
453
+ replyToMessageId: replyToMessageId?.trim() || undefined,
454
+ threadId: threadId?.trim() || undefined,
455
+ ptt: ptt ?? undefined,
456
+ })
457
+ return JSON.stringify({
458
+ status: 'scheduled',
459
+ connectorId: selected.id,
460
+ platform: selected.platform,
461
+ to: channelId,
462
+ followUpId: scheduled.followUpId,
463
+ sendAt: scheduled.sendAt,
464
+ })
465
+ }
466
+
408
467
  const sent = await sendConnectorMessage({
409
468
  connectorId: selected.id, channelId, text: message?.trim() || '',
469
+ sessionId,
410
470
  imageUrl: media.imageUrl, fileUrl: media.fileUrl, mediaPath: media.mediaPath,
411
471
  mimeType: mimeType?.trim() || undefined, fileName: fileName?.trim() || undefined,
412
- caption: caption?.trim() || undefined, ptt: ptt ?? undefined,
472
+ caption: caption?.trim() || undefined,
473
+ replyToMessageId: replyToMessageId?.trim() || undefined,
474
+ threadId: threadId?.trim() || undefined,
475
+ ptt: ptt ?? undefined,
413
476
  })
414
477
 
415
478
  const result = JSON.stringify({ status: 'sent', connectorId: sent.connectorId, platform: sent.platform, to: sent.channelId, messageId: sent.messageId || null })
@@ -417,6 +480,35 @@ async function executeConnectorAction(input: ConnectorActionInput, bctx: Connect
417
480
  return result
418
481
  }
419
482
 
483
+ if (actionName === 'react' || actionName === 'edit' || actionName === 'delete' || actionName === 'pin') {
484
+ const resolved = resolveSelectedConnector()
485
+ if ('error' in resolved) return resolved.error
486
+ const { selected } = resolved
487
+ const target = pickChannelTarget({
488
+ connector: resolved.connector,
489
+ to,
490
+ recentChannelId: getConnectorRecentChannelId(selected.id),
491
+ })
492
+ if (target.error) return target.error
493
+ const result = await performConnectorMessageAction({
494
+ connectorId: selected.id,
495
+ channelId: selected.platform === 'whatsapp' ? normalizeWhatsAppTarget(target.channelId) : target.channelId,
496
+ action: actionName,
497
+ messageId: messageId?.trim() || undefined,
498
+ emoji: emoji?.trim() || undefined,
499
+ text: message?.trim() || undefined,
500
+ sessionId,
501
+ targetMessage,
502
+ })
503
+ return JSON.stringify({
504
+ status: actionName,
505
+ connectorId: result.connectorId,
506
+ platform: result.platform,
507
+ to: result.channelId,
508
+ messageId: result.messageId || null,
509
+ })
510
+ }
511
+
420
512
  return 'Unknown action.'
421
513
  } catch (err: unknown) {
422
514
  return `Error: ${err instanceof Error ? err.message : String(err)}`
@@ -440,11 +532,20 @@ const ConnectorPlugin: Plugin = {
440
532
  parameters: {
441
533
  type: 'object',
442
534
  properties: {
443
- action: { type: 'string', enum: ['list_running', 'start', 'stop', 'send', 'send_voice_note'] },
535
+ action: { type: 'string', enum: ['list_running', 'start', 'stop', 'send', 'send_voice_note', 'schedule_followup', 'react', 'edit', 'delete', 'pin'] },
444
536
  connectorId: { type: 'string' },
445
537
  platform: { type: 'string' },
446
538
  to: { type: 'string' },
447
- message: { type: 'string' }
539
+ message: { type: 'string' },
540
+ messageId: { type: 'string' },
541
+ targetMessage: { type: 'string', enum: ['last_inbound', 'last_outbound'] },
542
+ emoji: { type: 'string' },
543
+ replyToMessageId: { type: 'string' },
544
+ threadId: { type: 'string' },
545
+ delaySec: { type: 'number' },
546
+ followUpMessage: { type: 'string' },
547
+ followUpDelaySec: { type: 'number' },
548
+ dedupeKey: { type: 'string' },
448
549
  },
449
550
  required: ['action']
450
551
  },
@@ -34,9 +34,21 @@ export interface ToolBuildContext {
34
34
  activePlugins: string[]
35
35
  }
36
36
 
37
+ function normalizeWorkspaceAlias(cwd: string, filePath: string): string {
38
+ const trimmed = filePath.trim()
39
+ if (!trimmed) return trimmed
40
+ if (trimmed === '/workspace' || trimmed === 'workspace') return cwd
41
+ if (trimmed.startsWith('/workspace/')) return trimmed.slice('/workspace/'.length)
42
+ if (trimmed.startsWith('workspace/')) return trimmed.slice('workspace/'.length)
43
+ return trimmed
44
+ }
45
+
37
46
  export function safePath(cwd: string, filePath: string): string {
38
- const resolved = require('path').resolve(cwd, filePath)
39
- if (!resolved.startsWith(require('path').resolve(cwd))) {
47
+ const path = require('path')
48
+ const normalized = normalizeWorkspaceAlias(cwd, filePath)
49
+ const resolvedRoot = path.resolve(cwd)
50
+ const resolved = path.resolve(resolvedRoot, normalized)
51
+ if (!resolved.startsWith(resolvedRoot)) {
40
52
  throw new Error('Path traversal not allowed')
41
53
  }
42
54
  return resolved