@yemi33/minions 0.1.1768 → 0.1.1770
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 +13 -6
- package/bin/minions.js +24 -7
- package/dashboard/js/state.js +49 -0
- package/dashboard.js +105 -35
- package/engine/copilot-models.json +1 -1
- package/engine/shared.js +41 -6
- package/minions.js +2 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.1770 (2026-05-07)
|
|
4
4
|
|
|
5
5
|
### Fixes
|
|
6
|
-
-
|
|
7
|
-
-
|
|
6
|
+
- open dashboard on update cold start
|
|
7
|
+
- silence generated Claude defaults warning
|
|
8
|
+
- drop TTL and turn-count cap so users can resume any time
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
## 0.1.1769 (2026-05-07)
|
|
11
|
+
|
|
12
|
+
### Fixes
|
|
13
|
+
- drop /T from engine taskkill so agents survive restart
|
|
14
|
+
|
|
15
|
+
## 0.1.1767 (2026-05-07)
|
|
16
|
+
|
|
17
|
+
### Fixes
|
|
18
|
+
- stop test fixtures leaking into live engine/log.json (#2154)
|
|
12
19
|
|
|
13
20
|
## 0.1.1765 (2026-05-07)
|
|
14
21
|
|
package/bin/minions.js
CHANGED
|
@@ -29,6 +29,7 @@ const { spawn, spawnSync, execSync } = require('child_process');
|
|
|
29
29
|
|
|
30
30
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
31
31
|
const DASH_PORT = 7331;
|
|
32
|
+
const DASHBOARD_BROWSER_PRESENCE_MAX_AGE_MS = 45000;
|
|
32
33
|
|
|
33
34
|
/** Returns PIDs (as strings) of processes LISTENING on `port`. Empty on no match
|
|
34
35
|
* or when the platform tool (netstat/findstr/lsof) is unavailable. */
|
|
@@ -60,6 +61,18 @@ function killByPort(port) {
|
|
|
60
61
|
|
|
61
62
|
const isPortListening = (port) => getListeningPids(port).length > 0;
|
|
62
63
|
|
|
64
|
+
function hasRecentDashboardBrowserTab(minionsHome, now = Date.now()) {
|
|
65
|
+
try {
|
|
66
|
+
const fp = path.join(minionsHome, 'engine', 'dashboard-browser.json');
|
|
67
|
+
const state = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
68
|
+
const tabs = state && state.tabs && typeof state.tabs === 'object' ? state.tabs : {};
|
|
69
|
+
return Object.values(tabs).some(tab => {
|
|
70
|
+
const lastSeen = Number(tab && tab.lastSeen);
|
|
71
|
+
return Number.isFinite(lastSeen) && now - lastSeen <= DASHBOARD_BROWSER_PRESENCE_MAX_AGE_MS;
|
|
72
|
+
});
|
|
73
|
+
} catch { return false; }
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
/**
|
|
64
77
|
* Read the engine's recorded PID from engine/control.json. Returns null if
|
|
65
78
|
* the file is missing/corrupt or the PID isn't a positive integer.
|
|
@@ -118,7 +131,7 @@ function killMinionsProcesses(patterns) {
|
|
|
118
131
|
}
|
|
119
132
|
|
|
120
133
|
/** Spawn a detached dashboard. When `suppressOpen` is true, the new dashboard
|
|
121
|
-
* skips its auto-open of the browser — the existing tab will
|
|
134
|
+
* skips its auto-open of the browser — the existing tab will reconnect. */
|
|
122
135
|
function spawnDashboard(suppressOpen) {
|
|
123
136
|
const env = suppressOpen ? { ...process.env, MINIONS_NO_AUTO_OPEN: '1' } : process.env;
|
|
124
137
|
const proc = spawn(process.execPath, [path.join(MINIONS_HOME, 'dashboard.js')], {
|
|
@@ -397,9 +410,11 @@ function init() {
|
|
|
397
410
|
if (isUpgrade && skipStart) return;
|
|
398
411
|
|
|
399
412
|
// Auto-start on fresh install; direct force-upgrade restarts automatically.
|
|
400
|
-
// Probe before kill so we
|
|
401
|
-
//
|
|
413
|
+
// Probe before kill so we suppress browser auto-open only when a browser tab
|
|
414
|
+
// was recently polling the dashboard. A bare/orphan dashboard process on the
|
|
415
|
+
// port is not enough; cold starts should still open the UI.
|
|
402
416
|
const dashWasUp = isPortListening(DASH_PORT);
|
|
417
|
+
const suppressDashboardOpen = dashWasUp && hasRecentDashboardBrowserTab(MINIONS_HOME);
|
|
403
418
|
if (isUpgrade) {
|
|
404
419
|
try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME, timeout: 10000, windowsHide: true }); } catch {}
|
|
405
420
|
// Free the dashboard port too — without this the new dashboard EADDRINUSE-dies
|
|
@@ -415,7 +430,7 @@ function init() {
|
|
|
415
430
|
engineProc.unref();
|
|
416
431
|
console.log(` Engine started (PID: ${engineProc.pid})`);
|
|
417
432
|
|
|
418
|
-
const dashProc = spawnDashboard(
|
|
433
|
+
const dashProc = spawnDashboard(suppressDashboardOpen);
|
|
419
434
|
console.log(` Dashboard started (PID: ${dashProc.pid})`);
|
|
420
435
|
console.log(` Dashboard: http://localhost:${DASH_PORT}`);
|
|
421
436
|
|
|
@@ -670,9 +685,11 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
670
685
|
// `--cli` / `--model` flags forward to `engine.js start` so the runtime
|
|
671
686
|
// fleet flips before the daemon spawns (P-6b3f9c2e AC: works on restart).
|
|
672
687
|
ensureInstalled();
|
|
673
|
-
// Probe before kill so we
|
|
674
|
-
//
|
|
688
|
+
// Probe before kill so we suppress browser auto-open only when a browser tab
|
|
689
|
+
// was recently polling the dashboard. A bare/orphan dashboard process on the
|
|
690
|
+
// port is not enough; cold starts should still open the UI.
|
|
675
691
|
const dashWasUp = isPortListening(DASH_PORT);
|
|
692
|
+
const suppressDashboardOpen = dashWasUp && hasRecentDashboardBrowserTab(MINIONS_HOME);
|
|
676
693
|
// Layered kill — each step is best-effort, layered so the next still runs if
|
|
677
694
|
// one fails. Goal: the old engine is gone before we spawn a new one, even if
|
|
678
695
|
// PowerShell is unavailable, the engine is hung, or its cmdline doesn't match.
|
|
@@ -691,7 +708,7 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
691
708
|
});
|
|
692
709
|
engineProc.unref();
|
|
693
710
|
console.log(`\n Engine started (PID: ${engineProc.pid})`);
|
|
694
|
-
const dashProc = spawnDashboard(
|
|
711
|
+
const dashProc = spawnDashboard(suppressDashboardOpen);
|
|
695
712
|
console.log(` Dashboard started (PID: ${dashProc.pid})`);
|
|
696
713
|
console.log(` Dashboard: http://localhost:${DASH_PORT}\n`);
|
|
697
714
|
} else if (cmd === 'nuke') {
|
package/dashboard/js/state.js
CHANGED
|
@@ -4,6 +4,17 @@ let inboxData = [];
|
|
|
4
4
|
let agentData = [];
|
|
5
5
|
let currentAgentId = null;
|
|
6
6
|
let currentTab = 'thought-process';
|
|
7
|
+
const DASHBOARD_TAB_ID = (function() {
|
|
8
|
+
try {
|
|
9
|
+
var existing = sessionStorage.getItem('minions-dashboard-tab-id');
|
|
10
|
+
if (existing) return existing;
|
|
11
|
+
var id = 'tab-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
|
12
|
+
sessionStorage.setItem('minions-dashboard-tab-id', id);
|
|
13
|
+
return id;
|
|
14
|
+
} catch {
|
|
15
|
+
return 'tab-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
|
16
|
+
}
|
|
17
|
+
})();
|
|
7
18
|
|
|
8
19
|
// Sidebar page navigation — URL-routed
|
|
9
20
|
function getPageFromUrl() {
|
|
@@ -87,6 +98,37 @@ function prunePrdRequeueState(workItems) {
|
|
|
87
98
|
}
|
|
88
99
|
}
|
|
89
100
|
|
|
101
|
+
function _dashboardPresencePayload(closed) {
|
|
102
|
+
return JSON.stringify({
|
|
103
|
+
tabId: DASHBOARD_TAB_ID,
|
|
104
|
+
closed: !!closed,
|
|
105
|
+
url: location.pathname + location.search + location.hash,
|
|
106
|
+
visibility: document.visibilityState || '',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function _sendDashboardPresence(closed) {
|
|
111
|
+
var payload = _dashboardPresencePayload(closed);
|
|
112
|
+
try {
|
|
113
|
+
if (navigator.sendBeacon) {
|
|
114
|
+
var blob = new Blob([payload], { type: 'application/json' });
|
|
115
|
+
if (navigator.sendBeacon('/api/browser-presence', blob)) return;
|
|
116
|
+
}
|
|
117
|
+
} catch {}
|
|
118
|
+
try {
|
|
119
|
+
fetch('/api/browser-presence', {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: { 'Content-Type': 'application/json' },
|
|
122
|
+
body: payload,
|
|
123
|
+
keepalive: true,
|
|
124
|
+
}).catch(function() {});
|
|
125
|
+
} catch {}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
window.addEventListener('pagehide', function() { _sendDashboardPresence(true); });
|
|
129
|
+
window.addEventListener('beforeunload', function() { _sendDashboardPresence(true); });
|
|
130
|
+
document.addEventListener('visibilitychange', function() { _sendDashboardPresence(false); });
|
|
131
|
+
|
|
90
132
|
function rerenderPrdFromCache() {
|
|
91
133
|
if (!window._lastStatus || !window._lastStatus.prdProgress) return;
|
|
92
134
|
renderPrdProgress(window._lastStatus.prdProgress);
|
|
@@ -100,6 +142,13 @@ function safeFetch(url, opts) {
|
|
|
100
142
|
var controller = new AbortController();
|
|
101
143
|
var timer = setTimeout(function() { controller.abort(); }, timeout);
|
|
102
144
|
var fetchOpts = Object.assign({}, opts, { signal: controller.signal });
|
|
145
|
+
if (typeof url === 'string' && url.indexOf('/api/') === 0) {
|
|
146
|
+
var headers = Object.assign({}, fetchOpts.headers || {});
|
|
147
|
+
headers['X-Minions-Dashboard-Tab'] = DASHBOARD_TAB_ID;
|
|
148
|
+
headers['X-Minions-Dashboard-Url'] = location.pathname + location.search + location.hash;
|
|
149
|
+
headers['X-Minions-Dashboard-Visibility'] = document.visibilityState || '';
|
|
150
|
+
fetchOpts.headers = headers;
|
|
151
|
+
}
|
|
103
152
|
delete fetchOpts.timeout;
|
|
104
153
|
return fetch(url, fetchOpts).finally(function() { clearTimeout(timer); });
|
|
105
154
|
}
|
package/dashboard.js
CHANGED
|
@@ -60,6 +60,8 @@ const { getAgents, getAgentDetail, getPrdInfo, getWorkItems, getDispatchQueue,
|
|
|
60
60
|
const PORT = parseInt(process.env.PORT || process.argv[2]) || 7331;
|
|
61
61
|
let CONFIG = queries.getConfig();
|
|
62
62
|
let PROJECTS = _getProjects(CONFIG);
|
|
63
|
+
const DASHBOARD_BROWSER_PRESENCE_PATH = path.join(ENGINE_DIR, 'dashboard-browser.json');
|
|
64
|
+
const DASHBOARD_BROWSER_PRESENCE_MAX_AGE_MS = 45000;
|
|
63
65
|
|
|
64
66
|
function ensureConfiguredProjectStateFiles() {
|
|
65
67
|
for (const p of PROJECTS) {
|
|
@@ -571,6 +573,66 @@ function _trackSseClient(clientSet, req, res, { heartbeatMs = SSE_CLIENT_HEARTBE
|
|
|
571
573
|
return cleanup;
|
|
572
574
|
}
|
|
573
575
|
|
|
576
|
+
function _normalizeDashboardTabId(value) {
|
|
577
|
+
const id = typeof value === 'string' ? value.trim() : '';
|
|
578
|
+
return /^[A-Za-z0-9._:-]{1,96}$/.test(id) ? id : null;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function _pruneDashboardBrowserTabs(tabs, now = Date.now()) {
|
|
582
|
+
for (const [id, tab] of Object.entries(tabs || {})) {
|
|
583
|
+
const lastSeen = Number(tab && tab.lastSeen);
|
|
584
|
+
if (!Number.isFinite(lastSeen) || now - lastSeen > DASHBOARD_BROWSER_PRESENCE_MAX_AGE_MS) delete tabs[id];
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function _recordDashboardBrowserPresence(tabId, { closed = false, url = '', visibility = '' } = {}) {
|
|
589
|
+
const id = _normalizeDashboardTabId(tabId);
|
|
590
|
+
if (!id) return false;
|
|
591
|
+
const now = Date.now();
|
|
592
|
+
mutateJsonFileLocked(DASHBOARD_BROWSER_PRESENCE_PATH, (state) => {
|
|
593
|
+
if (!state || typeof state !== 'object' || Array.isArray(state)) state = {};
|
|
594
|
+
if (!state.tabs || typeof state.tabs !== 'object' || Array.isArray(state.tabs)) state.tabs = {};
|
|
595
|
+
_pruneDashboardBrowserTabs(state.tabs, now);
|
|
596
|
+
if (closed) {
|
|
597
|
+
delete state.tabs[id];
|
|
598
|
+
} else {
|
|
599
|
+
state.tabs[id] = {
|
|
600
|
+
id,
|
|
601
|
+
lastSeen: now,
|
|
602
|
+
lastSeenAt: new Date(now).toISOString(),
|
|
603
|
+
url: typeof url === 'string' ? url.slice(0, 512) : '',
|
|
604
|
+
visibility: typeof visibility === 'string' ? visibility.slice(0, 32) : '',
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
state.updatedAt = new Date(now).toISOString();
|
|
608
|
+
return state;
|
|
609
|
+
}, { defaultValue: { tabs: {} } });
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function _recordDashboardBrowserPresenceFromRequest(req) {
|
|
614
|
+
const tabId = req && req.headers && req.headers['x-minions-dashboard-tab'];
|
|
615
|
+
if (!tabId) return;
|
|
616
|
+
try {
|
|
617
|
+
_recordDashboardBrowserPresence(tabId, {
|
|
618
|
+
url: req.headers['x-minions-dashboard-url'] || '',
|
|
619
|
+
visibility: req.headers['x-minions-dashboard-visibility'] || '',
|
|
620
|
+
});
|
|
621
|
+
} catch (e) {
|
|
622
|
+
shared.log('warn', `Dashboard browser presence update failed: ${e.message}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function _getDashboardBrowserPresence(now = Date.now()) {
|
|
627
|
+
const state = safeJsonObj(DASHBOARD_BROWSER_PRESENCE_PATH);
|
|
628
|
+
const tabs = state.tabs && typeof state.tabs === 'object' && !Array.isArray(state.tabs) ? state.tabs : {};
|
|
629
|
+
const activeTabs = Object.values(tabs).filter(tab => {
|
|
630
|
+
const lastSeen = Number(tab && tab.lastSeen);
|
|
631
|
+
return Number.isFinite(lastSeen) && now - lastSeen <= DASHBOARD_BROWSER_PRESENCE_MAX_AGE_MS;
|
|
632
|
+
});
|
|
633
|
+
return { active: activeTabs.length > 0, activeTabs: activeTabs.length, maxAgeMs: DASHBOARD_BROWSER_PRESENCE_MAX_AGE_MS };
|
|
634
|
+
}
|
|
635
|
+
|
|
574
636
|
// Hot-reload: watch dashboard/ directory for changes, rebuild, and push reload to browsers
|
|
575
637
|
const _hotReloadClients = new Set();
|
|
576
638
|
|
|
@@ -989,12 +1051,7 @@ setInterval(() => {
|
|
|
989
1051
|
|
|
990
1052
|
// CC chat sessions do NOT auto-expire. A tab is removed only via explicit user
|
|
991
1053
|
// deletion (DELETE /api/cc-sessions/:id, wired from ccCloseTab). Doc-chat
|
|
992
|
-
// sessions
|
|
993
|
-
//
|
|
994
|
-
// CC_SESSION_MAX_TURNS is reused by the doc-chat session pruner to cap
|
|
995
|
-
// per-session turn growth there; CC chat sessions are not capped because
|
|
996
|
-
// users are expected to keep long-running tabs alive indefinitely.
|
|
997
|
-
const CC_SESSION_MAX_TURNS = shared.ENGINE_DEFAULTS.ccMaxTurns;
|
|
1054
|
+
// sessions follow the same policy — see DOC_SESSIONS section below.
|
|
998
1055
|
let ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
|
|
999
1056
|
const ccInFlightTabs = new Map(); // tabId → timestamp — per-tab in-flight tracking for parallel CC requests
|
|
1000
1057
|
const ccInFlightAborts = new Map(); // tabId → abortFn — lets a new request kill the stale LLM
|
|
@@ -2177,11 +2234,14 @@ async function executeDocChatActions(actions) {
|
|
|
2177
2234
|
|
|
2178
2235
|
// ── Shared LLM call core — used by CC panel and doc modals ──────────────────
|
|
2179
2236
|
|
|
2180
|
-
// Session store for doc modals — keyed by filePath or title, persisted to disk
|
|
2237
|
+
// Session store for doc modals — keyed by filePath or title, persisted to disk.
|
|
2238
|
+
// Doc-chat sessions do NOT auto-expire (parity with CC tabs). The only
|
|
2239
|
+
// invalidation paths are prompt-hash mismatch (correctness — a stale system
|
|
2240
|
+
// prompt would carry the old persona/rules into resume turns) and the
|
|
2241
|
+
// LRU cap below (storage hygiene). Users can resume a doc-chat any time.
|
|
2181
2242
|
const CC_SESSIONS_PATH = path.join(ENGINE_DIR, 'cc-sessions.json');
|
|
2182
2243
|
const DOC_SESSIONS_PATH = path.join(ENGINE_DIR, 'doc-sessions.json');
|
|
2183
2244
|
const CC_SESSION_PATH = path.join(ENGINE_DIR, 'cc-session.json');
|
|
2184
|
-
const DOC_SESSION_TTL_MS = shared.ENGINE_DEFAULTS.docSessionTtlMs;
|
|
2185
2245
|
const DOC_SESSION_MAX_ENTRIES = shared.ENGINE_DEFAULTS.docSessionMaxEntries;
|
|
2186
2246
|
const docSessions = new Map(); // key → { sessionId, lastActiveAt, turnCount }
|
|
2187
2247
|
|
|
@@ -2193,9 +2253,7 @@ function _docSessionLastActiveMs(session) {
|
|
|
2193
2253
|
function pruneDocSessions() {
|
|
2194
2254
|
let changed = false;
|
|
2195
2255
|
for (const [key, s] of docSessions.entries()) {
|
|
2196
|
-
if (!s ||
|
|
2197
|
-
_sessionExpired(s.lastActiveAt || s.createdAt, DOC_SESSION_TTL_MS) ||
|
|
2198
|
-
s._promptHash !== _docChatPromptHash) {
|
|
2256
|
+
if (!s || s._promptHash !== _docChatPromptHash) {
|
|
2199
2257
|
docSessions.delete(key);
|
|
2200
2258
|
changed = true;
|
|
2201
2259
|
}
|
|
@@ -2212,13 +2270,12 @@ function pruneDocSessions() {
|
|
|
2212
2270
|
return changed;
|
|
2213
2271
|
}
|
|
2214
2272
|
|
|
2215
|
-
// Load persisted doc sessions on startup
|
|
2273
|
+
// Load persisted doc sessions on startup — no TTL/turn filtering, sessions
|
|
2274
|
+
// are non-expiring. Stale prompt-hash entries are dropped by pruneDocSessions.
|
|
2216
2275
|
try {
|
|
2217
2276
|
const saved = safeJson(DOC_SESSIONS_PATH);
|
|
2218
2277
|
if (saved && typeof saved === 'object') {
|
|
2219
2278
|
for (const [key, s] of Object.entries(saved)) {
|
|
2220
|
-
if (s.turnCount >= CC_SESSION_MAX_TURNS) continue;
|
|
2221
|
-
if (_sessionExpired(s.lastActiveAt || s.createdAt, DOC_SESSION_TTL_MS)) continue;
|
|
2222
2279
|
docSessions.set(key, s);
|
|
2223
2280
|
}
|
|
2224
2281
|
pruneDocSessions();
|
|
@@ -2233,9 +2290,11 @@ function persistDocSessions() {
|
|
|
2233
2290
|
mutateJsonFileLocked(DOC_SESSIONS_PATH, () => obj, { defaultValue: {} });
|
|
2234
2291
|
}
|
|
2235
2292
|
|
|
2293
|
+
// Hourly hygiene sweep — drops prompt-hash mismatches and trims the LRU cap.
|
|
2294
|
+
// No TTL involved; sessions live until explicitly evicted by these two policies.
|
|
2236
2295
|
const _docSessionPruneTimer = setInterval(() => {
|
|
2237
2296
|
if (pruneDocSessions()) persistDocSessions();
|
|
2238
|
-
},
|
|
2297
|
+
}, 60 * 60 * 1000);
|
|
2239
2298
|
if (_docSessionPruneTimer.unref) _docSessionPruneTimer.unref();
|
|
2240
2299
|
|
|
2241
2300
|
// Debounced variant — coalesces rapid writes (e.g. back-to-back doc-chat turns)
|
|
@@ -2270,16 +2329,6 @@ function resolveSession(store, key) {
|
|
|
2270
2329
|
persistDocSessions();
|
|
2271
2330
|
return null;
|
|
2272
2331
|
}
|
|
2273
|
-
if (s.turnCount >= CC_SESSION_MAX_TURNS) {
|
|
2274
|
-
docSessions.delete(key);
|
|
2275
|
-
persistDocSessions();
|
|
2276
|
-
return null;
|
|
2277
|
-
}
|
|
2278
|
-
if (_sessionExpired(s.lastActiveAt || s.createdAt, DOC_SESSION_TTL_MS)) {
|
|
2279
|
-
docSessions.delete(key);
|
|
2280
|
-
persistDocSessions();
|
|
2281
|
-
return null;
|
|
2282
|
-
}
|
|
2283
2332
|
return s;
|
|
2284
2333
|
}
|
|
2285
2334
|
|
|
@@ -3083,12 +3132,17 @@ function spawnEngine() {
|
|
|
3083
3132
|
return engineProc.pid;
|
|
3084
3133
|
}
|
|
3085
3134
|
|
|
3135
|
+
// Force-kill the engine process WITHOUT /T — agents are spawned as the engine's
|
|
3136
|
+
// children with `detached: true` and must survive an engine restart so the new
|
|
3137
|
+
// engine can re-attach via PID files + live-output.log mtimes (engine.js:1102,
|
|
3138
|
+
// engine/cli.js re-attach). Tree-kill would orphan in-flight work. Mirrors
|
|
3139
|
+
// bin/minions.js:killPidOnly.
|
|
3086
3140
|
function killEnginePid(pid) {
|
|
3087
3141
|
const { execFileSync } = require('child_process');
|
|
3088
3142
|
try {
|
|
3089
3143
|
const safePid = shared.validatePid(pid);
|
|
3090
3144
|
if (process.platform === 'win32') {
|
|
3091
|
-
execFileSync('taskkill', ['/PID', String(safePid), '/F'
|
|
3145
|
+
execFileSync('taskkill', ['/PID', String(safePid), '/F'], { stdio: 'pipe', timeout: 5000, windowsHide: true });
|
|
3092
3146
|
} else {
|
|
3093
3147
|
process.kill(safePid, 'SIGKILL');
|
|
3094
3148
|
}
|
|
@@ -6343,9 +6397,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6343
6397
|
const configPath = path.join(MINIONS_DIR, 'config.json');
|
|
6344
6398
|
const config = safeJson(configPath) || {};
|
|
6345
6399
|
if (!config.engine) config.engine = {};
|
|
6346
|
-
if (!config.claude) config.claude = {};
|
|
6347
6400
|
if (!config.agents) config.agents = {};
|
|
6348
|
-
|
|
6401
|
+
shared.pruneDefaultClaudeConfig(config);
|
|
6349
6402
|
|
|
6350
6403
|
const _clamped = [];
|
|
6351
6404
|
const _engineModelDiscovery = require('./engine/model-discovery');
|
|
@@ -6513,9 +6566,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6513
6566
|
}
|
|
6514
6567
|
|
|
6515
6568
|
if (body.claude) {
|
|
6569
|
+
if (!config.claude) config.claude = {};
|
|
6516
6570
|
for (const key of ['allowedTools', 'outputFormat']) {
|
|
6517
6571
|
if (body.claude[key] !== undefined) config.claude[key] = String(body.claude[key]);
|
|
6518
6572
|
}
|
|
6573
|
+
shared.pruneDefaultClaudeConfig(config);
|
|
6519
6574
|
}
|
|
6520
6575
|
|
|
6521
6576
|
if (body.agents) {
|
|
@@ -6642,6 +6697,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6642
6697
|
}
|
|
6643
6698
|
}
|
|
6644
6699
|
|
|
6700
|
+
shared.pruneDefaultClaudeConfig(config);
|
|
6645
6701
|
safeWrite(configPath, config);
|
|
6646
6702
|
// Refresh in-memory CONFIG so subsequent reads see the update
|
|
6647
6703
|
reloadConfig();
|
|
@@ -6667,7 +6723,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6667
6723
|
try {
|
|
6668
6724
|
const config = queries.getConfig();
|
|
6669
6725
|
config.engine = { ...shared.ENGINE_DEFAULTS };
|
|
6670
|
-
config.claude
|
|
6726
|
+
delete config.claude;
|
|
6671
6727
|
config.agents = { ...shared.DEFAULT_AGENTS };
|
|
6672
6728
|
safeWrite(path.join(MINIONS_DIR, 'config.json'), config);
|
|
6673
6729
|
reloadConfig();
|
|
@@ -6732,6 +6788,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6732
6788
|
|
|
6733
6789
|
async function handleStatus(req, res) {
|
|
6734
6790
|
try {
|
|
6791
|
+
_recordDashboardBrowserPresenceFromRequest(req);
|
|
6735
6792
|
// Use pre-serialized JSON and pre-computed gzip buffer — zero per-request compression
|
|
6736
6793
|
const json = getStatusJson();
|
|
6737
6794
|
res.setHeader('Content-Type', 'application/json');
|
|
@@ -6871,6 +6928,20 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6871
6928
|
|
|
6872
6929
|
// Status & health
|
|
6873
6930
|
{ method: 'GET', path: '/api/status', desc: 'Full dashboard status snapshot (agents, PRDs, work items, dispatch, etc.)', handler: handleStatus },
|
|
6931
|
+
{ method: 'GET', path: '/api/browser-presence', desc: 'Whether a dashboard browser tab was recently active', handler: (req, res) => {
|
|
6932
|
+
return jsonReply(res, 200, _getDashboardBrowserPresence(), req);
|
|
6933
|
+
}},
|
|
6934
|
+
{ method: 'POST', path: '/api/browser-presence', desc: 'Record dashboard browser tab heartbeat or close', params: 'tabId, closed?, url?, visibility?', handler: async (req, res) => {
|
|
6935
|
+
const body = await readBody(req);
|
|
6936
|
+
const tabId = _normalizeDashboardTabId(body.tabId);
|
|
6937
|
+
if (!tabId) return jsonReply(res, 400, { error: 'valid tabId required' }, req);
|
|
6938
|
+
_recordDashboardBrowserPresence(tabId, {
|
|
6939
|
+
closed: !!body.closed,
|
|
6940
|
+
url: body.url || '',
|
|
6941
|
+
visibility: body.visibility || '',
|
|
6942
|
+
});
|
|
6943
|
+
return jsonReply(res, 200, { ok: true }, req);
|
|
6944
|
+
}},
|
|
6874
6945
|
{ method: 'GET', path: '/api/status-stream', desc: 'SSE stream of real-time status updates', handler: (req, res) => {
|
|
6875
6946
|
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
|
6876
6947
|
res.write('data: ' + getStatusJson() + '\n\n');
|
|
@@ -7512,14 +7583,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7512
7583
|
if (route.path === '/api/skill') {
|
|
7513
7584
|
if (!req.url.startsWith('/api/skill?') && req.url !== '/api/skill') continue;
|
|
7514
7585
|
const _result = await route.handler(req, res, {});
|
|
7515
|
-
if (pathname.startsWith('/api/') && !pathname.includes('/status') && !pathname.includes('/hot-reload') && !pathname.includes('/status-stream')) {
|
|
7586
|
+
if (pathname.startsWith('/api/') && !pathname.includes('/status') && !pathname.includes('/hot-reload') && !pathname.includes('/status-stream') && !pathname.includes('/browser-presence')) {
|
|
7516
7587
|
console.log(` ${req.method} ${pathname} ${Date.now() - _reqStart}ms`);
|
|
7517
7588
|
}
|
|
7518
7589
|
return _result;
|
|
7519
7590
|
}
|
|
7520
7591
|
if (pathname !== route.path) continue;
|
|
7521
7592
|
const _result = await route.handler(req, res, {});
|
|
7522
|
-
if (pathname.startsWith('/api/') && !pathname.includes('/status') && !pathname.includes('/hot-reload') && !pathname.includes('/status-stream')) {
|
|
7593
|
+
if (pathname.startsWith('/api/') && !pathname.includes('/status') && !pathname.includes('/hot-reload') && !pathname.includes('/status-stream') && !pathname.includes('/browser-presence')) {
|
|
7523
7594
|
console.log(` ${req.method} ${pathname} ${Date.now() - _reqStart}ms`);
|
|
7524
7595
|
}
|
|
7525
7596
|
return _result;
|
|
@@ -7527,7 +7598,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7527
7598
|
const m = pathname.match(route.path);
|
|
7528
7599
|
if (m) {
|
|
7529
7600
|
const _result = await route.handler(req, res, m);
|
|
7530
|
-
if (pathname.startsWith('/api/') && !pathname.includes('/status') && !pathname.includes('/hot-reload') && !pathname.includes('/status-stream')) {
|
|
7601
|
+
if (pathname.startsWith('/api/') && !pathname.includes('/status') && !pathname.includes('/hot-reload') && !pathname.includes('/status-stream') && !pathname.includes('/browser-presence')) {
|
|
7531
7602
|
console.log(` ${req.method} ${pathname} ${Date.now() - _reqStart}ms`);
|
|
7532
7603
|
}
|
|
7533
7604
|
return _result;
|
|
@@ -7649,9 +7720,8 @@ if (require.main === module) {
|
|
|
7649
7720
|
console.log(`\n Auto-refreshes every 4s. Ctrl+C to stop.\n`);
|
|
7650
7721
|
|
|
7651
7722
|
// Auto-open the browser unless suppressed. `minions restart` and the
|
|
7652
|
-
// upgrade path set MINIONS_NO_AUTO_OPEN=1 when
|
|
7653
|
-
//
|
|
7654
|
-
// would just be a duplicate.
|
|
7723
|
+
// upgrade path set MINIONS_NO_AUTO_OPEN=1 only when a browser tab was
|
|
7724
|
+
// recently polling the old dashboard, so a new tab would just be a duplicate.
|
|
7655
7725
|
if (!process.env.MINIONS_NO_AUTO_OPEN) {
|
|
7656
7726
|
const { exec } = require('child_process');
|
|
7657
7727
|
try {
|
package/engine/shared.js
CHANGED
|
@@ -1029,9 +1029,8 @@ const ENGINE_DEFAULTS = {
|
|
|
1029
1029
|
mainBranchCacheMaxEntries: 100, // bound repo/branch detection cache in long-lived dashboard/engine processes
|
|
1030
1030
|
removeWorktreeFailureTtlMs: 24 * 60 * 60 * 1000, // stale failed paths are forgotten after a day
|
|
1031
1031
|
removeWorktreeFailureMaxEntries: 1000, // bound failed-worktree retry suppression cache
|
|
1032
|
-
ccMaxTurns: 50, // max tool-use turns
|
|
1033
|
-
|
|
1034
|
-
docSessionMaxEntries: 200, // cap doc-chat session map/disk store by least-recent activity
|
|
1032
|
+
ccMaxTurns: 50, // max tool-use turns per CC/doc-chat call before CLI stops (per response, not per session)
|
|
1033
|
+
docSessionMaxEntries: 200, // cap doc-chat session map/disk store by least-recent activity (LRU; sessions are non-expiring otherwise)
|
|
1035
1034
|
ccLiveStreamMaxAgeMs: 30 * 60 * 1000, // hard cap reconnect buffers if abort/cleanup stalls
|
|
1036
1035
|
metricsFlushIntervalMs: 10000, // batch trackEngineUsage writes to metrics.json — flushed every 10s instead of per-call to cut lock contention and dashboard mtime churn
|
|
1037
1036
|
maxLlmRawBytes: 256 * 1024, // keep only a bounded stdout tail from direct Claude calls
|
|
@@ -1244,7 +1243,7 @@ function _resetLegacyCcModelMigrationFlag() {
|
|
|
1244
1243
|
* Warnings emitted:
|
|
1245
1244
|
* - Unknown CLI: any `cli` value (per-agent, ccCli, defaultCli) not in
|
|
1246
1245
|
* `registeredRuntimes`. Each unknown value produces one entry.
|
|
1247
|
-
* - Deprecated `config.claude.*`
|
|
1246
|
+
* - Deprecated `config.claude.*` overrides: non-default fields listed in
|
|
1248
1247
|
* `ENGINE_DEFAULTS._deprecatedConfigClaudeFields` under `config.claude`.
|
|
1249
1248
|
* - Bare-mode misconfig: `engine.claudeBareMode === true` paired with
|
|
1250
1249
|
* CC running on the Claude runtime (resolved via `resolveCcCli`) and no
|
|
@@ -1283,11 +1282,15 @@ function runtimeConfigWarnings(config, registeredRuntimes) {
|
|
|
1283
1282
|
if (agent && typeof agent === 'object') checkCli(`agents.${agentId}.cli`, agent.cli);
|
|
1284
1283
|
}
|
|
1285
1284
|
|
|
1286
|
-
// 2. Deprecated `config.claude.*`
|
|
1285
|
+
// 2. Deprecated `config.claude.*` overrides. Generated defaults from older
|
|
1286
|
+
// init versions are ignored here and pruned the next time config is saved.
|
|
1287
1287
|
const claude = config.claude;
|
|
1288
1288
|
if (claude && typeof claude === 'object') {
|
|
1289
1289
|
const deprecatedKeys = ENGINE_DEFAULTS._deprecatedConfigClaudeFields || [];
|
|
1290
|
-
const present = deprecatedKeys.filter(k =>
|
|
1290
|
+
const present = deprecatedKeys.filter(k => {
|
|
1291
|
+
if (!Object.prototype.hasOwnProperty.call(claude, k)) return false;
|
|
1292
|
+
return !(Object.prototype.hasOwnProperty.call(DEFAULT_CLAUDE, k) && claude[k] === DEFAULT_CLAUDE[k]);
|
|
1293
|
+
});
|
|
1291
1294
|
if (present.length > 0) {
|
|
1292
1295
|
warnings.push({
|
|
1293
1296
|
id: 'deprecated-config-claude',
|
|
@@ -1573,6 +1576,37 @@ const DEFAULT_CLAUDE = {
|
|
|
1573
1576
|
allowedTools: 'Edit,Write,Read,Bash,Glob,Grep,Agent,WebFetch,WebSearch',
|
|
1574
1577
|
};
|
|
1575
1578
|
|
|
1579
|
+
function pruneDefaultClaudeConfig(config) {
|
|
1580
|
+
if (!config || typeof config !== 'object') return false;
|
|
1581
|
+
const claude = config.claude;
|
|
1582
|
+
if (claude === undefined || claude === null) return false;
|
|
1583
|
+
if (typeof claude !== 'object' || Array.isArray(claude)) {
|
|
1584
|
+
delete config.claude;
|
|
1585
|
+
return true;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
let changed = false;
|
|
1589
|
+
const removeKey = (key) => {
|
|
1590
|
+
if (Object.prototype.hasOwnProperty.call(claude, key)) {
|
|
1591
|
+
delete claude[key];
|
|
1592
|
+
changed = true;
|
|
1593
|
+
}
|
|
1594
|
+
};
|
|
1595
|
+
|
|
1596
|
+
removeKey('permissionMode');
|
|
1597
|
+
for (const key of ['binary', 'outputFormat', 'allowedTools']) {
|
|
1598
|
+
if (Object.prototype.hasOwnProperty.call(claude, key) && claude[key] === DEFAULT_CLAUDE[key]) {
|
|
1599
|
+
removeKey(key);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
if (Object.keys(claude).length === 0) {
|
|
1604
|
+
delete config.claude;
|
|
1605
|
+
changed = true;
|
|
1606
|
+
}
|
|
1607
|
+
return changed;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1576
1610
|
// ── Project Helpers ──────────────────────────────────────────────────────────
|
|
1577
1611
|
|
|
1578
1612
|
function getProjects(config) {
|
|
@@ -3068,6 +3102,7 @@ module.exports = {
|
|
|
3068
3102
|
DEFAULT_AGENT_METRICS,
|
|
3069
3103
|
DEFAULT_AGENTS,
|
|
3070
3104
|
DEFAULT_CLAUDE,
|
|
3105
|
+
pruneDefaultClaudeConfig,
|
|
3071
3106
|
getProjects,
|
|
3072
3107
|
projectRoot,
|
|
3073
3108
|
projectStateDir,
|
package/minions.js
CHANGED
|
@@ -17,7 +17,7 @@ const path = require('path');
|
|
|
17
17
|
const readline = require('readline');
|
|
18
18
|
const { execSync } = require('child_process');
|
|
19
19
|
const shared = require('./engine/shared');
|
|
20
|
-
const { ENGINE_DEFAULTS, DEFAULT_AGENTS
|
|
20
|
+
const { ENGINE_DEFAULTS, DEFAULT_AGENTS } = shared;
|
|
21
21
|
const projectDiscovery = require('./engine/project-discovery');
|
|
22
22
|
|
|
23
23
|
const MINIONS_HOME = process.env.MINIONS_HOME ? path.resolve(process.env.MINIONS_HOME) : __dirname;
|
|
@@ -385,11 +385,7 @@ async function initMinions({ skipScan = false, scanRoot, scanDepth } = {}) {
|
|
|
385
385
|
if (k === 'defaultCli') continue;
|
|
386
386
|
if (config.engine[k] === undefined) config.engine[k] = v;
|
|
387
387
|
}
|
|
388
|
-
|
|
389
|
-
delete config.claude.permissionMode;
|
|
390
|
-
for (const [k, v] of Object.entries(DEFAULT_CLAUDE)) {
|
|
391
|
-
if (config.claude[k] === undefined) config.claude[k] = v;
|
|
392
|
-
}
|
|
388
|
+
shared.pruneDefaultClaudeConfig(config);
|
|
393
389
|
if (!config.agents || Object.keys(config.agents).length === 0) {
|
|
394
390
|
config.agents = { ...DEFAULT_AGENTS };
|
|
395
391
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1770",
|
|
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"
|