@yemi33/minions 0.1.2016 → 0.1.2017

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.
@@ -465,28 +465,17 @@ function qaOpenRunAgent(workItemId, agentId) {
465
465
  }
466
466
 
467
467
  // ── Page-navigation hooks ──────────────────────────────────────────────────
468
- // switchPage wrapper:
469
- // - clears the runs polling interval (section 3) so the QA page stops
470
- // polling when the user navigates away
471
- // - closes any open managed-log SSE stream so a modal opened from /engine
472
- // doesn't keep streaming behind the new page
473
- // - lazily kicks off QA page loads + polling when entering /qa
474
- (function () {
475
- if (typeof switchPage !== 'function' || switchPage.__qaWrapped) return;
476
- const _origSwitchPage = switchPage;
477
- window.switchPage = function (page, pushState) {
478
- try { if (typeof closeManagedLog === 'function') closeManagedLog(); } catch {}
479
- try { _stopQaRunsPoll(); } catch {}
480
- const ret = _origSwitchPage(page, pushState);
481
- if (page === 'qa') {
482
- try { loadQaTargets(); } catch {}
483
- try { loadQaRunbooks(); } catch {}
484
- try { _startQaRunsPoll(); } catch {}
485
- }
486
- return ret;
487
- };
488
- window.switchPage.__qaWrapped = true;
489
- })();
468
+ // W-mpgb0xa7000d90d4 — the per-tab switchPage monkey-patch this file used to
469
+ // install (__qaWrapped wrapper that called closeManagedLog + _stopQaRunsPoll
470
+ // on every navigation and kicked off loadQaTargets / loadQaRunbooks /
471
+ // _startQaRunsPoll on enter) has been generalized into one canonical
472
+ // lifecycle. See `PAGE_LAZY_LOADERS` + `PAGE_LEAVE_HOOKS` in state.js:
473
+ // - QA enter hooks live in PAGE_LAZY_LOADERS.qa
474
+ // - closeManagedLog + _stopQaRunsPoll live in PAGE_LEAVE_HOOKS (fire on
475
+ // every page transition both are no-ops when nothing is active)
476
+ // The loader / stop functions themselves stay co-located with the QA UI here
477
+ // and are resolved by name through window[name] at switchPage time, so this
478
+ // file no longer needs to wrap switchPage.
490
479
 
491
480
  // Refresh log previews after every runs render — _qaFillLogPreviews is a
492
481
  // no-op for already-loaded blocks, so polling repeatedly is safe.
@@ -26,12 +26,58 @@ function _detectPageChanges(data) {
26
26
  return changes;
27
27
  }
28
28
 
29
- // Change detection — skip renders for sections that haven't changed since last refresh
29
+ // Change detection — skip renders for sections that haven't changed since last refresh.
30
+ //
31
+ // RENDER_VERSIONS is the in-process cache-bust knob (R3, W-mpgb0xgc000hf1d3).
32
+ // _changed() caches JSON.stringify(value) per key, so when the input data is
33
+ // byte-identical between ticks the render is skipped. That's the right call
34
+ // 99% of the time, but it has a sharp edge: when a renderer body itself is
35
+ // edited (hot-reloaded via dashboard-build.js or a freshly shipped bundle)
36
+ // and the input stays the same, the stale render persists forever because
37
+ // the cache key still matches. F8 / "projects chip" class of bug.
38
+ //
39
+ // Bump the matching entry below whenever a renderer's *output* may change
40
+ // for the same input. Cross-restart safety lives in the dashboardBuildId
41
+ // reload path below — RENDER_VERSIONS handles the within-process case.
42
+ const RENDER_VERSIONS = {
43
+ agents: 1,
44
+ prdProgress: 1,
45
+ prdPrs: 1,
46
+ inbox: 1,
47
+ projects: 1,
48
+ notes: 1,
49
+ prd: 1,
50
+ prs: 1,
51
+ archivedPrds: 1,
52
+ engine: 1,
53
+ version: 1,
54
+ adoThrottle: 1,
55
+ ghThrottle: 1,
56
+ dispatch: 1,
57
+ engineLog: 1,
58
+ metrics: 1,
59
+ workItems: 1,
60
+ skills: 1,
61
+ mcpServers: 1,
62
+ schedules: 1,
63
+ watches: 1,
64
+ meetings: 1,
65
+ pipelines: 1,
66
+ pinned: 1,
67
+ };
30
68
  const _sectionCache = {};
