@geekbeer/minion 2.62.0 → 2.67.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/api.js +110 -0
- package/core/lib/config-warnings.js +85 -0
- package/core/lib/reflection-scheduler.js +1 -5
- package/core/lib/revision-watcher.js +12 -1
- package/core/lib/step-poller.js +12 -1
- package/core/lib/thread-watcher.js +359 -0
- package/core/routes/daemons.js +34 -0
- package/core/routes/help-threads.js +189 -0
- package/docs/api-reference.md +195 -0
- package/linux/minion-cli.sh +6 -0
- package/linux/server.js +15 -2
- package/package.json +1 -1
- package/win/lib/process-manager.js +6 -3
- package/win/minion-cli.ps1 +12 -2
- package/win/server.js +15 -2
package/core/api.js
CHANGED
|
@@ -86,10 +86,120 @@ async function sendHeartbeat(data) {
|
|
|
86
86
|
})
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Create a project thread (help or discussion) on HQ.
|
|
91
|
+
* @param {object} data - { project_id, title, description, thread_type?, mentions?, context? }
|
|
92
|
+
* @returns {Promise<{ thread: object }>}
|
|
93
|
+
*/
|
|
94
|
+
async function createHelpThread(data) {
|
|
95
|
+
return request('/help-threads', {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
body: JSON.stringify(data),
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get open help threads in this minion's projects.
|
|
103
|
+
* @returns {Promise<{ threads: object[] }>}
|
|
104
|
+
*/
|
|
105
|
+
async function getOpenHelpThreads() {
|
|
106
|
+
return request('/help-threads/open')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get a help thread by ID with messages.
|
|
111
|
+
* @param {string} threadId
|
|
112
|
+
* @returns {Promise<{ thread: object, messages: object[] }>}
|
|
113
|
+
*/
|
|
114
|
+
async function getHelpThread(threadId) {
|
|
115
|
+
return request(`/help-threads/${threadId}`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Post a message to a help thread.
|
|
120
|
+
* @param {string} threadId
|
|
121
|
+
* @param {object} data - { content, attachments?, mentions? }
|
|
122
|
+
*/
|
|
123
|
+
async function postHelpMessage(threadId, data) {
|
|
124
|
+
return request(`/help-threads/${threadId}/messages`, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
body: JSON.stringify(data),
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Resolve a help thread.
|
|
132
|
+
* @param {string} threadId
|
|
133
|
+
* @param {string} resolution
|
|
134
|
+
*/
|
|
135
|
+
async function resolveHelpThread(threadId, resolution) {
|
|
136
|
+
return request(`/help-threads/${threadId}/resolve`, {
|
|
137
|
+
method: 'PATCH',
|
|
138
|
+
body: JSON.stringify({ resolution }),
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Cancel a help thread.
|
|
144
|
+
* @param {string} threadId
|
|
145
|
+
* @param {string} [reason]
|
|
146
|
+
*/
|
|
147
|
+
async function cancelHelpThread(threadId, reason) {
|
|
148
|
+
return request(`/help-threads/${threadId}/cancel`, {
|
|
149
|
+
method: 'PATCH',
|
|
150
|
+
body: JSON.stringify({ reason: reason || null }),
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Permanently delete a help thread (PM only).
|
|
156
|
+
* @param {string} threadId
|
|
157
|
+
*/
|
|
158
|
+
async function deleteHelpThread(threadId) {
|
|
159
|
+
return request(`/help-threads/${threadId}`, {
|
|
160
|
+
method: 'DELETE',
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Create a project memory entry.
|
|
166
|
+
* @param {object} data - { project_id, title, content, category, tags?, source_thread_id? }
|
|
167
|
+
*/
|
|
168
|
+
async function createProjectMemory(data) {
|
|
169
|
+
return request('/project-memories', {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
body: JSON.stringify(data),
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Search project memories.
|
|
177
|
+
* @param {string} projectId
|
|
178
|
+
* @param {object} [opts] - { search?, category?, tags?, status? }
|
|
179
|
+
*/
|
|
180
|
+
async function searchProjectMemories(projectId, opts = {}) {
|
|
181
|
+
const params = new URLSearchParams({ project_id: projectId })
|
|
182
|
+
if (opts.search) params.set('search', opts.search)
|
|
183
|
+
if (opts.category) params.set('category', opts.category)
|
|
184
|
+
if (opts.tags) params.set('tags', opts.tags.join(','))
|
|
185
|
+
if (opts.status) params.set('status', opts.status)
|
|
186
|
+
return request(`/project-memories?${params.toString()}`)
|
|
187
|
+
}
|
|
188
|
+
|
|
89
189
|
module.exports = {
|
|
90
190
|
request,
|
|
91
191
|
reportExecution,
|
|
92
192
|
reportStepComplete,
|
|
93
193
|
reportIssue,
|
|
94
194
|
sendHeartbeat,
|
|
195
|
+
createHelpThread,
|
|
196
|
+
getOpenHelpThreads,
|
|
197
|
+
getHelpThread,
|
|
198
|
+
postHelpMessage,
|
|
199
|
+
resolveHelpThread,
|
|
200
|
+
|
|
201
|
+
cancelHelpThread,
|
|
202
|
+
deleteHelpThread,
|
|
203
|
+
createProjectMemory,
|
|
204
|
+
searchProjectMemories,
|
|
95
205
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Warnings - checks for missing required configurations
|
|
3
|
+
*
|
|
4
|
+
* Computes a list of warning objects that indicate missing or misconfigured
|
|
5
|
+
* settings that prevent the minion from functioning properly.
|
|
6
|
+
* Results are included in heartbeat payloads so HQ can display them on the dashboard.
|
|
7
|
+
*
|
|
8
|
+
* Warning categories:
|
|
9
|
+
* - llm_not_authenticated: No LLM service is authenticated (Claude/Gemini/Codex)
|
|
10
|
+
* - llm_command_not_set: LLM_COMMAND is not configured (required for workflow/routine execution)
|
|
11
|
+
* - tunnel_not_running: Cloudflare tunnel is not running (required for VNC/terminal access)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { execSync } = require('child_process')
|
|
15
|
+
const { config } = require('../config')
|
|
16
|
+
const { getLlmServices, isLlmCommandConfigured } = require('./llm-checker')
|
|
17
|
+
const { IS_WINDOWS } = require('./platform')
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if Cloudflare tunnel is running.
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
function isTunnelRunning() {
|
|
24
|
+
try {
|
|
25
|
+
if (IS_WINDOWS) {
|
|
26
|
+
const out = execSync('tasklist /FI "IMAGENAME eq cloudflared.exe" /NH', {
|
|
27
|
+
encoding: 'utf-8',
|
|
28
|
+
timeout: 5000,
|
|
29
|
+
stdio: 'pipe',
|
|
30
|
+
})
|
|
31
|
+
return out.toLowerCase().includes('cloudflared.exe')
|
|
32
|
+
} else {
|
|
33
|
+
execSync('pgrep -f "cloudflared"', {
|
|
34
|
+
encoding: 'utf-8',
|
|
35
|
+
timeout: 5000,
|
|
36
|
+
stdio: 'pipe',
|
|
37
|
+
})
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compute config warnings for the current minion state.
|
|
47
|
+
* Each warning is an object with:
|
|
48
|
+
* - code: string identifier (e.g. 'llm_not_authenticated')
|
|
49
|
+
* - message: human-readable description (Japanese)
|
|
50
|
+
*
|
|
51
|
+
* @returns {{ code: string, message: string }[]}
|
|
52
|
+
*/
|
|
53
|
+
function getConfigWarnings() {
|
|
54
|
+
const warnings = []
|
|
55
|
+
|
|
56
|
+
// Check LLM authentication
|
|
57
|
+
const services = getLlmServices()
|
|
58
|
+
const hasAuthenticatedLlm = services.some(s => s.authenticated)
|
|
59
|
+
if (!hasAuthenticatedLlm) {
|
|
60
|
+
warnings.push({
|
|
61
|
+
code: 'llm_not_authenticated',
|
|
62
|
+
message: 'LLM\u304C\u672A\u8A8D\u8A3C\u3067\u3059\u3002Claude/Gemini/Codex\u306E\u3044\u305A\u308C\u304B\u3092\u8A8D\u8A3C\u3057\u3066\u304F\u3060\u3055\u3044',
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check LLM_COMMAND
|
|
67
|
+
if (!isLlmCommandConfigured()) {
|
|
68
|
+
warnings.push({
|
|
69
|
+
code: 'llm_command_not_set',
|
|
70
|
+
message: 'LLM_COMMAND\u304C\u672A\u8A2D\u5B9A\u3067\u3059\u3002\u30EF\u30FC\u30AF\u30D5\u30ED\u30FC/\u30EB\u30FC\u30C6\u30A3\u30F3\u5B9F\u884C\u306B\u5FC5\u8981\u3067\u3059',
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check tunnel (only for HQ-connected minions)
|
|
75
|
+
if (config.HQ_URL && !isTunnelRunning()) {
|
|
76
|
+
warnings.push({
|
|
77
|
+
code: 'tunnel_not_running',
|
|
78
|
+
message: 'Cloudflare\u30C8\u30F3\u30CD\u30EB\u304C\u505C\u6B62\u3057\u3066\u3044\u307E\u3059\u3002VNC/\u30BF\u30FC\u30DF\u30CA\u30EB\u63A5\u7D9A\u304C\u3067\u304D\u307E\u305B\u3093',
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return warnings
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { getConfigWarnings }
|
|
@@ -156,13 +156,9 @@ async function runReflection() {
|
|
|
156
156
|
* @returns {{ enabled: boolean, reflection_time: string, timezone: string, next_run: string|null }}
|
|
157
157
|
*/
|
|
158
158
|
function getStatus() {
|
|
159
|
-
const reflectionTime = config.REFLECTION_TIME || ''
|
|
160
159
|
const nextRun = cronJob ? cronJob.nextRun() : null
|
|
161
|
-
|
|
162
160
|
return {
|
|
163
|
-
|
|
164
|
-
reflection_time: reflectionTime,
|
|
165
|
-
timezone: getSystemTimezone(),
|
|
161
|
+
running: !!cronJob,
|
|
166
162
|
next_run: nextRun ? nextRun.toISOString() : null,
|
|
167
163
|
}
|
|
168
164
|
}
|
|
@@ -22,6 +22,9 @@ const POLL_INTERVAL_MS = 30_000
|
|
|
22
22
|
let polling = false
|
|
23
23
|
let pollTimer = null
|
|
24
24
|
|
|
25
|
+
// Last successful poll timestamp
|
|
26
|
+
let lastPollAt = null
|
|
27
|
+
|
|
25
28
|
// Track revisions being processed to avoid duplicate handling
|
|
26
29
|
const processingRevisions = new Set()
|
|
27
30
|
|
|
@@ -63,6 +66,7 @@ async function pollOnce() {
|
|
|
63
66
|
}
|
|
64
67
|
} finally {
|
|
65
68
|
polling = false
|
|
69
|
+
lastPollAt = new Date().toISOString()
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
72
|
|
|
@@ -249,4 +253,11 @@ function stop() {
|
|
|
249
253
|
}
|
|
250
254
|
}
|
|
251
255
|
|
|
252
|
-
|
|
256
|
+
function getStatus() {
|
|
257
|
+
return {
|
|
258
|
+
running: pollTimer !== null,
|
|
259
|
+
last_poll_at: lastPollAt,
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = { start, stop, pollOnce, getStatus }
|
package/core/lib/step-poller.js
CHANGED
|
@@ -33,6 +33,9 @@ let pollTimer = null
|
|
|
33
33
|
// Track currently executing step to avoid double-dispatch
|
|
34
34
|
let activeStepExecutionId = null
|
|
35
35
|
|
|
36
|
+
// Last successful poll timestamp
|
|
37
|
+
let lastPollAt = null
|
|
38
|
+
|
|
36
39
|
/**
|
|
37
40
|
* Poll HQ for pending steps and execute them.
|
|
38
41
|
*/
|
|
@@ -73,6 +76,7 @@ async function pollOnce() {
|
|
|
73
76
|
}
|
|
74
77
|
} finally {
|
|
75
78
|
polling = false
|
|
79
|
+
lastPollAt = new Date().toISOString()
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
|
|
@@ -224,4 +228,11 @@ function stop() {
|
|
|
224
228
|
}
|
|
225
229
|
}
|
|
226
230
|
|
|
227
|
-
|
|
231
|
+
function getStatus() {
|
|
232
|
+
return {
|
|
233
|
+
running: pollTimer !== null,
|
|
234
|
+
last_poll_at: lastPollAt,
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = { start, stop, pollOnce, getStatus }
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Thread Watcher
|
|
3
|
+
*
|
|
4
|
+
* Polling daemon that monitors open help/discussion threads in the minion's
|
|
5
|
+
* projects. Uses LLM to decide whether to participate in conversations.
|
|
6
|
+
*
|
|
7
|
+
* Token-saving strategies:
|
|
8
|
+
* 1. Read tracking: only evaluate threads with new messages since last check
|
|
9
|
+
* 2. Mention targeting: if thread/message has mentions, only targeted minions evaluate
|
|
10
|
+
* 3. Rate limiting: max 1 LLM evaluation per thread per 5 minutes
|
|
11
|
+
*
|
|
12
|
+
* Flow per poll cycle:
|
|
13
|
+
* 1. GET /api/minion/help-threads/open → list of open threads
|
|
14
|
+
* 2. For each thread with new activity:
|
|
15
|
+
* a. Check mentions → if mentioned, must evaluate
|
|
16
|
+
* b. Check read state → skip if no new messages
|
|
17
|
+
* c. Fetch thread detail + messages
|
|
18
|
+
* d. LLM evaluates relevance + generates response if appropriate
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const { config, isHqConfigured, isLlmConfigured } = require('../config')
|
|
22
|
+
const api = require('../api')
|
|
23
|
+
|
|
24
|
+
// Poll every 15 seconds
|
|
25
|
+
const POLL_INTERVAL_MS = 15_000
|
|
26
|
+
|
|
27
|
+
// Minimum interval between LLM evaluations for the same thread (5 minutes)
|
|
28
|
+
const EVAL_COOLDOWN_MS = 5 * 60 * 1000
|
|
29
|
+
|
|
30
|
+
// Prevent concurrent poll cycles
|
|
31
|
+
let polling = false
|
|
32
|
+
let pollTimer = null
|
|
33
|
+
|
|
34
|
+
// Injected LLM function (set via start())
|
|
35
|
+
let llmCallFn = null
|
|
36
|
+
|
|
37
|
+
// Last successful poll timestamp
|
|
38
|
+
let lastPollAt = null
|
|
39
|
+
|
|
40
|
+
// Read tracking: threadId → { lastMessageCount, lastEvalAt }
|
|
41
|
+
const readState = new Map()
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get minion's role and project memberships for context.
|
|
45
|
+
* Cached per poll cycle.
|
|
46
|
+
*/
|
|
47
|
+
let cachedProjects = null
|
|
48
|
+
let cachedProjectsAt = 0
|
|
49
|
+
|
|
50
|
+
async function getMyProjects() {
|
|
51
|
+
// Cache for 60 seconds
|
|
52
|
+
if (cachedProjects && Date.now() - cachedProjectsAt < 60_000) {
|
|
53
|
+
return cachedProjects
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const data = await api.request('/me/projects')
|
|
57
|
+
cachedProjects = data.projects || []
|
|
58
|
+
cachedProjectsAt = Date.now()
|
|
59
|
+
return cachedProjects
|
|
60
|
+
} catch {
|
|
61
|
+
return cachedProjects || []
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if this minion is mentioned in a thread or its messages.
|
|
67
|
+
* Mention formats: 'role:engineer', 'role:pm', 'minion:<minion_id>'
|
|
68
|
+
*
|
|
69
|
+
* @param {object} thread
|
|
70
|
+
* @param {object[]} messages - Only new messages to check
|
|
71
|
+
* @param {string} myRole - This minion's role in the project
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
function isMentioned(thread, messages, myRole) {
|
|
75
|
+
const myMinionId = config.MINION_ID
|
|
76
|
+
const targets = [
|
|
77
|
+
`role:${myRole}`,
|
|
78
|
+
`minion:${myMinionId}`,
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
// Check thread-level mentions
|
|
82
|
+
if (thread.mentions?.some(m => targets.includes(m))) return true
|
|
83
|
+
|
|
84
|
+
// Check message-level mentions
|
|
85
|
+
for (const msg of messages) {
|
|
86
|
+
if (msg.mentions?.some(m => targets.includes(m))) return true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Poll for open threads and evaluate new activity.
|
|
94
|
+
*/
|
|
95
|
+
async function pollOnce() {
|
|
96
|
+
if (!isHqConfigured()) return
|
|
97
|
+
if (polling) return
|
|
98
|
+
|
|
99
|
+
polling = true
|
|
100
|
+
try {
|
|
101
|
+
const data = await api.request('/help-threads/open')
|
|
102
|
+
|
|
103
|
+
if (!data.threads || data.threads.length === 0) {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const now = Date.now()
|
|
108
|
+
const myProjects = await getMyProjects()
|
|
109
|
+
|
|
110
|
+
for (const thread of data.threads) {
|
|
111
|
+
try {
|
|
112
|
+
await processThread(thread, myProjects, now)
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error(`[ThreadWatcher] Error processing thread ${thread.id}: ${err.message}`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (err.message?.includes('fetch failed') || err.message?.includes('ECONNREFUSED')) {
|
|
119
|
+
// HQ unreachable — silent retry
|
|
120
|
+
} else {
|
|
121
|
+
console.error(`[ThreadWatcher] Poll error: ${err.message}`)
|
|
122
|
+
}
|
|
123
|
+
} finally {
|
|
124
|
+
polling = false
|
|
125
|
+
lastPollAt = new Date().toISOString()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Process a single thread: check for new activity and evaluate if needed.
|
|
131
|
+
*/
|
|
132
|
+
async function processThread(thread, myProjects, now) {
|
|
133
|
+
// Find my role in this project
|
|
134
|
+
const myProject = myProjects.find(p => p.id === thread.project_id)
|
|
135
|
+
if (!myProject) return // Not a member
|
|
136
|
+
|
|
137
|
+
const state = readState.get(thread.id) || { lastMessageCount: 0, lastEvalAt: 0 }
|
|
138
|
+
|
|
139
|
+
// Fetch thread detail to get message count and check for new messages
|
|
140
|
+
let detail
|
|
141
|
+
try {
|
|
142
|
+
detail = await api.request(`/help-threads/${thread.id}`)
|
|
143
|
+
} catch {
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const messages = detail.messages || []
|
|
148
|
+
const messageCount = messages.length
|
|
149
|
+
|
|
150
|
+
// Skip if no new messages since last check
|
|
151
|
+
if (messageCount <= state.lastMessageCount) {
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// New messages detected
|
|
156
|
+
const newMessages = messages.slice(state.lastMessageCount)
|
|
157
|
+
|
|
158
|
+
// Update read state (message count only — eval timestamp updated after LLM call)
|
|
159
|
+
readState.set(thread.id, { ...state, lastMessageCount: messageCount })
|
|
160
|
+
|
|
161
|
+
// Check mentions first (no LLM needed to decide relevance)
|
|
162
|
+
const mentioned = isMentioned(thread, newMessages, myProject.role)
|
|
163
|
+
|
|
164
|
+
// If not mentioned and cooldown not expired, skip LLM evaluation
|
|
165
|
+
if (!mentioned && now - state.lastEvalAt < EVAL_COOLDOWN_MS) {
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// If LLM is not configured, fall back to memory-only matching
|
|
170
|
+
if (!llmCallFn || !isLlmConfigured()) {
|
|
171
|
+
await fallbackMemoryMatch(thread, messages)
|
|
172
|
+
readState.set(thread.id, { lastMessageCount: messageCount, lastEvalAt: now })
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// LLM evaluation
|
|
177
|
+
await evaluateWithLlm(thread, detail.thread, messages, newMessages, myProject, mentioned)
|
|
178
|
+
readState.set(thread.id, { lastMessageCount: messageCount, lastEvalAt: now })
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Use LLM to evaluate thread and potentially respond.
|
|
183
|
+
*/
|
|
184
|
+
async function evaluateWithLlm(threadSummary, threadDetail, allMessages, newMessages, myProject, mentioned) {
|
|
185
|
+
// Build conversation context (limit to last 20 messages to control tokens)
|
|
186
|
+
const recentMessages = allMessages.slice(-20)
|
|
187
|
+
const messageHistory = recentMessages
|
|
188
|
+
.map(m => {
|
|
189
|
+
const sender = m.sender_minion_id
|
|
190
|
+
? (m.sender_minion_id === config.MINION_ID ? 'You' : `Minion(${m.sender_minion_id.slice(0, 8)})`)
|
|
191
|
+
: 'User'
|
|
192
|
+
return `[${sender}] ${m.content}`
|
|
193
|
+
})
|
|
194
|
+
.join('\n')
|
|
195
|
+
|
|
196
|
+
const threadType = threadDetail.thread_type || 'help'
|
|
197
|
+
const mentionNote = mentioned
|
|
198
|
+
? '\n\nあなたはこのスレッドでメンションされています。必ず返信してください。'
|
|
199
|
+
: ''
|
|
200
|
+
|
|
201
|
+
// Extract optional context metadata
|
|
202
|
+
const ctx = threadDetail.context || {}
|
|
203
|
+
const contextInfo = [
|
|
204
|
+
ctx.category ? `カテゴリ: ${ctx.category}` : '',
|
|
205
|
+
ctx.attempted_resolution ? `試行済み: ${ctx.attempted_resolution}` : '',
|
|
206
|
+
].filter(Boolean).join('\n')
|
|
207
|
+
|
|
208
|
+
const prompt = `あなたはプロジェクト「${myProject.name}」のチームメンバー(ロール: ${myProject.role})です。
|
|
209
|
+
以下のスレッドに対して、あなたが返信すべきかどうかを判断し、返信する場合はその内容を生成してください。
|
|
210
|
+
|
|
211
|
+
スレッドタイプ: ${threadType}
|
|
212
|
+
タイトル: ${threadDetail.title}
|
|
213
|
+
説明: ${threadDetail.description}
|
|
214
|
+
${contextInfo}${mentionNote}
|
|
215
|
+
|
|
216
|
+
--- メッセージ履歴 ---
|
|
217
|
+
${messageHistory || '(メッセージなし)'}
|
|
218
|
+
--- 履歴ここまで ---
|
|
219
|
+
|
|
220
|
+
以下のJSON形式で回答してください:
|
|
221
|
+
{
|
|
222
|
+
"should_respond": true/false,
|
|
223
|
+
"reason": "判断の理由(1行)",
|
|
224
|
+
"response": "返信内容(should_respondがtrueの場合のみ)"
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
判断基準:
|
|
228
|
+
- 自分のロール(${myProject.role})に関連する話題か
|
|
229
|
+
- 自分が貢献できる知見や意見があるか
|
|
230
|
+
- 既に十分な回答がある場合は重複を避ける
|
|
231
|
+
- メンションされている場合は必ず返信する
|
|
232
|
+
- 人間に聞くべき場合は @user メンションを含めて返信する`
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const result = await llmCallFn(prompt)
|
|
236
|
+
|
|
237
|
+
let parsed
|
|
238
|
+
try {
|
|
239
|
+
// LLM may return JSON wrapped in markdown code block
|
|
240
|
+
const cleaned = result.replace(/```json?\n?/g, '').replace(/```/g, '').trim()
|
|
241
|
+
parsed = JSON.parse(cleaned)
|
|
242
|
+
} catch {
|
|
243
|
+
console.log(`[ThreadWatcher] LLM returned non-JSON for thread ${threadSummary.id}, skipping`)
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (parsed.should_respond && parsed.response) {
|
|
248
|
+
// Check if the response contains @user mention
|
|
249
|
+
const mentions = []
|
|
250
|
+
if (parsed.response.includes('@user')) {
|
|
251
|
+
mentions.push('user')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
await api.request(`/help-threads/${threadSummary.id}/messages`, {
|
|
255
|
+
method: 'POST',
|
|
256
|
+
body: JSON.stringify({
|
|
257
|
+
content: parsed.response,
|
|
258
|
+
mentions,
|
|
259
|
+
}),
|
|
260
|
+
})
|
|
261
|
+
console.log(`[ThreadWatcher] Responded to thread "${threadDetail.title}" (reason: ${parsed.reason})`)
|
|
262
|
+
} else {
|
|
263
|
+
console.log(`[ThreadWatcher] Skipped thread "${threadDetail.title}" (reason: ${parsed.reason || 'not relevant'})`)
|
|
264
|
+
}
|
|
265
|
+
} catch (err) {
|
|
266
|
+
console.error(`[ThreadWatcher] LLM evaluation failed for thread ${threadSummary.id}: ${err.message}`)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Fallback: memory-only matching when LLM is not available.
|
|
272
|
+
*/
|
|
273
|
+
async function fallbackMemoryMatch(thread) {
|
|
274
|
+
try {
|
|
275
|
+
const ctx = thread.context || {}
|
|
276
|
+
const category = ctx.category || ''
|
|
277
|
+
const searchParam = category ? `&category=${category}` : ''
|
|
278
|
+
const memData = await api.request(
|
|
279
|
+
`/project-memories?project_id=${thread.project_id}${searchParam}`
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
if (!memData.memories || memData.memories.length === 0) return
|
|
283
|
+
|
|
284
|
+
const keywords = (thread.title + ' ' + thread.description)
|
|
285
|
+
.toLowerCase()
|
|
286
|
+
.split(/[\s,.\-_/]+/)
|
|
287
|
+
.filter(w => w.length >= 3)
|
|
288
|
+
.filter((w, i, arr) => arr.indexOf(w) === i)
|
|
289
|
+
.slice(0, 10)
|
|
290
|
+
|
|
291
|
+
const relevant = memData.memories.filter(m =>
|
|
292
|
+
keywords.some(kw =>
|
|
293
|
+
m.title.toLowerCase().includes(kw) ||
|
|
294
|
+
m.content.toLowerCase().includes(kw)
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if (relevant.length === 0) return
|
|
299
|
+
|
|
300
|
+
const knowledgeSummary = relevant
|
|
301
|
+
.map(m => `[${m.title}] ${m.content}`)
|
|
302
|
+
.join('\n\n')
|
|
303
|
+
|
|
304
|
+
await api.request(`/help-threads/${thread.id}/messages`, {
|
|
305
|
+
method: 'POST',
|
|
306
|
+
body: JSON.stringify({
|
|
307
|
+
content: `関連するプロジェクトメモリーが見つかりました:\n\n${knowledgeSummary}`,
|
|
308
|
+
}),
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
console.log(`[ThreadWatcher] Posted ${relevant.length} relevant memories for thread ${thread.id}`)
|
|
312
|
+
} catch (err) {
|
|
313
|
+
console.error(`[ThreadWatcher] Memory check failed for thread ${thread.id}: ${err.message}`)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Start the thread watcher daemon.
|
|
319
|
+
* @param {Function} runQuickLlmCall - LLM function for evaluating threads
|
|
320
|
+
*/
|
|
321
|
+
function start(runQuickLlmCall) {
|
|
322
|
+
llmCallFn = runQuickLlmCall || null
|
|
323
|
+
|
|
324
|
+
if (!isHqConfigured()) {
|
|
325
|
+
console.log('[ThreadWatcher] HQ not configured, thread watcher disabled')
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!llmCallFn) {
|
|
330
|
+
console.log('[ThreadWatcher] LLM not available, using memory-only matching')
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Initial poll after a short delay
|
|
334
|
+
setTimeout(() => pollOnce(), 8000)
|
|
335
|
+
|
|
336
|
+
// Periodic polling
|
|
337
|
+
pollTimer = setInterval(() => pollOnce(), POLL_INTERVAL_MS)
|
|
338
|
+
console.log(`[ThreadWatcher] Started (polling every ${POLL_INTERVAL_MS / 1000}s)`)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Stop the thread watcher daemon.
|
|
343
|
+
*/
|
|
344
|
+
function stop() {
|
|
345
|
+
if (pollTimer) {
|
|
346
|
+
clearInterval(pollTimer)
|
|
347
|
+
pollTimer = null
|
|
348
|
+
console.log('[ThreadWatcher] Stopped')
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function getStatus() {
|
|
353
|
+
return {
|
|
354
|
+
running: pollTimer !== null,
|
|
355
|
+
last_poll_at: lastPollAt,
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
module.exports = { start, stop, pollOnce, getStatus }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon status route
|
|
3
|
+
*
|
|
4
|
+
* GET /api/daemons/status - Returns the status of all background daemons
|
|
5
|
+
*
|
|
6
|
+
* No auth required (local CLI access, same as /api/health and /api/status).
|
|
7
|
+
* Requires opts.heartbeatStatus (injected from server.js) for heartbeat state.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const stepPoller = require('../lib/step-poller')
|
|
11
|
+
const revisionWatcher = require('../lib/revision-watcher')
|
|
12
|
+
const threadWatcher = require('../lib/thread-watcher')
|
|
13
|
+
const reflectionScheduler = require('../lib/reflection-scheduler')
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {import('fastify').FastifyInstance} fastify
|
|
17
|
+
* @param {{ heartbeatStatus: () => object }} opts
|
|
18
|
+
*/
|
|
19
|
+
async function daemonRoutes(fastify, opts) {
|
|
20
|
+
fastify.get('/api/daemons/status', async () => {
|
|
21
|
+
return {
|
|
22
|
+
success: true,
|
|
23
|
+
daemons: {
|
|
24
|
+
step_poller: stepPoller.getStatus(),
|
|
25
|
+
revision_watcher: revisionWatcher.getStatus(),
|
|
26
|
+
thread_watcher: threadWatcher.getStatus(),
|
|
27
|
+
reflection_scheduler: reflectionScheduler.getStatus(),
|
|
28
|
+
heartbeat: opts.heartbeatStatus ? opts.heartbeatStatus() : { running: false },
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { daemonRoutes }
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project thread routes (local API on minion)
|
|
3
|
+
*
|
|
4
|
+
* Endpoints:
|
|
5
|
+
* - GET /api/help-threads - List open threads in minion's projects
|
|
6
|
+
* - POST /api/help-threads - Create a new thread (proxied to HQ)
|
|
7
|
+
* - GET /api/help-threads/:id - Get thread detail with messages
|
|
8
|
+
* - POST /api/help-threads/:id/messages - Post a message to a thread
|
|
9
|
+
* - POST /api/help-threads/:id/resolve - Resolve a thread
|
|
10
|
+
* - POST /api/help-threads/:id/cancel - Cancel a thread
|
|
11
|
+
* - DELETE /api/help-threads/:id - Permanently delete a thread (PM only)
|
|
12
|
+
* - GET /api/project-memories - Search project memories
|
|
13
|
+
* - POST /api/project-memories - Create a project memory
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { verifyToken } = require('../lib/auth')
|
|
17
|
+
const api = require('../api')
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {import('fastify').FastifyInstance} fastify
|
|
21
|
+
*/
|
|
22
|
+
async function helpThreadRoutes(fastify) {
|
|
23
|
+
// List open threads
|
|
24
|
+
fastify.get('/api/help-threads', async (request, reply) => {
|
|
25
|
+
if (!verifyToken(request)) {
|
|
26
|
+
reply.code(401)
|
|
27
|
+
return { success: false, error: 'Unauthorized' }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const data = await api.getOpenHelpThreads()
|
|
32
|
+
return { success: true, threads: data.threads || [] }
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error(`[HelpThreads] List error: ${error.message}`)
|
|
35
|
+
reply.code(500)
|
|
36
|
+
return { success: false, error: error.message }
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Create thread
|
|
41
|
+
fastify.post('/api/help-threads', async (request, reply) => {
|
|
42
|
+
if (!verifyToken(request)) {
|
|
43
|
+
reply.code(401)
|
|
44
|
+
return { success: false, error: 'Unauthorized' }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const data = await api.createHelpThread(request.body)
|
|
49
|
+
return { success: true, thread: data.thread }
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(`[HelpThreads] Create error: ${error.message}`)
|
|
52
|
+
reply.code(error.statusCode || 500)
|
|
53
|
+
return { success: false, error: error.message }
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Get thread detail
|
|
58
|
+
fastify.get('/api/help-threads/:id', async (request, reply) => {
|
|
59
|
+
if (!verifyToken(request)) {
|
|
60
|
+
reply.code(401)
|
|
61
|
+
return { success: false, error: 'Unauthorized' }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const data = await api.getHelpThread(request.params.id)
|
|
66
|
+
return { success: true, thread: data.thread, messages: data.messages }
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(`[HelpThreads] Get error: ${error.message}`)
|
|
69
|
+
reply.code(error.statusCode || 500)
|
|
70
|
+
return { success: false, error: error.message }
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// Post message
|
|
75
|
+
fastify.post('/api/help-threads/:id/messages', async (request, reply) => {
|
|
76
|
+
if (!verifyToken(request)) {
|
|
77
|
+
reply.code(401)
|
|
78
|
+
return { success: false, error: 'Unauthorized' }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const data = await api.postHelpMessage(request.params.id, request.body)
|
|
83
|
+
return { success: true, message: data.message }
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(`[HelpThreads] Message error: ${error.message}`)
|
|
86
|
+
reply.code(error.statusCode || 500)
|
|
87
|
+
return { success: false, error: error.message }
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Resolve thread
|
|
92
|
+
fastify.post('/api/help-threads/:id/resolve', async (request, reply) => {
|
|
93
|
+
if (!verifyToken(request)) {
|
|
94
|
+
reply.code(401)
|
|
95
|
+
return { success: false, error: 'Unauthorized' }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const { resolution } = request.body || {}
|
|
100
|
+
const data = await api.resolveHelpThread(request.params.id, resolution)
|
|
101
|
+
return { success: true, thread: data.thread }
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error(`[HelpThreads] Resolve error: ${error.message}`)
|
|
104
|
+
reply.code(error.statusCode || 500)
|
|
105
|
+
return { success: false, error: error.message }
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// Permanently delete thread (PM only)
|
|
110
|
+
fastify.delete('/api/help-threads/:id', async (request, reply) => {
|
|
111
|
+
if (!verifyToken(request)) {
|
|
112
|
+
reply.code(401)
|
|
113
|
+
return { success: false, error: 'Unauthorized' }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const data = await api.deleteHelpThread(request.params.id)
|
|
118
|
+
return { success: true, deleted: data.deleted }
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error(`[HelpThreads] Delete error: ${error.message}`)
|
|
121
|
+
reply.code(error.statusCode || 500)
|
|
122
|
+
return { success: false, error: error.message }
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// Cancel thread
|
|
127
|
+
fastify.post('/api/help-threads/:id/cancel', async (request, reply) => {
|
|
128
|
+
if (!verifyToken(request)) {
|
|
129
|
+
reply.code(401)
|
|
130
|
+
return { success: false, error: 'Unauthorized' }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const { reason } = request.body || {}
|
|
135
|
+
const data = await api.cancelHelpThread(request.params.id, reason)
|
|
136
|
+
return { success: true, thread: data.thread }
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error(`[HelpThreads] Cancel error: ${error.message}`)
|
|
139
|
+
reply.code(error.statusCode || 500)
|
|
140
|
+
return { success: false, error: error.message }
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// Search project memories
|
|
145
|
+
fastify.get('/api/project-memories', async (request, reply) => {
|
|
146
|
+
if (!verifyToken(request)) {
|
|
147
|
+
reply.code(401)
|
|
148
|
+
return { success: false, error: 'Unauthorized' }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const { project_id, search, category, tags, status } = request.query || {}
|
|
153
|
+
if (!project_id) {
|
|
154
|
+
reply.code(400)
|
|
155
|
+
return { success: false, error: 'project_id is required' }
|
|
156
|
+
}
|
|
157
|
+
const data = await api.searchProjectMemories(project_id, {
|
|
158
|
+
search,
|
|
159
|
+
category,
|
|
160
|
+
tags: tags ? tags.split(',') : undefined,
|
|
161
|
+
status,
|
|
162
|
+
})
|
|
163
|
+
return { success: true, memories: data.memories || [] }
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error(`[ProjectMemories] Search error: ${error.message}`)
|
|
166
|
+
reply.code(error.statusCode || 500)
|
|
167
|
+
return { success: false, error: error.message }
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// Create project memory
|
|
172
|
+
fastify.post('/api/project-memories', async (request, reply) => {
|
|
173
|
+
if (!verifyToken(request)) {
|
|
174
|
+
reply.code(401)
|
|
175
|
+
return { success: false, error: 'Unauthorized' }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const data = await api.createProjectMemory(request.body)
|
|
180
|
+
return { success: true, memory: data.memory }
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error(`[ProjectMemories] Create error: ${error.message}`)
|
|
183
|
+
reply.code(error.statusCode || 500)
|
|
184
|
+
return { success: false, error: error.message }
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = { helpThreadRoutes }
|
package/docs/api-reference.md
CHANGED
|
@@ -244,6 +244,174 @@ Supported `cli_type`: `claude-code`, `gemini`, `codex`
|
|
|
244
244
|
|
|
245
245
|
Note: Claude Code の場合、書き込み先は `settings.local.json`(サーバー再起動時に上書きされない)。
|
|
246
246
|
|
|
247
|
+
### Daemon Status (デーモン監視)
|
|
248
|
+
|
|
249
|
+
バックグラウンドデーモンの稼働状態を確認する。
|
|
250
|
+
|
|
251
|
+
| Method | Endpoint | Description |
|
|
252
|
+
|--------|----------|-------------|
|
|
253
|
+
| GET | `/api/daemons/status` | 全デーモンのステータス一覧 |
|
|
254
|
+
|
|
255
|
+
GET `/api/daemons/status` response:
|
|
256
|
+
```json
|
|
257
|
+
{
|
|
258
|
+
"success": true,
|
|
259
|
+
"daemons": {
|
|
260
|
+
"step_poller": { "running": true, "last_poll_at": "2026-03-19T15:30:00.000Z" },
|
|
261
|
+
"revision_watcher": { "running": true, "last_poll_at": "2026-03-19T15:30:05.000Z" },
|
|
262
|
+
"thread_watcher": { "running": true, "last_poll_at": "2026-03-19T15:29:52.000Z" },
|
|
263
|
+
"reflection_scheduler": { "running": true, "next_run": "2026-03-20T03:00:00.000Z" },
|
|
264
|
+
"heartbeat": { "running": true, "last_beat_at": "2026-03-19T15:30:10.000Z" }
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
| Daemon | 概要 |
|
|
270
|
+
|--------|------|
|
|
271
|
+
| `step_poller` | ワークフローステップの取得・実行(30秒間隔) |
|
|
272
|
+
| `revision_watcher` | リビジョン要求の検知(30秒間隔、PMのみ) |
|
|
273
|
+
| `thread_watcher` | プロジェクトスレッドの監視・LLM評価(15秒間隔) |
|
|
274
|
+
| `reflection_scheduler` | 1日1回の振り返り(cron) |
|
|
275
|
+
| `heartbeat` | HQへのハートビート(30秒間隔) |
|
|
276
|
+
|
|
277
|
+
### Project Threads (プロジェクトスレッド)
|
|
278
|
+
|
|
279
|
+
プロジェクト内のコミュニケーションチャネル。ブロッカー共有やチーム議論に使う。
|
|
280
|
+
リクエストは HQ にプロキシされる。
|
|
281
|
+
|
|
282
|
+
| Method | Endpoint | Description |
|
|
283
|
+
|--------|----------|-------------|
|
|
284
|
+
| GET | `/api/help-threads` | 参加プロジェクトのオープンスレッド一覧 |
|
|
285
|
+
| POST | `/api/help-threads` | スレッドを起票 |
|
|
286
|
+
| GET | `/api/help-threads/:id` | スレッド詳細 + メッセージ一覧 |
|
|
287
|
+
| POST | `/api/help-threads/:id/messages` | スレッドにメッセージを投稿 |
|
|
288
|
+
| POST | `/api/help-threads/:id/resolve` | スレッドを解決済みにする |
|
|
289
|
+
| POST | `/api/help-threads/:id/cancel` | スレッドをキャンセル |
|
|
290
|
+
| DELETE | `/api/help-threads/:id` | スレッドを完全削除(PMのみ) |
|
|
291
|
+
|
|
292
|
+
POST `/api/help-threads` body (ヘルプスレッド起票):
|
|
293
|
+
```json
|
|
294
|
+
{
|
|
295
|
+
"project_id": "uuid",
|
|
296
|
+
"thread_type": "help",
|
|
297
|
+
"title": "ランサーズの2FA認証コードが必要",
|
|
298
|
+
"description": "ログイン時に2段階認証を要求された。メールで届く6桁コードの入力が必要。",
|
|
299
|
+
"mentions": ["role:pm"],
|
|
300
|
+
"context": {
|
|
301
|
+
"category": "auth",
|
|
302
|
+
"attempted_resolution": "保存済み認証情報で再ログイン試行済み、2FA必須"
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
POST `/api/help-threads` body (ディスカッションスレッド起票):
|
|
308
|
+
```json
|
|
309
|
+
{
|
|
310
|
+
"project_id": "uuid",
|
|
311
|
+
"thread_type": "discussion",
|
|
312
|
+
"title": "デプロイ手順の確認",
|
|
313
|
+
"description": "本番デプロイ前にステージングで確認するフローに変えたい。意見ある?",
|
|
314
|
+
"mentions": ["role:engineer"]
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
| Field | Type | Required | Description |
|
|
319
|
+
|-------|------|----------|-------------|
|
|
320
|
+
| `project_id` | string | Yes | プロジェクト UUID |
|
|
321
|
+
| `thread_type` | string | No | `help`(デフォルト)or `discussion` |
|
|
322
|
+
| `title` | string | Yes | スレッドの要約 |
|
|
323
|
+
| `description` | string | Yes | 詳細説明 |
|
|
324
|
+
| `mentions` | string[] | No | メンション対象。形式: `role:engineer`, `role:pm`, `minion:<minion_id>`, `user` |
|
|
325
|
+
| `context` | object | No | 任意のメタデータ(category, urgency, workflow_execution_id等) |
|
|
326
|
+
|
|
327
|
+
**thread_type の違い:**
|
|
328
|
+
- `help`: ブロッカー解決。`resolve` で解決
|
|
329
|
+
- `discussion`: チーム内ディスカッション。`close` で完了
|
|
330
|
+
|
|
331
|
+
**メンションルール:**
|
|
332
|
+
- メンションされたミニオンは優先的にスレッドを評価する
|
|
333
|
+
- メンションがない場合、全チームメンバーがLLMで関連性を判定してから参加
|
|
334
|
+
- トークン消費を抑えるため、当事者が明確な場合はメンションを推奨
|
|
335
|
+
- ミニオンが自力で解決できない場合、`@user` メンション付きで返信して人間に助けを求める
|
|
336
|
+
|
|
337
|
+
POST `/api/help-threads/:id/messages` body:
|
|
338
|
+
```json
|
|
339
|
+
{
|
|
340
|
+
"content": "関連するプロジェクトメモリーが見つかりました: ...",
|
|
341
|
+
"mentions": ["user"]
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
| Field | Type | Required | Description |
|
|
346
|
+
|-------|------|----------|-------------|
|
|
347
|
+
| `content` | string | Yes | メッセージ本文 |
|
|
348
|
+
| `attachments` | object | No | 添付ファイル情報(JSON) |
|
|
349
|
+
| `mentions` | string[] | No | メンション対象 |
|
|
350
|
+
|
|
351
|
+
POST `/api/help-threads/:id/resolve` body:
|
|
352
|
+
```json
|
|
353
|
+
{
|
|
354
|
+
"resolution": "ユーザーから2FAコード(123456)を受け取りログイン成功"
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
POST `/api/help-threads/:id/cancel` body (optional):
|
|
359
|
+
```json
|
|
360
|
+
{
|
|
361
|
+
"reason": "誤って起票したため取り消し"
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
スレッドのステータスは Supabase Realtime で配信されるため、ポーリングに加えて
|
|
366
|
+
HQ ダッシュボードではリアルタイムに更新が表示される。
|
|
367
|
+
|
|
368
|
+
### Project Memories (プロジェクト共有知識)
|
|
369
|
+
|
|
370
|
+
プロジェクトレベルのチーム共有知識ベース。ヘルプスレッドの解決知見を蓄積して再利用する。
|
|
371
|
+
リクエストは HQ にプロキシされる。
|
|
372
|
+
|
|
373
|
+
| Method | Endpoint | Description |
|
|
374
|
+
|--------|----------|-------------|
|
|
375
|
+
| GET | `/api/project-memories` | メモリー検索。Query: `?project_id=&search=&category=&tags=&status=` |
|
|
376
|
+
| POST | `/api/project-memories` | メモリーを投稿 |
|
|
377
|
+
|
|
378
|
+
GET Query Parameters:
|
|
379
|
+
|
|
380
|
+
| Param | Required | Description |
|
|
381
|
+
|-------|----------|-------------|
|
|
382
|
+
| `project_id` | Yes | プロジェクト UUID |
|
|
383
|
+
| `search` | No | タイトル・内容でのテキスト検索 |
|
|
384
|
+
| `category` | No | カテゴリフィルター |
|
|
385
|
+
| `tags` | No | タグフィルター(カンマ区切り) |
|
|
386
|
+
| `status` | No | `active`(デフォルト), `outdated`, `archived` |
|
|
387
|
+
|
|
388
|
+
POST `/api/project-memories` body:
|
|
389
|
+
```json
|
|
390
|
+
{
|
|
391
|
+
"project_id": "uuid",
|
|
392
|
+
"title": "ランサーズは2FA必須",
|
|
393
|
+
"content": "ランサーズはログイン時に2段階認証を要求する。メールで届くコードが必要なため、人間へのエスカレーションが必須。team_timeout=30sが適切。",
|
|
394
|
+
"category": "auth",
|
|
395
|
+
"tags": ["lancers", "2fa", "login"],
|
|
396
|
+
"source_thread_id": "uuid"
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
| Field | Type | Required | Description |
|
|
401
|
+
|-------|------|----------|-------------|
|
|
402
|
+
| `project_id` | string | Yes | プロジェクト UUID |
|
|
403
|
+
| `title` | string | Yes | 知見のタイトル |
|
|
404
|
+
| `content` | string | Yes | 知見の内容 |
|
|
405
|
+
| `category` | string | Yes | `auth`, `environment`, `know-how`, `decision`, `preference` |
|
|
406
|
+
| `tags` | string[] | No | 自由タグ(検索用) |
|
|
407
|
+
| `source_thread_id` | string | No | 知見の出典ヘルプスレッド UUID |
|
|
408
|
+
|
|
409
|
+
**推奨ワークフロー:**
|
|
410
|
+
1. ブロッカー発生 → `GET /api/project-memories?project_id=...&category=auth&search=2fa` で既知の知見を検索
|
|
411
|
+
2. 該当あり → 知見に基づいて自己解決 or 即エスカレーション
|
|
412
|
+
3. 該当なし → `POST /api/help-threads` でブロッカー起票
|
|
413
|
+
4. 解決後 → `POST /api/project-memories` で知見を蓄積
|
|
414
|
+
|
|
247
415
|
### Commands
|
|
248
416
|
|
|
249
417
|
| Method | Endpoint | Description |
|
|
@@ -601,3 +769,30 @@ Response:
|
|
|
601
769
|
| POST | `/api/minion/skills` | スキル登録/更新(新バージョン自動作成) |
|
|
602
770
|
|
|
603
771
|
スキルはバージョン管理される。push ごとに新バージョンが作成され、ファイルは Supabase Storage に保存される。
|
|
772
|
+
|
|
773
|
+
### Help Threads (HQ)
|
|
774
|
+
|
|
775
|
+
| Method | Endpoint | Description |
|
|
776
|
+
|--------|----------|-------------|
|
|
777
|
+
| POST | `/api/minion/help-threads` | ブロッカースレッドを起票 |
|
|
778
|
+
| GET | `/api/minion/help-threads/open` | 参加プロジェクトの未解決スレッド一覧 |
|
|
779
|
+
| GET | `/api/minion/help-threads/:id` | スレッド詳細 + メッセージ一覧 |
|
|
780
|
+
| POST | `/api/minion/help-threads/:id/messages` | スレッドにメッセージを投稿 |
|
|
781
|
+
| PATCH | `/api/minion/help-threads/:id/resolve` | スレッドを解決済みにする。Body: `{resolution}` |
|
|
782
|
+
| PATCH | `/api/minion/help-threads/:id/cancel` | スレッドをキャンセル。Body: `{reason?}` |
|
|
783
|
+
| DELETE | `/api/minion/help-threads/:id` | スレッドを完全削除(PMのみ)。メッセージもCASCADE削除 |
|
|
784
|
+
| PATCH | `/api/minion/help-threads/:id/escalate` | スレッドを手動エスカレーション |
|
|
785
|
+
|
|
786
|
+
ローカルエージェントの `/api/help-threads` は上記 HQ API へのプロキシ。
|
|
787
|
+
詳細なリクエスト/レスポンス仕様はローカル API セクションの「Help Threads」を参照。
|
|
788
|
+
|
|
789
|
+
### Project Memories (HQ)
|
|
790
|
+
|
|
791
|
+
| Method | Endpoint | Description |
|
|
792
|
+
|--------|----------|-------------|
|
|
793
|
+
| POST | `/api/minion/project-memories` | プロジェクトメモリーを投稿 |
|
|
794
|
+
| GET | `/api/minion/project-memories` | メモリー検索。Query: `?project_id=&search=&category=&tags=&status=` |
|
|
795
|
+
| PATCH | `/api/minion/project-memories/:id` | メモリーを更新。Body: `{title?, content?, category?, tags?, status?}` |
|
|
796
|
+
|
|
797
|
+
ローカルエージェントの `/api/project-memories` は上記 HQ API へのプロキシ。
|
|
798
|
+
詳細なリクエスト/レスポンス仕様はローカル API セクションの「Project Memories」を参照。
|
package/linux/minion-cli.sh
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
# sudo minion-cli restart # Restart agent service (root)
|
|
13
13
|
# minion-cli status # Get current status
|
|
14
14
|
# minion-cli health # Health check
|
|
15
|
+
# minion-cli daemons # Daemon status
|
|
15
16
|
# minion-cli diagnose # Run full service diagnostics
|
|
16
17
|
# minion-cli set-status busy "Running X" # Set status and task
|
|
17
18
|
# minion-cli set-status online # Set status only
|
|
@@ -814,6 +815,7 @@ CFEOF
|
|
|
814
815
|
echo "Useful commands:"
|
|
815
816
|
echo " minion-cli status # Agent status"
|
|
816
817
|
echo " minion-cli health # Health check"
|
|
818
|
+
echo " minion-cli daemons # Daemon status"
|
|
817
819
|
echo " sudo minion-cli restart # Restart agent"
|
|
818
820
|
echo " sudo minion-cli stop # Stop agent"
|
|
819
821
|
if [ "$PROC_MGR" = "systemd" ]; then
|
|
@@ -1219,6 +1221,10 @@ case "${1:-}" in
|
|
|
1219
1221
|
curl -s "$AGENT_URL/api/health" | jq .
|
|
1220
1222
|
;;
|
|
1221
1223
|
|
|
1224
|
+
daemons)
|
|
1225
|
+
curl -s "$AGENT_URL/api/daemons/status" | jq .
|
|
1226
|
+
;;
|
|
1227
|
+
|
|
1222
1228
|
diagnose)
|
|
1223
1229
|
echo "Running diagnostics..."
|
|
1224
1230
|
echo ""
|
package/linux/server.js
CHANGED
|
@@ -43,6 +43,7 @@ const routineStore = require('../core/stores/routine-store')
|
|
|
43
43
|
// Heartbeat interval: fixed at 30s (not user-configurable)
|
|
44
44
|
const HEARTBEAT_INTERVAL_MS = 30_000
|
|
45
45
|
let heartbeatTimer = null
|
|
46
|
+
let lastBeatAt = null
|
|
46
47
|
|
|
47
48
|
// Linux-specific modules
|
|
48
49
|
const workflowRunner = require('./workflow-runner')
|
|
@@ -50,10 +51,14 @@ const routineRunner = require('./routine-runner')
|
|
|
50
51
|
const { cleanupTtyd, killStaleTtydProcesses } = require('./routes/terminal')
|
|
51
52
|
const { startTerminalProxy, stopTerminalProxy } = require('./terminal-proxy')
|
|
52
53
|
|
|
54
|
+
// Config warnings (included in heartbeat)
|
|
55
|
+
const { getConfigWarnings } = require('../core/lib/config-warnings')
|
|
56
|
+
|
|
53
57
|
// Pull-model daemons (from core/)
|
|
54
58
|
const stepPoller = require('../core/lib/step-poller')
|
|
55
59
|
const revisionWatcher = require('../core/lib/revision-watcher')
|
|
56
60
|
const reflectionScheduler = require('../core/lib/reflection-scheduler')
|
|
61
|
+
const threadWatcher = require('../core/lib/thread-watcher')
|
|
57
62
|
const runningTasks = require('../core/lib/running-tasks')
|
|
58
63
|
|
|
59
64
|
// Shared routes (from core/)
|
|
@@ -68,6 +73,8 @@ const { memoryRoutes } = require('../core/routes/memory')
|
|
|
68
73
|
const { dailyLogRoutes } = require('../core/routes/daily-logs')
|
|
69
74
|
const { sudoersRoutes } = require('../core/routes/sudoers')
|
|
70
75
|
const { permissionRoutes } = require('../core/routes/permissions')
|
|
76
|
+
const { helpThreadRoutes } = require('../core/routes/help-threads')
|
|
77
|
+
const { daemonRoutes } = require('../core/routes/daemons')
|
|
71
78
|
|
|
72
79
|
// Linux-specific routes
|
|
73
80
|
const { commandRoutes, getProcessManager, getAllowedCommands } = require('./routes/commands')
|
|
@@ -113,6 +120,7 @@ async function shutdown(signal) {
|
|
|
113
120
|
stepPoller.stop()
|
|
114
121
|
revisionWatcher.stop()
|
|
115
122
|
reflectionScheduler.stop()
|
|
123
|
+
threadWatcher.stop()
|
|
116
124
|
workflowRunner.stopAll()
|
|
117
125
|
routineRunner.stopAll()
|
|
118
126
|
|
|
@@ -270,6 +278,8 @@ async function registerAllRoutes(app) {
|
|
|
270
278
|
await app.register(dailyLogRoutes)
|
|
271
279
|
await app.register(sudoersRoutes)
|
|
272
280
|
await app.register(permissionRoutes)
|
|
281
|
+
await app.register(helpThreadRoutes)
|
|
282
|
+
await app.register(daemonRoutes, { heartbeatStatus: () => ({ running: !!heartbeatTimer, last_beat_at: lastBeatAt }) })
|
|
273
283
|
|
|
274
284
|
// Linux-specific routes
|
|
275
285
|
await app.register(commandRoutes)
|
|
@@ -353,14 +363,16 @@ async function start() {
|
|
|
353
363
|
// Send initial online heartbeat
|
|
354
364
|
const { getStatus } = require('../core/routes/health')
|
|
355
365
|
const { currentTask } = getStatus()
|
|
356
|
-
sendHeartbeat({ status: 'online', current_task: currentTask, running_tasks: runningTasks.getAll(), version }).catch(err => {
|
|
366
|
+
sendHeartbeat({ status: 'online', current_task: currentTask, running_tasks: runningTasks.getAll(), config_warnings: getConfigWarnings(), version }).catch(err => {
|
|
357
367
|
console.error('[Heartbeat] Initial heartbeat failed:', err.message)
|
|
358
368
|
})
|
|
359
369
|
|
|
360
370
|
// Start periodic heartbeat
|
|
361
371
|
heartbeatTimer = setInterval(() => {
|
|
362
372
|
const { currentStatus, currentTask } = getStatus()
|
|
363
|
-
sendHeartbeat({ status: currentStatus, current_task: currentTask, running_tasks: runningTasks.getAll(), version }).
|
|
373
|
+
sendHeartbeat({ status: currentStatus, current_task: currentTask, running_tasks: runningTasks.getAll(), config_warnings: getConfigWarnings(), version }).then(() => {
|
|
374
|
+
lastBeatAt = new Date().toISOString()
|
|
375
|
+
}).catch(err => {
|
|
364
376
|
console.error('[Heartbeat] Periodic heartbeat failed:', err.message)
|
|
365
377
|
})
|
|
366
378
|
}, HEARTBEAT_INTERVAL_MS)
|
|
@@ -369,6 +381,7 @@ async function start() {
|
|
|
369
381
|
// Start Pull-model daemons
|
|
370
382
|
stepPoller.start()
|
|
371
383
|
revisionWatcher.start()
|
|
384
|
+
threadWatcher.start(runQuickLlmCall)
|
|
372
385
|
} else {
|
|
373
386
|
console.log('[Server] Running in standalone mode (no HQ connection)')
|
|
374
387
|
}
|
package/package.json
CHANGED
|
@@ -54,10 +54,10 @@ function detectProcessManager() {
|
|
|
54
54
|
* @param {string} startCmd - Command/script block to start the agent
|
|
55
55
|
* @returns {string} - Path to the generated update script (.ps1)
|
|
56
56
|
*/
|
|
57
|
-
function buildUpdateScript(npmInstallCmd, stopCmd, startCmd) {
|
|
57
|
+
function buildUpdateScript(npmInstallCmd, stopCmd, startCmd, scriptName = 'update-agent') {
|
|
58
58
|
const dataDir = path.join(os.homedir(), '.minion')
|
|
59
|
-
const scriptPath = path.join(dataDir,
|
|
60
|
-
const logPath = path.join(dataDir,
|
|
59
|
+
const scriptPath = path.join(dataDir, `${scriptName}.ps1`)
|
|
60
|
+
const logPath = path.join(dataDir, `${scriptName}.log`)
|
|
61
61
|
|
|
62
62
|
// PowerShell script content: stop → install → start, with logging
|
|
63
63
|
// The stopCmd kills all child processes of the agent. Since this script
|
|
@@ -136,6 +136,7 @@ function buildAllowedCommands(procMgr) {
|
|
|
136
136
|
'npm install -g @geekbeer/minion@latest --registry http://verdaccio:4873',
|
|
137
137
|
stopBlock,
|
|
138
138
|
startBlock,
|
|
139
|
+
'update-agent-dev',
|
|
139
140
|
)]],
|
|
140
141
|
deferred: true,
|
|
141
142
|
}
|
|
@@ -164,6 +165,7 @@ function buildAllowedCommands(procMgr) {
|
|
|
164
165
|
'npm install -g @geekbeer/minion@latest --registry http://verdaccio:4873',
|
|
165
166
|
'nssm stop minion-agent',
|
|
166
167
|
'nssm start minion-agent',
|
|
168
|
+
'update-agent-dev',
|
|
167
169
|
)]],
|
|
168
170
|
deferred: true,
|
|
169
171
|
}
|
|
@@ -196,6 +198,7 @@ function buildAllowedCommands(procMgr) {
|
|
|
196
198
|
'npm install -g @geekbeer/minion@latest --registry http://verdaccio:4873',
|
|
197
199
|
'net stop minion-agent',
|
|
198
200
|
'net start minion-agent',
|
|
201
|
+
'update-agent-dev',
|
|
199
202
|
)]],
|
|
200
203
|
deferred: true,
|
|
201
204
|
}
|
package/win/minion-cli.ps1
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# minion-cli-win setup --hq-url https://... --minion-id <UUID> --api-token <TOKEN>
|
|
5
5
|
# minion-cli-win setup --setup-tunnel
|
|
6
6
|
# minion-cli-win uninstall [--keep-data]
|
|
7
|
-
# minion-cli-win start | stop | restart | status | health | diagnose | version | help
|
|
7
|
+
# minion-cli-win start | stop | restart | status | health | daemons | diagnose | version | help
|
|
8
8
|
|
|
9
9
|
# Parse arguments manually to avoid issues with npm wrapper passing $args as array
|
|
10
10
|
$Command = 'help'
|
|
@@ -18,7 +18,7 @@ $i = 0
|
|
|
18
18
|
while ($i -lt $args.Count) {
|
|
19
19
|
$arg = [string]$args[$i]
|
|
20
20
|
switch -Regex ($arg) {
|
|
21
|
-
'^(setup|reconfigure|uninstall|start|stop|restart|status|health|diagnose|version|help)$' { $Command = $arg }
|
|
21
|
+
'^(setup|reconfigure|uninstall|start|stop|restart|status|health|daemons|diagnose|version|help)$' { $Command = $arg }
|
|
22
22
|
'^(-v|--version)$' { $Command = 'version' }
|
|
23
23
|
'^--hq-url$' { $i++; if ($i -lt $args.Count) { $HqUrl = [string]$args[$i] } }
|
|
24
24
|
'^--minion-id$' { $i++; if ($i -lt $args.Count) { $MinionId = [string]$args[$i] } }
|
|
@@ -895,6 +895,7 @@ Remove-Item `$PidFile -Force -ErrorAction SilentlyContinue
|
|
|
895
895
|
Write-Host "Useful commands:"
|
|
896
896
|
Write-Host " minion-cli-win status # Agent status"
|
|
897
897
|
Write-Host " minion-cli-win health # Health check"
|
|
898
|
+
Write-Host " minion-cli-win daemons # Daemon status"
|
|
898
899
|
Write-Host " minion-cli-win restart # Restart agent"
|
|
899
900
|
Write-Host " minion-cli-win stop # Stop agent"
|
|
900
901
|
Write-Host " Get-Content $(Join-Path $LogDir 'service-stdout.log') -Tail 50 # View logs"
|
|
@@ -1164,6 +1165,15 @@ switch ($Command) {
|
|
|
1164
1165
|
Write-Error "Health check failed. Is the agent running?"
|
|
1165
1166
|
}
|
|
1166
1167
|
}
|
|
1168
|
+
'daemons' {
|
|
1169
|
+
try {
|
|
1170
|
+
$response = Invoke-RestMethod -Uri "$AgentUrl/api/daemons/status" -TimeoutSec 5
|
|
1171
|
+
$response | ConvertTo-Json -Depth 5
|
|
1172
|
+
}
|
|
1173
|
+
catch {
|
|
1174
|
+
Write-Error "Failed to get daemon status. Is the agent running?"
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1167
1177
|
'diagnose' {
|
|
1168
1178
|
Write-Host "Running diagnostics..."
|
|
1169
1179
|
Write-Host ""
|
package/win/server.js
CHANGED
|
@@ -23,15 +23,20 @@ const routineStore = require('../core/stores/routine-store')
|
|
|
23
23
|
// Heartbeat interval: fixed at 30s (not user-configurable)
|
|
24
24
|
const HEARTBEAT_INTERVAL_MS = 30_000
|
|
25
25
|
let heartbeatTimer = null
|
|
26
|
+
let lastBeatAt = null
|
|
26
27
|
|
|
27
28
|
// Windows-specific modules
|
|
28
29
|
const workflowRunner = require('./workflow-runner')
|
|
29
30
|
const routineRunner = require('./routine-runner')
|
|
30
31
|
|
|
32
|
+
// Config warnings (included in heartbeat)
|
|
33
|
+
const { getConfigWarnings } = require('../core/lib/config-warnings')
|
|
34
|
+
|
|
31
35
|
// Pull-model daemons (from core/)
|
|
32
36
|
const stepPoller = require('../core/lib/step-poller')
|
|
33
37
|
const revisionWatcher = require('../core/lib/revision-watcher')
|
|
34
38
|
const reflectionScheduler = require('../core/lib/reflection-scheduler')
|
|
39
|
+
const threadWatcher = require('../core/lib/thread-watcher')
|
|
35
40
|
const { commandRoutes, getProcessManager, getAllowedCommands } = require('./routes/commands')
|
|
36
41
|
const { terminalRoutes, cleanupSessions } = require('./routes/terminal')
|
|
37
42
|
const { startTerminalServer, stopTerminalServer } = require('./terminal-server')
|
|
@@ -51,6 +56,8 @@ const { variableRoutes } = require('../core/routes/variables')
|
|
|
51
56
|
const { memoryRoutes } = require('../core/routes/memory')
|
|
52
57
|
const { dailyLogRoutes } = require('../core/routes/daily-logs')
|
|
53
58
|
const { permissionRoutes } = require('../core/routes/permissions')
|
|
59
|
+
const { helpThreadRoutes } = require('../core/routes/help-threads')
|
|
60
|
+
const { daemonRoutes } = require('../core/routes/daemons')
|
|
54
61
|
|
|
55
62
|
// Validate configuration
|
|
56
63
|
validate()
|
|
@@ -86,6 +93,7 @@ async function shutdown(signal) {
|
|
|
86
93
|
stepPoller.stop()
|
|
87
94
|
revisionWatcher.stop()
|
|
88
95
|
reflectionScheduler.stop()
|
|
96
|
+
threadWatcher.stop()
|
|
89
97
|
workflowRunner.stopAll()
|
|
90
98
|
routineRunner.stopAll()
|
|
91
99
|
stopTerminalServer()
|
|
@@ -203,6 +211,8 @@ async function registerRoutes(app) {
|
|
|
203
211
|
await app.register(memoryRoutes)
|
|
204
212
|
await app.register(dailyLogRoutes)
|
|
205
213
|
await app.register(permissionRoutes)
|
|
214
|
+
await app.register(helpThreadRoutes)
|
|
215
|
+
await app.register(daemonRoutes, { heartbeatStatus: () => ({ running: !!heartbeatTimer, last_beat_at: lastBeatAt }) })
|
|
206
216
|
|
|
207
217
|
// Windows-specific routes
|
|
208
218
|
await app.register(commandRoutes)
|
|
@@ -284,14 +294,16 @@ async function start() {
|
|
|
284
294
|
// Send initial online heartbeat
|
|
285
295
|
const { getStatus } = require('../core/routes/health')
|
|
286
296
|
const { currentTask } = getStatus()
|
|
287
|
-
sendHeartbeat({ status: 'online', current_task: currentTask, version }).catch(err => {
|
|
297
|
+
sendHeartbeat({ status: 'online', current_task: currentTask, config_warnings: getConfigWarnings(), version }).catch(err => {
|
|
288
298
|
console.error('[Heartbeat] Initial heartbeat failed:', err.message)
|
|
289
299
|
})
|
|
290
300
|
|
|
291
301
|
// Start periodic heartbeat
|
|
292
302
|
heartbeatTimer = setInterval(() => {
|
|
293
303
|
const { currentStatus, currentTask } = getStatus()
|
|
294
|
-
sendHeartbeat({ status: currentStatus, current_task: currentTask, version }).
|
|
304
|
+
sendHeartbeat({ status: currentStatus, current_task: currentTask, config_warnings: getConfigWarnings(), version }).then(() => {
|
|
305
|
+
lastBeatAt = new Date().toISOString()
|
|
306
|
+
}).catch(err => {
|
|
295
307
|
console.error('[Heartbeat] Periodic heartbeat failed:', err.message)
|
|
296
308
|
})
|
|
297
309
|
}, HEARTBEAT_INTERVAL_MS)
|
|
@@ -300,6 +312,7 @@ async function start() {
|
|
|
300
312
|
// Start Pull-model daemons
|
|
301
313
|
stepPoller.start()
|
|
302
314
|
revisionWatcher.start()
|
|
315
|
+
threadWatcher.start(runQuickLlmCall)
|
|
303
316
|
} else {
|
|
304
317
|
console.log('[Server] Running in standalone mode (no HQ connection)')
|
|
305
318
|
}
|