@yemi33/minions 0.1.957 → 0.1.959

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,9 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.957 (2026-04-14)
3
+ ## 0.1.959 (2026-04-14)
4
4
 
5
5
  ### Features
6
+ - surface in-flight tool calls in lastAction (#1064)
6
7
  - wire agentBusyReassignMs into settings UI and persist
7
8
  - ADO throttle detection, poll guards, and dashboard banner (#1051)
8
9
  - gate auto-fix conflict dispatch behind autoFixConflicts flag
@@ -22,9 +23,11 @@
22
23
  - replace magic string 'active' with PR_STATUS.ACTIVE in lifecycle.js (#969)
23
24
  - fix dashboard plan-pause nested lock violation (#968)
24
25
  - add missing branch_name to central dispatch vars (#967)
25
- - fix dispatch.js mutator fallback to use nullish coalescing (#966)
26
26
 
27
27
  ### Fixes
28
+ - fix CC queued message 'already processing' and thinking UX stacking
29
+ - enforce --timeout 1 on all azureauth calls to prevent agent orphans (#1063)
30
+ - cancel steering kill watcher on resume spawn (#1052) (#1062)
28
31
  - suppress warn for optional template variables (#1061)
29
32
  - fix ENGINE_DEFAULTS ref and derive boolean settings from defaults
30
33
  - add autoFixConflicts to boolean settings persist list
@@ -42,9 +45,6 @@
42
45
  - show doc-chat badges on Notes and KB items (#1016)
43
46
  - steering kill on no-session re-queues dispatch instead of erroring (#1015)
44
47
  - inject cached ADO token into spawned agents (#998) (#1012)
45
- - qaAbort race + skip kill on completed doc-chat
46
- - doc-chat Stop button actually works — release server guard on disconnect
47
- - CC streaming auto-retries fresh session when resume fails
48
48
 
49
49
  ### Other
50
50
  - test(engine): add regression tests for autoFixConflicts flag and DEFAULTS alias
@@ -547,8 +547,9 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
547
547
  // 429 = server still releasing previous request (abort race) — retry silently up to 3 times
548
548
  if (res.status === 429 && (!activeTab._429retries || activeTab._429retries < 3)) {
549
549
  activeTab._429retries = (activeTab._429retries || 0) + 1;
550
+ _cleanupStreamDiv(); // remove current thinking div — prevents stacking on each retry
550
551
  await new Promise(function(r) { setTimeout(r, 1500); });
551
- return await _ccDoSend(message, true); // retry with skipUserMsg=true (already displayed)
552
+ return await _ccDoSend(message, true, forceTabId || activeTabId); // retry pass tabId so timer closures don't fight
552
553
  }
553
554
  activeTab._429retries = 0;
554
555
  _cleanupStreamDiv();
package/dashboard.js CHANGED
@@ -471,6 +471,7 @@ setInterval(() => {
471
471
  const CC_SESSION_MAX_TURNS = Infinity;
472
472
  let ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
473
473
  const ccInFlightTabs = new Set(); // per-tab in-flight tracking for parallel CC requests
474
+ const ccInFlightAborts = new Map(); // tabId → abortFn — lets a new request kill the stale LLM
474
475
  const CC_INFLIGHT_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes — auto-release if request hangs
475
476
 
476
477
  // _ccPromptHash computed after CC_STATIC_SYSTEM_PROMPT is defined (see below)
@@ -3400,7 +3401,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3400
3401
  // Per-tab concurrency guard
3401
3402
  const tabId = body.tabId || 'default';
3402
3403
  if (ccInFlightTabs.has(tabId)) {
3403
- return jsonReply(res, 429, { error: 'This tab is already processing — wait or open a new tab.' });
3404
+ await new Promise(r => setTimeout(r, 200));
3405
+ if (ccInFlightTabs.has(tabId)) {
3406
+ return jsonReply(res, 429, { error: 'This tab is already processing — wait or open a new tab.' });
3407
+ }
3404
3408
  }
3405
3409
  ccInFlightTabs.add(tabId);
3406
3410
 
@@ -3471,7 +3475,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3471
3475
  if (!body.message) { res.statusCode = 400; res.end('message required'); return; }
3472
3476
  const tabId = body.tabId || 'default';
3473
3477
  if (ccInFlightTabs.has(tabId)) {
3474
- res.statusCode = 429; res.end('This tab is already processing'); return;
3478
+ // Previous request still in-flight — abort its LLM (handles keep-alive abort where close event didn't fire)
3479
+ const prevAbort = ccInFlightAborts.get(tabId);
3480
+ if (prevAbort) { prevAbort(); }
3481
+ await new Promise(r => setTimeout(r, 200)); // let previous finally run and release the lock
3482
+ if (ccInFlightTabs.has(tabId)) {
3483
+ res.statusCode = 429; res.end('This tab is already processing'); return;
3484
+ }
3475
3485
  }
3476
3486
  ccInFlightTabs.add(tabId);
3477
3487
 
@@ -3481,6 +3491,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3481
3491
  // Kill LLM process immediately if client disconnects mid-stream
3482
3492
  req.on('close', () => {
3483
3493
  ccInFlightTabs.delete(tabId);
3494
+ ccInFlightAborts.delete(tabId);
3484
3495
  if (!_ccStreamEnded && _ccStreamAbort) {
3485
3496
  console.log(`[CC-stream] Client disconnected for tab ${tabId} — aborting LLM`);
3486
3497
  _ccStreamAbort();
@@ -3523,6 +3534,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3523
3534
  }
3524
3535
  });
3525
3536
  _ccStreamAbort = llmPromise.abort;
3537
+ ccInFlightAborts.set(tabId, _ccStreamAbort); // store so a new request for this tab can abort this LLM
3526
3538
  const result = await llmPromise;
3527
3539
  trackUsage('command-center', result.usage);
3528
3540
 
@@ -3605,9 +3617,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3605
3617
  _ccStreamEnded = true; res.end();
3606
3618
  } finally {
3607
3619
  ccInFlightTabs.delete(tabId);
3620
+ ccInFlightAborts.delete(tabId);
3608
3621
  }
3609
3622
  } catch (e) {
3610
3623
  ccInFlightTabs.delete(tabId);
3624
+ ccInFlightAborts.delete(tabId);
3611
3625
  try { res.write('data: ' + JSON.stringify({ type: 'error', error: e.message }) + '\n\n'); } catch {}
3612
3626
  _ccStreamEnded = true; try { res.end(); } catch {}
3613
3627
  }
@@ -137,7 +137,7 @@ The engine directly polls the Azure DevOps REST API for **all** PR metadata: bui
137
137
  | `buildStatus` | PR statuses (codecoverage/deploy/build/ci contexts) | `passing` / `failing` / `running` / `none` |
138
138
  | `buildFailReason` | Failed status description | Set on failure, cleared otherwise |
139
139
 
140
- **Auth:** Bearer token via `azureauth ado token --output token` (cached 30 minutes).
140
+ **Auth:** Bearer token via `azureauth ado token --output token --timeout 1` (cached 30 minutes). The `--timeout 1` flag is required — without it, azureauth can hang indefinitely in headless sessions.
141
141
 
142
142
  This feeds `discoverFromPrs` — when `buildStatus` flips to `"failing"`, the next discovery tick dispatches a fix agent. When `status` becomes `"merged"`, the PR drops out of active polling.
143
143
 
package/engine/queries.js CHANGED
@@ -42,6 +42,34 @@ function readHeadTail(filePath, bytes = 1024) {
42
42
  }
43
43
  }
44
44
 
45
+ /**
46
+ * Detect in-flight tool calls from live-output.log tail content.
47
+ * Scans for task_started events with no matching task_notification (by task_id).
48
+ * Returns { description, taskId } for the most recent in-flight tool, or null.
49
+ */
50
+ function detectInFlightTool(tail) {
51
+ if (!tail) return null;
52
+ const lines = tail.split('\n');
53
+ const completed = new Set();
54
+
55
+ // Reverse scan: collect task_notification ids first (most recent lines),
56
+ // then the first task_started not in completed is the in-flight tool.
57
+ for (let i = lines.length - 1; i >= 0; i--) {
58
+ const line = lines[i];
59
+ if (!line.includes('"task_')) continue; // fast skip non-task lines
60
+ try {
61
+ const parsed = JSON.parse(line);
62
+ if (parsed.type !== 'system') continue;
63
+ if (parsed.subtype === 'task_notification') {
64
+ completed.add(parsed.task_id);
65
+ } else if (parsed.subtype === 'task_started' && !completed.has(parsed.task_id)) {
66
+ return { description: parsed.description || null, taskId: parsed.task_id };
67
+ }
68
+ } catch { /* not valid JSON — skip heartbeats, headers, partial lines */ }
69
+ }
70
+ return null;
71
+ }
72
+
45
73
  // ── Paths ───────────────────────────────────────────────────────────────────
46
74
 
47
75
  const MINIONS_DIR = shared.MINIONS_DIR;
@@ -220,7 +248,7 @@ function getAgentStatus(agentId) {
220
248
  if (active._blockingToolCall) {
221
249
  result._blockingToolCall = active._blockingToolCall;
222
250
  }
223
- // Detect permission-waiting: read only head+tail of live-output.log (max 2KB total)
251
+ // Detect permission-waiting and in-flight tools: read only head+tail of live-output.log (max 2KB total)
224
252
  try {
225
253
  const liveLogPath = path.join(AGENTS_DIR, agentId, 'live-output.log');
226
254
  const { head, tail } = readHeadTail(liveLogPath, 1024);
@@ -239,6 +267,11 @@ function getAgentStatus(agentId) {
239
267
  result._warning = 'Possibly waiting for permission approval — agent is not in bypass mode';
240
268
  }
241
269
  }
270
+ // Detect in-flight tool calls (task_started with no task_notification)
271
+ const inFlight = detectInFlightTool(tail);
272
+ if (inFlight && inFlight.description) {
273
+ result._runningToolDescription = inFlight.description;
274
+ }
242
275
  }
243
276
  } catch { /* optional — don't block status */ }
244
277
  return result;
@@ -328,7 +361,7 @@ function getAgents(config) {
328
361
  const s = getAgentStatus(a.id); // derives from dispatch.json
329
362
 
330
363
  let lastAction = 'Waiting for assignment';
331
- if (s.status === 'working') lastAction = `Working: ${s.task}`;
364
+ if (s.status === 'working') lastAction = s._runningToolDescription ? `Running: ${s._runningToolDescription}` : `Working: ${s.task}`;
332
365
  else if (s.status === 'done') lastAction = `Done: ${s.task}`;
333
366
  else if (s.status === 'error') lastAction = `Error: ${s.task}`;
334
367
  else if (inboxFiles.length > 0) {
@@ -1033,6 +1066,7 @@ module.exports = {
1033
1066
  // Helpers
1034
1067
  timeSince,
1035
1068
  readHeadTail, // exported for testing
1069
+ detectInFlightTool, // exported for testing
1036
1070
  resetPrdInfoCache,
1037
1071
 
1038
1072
  // Core state
package/engine.js CHANGED
@@ -1051,8 +1051,12 @@ async function spawnAgent(dispatchItem, config) {
1051
1051
  return;
1052
1052
  }
1053
1053
 
1054
- // Re-attach to existing tracking
1055
- activeProcesses.set(id, { proc: resumeProc, agentId, startedAt: procInfo.startedAt, sessionId: steerSessionId, _steeringAt: Date.now(), lastRealOutputAt: Date.now() });
1054
+ // Re-attach to existing tracking — do NOT carry _steeringAt forward (#1052).
1055
+ // The kill watcher in timeout.js fires 30s after _steeringAt is set. If we carry it
1056
+ // into the resumed process, it kills the resumed session. The kill watcher only exists
1057
+ // to handle cases where the original kill didn't take effect — once the process has
1058
+ // exited and the resume is spawned, _steeringAt must not be present.
1059
+ activeProcesses.set(id, { proc: resumeProc, agentId, startedAt: procInfo.startedAt, sessionId: steerSessionId, lastRealOutputAt: Date.now() });
1056
1060
 
1057
1061
  // Reset output buffers so post-completion parsing only sees the resumed session
1058
1062
  stdout = '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.957",
3
+ "version": "0.1.959",
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"
@@ -54,3 +54,5 @@ node engine/ado-status.js <prNumber> --live --project MyProject
54
54
  Output is JSON with the same fields. Exit 0 on success, 1 if not found.
55
55
 
56
56
  **Never make raw `curl` calls to ADO APIs directly.** Use `node engine/ado-status.js` which routes through `ado.js` — authenticated, retried, circuit-broken. Raw `azureauth` + curl bypasses all of that.
57
+
58
+ **If you must run `azureauth` directly, ALWAYS include `--timeout 1`.** Without this flag, `azureauth ado token` can hang indefinitely waiting for interactive broker UI that never appears in headless agent sessions. This causes the Claude Code process to silently exit and the engine to declare the agent orphaned. Example: `azureauth ado token --mode iwa --mode broker --output token --timeout 1`.