@yemi33/minions 0.1.24 → 0.1.26
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/CHANGELOG.md +19 -0
- package/dashboard/js/detail-panel.js +13 -4
- package/dashboard/js/live-stream.js +101 -11
- package/dashboard.js +18 -0
- package/engine/lifecycle.js +2 -1
- package/engine.js +121 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.26 (2026-03-28)
|
|
4
|
+
|
|
5
|
+
### Engine
|
|
6
|
+
- engine.js
|
|
7
|
+
|
|
8
|
+
### Dashboard
|
|
9
|
+
- dashboard.js
|
|
10
|
+
- dashboard/js/detail-panel.js
|
|
11
|
+
- dashboard/js/live-stream.js
|
|
12
|
+
|
|
13
|
+
## 0.1.25 (2026-03-28)
|
|
14
|
+
|
|
15
|
+
### Engine
|
|
16
|
+
- engine.js
|
|
17
|
+
- engine/lifecycle.js
|
|
18
|
+
|
|
19
|
+
### Other
|
|
20
|
+
- test/unit.test.js
|
|
21
|
+
|
|
3
22
|
## 0.1.24 (2026-03-28)
|
|
4
23
|
|
|
5
24
|
### Engine
|
|
@@ -63,10 +63,19 @@ function renderDetailContent(detail, tab) {
|
|
|
63
63
|
|
|
64
64
|
el.innerHTML = html;
|
|
65
65
|
} else if (tab === 'live') {
|
|
66
|
-
el.innerHTML =
|
|
67
|
-
'<div style="
|
|
68
|
-
'<
|
|
69
|
-
'<
|
|
66
|
+
el.innerHTML =
|
|
67
|
+
'<div id="live-chat" style="display:flex;flex-direction:column;height:60vh">' +
|
|
68
|
+
'<div id="live-messages" style="flex:1;overflow-y:auto;padding:8px;font-size:11px;line-height:1.6"></div>' +
|
|
69
|
+
'<div id="live-status-bar" style="padding:4px 8px;display:flex;align-items:center;gap:8px;border-top:1px solid var(--border)">' +
|
|
70
|
+
'<span class="pulse"></span><span id="live-status-label" style="font-size:11px;color:var(--green)">Streaming live</span>' +
|
|
71
|
+
'<button class="pr-pager-btn" onclick="refreshLiveOutput()" style="font-size:10px">Refresh</button>' +
|
|
72
|
+
'</div>' +
|
|
73
|
+
'<div id="live-steer-bar" style="display:flex;gap:8px;padding:8px;border-top:1px solid var(--border)">' +
|
|
74
|
+
'<input id="live-steer-input" type="text" placeholder="Steer this agent... (give additional context or redirect)" ' +
|
|
75
|
+
'style="flex:1;padding:6px 10px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:12px;font-family:inherit" ' +
|
|
76
|
+
'onkeydown="if(event.key===\'Enter\')sendSteering()" />' +
|
|
77
|
+
'<button onclick="sendSteering()" style="padding:6px 12px;background:var(--blue);color:#fff;border:none;border-radius:var(--radius-sm);cursor:pointer;font-size:11px">Send</button>' +
|
|
78
|
+
'</div>' +
|
|
70
79
|
'</div>';
|
|
71
80
|
startLiveStream(currentAgentId);
|
|
72
81
|
} else if (tab === 'charter') {
|
|
@@ -3,29 +3,100 @@
|
|
|
3
3
|
let livePollingInterval = null;
|
|
4
4
|
let liveEventSource = null;
|
|
5
5
|
|
|
6
|
+
function renderLiveChatMessage(raw) {
|
|
7
|
+
const el = document.getElementById('live-messages');
|
|
8
|
+
if (!el) return;
|
|
9
|
+
|
|
10
|
+
const lines = raw.split('\n');
|
|
11
|
+
for (const line of lines) {
|
|
12
|
+
const trimmed = line.trim();
|
|
13
|
+
if (!trimmed) continue;
|
|
14
|
+
|
|
15
|
+
// Human steering messages
|
|
16
|
+
if (trimmed.startsWith('[human-steering]')) {
|
|
17
|
+
const msg = trimmed.replace('[human-steering] ', '');
|
|
18
|
+
el.innerHTML += '<div style="align-self:flex-end;background:var(--blue);color:#fff;padding:6px 12px;border-radius:12px 12px 2px 12px;max-width:80%;margin:4px 0;font-size:12px">' + escHtml(msg) + '</div>';
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Heartbeat lines
|
|
23
|
+
if (trimmed.startsWith('[heartbeat]')) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Try to parse as JSON (stream-json format)
|
|
28
|
+
if (trimmed.startsWith('{')) {
|
|
29
|
+
try {
|
|
30
|
+
const obj = JSON.parse(trimmed);
|
|
31
|
+
|
|
32
|
+
// Assistant text message
|
|
33
|
+
if (obj.type === 'assistant' && obj.message?.content) {
|
|
34
|
+
for (const block of obj.message.content) {
|
|
35
|
+
if (block.type === 'text' && block.text) {
|
|
36
|
+
el.innerHTML += '<div style="background:var(--surface2);padding:8px 12px;border-radius:12px 12px 12px 2px;max-width:90%;margin:4px 0;font-size:12px;white-space:pre-wrap;word-break:break-word">' + escHtml(block.text) + '</div>';
|
|
37
|
+
}
|
|
38
|
+
if (block.type === 'tool_use') {
|
|
39
|
+
el.innerHTML += '<div style="background:var(--surface);border:1px solid var(--border);padding:4px 8px;border-radius:4px;margin:2px 0;font-size:10px;color:var(--muted);cursor:pointer" onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display===\'none\'?\'block\':\'none\'">' +
|
|
40
|
+
'🔧 ' + escHtml(block.name || 'tool') + '</div>' +
|
|
41
|
+
'<div style="display:none;background:var(--bg);padding:4px 8px;border-radius:4px;margin:0 0 4px;font-size:10px;font-family:monospace;white-space:pre-wrap;max-height:200px;overflow-y:auto;color:var(--muted)">' + escHtml(JSON.stringify(block.input, null, 2).slice(0, 500)) + '</div>';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Tool result
|
|
47
|
+
if (obj.type === 'tool_result' || (obj.type === 'user' && obj.message?.content?.[0]?.type === 'tool_result')) {
|
|
48
|
+
const content = obj.message?.content?.[0]?.content || obj.content || '';
|
|
49
|
+
const text = typeof content === 'string' ? content : JSON.stringify(content);
|
|
50
|
+
if (text.length > 10) {
|
|
51
|
+
el.innerHTML += '<div style="background:var(--bg);border-left:2px solid var(--border);padding:2px 8px;margin:0 0 2px 16px;font-size:9px;font-family:monospace;color:var(--muted);max-height:100px;overflow-y:auto;white-space:pre-wrap;cursor:pointer" onclick="this.style.maxHeight=this.style.maxHeight===\'100px\'?\'none\':\'100px\'">' + escHtml(text.slice(0, 1000)) + (text.length > 1000 ? '...' : '') + '</div>';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Result (final)
|
|
56
|
+
if (obj.type === 'result') {
|
|
57
|
+
el.innerHTML += '<div style="background:rgba(63,185,80,0.1);border:1px solid var(--green);padding:8px 12px;border-radius:8px;margin:8px 0;font-size:12px;color:var(--green)">✓ Task complete</div>';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
continue;
|
|
61
|
+
} catch {}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fallback: raw text (stderr, non-JSON lines)
|
|
65
|
+
if (trimmed.startsWith('[stderr]')) {
|
|
66
|
+
el.innerHTML += '<div style="font-size:9px;color:var(--red);font-family:monospace;padding:1px 4px">' + escHtml(trimmed) + '</div>';
|
|
67
|
+
} else {
|
|
68
|
+
el.innerHTML += '<div style="font-size:10px;color:var(--muted);font-family:monospace;padding:1px 4px">' + escHtml(trimmed) + '</div>';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Auto-scroll
|
|
73
|
+
if (el.scrollHeight - el.scrollTop - el.clientHeight < 150) {
|
|
74
|
+
el.scrollTop = el.scrollHeight;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
6
78
|
function startLiveStream(agentId) {
|
|
7
79
|
stopLiveStream();
|
|
8
80
|
if (!agentId) return;
|
|
9
81
|
|
|
10
|
-
const
|
|
11
|
-
if (
|
|
82
|
+
const msgEl = document.getElementById('live-messages');
|
|
83
|
+
if (msgEl) msgEl.innerHTML = '';
|
|
12
84
|
|
|
13
85
|
liveEventSource = new EventSource('/api/agent/' + agentId + '/live-stream');
|
|
14
86
|
|
|
15
87
|
liveEventSource.onmessage = function(e) {
|
|
16
88
|
try {
|
|
17
89
|
const chunk = JSON.parse(e.data);
|
|
18
|
-
|
|
19
|
-
if (el) {
|
|
20
|
-
const wasAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 100;
|
|
21
|
-
el.textContent += chunk;
|
|
22
|
-
if (wasAtBottom) el.scrollTop = el.scrollHeight;
|
|
23
|
-
}
|
|
90
|
+
renderLiveChatMessage(chunk);
|
|
24
91
|
} catch {}
|
|
25
92
|
};
|
|
26
93
|
|
|
27
94
|
liveEventSource.addEventListener('done', function() {
|
|
28
95
|
stopLiveStream();
|
|
96
|
+
const steerBar = document.getElementById('live-steer-bar');
|
|
97
|
+
if (steerBar) steerBar.style.display = 'none';
|
|
98
|
+
const statusLabel = document.getElementById('live-status-label');
|
|
99
|
+
if (statusLabel) { statusLabel.textContent = 'Completed'; statusLabel.style.color = 'var(--muted)'; }
|
|
29
100
|
});
|
|
30
101
|
|
|
31
102
|
liveEventSource.onerror = function() {
|
|
@@ -59,11 +130,30 @@ async function refreshLiveOutput() {
|
|
|
59
130
|
if (!currentAgentId || currentTab !== 'live') { stopLivePolling(); return; }
|
|
60
131
|
try {
|
|
61
132
|
const text = await fetch('/api/agent/' + currentAgentId + '/live?tail=16384').then(r => r.text());
|
|
62
|
-
const el = document.getElementById('live-
|
|
133
|
+
const el = document.getElementById('live-messages');
|
|
63
134
|
if (el) {
|
|
64
|
-
const wasAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight <
|
|
65
|
-
el.
|
|
135
|
+
const wasAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 150;
|
|
136
|
+
el.innerHTML = '';
|
|
137
|
+
renderLiveChatMessage(text);
|
|
66
138
|
if (wasAtBottom) el.scrollTop = el.scrollHeight;
|
|
67
139
|
}
|
|
68
140
|
} catch {}
|
|
69
141
|
}
|
|
142
|
+
|
|
143
|
+
async function sendSteering() {
|
|
144
|
+
const input = document.getElementById('live-steer-input');
|
|
145
|
+
if (!input || !input.value.trim() || !currentAgentId) return;
|
|
146
|
+
const message = input.value.trim();
|
|
147
|
+
input.value = '';
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const res = await fetch('/api/agents/steer', {
|
|
151
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
152
|
+
body: JSON.stringify({ agent: currentAgentId, message })
|
|
153
|
+
});
|
|
154
|
+
if (!res.ok) {
|
|
155
|
+
const d = await res.json().catch(() => ({}));
|
|
156
|
+
alert('Steering failed: ' + (d.error || 'unknown'));
|
|
157
|
+
}
|
|
158
|
+
} catch (e) { alert('Error: ' + e.message); }
|
|
159
|
+
}
|
package/dashboard.js
CHANGED
|
@@ -2850,6 +2850,24 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
2850
2850
|
{ method: 'POST', path: '/api/prd/regenerate', desc: 'Regenerate PRD from revised source plan', params: 'file', handler: handlePrdRegenerate },
|
|
2851
2851
|
|
|
2852
2852
|
// Agents
|
|
2853
|
+
{ method: 'POST', path: '/api/agents/steer', desc: 'Inject steering message into a running agent', params: 'agent, message', handler: async (req, res) => {
|
|
2854
|
+
const body = await readBody(req);
|
|
2855
|
+
const { agent: agentId, message } = body;
|
|
2856
|
+
if (!agentId || !message) return jsonReply(res, 400, { error: 'agent and message required' });
|
|
2857
|
+
|
|
2858
|
+
const steerPath = path.join(MINIONS_DIR, 'agents', agentId, 'steer.md');
|
|
2859
|
+
const agentDir = path.join(MINIONS_DIR, 'agents', agentId);
|
|
2860
|
+
if (!fs.existsSync(agentDir)) return jsonReply(res, 404, { error: 'Agent not found' });
|
|
2861
|
+
|
|
2862
|
+
// Write steering file — engine picks it up on next tick
|
|
2863
|
+
safeWrite(steerPath, message);
|
|
2864
|
+
|
|
2865
|
+
// Also append to live-output.log so it shows in the chat view
|
|
2866
|
+
const liveLogPath = path.join(agentDir, 'live-output.log');
|
|
2867
|
+
try { fs.appendFileSync(liveLogPath, '\n[human-steering] ' + message + '\n'); } catch {}
|
|
2868
|
+
|
|
2869
|
+
return jsonReply(res, 200, { ok: true, message: 'Steering message sent' });
|
|
2870
|
+
}},
|
|
2853
2871
|
{ method: 'POST', path: '/api/agents/cancel', desc: 'Cancel an active agent by ID or task substring', params: 'agent?, task?', handler: handleAgentsCancel },
|
|
2854
2872
|
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live-stream(?:\?.*)?$/, desc: 'SSE real-time live output streaming', handler: handleAgentLiveStream },
|
|
2855
2873
|
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live(?:\?.*)?$/, desc: 'Tail live output for a working agent', params: 'tail? (bytes, default 8192)', handler: handleAgentLive },
|
package/engine/lifecycle.js
CHANGED
|
@@ -1011,7 +1011,8 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1011
1011
|
if (isSuccess && sessionId && agentId && !agentId.startsWith('temp-')) {
|
|
1012
1012
|
try {
|
|
1013
1013
|
shared.safeWrite(path.join(AGENTS_DIR, agentId, 'session.json'), {
|
|
1014
|
-
sessionId, dispatchId: dispatchItem.id, savedAt: new Date().toISOString()
|
|
1014
|
+
sessionId, dispatchId: dispatchItem.id, savedAt: new Date().toISOString(),
|
|
1015
|
+
branch: dispatchItem.meta?.branch || null,
|
|
1015
1016
|
});
|
|
1016
1017
|
} catch {}
|
|
1017
1018
|
}
|
package/engine.js
CHANGED
|
@@ -939,15 +939,18 @@ function spawnAgent(dispatchItem, config) {
|
|
|
939
939
|
args.push('--allowedTools', claudeConfig.allowedTools);
|
|
940
940
|
}
|
|
941
941
|
|
|
942
|
-
// Session resume: reuse last session if recent enough (< 2 hours)
|
|
942
|
+
// Session resume: reuse last session if same branch and recent enough (< 2 hours)
|
|
943
|
+
// Only resume when the context is relevant — same branch means the agent is
|
|
944
|
+
// continuing work on the same PR/feature (e.g., author fixing their own build failure)
|
|
943
945
|
if (!agentId.startsWith('temp-')) {
|
|
944
946
|
try {
|
|
945
947
|
const sessionFile = safeJson(path.join(AGENTS_DIR, agentId, 'session.json'));
|
|
946
948
|
if (sessionFile?.sessionId && sessionFile.savedAt) {
|
|
947
949
|
const sessionAge = Date.now() - new Date(sessionFile.savedAt).getTime();
|
|
948
|
-
|
|
950
|
+
const sameBranch = branchName && sessionFile.branch && sessionFile.branch === branchName;
|
|
951
|
+
if (sessionAge < 2 * 60 * 60 * 1000 && sameBranch) {
|
|
949
952
|
args.push('--resume', sessionFile.sessionId);
|
|
950
|
-
log('info', `Resuming session ${sessionFile.sessionId} for ${agentId} (age: ${Math.round(sessionAge / 60000)}min)`);
|
|
953
|
+
log('info', `Resuming session ${sessionFile.sessionId} for ${agentId} on branch ${branchName} (age: ${Math.round(sessionAge / 60000)}min)`);
|
|
951
954
|
}
|
|
952
955
|
}
|
|
953
956
|
} catch {}
|
|
@@ -999,6 +1002,24 @@ function spawnAgent(dispatchItem, config) {
|
|
|
999
1002
|
lastOutputAt = Date.now();
|
|
1000
1003
|
if (stdout.length < MAX_OUTPUT) stdout += chunk.slice(0, MAX_OUTPUT - stdout.length);
|
|
1001
1004
|
try { fs.appendFileSync(liveOutputPath, chunk); } catch {}
|
|
1005
|
+
|
|
1006
|
+
// Capture sessionId early for mid-session steering
|
|
1007
|
+
const procInfo = activeProcesses.get(id);
|
|
1008
|
+
if (procInfo && !procInfo.sessionId && chunk.includes('session_id')) {
|
|
1009
|
+
try {
|
|
1010
|
+
for (const line of chunk.split('\n')) {
|
|
1011
|
+
if (!line.trim() || !line.startsWith('{')) continue;
|
|
1012
|
+
const obj = JSON.parse(line);
|
|
1013
|
+
if (obj.session_id) {
|
|
1014
|
+
procInfo.sessionId = obj.session_id;
|
|
1015
|
+
safeWrite(path.join(AGENTS_DIR, agentId, 'session.json'), {
|
|
1016
|
+
sessionId: obj.session_id, dispatchId: id, savedAt: new Date().toISOString(), branch: branchName
|
|
1017
|
+
});
|
|
1018
|
+
break;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
} catch {}
|
|
1022
|
+
}
|
|
1002
1023
|
});
|
|
1003
1024
|
|
|
1004
1025
|
proc.stderr.on('data', (data) => {
|
|
@@ -1008,9 +1029,72 @@ function spawnAgent(dispatchItem, config) {
|
|
|
1008
1029
|
try { fs.appendFileSync(liveOutputPath, '[stderr] ' + chunk); } catch {}
|
|
1009
1030
|
});
|
|
1010
1031
|
|
|
1011
|
-
|
|
1032
|
+
function onAgentClose(code) {
|
|
1012
1033
|
if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
|
|
1013
1034
|
log('info', `Agent ${agentId} (${id}) exited with code ${code}`);
|
|
1035
|
+
|
|
1036
|
+
// Check if this was a steering kill — re-spawn with resume
|
|
1037
|
+
const procInfo = activeProcesses.get(id);
|
|
1038
|
+
if (procInfo?._steeringMessage) {
|
|
1039
|
+
const steerMsg = procInfo._steeringMessage;
|
|
1040
|
+
const steerSessionId = procInfo._steeringSessionId;
|
|
1041
|
+
delete procInfo._steeringMessage;
|
|
1042
|
+
delete procInfo._steeringSessionId;
|
|
1043
|
+
|
|
1044
|
+
log('info', `Steering: re-spawning ${agentId} with --resume ${steerSessionId}`);
|
|
1045
|
+
|
|
1046
|
+
// Write new prompt with steering message
|
|
1047
|
+
const steerPrompt = `HUMAN STEERING MESSAGE:\n\n${steerMsg}\n\nPlease acknowledge this update and adjust your approach accordingly. Continue working on your current task with this new context.`;
|
|
1048
|
+
const steerPromptPath = path.join(ENGINE_DIR, 'tmp', `prompt-steer-${id}.md`);
|
|
1049
|
+
safeWrite(steerPromptPath, steerPrompt);
|
|
1050
|
+
|
|
1051
|
+
// Build resume args
|
|
1052
|
+
const resumeArgs = [
|
|
1053
|
+
'--output-format', claudeConfig?.outputFormat || 'stream-json',
|
|
1054
|
+
'--max-turns', String(engineConfig?.maxTurns || DEFAULTS.maxTurns),
|
|
1055
|
+
'--verbose',
|
|
1056
|
+
'--permission-mode', claudeConfig?.permissionMode || 'bypassPermissions',
|
|
1057
|
+
'--resume', steerSessionId,
|
|
1058
|
+
];
|
|
1059
|
+
if (claudeConfig?.allowedTools) resumeArgs.push('--allowedTools', claudeConfig.allowedTools);
|
|
1060
|
+
|
|
1061
|
+
const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
|
|
1062
|
+
const childEnv = shared.cleanChildEnv();
|
|
1063
|
+
const resumeProc = runFile(process.execPath, [spawnScript, steerPromptPath, steerPromptPath, ...resumeArgs], {
|
|
1064
|
+
cwd,
|
|
1065
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1066
|
+
env: childEnv,
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
// Re-attach to existing tracking
|
|
1070
|
+
activeProcesses.set(id, { proc: resumeProc, agentId, startedAt: procInfo.startedAt, sessionId: steerSessionId });
|
|
1071
|
+
|
|
1072
|
+
// Re-wire stdout/stderr handlers (same as original)
|
|
1073
|
+
resumeProc.stdout.on('data', (data) => {
|
|
1074
|
+
const chunk = data.toString();
|
|
1075
|
+
lastOutputAt = Date.now();
|
|
1076
|
+
if (stdout.length < MAX_OUTPUT) stdout += chunk.slice(0, MAX_OUTPUT - stdout.length);
|
|
1077
|
+
try { fs.appendFileSync(liveOutputPath, chunk); } catch {}
|
|
1078
|
+
});
|
|
1079
|
+
resumeProc.stderr.on('data', (data) => {
|
|
1080
|
+
const chunk = data.toString();
|
|
1081
|
+
lastOutputAt = Date.now();
|
|
1082
|
+
if (stderr.length < MAX_OUTPUT) stderr += chunk.slice(0, MAX_OUTPUT - stderr.length);
|
|
1083
|
+
try { fs.appendFileSync(liveOutputPath, '[stderr] ' + chunk); } catch {}
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
// Re-wire close handler for the resumed process
|
|
1087
|
+
resumeProc.on('close', onAgentClose);
|
|
1088
|
+
resumeProc.on('error', (err) => {
|
|
1089
|
+
log('error', `Steering re-spawn failed for ${agentId}: ${err.message}`);
|
|
1090
|
+
activeProcesses.delete(id);
|
|
1091
|
+
completeDispatch(id, 'error', `Steering re-spawn error: ${err.message}`);
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
// Don't run completion hooks — agent is still working
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1014
1098
|
activeProcesses.delete(id);
|
|
1015
1099
|
|
|
1016
1100
|
// If timeout checker already finalized this dispatch, don't overwrite work-item status again.
|
|
@@ -1066,7 +1150,9 @@ function spawnAgent(dispatchItem, config) {
|
|
|
1066
1150
|
log('info', `Temp agent ${agentId} cleaned up`);
|
|
1067
1151
|
} catch {}
|
|
1068
1152
|
}
|
|
1069
|
-
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
proc.on('close', onAgentClose);
|
|
1070
1156
|
|
|
1071
1157
|
proc.on('error', (err) => {
|
|
1072
1158
|
if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
|
|
@@ -1429,6 +1515,34 @@ function checkIdleThreshold(config) {
|
|
|
1429
1515
|
}
|
|
1430
1516
|
}
|
|
1431
1517
|
|
|
1518
|
+
// ─── Steering Checker ────────────────────────────────────────────────────────
|
|
1519
|
+
|
|
1520
|
+
function checkSteering(config) {
|
|
1521
|
+
for (const [id, info] of activeProcesses) {
|
|
1522
|
+
const steerPath = path.join(AGENTS_DIR, info.agentId, 'steer.md');
|
|
1523
|
+
if (!fs.existsSync(steerPath)) continue;
|
|
1524
|
+
|
|
1525
|
+
const message = safeRead(steerPath);
|
|
1526
|
+
try { fs.unlinkSync(steerPath); } catch {}
|
|
1527
|
+
if (!message) continue;
|
|
1528
|
+
|
|
1529
|
+
const sessionId = info.sessionId;
|
|
1530
|
+
if (!sessionId) {
|
|
1531
|
+
log('warn', `Steering: no sessionId for ${info.agentId} — cannot resume. Message dropped.`);
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
log('info', `Steering: killing ${info.agentId} (${id}) for session resume with human message`);
|
|
1536
|
+
|
|
1537
|
+
// Kill current process
|
|
1538
|
+
try { info.proc.kill('SIGTERM'); } catch {}
|
|
1539
|
+
|
|
1540
|
+
// Store steering context for re-spawn on close
|
|
1541
|
+
info._steeringMessage = message;
|
|
1542
|
+
info._steeringSessionId = sessionId;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1432
1546
|
// ─── Timeout Checker ────────────────────────────────────────────────────────
|
|
1433
1547
|
|
|
1434
1548
|
function checkTimeouts(config) {
|
|
@@ -3257,8 +3371,9 @@ async function tickInner() {
|
|
|
3257
3371
|
const config = getConfig();
|
|
3258
3372
|
tickCount++;
|
|
3259
3373
|
|
|
3260
|
-
// 1. Check for timed-out agents and idle threshold
|
|
3374
|
+
// 1. Check for timed-out agents, steering messages, and idle threshold
|
|
3261
3375
|
checkTimeouts(config);
|
|
3376
|
+
checkSteering(config);
|
|
3262
3377
|
checkIdleThreshold(config);
|
|
3263
3378
|
|
|
3264
3379
|
// In stopping state, only track agent completions — skip discovery and dispatch
|
package/package.json
CHANGED