@yemi33/minions 0.1.1992 → 0.1.1993
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/command-center.js +107 -25
- package/dashboard.js +11 -0
- package/package.json +1 -1
|
@@ -179,48 +179,130 @@ async function _ccDashboardHealth() {
|
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
// Pure predicate — extracted so test/unit/dashboard-resilience.test.js can
|
|
183
|
+
// behaviour-test the new-vs-old-instance decision without standing up a DOM.
|
|
184
|
+
// Returns true only when newDashId is provably a NEW dashboard process:
|
|
185
|
+
// - When preRestartDashId is known, any difference proves a different process.
|
|
186
|
+
// - When preRestartDashId is unknown (page just loaded, _lastStatus empty),
|
|
187
|
+
// accept only if newDashId parses to a timestamp strictly AFTER the click.
|
|
188
|
+
// Guards against the still-alive OLD dashboard answering /api/status
|
|
189
|
+
// before its kill lands (~1s into the restart child's lifetime).
|
|
190
|
+
function _ccIsNewDashboardInstance(preRestartDashId, newDashId, clickTimeMs) {
|
|
191
|
+
if (!newDashId) return false;
|
|
192
|
+
if (preRestartDashId) return newDashId !== preRestartDashId;
|
|
193
|
+
var parsed = Date.parse(newDashId);
|
|
194
|
+
return Number.isFinite(parsed) && parsed > clickTimeMs;
|
|
195
|
+
}
|
|
196
|
+
|
|
182
197
|
// Triggered by the CC "Restart Minions" recovery button when a stale dashboard
|
|
183
198
|
// connection is killing CC streams with "Failed to fetch". Spawns the same
|
|
184
199
|
// `minions restart` flow as the CLI command (kills + respawns engine AND
|
|
185
|
-
// dashboard)
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
200
|
+
// dashboard) and reloads once the NEW dashboard is up.
|
|
201
|
+
//
|
|
202
|
+
// W-mpehvbed0018e27d: previous version polled /api/health which returns 200
|
|
203
|
+
// from EITHER the still-alive OLD dashboard (before the spawned restart child
|
|
204
|
+
// finished its kill sequence) OR the NEW one. Reloading on the OLD instance
|
|
205
|
+
// landed the page on a dying port; reloading on the NEW instance with a cold
|
|
206
|
+
// status cache hung the freshly-reloaded page on its first /api/status fetch.
|
|
207
|
+
// Both produced the "click does nothing / page still broken" symptoms.
|
|
208
|
+
//
|
|
209
|
+
// Fix:
|
|
210
|
+
// 1. Capture pre-restart dashboardStartedAt before POSTing the restart
|
|
211
|
+
// (so we can detect the NEW instance vs the still-alive OLD one).
|
|
212
|
+
// 2. Poll /api/status (not /api/health) — a 200 response proves the status
|
|
213
|
+
// cache has been built once, so the post-reload first request is warm.
|
|
214
|
+
// 3. Only reload when _ccIsNewDashboardInstance() confirms a different
|
|
215
|
+
// dashboardStartedAt, with per-poll AbortController so a hanging cold
|
|
216
|
+
// rebuild can't starve the global deadline.
|
|
217
|
+
// 4. Cache-bust the reload (?cb=) so an updated dashboard JS bundle is not
|
|
218
|
+
// served from the browser disk cache. location.replace() so the broken
|
|
219
|
+
// pre-restart URL doesn't pollute history.
|
|
189
220
|
async function ccRestartMinions(btn) {
|
|
190
221
|
if (btn) { try { btn.disabled = true; btn.textContent = 'Restarting...'; } catch {} }
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
//
|
|
194
|
-
//
|
|
222
|
+
var clickTimeMs = Date.now();
|
|
223
|
+
|
|
224
|
+
// Capture pre-restart dashboard instance ID BEFORE the POST so the polling
|
|
225
|
+
// step has a baseline to compare against. _lastStatus is populated by
|
|
226
|
+
// refresh.js's 4s poll; on a freshly-opened page it may still be null, so
|
|
227
|
+
// we fall back to a bounded /api/status fetch, then to clickTime-based
|
|
228
|
+
// detection inside _ccIsNewDashboardInstance.
|
|
229
|
+
var preRestartDashId = (window._lastStatus && window._lastStatus.version)
|
|
230
|
+
? window._lastStatus.version.dashboardStartedAt : null;
|
|
231
|
+
if (!preRestartDashId) {
|
|
232
|
+
try {
|
|
233
|
+
var preCtl = new AbortController();
|
|
234
|
+
var preTimer = setTimeout(function() { preCtl.abort(); }, 2000);
|
|
235
|
+
try {
|
|
236
|
+
var preRes = await fetch('/api/status', { cache: 'no-store', signal: preCtl.signal });
|
|
237
|
+
if (preRes && preRes.ok) {
|
|
238
|
+
var preData = await preRes.json().catch(function() { return null; });
|
|
239
|
+
if (preData && preData.version) preRestartDashId = preData.version.dashboardStartedAt || null;
|
|
240
|
+
}
|
|
241
|
+
} finally { clearTimeout(preTimer); }
|
|
242
|
+
} catch { /* best-effort — clickTime fallback inside helper still covers us */ }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Fire-and-forget the restart POST. We do NOT await it — the dashboard often
|
|
246
|
+
// kills its own process before the response is flushed, so the fetch throws
|
|
247
|
+
// even though the restart child (a detached `minions restart`) is happily
|
|
248
|
+
// running. The polling loop is the source of truth for completion.
|
|
195
249
|
try {
|
|
196
250
|
fetch('/api/dashboard/restart', { method: 'POST', headers: { 'Content-Type': 'application/json' } })
|
|
197
251
|
.catch(function() { /* dashboard process likely killed mid-response — expected */ });
|
|
198
252
|
} catch { /* network layer threw before fetch even queued — also expected */ }
|
|
199
253
|
if (btn) { try { btn.textContent = 'Restarting Minions — waiting for new dashboard...'; } catch {} }
|
|
254
|
+
|
|
200
255
|
var startedAt = Date.now();
|
|
201
|
-
var DEADLINE_MS =
|
|
256
|
+
var DEADLINE_MS = 90000; // generous: cold Windows AV + status rebuild can eat 30s
|
|
202
257
|
var INTERVAL_MS = 500;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
258
|
+
var POLL_TIMEOUT_MS = 5000;
|
|
259
|
+
|
|
260
|
+
function reloadWithCacheBust(label) {
|
|
261
|
+
if (btn) { try { btn.textContent = label; } catch {} }
|
|
262
|
+
try {
|
|
263
|
+
var url = new URL(location.href);
|
|
264
|
+
url.searchParams.set('cb', String(Date.now()));
|
|
265
|
+
location.replace(url.toString());
|
|
266
|
+
} catch {
|
|
267
|
+
try { location.reload(); } catch {}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function pollStatus() {
|
|
272
|
+
var ctl = new AbortController();
|
|
273
|
+
var pollTimer = setTimeout(function() { ctl.abort(); }, POLL_TIMEOUT_MS);
|
|
274
|
+
var shouldRetry = false;
|
|
275
|
+
try {
|
|
276
|
+
var res = await fetch('/api/status', { cache: 'no-store', signal: ctl.signal });
|
|
277
|
+
if (!res || !res.ok) { shouldRetry = true; }
|
|
278
|
+
else {
|
|
279
|
+
var data = await res.json().catch(function() { return null; });
|
|
280
|
+
var newDashId = (data && data.version) ? data.version.dashboardStartedAt : null;
|
|
281
|
+
if (_ccIsNewDashboardInstance(preRestartDashId, newDashId, clickTimeMs)) {
|
|
282
|
+
reloadWithCacheBust('Dashboard online — reloading...');
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// Same instance (or unparseable) — old dashboard still answering or
|
|
286
|
+
// new dashboard ID hasn't advanced past clickTime yet. Keep polling.
|
|
287
|
+
shouldRetry = true;
|
|
209
288
|
}
|
|
210
|
-
|
|
211
|
-
|
|
289
|
+
} catch { shouldRetry = true; }
|
|
290
|
+
finally { clearTimeout(pollTimer); }
|
|
291
|
+
if (shouldRetry) {
|
|
212
292
|
if (Date.now() - startedAt > DEADLINE_MS) {
|
|
213
|
-
|
|
214
|
-
|
|
293
|
+
// Last-ditch reload. refresh.js's auto-detection (4s poll on
|
|
294
|
+
// dashboardStartedAt change) will pick up the new instance once it
|
|
295
|
+
// does come up.
|
|
296
|
+
reloadWithCacheBust('Restart timed out — reloading anyway...');
|
|
215
297
|
return;
|
|
216
298
|
}
|
|
217
|
-
setTimeout(
|
|
218
|
-
}
|
|
299
|
+
setTimeout(pollStatus, INTERVAL_MS);
|
|
300
|
+
}
|
|
219
301
|
}
|
|
220
|
-
|
|
221
|
-
// the
|
|
222
|
-
//
|
|
223
|
-
setTimeout(
|
|
302
|
+
|
|
303
|
+
// Give the restart child a moment to actually kill the old dashboard before
|
|
304
|
+
// we start polling. Polling at t=0 risks confirming the dying old instance.
|
|
305
|
+
setTimeout(pollStatus, 2000);
|
|
224
306
|
}
|
|
225
307
|
|
|
226
308
|
function _ccIsReconnectableStreamError(err) {
|
package/dashboard.js
CHANGED
|
@@ -9379,6 +9379,17 @@ if (require.main === module) {
|
|
|
9379
9379
|
} catch { /* warm-up is best-effort */ }
|
|
9380
9380
|
});
|
|
9381
9381
|
|
|
9382
|
+
// Pre-warm /api/status cache (W-mpehvbed0018e27d). The CC "Restart Minions"
|
|
9383
|
+
// recovery button polls /api/status and only reloads once the new dashboard
|
|
9384
|
+
// reports a NEW dashboardStartedAt. Without warming, the first /api/status
|
|
9385
|
+
// request (from the button OR from refresh.js OR from the post-reload page
|
|
9386
|
+
// load) triggers a cold getStatus() rebuild that takes 2-5s on Windows+AV
|
|
9387
|
+
// and stalls the freshly-reloaded page in a spinner. Warming on listen
|
|
9388
|
+
// means the first request hits the cached buffer.
|
|
9389
|
+
setImmediate(() => {
|
|
9390
|
+
try { getStatusJson(); } catch { /* warm-up is best-effort */ }
|
|
9391
|
+
});
|
|
9392
|
+
|
|
9382
9393
|
// ─── Engine Watchdog ─────────────────────────────────────────────────────
|
|
9383
9394
|
// Every 30s, check if engine PID is alive. If dead but control.json says
|
|
9384
9395
|
// running, auto-restart it. Prevents silent engine death.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1993",
|
|
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"
|