@yemi33/minions 0.1.668 → 0.1.670
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 +3 -1
- package/dashboard/js/render-agents.js +2 -0
- package/engine/queries.js +5 -0
- package/engine/timeout.js +77 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.670 (2026-04-09)
|
|
4
4
|
|
|
5
5
|
### Fixes
|
|
6
|
+
- surface blocking tool call state in dashboard (closes #619) (#634)
|
|
7
|
+
- interrupt blocking tool calls immediately on steering (closes #627) (#632)
|
|
6
8
|
- reduce Bash blocking grace and errored task dedup window (closes #593) (#630)
|
|
7
9
|
|
|
8
10
|
## 0.1.667 (2026-04-09)
|
|
@@ -11,6 +11,7 @@ function renderAgents(agents) {
|
|
|
11
11
|
</div>
|
|
12
12
|
<div class="agent-role">${escHtml(a.role)}</div>
|
|
13
13
|
<div class="agent-action" title="${escHtml(a.lastAction)}">${escHtml(a.lastAction)}</div>
|
|
14
|
+
${a._blockingToolCall ? `<div style="margin-top:4px;padding:4px 8px;background:rgba(130,160,210,0.13);border:1px solid rgba(130,160,210,0.3);border-radius:4px;font-size:10px;color:var(--muted)">⏳ Blocking tool call (${escHtml(a._blockingToolCall.tool)}) — silent ${Math.round(a._blockingToolCall.silentMs/60000)}min, timeout in ${Math.round(a._blockingToolCall.remainingMs/60000)}min</div>` : ''}
|
|
14
15
|
${a._warning ? `<div style="margin-top:4px;padding:4px 8px;background:rgba(210,153,34,0.15);border:1px solid rgba(210,153,34,0.3);border-radius:4px;font-size:10px;color:var(--yellow)">⚠ ${escHtml(a._warning)}</div>` : ''}
|
|
15
16
|
${a._permissionMode && a._permissionMode !== 'bypassPermissions' && !a._warning ? `<div style="margin-top:4px;font-size:9px;color:var(--muted)">Permission mode: ${escHtml(a._permissionMode)}</div>` : ''}
|
|
16
17
|
${a.resultSummary ? `<div class="agent-result" title="${escHtml(a.resultSummary)}">${renderMd(a.resultSummary.slice(0, 200))}${a.resultSummary.length > 200 ? '...' : ''}</div>` : ''}
|
|
@@ -31,6 +32,7 @@ async function openAgentDetail(id) {
|
|
|
31
32
|
document.getElementById('detail-status-line').innerHTML =
|
|
32
33
|
'<span class="status-badge ' + badgeClass + '">' + agent.status.toUpperCase() + '</span> ' +
|
|
33
34
|
'<span style="color:var(--muted)">' + escHtml(agent.lastAction) + '</span>' +
|
|
35
|
+
(agent._blockingToolCall ? '<div style="margin-top:4px;padding:4px 8px;background:rgba(130,160,210,0.13);border:1px solid rgba(130,160,210,0.3);border-radius:4px;font-size:11px;color:var(--muted)">⏳ Blocking tool call (' + escHtml(agent._blockingToolCall.tool) + ') — silent ' + Math.round(agent._blockingToolCall.silentMs/60000) + 'min, timeout in ' + Math.round(agent._blockingToolCall.remainingMs/60000) + 'min</div>' : '') +
|
|
34
36
|
(agent.resultSummary ? '<div style="margin-top:4px;font-size:11px;color:var(--text);line-height:1.4">' + renderMd(agent.resultSummary.slice(0, 300)) + '</div>' : '');
|
|
35
37
|
|
|
36
38
|
// Show panel immediately with loading state — don't wait for API
|
package/engine/queries.js
CHANGED
|
@@ -216,6 +216,10 @@ function getAgentStatus(agentId) {
|
|
|
216
216
|
branch: active.meta?.branch || '',
|
|
217
217
|
started_at: active.started_at || active.created_at || null,
|
|
218
218
|
};
|
|
219
|
+
// Surface blocking tool call state from dispatch annotation (set by timeout.js)
|
|
220
|
+
if (active._blockingToolCall) {
|
|
221
|
+
result._blockingToolCall = active._blockingToolCall;
|
|
222
|
+
}
|
|
219
223
|
// Detect permission-waiting: read only head+tail of live-output.log (max 2KB total)
|
|
220
224
|
try {
|
|
221
225
|
const liveLogPath = path.join(AGENTS_DIR, agentId, 'live-output.log');
|
|
@@ -330,6 +334,7 @@ function getAgents(config) {
|
|
|
330
334
|
...a, status: s.status, lastAction,
|
|
331
335
|
currentTask: (s.task || '').slice(0, 200),
|
|
332
336
|
resultSummary: (s.resultSummary || '').slice(0, 500),
|
|
337
|
+
_blockingToolCall: s._blockingToolCall || null,
|
|
333
338
|
_warning: s._warning || null,
|
|
334
339
|
_permissionMode: s._permissionMode || null,
|
|
335
340
|
chartered, inboxCount: inboxFiles.length
|
package/engine/timeout.js
CHANGED
|
@@ -54,9 +54,28 @@ function checkIdleThreshold(config) {
|
|
|
54
54
|
|
|
55
55
|
// ─── Steering Checker ────────────────────────────────────────────────────────
|
|
56
56
|
|
|
57
|
+
// How long to wait for a steered agent to exit before retrying the kill
|
|
58
|
+
const STEERING_KILL_RETRY_MS = 30000;
|
|
59
|
+
|
|
57
60
|
function checkSteering(config) {
|
|
58
61
|
const activeProcesses = engine().activeProcesses;
|
|
59
62
|
for (const [id, info] of activeProcesses) {
|
|
63
|
+
// Recovery: if steering kill hasn't resulted in process exit within 30s, force-retry.
|
|
64
|
+
// This catches cases where killImmediate silently failed (e.g., orphaned subprocess
|
|
65
|
+
// on Unix where SIGKILL only hit spawn-agent.js, not the Claude CLI tree).
|
|
66
|
+
if (info._steeringAt && Date.now() - info._steeringAt > STEERING_KILL_RETRY_MS) {
|
|
67
|
+
if (!info._steeringRetried) {
|
|
68
|
+
log('warn', `Steering: ${info.agentId} (${id}) didn't exit ${STEERING_KILL_RETRY_MS / 1000}s after kill — retrying`);
|
|
69
|
+
shared.killImmediate(info.proc);
|
|
70
|
+
// On Unix, also try to kill children that may have been orphaned
|
|
71
|
+
if (process.platform !== 'win32' && info.proc?.pid) {
|
|
72
|
+
try { shared.exec(`pkill -KILL -P ${info.proc.pid}`, { timeout: 3000 }); } catch { /* children may already be dead */ }
|
|
73
|
+
}
|
|
74
|
+
info._steeringRetried = true;
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
60
79
|
// Skip if already being steered (prevents double-kill race)
|
|
61
80
|
if (info._steeringMessage || info._steeringAt) continue;
|
|
62
81
|
|
|
@@ -64,19 +83,34 @@ function checkSteering(config) {
|
|
|
64
83
|
let steerMtime;
|
|
65
84
|
try { steerMtime = fs.statSync(steerPath).mtimeMs; } catch { continue; } // ENOENT = no steering message
|
|
66
85
|
|
|
86
|
+
// Read and consume the message immediately — always delete to prevent stale messages
|
|
87
|
+
const message = safeRead(steerPath);
|
|
88
|
+
try { fs.unlinkSync(steerPath); } catch { /* cleanup */ }
|
|
89
|
+
if (!message) continue;
|
|
90
|
+
|
|
67
91
|
const sessionId = info.sessionId;
|
|
68
92
|
if (!sessionId) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
93
|
+
// No session to resume — kill agent and deliver message via inbox for retry.
|
|
94
|
+
// Previously this silently skipped for up to 5m then deleted the message (#627).
|
|
95
|
+
log('info', `Steering: no sessionId for ${info.agentId} (${id}) — killing and forwarding message to inbox`);
|
|
96
|
+
|
|
97
|
+
// Write steering message to agent inbox so it survives the retry
|
|
98
|
+
const inboxDir = path.join(AGENTS_DIR, info.agentId, 'inbox');
|
|
99
|
+
try { fs.mkdirSync(inboxDir, { recursive: true }); } catch {}
|
|
100
|
+
safeWrite(path.join(inboxDir, `steering-${Date.now()}.md`), `# Steering Message (Forwarded)\n\nOriginal steering from human:\n\n${message}\n`);
|
|
101
|
+
|
|
102
|
+
// Append to live output so user sees confirmation in the dashboard
|
|
103
|
+
try {
|
|
104
|
+
const liveLogPath = path.join(AGENTS_DIR, info.agentId, 'live-output.log');
|
|
105
|
+
fs.appendFileSync(liveLogPath, `\n[steering] Message received but no session to resume. Killing agent — your message will be delivered on retry.\n`);
|
|
106
|
+
} catch { /* optional */ }
|
|
107
|
+
|
|
108
|
+
shared.killImmediate(info.proc);
|
|
109
|
+
info._steeringAt = Date.now();
|
|
110
|
+
info._steeringNoSession = true;
|
|
73
111
|
continue;
|
|
74
112
|
}
|
|
75
113
|
|
|
76
|
-
const message = safeRead(steerPath);
|
|
77
|
-
try { fs.unlinkSync(steerPath); } catch { /* cleanup */ }
|
|
78
|
-
if (!message) continue;
|
|
79
|
-
|
|
80
114
|
log('info', `Steering: killing ${info.agentId} (${id}) for session resume with human message`);
|
|
81
115
|
|
|
82
116
|
shared.killImmediate(info.proc);
|
|
@@ -112,6 +146,7 @@ function checkTimeouts(config) {
|
|
|
112
146
|
// Uses live-output.log mtime as heartbeat. If no output for heartbeatTimeout, agent is dead.
|
|
113
147
|
const dispatchData = getDispatch();
|
|
114
148
|
const deadItems = [];
|
|
149
|
+
const blockingAnnotations = new Map(); // id → { tool, silentMs, remainingMs } or null (clear)
|
|
115
150
|
|
|
116
151
|
for (const item of (dispatchData.active || [])) {
|
|
117
152
|
if (!item.agent) continue;
|
|
@@ -164,6 +199,7 @@ function checkTimeouts(config) {
|
|
|
164
199
|
// Check for BOTH tracked and untracked processes (orphan case after engine restart)
|
|
165
200
|
let isBlocking = false;
|
|
166
201
|
let blockingTimeout = heartbeatTimeout;
|
|
202
|
+
let blockingTool = '';
|
|
167
203
|
if (silentMs > heartbeatTimeout) {
|
|
168
204
|
try {
|
|
169
205
|
const liveLog = safeRead(liveLogPath);
|
|
@@ -184,6 +220,7 @@ function checkTimeouts(config) {
|
|
|
184
220
|
const taskTimeout = input.timeout || 600000; // default 10min
|
|
185
221
|
blockingTimeout = Math.max(heartbeatTimeout, taskTimeout + 60000); // task timeout + 1min grace
|
|
186
222
|
isBlocking = true;
|
|
223
|
+
blockingTool = 'TaskOutput';
|
|
187
224
|
}
|
|
188
225
|
// Bash tool call — may be running a long build/install with no stdout
|
|
189
226
|
if (name === 'Bash') {
|
|
@@ -191,21 +228,35 @@ function checkTimeouts(config) {
|
|
|
191
228
|
const bashTimeout = input.timeout || 120000;
|
|
192
229
|
blockingTimeout = Math.max(heartbeatTimeout, bashTimeout + 60000);
|
|
193
230
|
isBlocking = true;
|
|
231
|
+
blockingTool = 'Bash';
|
|
194
232
|
}
|
|
195
233
|
// Agent (subagent) tool call — parent waits silently for child to complete
|
|
196
234
|
if (name === 'Agent') {
|
|
197
235
|
blockingTimeout = Math.max(heartbeatTimeout, 1800000); // 30min for subagents
|
|
198
236
|
isBlocking = true;
|
|
237
|
+
blockingTool = 'Agent';
|
|
199
238
|
}
|
|
200
239
|
break; // only check the most recent tool_use
|
|
201
240
|
} catch { /* JSON parse — line may not be valid JSON */ }
|
|
202
241
|
}
|
|
203
242
|
if (isBlocking) {
|
|
204
|
-
|
|
243
|
+
// Only log on transition — avoid spamming every tick while blocking persists
|
|
244
|
+
if (!item._blockingToolCall) {
|
|
245
|
+
log('info', `Agent ${item.agent} (${item.id}) is in a blocking tool call (${blockingTool}) — extended timeout to ${Math.round(blockingTimeout / 1000)}s (silent for ${silentSec}s)`, { event: 'blocking_tool_call_detected' });
|
|
246
|
+
}
|
|
247
|
+
blockingAnnotations.set(item.id, {
|
|
248
|
+
tool: blockingTool,
|
|
249
|
+
silentMs,
|
|
250
|
+
remainingMs: Math.max(0, blockingTimeout - silentMs),
|
|
251
|
+
});
|
|
205
252
|
}
|
|
206
253
|
}
|
|
207
254
|
} catch (e) { log('warn', 'blocking tool detection: ' + e.message); }
|
|
208
255
|
}
|
|
256
|
+
// Agent recovered from blocking state — clear annotation
|
|
257
|
+
if (!isBlocking && item._blockingToolCall) {
|
|
258
|
+
blockingAnnotations.set(item.id, null);
|
|
259
|
+
}
|
|
209
260
|
|
|
210
261
|
const effectiveTimeout = isBlocking ? blockingTimeout : heartbeatTimeout;
|
|
211
262
|
|
|
@@ -239,6 +290,23 @@ function checkTimeouts(config) {
|
|
|
239
290
|
completeDispatch(item.id, DISPATCH_RESULT.ERROR, reason);
|
|
240
291
|
}
|
|
241
292
|
|
|
293
|
+
// Batch-write blocking tool call annotations to dispatch entries.
|
|
294
|
+
// This surfaces blocking state via GET /api/status → dashboard badges.
|
|
295
|
+
if (blockingAnnotations.size > 0) {
|
|
296
|
+
const { mutateDispatch: mutateDispatchFn } = dispatch();
|
|
297
|
+
mutateDispatchFn((dp) => {
|
|
298
|
+
for (const activeItem of dp.active) {
|
|
299
|
+
if (!blockingAnnotations.has(activeItem.id)) continue;
|
|
300
|
+
const ann = blockingAnnotations.get(activeItem.id);
|
|
301
|
+
if (ann) {
|
|
302
|
+
activeItem._blockingToolCall = ann;
|
|
303
|
+
} else {
|
|
304
|
+
delete activeItem._blockingToolCall;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
242
310
|
// Agent status is now derived from dispatch.json at read time (getAgentStatus).
|
|
243
311
|
// No reconcile sweep needed — dispatch IS the source of truth.
|
|
244
312
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.670",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|