@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.
- package/engine/supervisor.js +48 -11
- package/package.json +1 -1
package/engine/supervisor.js
CHANGED
|
@@ -507,7 +507,39 @@ function _isRespawnEligibleEngineState(state) {
|
|
|
507
507
|
return state === 'running' || state === 'degraded';
|
|
508
508
|
}
|
|
509
509
|
|
|
510
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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"
|