@yemi33/minions 0.1.1731 → 0.1.1733
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 +11 -2
- package/dashboard/js/render-pipelines.js +8 -1
- package/dashboard/js/render-schedules.js +49 -6
- package/dashboard.js +119 -10
- package/engine/copilot-models.json +1 -1
- package/engine/routing.js +7 -6
- package/engine/shared.js +44 -5
- package/engine.js +22 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.1733 (2026-05-05)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- dryRun resolveAgent, bounded mainBranch cache, SSE heartbeat helper
|
|
7
|
+
|
|
8
|
+
## 0.1.1732 (2026-05-05)
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
- fix pipeline cron display (#2087)
|
|
12
|
+
|
|
13
|
+
## 0.1.1730 (2026-05-05)
|
|
4
14
|
|
|
5
15
|
### Features
|
|
6
|
-
- fix project discovery main branch fallback (#2085)
|
|
7
16
|
- detect no-op PR fixes (#2077)
|
|
8
17
|
|
|
9
18
|
## 0.1.1728 (2026-05-05)
|
|
@@ -268,6 +268,13 @@ function _buildNodeChain(stages, run, options) {
|
|
|
268
268
|
return html;
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
function _renderPipelineTriggerLabel(cron) {
|
|
272
|
+
if (!cron) return 'Manual trigger';
|
|
273
|
+
var human = _cronToHuman(cron);
|
|
274
|
+
var tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
275
|
+
return escHtml(human) + ' <span style="opacity:0.6">(' + escHtml(tz) + ')</span>';
|
|
276
|
+
}
|
|
277
|
+
|
|
271
278
|
function renderPipelines(pipelines) {
|
|
272
279
|
pipelines = (pipelines || []).filter(function(p) { return !isDeleted('pipeline:' + p.id); });
|
|
273
280
|
_pipelinesData = pipelines;
|
|
@@ -331,7 +338,7 @@ function openPipelineDetail(id) {
|
|
|
331
338
|
// Status + actions
|
|
332
339
|
var activeRun = _getPipelineActiveRun(p);
|
|
333
340
|
html += '<div style="display:flex;justify-content:space-between;align-items:center">' +
|
|
334
|
-
'<span style="font-size:10px;color:var(--muted)">' + (p.trigger?.cron
|
|
341
|
+
'<span style="font-size:10px;color:var(--muted)">' + _renderPipelineTriggerLabel(p.trigger?.cron) + ' · ' + escHtml(_getPipelineStageLabel(p)) + '</span>' +
|
|
335
342
|
'<div style="display:flex;gap:6px">' +
|
|
336
343
|
(activeRun
|
|
337
344
|
? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--red);border-color:var(--red)" onclick="_abortPipeline(\'' + escHtml(id) + '\',this)">Abort</button>' +
|
|
@@ -5,14 +5,50 @@
|
|
|
5
5
|
const _DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
6
6
|
const _DAY_NAMES = ['Sundays', 'Mondays', 'Tuesdays', 'Wednesdays', 'Thursdays', 'Fridays', 'Saturdays'];
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
function _formatCronDowList(days, timeStr, fallback) {
|
|
9
|
+
const normalized = [...new Set(days)].sort((a, b) => a - b);
|
|
10
|
+
const key = normalized.join(',');
|
|
11
|
+
if (key === '0,1,2,3,4,5,6') return 'Daily at ' + timeStr;
|
|
12
|
+
if (key === '1,2,3,4,5') return 'Weekdays at ' + timeStr;
|
|
13
|
+
if (key === '0,6') return 'Weekends at ' + timeStr;
|
|
14
|
+
if (normalized.length === 1) return _DAY_NAMES[normalized[0]] + ' at ' + timeStr;
|
|
15
|
+
if (normalized.length > 1) return normalized.map(d => _DAY_NAMES[d]).join(', ') + ' at ' + timeStr;
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function _parseCronDowList(dow, allowSevenAsSunday) {
|
|
20
|
+
if (!dow || typeof dow !== 'string') return null;
|
|
21
|
+
const values = [];
|
|
22
|
+
const parts = dow.split(',');
|
|
23
|
+
for (const part of parts) {
|
|
24
|
+
const token = part.trim();
|
|
25
|
+
if (!/^\d+$/.test(token)) return null;
|
|
26
|
+
let day = parseInt(token, 10);
|
|
27
|
+
if (allowSevenAsSunday && day === 7) day = 0;
|
|
28
|
+
if (day < 0 || day > 6) return null;
|
|
29
|
+
values.push(day);
|
|
30
|
+
}
|
|
31
|
+
return values;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function _cronTimeString(minute, hour) {
|
|
35
|
+
const h = parseInt(hour, 10);
|
|
36
|
+
const m = parseInt(minute, 10);
|
|
37
|
+
if (isNaN(h) || isNaN(m)) return null;
|
|
38
|
+
return String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Convert 3-field Minions cron or common 5-field cron to human-readable text */
|
|
9
42
|
function _cronToHuman(cron) {
|
|
10
43
|
if (!cron || typeof cron !== 'string') return cron || '';
|
|
11
44
|
const parts = cron.trim().split(/\s+/);
|
|
12
|
-
if (parts.length !== 3) return cron;
|
|
13
|
-
const
|
|
45
|
+
if (parts.length !== 3 && parts.length !== 5) return cron;
|
|
46
|
+
const minute = parts[0];
|
|
47
|
+
const hour = parts[1];
|
|
48
|
+
const dow = parts.length === 3 ? parts[2] : parts[4];
|
|
14
49
|
|
|
15
|
-
if (
|
|
50
|
+
if (parts.length === 5 && (parts[2] !== '*' || parts[3] !== '*')) return cron;
|
|
51
|
+
if (parts.length === 3 && minute === '*' && hour === '*' && dow === '*') return 'Every minute';
|
|
16
52
|
|
|
17
53
|
const h = parseInt(hour, 10);
|
|
18
54
|
const m = parseInt(minute, 10);
|
|
@@ -26,13 +62,20 @@ function _cronToHuman(cron) {
|
|
|
26
62
|
const normalized = dow.split(',').map(d => d.trim()).sort().join(',');
|
|
27
63
|
if (dow === '1-5' || normalized === '1,2,3,4,5') return 'Weekdays at ' + timeStr;
|
|
28
64
|
if (normalized === '0,6' || normalized === '6,0') return 'Weekends at ' + timeStr;
|
|
65
|
+
if (parts.length === 5 && (dow === '0-6' || dow === '0-7')) return 'Daily at ' + timeStr;
|
|
29
66
|
|
|
30
67
|
// Single day
|
|
31
|
-
|
|
32
|
-
if (
|
|
68
|
+
let dayNum = parseInt(dow, 10);
|
|
69
|
+
if (parts.length === 5 && dayNum === 7 && dow === '7') dayNum = 0;
|
|
70
|
+
if (!isNaN(dayNum) && dayNum >= 0 && dayNum <= 6 && String(parseInt(dow, 10)) === dow) {
|
|
33
71
|
return _DAY_NAMES[dayNum] + ' at ' + timeStr;
|
|
34
72
|
}
|
|
35
73
|
|
|
74
|
+
if (parts.length === 5) {
|
|
75
|
+
const days = _parseCronDowList(dow, true);
|
|
76
|
+
if (days) return _formatCronDowList(days, timeStr, cron);
|
|
77
|
+
}
|
|
78
|
+
|
|
36
79
|
return cron;
|
|
37
80
|
}
|
|
38
81
|
|
package/dashboard.js
CHANGED
|
@@ -472,6 +472,43 @@ let HTML = HTML_RAW;
|
|
|
472
472
|
let HTML_GZ = zlib.gzipSync(HTML);
|
|
473
473
|
let HTML_ETAG = '"' + require('crypto').createHash('md5').update(HTML).digest('hex') + '"';
|
|
474
474
|
|
|
475
|
+
const SSE_CLIENT_HEARTBEAT_MS = 30000;
|
|
476
|
+
const _sseClientCleanups = new WeakMap();
|
|
477
|
+
function _removeSseClient(clientSet, res) {
|
|
478
|
+
const cleanup = _sseClientCleanups.get(res);
|
|
479
|
+
if (cleanup) cleanup();
|
|
480
|
+
else clientSet.delete(res);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function _trackSseClient(clientSet, req, res, { heartbeatMs = SSE_CLIENT_HEARTBEAT_MS } = {}) {
|
|
484
|
+
let closed = false;
|
|
485
|
+
let heartbeatTimer = null;
|
|
486
|
+
const cleanup = () => {
|
|
487
|
+
if (closed) return;
|
|
488
|
+
closed = true;
|
|
489
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
490
|
+
heartbeatTimer = null;
|
|
491
|
+
clientSet.delete(res);
|
|
492
|
+
_sseClientCleanups.delete(res);
|
|
493
|
+
};
|
|
494
|
+
const heartbeat = () => {
|
|
495
|
+
if (res.destroyed || res.writableEnded) {
|
|
496
|
+
cleanup();
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
try { res.write(': heartbeat\n\n'); } catch { cleanup(); }
|
|
500
|
+
};
|
|
501
|
+
heartbeatTimer = setInterval(heartbeat, heartbeatMs);
|
|
502
|
+
if (heartbeatTimer.unref) heartbeatTimer.unref();
|
|
503
|
+
_sseClientCleanups.set(res, cleanup);
|
|
504
|
+
clientSet.add(res);
|
|
505
|
+
req.on('close', cleanup);
|
|
506
|
+
req.on('aborted', cleanup);
|
|
507
|
+
res.on('close', cleanup);
|
|
508
|
+
res.on('error', cleanup);
|
|
509
|
+
return cleanup;
|
|
510
|
+
}
|
|
511
|
+
|
|
475
512
|
// Hot-reload: watch dashboard/ directory for changes, rebuild, and push reload to browsers
|
|
476
513
|
const _hotReloadClients = new Set();
|
|
477
514
|
|
|
@@ -486,11 +523,11 @@ function rebuildDashboardHtml() {
|
|
|
486
523
|
console.log(' Dashboard hot-reloaded');
|
|
487
524
|
// Push reload to all connected browsers via status-stream (saves a connection)
|
|
488
525
|
for (const res of _statusStreamClients) {
|
|
489
|
-
try { res.write('event: reload\ndata: reload\n\n'); } catch { _statusStreamClients
|
|
526
|
+
try { res.write('event: reload\ndata: reload\n\n'); } catch { _removeSseClient(_statusStreamClients, res); }
|
|
490
527
|
}
|
|
491
528
|
// Legacy hot-reload clients
|
|
492
529
|
for (const res of _hotReloadClients) {
|
|
493
|
-
try { res.write('data: reload\n\n'); } catch { _hotReloadClients
|
|
530
|
+
try { res.write('data: reload\n\n'); } catch { _removeSseClient(_hotReloadClients, res); }
|
|
494
531
|
}
|
|
495
532
|
} catch (e) { console.error(' Hot-reload error:', e.message); }
|
|
496
533
|
}
|
|
@@ -742,7 +779,7 @@ function invalidateStatusCache(opts) {
|
|
|
742
779
|
if (_statusStreamClients.size === 0) return;
|
|
743
780
|
const data = getStatusJson();
|
|
744
781
|
for (const res of _statusStreamClients) {
|
|
745
|
-
try { res.write('data: ' + data + '\n\n'); } catch { _statusStreamClients
|
|
782
|
+
try { res.write('data: ' + data + '\n\n'); } catch { _removeSseClient(_statusStreamClients, res); }
|
|
746
783
|
}
|
|
747
784
|
}, 500);
|
|
748
785
|
}
|
|
@@ -874,7 +911,7 @@ setInterval(() => {
|
|
|
874
911
|
if (data === _lastStatusPushRef) return; // O(1) reference comparison — new string ref means content changed
|
|
875
912
|
_lastStatusPushRef = data;
|
|
876
913
|
for (const res of _statusStreamClients) {
|
|
877
|
-
try { res.write('data: ' + data + '\n\n'); } catch { _statusStreamClients
|
|
914
|
+
try { res.write('data: ' + data + '\n\n'); } catch { _removeSseClient(_statusStreamClients, res); }
|
|
878
915
|
}
|
|
879
916
|
}, 10000);
|
|
880
917
|
|
|
@@ -894,6 +931,7 @@ const CC_LOCK_WAIT_MS = 200; // grace period for previous handler's finally to r
|
|
|
894
931
|
const CC_STREAM_HEARTBEAT_MS = 15000; // keep streaming responses alive across proxies/restart races
|
|
895
932
|
const CC_STREAM_REATTACH_GRACE_MS = 60000; // keep CC job alive briefly after disconnect so the UI can reattach
|
|
896
933
|
const CC_STREAM_DONE_RETENTION_MS = 30000; // retain final payload briefly so reconnect can still receive it
|
|
934
|
+
const CC_LIVE_STREAM_MAX_AGE_MS = shared.ENGINE_DEFAULTS.ccLiveStreamMaxAgeMs;
|
|
897
935
|
// Doc-chat is interactive — long-doc edits with multi-step Read+Write tool use can run
|
|
898
936
|
// 4–5 min on `canEdit:true` paths. CC's default 2-min timeout was killing legitimate
|
|
899
937
|
// edits mid-stream. Pinned to 6 min as the bounded but generous ceiling.
|
|
@@ -902,6 +940,9 @@ function _releaseCCTab(tabId) { ccInFlightTabs.delete(tabId); ccInFlightAborts.d
|
|
|
902
940
|
function _getCcLiveStream(tabId) {
|
|
903
941
|
return ccLiveStreams.get(tabId) || null;
|
|
904
942
|
}
|
|
943
|
+
function _touchCcLiveStream(state) {
|
|
944
|
+
if (state) state.updatedAt = Date.now();
|
|
945
|
+
}
|
|
905
946
|
function _clearCcLiveTimers(tabId) {
|
|
906
947
|
const state = _getCcLiveStream(tabId);
|
|
907
948
|
if (!state) return;
|
|
@@ -934,6 +975,8 @@ function _ensureCcLiveStream(tabId) {
|
|
|
934
975
|
abortFn: null,
|
|
935
976
|
abortTimer: null,
|
|
936
977
|
cleanupTimer: null,
|
|
978
|
+
createdAt: Date.now(),
|
|
979
|
+
updatedAt: Date.now(),
|
|
937
980
|
};
|
|
938
981
|
ccLiveStreams.set(tabId, state);
|
|
939
982
|
return state;
|
|
@@ -943,6 +986,7 @@ function _attachCcLiveStream(tabId, writer, endResponse) {
|
|
|
943
986
|
_clearCcLiveTimers(tabId);
|
|
944
987
|
state.writer = writer;
|
|
945
988
|
state.endResponse = endResponse;
|
|
989
|
+
_touchCcLiveStream(state);
|
|
946
990
|
return state;
|
|
947
991
|
}
|
|
948
992
|
function _detachCcLiveStream(tabId, writer) {
|
|
@@ -961,7 +1005,9 @@ function _scheduleCcLiveAbort(tabId) {
|
|
|
961
1005
|
const live = _getCcLiveStream(tabId);
|
|
962
1006
|
if (!live || live.donePayload || live.writer) return;
|
|
963
1007
|
try { if (live.abortFn) live.abortFn(); } catch {}
|
|
1008
|
+
_scheduleCcLiveCleanup(tabId, CC_STREAM_REATTACH_GRACE_MS);
|
|
964
1009
|
}, CC_STREAM_REATTACH_GRACE_MS);
|
|
1010
|
+
if (state.abortTimer.unref) state.abortTimer.unref();
|
|
965
1011
|
}
|
|
966
1012
|
function _scheduleCcLiveCleanup(tabId, delayMs = CC_STREAM_DONE_RETENTION_MS) {
|
|
967
1013
|
const state = _getCcLiveStream(tabId);
|
|
@@ -972,7 +1018,25 @@ function _scheduleCcLiveCleanup(tabId, delayMs = CC_STREAM_DONE_RETENTION_MS) {
|
|
|
972
1018
|
if (!live || live.writer) return;
|
|
973
1019
|
_clearCcLiveStream(tabId);
|
|
974
1020
|
}, delayMs);
|
|
1021
|
+
if (state.cleanupTimer.unref) state.cleanupTimer.unref();
|
|
1022
|
+
}
|
|
1023
|
+
function _sweepCcLiveStreams() {
|
|
1024
|
+
const now = Date.now();
|
|
1025
|
+
for (const [tabId, state] of ccLiveStreams.entries()) {
|
|
1026
|
+
const age = now - (state.updatedAt || state.createdAt || now);
|
|
1027
|
+
if (state.donePayload && !state.writer && age > CC_STREAM_DONE_RETENTION_MS) {
|
|
1028
|
+
_clearCcLiveStream(tabId);
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
if (age <= CC_LIVE_STREAM_MAX_AGE_MS) continue;
|
|
1032
|
+
try { if (state.abortFn) state.abortFn(); } catch {}
|
|
1033
|
+
try { if (state.endResponse) state.endResponse(); } catch {}
|
|
1034
|
+
_releaseCCTab(tabId);
|
|
1035
|
+
_clearCcLiveStream(tabId);
|
|
1036
|
+
}
|
|
975
1037
|
}
|
|
1038
|
+
const _ccLiveSweepTimer = setInterval(_sweepCcLiveStreams, Math.min(CC_LIVE_STREAM_MAX_AGE_MS, 5 * 60 * 1000));
|
|
1039
|
+
if (_ccLiveSweepTimer.unref) _ccLiveSweepTimer.unref();
|
|
976
1040
|
function _ccTabIsInFlight(tabId) {
|
|
977
1041
|
if (!ccInFlightTabs.has(tabId)) return false;
|
|
978
1042
|
// Auto-release stale locks — if a request has been in-flight longer than CC_INFLIGHT_TIMEOUT_MS,
|
|
@@ -1797,7 +1861,7 @@ async function executeCCActions(actions) {
|
|
|
1797
1861
|
// Pre-flight routing check: warn the user if no agent is currently available so the new
|
|
1798
1862
|
// item won't sit pending invisibly. Routing failure is non-fatal — the WI was created.
|
|
1799
1863
|
try {
|
|
1800
|
-
const resolvedAgent = routing.resolveAgent(workType, CONFIG, { agentHints });
|
|
1864
|
+
const resolvedAgent = routing.resolveAgent(workType, CONFIG, { agentHints, dryRun: true });
|
|
1801
1865
|
if (!resolvedAgent) {
|
|
1802
1866
|
const lastResult = results[results.length - 1];
|
|
1803
1867
|
lastResult.warning = `Created ${id} but no agent is currently available to dispatch (routing returned no match for workType=${workType}${agentHints.length ? ', hints=' + agentHints.join(',') : ''}). Item will sit pending until an agent becomes available.`;
|
|
@@ -1841,12 +1905,20 @@ async function executeCCActions(actions) {
|
|
|
1841
1905
|
project_path: project.localPath || '',
|
|
1842
1906
|
task: `Build & test ${pr.id}: ${pr.title || ''}`,
|
|
1843
1907
|
}, `Build & test ${pr.id}: ${pr.title || ''}`,
|
|
1844
|
-
|
|
1908
|
+
{ dispatchKey, source: 'cc-build-and-test', pr, branch: pr.branch, project: { name: project.name, localPath: project.localPath } });
|
|
1845
1909
|
if (!item) {
|
|
1910
|
+
if (agentId?.startsWith('temp-')) {
|
|
1911
|
+
routing.tempAgents.delete(agentId);
|
|
1912
|
+
routing._claimedAgents.delete(agentId);
|
|
1913
|
+
}
|
|
1846
1914
|
results.push({ type: 'build-and-test', error: 'Failed to render build-and-test playbook' });
|
|
1847
1915
|
break;
|
|
1848
1916
|
}
|
|
1849
1917
|
const id = dispatchMod.addToDispatch(item);
|
|
1918
|
+
if (agentId?.startsWith('temp-')) {
|
|
1919
|
+
routing.tempAgents.delete(agentId);
|
|
1920
|
+
routing._claimedAgents.delete(agentId);
|
|
1921
|
+
}
|
|
1850
1922
|
results.push({ type: 'build-and-test', id, agent: agentId, pr: pr.id, ok: true });
|
|
1851
1923
|
break;
|
|
1852
1924
|
}
|
|
@@ -1975,8 +2047,36 @@ async function executeDocChatActions(actions) {
|
|
|
1975
2047
|
const CC_SESSIONS_PATH = path.join(ENGINE_DIR, 'cc-sessions.json');
|
|
1976
2048
|
const DOC_SESSIONS_PATH = path.join(ENGINE_DIR, 'doc-sessions.json');
|
|
1977
2049
|
const DOC_SESSION_TTL_MS = shared.ENGINE_DEFAULTS.docSessionTtlMs;
|
|
2050
|
+
const DOC_SESSION_MAX_ENTRIES = shared.ENGINE_DEFAULTS.docSessionMaxEntries;
|
|
1978
2051
|
const docSessions = new Map(); // key → { sessionId, lastActiveAt, turnCount }
|
|
1979
2052
|
|
|
2053
|
+
function _docSessionLastActiveMs(session) {
|
|
2054
|
+
const ms = Date.parse(session?.lastActiveAt || session?.createdAt || '');
|
|
2055
|
+
return Number.isFinite(ms) ? ms : 0;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
function pruneDocSessions() {
|
|
2059
|
+
let changed = false;
|
|
2060
|
+
for (const [key, s] of docSessions.entries()) {
|
|
2061
|
+
if (!s || (s.turnCount || 0) >= CC_SESSION_MAX_TURNS ||
|
|
2062
|
+
_sessionExpired(s.lastActiveAt || s.createdAt, DOC_SESSION_TTL_MS) ||
|
|
2063
|
+
s._promptHash !== _docChatPromptHash) {
|
|
2064
|
+
docSessions.delete(key);
|
|
2065
|
+
changed = true;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
const maxEntries = Number(DOC_SESSION_MAX_ENTRIES) > 0 ? Number(DOC_SESSION_MAX_ENTRIES) : 200;
|
|
2069
|
+
if (docSessions.size > maxEntries) {
|
|
2070
|
+
const oldest = Array.from(docSessions.entries())
|
|
2071
|
+
.sort((a, b) => _docSessionLastActiveMs(a[1]) - _docSessionLastActiveMs(b[1]));
|
|
2072
|
+
for (const [key] of oldest.slice(0, docSessions.size - maxEntries)) {
|
|
2073
|
+
docSessions.delete(key);
|
|
2074
|
+
changed = true;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
return changed;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
1980
2080
|
// Load persisted doc sessions on startup
|
|
1981
2081
|
try {
|
|
1982
2082
|
const saved = safeJson(DOC_SESSIONS_PATH);
|
|
@@ -1986,15 +2086,22 @@ try {
|
|
|
1986
2086
|
if (_sessionExpired(s.lastActiveAt || s.createdAt, DOC_SESSION_TTL_MS)) continue;
|
|
1987
2087
|
docSessions.set(key, s);
|
|
1988
2088
|
}
|
|
2089
|
+
pruneDocSessions();
|
|
1989
2090
|
}
|
|
1990
2091
|
} catch { /* optional */ }
|
|
1991
2092
|
|
|
1992
2093
|
function persistDocSessions() {
|
|
2094
|
+
pruneDocSessions();
|
|
1993
2095
|
const obj = {};
|
|
1994
2096
|
for (const [key, s] of docSessions) obj[key] = s;
|
|
1995
2097
|
safeWrite(DOC_SESSIONS_PATH, obj);
|
|
1996
2098
|
}
|
|
1997
2099
|
|
|
2100
|
+
const _docSessionPruneTimer = setInterval(() => {
|
|
2101
|
+
if (pruneDocSessions()) persistDocSessions();
|
|
2102
|
+
}, Math.min(DOC_SESSION_TTL_MS, 60 * 60 * 1000));
|
|
2103
|
+
if (_docSessionPruneTimer.unref) _docSessionPruneTimer.unref();
|
|
2104
|
+
|
|
1998
2105
|
// Debounced variant — coalesces rapid writes (e.g. back-to-back doc-chat turns)
|
|
1999
2106
|
let _persistDocSessionsTimer = null;
|
|
2000
2107
|
function schedulePersistDocSessions() {
|
|
@@ -2062,6 +2169,7 @@ function updateSession(store, key, sessionId, existing) {
|
|
|
2062
2169
|
_docHash: prev?._docHash || null,
|
|
2063
2170
|
_promptHash: _docChatPromptHash,
|
|
2064
2171
|
});
|
|
2172
|
+
pruneDocSessions();
|
|
2065
2173
|
schedulePersistDocSessions();
|
|
2066
2174
|
}
|
|
2067
2175
|
}
|
|
@@ -5255,6 +5363,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5255
5363
|
sessionId, effort, direct: true,
|
|
5256
5364
|
engineConfig,
|
|
5257
5365
|
onChunk: (text) => {
|
|
5366
|
+
_touchCcLiveStream(liveState);
|
|
5258
5367
|
const display = stripCCActionsForStream(text);
|
|
5259
5368
|
liveState.text = display;
|
|
5260
5369
|
// Once text is flowing, the SSE-replay branch (live.thinkingSent &&
|
|
@@ -5263,11 +5372,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5263
5372
|
if (liveState.writer) liveState.writer({ type: 'chunk', text: display });
|
|
5264
5373
|
},
|
|
5265
5374
|
onToolUse: (name, input) => {
|
|
5375
|
+
_touchCcLiveStream(liveState);
|
|
5266
5376
|
toolUses.push({ name, input: input || {} });
|
|
5267
5377
|
liveState.tools.push({ name, input: input || {} });
|
|
5268
5378
|
if (liveState.writer) liveState.writer({ type: 'tool', name, input: _lightToolInput(input) });
|
|
5269
5379
|
},
|
|
5270
5380
|
onThinking: () => {
|
|
5381
|
+
_touchCcLiveStream(liveState);
|
|
5271
5382
|
liveState.thinkingSent = true;
|
|
5272
5383
|
if (liveState.writer) liveState.writer({ type: 'thinking', text: 'Thinking...' });
|
|
5273
5384
|
},
|
|
@@ -6318,15 +6429,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6318
6429
|
{ method: 'GET', path: '/api/status-stream', desc: 'SSE stream of real-time status updates', handler: (req, res) => {
|
|
6319
6430
|
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
|
6320
6431
|
res.write('data: ' + getStatusJson() + '\n\n');
|
|
6321
|
-
_statusStreamClients
|
|
6322
|
-
req.on('close', () => _statusStreamClients.delete(res));
|
|
6432
|
+
_trackSseClient(_statusStreamClients, req, res);
|
|
6323
6433
|
}},
|
|
6324
6434
|
{ method: 'GET', path: '/api/health', desc: 'Lightweight health check for monitoring', handler: handleHealth },
|
|
6325
6435
|
{ method: 'GET', path: '/api/hot-reload', desc: 'SSE stream for dashboard hot-reload notifications', handler: (req, res) => {
|
|
6326
6436
|
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
|
6327
6437
|
res.write('data: connected\n\n');
|
|
6328
|
-
_hotReloadClients
|
|
6329
|
-
req.on('close', () => _hotReloadClients.delete(res));
|
|
6438
|
+
_trackSseClient(_hotReloadClients, req, res);
|
|
6330
6439
|
}},
|
|
6331
6440
|
|
|
6332
6441
|
// Work items
|
package/engine/routing.js
CHANGED
|
@@ -190,7 +190,7 @@ function normalizeAgentHints(agentHints, authorAgent = null, agents = null) {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
function resolveAgent(workType, config, opts = {}) {
|
|
193
|
-
const { authorAgent = null, agentHints = null } = opts || {};
|
|
193
|
+
const { authorAgent = null, agentHints = null, dryRun = false } = opts || {};
|
|
194
194
|
const route = routeForWorkType(workType);
|
|
195
195
|
const agents = config.agents || {};
|
|
196
196
|
|
|
@@ -199,7 +199,7 @@ function resolveAgent(workType, config, opts = {}) {
|
|
|
199
199
|
let fallback = route.fallback === '_author_' ? authorAgent : route.fallback;
|
|
200
200
|
|
|
201
201
|
const isAvailable = (id) => {
|
|
202
|
-
if (!agents[id] || !isAgentIdle(id) || _claimedAgents.has(id)) return false;
|
|
202
|
+
if (!agents[id] || !isAgentIdle(id) || (!dryRun && _claimedAgents.has(id))) return false;
|
|
203
203
|
// Budget check — no budget means infinite (no limit)
|
|
204
204
|
const budget = agents[id].monthlyBudgetUsd;
|
|
205
205
|
if (budget && budget > 0) {
|
|
@@ -214,23 +214,23 @@ function resolveAgent(workType, config, opts = {}) {
|
|
|
214
214
|
const idle = Object.keys(agents)
|
|
215
215
|
.filter(id => !excludeSet.has(id) && isAvailable(id))
|
|
216
216
|
.sort((a, b) => getAgentErrorRate(a) - getAgentErrorRate(b));
|
|
217
|
-
if (idle[0]) { _claimedAgents.add(idle[0]); return idle[0]; }
|
|
217
|
+
if (idle[0]) { if (!dryRun) _claimedAgents.add(idle[0]); return idle[0]; }
|
|
218
218
|
return null;
|
|
219
219
|
};
|
|
220
220
|
|
|
221
221
|
const hintedAgents = normalizeAgentHints(agentHints, authorAgent, agents);
|
|
222
222
|
if (hintedAgents.length > 0) {
|
|
223
223
|
for (const id of hintedAgents) {
|
|
224
|
-
if (isAvailable(id)) { _claimedAgents.add(id); return id; }
|
|
224
|
+
if (isAvailable(id)) { if (!dryRun) _claimedAgents.add(id); return id; }
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
// Resolve _any_ token — pick any available agent (#480)
|
|
229
229
|
if (preferred === ANY_AGENT) { const pick = pickAnyIdle(); if (pick) return pick; }
|
|
230
|
-
else if (preferred && isAvailable(preferred)) { _claimedAgents.add(preferred); return preferred; }
|
|
230
|
+
else if (preferred && isAvailable(preferred)) { if (!dryRun) _claimedAgents.add(preferred); return preferred; }
|
|
231
231
|
|
|
232
232
|
if (fallback === ANY_AGENT) { const pick = pickAnyIdle([preferred]); if (pick) return pick; }
|
|
233
|
-
else if (fallback && isAvailable(fallback)) { _claimedAgents.add(fallback); return fallback; }
|
|
233
|
+
else if (fallback && isAvailable(fallback)) { if (!dryRun) _claimedAgents.add(fallback); return fallback; }
|
|
234
234
|
|
|
235
235
|
// Fall back to any idle agent, preferring lower error rates
|
|
236
236
|
const anyIdle = pickAnyIdle([preferred, fallback]);
|
|
@@ -247,6 +247,7 @@ function resolveAgent(workType, config, opts = {}) {
|
|
|
247
247
|
log('info', `Temp agent refused for ${workType} — per-tick budget exhausted (maxConcurrent reached)`);
|
|
248
248
|
return null;
|
|
249
249
|
}
|
|
250
|
+
if (dryRun) return 'temp-preview';
|
|
250
251
|
_tempBudget--;
|
|
251
252
|
const tempId = `temp-${shared.uid()}`;
|
|
252
253
|
_claimedAgents.add(tempId);
|
package/engine/shared.js
CHANGED
|
@@ -684,10 +684,34 @@ function execAsync(cmd, opts = {}) {
|
|
|
684
684
|
* Cached per rootDir to avoid repeated git calls within a tick.
|
|
685
685
|
*/
|
|
686
686
|
const _mainBranchCache = new Map();
|
|
687
|
+
function _pruneTimedMap(map, { maxEntries, ttlMs, getTs, now = Date.now() }) {
|
|
688
|
+
const max = Number(maxEntries) > 0 ? Number(maxEntries) : Infinity;
|
|
689
|
+
const ttl = Number(ttlMs) > 0 ? Number(ttlMs) : Infinity;
|
|
690
|
+
for (const [key, value] of map) {
|
|
691
|
+
const entryTs = Number(getTs(value)) || 0;
|
|
692
|
+
if (entryTs <= 0 || now - entryTs > ttl) map.delete(key);
|
|
693
|
+
}
|
|
694
|
+
if (map.size <= max) return;
|
|
695
|
+
const oldest = Array.from(map.entries())
|
|
696
|
+
.sort((a, b) => (Number(getTs(a[1])) || 0) - (Number(getTs(b[1])) || 0));
|
|
697
|
+
for (const [key] of oldest.slice(0, Math.max(0, map.size - max))) map.delete(key);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function _setMainBranchCache(cacheKey, branch) {
|
|
701
|
+
_pruneTimedMap(_mainBranchCache, {
|
|
702
|
+
maxEntries: ENGINE_DEFAULTS.mainBranchCacheMaxEntries,
|
|
703
|
+
ttlMs: ENGINE_DEFAULTS.mainBranchCacheTtlMs,
|
|
704
|
+
getTs: entry => entry?.ts,
|
|
705
|
+
});
|
|
706
|
+
_mainBranchCache.set(cacheKey, { branch, ts: Date.now() });
|
|
707
|
+
}
|
|
708
|
+
|
|
687
709
|
function resolveMainBranch(rootDir, configuredBranch) {
|
|
688
710
|
const cacheKey = rootDir + ':' + (configuredBranch || '');
|
|
711
|
+
// _setMainBranchCache prunes on every write, which bounds the map without
|
|
712
|
+
// an extra O(n) scan on hits. The TTL check below handles per-entry expiry.
|
|
689
713
|
const cached = _mainBranchCache.get(cacheKey);
|
|
690
|
-
if (cached && (Date.now() - cached.ts) <
|
|
714
|
+
if (cached && (Date.now() - cached.ts) < ENGINE_DEFAULTS.mainBranchCacheTtlMs) return cached.branch;
|
|
691
715
|
|
|
692
716
|
const gitOpts = { cwd: rootDir, encoding: 'utf8', stdio: 'pipe', timeout: 5000, windowsHide: true };
|
|
693
717
|
|
|
@@ -695,12 +719,12 @@ function resolveMainBranch(rootDir, configuredBranch) {
|
|
|
695
719
|
if (configuredBranch) {
|
|
696
720
|
try {
|
|
697
721
|
_execSync(`git rev-parse --verify "${configuredBranch}"`, gitOpts);
|
|
698
|
-
|
|
722
|
+
_setMainBranchCache(cacheKey, configuredBranch);
|
|
699
723
|
return configuredBranch;
|
|
700
724
|
} catch { /* configured branch doesn't exist locally */ }
|
|
701
725
|
try {
|
|
702
726
|
_execSync(`git rev-parse --verify "origin/${configuredBranch}"`, gitOpts);
|
|
703
|
-
|
|
727
|
+
_setMainBranchCache(cacheKey, configuredBranch);
|
|
704
728
|
return configuredBranch;
|
|
705
729
|
} catch { /* not on remote either */ }
|
|
706
730
|
}
|
|
@@ -710,14 +734,14 @@ function resolveMainBranch(rootDir, configuredBranch) {
|
|
|
710
734
|
const ref = _execSync('git symbolic-ref refs/remotes/origin/HEAD', gitOpts).trim();
|
|
711
735
|
const branch = ref.replace('refs/remotes/origin/', '');
|
|
712
736
|
if (branch) {
|
|
713
|
-
|
|
737
|
+
_setMainBranchCache(cacheKey, branch);
|
|
714
738
|
return branch;
|
|
715
739
|
}
|
|
716
740
|
} catch { /* no remote HEAD set */ }
|
|
717
741
|
|
|
718
742
|
// 3. Fallback
|
|
719
743
|
const fallback = configuredBranch || 'main';
|
|
720
|
-
|
|
744
|
+
_setMainBranchCache(cacheKey, fallback);
|
|
721
745
|
return fallback;
|
|
722
746
|
}
|
|
723
747
|
|
|
@@ -885,9 +909,15 @@ const ENGINE_DEFAULTS = {
|
|
|
885
909
|
maxPendingContextEntryBytes: 256 * 1024, // 256 KB — cap each pendingContexts entry to prevent huge PR comments from bloating cooldowns.json
|
|
886
910
|
maxDispatchPromptBytes: 1024 * 1024, // 1 MB — dispatch items with prompts larger than this sidecar to engine/contexts/ to prevent dispatch.json OOM (#1167)
|
|
887
911
|
maxStateFileBytes: 100 * 1024 * 1024, // 100 MB — fail startup with a clear error when dispatch.json / cooldowns.json exceed this, rather than silently OOMing on JSON.parse (#1167)
|
|
912
|
+
mainBranchCacheTtlMs: 300000, // 5min — cache git default-branch detection, then prune expired entries
|
|
913
|
+
mainBranchCacheMaxEntries: 100, // bound repo/branch detection cache in long-lived dashboard/engine processes
|
|
914
|
+
removeWorktreeFailureTtlMs: 24 * 60 * 60 * 1000, // stale failed paths are forgotten after a day
|
|
915
|
+
removeWorktreeFailureMaxEntries: 1000, // bound failed-worktree retry suppression cache
|
|
888
916
|
ccMaxTurns: 50, // max tool-use turns for CC/doc-chat before CLI stops
|
|
889
917
|
ccSessionTtlMs: 7 * 24 * 60 * 60 * 1000, // 7d — keep chats resumable after breaks, still bounded by turn cap
|
|
890
918
|
docSessionTtlMs: 7 * 24 * 60 * 60 * 1000, // 7d — longer-lived doc sessions, still bounded
|
|
919
|
+
docSessionMaxEntries: 200, // cap doc-chat session map/disk store by least-recent activity
|
|
920
|
+
ccLiveStreamMaxAgeMs: 30 * 60 * 1000, // hard cap reconnect buffers if abort/cleanup stalls
|
|
891
921
|
maxLlmRawBytes: 256 * 1024, // keep only a bounded stdout tail from direct Claude calls
|
|
892
922
|
maxLlmStderrBytes: 64 * 1024, // keep only a bounded stderr tail from direct Claude calls
|
|
893
923
|
maxLlmLineBufferBytes: 128 * 1024, // cap the incremental JSON line buffer to avoid malformed-stream OOMs
|
|
@@ -2485,6 +2515,13 @@ function mutatePullRequests(filePath, mutator) {
|
|
|
2485
2515
|
* 2. cmd /c rd /s /q as final fallback handles any remaining reserved names
|
|
2486
2516
|
*/
|
|
2487
2517
|
const _removeWorktreeFailures = new Map(); // path → { count, lastAttempt }
|
|
2518
|
+
function _pruneRemoveWorktreeFailures() {
|
|
2519
|
+
_pruneTimedMap(_removeWorktreeFailures, {
|
|
2520
|
+
maxEntries: ENGINE_DEFAULTS.removeWorktreeFailureMaxEntries,
|
|
2521
|
+
ttlMs: ENGINE_DEFAULTS.removeWorktreeFailureTtlMs,
|
|
2522
|
+
getTs: entry => entry?.lastAttempt,
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2488
2525
|
|
|
2489
2526
|
// Windows reserved device names that cannot be deleted via normal paths
|
|
2490
2527
|
const _WIN_RESERVED_NAMES = new Set([
|
|
@@ -2583,6 +2620,7 @@ function removeWorktree(wtPath, gitRoot, worktreeRoot) {
|
|
|
2583
2620
|
log('warn', `removeWorktree: refusing to remove ${wtPath} — not under ${worktreeRoot}`);
|
|
2584
2621
|
return false;
|
|
2585
2622
|
}
|
|
2623
|
+
_pruneRemoveWorktreeFailures();
|
|
2586
2624
|
// Skip paths that failed 3+ times — retry after 1 hour cooldown
|
|
2587
2625
|
const prior = _removeWorktreeFailures.get(resolved);
|
|
2588
2626
|
if (prior && prior.count >= 3 && Date.now() - prior.lastAttempt < 3600000) return false;
|
|
@@ -2619,6 +2657,7 @@ function removeWorktree(wtPath, gitRoot, worktreeRoot) {
|
|
|
2619
2657
|
fail.count++;
|
|
2620
2658
|
fail.lastAttempt = Date.now();
|
|
2621
2659
|
_removeWorktreeFailures.set(resolved, fail);
|
|
2660
|
+
_pruneRemoveWorktreeFailures();
|
|
2622
2661
|
if (fail.count <= 3) log('warn', `removeWorktree: failed for ${wtPath} (attempt ${fail.count}/3): ${rmErr.message}`);
|
|
2623
2662
|
return false;
|
|
2624
2663
|
}
|
package/engine.js
CHANGED
|
@@ -154,6 +154,17 @@ const realActivityMap = new Map(); // dispatchId → timestamp of last agent std
|
|
|
154
154
|
let engineRestartGraceUntil = 0; // timestamp — suppress orphan detection until this time
|
|
155
155
|
const engineRestartGraceExempt = new Set(); // dispatch IDs with confirmed-dead PIDs at restart — bypass grace period
|
|
156
156
|
|
|
157
|
+
function cleanupTempAgent(agentId) {
|
|
158
|
+
if (!tempAgents.has(agentId)) return;
|
|
159
|
+
tempAgents.delete(agentId);
|
|
160
|
+
try {
|
|
161
|
+
const agentDir = path.join(AGENTS_DIR, agentId);
|
|
162
|
+
// Keep output archive but remove temp agent directory (live-output.log etc.)
|
|
163
|
+
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
164
|
+
log('info', `Temp agent ${agentId} cleaned up`);
|
|
165
|
+
} catch { /* cleanup */ }
|
|
166
|
+
}
|
|
167
|
+
|
|
157
168
|
// Per-tick cache of refs that failed to fetch — avoids repeating 30s ETIMEDOUT for same missing ref
|
|
158
169
|
// Cleared at the start of each tick cycle (see tickInner)
|
|
159
170
|
const _failedRefCache = new Set();
|
|
@@ -692,6 +703,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
692
703
|
log('error', `Failed to create worktree for ${branchName}: ${err.message}${err.stderr ? '\n' + err.stderr.toString().slice(0, 500) : ''}`);
|
|
693
704
|
_cleanupPromptFiles();
|
|
694
705
|
completeDispatch(id, DISPATCH_RESULT.ERROR, 'Worktree creation failed: ' + (err.message || '').slice(0, 200));
|
|
706
|
+
cleanupTempAgent(agentId);
|
|
695
707
|
return null;
|
|
696
708
|
}
|
|
697
709
|
}
|
|
@@ -945,6 +957,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
945
957
|
});
|
|
946
958
|
} catch (e) { log('warn', `Failed to auto-queue conflict-fix: ${e.message}`); }
|
|
947
959
|
}
|
|
960
|
+
cleanupTempAgent(agentId);
|
|
948
961
|
return;
|
|
949
962
|
}
|
|
950
963
|
} catch (e) {
|
|
@@ -1091,6 +1104,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1091
1104
|
// orphan detector's "logSize > stub-only" check can tell this apart from a
|
|
1092
1105
|
// hung process. Then rethrow so the dispatch loop handles it normally.
|
|
1093
1106
|
try { fs.appendFileSync(liveOutputPath, `[${new Date().toISOString()}] spawn-failed: ${spawnErr.message}\n[process-exit] spawn-failed\n`); } catch { /* cleanup-only best effort */ }
|
|
1107
|
+
cleanupTempAgent(agentId);
|
|
1094
1108
|
throw spawnErr;
|
|
1095
1109
|
}
|
|
1096
1110
|
|
|
@@ -1222,6 +1236,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1222
1236
|
try { fs.appendFileSync(liveOutputPath, `\n[steering-failed] No session to resume. Message was: ${steerMsg}\n`); } catch {}
|
|
1223
1237
|
activeProcesses.delete(id);
|
|
1224
1238
|
completeDispatch(id, DISPATCH_RESULT.SUCCESS, 'Steering skipped (no session)', '', { processWorkItemFailure: false });
|
|
1239
|
+
cleanupTempAgent(agentId);
|
|
1225
1240
|
return;
|
|
1226
1241
|
}
|
|
1227
1242
|
|
|
@@ -1252,6 +1267,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1252
1267
|
try { fs.appendFileSync(liveOutputPath, `\n[steering-failed] Could not write prompt. Message was: ${steerMsg}\n`); } catch {}
|
|
1253
1268
|
activeProcesses.delete(id);
|
|
1254
1269
|
completeDispatch(id, DISPATCH_RESULT.SUCCESS, 'Steering prompt write failed', '', { processWorkItemFailure: false });
|
|
1270
|
+
cleanupTempAgent(agentId);
|
|
1255
1271
|
return;
|
|
1256
1272
|
}
|
|
1257
1273
|
|
|
@@ -1274,6 +1290,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1274
1290
|
try { fs.unlinkSync(steerPromptPath); } catch {}
|
|
1275
1291
|
activeProcesses.delete(id);
|
|
1276
1292
|
completeDispatch(id, DISPATCH_RESULT.SUCCESS, 'Steering not supported by runtime', '', { processWorkItemFailure: false });
|
|
1293
|
+
cleanupTempAgent(agentId);
|
|
1277
1294
|
return;
|
|
1278
1295
|
}
|
|
1279
1296
|
|
|
@@ -1302,6 +1319,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1302
1319
|
try { fs.unlinkSync(steerPromptPath); } catch {}
|
|
1303
1320
|
activeProcesses.delete(id);
|
|
1304
1321
|
completeDispatch(id, DISPATCH_RESULT.SUCCESS, 'Steering spawn failed', '', { processWorkItemFailure: false });
|
|
1322
|
+
cleanupTempAgent(agentId);
|
|
1305
1323
|
return;
|
|
1306
1324
|
}
|
|
1307
1325
|
|
|
@@ -1420,6 +1438,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1420
1438
|
try { fs.unlinkSync(sysPromptPath); } catch { /* cleanup */ }
|
|
1421
1439
|
try { fs.unlinkSync(promptPath); } catch { /* cleanup */ }
|
|
1422
1440
|
try { fs.unlinkSync(promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid')); } catch { /* cleanup */ }
|
|
1441
|
+
cleanupTempAgent(agentId);
|
|
1423
1442
|
return;
|
|
1424
1443
|
}
|
|
1425
1444
|
|
|
@@ -1443,6 +1462,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1443
1462
|
try { fs.unlinkSync(sysPromptPath); } catch { /* cleanup */ }
|
|
1444
1463
|
try { fs.unlinkSync(promptPath); } catch { /* cleanup */ }
|
|
1445
1464
|
try { fs.unlinkSync(promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid')); } catch { /* cleanup */ }
|
|
1465
|
+
cleanupTempAgent(agentId);
|
|
1446
1466
|
return;
|
|
1447
1467
|
}
|
|
1448
1468
|
|
|
@@ -1539,16 +1559,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1539
1559
|
} catch (err) { log('warn', `Artifact tracking: ${err.message}`); }
|
|
1540
1560
|
}
|
|
1541
1561
|
|
|
1542
|
-
|
|
1543
|
-
if (tempAgents.has(agentId)) {
|
|
1544
|
-
tempAgents.delete(agentId);
|
|
1545
|
-
try {
|
|
1546
|
-
const agentDir = path.join(AGENTS_DIR, agentId);
|
|
1547
|
-
// Keep output archive but remove temp agent directory (live-output.log etc.)
|
|
1548
|
-
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
1549
|
-
log('info', `Temp agent ${agentId} cleaned up`);
|
|
1550
|
-
} catch { /* cleanup */ }
|
|
1551
|
-
}
|
|
1562
|
+
cleanupTempAgent(agentId);
|
|
1552
1563
|
}
|
|
1553
1564
|
|
|
1554
1565
|
proc.on('close', onAgentClose);
|
|
@@ -1558,6 +1569,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1558
1569
|
activeProcesses.delete(id);
|
|
1559
1570
|
realActivityMap.delete(id);
|
|
1560
1571
|
completeDispatch(id, DISPATCH_RESULT.ERROR, `Spawn error: ${err.message}`);
|
|
1572
|
+
cleanupTempAgent(agentId);
|
|
1561
1573
|
});
|
|
1562
1574
|
|
|
1563
1575
|
// Safety: if process exits immediately (within 3s), log it
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1733",
|
|
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"
|