@yemi33/minions 0.1.499 → 0.1.501

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/engine.js +43 -19
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,10 +1,14 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.499 (2026-04-07)
3
+ ## 0.1.501 (2026-04-07)
4
4
 
5
5
  ### Features
6
+ - explicitly-assigned work items bypass concurrency cap
6
7
  - strengthen CC dispatch contract with domain terminology mapping
7
8
 
9
+ ### Fixes
10
+ - comprehensive steering failure handling with user-visible feedback
11
+
8
12
  ## 0.1.498 (2026-04-07)
9
13
 
10
14
  ### Features
package/engine.js CHANGED
@@ -576,12 +576,27 @@ function spawnAgent(dispatchItem, config) {
576
576
  delete procInfo._steeringMessage;
577
577
  delete procInfo._steeringSessionId;
578
578
 
579
+ // Guard: can't resume without a session
580
+ if (!steerSessionId) {
581
+ log('warn', `Steering: no sessionId for ${agentId} — appending message to live output only`);
582
+ try { fs.appendFileSync(liveOutputPath, `\n[steering-failed] No session to resume. Message was: ${steerMsg}\n`); } catch {}
583
+ activeProcesses.delete(id);
584
+ completeDispatch(id, DISPATCH_RESULT.SUCCESS, 'Steering skipped (no session)', '', { processWorkItemFailure: false });
585
+ return;
586
+ }
587
+
579
588
  log('info', `Steering: re-spawning ${agentId} with --resume ${steerSessionId}`);
580
589
 
581
590
  // Write new prompt with steering message
582
591
  const steerPrompt = `Message from your human teammate:\n\n${steerMsg}\n\nRespond to this, then continue working on your current task.`;
583
592
  const steerPromptPath = path.join(ENGINE_DIR, 'tmp', `prompt-steer-${safeId}.md`);
584
- safeWrite(steerPromptPath, steerPrompt);
593
+ try { safeWrite(steerPromptPath, steerPrompt); } catch (e) {
594
+ log('warn', `Steering: failed to write prompt for ${agentId}: ${e.message}`);
595
+ try { fs.appendFileSync(liveOutputPath, `\n[steering-failed] Could not write prompt. Message was: ${steerMsg}\n`); } catch {}
596
+ activeProcesses.delete(id);
597
+ completeDispatch(id, DISPATCH_RESULT.SUCCESS, 'Steering prompt write failed', '', { processWorkItemFailure: false });
598
+ return;
599
+ }
585
600
 
586
601
  // Build resume args
587
602
  const resumeArgs = [
@@ -595,11 +610,21 @@ function spawnAgent(dispatchItem, config) {
595
610
 
596
611
  const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
597
612
  const childEnv = shared.cleanChildEnv();
598
- const resumeProc = runFile(process.execPath, [spawnScript, steerPromptPath, steerPromptPath, ...resumeArgs], {
599
- cwd,
600
- stdio: ['pipe', 'pipe', 'pipe'],
601
- env: childEnv,
602
- });
613
+ let resumeProc;
614
+ try {
615
+ resumeProc = runFile(process.execPath, [spawnScript, steerPromptPath, steerPromptPath, ...resumeArgs], {
616
+ cwd,
617
+ stdio: ['pipe', 'pipe', 'pipe'],
618
+ env: childEnv,
619
+ });
620
+ } catch (e) {
621
+ log('warn', `Steering: spawn failed for ${agentId}: ${e.message}`);
622
+ try { fs.appendFileSync(liveOutputPath, `\n[steering-failed] Spawn error: ${e.message}. Message was: ${steerMsg}\n`); } catch {}
623
+ try { fs.unlinkSync(steerPromptPath); } catch {}
624
+ activeProcesses.delete(id);
625
+ completeDispatch(id, DISPATCH_RESULT.SUCCESS, 'Steering spawn failed', '', { processWorkItemFailure: false });
626
+ return;
627
+ }
603
628
 
604
629
  // Re-attach to existing tracking
605
630
  activeProcesses.set(id, { proc: resumeProc, agentId, startedAt: procInfo.startedAt, sessionId: steerSessionId, _steeringAt: Date.now() });
@@ -635,6 +660,7 @@ function spawnAgent(dispatchItem, config) {
635
660
  try { fs.unlinkSync(steerPromptPath); } catch { /* cleanup */ }
636
661
  if (resumeCode !== 0) {
637
662
  log('warn', `Steering resume for ${agentId} exited with code ${resumeCode} | stderr: ${stderr.slice(-300).replace(/\n/g, ' ')}`);
663
+ try { fs.appendFileSync(liveOutputPath, `\n[steering-failed] Resume exited with code ${resumeCode}. Your message was received but the agent could not continue the session.\n`); } catch {}
638
664
  activeProcesses.delete(id);
639
665
  completeDispatch(id, DISPATCH_RESULT.SUCCESS, 'Steering resume failed but original work completed', '', { processWorkItemFailure: false });
640
666
  return;
@@ -644,6 +670,7 @@ function spawnAgent(dispatchItem, config) {
644
670
  });
645
671
  resumeProc.on('error', (err) => {
646
672
  log('warn', `Steering re-spawn error for ${agentId}: ${err.message}`);
673
+ try { fs.appendFileSync(liveOutputPath, `\n[steering-failed] Spawn error: ${err.message}. Your message was received but the agent could not resume.\n`); } catch {}
647
674
  activeProcesses.delete(id);
648
675
  completeDispatch(id, DISPATCH_RESULT.SUCCESS, 'Steering re-spawn error but original work completed', '', { processWorkItemFailure: false });
649
676
  });
@@ -2446,12 +2473,7 @@ async function tickInner() {
2446
2473
  const activeCount = (dispatch.active || []).length;
2447
2474
  const maxConcurrent = config.engine?.maxConcurrent || 5;
2448
2475
 
2449
- if (activeCount >= maxConcurrent) {
2450
- log('info', `At max concurrency (${activeCount}/${maxConcurrent}) — skipping dispatch`);
2451
- return;
2452
- }
2453
-
2454
- const slotsAvailable = maxConcurrent - activeCount;
2476
+ const slotsAvailable = Math.max(0, maxConcurrent - activeCount);
2455
2477
 
2456
2478
  // Priority dispatch: fixes > reviews > plan-to-prd > implement > verify > other
2457
2479
  const typePriority = { 'implement:large': 0, implement: 0, fix: 1, ask: 1, review: 2, test: 3, verify: 3, plan: 4, 'plan-to-prd': 4 };
@@ -2468,23 +2490,25 @@ async function tickInner() {
2468
2490
  return dp;
2469
2491
  });
2470
2492
 
2471
- // Only dispatch to agents that aren't already busy (one task per agent at a time).
2472
- // Build set of agents currently active.
2493
+ // Build set of agents currently active (one task per agent at a time).
2473
2494
  const busyAgents = new Set((dispatch.active || []).map(d => d.agent));
2474
- // Bug fix #14: deduplicate pending by dispatch ID to prevent double-dispatch.
2475
- // This guards against the same item appearing twice in the in-memory pending array.
2476
2495
  const seenPendingIds = new Set();
2477
2496
  const toDispatch = [];
2497
+ let generalSlots = slotsAvailable;
2498
+
2478
2499
  for (const item of dispatch.pending) {
2479
- if (toDispatch.length >= slotsAvailable) break;
2480
2500
  if (seenPendingIds.has(item.id)) {
2481
2501
  log('warn', `Duplicate dispatch ID ${item.id} in pending queue — skipping`);
2482
2502
  continue;
2483
2503
  }
2504
+ if (busyAgents.has(item.agent)) continue;
2505
+ // Items explicitly assigned to an agent bypass concurrency cap — dispatch if agent is free
2506
+ const isExplicitAssignment = !!item.meta?.item?.agent;
2507
+ if (!isExplicitAssignment && generalSlots <= 0) continue;
2484
2508
  seenPendingIds.add(item.id);
2485
- if (busyAgents.has(item.agent)) continue; // agent already has an active task
2486
2509
  toDispatch.push(item);
2487
- busyAgents.add(item.agent); // mark busy for this dispatch round too
2510
+ busyAgents.add(item.agent);
2511
+ if (!isExplicitAssignment) generalSlots--;
2488
2512
  }
2489
2513
 
2490
2514
  // Dispatch items — spawnAgent moves each from pending→active on disk.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.499",
3
+ "version": "0.1.501",
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"