@yemi33/minions 0.1.669 → 0.1.671

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 CHANGED
@@ -1,8 +1,12 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.669 (2026-04-09)
3
+ ## 0.1.671 (2026-04-09)
4
+
5
+ ### Features
6
+ - populate _artifacts on work items for detail modal
4
7
 
5
8
  ### Fixes
9
+ - surface blocking tool call state in dashboard (closes #619) (#634)
6
10
  - interrupt blocking tool calls immediately on steering (closes #627) (#632)
7
11
  - reduce Bash blocking grace and errored task dedup window (closes #593) (#630)
8
12
 
@@ -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)">&#x23F3; Blocking tool call (${escHtml(a._blockingToolCall.tool)}) &mdash; 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)">&#x26A0; ${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)">&#x23F3; Blocking tool call (' + escHtml(agent._blockingToolCall.tool) + ') &mdash; 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
@@ -701,6 +706,29 @@ function getWorkItems(config) {
701
706
  }
702
707
  }
703
708
 
709
+ // Populate _artifacts for the work item detail modal
710
+ for (const item of allItems) {
711
+ const arts = {};
712
+ const agentId = item.dispatched_to || item.agent;
713
+ if (agentId) {
714
+ // Output log — find most recent matching dispatch output
715
+ const agentDir = path.join(MINIONS_DIR, 'agents', agentId);
716
+ try {
717
+ const files = safeReadDir(agentDir).filter(f => f.startsWith('output-') && f.includes(item.id?.slice(-8) || '___') && f.endsWith('.log'));
718
+ if (files.length > 0) arts.outputLog = agentId + '/' + files[files.length - 1];
719
+ } catch {}
720
+ // Inbox notes created by this agent for this task
721
+ try {
722
+ const inboxFiles = safeReadDir(INBOX_DIR).filter(f => f.includes(agentId) && (f.includes(item.id || '___') || f.includes(item.id?.slice(-8) || '___')));
723
+ if (inboxFiles.length > 0) arts.notes = inboxFiles;
724
+ } catch {}
725
+ }
726
+ if (item.branch) arts.branch = item.branch;
727
+ if (item.sourcePlan) arts.sourcePlan = item.sourcePlan;
728
+ if (item._pr) arts.pr = item._pr;
729
+ if (Object.keys(arts).length > 0) item._artifacts = arts;
730
+ }
731
+
704
732
  const statusOrder = {
705
733
  pending: 0,
706
734
  queued: 0,
package/engine/timeout.js CHANGED
@@ -146,6 +146,7 @@ function checkTimeouts(config) {
146
146
  // Uses live-output.log mtime as heartbeat. If no output for heartbeatTimeout, agent is dead.
147
147
  const dispatchData = getDispatch();
148
148
  const deadItems = [];
149
+ const blockingAnnotations = new Map(); // id → { tool, silentMs, remainingMs } or null (clear)
149
150
 
150
151
  for (const item of (dispatchData.active || [])) {
151
152
  if (!item.agent) continue;
@@ -198,6 +199,7 @@ function checkTimeouts(config) {
198
199
  // Check for BOTH tracked and untracked processes (orphan case after engine restart)
199
200
  let isBlocking = false;
200
201
  let blockingTimeout = heartbeatTimeout;
202
+ let blockingTool = '';
201
203
  if (silentMs > heartbeatTimeout) {
202
204
  try {
203
205
  const liveLog = safeRead(liveLogPath);
@@ -218,6 +220,7 @@ function checkTimeouts(config) {
218
220
  const taskTimeout = input.timeout || 600000; // default 10min
219
221
  blockingTimeout = Math.max(heartbeatTimeout, taskTimeout + 60000); // task timeout + 1min grace
220
222
  isBlocking = true;
223
+ blockingTool = 'TaskOutput';
221
224
  }
222
225
  // Bash tool call — may be running a long build/install with no stdout
223
226
  if (name === 'Bash') {
@@ -225,21 +228,35 @@ function checkTimeouts(config) {
225
228
  const bashTimeout = input.timeout || 120000;
226
229
  blockingTimeout = Math.max(heartbeatTimeout, bashTimeout + 60000);
227
230
  isBlocking = true;
231
+ blockingTool = 'Bash';
228
232
  }
229
233
  // Agent (subagent) tool call — parent waits silently for child to complete
230
234
  if (name === 'Agent') {
231
235
  blockingTimeout = Math.max(heartbeatTimeout, 1800000); // 30min for subagents
232
236
  isBlocking = true;
237
+ blockingTool = 'Agent';
233
238
  }
234
239
  break; // only check the most recent tool_use
235
240
  } catch { /* JSON parse — line may not be valid JSON */ }
236
241
  }
237
242
  if (isBlocking) {
238
- log('info', `Agent ${item.agent} (${item.id}) is in a blocking tool call extended timeout to ${Math.round(blockingTimeout / 1000)}s (silent for ${silentSec}s)`);
243
+ // Only log on transitionavoid 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
+ });
239
252
  }
240
253
  }
241
254
  } catch (e) { log('warn', 'blocking tool detection: ' + e.message); }
242
255
  }
256
+ // Agent recovered from blocking state — clear annotation
257
+ if (!isBlocking && item._blockingToolCall) {
258
+ blockingAnnotations.set(item.id, null);
259
+ }
243
260
 
244
261
  const effectiveTimeout = isBlocking ? blockingTimeout : heartbeatTimeout;
245
262
 
@@ -273,6 +290,23 @@ function checkTimeouts(config) {
273
290
  completeDispatch(item.id, DISPATCH_RESULT.ERROR, reason);
274
291
  }
275
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
+
276
310
  // Agent status is now derived from dispatch.json at read time (getAgentStatus).
277
311
  // No reconcile sweep needed — dispatch IS the source of truth.
278
312
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.669",
3
+ "version": "0.1.671",
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"