@yemi33/minions 0.1.225 → 0.1.227

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/CHANGELOG.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.225 (2026-04-03)
3
+ ## 0.1.227 (2026-04-03)
4
+
5
+ ### Features
6
+ - fix cooldown context accumulation bloat (#71)
7
+ - wire dead test and extract progress bar helper (#64)
4
8
 
5
9
  ### Fixes
6
10
  - silence git stderr noise during repo scan
@@ -64,6 +64,54 @@ function _collectRunArtifacts(run) {
64
64
  return { merged: merged, total: total };
65
65
  }
66
66
 
67
+ /**
68
+ * Build a segmented progress bar from pipeline stages and a run.
69
+ * @param {Array} stages - pipeline stage definitions
70
+ * @param {Object} run - the run object containing stages status map
71
+ * @param {Object} [options] - optional config: { height: '8px', detailLabel: true }
72
+ * @returns {string} HTML string for the progress bar
73
+ */
74
+ function _buildProgressBar(stages, run, options) {
75
+ var totalStages = stages.length;
76
+ var completedCount = 0;
77
+ var runningCount = 0;
78
+ var failedCount = 0;
79
+ stages.forEach(function(s) {
80
+ var st = run.stages?.[s.id]?.status;
81
+ if (st === 'completed') completedCount++;
82
+ else if (st === 'running') runningCount++;
83
+ else if (st === 'failed') failedCount++;
84
+ });
85
+ var pct = Math.round((completedCount / totalStages) * 100);
86
+
87
+ var segments = stages.map(function(s) {
88
+ var st = run.stages?.[s.id]?.status || 'pending';
89
+ var cls = st === 'completed' ? 'complete' : st === 'running' ? 'running' : st === 'failed' ? 'failed' : st === 'waiting-human' ? 'waiting' : 'pending';
90
+ return '<div class="pl-prog-seg ' + cls + '" style="width:' + (100 / totalStages) + '%" title="' + escHtml(s.id) + ': ' + st + '"></div>';
91
+ }).join('');
92
+
93
+ var barStyle = options && options.height ? ' style="height:' + options.height + '"' : '';
94
+
95
+ var label;
96
+ if (options && options.detailLabel) {
97
+ label = '<span style="font-weight:600;color:' + (pct === 100 ? 'var(--green)' : failedCount ? 'var(--red)' : 'var(--blue)') + '">' + pct + '% complete</span> <span style="color:var(--muted)">(' + completedCount + '/' + totalStages + ' stages)</span>';
98
+ } else {
99
+ var statusParts = [];
100
+ if (completedCount) statusParts.push(completedCount + ' done');
101
+ if (runningCount) statusParts.push(runningCount + ' running');
102
+ if (failedCount) statusParts.push(failedCount + ' failed');
103
+ var remaining = totalStages - completedCount - runningCount - failedCount;
104
+ if (remaining > 0) statusParts.push(remaining + ' pending');
105
+ label = '<span style="font-weight:600;color:' + (pct === 100 ? 'var(--green)' : failedCount ? 'var(--red)' : 'var(--blue)') + '">' + pct + '%</span>' +
106
+ '<span style="color:var(--muted)">' + statusParts.join(' \u00b7 ') + '</span>';
107
+ }
108
+
109
+ return '<div class="pl-progress-wrap">' +
110
+ '<div class="pl-progress-bar"' + barStyle + '>' + segments + '</div>' +
111
+ '<div class="pl-progress-label">' + label + '</div>' +
112
+ '</div>';
113
+ }
114
+
67
115
  function renderPipelines(pipelines) {
68
116
  _pipelinesData = pipelines || [];
69
117
  const el = document.getElementById('pipelines-content');
@@ -95,39 +143,7 @@ function renderPipelines(pipelines) {
95
143
  var progressHtml = '';
96
144
  var displayRun = activeRun || lastRun;
97
145
  if (displayRun && (p.stages || []).length > 0) {
98
- var totalStages = (p.stages || []).length;
99
- var completedCount = 0;
100
- var runningCount = 0;
101
- var failedCount = 0;
102
- (p.stages || []).forEach(function(s) {
103
- var st = displayRun.stages?.[s.id]?.status;
104
- if (st === 'completed') completedCount++;
105
- else if (st === 'running') runningCount++;
106
- else if (st === 'failed') failedCount++;
107
- });
108
- var pct = Math.round((completedCount / totalStages) * 100);
109
-
110
- // Segmented progress bar — one segment per stage
111
- var segments = (p.stages || []).map(function(s) {
112
- var st = displayRun.stages?.[s.id]?.status || 'pending';
113
- var cls = st === 'completed' ? 'complete' : st === 'running' ? 'running' : st === 'failed' ? 'failed' : st === 'waiting-human' ? 'waiting' : 'pending';
114
- return '<div class="pl-prog-seg ' + cls + '" style="width:' + (100 / totalStages) + '%" title="' + escHtml(s.id) + ': ' + st + '"></div>';
115
- }).join('');
116
-
117
- var statusParts = [];
118
- if (completedCount) statusParts.push(completedCount + ' done');
119
- if (runningCount) statusParts.push(runningCount + ' running');
120
- if (failedCount) statusParts.push(failedCount + ' failed');
121
- var remaining = totalStages - completedCount - runningCount - failedCount;
122
- if (remaining > 0) statusParts.push(remaining + ' pending');
123
-
124
- progressHtml = '<div class="pl-progress-wrap">' +
125
- '<div class="pl-progress-bar">' + segments + '</div>' +
126
- '<div class="pl-progress-label">' +
127
- '<span style="font-weight:600;color:' + (pct === 100 ? 'var(--green)' : failedCount ? 'var(--red)' : 'var(--blue)') + '">' + pct + '%</span>' +
128
- '<span style="color:var(--muted)">' + statusParts.join(' \u00b7 ') + '</span>' +
129
- '</div>' +
130
- '</div>';
146
+ progressHtml = _buildProgressBar(p.stages || [], displayRun);
131
147
  }
132
148
 
133
149
  return '<div style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin-bottom:8px;cursor:pointer" onclick="openPipelineDetail(\'' + escHtml(p.id) + '\')">' +
@@ -166,24 +182,7 @@ function openPipelineDetail(id) {
166
182
  // Stage detail with progress bar
167
183
  var detailRun = activeRun || (p.runs || []).slice(-1)[0];
168
184
  if (detailRun && (p.stages || []).length > 0) {
169
- var dtotal = (p.stages || []).length;
170
- var ddone = 0, drun = 0, dfail = 0;
171
- (p.stages || []).forEach(function(s) {
172
- var st = detailRun.stages?.[s.id]?.status;
173
- if (st === 'completed') ddone++;
174
- else if (st === 'running') drun++;
175
- else if (st === 'failed') dfail++;
176
- });
177
- var dpct = Math.round((ddone / dtotal) * 100);
178
- var dsegs = (p.stages || []).map(function(s) {
179
- var st = detailRun.stages?.[s.id]?.status || 'pending';
180
- var cls = st === 'completed' ? 'complete' : st === 'running' ? 'running' : st === 'failed' ? 'failed' : st === 'waiting-human' ? 'waiting' : 'pending';
181
- return '<div class="pl-prog-seg ' + cls + '" style="width:' + (100 / dtotal) + '%" title="' + escHtml(s.id) + ': ' + st + '"></div>';
182
- }).join('');
183
- html += '<div class="pl-progress-wrap">' +
184
- '<div class="pl-progress-bar" style="height:8px">' + dsegs + '</div>' +
185
- '<div class="pl-progress-label"><span style="font-weight:600;color:' + (dpct === 100 ? 'var(--green)' : dfail ? 'var(--red)' : 'var(--blue)') + '">' + dpct + '% complete</span> <span style="color:var(--muted)">(' + ddone + '/' + dtotal + ' stages)</span></div>' +
186
- '</div>';
185
+ html += _buildProgressBar(p.stages || [], detailRun, { height: '8px', detailLabel: true });
187
186
  }
188
187
  html += '<h4 style="font-size:12px;color:var(--blue);margin:0">Stages</h4>';
189
188
  (p.stages || []).forEach(function(s, i) {
@@ -7,10 +7,12 @@ const path = require('path');
7
7
  const shared = require('./shared');
8
8
  const queries = require('./queries');
9
9
 
10
+ const { createHash } = require('crypto');
10
11
  const { safeJson, safeWrite, log } = shared;
11
12
  const { ENGINE_DIR } = queries;
12
13
 
13
14
  const COOLDOWN_PATH = path.join(ENGINE_DIR, 'cooldowns.json');
15
+ const PENDING_CONTEXTS_CAP = 10;
14
16
  const dispatchCooldowns = new Map(); // key → { timestamp, failures }
15
17
 
16
18
  function loadCooldowns() {
@@ -24,6 +26,35 @@ function loadCooldowns() {
24
26
  }
25
27
  }
26
28
  log('info', `Loaded ${dispatchCooldowns.size} cooldowns from disk`);
29
+ // One-time purge of bloated pendingContexts on startup
30
+ purgeBloatedCooldowns();
31
+ }
32
+
33
+ /** Deduplicate and cap pendingContexts in all loaded cooldown entries. */
34
+ function purgeBloatedCooldowns() {
35
+ let totalRemoved = 0;
36
+ for (const [k, v] of dispatchCooldowns) {
37
+ if (!Array.isArray(v.pendingContexts) || v.pendingContexts.length <= 1) continue;
38
+ const seen = new Set();
39
+ const deduped = [];
40
+ for (const ctx of v.pendingContexts) {
41
+ const hash = _contentHash(ctx);
42
+ if (!seen.has(hash)) {
43
+ seen.add(hash);
44
+ deduped.push(ctx);
45
+ }
46
+ }
47
+ const before = v.pendingContexts.length;
48
+ // Apply FIFO cap after dedup — keep the most recent entries
49
+ v.pendingContexts = deduped.length > PENDING_CONTEXTS_CAP
50
+ ? deduped.slice(deduped.length - PENDING_CONTEXTS_CAP)
51
+ : deduped;
52
+ totalRemoved += before - v.pendingContexts.length;
53
+ }
54
+ if (totalRemoved > 0) {
55
+ log('info', `Purged ${totalRemoved} duplicate/excess pendingContexts entries from cooldowns`);
56
+ saveCooldowns();
57
+ }
27
58
  }
28
59
 
29
60
  let _cooldownWriteTimer = null;
@@ -55,10 +86,26 @@ function setCooldown(key) {
55
86
  saveCooldowns();
56
87
  }
57
88
 
89
+ function _contentHash(content) {
90
+ const str = typeof content === 'string' ? content : JSON.stringify(content);
91
+ return createHash('sha256').update(str).digest('hex');
92
+ }
93
+
58
94
  function setCooldownWithContext(key, context) {
59
95
  const existing = dispatchCooldowns.get(key);
60
96
  const pendingContexts = existing?.pendingContexts || [];
61
- if (context) pendingContexts.push(context);
97
+ if (context) {
98
+ // Dedup: only append if content differs from all existing entries
99
+ const newHash = _contentHash(context);
100
+ const isDuplicate = pendingContexts.some(c => _contentHash(c) === newHash);
101
+ if (!isDuplicate) {
102
+ pendingContexts.push(context);
103
+ // FIFO cap: drop oldest entries when exceeding cap
104
+ while (pendingContexts.length > PENDING_CONTEXTS_CAP) {
105
+ pendingContexts.shift();
106
+ }
107
+ }
108
+ }
62
109
  dispatchCooldowns.set(key, {
63
110
  timestamp: Date.now(),
64
111
  failures: existing?.failures || 0,
@@ -101,13 +148,16 @@ function isAlreadyDispatched(key) {
101
148
 
102
149
  module.exports = {
103
150
  COOLDOWN_PATH,
151
+ PENDING_CONTEXTS_CAP,
104
152
  dispatchCooldowns,
105
153
  loadCooldowns,
106
154
  saveCooldowns,
155
+ purgeBloatedCooldowns,
107
156
  isOnCooldown,
108
157
  setCooldown,
109
158
  setCooldownWithContext,
110
159
  getCoalescedContexts,
111
160
  setCooldownFailure,
112
161
  isAlreadyDispatched,
162
+ _contentHash,
113
163
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.225",
3
+ "version": "0.1.227",
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"