@swarmclawai/swarmclaw 1.2.4 → 1.2.6

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 (262) hide show
  1. package/README.md +14 -0
  2. package/bin/daemon-cmd.js +169 -0
  3. package/bin/server-cmd.js +3 -0
  4. package/bin/swarmclaw.js +11 -0
  5. package/package.json +17 -16
  6. package/src/app/api/agents/[id]/clone/route.ts +3 -32
  7. package/src/app/api/agents/[id]/route.ts +6 -158
  8. package/src/app/api/agents/[id]/status/route.ts +2 -3
  9. package/src/app/api/agents/[id]/thread/route.ts +4 -17
  10. package/src/app/api/agents/bulk/route.ts +5 -47
  11. package/src/app/api/agents/route.ts +5 -119
  12. package/src/app/api/agents/trash/route.ts +13 -24
  13. package/src/app/api/auth/route.ts +3 -9
  14. package/src/app/api/autonomy/estop/route.ts +5 -5
  15. package/src/app/api/chatrooms/[id]/chat/route.ts +11 -5
  16. package/src/app/api/chatrooms/[id]/route.ts +23 -2
  17. package/src/app/api/chatrooms/route.ts +13 -2
  18. package/src/app/api/chats/[id]/clear/route.ts +2 -13
  19. package/src/app/api/chats/[id]/deploy/route.ts +2 -3
  20. package/src/app/api/chats/[id]/edit-resend/route.ts +7 -13
  21. package/src/app/api/chats/[id]/mailbox/route.ts +6 -8
  22. package/src/app/api/chats/[id]/queue/route.ts +17 -64
  23. package/src/app/api/chats/[id]/retry/route.ts +4 -22
  24. package/src/app/api/chats/[id]/route.ts +10 -138
  25. package/src/app/api/chats/heartbeat/route.ts +2 -1
  26. package/src/app/api/chats/migrate-messages/route.ts +7 -0
  27. package/src/app/api/chats/route.ts +13 -134
  28. package/src/app/api/connectors/[id]/access/route.ts +12 -229
  29. package/src/app/api/connectors/[id]/doctor/route.ts +1 -1
  30. package/src/app/api/connectors/[id]/health/route.ts +12 -39
  31. package/src/app/api/connectors/[id]/route.ts +14 -122
  32. package/src/app/api/connectors/[id]/webhook/route.ts +1 -1
  33. package/src/app/api/connectors/doctor/route.ts +1 -1
  34. package/src/app/api/connectors/route.ts +12 -70
  35. package/src/app/api/credentials/[id]/route.ts +2 -4
  36. package/src/app/api/credentials/route.ts +10 -19
  37. package/src/app/api/daemon/health-check/route.ts +3 -4
  38. package/src/app/api/daemon/route.ts +10 -8
  39. package/src/app/api/documents/route.ts +11 -10
  40. package/src/app/api/external-agents/route.ts +3 -3
  41. package/src/app/api/gateways/[id]/health/route.ts +2 -3
  42. package/src/app/api/gateways/[id]/route.ts +7 -122
  43. package/src/app/api/gateways/route.ts +3 -103
  44. package/src/app/api/mcp-servers/[id]/tools/route.ts +5 -5
  45. package/src/app/api/openclaw/dashboard-url/route.ts +8 -16
  46. package/src/app/api/openclaw/directory/route.ts +2 -2
  47. package/src/app/api/openclaw/history/route.ts +3 -5
  48. package/src/app/api/providers/[id]/route.test.ts +49 -0
  49. package/src/app/api/providers/ollama/route.ts +6 -5
  50. package/src/app/api/schedules/[id]/route.ts +14 -108
  51. package/src/app/api/schedules/[id]/run/route.ts +6 -67
  52. package/src/app/api/schedules/route.ts +9 -51
  53. package/src/app/api/settings/route.ts +4 -3
  54. package/src/app/api/setup/check-provider/route.ts +23 -1
  55. package/src/app/api/setup/openclaw-device/route.ts +2 -2
  56. package/src/app/api/system/status/route.ts +2 -2
  57. package/src/app/api/tasks/[id]/route.ts +16 -202
  58. package/src/app/api/tasks/bulk/route.ts +5 -86
  59. package/src/app/api/tasks/metrics/route.ts +2 -1
  60. package/src/app/api/tasks/route.ts +11 -171
  61. package/src/app/api/upload/route.ts +1 -1
  62. package/src/app/api/uploads/[filename]/route.ts +1 -1
  63. package/src/app/api/uploads/route.ts +1 -1
  64. package/src/app/api/webhooks/[id]/history/route.ts +2 -2
  65. package/src/app/layout.tsx +9 -6
  66. package/src/app/protocols/page.tsx +71 -89
  67. package/src/app/tasks/page.tsx +32 -32
  68. package/src/cli/index.js +1 -0
  69. package/src/cli/spec.js +1 -0
  70. package/src/components/agents/agent-sheet.tsx +5 -5
  71. package/src/components/auth/setup-wizard/index.tsx +4 -4
  72. package/src/components/auth/setup-wizard/step-agents.tsx +1 -1
  73. package/src/components/auth/setup-wizard/step-connect.tsx +1 -1
  74. package/src/components/auth/setup-wizard/utils.ts +1 -1
  75. package/src/components/chatrooms/chatroom-sheet.tsx +16 -276
  76. package/src/components/connectors/connector-list.tsx +26 -40
  77. package/src/components/connectors/connector-sheet.tsx +95 -149
  78. package/src/components/gateways/gateway-sheet.tsx +61 -110
  79. package/src/components/layout/live-query-sync.tsx +121 -0
  80. package/src/components/protocols/structured-session-launcher.tsx +24 -45
  81. package/src/components/providers/app-query-provider.tsx +17 -0
  82. package/src/components/providers/provider-list.tsx +60 -61
  83. package/src/components/providers/provider-sheet.tsx +74 -56
  84. package/src/components/skills/skill-list.tsx +5 -18
  85. package/src/components/skills/skill-sheet.tsx +21 -20
  86. package/src/components/skills/skills-workspace.tsx +48 -87
  87. package/src/components/tasks/task-card.tsx +20 -13
  88. package/src/components/tasks/task-column.tsx +22 -7
  89. package/src/components/tasks/task-list.tsx +8 -11
  90. package/src/components/tasks/task-sheet.tsx +111 -103
  91. package/src/features/agents/queries.ts +20 -0
  92. package/src/features/chatrooms/queries.ts +20 -0
  93. package/src/features/chats/queries.ts +27 -0
  94. package/src/features/connectors/queries.ts +145 -0
  95. package/src/features/credentials/queries.ts +37 -0
  96. package/src/features/extensions/queries.ts +26 -0
  97. package/src/features/external-agents/queries.ts +36 -0
  98. package/src/features/gateways/queries.ts +274 -0
  99. package/src/features/missions/queries.ts +23 -0
  100. package/src/features/projects/queries.ts +20 -0
  101. package/src/features/protocols/queries.ts +149 -0
  102. package/src/features/providers/queries.ts +142 -0
  103. package/src/features/settings/queries.ts +20 -0
  104. package/src/features/skills/queries.ts +182 -0
  105. package/src/features/tasks/queries.ts +189 -0
  106. package/src/hooks/use-ws.ts +3 -2
  107. package/src/lib/app/api-client.ts +2 -2
  108. package/src/lib/providers/index.test.ts +108 -0
  109. package/src/lib/providers/index.ts +38 -15
  110. package/src/lib/query/client.ts +17 -0
  111. package/src/lib/server/agents/agent-runtime-config.ts +1 -1
  112. package/src/lib/server/agents/agent-service.ts +429 -0
  113. package/src/lib/server/agents/agent-thread-session.ts +6 -5
  114. package/src/lib/server/agents/autonomy-contract.ts +1 -4
  115. package/src/lib/server/agents/delegation-advisory.test.ts +206 -0
  116. package/src/lib/server/agents/delegation-advisory.ts +251 -0
  117. package/src/lib/server/agents/main-agent-loop.ts +98 -40
  118. package/src/lib/server/agents/subagent-runtime.ts +12 -0
  119. package/src/lib/server/autonomy/supervisor-reflection.test.ts +20 -1
  120. package/src/lib/server/autonomy/supervisor-reflection.ts +39 -19
  121. package/src/lib/server/build-llm.ts +7 -15
  122. package/src/lib/server/capability-router.test.ts +70 -1
  123. package/src/lib/server/capability-router.ts +24 -99
  124. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -15
  125. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -4
  126. package/src/lib/server/chat-execution/chat-turn-finalization.ts +77 -12
  127. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +4 -4
  128. package/src/lib/server/chat-execution/chat-turn-preflight.ts +2 -2
  129. package/src/lib/server/chat-execution/chat-turn-preparation.ts +41 -17
  130. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -2
  131. package/src/lib/server/chat-execution/chat-turn-tool-routing.test.ts +45 -0
  132. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +48 -17
  133. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -1
  134. package/src/lib/server/chat-execution/direct-memory-intent.test.ts +9 -0
  135. package/src/lib/server/chat-execution/direct-memory-intent.ts +12 -2
  136. package/src/lib/server/chat-execution/message-classifier.test.ts +35 -23
  137. package/src/lib/server/chat-execution/message-classifier.ts +74 -32
  138. package/src/lib/server/chat-execution/prompt-builder.test.ts +29 -0
  139. package/src/lib/server/chat-execution/prompt-builder.ts +37 -2
  140. package/src/lib/server/chat-execution/prompt-sections.test.ts +56 -0
  141. package/src/lib/server/chat-execution/prompt-sections.ts +193 -0
  142. package/src/lib/server/chat-execution/stream-agent-chat.ts +63 -7
  143. package/src/lib/server/chat-execution/stream-continuation.test.ts +36 -0
  144. package/src/lib/server/chat-execution/stream-continuation.ts +28 -13
  145. package/src/lib/server/chatrooms/chatroom-agent-signals.ts +26 -18
  146. package/src/lib/server/chatrooms/chatroom-helpers.ts +19 -18
  147. package/src/lib/server/chatrooms/chatroom-repository.ts +16 -0
  148. package/src/lib/server/chatrooms/chatroom-routing.test.ts +96 -0
  149. package/src/lib/server/chatrooms/chatroom-routing.ts +207 -53
  150. package/src/lib/server/chatrooms/mailbox-utils.ts +4 -2
  151. package/src/lib/server/chatrooms/session-mailbox.ts +50 -40
  152. package/src/lib/server/chats/chat-session-service.ts +410 -0
  153. package/src/lib/server/connectors/access.ts +1 -1
  154. package/src/lib/server/connectors/commands.ts +7 -6
  155. package/src/lib/server/connectors/connector-inbound.ts +14 -7
  156. package/src/lib/server/connectors/connector-outbound.ts +16 -11
  157. package/src/lib/server/connectors/connector-service.ts +453 -0
  158. package/src/lib/server/connectors/delivery.ts +17 -12
  159. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -14
  160. package/src/lib/server/connectors/media.ts +1 -1
  161. package/src/lib/server/connectors/response-media.ts +1 -1
  162. package/src/lib/server/connectors/session-consolidation.ts +11 -7
  163. package/src/lib/server/connectors/session.ts +9 -7
  164. package/src/lib/server/connectors/voice-note.ts +2 -1
  165. package/src/lib/server/context-manager.ts +20 -1
  166. package/src/lib/server/cost.ts +2 -3
  167. package/src/lib/server/credentials/credential-repository.ts +43 -4
  168. package/src/lib/server/credentials/credential-service.ts +112 -0
  169. package/src/lib/server/daemon/admin-metadata.ts +64 -0
  170. package/src/lib/server/daemon/controller.ts +577 -0
  171. package/src/lib/server/daemon/daemon-runtime.ts +352 -0
  172. package/src/lib/server/daemon/daemon-status-repository.ts +63 -0
  173. package/src/lib/server/daemon/types.ts +101 -0
  174. package/src/lib/server/embeddings.ts +3 -9
  175. package/src/lib/server/eval/agent-regression.ts +3 -2
  176. package/src/lib/server/eval/runner.ts +2 -2
  177. package/src/lib/server/execution-brief.test.ts +167 -0
  178. package/src/lib/server/execution-brief.ts +295 -0
  179. package/src/lib/server/execution-engine/chat-turn.ts +9 -0
  180. package/src/lib/server/execution-engine/import-boundary.test.ts +44 -0
  181. package/src/lib/server/execution-engine/index.ts +35 -0
  182. package/src/lib/server/execution-engine/task-attempt.ts +303 -0
  183. package/src/lib/server/execution-engine/types.ts +33 -0
  184. package/src/lib/server/gateways/gateway-profile-repository.ts +47 -3
  185. package/src/lib/server/gateways/gateway-profile-service.ts +200 -0
  186. package/src/lib/server/memory/session-archive-memory.ts +12 -10
  187. package/src/lib/server/messages/message-repository.ts +330 -0
  188. package/src/lib/server/missions/mission-service/core.ts +8 -6
  189. package/src/lib/server/openclaw/agent-resolver.ts +2 -3
  190. package/src/lib/server/openclaw/doctor.ts +1 -1
  191. package/src/lib/server/openclaw/gateway.test.ts +10 -1
  192. package/src/lib/server/openclaw/gateway.ts +5 -14
  193. package/src/lib/server/openclaw/health.ts +3 -11
  194. package/src/lib/server/openclaw/sync.ts +8 -6
  195. package/src/lib/server/persistence/storage-context.ts +3 -0
  196. package/src/lib/server/protocols/protocol-agent-turn.ts +25 -17
  197. package/src/lib/server/protocols/protocol-normalization.ts +1 -1
  198. package/src/lib/server/protocols/protocol-queries.ts +13 -7
  199. package/src/lib/server/protocols/protocol-run-lifecycle.ts +16 -20
  200. package/src/lib/server/protocols/protocol-run-repository.ts +81 -0
  201. package/src/lib/server/protocols/protocol-step-processors.ts +23 -31
  202. package/src/lib/server/protocols/protocol-swarm.ts +8 -8
  203. package/src/lib/server/protocols/protocol-template-repository.ts +42 -0
  204. package/src/lib/server/protocols/protocol-templates.ts +4 -2
  205. package/src/lib/server/protocols/protocol-types.ts +10 -7
  206. package/src/lib/server/provider-endpoint.ts +7 -12
  207. package/src/lib/server/provider-model-discovery.ts +2 -11
  208. package/src/lib/server/query-expansion.ts +5 -6
  209. package/src/lib/server/run-context.test.ts +365 -0
  210. package/src/lib/server/run-context.ts +367 -0
  211. package/src/lib/server/runtime/heartbeat-service.ts +7 -5
  212. package/src/lib/server/runtime/queue/core.ts +61 -190
  213. package/src/lib/server/runtime/run-ledger.ts +8 -0
  214. package/src/lib/server/runtime/session-run-manager/drain.ts +2 -2
  215. package/src/lib/server/runtime/session-run-manager/enqueue.ts +6 -0
  216. package/src/lib/server/runtime/session-run-manager/state.ts +4 -0
  217. package/src/lib/server/schedules/schedule-route-service.ts +230 -0
  218. package/src/lib/server/service-result.ts +16 -0
  219. package/src/lib/server/session-note.ts +2 -3
  220. package/src/lib/server/session-reset-policy.ts +4 -3
  221. package/src/lib/server/session-tools/connector.ts +9 -6
  222. package/src/lib/server/session-tools/context-mgmt.ts +58 -9
  223. package/src/lib/server/session-tools/crud.ts +162 -10
  224. package/src/lib/server/session-tools/delegate.ts +1 -1
  225. package/src/lib/server/session-tools/manage-tasks.test.ts +152 -0
  226. package/src/lib/server/session-tools/memory.ts +6 -4
  227. package/src/lib/server/session-tools/session-info.test.ts +56 -0
  228. package/src/lib/server/session-tools/session-info.ts +119 -12
  229. package/src/lib/server/session-tools/skill-runtime.ts +3 -1
  230. package/src/lib/server/session-tools/skills.ts +15 -15
  231. package/src/lib/server/session-tools/subagent.test.ts +115 -1
  232. package/src/lib/server/session-tools/subagent.ts +125 -7
  233. package/src/lib/server/session-tools/team-context.ts +4 -3
  234. package/src/lib/server/session-tools/wallet.ts +0 -58
  235. package/src/lib/server/sessions/session-lineage.ts +55 -0
  236. package/src/lib/server/sessions/session-repository.ts +2 -2
  237. package/src/lib/server/skills/learned-skills.ts +24 -23
  238. package/src/lib/server/skills/runtime-skill-resolver.ts +2 -1
  239. package/src/lib/server/skills/skill-repository.ts +136 -13
  240. package/src/lib/server/skills/skill-suggestions.ts +25 -28
  241. package/src/lib/server/storage-normalization.test.ts +44 -267
  242. package/src/lib/server/storage-normalization.ts +75 -0
  243. package/src/lib/server/storage.ts +19 -0
  244. package/src/lib/server/structured-extract.ts +3 -14
  245. package/src/lib/server/tasks/task-followups.ts +16 -11
  246. package/src/lib/server/tasks/task-result.test.ts +25 -29
  247. package/src/lib/server/tasks/task-result.ts +5 -9
  248. package/src/lib/server/tasks/task-route-service.ts +449 -0
  249. package/src/lib/server/text-normalization.ts +41 -0
  250. package/src/lib/server/tool-planning.ts +6 -42
  251. package/src/lib/server/upload-path.ts +5 -0
  252. package/src/lib/server/working-state/extraction.ts +614 -0
  253. package/src/lib/server/working-state/normalization.ts +866 -0
  254. package/src/lib/server/working-state/prompt.ts +60 -0
  255. package/src/lib/server/working-state/repository.ts +38 -0
  256. package/src/lib/server/working-state/service.test.ts +253 -0
  257. package/src/lib/server/working-state/service.ts +293 -0
  258. package/src/lib/validation/schemas.ts +1 -0
  259. package/src/lib/ws-client.ts +3 -3
  260. package/src/stores/slices/task-slice.ts +1 -4
  261. package/src/stores/use-chatroom-store.ts +2 -2
  262. package/src/types/index.ts +277 -12
