@yemi33/minions 0.1.763 → 0.1.765
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 +65 -0
- package/engine/queries.js +12 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.765 (2026-04-10)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- id/kill endpoint
|
|
7
|
+
|
|
8
|
+
## 0.1.764 (2026-04-10)
|
|
4
9
|
|
|
5
10
|
### Features
|
|
6
11
|
- pulsing blue dot on CC tabs with active requests
|
|
7
12
|
|
|
8
13
|
### Fixes
|
|
14
|
+
- prevent stale dispatched work items from showing agent as working indefinitely
|
|
9
15
|
- only trigger verify when all PRD items succeed, not on partial failure
|
|
10
16
|
- clean handoff between restore and original stream intervals
|
|
11
17
|
- prevent competing thinking intervals on tab switch
|
package/dashboard.js
CHANGED
|
@@ -1459,6 +1459,70 @@ const server = http.createServer(async (req, res) => {
|
|
|
1459
1459
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
1460
1460
|
}
|
|
1461
1461
|
|
|
1462
|
+
async function handleAgentKill(req, res, match) {
|
|
1463
|
+
try {
|
|
1464
|
+
const agentId = match[1].replace(/[^a-zA-Z0-9_-]/g, '');
|
|
1465
|
+
if (!agentId) return jsonReply(res, 400, { error: 'agent id required' });
|
|
1466
|
+
|
|
1467
|
+
const agentDir = path.join(MINIONS_DIR, 'agents', agentId);
|
|
1468
|
+
if (!fs.existsSync(agentDir)) return jsonReply(res, 404, { error: 'Agent not found' });
|
|
1469
|
+
|
|
1470
|
+
// 1. Kill process via pid file
|
|
1471
|
+
const pidPath = path.join(agentDir, 'pid');
|
|
1472
|
+
try {
|
|
1473
|
+
const pid = parseInt(shared.safeRead(pidPath) || '', 10);
|
|
1474
|
+
if (pid) {
|
|
1475
|
+
const safePid = shared.validatePid(pid);
|
|
1476
|
+
if (process.platform === 'win32') {
|
|
1477
|
+
require('child_process').execFileSync('taskkill', ['/PID', String(safePid), '/F', '/T'], { stdio: 'pipe', timeout: 5000, windowsHide: true });
|
|
1478
|
+
} else {
|
|
1479
|
+
process.kill(safePid, 'SIGTERM');
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
} catch { /* process already dead or no pid file */ }
|
|
1483
|
+
try { fs.unlinkSync(pidPath); } catch { /* optional */ }
|
|
1484
|
+
|
|
1485
|
+
// 2. Clear session.json so retry starts fresh
|
|
1486
|
+
try { fs.unlinkSync(path.join(agentDir, 'session.json')); } catch { /* optional */ }
|
|
1487
|
+
|
|
1488
|
+
// 3. Remove all active dispatch entries for this agent
|
|
1489
|
+
const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
|
|
1490
|
+
const removedIds = [];
|
|
1491
|
+
mutateJsonFileLocked(dispatchPath, (dp) => {
|
|
1492
|
+
const before = (dp.active || []).length;
|
|
1493
|
+
const removed = (dp.active || []).filter(d => d.agent === agentId);
|
|
1494
|
+
removed.forEach(d => removedIds.push(d.id));
|
|
1495
|
+
dp.active = (dp.active || []).filter(d => d.agent !== agentId);
|
|
1496
|
+
return dp;
|
|
1497
|
+
}, { defaultValue: { pending: [], active: [], completed: [] } });
|
|
1498
|
+
|
|
1499
|
+
// 4. Reset work items from dispatched → pending so they can be retried
|
|
1500
|
+
const allWiPaths = [];
|
|
1501
|
+
for (const proj of PROJECTS) allWiPaths.push(shared.projectWorkItemsPath(proj));
|
|
1502
|
+
let resetCount = 0;
|
|
1503
|
+
for (const wiPath of allWiPaths) {
|
|
1504
|
+
try {
|
|
1505
|
+
mutateWorkItems(wiPath, items => {
|
|
1506
|
+
for (const item of items) {
|
|
1507
|
+
if (item.dispatched_to === agentId && item.status === WI_STATUS.DISPATCHED) {
|
|
1508
|
+
item.status = WI_STATUS.PENDING;
|
|
1509
|
+
item._retryCount = (item._retryCount || 0) + 1;
|
|
1510
|
+
delete item.dispatched_at;
|
|
1511
|
+
delete item.dispatched_to;
|
|
1512
|
+
delete item._pendingReason;
|
|
1513
|
+
resetCount++;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
return items;
|
|
1517
|
+
});
|
|
1518
|
+
} catch { /* optional */ }
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
invalidateStatusCache();
|
|
1522
|
+
return jsonReply(res, 200, { ok: true, agent: agentId, dispatchCleared: removedIds.length, workItemsReset: resetCount });
|
|
1523
|
+
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1462
1526
|
async function handleAgentsCancel(req, res) {
|
|
1463
1527
|
try {
|
|
1464
1528
|
const body = await readBody(req);
|
|
@@ -4027,6 +4091,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4027
4091
|
return jsonReply(res, 200, { ok: true, message: 'Steering message sent' });
|
|
4028
4092
|
}},
|
|
4029
4093
|
{ method: 'POST', path: '/api/agents/cancel', desc: 'Cancel an active agent by ID or task substring', params: 'agent?, task?', handler: handleAgentsCancel },
|
|
4094
|
+
{ method: 'POST', path: /^\/api\/agent\/([\w-]+)\/kill$/, desc: 'Kill a running agent: stop process, clear dispatch, reset work items to pending', handler: handleAgentKill },
|
|
4030
4095
|
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live-stream(?:\?.*)?$/, desc: 'SSE real-time live output streaming', handler: handleAgentLiveStream },
|
|
4031
4096
|
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)\/live(?:\?.*)?$/, desc: 'Tail live output for a working agent', params: 'tail? (bytes, default 8192)', handler: handleAgentLive },
|
|
4032
4097
|
{ method: 'GET', path: /^\/api\/agent\/([\w-]+)\/output(?:\?.*)?$/, desc: 'Fetch final output.log for an agent', handler: handleAgentOutput },
|
package/engine/queries.js
CHANGED
|
@@ -11,7 +11,7 @@ const shared = require('./shared');
|
|
|
11
11
|
|
|
12
12
|
const { safeRead, safeReadDir, safeJson, safeWrite, getProjects,
|
|
13
13
|
projectWorkItemsPath, projectPrPath, parseSkillFrontmatter, KB_CATEGORIES,
|
|
14
|
-
WI_STATUS } = shared;
|
|
14
|
+
WI_STATUS, ENGINE_DEFAULTS } = shared;
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Read the first `bytes` and last `bytes` of a file efficiently using byte offsets.
|
|
@@ -265,14 +265,21 @@ function getAgentStatus(agentId) {
|
|
|
265
265
|
|
|
266
266
|
// Fallback: derive active state from work-item markers.
|
|
267
267
|
// This protects UI status when dispatch.json briefly desyncs from work-item files.
|
|
268
|
+
// Guard: only trust dispatched state within 2x heartbeatTimeout to prevent stale
|
|
269
|
+
// dispatched items from permanently showing an agent as working after a dead process.
|
|
268
270
|
try {
|
|
269
271
|
const config = getConfig();
|
|
272
|
+
const heartbeatTimeout = config.engine?.heartbeatTimeout || ENGINE_DEFAULTS.heartbeatTimeout;
|
|
273
|
+
const staleThresholdMs = heartbeatTimeout * 2;
|
|
274
|
+
const now = Date.now();
|
|
270
275
|
const allItems = getWorkItems(config);
|
|
271
276
|
const latestInFlight = allItems
|
|
272
|
-
.filter(w =>
|
|
273
|
-
(w.dispatched_to || '').toLowerCase()
|
|
274
|
-
w.status
|
|
275
|
-
|
|
277
|
+
.filter(w => {
|
|
278
|
+
if ((w.dispatched_to || '').toLowerCase() !== String(agentId).toLowerCase()) return false;
|
|
279
|
+
if (w.status !== WI_STATUS.DISPATCHED) return false;
|
|
280
|
+
const ageMs = w.dispatched_at ? now - new Date(w.dispatched_at).getTime() : Infinity;
|
|
281
|
+
return ageMs < staleThresholdMs;
|
|
282
|
+
})
|
|
276
283
|
.sort((a, b) => (b.dispatched_at || '').localeCompare(a.dispatched_at || ''))[0];
|
|
277
284
|
if (latestInFlight) {
|
|
278
285
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.765",
|
|
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"
|