@geekbeer/minion 3.14.0 → 3.16.1
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/core/lib/dag-node-executor.js +81 -0
- package/core/lib/dag-step-poller.js +282 -0
- package/docs/api-reference.md +1 -1
- package/linux/lib/process-manager.js +10 -0
- package/linux/server.js +3 -0
- package/package.json +1 -1
- package/win/lib/process-manager.js +62 -0
- package/win/server.js +3 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DAG Node Executor
|
|
3
|
+
*
|
|
4
|
+
* Handles the execution lifecycle of a single DAG node.
|
|
5
|
+
* Responsible for:
|
|
6
|
+
* - Extracting structured output_data from skill execution results
|
|
7
|
+
* - Reporting completion to HQ via /api/dag/minion/node-complete
|
|
8
|
+
*
|
|
9
|
+
* Called by the post-execution hook in skills.js when a DAG node skill completes.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { reportNodeComplete } = require('./dag-step-poller')
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse structured output_data from a skill's execution output.
|
|
16
|
+
*
|
|
17
|
+
* Convention: Skills produce structured output via a "## Output Data" section
|
|
18
|
+
* in their execution report, containing a JSON code block.
|
|
19
|
+
*
|
|
20
|
+
* Example:
|
|
21
|
+
* ```
|
|
22
|
+
* ## Output Data
|
|
23
|
+
* ```json
|
|
24
|
+
* { "results": [...], "count": 5 }
|
|
25
|
+
* ```
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* If no structured output is found, wraps the raw summary as { _raw: "..." }
|
|
29
|
+
*/
|
|
30
|
+
function extractOutputData(summary, details) {
|
|
31
|
+
const fullText = [summary, details].filter(Boolean).join('\n\n')
|
|
32
|
+
|
|
33
|
+
// Try to find ## Output Data section with JSON
|
|
34
|
+
const outputDataMatch = fullText.match(
|
|
35
|
+
/##\s*Output\s*Data\s*\n+```(?:json)?\s*\n([\s\S]*?)\n```/i
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if (outputDataMatch) {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(outputDataMatch[1].trim())
|
|
41
|
+
} catch {
|
|
42
|
+
console.warn('[DagNodeExecutor] Failed to parse Output Data JSON, using raw')
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Fallback: try to parse the entire summary as JSON
|
|
47
|
+
if (summary) {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(summary)
|
|
50
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
51
|
+
return parsed
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Not JSON, use raw
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { _raw: fullText || '' }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Report DAG node completion with extracted output_data.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} nodeExecutionId - The dag_node_execution ID
|
|
65
|
+
* @param {string} status - 'completed' or 'failed'
|
|
66
|
+
* @param {string} summary - Execution summary text
|
|
67
|
+
* @param {string} details - Execution details text
|
|
68
|
+
*/
|
|
69
|
+
async function reportDagNodeComplete(nodeExecutionId, status, summary, details) {
|
|
70
|
+
const outputSummary = [summary, details].filter(Boolean).join('\n\n')
|
|
71
|
+
const outputData = status === 'completed'
|
|
72
|
+
? extractOutputData(summary, details)
|
|
73
|
+
: {}
|
|
74
|
+
|
|
75
|
+
return reportNodeComplete(nodeExecutionId, status, outputData, outputSummary)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
extractOutputData,
|
|
80
|
+
reportDagNodeComplete,
|
|
81
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DAG Step Poller
|
|
3
|
+
*
|
|
4
|
+
* Polling daemon for DAG workflow execution.
|
|
5
|
+
* Similar to step-poller.js but with key differences:
|
|
6
|
+
* - Polls /api/dag/minion/pending-nodes instead of /api/minion/pending-steps
|
|
7
|
+
* - Supports concurrent node execution (up to MAX_CONCURRENT)
|
|
8
|
+
* - Reports completion to /api/dag/minion/node-complete
|
|
9
|
+
* - Injects input_data into skill context
|
|
10
|
+
*
|
|
11
|
+
* Feature flag: only starts when DAG_ENGINE_ENABLED=true
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { config, isHqConfigured } = require('../config')
|
|
15
|
+
const api = require('../api')
|
|
16
|
+
const variableStore = require('../stores/variable-store')
|
|
17
|
+
|
|
18
|
+
// Polling interval: 30 seconds (matches step-poller)
|
|
19
|
+
const POLL_INTERVAL_MS = 30_000
|
|
20
|
+
|
|
21
|
+
// Maximum concurrent DAG nodes this minion can execute
|
|
22
|
+
const MAX_CONCURRENT = 2
|
|
23
|
+
|
|
24
|
+
// Prevent concurrent poll cycles
|
|
25
|
+
let polling = false
|
|
26
|
+
let pollTimer = null
|
|
27
|
+
let lastPollAt = null
|
|
28
|
+
|
|
29
|
+
// Track active node executions: nodeExecId -> Promise
|
|
30
|
+
const activeNodes = new Map()
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Send request to HQ's DAG API endpoints.
|
|
34
|
+
*/
|
|
35
|
+
async function dagRequest(endpoint, options = {}) {
|
|
36
|
+
if (!isHqConfigured()) {
|
|
37
|
+
return { skipped: true, reason: 'HQ not configured' }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const url = `${config.HQ_URL}/api/dag/minion${endpoint}`
|
|
41
|
+
const resp = await fetch(url, {
|
|
42
|
+
...options,
|
|
43
|
+
headers: {
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
'Authorization': `Bearer ${config.API_TOKEN}`,
|
|
46
|
+
...(options.headers || {}),
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
if (!resp.ok) {
|
|
51
|
+
const err = new Error(`DAG API ${endpoint} failed: ${resp.status}`)
|
|
52
|
+
err.statusCode = resp.status
|
|
53
|
+
throw err
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return resp.json()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Poll HQ for pending DAG nodes and execute them.
|
|
61
|
+
*/
|
|
62
|
+
async function pollOnce() {
|
|
63
|
+
if (!isHqConfigured()) return
|
|
64
|
+
if (polling) return
|
|
65
|
+
|
|
66
|
+
polling = true
|
|
67
|
+
try {
|
|
68
|
+
const data = await dagRequest('/pending-nodes')
|
|
69
|
+
|
|
70
|
+
if (!data.nodes || data.nodes.length === 0) return
|
|
71
|
+
|
|
72
|
+
console.log(`[DagPoller] Found ${data.nodes.length} pending node(s), active: ${activeNodes.size}/${MAX_CONCURRENT}`)
|
|
73
|
+
|
|
74
|
+
for (const node of data.nodes) {
|
|
75
|
+
if (activeNodes.size >= MAX_CONCURRENT) break
|
|
76
|
+
if (activeNodes.has(node.node_execution_id)) continue
|
|
77
|
+
|
|
78
|
+
const promise = executeNode(node)
|
|
79
|
+
.catch(err => {
|
|
80
|
+
console.error(`[DagPoller] Node ${node.node_id} execution error: ${err.message}`)
|
|
81
|
+
})
|
|
82
|
+
.finally(() => {
|
|
83
|
+
activeNodes.delete(node.node_execution_id)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
activeNodes.set(node.node_execution_id, promise)
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (err.message?.includes('fetch failed') || err.message?.includes('ECONNREFUSED')) {
|
|
90
|
+
console.log('[DagPoller] HQ unreachable, will retry next cycle')
|
|
91
|
+
} else {
|
|
92
|
+
console.error(`[DagPoller] Poll error: ${err.message}`)
|
|
93
|
+
}
|
|
94
|
+
} finally {
|
|
95
|
+
polling = false
|
|
96
|
+
lastPollAt = new Date().toISOString()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Execute a single pending DAG node:
|
|
102
|
+
* 1. Claim the node
|
|
103
|
+
* 2. Fetch the skill from HQ
|
|
104
|
+
* 3. Run the skill locally (with input_data injected)
|
|
105
|
+
* 4. Report completion with output_data
|
|
106
|
+
*/
|
|
107
|
+
async function executeNode(node) {
|
|
108
|
+
const {
|
|
109
|
+
node_execution_id,
|
|
110
|
+
execution_id,
|
|
111
|
+
dag_workflow_name,
|
|
112
|
+
node_id,
|
|
113
|
+
scope_path,
|
|
114
|
+
skill_version_id,
|
|
115
|
+
skill_name: resolvedSkillName,
|
|
116
|
+
assigned_role,
|
|
117
|
+
input_data,
|
|
118
|
+
revision_feedback,
|
|
119
|
+
} = node
|
|
120
|
+
|
|
121
|
+
console.log(
|
|
122
|
+
`[DagPoller] Executing node "${node_id}" of DAG "${dag_workflow_name}" ` +
|
|
123
|
+
`(skill: ${resolvedSkillName || skill_version_id}, scope: "${scope_path || 'root'}", role: ${assigned_role})`
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
// 1. Claim the node
|
|
128
|
+
try {
|
|
129
|
+
await dagRequest('/claim-node', {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
body: JSON.stringify({ node_execution_id }),
|
|
132
|
+
})
|
|
133
|
+
} catch (claimErr) {
|
|
134
|
+
if (claimErr.statusCode === 409) {
|
|
135
|
+
console.log(`[DagPoller] Node ${node_id} already claimed, skipping`)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
throw claimErr
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 2. Resolve skill name (prefer pre-resolved from pending-nodes, fall back to API)
|
|
142
|
+
const skillName = resolvedSkillName || await resolveSkillName(skill_version_id)
|
|
143
|
+
if (!skillName) {
|
|
144
|
+
console.error(`[DagPoller] Could not resolve skill name for version ${skill_version_id}`)
|
|
145
|
+
await reportNodeComplete(node_execution_id, 'failed', null, 'Could not resolve skill name')
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 3. Fetch the skill
|
|
150
|
+
if (skillName) {
|
|
151
|
+
try {
|
|
152
|
+
const minionVars = variableStore.getAll('variables')
|
|
153
|
+
const mergedVars = { ...minionVars }
|
|
154
|
+
const varsParam = Object.keys(mergedVars).length > 0
|
|
155
|
+
? `?vars=${Buffer.from(JSON.stringify(mergedVars)).toString('base64')}`
|
|
156
|
+
: ''
|
|
157
|
+
const fetchUrl = `http://localhost:${config.AGENT_PORT || 8080}/api/skills/fetch/${encodeURIComponent(skillName)}${varsParam}`
|
|
158
|
+
await fetch(fetchUrl, {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: { 'Authorization': `Bearer ${config.API_TOKEN}` },
|
|
161
|
+
})
|
|
162
|
+
} catch (fetchErr) {
|
|
163
|
+
console.error(`[DagPoller] Skill fetch error: ${fetchErr.message}`)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 4. Run the skill with input_data context
|
|
168
|
+
const runPayload = {
|
|
169
|
+
skill_name: skillName,
|
|
170
|
+
execution_id,
|
|
171
|
+
workflow_name: dag_workflow_name,
|
|
172
|
+
role: assigned_role,
|
|
173
|
+
// DAG-specific: inject input_data as context
|
|
174
|
+
dag_node_id: node_id,
|
|
175
|
+
dag_input_data: input_data,
|
|
176
|
+
dag_node_execution_id: node_execution_id,
|
|
177
|
+
}
|
|
178
|
+
if (revision_feedback) {
|
|
179
|
+
runPayload.revision_feedback = revision_feedback
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const runUrl = `http://localhost:${config.AGENT_PORT || 8080}/api/skills/run`
|
|
183
|
+
const runResp = await fetch(runUrl, {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
headers: {
|
|
186
|
+
'Content-Type': 'application/json',
|
|
187
|
+
'Authorization': `Bearer ${config.API_TOKEN}`,
|
|
188
|
+
},
|
|
189
|
+
body: JSON.stringify(runPayload),
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
if (!runResp.ok) {
|
|
193
|
+
const errData = await runResp.json().catch(() => ({}))
|
|
194
|
+
console.error(`[DagPoller] Skill run failed: ${errData.error || runResp.status}`)
|
|
195
|
+
await reportNodeComplete(
|
|
196
|
+
node_execution_id,
|
|
197
|
+
'failed',
|
|
198
|
+
null,
|
|
199
|
+
`Failed to start skill: ${errData.error || 'unknown error'}`
|
|
200
|
+
)
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const runData = await runResp.json()
|
|
205
|
+
console.log(
|
|
206
|
+
`[DagPoller] Skill "${skillName}" started for node "${node_id}" ` +
|
|
207
|
+
`(session: ${runData.session_name}). Completion reported by post-execution hook.`
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
// Note: The post-execution hook in skills.js needs to be extended to handle DAG nodes.
|
|
211
|
+
// For now, we wait for the skill to complete and then report via the session monitor.
|
|
212
|
+
// This will be handled by dag-node-executor.js which watches the session.
|
|
213
|
+
|
|
214
|
+
} catch (err) {
|
|
215
|
+
console.error(`[DagPoller] Failed to execute node ${node_id}: ${err.message}`)
|
|
216
|
+
try {
|
|
217
|
+
await reportNodeComplete(node_execution_id, 'failed', null, err.message)
|
|
218
|
+
} catch {
|
|
219
|
+
// best-effort
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Resolve skill_version_id to skill name via HQ API.
|
|
226
|
+
*/
|
|
227
|
+
async function resolveSkillName(skillVersionId) {
|
|
228
|
+
if (!skillVersionId) return null
|
|
229
|
+
try {
|
|
230
|
+
// Use existing skill fetch pattern — the skill name is returned by pending-nodes
|
|
231
|
+
// But if not available, we need to resolve it
|
|
232
|
+
const data = await api.request(`/skill-version/${skillVersionId}`)
|
|
233
|
+
return data?.skill_name || null
|
|
234
|
+
} catch {
|
|
235
|
+
return null
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Report node completion to HQ.
|
|
241
|
+
*/
|
|
242
|
+
async function reportNodeComplete(nodeExecutionId, status, outputData, outputSummary) {
|
|
243
|
+
return dagRequest('/node-complete', {
|
|
244
|
+
method: 'POST',
|
|
245
|
+
body: JSON.stringify({
|
|
246
|
+
node_execution_id: nodeExecutionId,
|
|
247
|
+
status,
|
|
248
|
+
output_data: outputData || {},
|
|
249
|
+
output_summary: outputSummary || null,
|
|
250
|
+
}),
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function start() {
|
|
255
|
+
if (!isHqConfigured()) {
|
|
256
|
+
console.log('[DagPoller] HQ not configured, DAG poller disabled')
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
setTimeout(() => pollOnce(), 7000) // Slightly delayed after step-poller
|
|
261
|
+
pollTimer = setInterval(() => pollOnce(), POLL_INTERVAL_MS)
|
|
262
|
+
console.log(`[DagPoller] Started (polling every ${POLL_INTERVAL_MS / 1000}s, max concurrent: ${MAX_CONCURRENT})`)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function stop() {
|
|
266
|
+
if (pollTimer) {
|
|
267
|
+
clearInterval(pollTimer)
|
|
268
|
+
pollTimer = null
|
|
269
|
+
console.log('[DagPoller] Stopped')
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function getStatus() {
|
|
274
|
+
return {
|
|
275
|
+
running: pollTimer !== null,
|
|
276
|
+
last_poll_at: lastPollAt,
|
|
277
|
+
active_nodes: activeNodes.size,
|
|
278
|
+
max_concurrent: MAX_CONCURRENT,
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
module.exports = { start, stop, pollOnce, getStatus, reportNodeComplete }
|
package/docs/api-reference.md
CHANGED
|
@@ -568,7 +568,7 @@ Note: 既読メールは受信後90日で自動削除される。未読メール
|
|
|
568
568
|
| GET | `/api/commands` | List available whitelisted commands |
|
|
569
569
|
| POST | `/api/command` | Execute command. Body: `{command}` |
|
|
570
570
|
|
|
571
|
-
Available commands: `restart-agent`, `update-agent`, `restart-display`, `status-services`
|
|
571
|
+
Available commands: `restart-agent`, `update-agent`, `restart-display`, `restart-all`, `status-services`
|
|
572
572
|
|
|
573
573
|
---
|
|
574
574
|
|
|
@@ -63,6 +63,11 @@ function buildAllowedCommands(procMgr) {
|
|
|
63
63
|
description: 'Restart Xvfb, Fluxbox, x11vnc and noVNC services',
|
|
64
64
|
command: `${SUDO}systemctl restart xvfb fluxbox x11vnc novnc`,
|
|
65
65
|
}
|
|
66
|
+
commands['restart-all'] = {
|
|
67
|
+
description: 'Restart all services (display stack + agent)',
|
|
68
|
+
command: `${SUDO}systemctl restart xvfb fluxbox x11vnc novnc && ${SUDO}systemctl restart minion-agent`,
|
|
69
|
+
deferred: true,
|
|
70
|
+
}
|
|
66
71
|
commands['status-services'] = {
|
|
67
72
|
description: 'Check status of all services',
|
|
68
73
|
command: 'systemctl status minion-agent xvfb fluxbox x11vnc novnc --no-pager',
|
|
@@ -87,6 +92,11 @@ function buildAllowedCommands(procMgr) {
|
|
|
87
92
|
description: 'Restart Xvfb, x11vnc and noVNC services',
|
|
88
93
|
command: `${SUDO}supervisorctl restart xvfb fluxbox x11vnc novnc`,
|
|
89
94
|
}
|
|
95
|
+
commands['restart-all'] = {
|
|
96
|
+
description: 'Restart all services (display stack + agent)',
|
|
97
|
+
command: `${SUDO}supervisorctl restart xvfb fluxbox x11vnc novnc && ${SUDO}supervisorctl restart minion-agent`,
|
|
98
|
+
deferred: true,
|
|
99
|
+
}
|
|
90
100
|
commands['status-services'] = {
|
|
91
101
|
description: 'Check status of all services',
|
|
92
102
|
command: `${SUDO}supervisorctl status`,
|
package/linux/server.js
CHANGED
|
@@ -58,6 +58,7 @@ const { getConfigWarnings } = require('../core/lib/config-warnings')
|
|
|
58
58
|
|
|
59
59
|
// Pull-model daemons (from core/)
|
|
60
60
|
const stepPoller = require('../core/lib/step-poller')
|
|
61
|
+
const dagStepPoller = require('../core/lib/dag-step-poller')
|
|
61
62
|
const revisionWatcher = require('../core/lib/revision-watcher')
|
|
62
63
|
const reflectionScheduler = require('../core/lib/reflection-scheduler')
|
|
63
64
|
const threadWatcher = require('../core/lib/thread-watcher')
|
|
@@ -122,6 +123,7 @@ async function shutdown(signal) {
|
|
|
122
123
|
|
|
123
124
|
// Stop pollers, runners, and scheduler
|
|
124
125
|
stepPoller.stop()
|
|
126
|
+
dagStepPoller.stop()
|
|
125
127
|
revisionWatcher.stop()
|
|
126
128
|
reflectionScheduler.stop()
|
|
127
129
|
threadWatcher.stop()
|
|
@@ -391,6 +393,7 @@ async function start() {
|
|
|
391
393
|
|
|
392
394
|
// Start Pull-model daemons
|
|
393
395
|
stepPoller.start()
|
|
396
|
+
dagStepPoller.start()
|
|
394
397
|
revisionWatcher.start()
|
|
395
398
|
threadWatcher.start(runQuickLlmCall)
|
|
396
399
|
} else {
|
package/package.json
CHANGED
|
@@ -220,6 +220,62 @@ function buildRestartScript(_nssmPath, agentPort, apiToken) {
|
|
|
220
220
|
return scriptPath
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Generate a temporary PowerShell script for restarting ALL services:
|
|
225
|
+
* 1. Restart display services (VNC + websockify)
|
|
226
|
+
* 2. Graceful shutdown of agent via HTTP API
|
|
227
|
+
* 3. Restart agent service via NSSM
|
|
228
|
+
* 4. Remove the temporary updater service (self-cleanup)
|
|
229
|
+
*
|
|
230
|
+
* @param {string} _nssmPath - Absolute path to nssm.exe
|
|
231
|
+
* @param {number} agentPort - The agent's HTTP port
|
|
232
|
+
* @param {string} apiToken - The agent's API token
|
|
233
|
+
* @returns {string} - Path to the generated restart script (.ps1)
|
|
234
|
+
*/
|
|
235
|
+
function buildRestartAllScript(_nssmPath, agentPort, apiToken) {
|
|
236
|
+
const dataDir = path.join(os.homedir(), '.minion')
|
|
237
|
+
const homeDir = os.homedir()
|
|
238
|
+
const scriptPath = path.join(dataDir, 'restart-all.ps1')
|
|
239
|
+
const logDir = path.join(dataDir, 'logs')
|
|
240
|
+
const logPath = path.join(logDir, 'restart-all.log')
|
|
241
|
+
|
|
242
|
+
const gracefulStop = buildGracefulStopBlock(agentPort, apiToken)
|
|
243
|
+
|
|
244
|
+
const ps1 = [
|
|
245
|
+
`$env:USERPROFILE = '${homeDir}'`,
|
|
246
|
+
`$env:HOME = '${homeDir}'`,
|
|
247
|
+
`$ErrorActionPreference = 'Stop'`,
|
|
248
|
+
`$nssm = '${path.join(dataDir, 'nssm.exe')}'`,
|
|
249
|
+
`$logFile = '${logPath}'`,
|
|
250
|
+
`function Log($msg) { "$(Get-Date -Format o) $msg" | Out-File -Append $logFile }`,
|
|
251
|
+
`Log 'Restart-all started'`,
|
|
252
|
+
`try {`,
|
|
253
|
+
` Log 'Restarting display services...'`,
|
|
254
|
+
` try { & $nssm restart minion-vnc 2>&1 | ForEach-Object { Log "minion-vnc: $_" } } catch { Log "minion-vnc restart failed: $_" }`,
|
|
255
|
+
` try { & $nssm restart minion-websockify 2>&1 | ForEach-Object { Log "minion-websockify: $_" } } catch { Log "minion-websockify restart failed: $_" }`,
|
|
256
|
+
` Log 'Display services restarted'`,
|
|
257
|
+
` Log 'Requesting graceful shutdown of agent...'`,
|
|
258
|
+
gracefulStop,
|
|
259
|
+
` Log 'Restarting agent service via NSSM...'`,
|
|
260
|
+
` & $nssm restart minion-agent`,
|
|
261
|
+
` Log 'Restart-all completed successfully'`,
|
|
262
|
+
`} catch {`,
|
|
263
|
+
` Log "Restart-all failed: $_"`,
|
|
264
|
+
` Log 'Attempting to start agent service anyway...'`,
|
|
265
|
+
` & $nssm start minion-agent`,
|
|
266
|
+
`} finally {`,
|
|
267
|
+
` Log 'Cleaning up updater service...'`,
|
|
268
|
+
` & $nssm stop minion-update confirm 2>$null`,
|
|
269
|
+
` & $nssm remove minion-update confirm 2>$null`,
|
|
270
|
+
`}`,
|
|
271
|
+
].join('\n')
|
|
272
|
+
|
|
273
|
+
try { fs.mkdirSync(dataDir, { recursive: true }) } catch { /* exists */ }
|
|
274
|
+
fs.writeFileSync(scriptPath, ps1, 'utf-8')
|
|
275
|
+
|
|
276
|
+
return scriptPath
|
|
277
|
+
}
|
|
278
|
+
|
|
223
279
|
/**
|
|
224
280
|
* Build allowed commands for NSSM-based service management.
|
|
225
281
|
*
|
|
@@ -271,6 +327,12 @@ function buildAllowedCommands(_procMgr, agentConfig = {}) {
|
|
|
271
327
|
command: `"${nssmPath}" restart minion-vnc & "${nssmPath}" restart minion-websockify`,
|
|
272
328
|
}
|
|
273
329
|
|
|
330
|
+
commands['restart-all'] = {
|
|
331
|
+
description: 'Restart all services (display + agent)',
|
|
332
|
+
nssmService: { scriptPath: buildRestartAllScript(nssmPath, agentPort, apiToken) },
|
|
333
|
+
deferred: true,
|
|
334
|
+
}
|
|
335
|
+
|
|
274
336
|
commands['restart-tunnel'] = {
|
|
275
337
|
description: 'Restart Cloudflare tunnel service',
|
|
276
338
|
command: `"${nssmPath}" restart minion-cloudflared`,
|
package/win/server.js
CHANGED
|
@@ -34,6 +34,7 @@ const { getConfigWarnings } = require('../core/lib/config-warnings')
|
|
|
34
34
|
|
|
35
35
|
// Pull-model daemons (from core/)
|
|
36
36
|
const stepPoller = require('../core/lib/step-poller')
|
|
37
|
+
const dagStepPoller = require('../core/lib/dag-step-poller')
|
|
37
38
|
const revisionWatcher = require('../core/lib/revision-watcher')
|
|
38
39
|
const reflectionScheduler = require('../core/lib/reflection-scheduler')
|
|
39
40
|
const threadWatcher = require('../core/lib/thread-watcher')
|
|
@@ -94,6 +95,7 @@ async function shutdown(signal) {
|
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
stepPoller.stop()
|
|
98
|
+
dagStepPoller.stop()
|
|
97
99
|
revisionWatcher.stop()
|
|
98
100
|
reflectionScheduler.stop()
|
|
99
101
|
threadWatcher.stop()
|
|
@@ -358,6 +360,7 @@ async function start() {
|
|
|
358
360
|
|
|
359
361
|
// Start Pull-model daemons
|
|
360
362
|
stepPoller.start()
|
|
363
|
+
dagStepPoller.start()
|
|
361
364
|
revisionWatcher.start()
|
|
362
365
|
threadWatcher.start(runQuickLlmCall)
|
|
363
366
|
} else {
|