31
- function _changed(key, value) {
69
+ const _sectionCacheVersions = {};
70
+ function _changed(key, value, version) {
71
+ var v = version == null ? (RENDER_VERSIONS[key] || 0) : version;
72
+ // Drop the stale-version entry so the cache doesn't grow unbounded across bumps.
73
+ if (_sectionCacheVersions[key] !== undefined && _sectionCacheVersions[key] !== v) {
74
+ delete _sectionCache[key + ':v' + _sectionCacheVersions[key]];
75
+ }
76
+ _sectionCacheVersions[key] = v;
77
+ var cacheKey = key + ':v' + v;
32
78
  var json = JSON.stringify(value);
33
- if (_sectionCache[key] === json) return false;
34
- _sectionCache[key] = json;
79
+ if (_sectionCache[cacheKey] === json) return false;
80
+ _sectionCache[cacheKey] = json;
35
81
  return true;
36
82
  }
37
83
 
@@ -191,6 +237,14 @@ function _processStatusUpdate(data) {
191
237
  }
192
238
 
193
239
  let _knownDashboardStartId = null;
240
+ // Hard-reload trigger when the assembled dashboard HTML (and therefore any
241
+ // renderer body) has changed since the page was first loaded. Mirrors the
242
+ // _knownDashboardStartId pattern (R3, W-mpgb0xgc000hf1d3): catches restart-
243
+ // crossing renderer drift that RENDER_VERSIONS (in-process bump) can't see,
244
+ // since a server restart wipes the JS module identity entirely. data.version.
245
+ // dashboardBuildId is the md5 of the assembled HTML — bumps automatically on
246
+ // hot-reload + on cold restart with any /dashboard/** change.
247
+ let _knownDashboardBuildId = null;
194
248
  // /api/status ETag cache (W-mpehsyhv0017085a). The dashboard polls every 4 s
195
249
  // but the server-side cache only changes every 10–60 s. Sending If-None-Match
196
250
  // lets the server short-circuit ~60 %+ of polls into a 304 with no body —
@@ -232,6 +286,16 @@ async function refresh() {
232
286
  return;
233
287
  }
234
288
  if (dashId) _knownDashboardStartId = dashId;
289
+ // Auto-reload when the assembled dashboard HTML changed (renderer body or
290
+ // any other concat'd JS) — catches drift the in-process RENDER_VERSIONS
291
+ // bump can't see (R3, W-mpgb0xgc000hf1d3).
292
+ const buildId = (data.version && data.version.dashboardBuildId) || null;
293
+ if (buildId && _knownDashboardBuildId && buildId !== _knownDashboardBuildId) {
294
+ console.log('Dashboard build changed — reloading page');
295
+ location.reload();
296
+ return;
297
+ }
298
+ if (buildId) _knownDashboardBuildId = buildId;
235
299
  _processStatusUpdate(data);
236
300
  } catch(e) { console.error('refresh error', e); }
237
301
  finally { _refreshInFlight = false; }