@@ -0,0 +1,577 @@
1
+ import crypto from 'node:crypto'
2
+ import { spawn } from 'node:child_process'
3
+ import fs from 'node:fs'
4
+ import net from 'node:net'
5
+ import path from 'node:path'
6
+
7
+ import { log } from '@/lib/server/logger'
8
+ import {
9
+ DAEMON_LOG_PATH,
10
+ clearDaemonAdminMetadata,
11
+ isProcessRunning,
12
+ readDaemonAdminMetadata,
13
+ writeDaemonAdminMetadata,
14
+ } from '@/lib/server/daemon/admin-metadata'
15
+ import {
16
+ loadDaemonStatusRecord,
17
+ patchDaemonStatusRecord,
18
+ } from '@/lib/server/daemon/daemon-status-repository'
19
+ import type {
20
+ DaemonAdminMetadata,
21
+ DaemonConnectorRuntimeState,
22
+ DaemonHealthSummaryPayload,
23
+ DaemonRunningConnectorInfo,
24
+ DaemonStatusPayload,
25
+ } from '@/lib/server/daemon/types'
26
+ import { DATA_DIR } from '@/lib/server/data-dir'
27
+ import { loadEstopState } from '@/lib/server/runtime/estop'
28
+ import { daemonAutostartEnvEnabled } from '@/lib/server/runtime/daemon-policy'
29
+ import {
30
+ releaseRuntimeLock,
31
+ tryAcquireRuntimeLock,
32
+ } from '@/lib/server/runtime/runtime-lock-repository'
33
+ import { errorMessage } from '@/lib/shared-utils'
34
+
35
+ const TAG = 'daemon-controller'
36
+ const LAUNCH_LOCK_NAME = 'daemon-launcher'
37
+ const LAUNCH_LOCK_TTL_MS = 20_000
38
+ const DAEMON_READY_TIMEOUT_MS = 20_000
39
+ const DAEMON_POLL_INTERVAL_MS = 250
40
+ const DAEMON_STALE_AFTER_MS = 20_000
41
+
42
+ function now(): number {
43
+ return Date.now()
44
+ }
45
+
46
+ function createLockOwner(): string {
47
+ return `launcher:${process.pid}:${crypto.randomBytes(6).toString('hex')}`
48
+ }
49
+
50
+ function buildDefaultStatus(): DaemonStatusPayload {
51
+ return {
52
+ running: false,
53
+ schedulerActive: false,
54
+ autostartEnabled: daemonAutostartEnvEnabled(),
55
+ backgroundServicesEnabled: true,
56
+ reducedMode: false,
57
+ manualStopRequested: false,
58
+ estop: loadEstopState(),
59
+ queueLength: 0,
60
+ lastProcessed: null,
61
+ nextScheduled: null,
62
+ heartbeat: null,
63
+ health: {
64
+ monitorActive: false,
65
+ connectorMonitorActive: false,
66
+ staleSessions: 0,
67
+ connectorsInBackoff: 0,
68
+ connectorsExhausted: 0,
69
+ checkIntervalSec: 120,
70
+ connectorCheckIntervalSec: 15,
71
+ integrity: {
72
+ enabled: true,
73
+ lastCheckedAt: null,
74
+ lastDriftCount: 0,
75
+ },
76
+ },
77
+ webhookRetry: {
78
+ pendingRetries: 0,
79
+ deadLettered: 0,
80
+ },
81
+ guards: {
82
+ healthCheckRunning: false,
83
+ connectorHealthCheckRunning: false,
84
+ shuttingDown: false,
85
+ providerCircuitBreakers: 0,
86
+ },
87
+ }
88
+ }
89
+
90
+ function buildDefaultHealthSummary(): DaemonHealthSummaryPayload {
91
+ const estop = loadEstopState().level !== 'none'
92
+ return {
93
+ ok: false,
94
+ uptime: 0,
95
+ components: {
96
+ daemon: { status: estop ? 'degraded' : 'stopped' },
97
+ connectors: { healthy: 0, errored: 0, total: 0 },
98
+ providers: { healthy: 0, cooldown: 0, total: 0 },
99
+ gateways: { healthy: 0, degraded: 0, total: 0 },
100
+ },
101
+ estop,
102
+ nextScheduledTask: null,
103
+ }
104
+ }
105
+
106
+ function getDaemonHomeDir(): string {
107
+ const configured = process.env.SWARMCLAW_HOME?.trim()
108
+ if (configured) return path.resolve(configured)
109
+ return path.dirname(DATA_DIR)
110
+ }
111
+
112
+ function resolveDaemonRoot(): string | null {
113
+ const candidates = [
114
+ process.env.SWARMCLAW_BUILD_ROOT,
115
+ process.env.SWARMCLAW_PACKAGE_ROOT,
116
+ process.cwd(),
117
+ ]
118
+ .filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
119
+ .map((value) => path.resolve(value))
120
+
121
+ for (const root of candidates) {
122
+ if (fs.existsSync(path.join(root, 'src', 'lib', 'server', 'daemon', 'daemon-runtime.ts'))) {
123
+ return root
124
+ }
125
+ }
126
+
127
+ return null
128
+ }
129
+
130
+ function resolveDaemonRuntimeEntry(): { root: string; entry: string } {
131
+ const root = resolveDaemonRoot()
132
+ if (!root) {
133
+ throw new Error('Unable to locate daemon runtime entry. Set SWARMCLAW_BUILD_ROOT or SWARMCLAW_PACKAGE_ROOT.')
134
+ }
135
+ return {
136
+ root,
137
+ entry: path.join(root, 'src', 'lib', 'server', 'daemon', 'daemon-runtime.ts'),
138
+ }
139
+ }
140
+
141
+ function buildDaemonUrl(port: number, routePath: string): string {
142
+ const normalized = routePath.startsWith('/') ? routePath : `/${routePath}`
143
+ return `http://127.0.0.1:${port}${normalized}`
144
+ }
145
+
146
+ function withTimeout(timeoutMs = 2_000): AbortSignal {
147
+ return AbortSignal.timeout(timeoutMs)
148
+ }
149
+
150
+ async function readJsonResponse<T>(response: Response): Promise<T> {
151
+ const text = await response.text()
152
+ if (!text) return {} as T
153
+ return JSON.parse(text) as T
154
+ }
155
+
156
+ type DaemonSnapshotResponse = {
157
+ status: DaemonStatusPayload
158
+ healthSummary: DaemonHealthSummaryPayload
159
+ }
160
+
161
+ async function requestDaemon<T>(
162
+ metadata: DaemonAdminMetadata,
163
+ routePath: string,
164
+ init?: RequestInit,
165
+ ): Promise<T> {
166
+ const headers = new Headers(init?.headers || {})
167
+ headers.set('authorization', `Bearer ${metadata.token}`)
168
+ if (init?.body && !headers.has('content-type')) {
169
+ headers.set('content-type', 'application/json')
170
+ }
171
+
172
+ const response = await fetch(buildDaemonUrl(metadata.port, routePath), {
173
+ ...init,
174
+ headers,
175
+ signal: init?.signal || withTimeout(),
176
+ })
177
+ if (!response.ok) {
178
+ const detail = await response.text().catch(() => '')
179
+ throw new Error(`Daemon admin request failed (${response.status}): ${detail || response.statusText}`)
180
+ }
181
+ return readJsonResponse<T>(response)
182
+ }
183
+
184
+ function daemonRecordLooksLive(): boolean {
185
+ const record = loadDaemonStatusRecord()
186
+ return Boolean(
187
+ record.pid
188
+ && isProcessRunning(record.pid)
189
+ && record.lastHeartbeatAt
190
+ && now() - record.lastHeartbeatAt <= DAEMON_STALE_AFTER_MS,
191
+ )
192
+ }
193
+
194
+ function buildFallbackStatus(): DaemonStatusPayload {
195
+ const record = loadDaemonStatusRecord()
196
+ const base = record.lastStatus ? { ...record.lastStatus } : buildDefaultStatus()
197
+ const running = daemonRecordLooksLive()
198
+ return {
199
+ ...base,
200
+ running,
201
+ schedulerActive: running,
202
+ autostartEnabled: daemonAutostartEnvEnabled(),
203
+ manualStopRequested: record.manualStopRequested,
204
+ estop: loadEstopState(),
205
+ }
206
+ }
207
+
208
+ function buildFallbackHealthSummary(): DaemonHealthSummaryPayload {
209
+ const record = loadDaemonStatusRecord()
210
+ const running = daemonRecordLooksLive()
211
+ const base = record.lastHealthSummary
212
+ ? {
213
+ ...record.lastHealthSummary,
214
+ components: {
215
+ ...record.lastHealthSummary.components,
216
+ daemon: {
217
+ ...record.lastHealthSummary.components.daemon,
218
+ },
219
+ },
220
+ }
221
+ : buildDefaultHealthSummary()
222
+
223
+ base.ok = running && base.components.daemon.status !== 'degraded'
224
+ base.components.daemon.status = running
225
+ ? (loadEstopState().level === 'none' ? 'healthy' : 'degraded')
226
+ : 'stopped'
227
+ base.estop = loadEstopState().level !== 'none'
228
+ return base
229
+ }
230
+
231
+ function markDaemonUnavailable(source: string, err?: unknown): void {
232
+ clearDaemonAdminMetadata()
233
+ patchDaemonStatusRecord((current) => {
234
+ const status = current.lastStatus ? { ...current.lastStatus } : buildDefaultStatus()
235
+ status.running = false
236
+ status.schedulerActive = false
237
+ status.estop = loadEstopState()
238
+ return {
239
+ ...current,
240
+ pid: null,
241
+ adminPort: null,
242
+ desiredState: current.manualStopRequested ? 'stopped' : current.desiredState,
243
+ stoppedAt: now(),
244
+ updatedAt: now(),
245
+ lastStopSource: source,
246
+ lastError: err ? errorMessage(err) : current.lastError,
247
+ lastStatus: status,
248
+ }
249
+ })
250
+ }
251
+
252
+ async function getLiveDaemonSnapshot(): Promise<DaemonSnapshotResponse | null> {
253
+ const metadata = readDaemonAdminMetadata()
254
+ if (!metadata) return null
255
+ if (!isProcessRunning(metadata.pid)) {
256
+ markDaemonUnavailable('pid-missing')
257
+ return null
258
+ }
259
+ try {
260
+ return await requestDaemon<DaemonSnapshotResponse>(metadata, '/status')
261
+ } catch (err: unknown) {
262
+ if (!isProcessRunning(metadata.pid)) {
263
+ markDaemonUnavailable('request-failed', err)
264
+ return null
265
+ }
266
+ return null
267
+ }
268
+ }
269
+
270
+ async function waitForDaemonReady(metadata: DaemonAdminMetadata): Promise<void> {
271
+ const deadline = now() + DAEMON_READY_TIMEOUT_MS
272
+ while (now() < deadline) {
273
+ if (!isProcessRunning(metadata.pid)) {
274
+ throw new Error(`Daemon process ${metadata.pid} exited before becoming ready.`)
275
+ }
276
+ try {
277
+ const snapshot = await requestDaemon<DaemonSnapshotResponse>(metadata, '/status')
278
+ patchDaemonStatusRecord((current) => ({
279
+ ...current,
280
+ pid: metadata.pid,
281
+ adminPort: metadata.port,
282
+ desiredState: 'running',
283
+ manualStopRequested: false,
284
+ startedAt: current.startedAt || metadata.launchedAt,
285
+ stoppedAt: null,
286
+ lastHeartbeatAt: now(),
287
+ updatedAt: now(),
288
+ lastError: null,
289
+ lastStatus: snapshot.status,
290
+ lastHealthSummary: snapshot.healthSummary,
291
+ }))
292
+ return
293
+ } catch {
294
+ await new Promise((resolve) => setTimeout(resolve, DAEMON_POLL_INTERVAL_MS))
295
+ }
296
+ }
297
+ throw new Error(`Timed out waiting for daemon admin server on port ${metadata.port}.`)
298
+ }
299
+
300
+ async function waitForProcessExit(pid: number, timeoutMs = 5_000): Promise<void> {
301
+ const deadline = now() + timeoutMs
302
+ while (now() < deadline) {
303
+ if (!isProcessRunning(pid)) return
304
+ await new Promise((resolve) => setTimeout(resolve, 100))
305
+ }
306
+ }
307
+
308
+ async function reservePort(): Promise<number> {
309
+ return new Promise<number>((resolve, reject) => {
310
+ const server = net.createServer()
311
+ server.once('error', reject)
312
+ server.listen(0, '127.0.0.1', () => {
313
+ const address = server.address()
314
+ if (!address || typeof address === 'string') {
315
+ server.close(() => reject(new Error('Failed to reserve daemon admin port.')))
316
+ return
317
+ }
318
+ const { port } = address
319
+ server.close((err) => {
320
+ if (err) reject(err)
321
+ else resolve(port)
322
+ })
323
+ })
324
+ })
325
+ }
326
+
327
+ function buildDaemonSpawnEnv(root: string, adminPort: number, adminToken: string): NodeJS.ProcessEnv {
328
+ return {
329
+ ...process.env,
330
+ SWARMCLAW_HOME: getDaemonHomeDir(),
331
+ DATA_DIR,
332
+ WORKSPACE_DIR: process.env.WORKSPACE_DIR,
333
+ BROWSER_PROFILES_DIR: process.env.BROWSER_PROFILES_DIR,
334
+ SWARMCLAW_BUILD_ROOT: process.env.SWARMCLAW_BUILD_ROOT || root,
335
+ SWARMCLAW_PACKAGE_ROOT: process.env.SWARMCLAW_PACKAGE_ROOT || root,
336
+ SWARMCLAW_RUNTIME_ROLE: 'daemon',
337
+ SWARMCLAW_DAEMON_BACKGROUND_SERVICES: '1',
338
+ SWARMCLAW_DAEMON_ADMIN_PORT: String(adminPort),
339
+ SWARMCLAW_DAEMON_ADMIN_TOKEN: adminToken,
340
+ }
341
+ }
342
+
343
+ export async function ensureDaemonProcessRunning(
344
+ source: string,
345
+ opts?: { manualStart?: boolean },
346
+ ): Promise<boolean> {
347
+ const manualStart = opts?.manualStart === true
348
+ const record = loadDaemonStatusRecord()
349
+ if (loadEstopState().level !== 'none') return false
350
+ if (!manualStart && !daemonAutostartEnvEnabled()) return false
351
+ if (!manualStart && record.manualStopRequested) return false
352
+
353
+ const live = await getLiveDaemonSnapshot()
354
+ if (live?.status.running) return false
355
+
356
+ const lockOwner = createLockOwner()
357
+ if (!tryAcquireRuntimeLock(LAUNCH_LOCK_NAME, lockOwner, LAUNCH_LOCK_TTL_MS)) return false
358
+
359
+ try {
360
+ const secondCheck = await getLiveDaemonSnapshot()
361
+ if (secondCheck?.status.running) return false
362
+
363
+ const { root, entry } = resolveDaemonRuntimeEntry()
364
+ const adminPort = await reservePort()
365
+ const adminToken = crypto.randomBytes(24).toString('hex')
366
+ fs.mkdirSync(path.dirname(DAEMON_LOG_PATH), { recursive: true })
367
+ const logStream = fs.openSync(DAEMON_LOG_PATH, 'a')
368
+ const child = spawn(
369
+ process.execPath,
370
+ ['--no-warnings', '--import', 'tsx', entry, '--port', String(adminPort), '--token', adminToken],
371
+ {
372
+ cwd: root,
373
+ detached: true,
374
+ env: buildDaemonSpawnEnv(root, adminPort, adminToken),
375
+ stdio: ['ignore', logStream, logStream],
376
+ },
377
+ )
378
+
379
+ const metadata: DaemonAdminMetadata = {
380
+ pid: child.pid ?? 0,
381
+ port: adminPort,
382
+ token: adminToken,
383
+ launchedAt: now(),
384
+ source,
385
+ }
386
+ if (!metadata.pid) {
387
+ throw new Error('Daemon process failed to spawn.')
388
+ }
389
+
390
+ writeDaemonAdminMetadata(metadata)
391
+ patchDaemonStatusRecord((current) => ({
392
+ ...current,
393
+ pid: metadata.pid,
394
+ adminPort: metadata.port,
395
+ desiredState: 'running',
396
+ manualStopRequested: false,
397
+ startedAt: current.startedAt,
398
+ stoppedAt: null,
399
+ updatedAt: now(),
400
+ lastLaunchSource: source,
401
+ lastError: null,
402
+ }))
403
+
404
+ await waitForDaemonReady(metadata)
405
+ child.unref()
406
+ return true
407
+ } catch (err: unknown) {
408
+ markDaemonUnavailable(`launch-failed:${source}`, err)
409
+ throw err
410
+ } finally {
411
+ releaseRuntimeLock(LAUNCH_LOCK_NAME, lockOwner)
412
+ }
413
+ }
414
+
415
+ export async function stopDaemonProcess(opts?: {
416
+ source?: string
417
+ manualStop?: boolean
418
+ }): Promise<boolean> {
419
+ const source = opts?.source || 'unknown'
420
+ const manualStop = opts?.manualStop === true
421
+ const metadata = readDaemonAdminMetadata()
422
+
423
+ if (!metadata || !isProcessRunning(metadata.pid)) {
424
+ clearDaemonAdminMetadata()
425
+ patchDaemonStatusRecord((current) => ({
426
+ ...current,
427
+ pid: null,
428
+ adminPort: null,
429
+ desiredState: 'stopped',
430
+ manualStopRequested: manualStop ? true : current.manualStopRequested,
431
+ stoppedAt: now(),
432
+ updatedAt: now(),
433
+ lastStopSource: source,
434
+ lastStatus: {
435
+ ...(current.lastStatus || buildDefaultStatus()),
436
+ running: false,
437
+ schedulerActive: false,
438
+ manualStopRequested: manualStop ? true : current.manualStopRequested,
439
+ estop: loadEstopState(),
440
+ },
441
+ }))
442
+ return false
443
+ }
444
+
445
+ try {
446
+ await requestDaemon<{ ok: boolean }>(metadata, '/stop', {
447
+ method: 'POST',
448
+ body: JSON.stringify({ source }),
449
+ })
450
+ } catch (err: unknown) {
451
+ if (isProcessRunning(metadata.pid)) {
452
+ try {
453
+ process.kill(metadata.pid, 'SIGTERM')
454
+ } catch {
455
+ // Fall through to stale cleanup below.
456
+ }
457
+ }
458
+ log.warn(TAG, `Daemon stop request fell back to SIGTERM (${source})`, errorMessage(err))
459
+ }
460
+
461
+ await waitForProcessExit(metadata.pid)
462
+ clearDaemonAdminMetadata()
463
+ patchDaemonStatusRecord((current) => ({
464
+ ...current,
465
+ pid: null,
466
+ adminPort: null,
467
+ desiredState: 'stopped',
468
+ manualStopRequested: manualStop ? true : current.manualStopRequested,
469
+ stoppedAt: now(),
470
+ updatedAt: now(),
471
+ lastStopSource: source,
472
+ lastStatus: {
473
+ ...(current.lastStatus || buildDefaultStatus()),
474
+ running: false,
475
+ schedulerActive: false,
476
+ manualStopRequested: manualStop ? true : current.manualStopRequested,
477
+ estop: loadEstopState(),
478
+ },
479
+ }))
480
+ return true
481
+ }
482
+
483
+ export async function getDaemonStatusSnapshot(): Promise<DaemonStatusPayload> {
484
+ const live = await getLiveDaemonSnapshot()
485
+ if (live) return live.status
486
+ return buildFallbackStatus()
487
+ }
488
+
489
+ export async function getDaemonHealthSummarySnapshot(): Promise<DaemonHealthSummaryPayload> {
490
+ const live = await getLiveDaemonSnapshot()
491
+ if (live) return live.healthSummary
492
+ return buildFallbackHealthSummary()
493
+ }
494
+
495
+ export async function runDaemonHealthCheckViaAdmin(source: string): Promise<DaemonSnapshotResponse> {
496
+ await ensureDaemonProcessRunning(source, { manualStart: true })
497
+ const metadata = readDaemonAdminMetadata()
498
+ if (!metadata) {
499
+ return {
500
+ status: buildFallbackStatus(),
501
+ healthSummary: buildFallbackHealthSummary(),
502
+ }
503
+ }
504
+ try {
505
+ return await requestDaemon<DaemonSnapshotResponse>(metadata, '/health-check', {
506
+ method: 'POST',
507
+ body: JSON.stringify({ source }),
508
+ })
509
+ } catch (err: unknown) {
510
+ markDaemonUnavailable(`health-check:${source}`, err)
511
+ return {
512
+ status: buildFallbackStatus(),
513
+ healthSummary: buildFallbackHealthSummary(),
514
+ }
515
+ }
516
+ }
517
+
518
+ export async function listDaemonConnectorRuntime(): Promise<Record<string, DaemonConnectorRuntimeState>> {
519
+ const metadata = readDaemonAdminMetadata()
520
+ if (!metadata || !isProcessRunning(metadata.pid)) return {}
521
+ try {
522
+ const result = await requestDaemon<{ connectors: Record<string, DaemonConnectorRuntimeState> }>(metadata, '/connectors')
523
+ return result.connectors || {}
524
+ } catch {
525
+ return {}
526
+ }
527
+ }
528
+
529
+ export async function getDaemonConnectorRuntime(connectorId: string): Promise<DaemonConnectorRuntimeState | null> {
530
+ const metadata = readDaemonAdminMetadata()
531
+ if (!metadata || !isProcessRunning(metadata.pid)) return null
532
+ try {
533
+ const result = await requestDaemon<{ connector: DaemonConnectorRuntimeState | null }>(
534
+ metadata,
535
+ `/connectors/${encodeURIComponent(connectorId)}`,
536
+ )
537
+ return result.connector || null
538
+ } catch {
539
+ return null
540
+ }
541
+ }
542
+
543
+ export async function runDaemonConnectorAction(
544
+ connectorId: string,
545
+ action: 'start' | 'stop' | 'repair',
546
+ source: string,
547
+ ): Promise<DaemonConnectorRuntimeState | null> {
548
+ if (action !== 'stop') {
549
+ await ensureDaemonProcessRunning(source, { manualStart: true })
550
+ }
551
+ const metadata = readDaemonAdminMetadata()
552
+ if (!metadata || !isProcessRunning(metadata.pid)) return null
553
+ const result = await requestDaemon<{ connector: DaemonConnectorRuntimeState | null }>(
554
+ metadata,
555
+ `/connectors/${encodeURIComponent(connectorId)}/actions`,
556
+ {
557
+ method: 'POST',
558
+ body: JSON.stringify({ action, source }),
559
+ },
560
+ )
561
+ return result.connector || null
562
+ }
563
+
564
+ export async function listDaemonRunningConnectors(platform?: string): Promise<DaemonRunningConnectorInfo[]> {
565
+ const metadata = readDaemonAdminMetadata()
566
+ if (!metadata || !isProcessRunning(metadata.pid)) return []
567
+ const query = platform ? `?platform=${encodeURIComponent(platform)}` : ''
568
+ try {
569
+ const result = await requestDaemon<{ connectors: DaemonRunningConnectorInfo[] }>(
570
+ metadata,
571
+ `/connectors/running${query}`,
572
+ )
573
+ return Array.isArray(result.connectors) ? result.connectors : []
574
+ } catch {
575
+ return []
576
+ }
577
+ }