@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.
- package/dashboard/js/render-dispatch.js +5 -1
- package/dashboard.js +33 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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.
|
|
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"
|