@@ -25,11 +25,47 @@ function getPageFromUrl() {
25
25
 
26
26
  let currentPage = getPageFromUrl();
27
27
 
28
+ // W-mpgb0xa7000d90d4 (dashboard-refresh-audit.md F5) — switchPage previously
29
+ // only cleaned intervals and flipped CSS; tabs like /plans and /inbox waited
30
+ // up to ~12s for refresh.js's slow-cycle (refreshKnowledgeBase + refreshPlans
31
+ // every 3rd status tick) before showing fresh data. The QA tab worked around
32
+ // this with its own switchPage monkey-patch in qa.js (__qaWrapped) — that
33
+ // per-tab pattern is now generalized into one canonical lifecycle below.
34
+ //
35
+ // PAGE_LAZY_LOADERS: functions to invoke when ENTERING a page (resolved by
36
+ // name through window so the function body can live alongside its tab UI).
37
+ // PAGE_LEAVE_HOOKS: functions to invoke on EVERY switchPage call regardless
38
+ // of source/destination. Each leave hook must be safe to invoke when there
39
+ // is nothing to clean up (idempotent / no-op when inactive) — _stopPlanPoll,
40
+ // _stopMeetingPoll, _stopQaRunsPoll all clearInterval on a nullable handle;
41
+ // closeDetail / closeManagedLog short-circuit when no panel/stream is open.
42
+ const PAGE_LAZY_LOADERS = {
43
+ qa: ['loadQaTargets', 'loadQaRunbooks', '_startQaRunsPoll'],
44
+ plans: ['refreshPlans'],
45
+ inbox: ['refreshKnowledgeBase'],
46
+ };
47
+
48
+ const PAGE_LEAVE_HOOKS = [
49
+ '_stopPlanPoll',
50
+ '_stopMeetingPoll',
51
+ 'closeDetail',
52
+ '_stopQaRunsPoll',
53
+ 'closeManagedLog',
54
+ ];
55
+
56
+ function _invokePageHooks(names) {
57
+ if (!names || !names.length) return;
58
+ const scope = (typeof window !== 'undefined') ? window
59
+ : (typeof globalThis !== 'undefined' ? globalThis : {});
60
+ for (const name of names) {
61
+ const fn = scope[name];
62
+ if (typeof fn !== 'function') continue;
63
+ try { fn(); } catch { /* per-hook isolation — one bad hook can't break the lifecycle */ }
64
+ }
65
+ }
66
+
28
67
  function switchPage(page, pushState) {
29
- // Clean up intervals and panels from previous page
30
- try { _stopPlanPoll(); } catch {}
31
- try { _stopMeetingPoll(); } catch {}
32
- try { closeDetail(); } catch {}
68
+ _invokePageHooks(PAGE_LEAVE_HOOKS);
33
69
 
34
70
  currentPage = page;
35
71
  document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
@@ -46,6 +82,8 @@ function switchPage(page, pushState) {
46
82
  const url = page === 'home' ? '/' : '/' + page;
47
83
  history.pushState({ page }, '', url);
48
84
  }
85
+
86
+ _invokePageHooks(PAGE_LAZY_LOADERS[page]);
49
87
  }
50
88
 
51
89
  // Browser back/forward navigation
@@ -160,4 +198,4 @@ function safeFetch(url, opts) {
160
198
  return fetch(url, fetchOpts).finally(function() { clearTimeout(timer); });
161
199
  }
162
200
 
163
- window.MinionsState = { getPageFromUrl, switchPage, getPrdRequeueState, setPrdRequeueState, clearPrdRequeueState, prunePrdRequeueState, rerenderPrdFromCache, safeFetch };
201
+ window.MinionsState = { getPageFromUrl, switchPage, getPrdRequeueState, setPrdRequeueState, clearPrdRequeueState, prunePrdRequeueState, rerenderPrdFromCache, safeFetch, PAGE_LAZY_LOADERS, PAGE_LEAVE_HOOKS };
package/dashboard.js CHANGED
@@ -1152,6 +1152,10 @@ function rebuildDashboardHtml() {
1152
1152
  HTML = HTML_RAW;
1153
1153
  HTML_GZ = zlib.gzipSync(HTML);
1154
1154
  HTML_ETAG = '"' + require('crypto').createHash('md5').update(HTML).digest('hex') + '"';
1155
+ // Bust the /api/status cache so the new dashboardBuildId propagates on the
1156
+ // next poll — refresh.js compares it against its first-observed value and
1157
+ // hard-reloads on mismatch (R3, W-mpgb0xgc000hf1d3).
1158
+ try { invalidateStatusCache(); } catch { /* status cache may not be initialized yet */ }
1155
1159
  console.log(' Dashboard hot-reloaded');
1156
1160
  // Push reload to all connected browsers via status-stream (saves a connection)
1157
1161
  for (const res of _statusStreamClients) {
@@ -1684,6 +1688,12 @@ function _buildStatusSlowState() {
1684
1688
  dashboardRunning: _dashboardVersion.codeVersion,
1685
1689
  dashboardRunningCommit: _dashboardVersion.codeCommit,
1686
1690
  dashboardStartedAt: _dashboardVersion.startedAt,
1691
+ // dashboardBuildId — md5 of the assembled HTML (built JS + pages + css).
1692
+ // Refresh.js compares this against its first-observed value and hard-
1693
+ // reloads on mismatch so renderer hot-edits across restarts always land
1694
+ // on the client (R3, W-mpgb0xgc000hf1d3). Quotes stripped from the
1695
+ // weak/strong ETag form so consumers see a bare hex digest.
1696
+ dashboardBuildId: HTML_ETAG ? HTML_ETAG.replace(/^"|"$/g, '') : null,
1687
1697
  disk: diskVersion,
1688
1698
  diskCommit,
1689
1699
  engineStale,
@@ -9306,6 +9316,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
9306
9316
  engineRunningCommit: engine.codeCommit || null,
9307
9317
  dashboardRunning: _dashboardVersion.codeVersion,
9308
9318
  dashboardRunningCommit: _dashboardVersion.codeCommit,
9319
+ dashboardBuildId: HTML_ETAG ? HTML_ETAG.replace(/^"|"$/g, '') : null,
9309
9320
  latest: npm.latest,
9310
9321
  updateAvailable: !isGitRepo && !!(diskVersion && npm.latest && _compareVersions(npm.latest, diskVersion) > 0),
9311
9322
  engineStale,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2016",
3
+ "version": "0.1.2017",
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"