@yemi33/minions 0.1.405 → 0.1.407
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 +8 -1
- package/dashboard.js +24 -16
- package/engine/ado.js +1 -0
- package/engine/github.js +1 -0
- package/engine.js +6 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.407 (2026-04-06)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- Add missing return in mutateJsonFileLocked metrics callbacks
|
|
7
|
+
- Fix SSE resource leak in handleAgentLiveStream
|
|
8
|
+
- Add projects[0] length guards in engine.js
|
|
4
9
|
|
|
5
10
|
### Fixes
|
|
11
|
+
- npm version check uses npm view instead of raw https.get
|
|
12
|
+
- re-check npm registry every 4 hours via setInterval
|
|
6
13
|
- PR delete is optimistic — row removed immediately, reverted on failure
|
|
7
14
|
- PR delete searches all project files, not just the first project
|
|
8
15
|
|
package/dashboard.js
CHANGED
|
@@ -179,18 +179,14 @@ async function checkNpmVersion() {
|
|
|
179
179
|
const now = Date.now();
|
|
180
180
|
if (_npmVersionCache && (now - _npmVersionCacheTs) < NPM_CHECK_INTERVAL) return _npmVersionCache;
|
|
181
181
|
try {
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
resp.on('data', c => body += c);
|
|
188
|
-
resp.on('end', () => { try { resolve(JSON.parse(body)); } catch { reject(new Error('bad json')); } });
|
|
182
|
+
// Use npm view — respects user's .npmrc proxy/registry config (unlike raw https.get)
|
|
183
|
+
const { execFile } = require('child_process');
|
|
184
|
+
const version = await new Promise((resolve, reject) => {
|
|
185
|
+
execFile('npm', ['view', PKG_NAME, 'version'], { timeout: 15000, windowsHide: true }, (err, stdout) => {
|
|
186
|
+
if (err) reject(err); else resolve((stdout || '').trim());
|
|
189
187
|
});
|
|
190
|
-
req.on('error', reject);
|
|
191
|
-
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
192
188
|
});
|
|
193
|
-
_npmVersionCache = { latest:
|
|
189
|
+
_npmVersionCache = { latest: version || null, checkedAt: new Date().toISOString() };
|
|
194
190
|
_npmVersionCacheTs = now;
|
|
195
191
|
} catch {
|
|
196
192
|
_npmVersionCache = _npmVersionCache || { latest: null, checkedAt: null, error: 'check failed' };
|
|
@@ -208,8 +204,9 @@ function _compareVersions(a, b) {
|
|
|
208
204
|
return 0;
|
|
209
205
|
}
|
|
210
206
|
|
|
211
|
-
// Kick off first npm check on startup
|
|
207
|
+
// Kick off first npm check on startup, then re-check every 4 hours
|
|
212
208
|
checkNpmVersion().catch(() => {});
|
|
209
|
+
setInterval(() => checkNpmVersion().catch(() => {}), NPM_CHECK_INTERVAL);
|
|
213
210
|
|
|
214
211
|
// Cache disk version + git commit (only changes on deploy/pull, not per-request)
|
|
215
212
|
let _diskVersionCache = null;
|
|
@@ -1457,6 +1454,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
1457
1454
|
const agentId = match[1];
|
|
1458
1455
|
const agentDir = path.join(MINIONS_DIR, 'agents', agentId);
|
|
1459
1456
|
const liveLogPath = path.join(agentDir, 'live-output.log');
|
|
1457
|
+
let _cleanedUp = false;
|
|
1458
|
+
|
|
1459
|
+
// Safe res.write wrapper — guards against writes after cleanup and EPIPE/ERR_STREAM_DESTROYED
|
|
1460
|
+
const safeWrite = (data) => {
|
|
1461
|
+
if (_cleanedUp) return;
|
|
1462
|
+
try { res.write(data); } catch { /* EPIPE or ERR_STREAM_DESTROYED — client gone */ }
|
|
1463
|
+
};
|
|
1460
1464
|
|
|
1461
1465
|
// Check if agent directory exists — avoid dangling watchers on nonexistent paths
|
|
1462
1466
|
if (!fs.existsSync(agentDir)) {
|
|
@@ -1478,13 +1482,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
1478
1482
|
try {
|
|
1479
1483
|
const content = fs.readFileSync(liveLogPath, 'utf8');
|
|
1480
1484
|
if (content.length > 0) {
|
|
1481
|
-
|
|
1485
|
+
safeWrite(`data: ${JSON.stringify(content)}\n\n`);
|
|
1482
1486
|
offset = Buffer.byteLength(content, 'utf8');
|
|
1483
1487
|
}
|
|
1484
1488
|
} catch { /* optional */ }
|
|
1485
1489
|
|
|
1486
1490
|
// Watch for changes using fs.watchFile (cross-platform, works on Windows)
|
|
1487
1491
|
const watcher = () => {
|
|
1492
|
+
if (_cleanedUp) return;
|
|
1488
1493
|
try {
|
|
1489
1494
|
const stat = fs.statSync(liveLogPath);
|
|
1490
1495
|
if (stat.size > offset) {
|
|
@@ -1494,29 +1499,32 @@ const server = http.createServer(async (req, res) => {
|
|
|
1494
1499
|
fs.closeSync(fd);
|
|
1495
1500
|
offset = stat.size;
|
|
1496
1501
|
const chunk = buf.toString('utf8');
|
|
1497
|
-
if (chunk)
|
|
1502
|
+
if (chunk) safeWrite(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
1498
1503
|
}
|
|
1499
1504
|
} catch { /* optional */ }
|
|
1500
1505
|
};
|
|
1501
1506
|
|
|
1502
1507
|
fs.watchFile(liveLogPath, { interval: 500 }, watcher);
|
|
1503
1508
|
|
|
1504
|
-
//
|
|
1509
|
+
// Idempotent cleanup helper to prevent handle leaks
|
|
1505
1510
|
const cleanup = () => {
|
|
1511
|
+
if (_cleanedUp) return;
|
|
1512
|
+
_cleanedUp = true;
|
|
1506
1513
|
try { clearInterval(doneCheck); } catch { /* optional */ }
|
|
1507
1514
|
try { fs.unwatchFile(liveLogPath, watcher); } catch { /* optional */ }
|
|
1508
1515
|
};
|
|
1509
1516
|
|
|
1510
1517
|
// Check if agent is still active (poll every 5s)
|
|
1511
1518
|
const doneCheck = setInterval(() => {
|
|
1519
|
+
if (_cleanedUp) return;
|
|
1512
1520
|
try {
|
|
1513
1521
|
const dispatch = getDispatchQueue();
|
|
1514
1522
|
const isActive = (dispatch.active || []).some(d => d.agent === agentId);
|
|
1515
1523
|
if (!isActive) {
|
|
1516
1524
|
watcher(); // flush final content
|
|
1517
|
-
|
|
1525
|
+
safeWrite(`event: done\ndata: complete\n\n`);
|
|
1518
1526
|
cleanup();
|
|
1519
|
-
res.end();
|
|
1527
|
+
try { res.end(); } catch { /* optional */ }
|
|
1520
1528
|
}
|
|
1521
1529
|
} catch (e) {
|
|
1522
1530
|
cleanup();
|
package/engine/ado.js
CHANGED
|
@@ -194,6 +194,7 @@ async function pollPrStatus(config) {
|
|
|
194
194
|
if (!metrics[authorId]) metrics[authorId] = {};
|
|
195
195
|
if (newReviewStatus === 'approved') metrics[authorId].prsApproved = (metrics[authorId].prsApproved || 0) + 1;
|
|
196
196
|
else metrics[authorId].prsRejected = (metrics[authorId].prsRejected || 0) + 1;
|
|
197
|
+
return metrics;
|
|
197
198
|
});
|
|
198
199
|
} catch (err) { log('warn', `Metrics update: ${err.message}`); }
|
|
199
200
|
}
|
package/engine/github.js
CHANGED
|
@@ -216,6 +216,7 @@ async function pollPrStatus(config) {
|
|
|
216
216
|
if (!metrics[authorId]) metrics[authorId] = {};
|
|
217
217
|
if (newReviewStatus === 'approved') metrics[authorId].prsApproved = (metrics[authorId].prsApproved || 0) + 1;
|
|
218
218
|
else metrics[authorId].prsRejected = (metrics[authorId].prsRejected || 0) + 1;
|
|
219
|
+
return metrics;
|
|
219
220
|
});
|
|
220
221
|
} catch (err) { log('warn', `Metrics update: ${err.message}`); }
|
|
221
222
|
}
|
package/engine.js
CHANGED
|
@@ -1827,7 +1827,8 @@ function discoverCentralWorkItems(config) {
|
|
|
1827
1827
|
const fanKey = `${key}-${agent.id}`;
|
|
1828
1828
|
if (isAlreadyDispatched(fanKey)) continue;
|
|
1829
1829
|
|
|
1830
|
-
const ap = assignedProject || projects[0];
|
|
1830
|
+
const ap = assignedProject || (projects.length > 0 ? projects[0] : null);
|
|
1831
|
+
if (!ap) { log('warn', `Fan-out: skipping ${fanKey} — no projects configured`); continue; }
|
|
1831
1832
|
const vars = {
|
|
1832
1833
|
...buildBaseVars(agent.id, config, ap),
|
|
1833
1834
|
item_id: item.id,
|
|
@@ -1897,7 +1898,8 @@ function discoverCentralWorkItems(config) {
|
|
|
1897
1898
|
|
|
1898
1899
|
const agentName = config.agents[agentId]?.name || agentId;
|
|
1899
1900
|
const agentRole = config.agents[agentId]?.role || 'Agent';
|
|
1900
|
-
const firstProject = projects[0];
|
|
1901
|
+
const firstProject = projects.length > 0 ? projects[0] : null;
|
|
1902
|
+
if (!firstProject) { log('warn', `Dispatch: skipping ${item.id} — no projects configured`); continue; }
|
|
1901
1903
|
|
|
1902
1904
|
const vars = {
|
|
1903
1905
|
...buildBaseVars(agentId, config, firstProject),
|
|
@@ -2291,7 +2293,7 @@ async function tickInner() {
|
|
|
2291
2293
|
if (pendingWithBlockedDeps.length > 0) {
|
|
2292
2294
|
// Auto-retry failed items that are blocking others (transient errors)
|
|
2293
2295
|
for (const item of items) {
|
|
2294
|
-
if (item.status !==
|
|
2296
|
+
if (item.status !== WI_STATUS.FAILED || isItemCompleted(item)) continue;
|
|
2295
2297
|
// Only retry if something depends on this item
|
|
2296
2298
|
const isBlocking = items.some(w => w.status === WI_STATUS.PENDING && (w.depends_on || []).includes(item.id));
|
|
2297
2299
|
if (!isBlocking) continue;
|
|
@@ -2333,7 +2335,7 @@ async function tickInner() {
|
|
|
2333
2335
|
const blockers = (dep.depends_on || []).filter(d => retriedIds.has(d));
|
|
2334
2336
|
if (blockers.length > 0) {
|
|
2335
2337
|
log('info', `Stall recovery: un-failing ${dep.id} (blocker ${blockers.join(',')} retried)`);
|
|
2336
|
-
dep.status =
|
|
2338
|
+
dep.status = WI_STATUS.PENDING;
|
|
2337
2339
|
dep._retryCount = 0;
|
|
2338
2340
|
delete dep.failReason;
|
|
2339
2341
|
delete dep.failedAt;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.407",
|
|
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"
|