@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
@@ -16,12 +16,19 @@ import { startHeartbeatService, stopHeartbeatService, getHeartbeatServiceStatus
16
16
  import { hasOpenClawAgents, ensureGatewayConnected, disconnectGateway, getGateway } from './openclaw-gateway'
17
17
  import { enqueueSessionRun } from './session-run-manager'
18
18
  import { WORKSPACE_DIR } from './data-dir'
19
+ import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/heartbeat-defaults'
19
20
  import { genId } from '@/lib/id'
20
21
  import path from 'node:path'
21
22
  import type { WebhookRetryEntry } from '@/types'
22
23
  import { createNotification } from '@/lib/server/create-notification'
23
24
  import { pingProvider, OPENAI_COMPATIBLE_DEFAULTS } from '@/lib/server/provider-health'
24
25
  import { runIntegrityMonitor } from '@/lib/server/integrity-monitor'
26
+ import { recoverStaleDelegationJobs } from './delegation-jobs'
27
+ import {
28
+ listPendingApprovalsNeedingConnectorNotification,
29
+ markApprovalConnectorNotificationAttempt,
30
+ markApprovalConnectorNotificationSent,
31
+ } from './approvals'
25
32
 
26
33
  const QUEUE_CHECK_INTERVAL = 30_000 // 30 seconds
27
34
  const BROWSER_SWEEP_INTERVAL = 60_000 // 60 seconds
@@ -51,7 +58,7 @@ function daemonAutostartEnvEnabled(): boolean {
51
58
  return parseBoolish(process.env.SWARMCLAW_DAEMON_AUTOSTART, true)
52
59
  }
53
60
 
54
- function parseHeartbeatIntervalSec(value: unknown, fallback = 120): number {
61
+ function parseHeartbeatIntervalSec(value: unknown, fallback = DEFAULT_HEARTBEAT_INTERVAL_SEC): number {
55
62
  const parsed = typeof value === 'number'
56
63
  ? value
57
64
  : typeof value === 'string'
@@ -161,6 +168,7 @@ export function startDaemon(options?: { source?: string; manualStart?: boolean }
161
168
  try {
162
169
  validateCompletedTasksQueue()
163
170
  cleanupFinishedTaskSessions()
171
+ recoverStaleDelegationJobs()
164
172
  startScheduler()
165
173
  startQueueProcessor()
166
174
  startBrowserSweep()
@@ -417,7 +425,7 @@ async function processWebhookRetries() {
417
425
  messages: [],
418
426
  createdAt: ts,
419
427
  lastActiveAt: ts,
420
- sessionType: 'orchestrated',
428
+ sessionType: 'human',
421
429
  agentId: agent.id,
422
430
  parentSessionId: null,
423
431
  plugins: agent.plugins || agent.tools || [],
@@ -664,6 +672,51 @@ async function runOpenClawGatewayHealthChecks() {
664
672
  }
665
673
  }
666
674
 
675
+ async function runPendingApprovalConnectorNotifications(now: number) {
676
+ const running = listRunningConnectors()
677
+ const pending = listPendingApprovalsNeedingConnectorNotification({
678
+ now,
679
+ runningConnectors: running,
680
+ })
681
+ if (!pending.length) return
682
+
683
+ for (const reminder of pending) {
684
+ try {
685
+ const result = await sendConnectorMessage({
686
+ connectorId: reminder.connectorId,
687
+ channelId: reminder.channelId,
688
+ text: reminder.text,
689
+ threadId: reminder.threadId || undefined,
690
+ })
691
+ markApprovalConnectorNotificationSent(reminder.approvalId, {
692
+ at: now,
693
+ connectorId: result.connectorId,
694
+ channelId: result.channelId,
695
+ threadId: reminder.threadId || null,
696
+ messageId: result.messageId || null,
697
+ })
698
+ createNotification({
699
+ type: 'info',
700
+ title: 'Approval reminder sent',
701
+ message: 'A pending approval reminder was delivered over an active connector.',
702
+ dedupKey: `approval-connector-reminder:${reminder.approvalId}`,
703
+ entityType: 'approval',
704
+ entityId: reminder.approvalId,
705
+ })
706
+ } catch (err: unknown) {
707
+ const errorMsg = err instanceof Error ? err.message : String(err)
708
+ markApprovalConnectorNotificationAttempt(reminder.approvalId, {
709
+ at: now,
710
+ connectorId: reminder.connectorId,
711
+ channelId: reminder.channelId,
712
+ threadId: reminder.threadId || null,
713
+ lastError: errorMsg,
714
+ })
715
+ console.warn(`[daemon] Approval connector reminder failed for ${reminder.approvalId}: ${errorMsg}`)
716
+ }
717
+ }
718
+ }
719
+
667
720
  async function runHealthChecks() {
668
721
  // Continuously keep the completed queue honest.
669
722
  validateCompletedTasksQueue()
@@ -683,7 +736,7 @@ async function runHealthChecks() {
683
736
 
684
737
  const sessionId = session.id
685
738
  const sessionLabel = String(session.name || sessionId)
686
- const intervalSec = parseHeartbeatIntervalSec(session.heartbeatIntervalSec, 120)
739
+ const intervalSec = parseHeartbeatIntervalSec(session.heartbeatIntervalSec, DEFAULT_HEARTBEAT_INTERVAL_SEC)
687
740
  if (intervalSec <= 0) continue
688
741
  const staleAfter = Math.max(intervalSec * STALE_MULTIPLIER * 1000, STALE_MIN_MS)
689
742
  const lastActive = typeof session.lastActiveAt === 'number' ? session.lastActiveAt : 0
@@ -739,6 +792,12 @@ async function runHealthChecks() {
739
792
  console.error('[daemon] OpenClaw gateway health check failed:', err instanceof Error ? err.message : String(err))
740
793
  }
741
794
 
795
+ try {
796
+ await runPendingApprovalConnectorNotifications(now)
797
+ } catch (err: unknown) {
798
+ console.error('[daemon] Approval connector reminder check failed:', err instanceof Error ? err.message : String(err))
799
+ }
800
+
742
801
  // Integrity drift monitoring for identity/config/plugin files.
743
802
  try {
744
803
  const integrity = runIntegrityMonitor(loadSettings())
@@ -19,3 +19,16 @@ function resolveWorkspaceDir(): string {
19
19
  }
20
20
 
21
21
  export const WORKSPACE_DIR = resolveWorkspaceDir()
22
+
23
+ function resolveBrowserProfilesDir(): string {
24
+ if (process.env.BROWSER_PROFILES_DIR) return process.env.BROWSER_PROFILES_DIR
25
+ const external = path.join(os.homedir(), '.swarmclaw', 'browser-profiles')
26
+ try {
27
+ fs.mkdirSync(external, { recursive: true })
28
+ return external
29
+ } catch {
30
+ return path.join(DATA_DIR, 'browser-profiles')
31
+ }
32
+ }
33
+
34
+ export const BROWSER_PROFILES_DIR = resolveBrowserProfilesDir()
@@ -0,0 +1,140 @@
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 { after, before, describe, it } from 'node:test'
6
+
7
+ const originalEnv = {
8
+ DATA_DIR: process.env.DATA_DIR,
9
+ WORKSPACE_DIR: process.env.WORKSPACE_DIR,
10
+ SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
11
+ }
12
+
13
+ let tempDir = ''
14
+ let delegationJobs: typeof import('./delegation-jobs')
15
+
16
+ before(async () => {
17
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-delegation-jobs-'))
18
+ process.env.DATA_DIR = path.join(tempDir, 'data')
19
+ process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
20
+ process.env.SWARMCLAW_BUILD_MODE = '1'
21
+ delegationJobs = await import('./delegation-jobs')
22
+ })
23
+
24
+ after(() => {
25
+ if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
26
+ else process.env.DATA_DIR = originalEnv.DATA_DIR
27
+ if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
28
+ else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
29
+ if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
30
+ else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
31
+ fs.rmSync(tempDir, { recursive: true, force: true })
32
+ })
33
+
34
+ describe('delegation-jobs', () => {
35
+ it('tracks a queued job through running and completion', () => {
36
+ const job = delegationJobs.createDelegationJob({
37
+ kind: 'delegate',
38
+ parentSessionId: 'session-1',
39
+ backend: 'codex',
40
+ task: 'Refactor the module',
41
+ cwd: process.cwd(),
42
+ })
43
+
44
+ const started = delegationJobs.startDelegationJob(job.id, { backend: 'codex' })
45
+ const completed = delegationJobs.completeDelegationJob(job.id, 'done')
46
+
47
+ assert.equal(started?.status, 'running')
48
+ assert.equal(completed?.status, 'completed')
49
+ assert.equal(completed?.resultPreview, 'done')
50
+ assert.equal(delegationJobs.listDelegationJobs({ parentSessionId: 'session-1' }).length >= 1, true)
51
+ })
52
+
53
+ it('keeps cancellation terminal even if late completions arrive', () => {
54
+ const job = delegationJobs.createDelegationJob({
55
+ kind: 'subagent',
56
+ parentSessionId: 'session-2',
57
+ agentId: 'agent-1',
58
+ task: 'Do the background work',
59
+ cwd: process.cwd(),
60
+ })
61
+
62
+ let cancelled = false
63
+ delegationJobs.startDelegationJob(job.id, { agentId: 'agent-1' })
64
+ delegationJobs.registerDelegationRuntime(job.id, {
65
+ cancel: () => {
66
+ cancelled = true
67
+ },
68
+ })
69
+
70
+ const stopped = delegationJobs.cancelDelegationJob(job.id)
71
+ const afterComplete = delegationJobs.completeDelegationJob(job.id, 'late success')
72
+ const afterFail = delegationJobs.failDelegationJob(job.id, 'late failure')
73
+
74
+ assert.equal(cancelled, true)
75
+ assert.equal(stopped?.status, 'cancelled')
76
+ assert.equal(afterComplete?.status, 'cancelled')
77
+ assert.equal(afterFail?.status, 'cancelled')
78
+ })
79
+
80
+ it('recovers stale running jobs as failed', () => {
81
+ const stale = delegationJobs.createDelegationJob({
82
+ kind: 'delegate',
83
+ parentSessionId: 'session-3',
84
+ backend: 'claude',
85
+ task: 'Stale job',
86
+ cwd: process.cwd(),
87
+ })
88
+
89
+ delegationJobs.startDelegationJob(stale.id)
90
+ delegationJobs.updateDelegationJob(stale.id, {
91
+ startedAt: Date.now() - 60_000,
92
+ })
93
+
94
+ const recovered = delegationJobs.recoverStaleDelegationJobs(-1)
95
+ const latest = delegationJobs.getDelegationJob(stale.id)
96
+
97
+ assert.equal(recovered >= 1, true)
98
+ assert.equal(latest?.status, 'failed')
99
+ assert.match(String(latest?.error || ''), /interrupted/i)
100
+ })
101
+
102
+ it('cancels all running jobs for a parent session', () => {
103
+ const jobA = delegationJobs.createDelegationJob({
104
+ kind: 'delegate',
105
+ parentSessionId: 'session-bulk',
106
+ backend: 'codex',
107
+ task: 'Task A',
108
+ cwd: process.cwd(),
109
+ })
110
+ const jobB = delegationJobs.createDelegationJob({
111
+ kind: 'subagent',
112
+ parentSessionId: 'session-bulk',
113
+ agentId: 'agent-2',
114
+ task: 'Task B',
115
+ cwd: process.cwd(),
116
+ })
117
+ const untouched = delegationJobs.createDelegationJob({
118
+ kind: 'delegate',
119
+ parentSessionId: 'other-session',
120
+ backend: 'claude',
121
+ task: 'Task C',
122
+ cwd: process.cwd(),
123
+ })
124
+
125
+ delegationJobs.startDelegationJob(jobA.id)
126
+ delegationJobs.startDelegationJob(jobB.id)
127
+ delegationJobs.startDelegationJob(untouched.id)
128
+
129
+ const cancelled = delegationJobs.cancelDelegationJobsForParentSession('session-bulk', 'Stopped by user')
130
+
131
+ assert.equal(cancelled, 2)
132
+ assert.equal(delegationJobs.getDelegationJob(jobA.id)?.status, 'cancelled')
133
+ assert.equal(delegationJobs.getDelegationJob(jobB.id)?.status, 'cancelled')
134
+ assert.equal(delegationJobs.getDelegationJob(untouched.id)?.status, 'running')
135
+ assert.equal(
136
+ delegationJobs.getDelegationJob(jobA.id)?.checkpoints?.some((entry) => entry.note === 'Stopped by user'),
137
+ true,
138
+ )
139
+ })
140
+ })
@@ -0,0 +1,248 @@
1
+ import { genId } from '@/lib/id'
2
+ import type { DelegationJobArtifact, DelegationJobCheckpoint, DelegationJobRecord, DelegationJobStatus } from '@/types'
3
+ import { loadDelegationJobs, upsertDelegationJob } from './storage'
4
+ import { notify } from './ws-hub'
5
+
6
+ interface DelegationRuntimeHandle {
7
+ cancel?: () => void
8
+ }
9
+
10
+ const runtimeKey = '__swarmclaw_delegation_job_runtime__' as const
11
+ const runtimeScope = globalThis as typeof globalThis & {
12
+ [runtimeKey]?: Map<string, DelegationRuntimeHandle>
13
+ }
14
+ const runtimeHandles = runtimeScope[runtimeKey] ?? (runtimeScope[runtimeKey] = new Map())
15
+
16
+ function now() {
17
+ return Date.now()
18
+ }
19
+
20
+ function isTerminalStatus(status: DelegationJobStatus | null | undefined): boolean {
21
+ return status === 'completed' || status === 'failed' || status === 'cancelled'
22
+ }
23
+
24
+ function notifyDelegationJobsChanged() {
25
+ notify('delegation_jobs')
26
+ }
27
+
28
+ export interface CreateDelegationJobInput {
29
+ kind: DelegationJobRecord['kind']
30
+ task: string
31
+ backend?: DelegationJobRecord['backend']
32
+ parentSessionId?: string | null
33
+ childSessionId?: string | null
34
+ agentId?: string | null
35
+ agentName?: string | null
36
+ cwd?: string | null
37
+ }
38
+
39
+ export function createDelegationJob(input: CreateDelegationJobInput): DelegationJobRecord {
40
+ const createdAt = now()
41
+ const job: DelegationJobRecord = {
42
+ id: genId(10),
43
+ kind: input.kind,
44
+ status: 'queued',
45
+ backend: input.backend ?? null,
46
+ parentSessionId: input.parentSessionId ?? null,
47
+ childSessionId: input.childSessionId ?? null,
48
+ agentId: input.agentId ?? null,
49
+ agentName: input.agentName ?? null,
50
+ cwd: input.cwd ?? null,
51
+ task: input.task,
52
+ result: null,
53
+ resultPreview: null,
54
+ error: null,
55
+ checkpoints: [{
56
+ at: createdAt,
57
+ note: 'Job queued',
58
+ status: 'queued',
59
+ }],
60
+ artifacts: [],
61
+ resumeId: null,
62
+ resumeIds: {},
63
+ createdAt,
64
+ updatedAt: createdAt,
65
+ startedAt: null,
66
+ completedAt: null,
67
+ }
68
+ upsertDelegationJob(job.id, job)
69
+ notifyDelegationJobsChanged()
70
+ return job
71
+ }
72
+
73
+ export function getDelegationJob(id: string): DelegationJobRecord | null {
74
+ const all = loadDelegationJobs()
75
+ const current = all[id]
76
+ if (!current || typeof current !== 'object') return null
77
+ return current as DelegationJobRecord
78
+ }
79
+
80
+ export function listDelegationJobs(filter?: {
81
+ parentSessionId?: string | null
82
+ status?: DelegationJobStatus | null
83
+ }): DelegationJobRecord[] {
84
+ return Object.values(loadDelegationJobs())
85
+ .filter((job): job is DelegationJobRecord => !!job && typeof job === 'object')
86
+ .filter((job) => !filter?.parentSessionId || job.parentSessionId === filter.parentSessionId)
87
+ .filter((job) => !filter?.status || job.status === filter.status)
88
+ .sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
89
+ }
90
+
91
+ export function updateDelegationJob(
92
+ id: string,
93
+ patch: Partial<DelegationJobRecord>,
94
+ ): DelegationJobRecord | null {
95
+ const current = getDelegationJob(id)
96
+ if (!current) return null
97
+ const next: DelegationJobRecord = {
98
+ ...current,
99
+ ...patch,
100
+ updatedAt: now(),
101
+ }
102
+ upsertDelegationJob(id, next)
103
+ notifyDelegationJobsChanged()
104
+ return next
105
+ }
106
+
107
+ export function appendDelegationCheckpoint(
108
+ id: string,
109
+ note: string,
110
+ status?: DelegationJobStatus,
111
+ ): DelegationJobRecord | null {
112
+ const current = getDelegationJob(id)
113
+ if (!current) return null
114
+ if (isTerminalStatus(current.status) && status && status !== current.status) {
115
+ return current
116
+ }
117
+ const checkpoints = [...(current.checkpoints || []), { at: now(), note, status }]
118
+ return updateDelegationJob(id, {
119
+ status: isTerminalStatus(current.status) ? current.status : (status ?? current.status),
120
+ checkpoints: checkpoints.slice(-24),
121
+ })
122
+ }
123
+
124
+ export function startDelegationJob(id: string, patch?: Partial<DelegationJobRecord>): DelegationJobRecord | null {
125
+ const current = getDelegationJob(id)
126
+ if (!current) return null
127
+ if (isTerminalStatus(current.status)) return current
128
+ return updateDelegationJob(id, {
129
+ ...patch,
130
+ status: 'running',
131
+ startedAt: now(),
132
+ })
133
+ }
134
+
135
+ export function completeDelegationJob(
136
+ id: string,
137
+ result: string,
138
+ patch?: Partial<DelegationJobRecord>,
139
+ ): DelegationJobRecord | null {
140
+ runtimeHandles.delete(id)
141
+ const current = getDelegationJob(id)
142
+ if (!current) return null
143
+ if (isTerminalStatus(current.status)) return current
144
+ return updateDelegationJob(id, {
145
+ ...patch,
146
+ status: 'completed',
147
+ result,
148
+ resultPreview: result.slice(0, 1000),
149
+ error: null,
150
+ completedAt: now(),
151
+ })
152
+ }
153
+
154
+ export function failDelegationJob(id: string, error: string, patch?: Partial<DelegationJobRecord>): DelegationJobRecord | null {
155
+ runtimeHandles.delete(id)
156
+ const current = getDelegationJob(id)
157
+ if (!current) return null
158
+ if (isTerminalStatus(current.status)) return current
159
+ return updateDelegationJob(id, {
160
+ ...patch,
161
+ status: 'failed',
162
+ error,
163
+ completedAt: now(),
164
+ })
165
+ }
166
+
167
+ export function cancelDelegationJob(id: string): DelegationJobRecord | null {
168
+ const current = getDelegationJob(id)
169
+ if (!current) return null
170
+ if (isTerminalStatus(current.status)) return current
171
+ const runtime = runtimeHandles.get(id)
172
+ try {
173
+ runtime?.cancel?.()
174
+ } catch {
175
+ // best-effort cancel
176
+ }
177
+ runtimeHandles.delete(id)
178
+ const checkpoint: DelegationJobCheckpoint = {
179
+ at: now(),
180
+ note: 'Job cancelled',
181
+ status: 'cancelled',
182
+ }
183
+ return updateDelegationJob(id, {
184
+ status: 'cancelled',
185
+ completedAt: now(),
186
+ error: null,
187
+ checkpoints: [
188
+ ...(current.checkpoints || []),
189
+ checkpoint,
190
+ ].slice(-24),
191
+ })
192
+ }
193
+
194
+ export function cancelDelegationJobsForParentSession(
195
+ parentSessionId: string,
196
+ note = 'Parent session cancelled',
197
+ ): number {
198
+ if (!parentSessionId) return 0
199
+ const jobs = listDelegationJobs({ parentSessionId })
200
+ .filter((job) => job.status === 'queued' || job.status === 'running')
201
+ let cancelled = 0
202
+ for (const job of jobs) {
203
+ const next = cancelDelegationJob(job.id)
204
+ if (!next || next.status !== 'cancelled') continue
205
+ cancelled += 1
206
+ const checkpoints = Array.isArray(next.checkpoints) ? next.checkpoints : []
207
+ const last = checkpoints[checkpoints.length - 1]
208
+ if (!last || last.note !== note) {
209
+ const checkpoint: DelegationJobCheckpoint = {
210
+ at: now(),
211
+ note,
212
+ status: 'cancelled',
213
+ }
214
+ updateDelegationJob(job.id, {
215
+ checkpoints: [
216
+ ...checkpoints,
217
+ checkpoint,
218
+ ].slice(-24),
219
+ })
220
+ }
221
+ }
222
+ return cancelled
223
+ }
224
+
225
+ export function registerDelegationRuntime(id: string, handle: DelegationRuntimeHandle) {
226
+ runtimeHandles.set(id, handle)
227
+ }
228
+
229
+ export function appendDelegationArtifacts(id: string, artifacts: DelegationJobArtifact[]): DelegationJobRecord | null {
230
+ const current = getDelegationJob(id)
231
+ if (!current) return null
232
+ return updateDelegationJob(id, {
233
+ artifacts: [...(current.artifacts || []), ...artifacts].slice(-24),
234
+ })
235
+ }
236
+
237
+ export function recoverStaleDelegationJobs(maxAgeMs = 15 * 60_000): number {
238
+ const threshold = now() - maxAgeMs
239
+ const stale = listDelegationJobs().filter((job) =>
240
+ (job.status === 'queued' || job.status === 'running')
241
+ && !runtimeHandles.has(job.id)
242
+ && (job.updatedAt || job.createdAt) < threshold,
243
+ )
244
+ for (const job of stale) {
245
+ failDelegationJob(job.id, 'Delegation job was interrupted before completion.')
246
+ }
247
+ return stale.length
248
+ }
@@ -0,0 +1,47 @@
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 { describe, it } from 'node:test'
6
+ import { extractDocumentArtifact, loadTabularFile, normalizeInlineRows, writeStructuredTable } from './document-utils'
7
+
8
+ describe('document-utils', () => {
9
+ it('extracts structured tables from JSON arrays', async () => {
10
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-doc-utils-'))
11
+ try {
12
+ const jsonPath = path.join(tempDir, 'people.json')
13
+ fs.writeFileSync(jsonPath, JSON.stringify([
14
+ { name: 'Ada', score: 10 },
15
+ { name: 'Grace', score: 9 },
16
+ ], null, 2))
17
+
18
+ const artifact = await extractDocumentArtifact(jsonPath)
19
+ assert.equal(artifact.method, 'json')
20
+ assert.equal(artifact.tables.length, 1)
21
+ assert.deepEqual(artifact.tables[0].headers, ['name', 'score'])
22
+ assert.equal(artifact.tables[0].rowCount, 2)
23
+ } finally {
24
+ fs.rmSync(tempDir, { recursive: true, force: true })
25
+ }
26
+ })
27
+
28
+ it('normalizes inline rows and round-trips CSV output', async () => {
29
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-doc-utils-'))
30
+ try {
31
+ const table = normalizeInlineRows([
32
+ { city: 'London', population: 9 },
33
+ { city: 'Paris', population: 2 },
34
+ ])
35
+ const csvPath = path.join(tempDir, 'cities.csv')
36
+ const written = await writeStructuredTable(csvPath, table)
37
+ const loaded = await loadTabularFile(csvPath)
38
+
39
+ assert.equal(written.format, 'csv')
40
+ assert.deepEqual(loaded.headers, ['city', 'population'])
41
+ assert.equal(loaded.rowCount, 2)
42
+ assert.equal(String(loaded.rows[0].city), 'London')
43
+ } finally {
44
+ fs.rmSync(tempDir, { recursive: true, force: true })
45
+ }
46
+ })
47
+ })