@swarmclawai/swarmclaw 1.2.4 → 1.2.5
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.
- package/README.md +14 -0
- package/bin/daemon-cmd.js +169 -0
- package/bin/server-cmd.js +3 -0
- package/bin/swarmclaw.js +11 -0
- package/package.json +17 -16
- package/src/app/api/agents/[id]/clone/route.ts +3 -32
- package/src/app/api/agents/[id]/route.ts +6 -158
- package/src/app/api/agents/[id]/status/route.ts +2 -3
- package/src/app/api/agents/[id]/thread/route.ts +4 -17
- package/src/app/api/agents/bulk/route.ts +5 -47
- package/src/app/api/agents/route.ts +5 -119
- package/src/app/api/agents/trash/route.ts +13 -24
- package/src/app/api/auth/route.ts +3 -9
- package/src/app/api/autonomy/estop/route.ts +5 -5
- package/src/app/api/chatrooms/[id]/chat/route.ts +11 -5
- package/src/app/api/chatrooms/[id]/route.ts +23 -2
- package/src/app/api/chatrooms/route.ts +13 -2
- package/src/app/api/chats/[id]/clear/route.ts +2 -13
- package/src/app/api/chats/[id]/deploy/route.ts +2 -3
- package/src/app/api/chats/[id]/edit-resend/route.ts +7 -13
- package/src/app/api/chats/[id]/mailbox/route.ts +6 -8
- package/src/app/api/chats/[id]/queue/route.ts +17 -64
- package/src/app/api/chats/[id]/retry/route.ts +4 -22
- package/src/app/api/chats/[id]/route.ts +10 -138
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/migrate-messages/route.ts +7 -0
- package/src/app/api/chats/route.ts +13 -134
- package/src/app/api/connectors/[id]/access/route.ts +12 -229
- package/src/app/api/connectors/[id]/doctor/route.ts +1 -1
- package/src/app/api/connectors/[id]/health/route.ts +12 -39
- package/src/app/api/connectors/[id]/route.ts +14 -122
- package/src/app/api/connectors/[id]/webhook/route.ts +1 -1
- package/src/app/api/connectors/doctor/route.ts +1 -1
- package/src/app/api/connectors/route.ts +12 -70
- package/src/app/api/credentials/[id]/route.ts +2 -4
- package/src/app/api/credentials/route.ts +10 -19
- package/src/app/api/daemon/health-check/route.ts +3 -4
- package/src/app/api/daemon/route.ts +10 -8
- package/src/app/api/documents/route.ts +11 -10
- package/src/app/api/external-agents/route.ts +3 -3
- package/src/app/api/gateways/[id]/health/route.ts +2 -3
- package/src/app/api/gateways/[id]/route.ts +7 -122
- package/src/app/api/gateways/route.ts +3 -103
- package/src/app/api/mcp-servers/[id]/tools/route.ts +5 -5
- package/src/app/api/openclaw/dashboard-url/route.ts +8 -16
- package/src/app/api/openclaw/directory/route.ts +2 -2
- package/src/app/api/openclaw/history/route.ts +3 -5
- package/src/app/api/providers/[id]/route.test.ts +49 -0
- package/src/app/api/providers/ollama/route.ts +6 -5
- package/src/app/api/schedules/[id]/route.ts +14 -108
- package/src/app/api/schedules/[id]/run/route.ts +6 -67
- package/src/app/api/schedules/route.ts +9 -51
- package/src/app/api/settings/route.ts +4 -3
- package/src/app/api/setup/check-provider/route.ts +15 -1
- package/src/app/api/setup/openclaw-device/route.ts +2 -2
- package/src/app/api/system/status/route.ts +2 -2
- package/src/app/api/tasks/[id]/route.ts +16 -202
- package/src/app/api/tasks/bulk/route.ts +5 -86
- package/src/app/api/tasks/metrics/route.ts +2 -1
- package/src/app/api/tasks/route.ts +11 -171
- package/src/app/api/upload/route.ts +1 -1
- package/src/app/api/uploads/[filename]/route.ts +1 -1
- package/src/app/api/uploads/route.ts +1 -1
- package/src/app/api/webhooks/[id]/history/route.ts +2 -2
- package/src/app/layout.tsx +9 -6
- package/src/app/protocols/page.tsx +71 -89
- package/src/app/tasks/page.tsx +32 -32
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-sheet.tsx +5 -5
- package/src/components/auth/setup-wizard/index.tsx +4 -4
- package/src/components/auth/setup-wizard/step-agents.tsx +1 -1
- package/src/components/auth/setup-wizard/step-connect.tsx +1 -1
- package/src/components/auth/setup-wizard/utils.ts +1 -1
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -276
- package/src/components/connectors/connector-list.tsx +26 -40
- package/src/components/connectors/connector-sheet.tsx +95 -149
- package/src/components/gateways/gateway-sheet.tsx +61 -110
- package/src/components/layout/live-query-sync.tsx +121 -0
- package/src/components/protocols/structured-session-launcher.tsx +24 -45
- package/src/components/providers/app-query-provider.tsx +17 -0
- package/src/components/providers/provider-list.tsx +60 -61
- package/src/components/providers/provider-sheet.tsx +74 -56
- package/src/components/skills/skill-list.tsx +5 -18
- package/src/components/skills/skill-sheet.tsx +21 -20
- package/src/components/skills/skills-workspace.tsx +48 -87
- package/src/components/tasks/task-card.tsx +20 -13
- package/src/components/tasks/task-column.tsx +22 -7
- package/src/components/tasks/task-list.tsx +8 -11
- package/src/components/tasks/task-sheet.tsx +111 -103
- package/src/features/agents/queries.ts +20 -0
- package/src/features/chatrooms/queries.ts +20 -0
- package/src/features/chats/queries.ts +27 -0
- package/src/features/connectors/queries.ts +145 -0
- package/src/features/credentials/queries.ts +37 -0
- package/src/features/extensions/queries.ts +26 -0
- package/src/features/external-agents/queries.ts +36 -0
- package/src/features/gateways/queries.ts +274 -0
- package/src/features/missions/queries.ts +23 -0
- package/src/features/projects/queries.ts +20 -0
- package/src/features/protocols/queries.ts +149 -0
- package/src/features/providers/queries.ts +142 -0
- package/src/features/settings/queries.ts +20 -0
- package/src/features/skills/queries.ts +182 -0
- package/src/features/tasks/queries.ts +189 -0
- package/src/hooks/use-ws.ts +3 -2
- package/src/lib/app/api-client.ts +2 -2
- package/src/lib/query/client.ts +17 -0
- package/src/lib/server/agents/agent-runtime-config.ts +1 -1
- package/src/lib/server/agents/agent-service.ts +429 -0
- package/src/lib/server/agents/agent-thread-session.ts +6 -5
- package/src/lib/server/agents/autonomy-contract.ts +1 -4
- package/src/lib/server/agents/delegation-advisory.test.ts +206 -0
- package/src/lib/server/agents/delegation-advisory.ts +251 -0
- package/src/lib/server/agents/main-agent-loop.ts +98 -40
- package/src/lib/server/agents/subagent-runtime.ts +12 -0
- package/src/lib/server/autonomy/supervisor-reflection.test.ts +20 -1
- package/src/lib/server/autonomy/supervisor-reflection.ts +39 -19
- package/src/lib/server/build-llm.ts +7 -15
- package/src/lib/server/capability-router.test.ts +70 -1
- package/src/lib/server/capability-router.ts +24 -99
- package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -15
- package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -4
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +77 -12
- package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +4 -4
- package/src/lib/server/chat-execution/chat-turn-preflight.ts +2 -2
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +41 -17
- package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -2
- package/src/lib/server/chat-execution/chat-turn-tool-routing.test.ts +45 -0
- package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +48 -17
- package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -1
- package/src/lib/server/chat-execution/direct-memory-intent.test.ts +9 -0
- package/src/lib/server/chat-execution/direct-memory-intent.ts +12 -2
- package/src/lib/server/chat-execution/message-classifier.test.ts +35 -23
- package/src/lib/server/chat-execution/message-classifier.ts +74 -32
- package/src/lib/server/chat-execution/prompt-builder.test.ts +29 -0
- package/src/lib/server/chat-execution/prompt-builder.ts +37 -2
- package/src/lib/server/chat-execution/prompt-sections.test.ts +56 -0
- package/src/lib/server/chat-execution/prompt-sections.ts +193 -0
- package/src/lib/server/chat-execution/stream-agent-chat.ts +63 -7
- package/src/lib/server/chat-execution/stream-continuation.test.ts +36 -0
- package/src/lib/server/chat-execution/stream-continuation.ts +28 -13
- package/src/lib/server/chatrooms/chatroom-agent-signals.ts +26 -18
- package/src/lib/server/chatrooms/chatroom-helpers.ts +19 -18
- package/src/lib/server/chatrooms/chatroom-repository.ts +16 -0
- package/src/lib/server/chatrooms/chatroom-routing.test.ts +96 -0
- package/src/lib/server/chatrooms/chatroom-routing.ts +207 -53
- package/src/lib/server/chatrooms/mailbox-utils.ts +4 -2
- package/src/lib/server/chatrooms/session-mailbox.ts +50 -40
- package/src/lib/server/chats/chat-session-service.ts +410 -0
- package/src/lib/server/connectors/access.ts +1 -1
- package/src/lib/server/connectors/commands.ts +7 -6
- package/src/lib/server/connectors/connector-inbound.ts +14 -7
- package/src/lib/server/connectors/connector-outbound.ts +16 -11
- package/src/lib/server/connectors/connector-service.ts +453 -0
- package/src/lib/server/connectors/delivery.ts +17 -12
- package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -14
- package/src/lib/server/connectors/media.ts +1 -1
- package/src/lib/server/connectors/response-media.ts +1 -1
- package/src/lib/server/connectors/session-consolidation.ts +11 -7
- package/src/lib/server/connectors/session.ts +9 -7
- package/src/lib/server/connectors/voice-note.ts +2 -1
- package/src/lib/server/context-manager.ts +20 -1
- package/src/lib/server/cost.ts +2 -3
- package/src/lib/server/credentials/credential-repository.ts +43 -4
- package/src/lib/server/credentials/credential-service.ts +112 -0
- package/src/lib/server/daemon/admin-metadata.ts +64 -0
- package/src/lib/server/daemon/controller.ts +577 -0
- package/src/lib/server/daemon/daemon-runtime.ts +352 -0
- package/src/lib/server/daemon/daemon-status-repository.ts +63 -0
- package/src/lib/server/daemon/types.ts +101 -0
- package/src/lib/server/embeddings.ts +3 -9
- package/src/lib/server/eval/agent-regression.ts +3 -2
- package/src/lib/server/eval/runner.ts +2 -2
- package/src/lib/server/execution-brief.test.ts +167 -0
- package/src/lib/server/execution-brief.ts +295 -0
- package/src/lib/server/execution-engine/chat-turn.ts +9 -0
- package/src/lib/server/execution-engine/import-boundary.test.ts +44 -0
- package/src/lib/server/execution-engine/index.ts +35 -0
- package/src/lib/server/execution-engine/task-attempt.ts +303 -0
- package/src/lib/server/execution-engine/types.ts +33 -0
- package/src/lib/server/gateways/gateway-profile-repository.ts +47 -3
- package/src/lib/server/gateways/gateway-profile-service.ts +200 -0
- package/src/lib/server/memory/session-archive-memory.ts +12 -10
- package/src/lib/server/messages/message-repository.ts +330 -0
- package/src/lib/server/missions/mission-service/core.ts +8 -6
- package/src/lib/server/openclaw/agent-resolver.ts +2 -3
- package/src/lib/server/openclaw/doctor.ts +1 -1
- package/src/lib/server/openclaw/gateway.test.ts +10 -1
- package/src/lib/server/openclaw/gateway.ts +5 -14
- package/src/lib/server/openclaw/health.ts +3 -11
- package/src/lib/server/openclaw/sync.ts +8 -6
- package/src/lib/server/persistence/storage-context.ts +3 -0
- package/src/lib/server/protocols/protocol-agent-turn.ts +25 -17
- package/src/lib/server/protocols/protocol-normalization.ts +1 -1
- package/src/lib/server/protocols/protocol-queries.ts +13 -7
- package/src/lib/server/protocols/protocol-run-lifecycle.ts +16 -20
- package/src/lib/server/protocols/protocol-run-repository.ts +81 -0
- package/src/lib/server/protocols/protocol-step-processors.ts +23 -31
- package/src/lib/server/protocols/protocol-swarm.ts +8 -8
- package/src/lib/server/protocols/protocol-template-repository.ts +42 -0
- package/src/lib/server/protocols/protocol-templates.ts +4 -2
- package/src/lib/server/protocols/protocol-types.ts +10 -7
- package/src/lib/server/provider-endpoint.ts +7 -12
- package/src/lib/server/provider-model-discovery.ts +2 -11
- package/src/lib/server/query-expansion.ts +5 -6
- package/src/lib/server/run-context.test.ts +365 -0
- package/src/lib/server/run-context.ts +367 -0
- package/src/lib/server/runtime/heartbeat-service.ts +7 -5
- package/src/lib/server/runtime/queue/core.ts +61 -190
- package/src/lib/server/runtime/run-ledger.ts +8 -0
- package/src/lib/server/runtime/session-run-manager/drain.ts +2 -2
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +6 -0
- package/src/lib/server/runtime/session-run-manager/state.ts +4 -0
- package/src/lib/server/schedules/schedule-route-service.ts +230 -0
- package/src/lib/server/service-result.ts +16 -0
- package/src/lib/server/session-note.ts +2 -3
- package/src/lib/server/session-reset-policy.ts +4 -3
- package/src/lib/server/session-tools/connector.ts +9 -6
- package/src/lib/server/session-tools/context-mgmt.ts +58 -9
- package/src/lib/server/session-tools/crud.ts +162 -10
- package/src/lib/server/session-tools/delegate.ts +1 -1
- package/src/lib/server/session-tools/manage-tasks.test.ts +152 -0
- package/src/lib/server/session-tools/memory.ts +6 -4
- package/src/lib/server/session-tools/session-info.test.ts +56 -0
- package/src/lib/server/session-tools/session-info.ts +119 -12
- package/src/lib/server/session-tools/skill-runtime.ts +3 -1
- package/src/lib/server/session-tools/skills.ts +15 -15
- package/src/lib/server/session-tools/subagent.test.ts +115 -1
- package/src/lib/server/session-tools/subagent.ts +125 -7
- package/src/lib/server/session-tools/team-context.ts +4 -3
- package/src/lib/server/session-tools/wallet.ts +0 -58
- package/src/lib/server/sessions/session-lineage.ts +55 -0
- package/src/lib/server/sessions/session-repository.ts +2 -2
- package/src/lib/server/skills/learned-skills.ts +24 -23
- package/src/lib/server/skills/runtime-skill-resolver.ts +2 -1
- package/src/lib/server/skills/skill-repository.ts +136 -13
- package/src/lib/server/skills/skill-suggestions.ts +25 -28
- package/src/lib/server/storage-normalization.test.ts +44 -267
- package/src/lib/server/storage-normalization.ts +75 -0
- package/src/lib/server/storage.ts +19 -0
- package/src/lib/server/structured-extract.ts +3 -14
- package/src/lib/server/tasks/task-followups.ts +16 -11
- package/src/lib/server/tasks/task-result.test.ts +25 -29
- package/src/lib/server/tasks/task-result.ts +5 -9
- package/src/lib/server/tasks/task-route-service.ts +449 -0
- package/src/lib/server/text-normalization.ts +41 -0
- package/src/lib/server/tool-planning.ts +6 -42
- package/src/lib/server/upload-path.ts +5 -0
- package/src/lib/server/working-state/extraction.ts +614 -0
- package/src/lib/server/working-state/normalization.ts +866 -0
- package/src/lib/server/working-state/prompt.ts +60 -0
- package/src/lib/server/working-state/repository.ts +38 -0
- package/src/lib/server/working-state/service.test.ts +253 -0
- package/src/lib/server/working-state/service.ts +293 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/ws-client.ts +3 -3
- package/src/stores/slices/task-slice.ts +1 -4
- package/src/stores/use-chatroom-store.ts +2 -2
- 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
|
+
}
|