@yemi33/minions 0.1.2077 → 0.1.2078

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.
@@ -511,8 +511,17 @@ let _lastStatusOkAt = Date.now();
511
511
  let _consecutiveStatusFails = 0;
512
512
  let _unreachableSince = 0; // 0 = currently reachable
513
513
  let _unreachableAgeTimer = null;
514
- const _UNREACHABLE_FAIL_THRESHOLD = 2;
515
- const _UNREACHABLE_AGE_MS = 12000;
514
+ // 3 + 20s (was 2 + 12s) — the prior thresholds tripped a banner on a single
515
+ // safeFetch abort (timeout 15s in state.js) because the OR'd age side was
516
+ // already satisfied. With safeFetch=15s, the age budget needs >15s to avoid
517
+ // the trip-on-first-slow-response footgun. 3 consecutive fails ≈ 12s of real
518
+ // outage at the 4s poll cadence, which still surfaces the banner promptly
519
+ // during a real dashboard crash but tolerates one slow rebuild + retry.
520
+ const _UNREACHABLE_FAIL_THRESHOLD = 3;
521
+ const _UNREACHABLE_AGE_MS = 20000;
522
+ // Once the banner is up we throttle polls (exponential, capped at 30s) to
523
+ // avoid hammering a struggling dashboard / network. Reset on recovery.
524
+ let _nextPollAllowedAt = 0;
516
525
 
517
526
  function _formatAge(ms) {
518
527
  if (ms < 1000) return 'just now';
@@ -595,10 +604,26 @@ window._resetDashboardUnreachableForTest = function() {
595
604
  _lastStatusOkAt = Date.now();
596
605
  _consecutiveStatusFails = 0;
597
606
  _unreachableSince = 0;
607
+ _nextPollAllowedAt = 0;
598
608
  if (_unreachableAgeTimer) { clearInterval(_unreachableAgeTimer); _unreachableAgeTimer = null; }
599
609
  delete window._dashboardUnreachable;
600
610
  };
601
611
 
612
+ // Visibility wake-up: Chromium throttles setInterval on hidden tabs to ~1/min,
613
+ // so _lastStatusOkAt can drift far past the age threshold while the tab is
614
+ // backgrounded. When the user refocuses, state.js fires refresh() (line 206)
615
+ // — without this reset the age side of the OR is already satisfied and a
616
+ // single transient post-wake failure (DNS not yet up after suspend, mid-
617
+ // restart server) trips the banner before any real evidence of trouble.
618
+ // Reset BEFORE state.js calls refresh() — listener order is fine because
619
+ // both fire on the same event; this listener registered first runs first.
620
+ document.addEventListener('visibilitychange', function() {
621
+ if (document.visibilityState === 'visible' && !_unreachableSince) {
622
+ _lastStatusOkAt = Date.now();
623
+ _consecutiveStatusFails = 0;
624
+ }
625
+ });
626
+
602
627
  // ── Refresh diagnostics (W-mphejzx100081972) ─────────────────────────────
603
628
  // Ring buffer capturing the last 50 /api/status poll cycles so a user
604
629
  // reporting "the dashboard didn't auto-update when X changed" can paste
@@ -649,6 +674,11 @@ document.addEventListener('visibilitychange', function() {
649
674
 
650
675
  async function refresh() {
651
676
  if (_refreshInFlight) return;
677
+ // Backoff gate — only active while the unreachable banner is up. Skips
678
+ // setInterval ticks until _nextPollAllowedAt is reached so a downed
679
+ // dashboard isn't hammered at the steady 4s cadence (which produces
680
+ // console-spam and adds load to whatever's wedged).
681
+ if (_nextPollAllowedAt && Date.now() < _nextPollAllowedAt) return;
652
682
  _refreshInFlight = true;
653
683
  const _diagOn = _isRefreshDiagOn();
654
684
  const _t0 = _diagOn ? Date.now() : 0;
@@ -730,6 +760,7 @@ async function refresh() {
730
760
  // instead of just dismissing the banner.
731
761
  _lastStatusOkAt = Date.now();
732
762
  _consecutiveStatusFails = 0;
763
+ _nextPollAllowedAt = 0;
733
764
  if (_unreachableSince) _markDashboardReachable();
734
765
  const _renderStart = _diagOn ? Date.now() : 0;
735
766
  let _diagChanges = null;
@@ -762,6 +793,14 @@ async function refresh() {
762
793
  if (_consecutiveStatusFails >= _UNREACHABLE_FAIL_THRESHOLD || ageMs > _UNREACHABLE_AGE_MS) {
763
794
  _markDashboardUnreachable(e);
764
795
  }
796
+ // Backoff: once we've tripped the banner, throttle subsequent polls
797
+ // (4s → 8s → 16s → 30s cap). Reset to 0 in the success path below so
798
+ // recovery snaps back to the steady 4s cadence on the next tick.
799
+ if (_unreachableSince) {
800
+ const failsSinceTrip = Math.max(1, _consecutiveStatusFails - _UNREACHABLE_FAIL_THRESHOLD + 1);
801
+ const backoffMs = Math.min(30000, 4000 * Math.pow(2, failsSinceTrip - 1));
802
+ _nextPollAllowedAt = Date.now() + backoffMs;
803
+ }
765
804
  }
766
805
  finally {
767
806
  _refreshInFlight = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2077",
3
+ "version": "0.1.2078",
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"