@yemi33/minions 0.1.2269 → 0.1.2270

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.
@@ -103,7 +103,11 @@ function renderEngineStatus(engine) {
103
103
  // against a possibly-cached heartbeat — the bug pattern that produced false-
104
104
  // positive STALE banners after control.json was dropped from the mtime tracker.
105
105
  const staleMs = engine?.heartbeatAgeMs || 0;
106
- if (state === 'running' && engine?.heartbeatStale) state = 'stale';
106
+ // Map running→stale and degraded→stale when both heartbeat signals are stale.
107
+ // degraded is set by the dashboard watchdog (issue #423) when the engine PID
108
+ // is alive but the tick loop is frozen; treat it identically to stale so the
109
+ // Restart Engine banner fires in both cases.
110
+ if ((state === 'running' || state === 'degraded') && engine?.heartbeatStale) state = 'stale';
107
111
 
108
112
  // Clear restart grace as soon as the engine reports a fresh heartbeat — the
109
113
  // new engine has caught up, so STALE/restart banners should vanish.
package/dashboard.js CHANGED
@@ -5951,6 +5951,35 @@ function restartEngine() {
5951
5951
  return newPid;
5952
5952
  }
5953
5953
 
5954
+ // ── Frozen-engine detection (issue #423) ─────────────────────────────────────
5955
+ // Called by the 30s watchdog when the engine PID is still alive. If both
5956
+ // control.heartbeat AND control.lastTickAt have aged past their thresholds,
5957
+ // the tick loop is frozen (event-loop blocked). Flip state to 'degraded' so:
5958
+ // • The dashboard UI surfaces a hard warning with a Restart Engine button.
5959
+ // • When the engine eventually unhangs, tickInner reads state !== 'running'
5960
+ // and calls process.exit(0); the PID-dead watchdog then auto-restarts it.
5961
+ // The restarted engine reclaims state:'running' on its next boot.
5962
+ // Returns true if it wrote the degraded state, false otherwise.
5963
+ function _markEngineAsDegradedIfFrozen() {
5964
+ const control = getEngineState();
5965
+ if (control.state !== 'running' || !control.pid) return false;
5966
+
5967
+ const hbAge = control.heartbeat ? Date.now() - control.heartbeat : 0;
5968
+ const tickInterval = Number(CONFIG?.engine?.tickInterval) || shared.ENGINE_DEFAULTS.tickInterval;
5969
+ const tickStaleThresholdMs = Math.max(ENGINE_HEARTBEAT_STALE_MS, 2 * tickInterval);
5970
+ const tickAge = control.lastTickAt ? Date.now() - control.lastTickAt : Infinity;
5971
+
5972
+ const bothStale = !!(control.heartbeat
5973
+ && hbAge > ENGINE_HEARTBEAT_STALE_MS
5974
+ && tickAge > tickStaleThresholdMs);
5975
+ if (!bothStale) return false;
5976
+
5977
+ shared.mutateControl(c => ({ ...c, state: 'degraded' }));
5978
+ console.log(`[watchdog] Engine tick frozen (heartbeat ${Math.round(hbAge / 1000)}s old, last tick ${Math.round(tickAge / 1000)}s old) — marking state as degraded`);
5979
+ try { invalidateStatusCache(); } catch { /* best effort */ }
5980
+ return true;
5981
+ }
5982
+
5954
5983
  // -- Server --
5955
5984
 
5956
5985
  // Mutating HTTP methods that require Origin and Content-Type gating.
@@ -14415,6 +14444,8 @@ module.exports = {
14415
14444
  // staleness verdict it stamps on engine.heartbeatStale is the contract under
14416
14445
  // test. No production caller imports this; it is a test seam.
14417
14446
  _buildStatusFastState,
14447
+ // #423 — exported for unit testing the frozen-engine watchdog logic.
14448
+ _markEngineAsDegradedIfFrozen,
14418
14449
  // W-mq5xg5e9000nec0e — exported for direct unit testing of the slim shape
14419
14450
  // produced by GET /api/work-items. Production callers go through the
14420
14451
  // route's `builder` closure (getWorkItems().map(slimWorkItemForList)).
@@ -14643,6 +14674,8 @@ if (require.main === module) {
14643
14674
  if (!alive) {
14644
14675
  console.log(`[watchdog] Engine PID ${control.pid} is dead — auto-restarting...`);
14645
14676
  restartEngine();
14677
+ } else {
14678
+ _markEngineAsDegradedIfFrozen();
14646
14679
  }
14647
14680
  } catch (e) {
14648
14681
  console.error(`[watchdog] Error: ${e.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2269",
3
+ "version": "0.1.2270",
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"