@yemi33/minions 0.1.2030 → 0.1.2032

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.
@@ -36,13 +36,21 @@ function _detectPageChanges(data) {
36
36
 
37
37
  // Change detection — skip renders for sections that haven't changed since last refresh.
38
38
  //
39
- // RENDER_VERSIONS is the in-process cache-bust knob (R3, W-mpgb0xgc000hf1d3).
40
- // _changed() caches JSON.stringify(value) per key, so when the input data is
41
- // byte-identical between ticks the render is skipped. That's the right call
42
- // 99% of the time, but it has a sharp edge: when a renderer body itself is
43
- // edited (hot-reloaded via dashboard-build.js or a freshly shipped bundle)
44
- // and the input stays the same, the stale render persists forever because
45
- // the cache key still matches. F8 / "projects chip" class of bug.
39
+ // Two-layer cache:
40
+ // 1. `_lastValueByKey` is an O(1) reference-equality precheck: when the server
41
+ // returns 304 Not Modified (refresh.js ~173) we reuse the same `_lastStatusData`
42
+ // reference tick after tick, so every `_changed(key, data.<slice>)` call below
43
+ // would otherwise re-stringify multi-MB slices for nothing. The JSON.stringify
44
+ // path stays as the fallback for the "fresh object, equal contents" case
45
+ // (S3, W-mpgcikfg000g8dc7).
46
+ // 2. RENDER_VERSIONS is the in-process cache-bust knob (R3, W-mpgb0xgc000hf1d3).
47
+ // _changed() caches JSON.stringify(value) per (key, version), so when the
48
+ // input data is byte-identical between ticks the render is skipped. That's
49
+ // the right call 99% of the time, but it has a sharp edge: when a renderer
50
+ // body itself is edited (hot-reloaded via dashboard-build.js or a freshly
51
+ // shipped bundle) and the input stays the same, the stale render persists
52
+ // forever because the cache key still matches. F8 / "projects chip" class
53
+ // of bug.
46
54
  //
47
55
  // Bump the matching entry below whenever a renderer's *output* may change
48
56
  // for the same input. Cross-restart safety lives in the dashboardBuildId
@@ -74,15 +82,24 @@ const RENDER_VERSIONS = {
74
82
  pinned: 1,
75
83
  };
76
84
  const _sectionCache = {};
85
+ const _lastValueByKey = {};
77
86
  const _sectionCacheVersions = {};
78
87
  function _changed(key, value, version) {
79
88
  var v = version == null ? (RENDER_VERSIONS[key] || 0) : version;
80
89
  // Drop the stale-version entry so the cache doesn't grow unbounded across bumps.
90
+ // A version bump must also reset the ref-eq sentinel — otherwise the S3
91
+ // short-circuit below would defeat the R3 cache-bust contract when the
92
+ // caller passes the SAME object reference across a version bump.
81
93
  if (_sectionCacheVersions[key] !== undefined && _sectionCacheVersions[key] !== v) {
82
94
  delete _sectionCache[key + ':v' + _sectionCacheVersions[key]];
95
+ _lastValueByKey[key] = undefined;
83
96
  }
84
97
  _sectionCacheVersions[key] = v;
85
98
  var cacheKey = key + ':v' + v;
99
+ // Reference-equality short-circuit: skip the stringify entirely when the
100
+ // server returned 304 and the same object reference is being re-checked.
101
+ if (_lastValueByKey[key] === value) return false;
102
+ _lastValueByKey[key] = value;
86
103
  var json = JSON.stringify(value);
87
104
  if (_sectionCache[cacheKey] === json) return false;
88
105
  _sectionCache[cacheKey] = json;
@@ -128,17 +145,35 @@ function _processStatusUpdate(data) {
128
145
  const threshEl = document.getElementById('inbox-threshold');
129
146
  if (threshEl && data.autoMode?.inboxThreshold) threshEl.textContent = data.autoMode.inboxThreshold;
130
147
 
131
- // Publish window._last* snapshots BEFORE any renderer runs. Several renderers
132
- // (renderProjects, renderPrd, renderPrs, refreshPlans, and others) read
133
- // window._lastStatus / window._lastWorkItems / window._lastDispatch during
134
- // their render path. If we assign these after the renderer calls, every tick
135
- // consumes the PREVIOUS tick's globals a one-tick (~4 s) staleness lag
136
- // baked into the polling loop (W-mpgb0x81000cf8df, audit F4). No renderer in
137
- // this function mutates `data`, so hoisting is safe.
148
+ // Publish window._last* snapshots BEFORE any renderer runs. Several
149
+ // renderers (renderPrd, renderPrs, derivePlanStatus inside refreshPlans,
150
+ // and the projectChipRemove/optimisticallyAddProject paths off
151
+ // renderProjects) read window._lastStatus / window._lastWorkItems /
152
+ // window._lastDispatch synchronously during their render path. When the
153
+ // assignments lived AFTER the renderer calls (the pre-fix layout in
154
+ // refresh.js:106-108), every tick consumed the PREVIOUS tick's globals —
155
+ // a one-tick (~4 s) staleness lag baked into the polling loop on top of
156
+ // the natural poll interval.
157
+ //
158
+ // See dashboard-refresh-audit.md finding F4 (W-mpgb0x81000cf8df / PR #2753
159
+ // landed the initial hoist; W-mpgbzpn9000390ae extends it with the
160
+ // render-agents / render-work-items invariants below). No renderer below
161
+ // mutates `data`, so hoisting the publish is safe; the renderers
162
+ // themselves still take their slice as a direct argument.
163
+ //
164
+ // Locking-in note (W-mpgbzpn9000390ae): renderAgents
165
+ // (render-agents.js:38) and renderWorkItems (render-work-items.js:98) —
166
+ // the two surfaces the user explicitly called out on /home and /work —
167
+ // take their data slice via argument and DO NOT read window._last*.
168
+ // Hoisting the publish here additionally guarantees that any future
169
+ // refactor that adds a window._last* read to those renderers will see
170
+ // fresh data on the same tick, not the previous one. Covered by
171
+ // dashboard-resilience.test.js source-inspection assertions.
138
172
  window._lastDispatch = data.dispatch;
139
173
  window._lastWorkItems = data.workItems || [];
140
174
  window._lastStatus = data;
141
175
 
176
+
142
177
  // Render only changed sections
143
178
  if (_changed('agents', data.agents)) { renderAgents(data.agents); cmdUpdateAgentList(data.agents); }
144
179
  if (_changed('prdProgress', data.prdProgress) || _changed('prdPrs', data.pullRequests?.length)) { renderPrdProgress(data.prdProgress); _cachePrdItems(data.prdProgress); }
@@ -491,7 +491,7 @@ function openPipelineDetail(id) {
491
491
  var fresh = list.find(function(x) { return x.id === id; });
492
492
  if (fresh) {
493
493
  // Only re-render if data changed
494
- var newHash = JSON.stringify({ runs: fresh.runs || [], enabled: fresh.enabled, _stoppedBy: fresh._stoppedBy, _stopReason: fresh._stopReason });
494
+ var newHash = _computePipelineDetailHash(fresh);
495
495
  if (newHash !== _pipelinePollHash) {
496
496
  _pipelinePollHash = newHash;
497
497
  _pipelinesData = _pipelinesData.map(function(x) { return x.id === id ? fresh : x; });
@@ -504,6 +504,24 @@ function openPipelineDetail(id) {
504
504
  }
505
505
  var _pipelinePollHash = '';
506
506
 
507
+ // F10: hash all pipeline fields the detail modal renders so stages/cron/monitoredResources/stopWhen
508
+ // edits in another tab trigger re-render. Previously only {runs, enabled, _stoppedBy, _stopReason}.
509
+ function _computePipelineDetailHash(p) {
510
+ if (!p) return '';
511
+ return JSON.stringify({
512
+ runs: p.runs || [],
513
+ enabled: p.enabled,
514
+ _stoppedBy: p._stoppedBy,
515
+ _stopReason: p._stopReason,
516
+ stages: p.stages,
517
+ monitoredResources: p.monitoredResources,
518
+ stopWhen: p.stopWhen,
519
+ trigger: p.trigger,
520
+ name: p.name,
521
+ description: p.description
522
+ });
523
+ }
524
+
507
525
  /**
508
526
  * Fetch fresh pipeline data and re-render the detail modal immediately.
509
527
  * Used after actions (continue, trigger, abort) to avoid waiting for the 4s poll.
@@ -516,7 +534,7 @@ async function _refreshPipelineDetail(id) {
516
534
  var fresh = list.find(function(x) { return x.id === id; });
517
535
  if (fresh) {
518
536
  _pipelinesData = _pipelinesData.map(function(x) { return x.id === id ? fresh : x; });
519
- _pipelinePollHash = JSON.stringify({ runs: fresh.runs || [], enabled: fresh.enabled, _stoppedBy: fresh._stoppedBy, _stopReason: fresh._stopReason });
537
+ _pipelinePollHash = _computePipelineDetailHash(fresh);
520
538
  renderPipelines(_pipelinesData);
521
539
  openPipelineDetail(id);
522
540
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2030",
3
+ "version": "0.1.2032",
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"