@yemi33/minions 0.1.406 → 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 +7 -1
- package/dashboard.js +22 -15
- 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,14 @@
|
|
|
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
|
|
6
12
|
- re-check npm registry every 4 hours via setInterval
|
|
7
13
|
- PR delete is optimistic — row removed immediately, reverted on failure
|
|
8
14
|
- PR delete searches all project files, not just the first project
|
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' };
|
|
@@ -1458,6 +1454,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
1458
1454
|
const agentId = match[1];
|
|
1459
1455
|
const agentDir = path.join(MINIONS_DIR, 'agents', agentId);
|
|
1460
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
|
+
};
|
|
1461
1464
|
|
|
1462
1465
|
// Check if agent directory exists — avoid dangling watchers on nonexistent paths
|
|
1463
1466
|
if (!fs.existsSync(agentDir)) {
|
|
@@ -1479,13 +1482,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
1479
1482
|
try {
|
|
1480
1483
|
const content = fs.readFileSync(liveLogPath, 'utf8');
|
|
1481
1484
|
if (content.length > 0) {
|
|
1482
|
-
|
|
1485
|
+
safeWrite(`data: ${JSON.stringify(content)}\n\n`);
|
|
1483
1486
|
offset = Buffer.byteLength(content, 'utf8');
|
|
1484
1487
|
}
|
|
1485
1488
|
} catch { /* optional */ }
|
|
1486
1489
|
|
|
1487
1490
|
// Watch for changes using fs.watchFile (cross-platform, works on Windows)
|
|
1488
1491
|
const watcher = () => {
|
|
1492
|
+
if (_cleanedUp) return;
|
|
1489
1493
|
try {
|
|
1490
1494
|
const stat = fs.statSync(liveLogPath);
|
|
1491
1495
|
if (stat.size > offset) {
|
|
@@ -1495,29 +1499,32 @@ const server = http.createServer(async (req, res) => {
|
|
|
1495
1499
|
fs.closeSync(fd);
|
|
1496
1500
|
offset = stat.size;
|
|
1497
1501
|
const chunk = buf.toString('utf8');
|
|
1498
|
-
if (chunk)
|
|
1502
|
+
if (chunk) safeWrite(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
1499
1503
|
}
|
|
1500
1504
|
} catch { /* optional */ }
|
|
1501
1505
|
};
|
|
1502
1506
|
|
|
1503
1507
|
fs.watchFile(liveLogPath, { interval: 500 }, watcher);
|
|
1504
1508
|
|
|
1505
|
-
//
|
|
1509
|
+
// Idempotent cleanup helper to prevent handle leaks
|
|
1506
1510
|
const cleanup = () => {
|
|
1511
|
+
if (_cleanedUp) return;
|
|
1512
|
+
_cleanedUp = true;
|
|
1507
1513
|
try { clearInterval(doneCheck); } catch { /* optional */ }
|
|
1508
1514
|
try { fs.unwatchFile(liveLogPath, watcher); } catch { /* optional */ }
|
|
1509
1515
|
};
|
|
1510
1516
|
|
|
1511
1517
|
// Check if agent is still active (poll every 5s)
|
|
1512
1518
|
const doneCheck = setInterval(() => {
|
|
1519
|
+
if (_cleanedUp) return;
|
|
1513
1520
|
try {
|
|
1514
1521
|
const dispatch = getDispatchQueue();
|
|
1515
1522
|
const isActive = (dispatch.active || []).some(d => d.agent === agentId);
|
|
1516
1523
|
if (!isActive) {
|
|
1517
1524
|
watcher(); // flush final content
|
|
1518
|
-
|
|
1525
|
+
safeWrite(`event: done\ndata: complete\n\n`);
|
|
1519
1526
|
cleanup();
|
|
1520
|
-
res.end();
|
|
1527
|
+
try { res.end(); } catch { /* optional */ }
|
|
1521
1528
|
}
|
|
1522
1529
|
} catch (e) {
|
|
1523
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"
|