@yemi33/minions 0.1.2387 → 0.1.2388

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.
@@ -507,7 +507,39 @@ function _isRespawnEligibleEngineState(state) {
507
507
  return state === 'running' || state === 'degraded';
508
508
  }
509
509
 
510
- function checkEngine(now) {
510
+ const ENGINE_WATCHDOG_OPERATION_NAMES = [
511
+ 'isPidAlive',
512
+ 'reapStrayProcesses',
513
+ 'spawnEngine',
514
+ 'recordCrashLoopRespawn',
515
+ ];
516
+
517
+ // Isolated state does not isolate this module's source checkout, so tests must
518
+ // provide every OS/process side effect before an engine respawn is possible.
519
+ function _resolveEngineWatchdogOperations(injected) {
520
+ const provided = injected && typeof injected === 'object' ? injected : {};
521
+ if (process.env.MINIONS_TEST_DIR) {
522
+ const missing = ENGINE_WATCHDOG_OPERATION_NAMES
523
+ .filter(name => typeof provided[name] !== 'function');
524
+ if (missing.length > 0) {
525
+ throw new Error(
526
+ `MINIONS_TEST_DIR is set; inject engine watchdog operations instead of using real process control: ${missing.join(', ')}`,
527
+ );
528
+ }
529
+ }
530
+ return {
531
+ isPidAlive: typeof provided.isPidAlive === 'function' ? provided.isPidAlive : isPidAlive,
532
+ reapStrayProcesses: typeof provided.reapStrayProcesses === 'function'
533
+ ? provided.reapStrayProcesses
534
+ : reapStrayProcesses,
535
+ spawnEngine: typeof provided.spawnEngine === 'function' ? provided.spawnEngine : spawnEngine,
536
+ recordCrashLoopRespawn: typeof provided.recordCrashLoopRespawn === 'function'
537
+ ? provided.recordCrashLoopRespawn
538
+ : _recordCrashLoopRespawn,
539
+ };
540
+ }
541
+
542
+ function checkEngine(now, operations) {
511
543
  if (now - _lastEngineRespawnAt < POST_SPAWN_GRACE_MS) return;
512
544
  const control = safeReadJson(CONTROL_PATH_FN());
513
545
  // Respawn when control.json says "running" (PID died unexpectedly) or
@@ -515,7 +547,6 @@ function checkEngine(now) {
515
547
  // above). "paused"/"stopped"/"stopping" are legitimate user-intent states the
516
548
  // supervisor must not override.
517
549
  if (!control || !_isRespawnEligibleEngineState(control.state)) return;
518
- if (control.pid && isPidAlive(control.pid)) return;
519
550
 
520
551
  // Cross-watchdog race guard: dashboard.js's in-process engine watchdog
521
552
  // writes `{pid: null, restarted_at}` BEFORE spawning a new engine. During
@@ -530,13 +561,16 @@ function checkEngine(now) {
530
561
  }
531
562
  }
532
563
 
564
+ const ops = _resolveEngineWatchdogOperations(operations);
565
+ if (control.pid && ops.isPidAlive(control.pid)) return;
566
+
533
567
  console.log(`[supervisor] Engine PID ${control.pid || '(none)'} is dead — respawning...`);
534
568
  // Reap any orphan engines first so a stale control.json PID can't snowball
535
569
  // into an unbounded pile of contending engine processes.
536
- reapStrayProcesses(path.join(MINIONS_DIR, 'engine.js'), 'engine');
537
- const newPid = spawnEngine();
570
+ ops.reapStrayProcesses(path.join(MINIONS_DIR, 'engine.js'), 'engine');
571
+ const newPid = ops.spawnEngine();
538
572
  _lastEngineRespawnAt = now;
539
- _recordCrashLoopRespawn('supervisor:dead-pid');
573
+ ops.recordCrashLoopRespawn('supervisor:dead-pid');
540
574
  console.log(`[supervisor] Engine respawned (new PID: ${newPid})`);
541
575
  }
542
576
 
@@ -546,7 +580,7 @@ function checkEngine(now) {
546
580
  // is alive but control.heartbeat has not advanced for SUPERVISOR_STALE_ENGINE_HEARTBEAT_MS.
547
581
  // The heartbeat is written every ~15s by a dedicated timer in engine/cli.js; if
548
582
  // it goes stale while the PID persists, the event loop is stuck.
549
- function checkEngineHung(now) {
583
+ function checkEngineHung(now, operations) {
550
584
  if (now - _lastEngineRespawnAt < POST_SPAWN_GRACE_MS) return;
551
585
  const control = safeReadJson(CONTROL_PATH_FN());
552
586
  // Include 'degraded' (see _isRespawnEligibleEngineState) so a permanently
@@ -554,8 +588,6 @@ function checkEngineHung(now) {
554
588
  // re-enters) still gets force-restarted once its own heartbeat goes stale,
555
589
  // instead of being ignored forever the moment the state flips to 'degraded'.
556
590
  if (!control || !_isRespawnEligibleEngineState(control.state)) return;
557
- // Only acts when the PID is alive — a dead PID is handled by checkEngine().
558
- if (!control.pid || !isPidAlive(control.pid)) return;
559
591
 
560
592
  const heartbeat = Number(control.heartbeat);
561
593
  if (!heartbeat || !Number.isFinite(heartbeat)) return; // no heartbeat yet (fresh start)
@@ -563,15 +595,20 @@ function checkEngineHung(now) {
563
595
  const heartbeatAge = now - heartbeat;
564
596
  if (heartbeatAge < SUPERVISOR_STALE_ENGINE_HEARTBEAT_MS) return;
565
597
 
598
+ // Only acts when the PID is alive — a dead PID is handled by checkEngine().
599
+ if (!control.pid) return;
600
+ const ops = _resolveEngineWatchdogOperations(operations);
601
+ if (!ops.isPidAlive(control.pid)) return;
602
+
566
603
  console.log(
567
604
  `[supervisor] Engine PID ${control.pid} is alive but heartbeat is stale ` +
568
605
  `(${Math.round(heartbeatAge / 1000)}s old, threshold ${SUPERVISOR_STALE_ENGINE_HEARTBEAT_MS / 1000}s) — ` +
569
606
  `event loop likely frozen; restarting engine`,
570
607
  );
571
- reapStrayProcesses(path.join(MINIONS_DIR, 'engine.js'), 'engine');
572
- const newPid = spawnEngine();
608
+ ops.reapStrayProcesses(path.join(MINIONS_DIR, 'engine.js'), 'engine');
609
+ const newPid = ops.spawnEngine();
573
610
  _lastEngineRespawnAt = now;
574
- _recordCrashLoopRespawn('supervisor:stale-heartbeat');
611
+ ops.recordCrashLoopRespawn('supervisor:stale-heartbeat');
575
612
  console.log(`[supervisor] Engine restarted due to stale heartbeat (new PID: ${newPid})`);
576
613
  }
577
614
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2387",
3
+ "version": "0.1.2388",
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"