@yemi33/minions 0.1.1039 → 0.1.1040
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 +5 -0
- package/dashboard/js/command-center.js +62 -15
- package/dashboard.js +36 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -54,6 +54,26 @@ function _ccActiveTab() {
|
|
|
54
54
|
return _ccTabs.find(function(t) { return t.id === _ccActiveTabId; }) || null;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
async function _ccDashboardHealth() {
|
|
58
|
+
var controller = new AbortController();
|
|
59
|
+
var timer = setTimeout(function() { controller.abort(); }, 3000);
|
|
60
|
+
try {
|
|
61
|
+
var res = await fetch('/api/status', { signal: controller.signal, cache: 'no-store' });
|
|
62
|
+
if (!(res && res.ok)) return { reachable: false, restarted: false };
|
|
63
|
+
var data = await res.json().catch(function() { return null; });
|
|
64
|
+
var currentDashId = data && data.version ? data.version.dashboardStartedAt : null;
|
|
65
|
+
var knownDashId = window._lastStatus && window._lastStatus.version ? window._lastStatus.version.dashboardStartedAt : null;
|
|
66
|
+
return {
|
|
67
|
+
reachable: true,
|
|
68
|
+
restarted: !!(currentDashId && knownDashId && currentDashId !== knownDashId)
|
|
69
|
+
};
|
|
70
|
+
} catch {
|
|
71
|
+
return { reachable: false, restarted: false };
|
|
72
|
+
} finally {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
57
77
|
function _ccFindPinTarget(query) {
|
|
58
78
|
for (var i = 0; i < (inboxData || []).length; i++) {
|
|
59
79
|
if (inboxData[i].name.toLowerCase().includes(query)) {
|
|
@@ -532,6 +552,16 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
532
552
|
if (activeTab._queue && activeTab._queue.length > 0) _renderQueueIndicator();
|
|
533
553
|
if (msgs.scrollHeight - msgs.scrollTop - msgs.clientHeight < 150) msgs.scrollTop = msgs.scrollHeight;
|
|
534
554
|
}
|
|
555
|
+
function _ccElapsedFooter(label) {
|
|
556
|
+
var seconds = Math.round((Date.now() - ccStartTime) / 1000);
|
|
557
|
+
return '<div style="font-size:9px;color:var(--muted);margin-top:6px;display:flex;justify-content:flex-end;padding-right:30px">' + label.replace('{seconds}', seconds) + '</div>';
|
|
558
|
+
}
|
|
559
|
+
function _ccRetryControls(extraHtml, showReload) {
|
|
560
|
+
return (extraHtml || '') +
|
|
561
|
+
'<button onclick="ccRetryLast()" style="margin-top:6px;padding:4px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--blue);cursor:pointer;font-size:11px">Retry</button>' +
|
|
562
|
+
(showReload ? ' <button onclick="location.reload()" style="margin-top:6px;padding:4px 12px;background:var(--orange);color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px">Reload Page</button>' : '') +
|
|
563
|
+
' <button onclick="ccNewTab()" style="margin-top:6px;padding:4px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--muted);cursor:pointer;font-size:11px">New Session</button>';
|
|
564
|
+
}
|
|
535
565
|
// Start phase timer immediately so thinking text updates while waiting for SSE
|
|
536
566
|
var phaseTimer = setInterval(updateStreamDiv, 1000);
|
|
537
567
|
updateStreamDiv(); // render proper layout immediately (not raw "Thinking..." text)
|
|
@@ -563,6 +593,7 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
563
593
|
var reader = res.body.getReader();
|
|
564
594
|
var decoder = new TextDecoder();
|
|
565
595
|
var buf = '';
|
|
596
|
+
var terminalEventSeen = false;
|
|
566
597
|
|
|
567
598
|
while (true) {
|
|
568
599
|
var readResult = await reader.read();
|
|
@@ -579,21 +610,23 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
579
610
|
streamedText = evt.text;
|
|
580
611
|
if (activeTab) activeTab._streamedText = streamedText;
|
|
581
612
|
updateStreamDiv();
|
|
613
|
+
} else if (evt.type === 'heartbeat') {
|
|
614
|
+
continue;
|
|
582
615
|
} else if (evt.type === 'tool') {
|
|
583
616
|
toolsUsed.push({ name: evt.name, input: evt.input || {} });
|
|
584
617
|
if (activeTab) activeTab._toolsUsed = toolsUsed.slice();
|
|
585
618
|
updateStreamDiv();
|
|
586
619
|
if (msgs.scrollHeight - msgs.scrollTop - msgs.clientHeight < 150) msgs.scrollTop = msgs.scrollHeight;
|
|
587
620
|
} else if (evt.type === 'done') {
|
|
621
|
+
terminalEventSeen = true;
|
|
588
622
|
_cleanupStreamDiv();
|
|
589
623
|
// If system prompt changed, show a notice before the response
|
|
590
624
|
if (evt.sessionReset) {
|
|
591
625
|
addMsg('system', '<div style="text-align:center;padding:6px 12px;font-size:11px;color:var(--muted);background:var(--surface2);border-radius:6px;margin:4px 0">Minions was updated — started a fresh session with latest context.</div>', false, activeTabId);
|
|
592
626
|
}
|
|
593
627
|
// placeholder was added with skipSave=true — nothing to pop
|
|
594
|
-
var ccElapsed = Math.round((Date.now() - ccStartTime) / 1000);
|
|
595
628
|
var rendered = renderMd(evt.text || streamedText || '');
|
|
596
|
-
addMsg('assistant', rendered +
|
|
629
|
+
addMsg('assistant', rendered + _ccElapsedFooter('{seconds}s'));
|
|
597
630
|
if (evt.sessionId !== undefined) {
|
|
598
631
|
// Save session to the originating tab, not whichever tab is active now
|
|
599
632
|
var originTab = _ccTabs.find(function(t) { return t.id === activeTabId; });
|
|
@@ -605,6 +638,7 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
605
638
|
for (var ai = 0; ai < evt.actions.length; ai++) { await ccExecuteAction(evt.actions[ai], activeTabId); }
|
|
606
639
|
}
|
|
607
640
|
} else if (evt.type === 'error') {
|
|
641
|
+
terminalEventSeen = true;
|
|
608
642
|
_cleanupStreamDiv();
|
|
609
643
|
// placeholder was skipSave — no pop needed
|
|
610
644
|
addMsg('assistant', '<span style="color:var(--red)">' + escHtml(evt.error) + '</span>');
|
|
@@ -621,11 +655,11 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
621
655
|
try {
|
|
622
656
|
var revt = JSON.parse(rline.slice(6));
|
|
623
657
|
if (revt.type === 'done') {
|
|
658
|
+
terminalEventSeen = true;
|
|
624
659
|
_cleanupStreamDiv();
|
|
625
660
|
// placeholder was skipSave — no pop needed
|
|
626
|
-
var ccElapsed2 = Math.round((Date.now() - ccStartTime) / 1000);
|
|
627
661
|
var rendered2 = renderMd(revt.text || streamedText || '');
|
|
628
|
-
addMsg('assistant', rendered2 +
|
|
662
|
+
addMsg('assistant', rendered2 + _ccElapsedFooter('{seconds}s'));
|
|
629
663
|
if (revt.sessionId !== undefined) {
|
|
630
664
|
var originTab2 = _ccTabs.find(function(t) { return t.id === activeTabId; });
|
|
631
665
|
if (originTab2) { originTab2.sessionId = revt.sessionId || null; }
|
|
@@ -635,6 +669,12 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
635
669
|
_tagServerExecuted(revt.actions, revt.actionResults);
|
|
636
670
|
for (var ai2 = 0; ai2 < revt.actions.length; ai2++) { await ccExecuteAction(revt.actions[ai2], activeTabId); }
|
|
637
671
|
}
|
|
672
|
+
} else if (revt.type === 'heartbeat') {
|
|
673
|
+
continue;
|
|
674
|
+
} else if (revt.type === 'error') {
|
|
675
|
+
terminalEventSeen = true;
|
|
676
|
+
_cleanupStreamDiv();
|
|
677
|
+
addMsg('assistant', '<span style="color:var(--red)">' + escHtml(revt.error) + '</span>');
|
|
638
678
|
} else if (revt.type === 'chunk') {
|
|
639
679
|
streamedText = revt.text;
|
|
640
680
|
updateStreamDiv();
|
|
@@ -643,11 +683,13 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
643
683
|
}
|
|
644
684
|
}
|
|
645
685
|
// If stream ended without a 'done' event, finalize with whatever we have
|
|
646
|
-
if (streamDiv.parentNode || document.getElementById('cc-restore-thinking') || document.querySelector('[data-stream-tab="' + activeTabId + '"]')) {
|
|
686
|
+
if (!terminalEventSeen && (streamDiv.parentNode || document.getElementById('cc-restore-thinking') || document.querySelector('[data-stream-tab="' + activeTabId + '"]'))) {
|
|
647
687
|
_cleanupStreamDiv();
|
|
688
|
+
var streamEndedHint = '<div style="font-size:10px;color:var(--muted);margin-top:4px">The response stream ended before completion. Retry to continue from the last user message.</div>';
|
|
648
689
|
if (streamedText) {
|
|
649
|
-
|
|
650
|
-
|
|
690
|
+
addMsg('assistant', renderMd(streamedText) + _ccElapsedFooter('Stream interrupted after {seconds}s') + _ccRetryControls(streamEndedHint, false));
|
|
691
|
+
} else {
|
|
692
|
+
addMsg('assistant', '<span style="color:var(--red)">The response stream ended before completion.</span>' + _ccRetryControls(streamEndedHint, false));
|
|
651
693
|
}
|
|
652
694
|
}
|
|
653
695
|
} catch (e) {
|
|
@@ -655,19 +697,24 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
655
697
|
if (e.name === 'AbortError') {
|
|
656
698
|
_wasAborted = true;
|
|
657
699
|
if (streamedText) {
|
|
658
|
-
|
|
659
|
-
addMsg('assistant', renderMd(streamedText) + '<div style="font-size:9px;color:var(--muted);margin-top:6px;display:flex;justify-content:flex-end;padding-right:30px">Stopped after ' + ccElapsed4 + 's</div>');
|
|
700
|
+
addMsg('assistant', renderMd(streamedText) + _ccElapsedFooter('Stopped after {seconds}s'));
|
|
660
701
|
} else {
|
|
661
702
|
addMsg('assistant', '<span style="color:var(--red);font-size:11px">Stopped</span>');
|
|
662
703
|
}
|
|
663
704
|
} else {
|
|
664
|
-
var retryId = 'cc-retry-' + Date.now();
|
|
665
705
|
var isNetworkError = e.message === 'Failed to fetch' || e.message.includes('NetworkError');
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
706
|
+
var dashboardHealth = isNetworkError ? await _ccDashboardHealth() : { reachable: false, restarted: false };
|
|
707
|
+
var connectionHint = '';
|
|
708
|
+
if (isNetworkError) {
|
|
709
|
+
connectionHint = dashboardHealth.restarted
|
|
710
|
+
? '<div style="font-size:10px;color:var(--muted);margin-top:4px">Dashboard restarted while this response was streaming. Reload the page to reconnect to the new instance.</div>'
|
|
711
|
+
: dashboardHealth.reachable
|
|
712
|
+
? '<div style="font-size:10px;color:var(--muted);margin-top:4px">The request stream was interrupted, but the dashboard is still reachable. Retry or start a new session.</div>'
|
|
713
|
+
: '<div style="font-size:10px;color:var(--muted);margin-top:4px">Dashboard connection lost. Reload the page to reconnect.</div>';
|
|
714
|
+
}
|
|
715
|
+
addMsg('assistant', (streamedText ? renderMd(streamedText) + _ccElapsedFooter('Stream interrupted after {seconds}s') : '') +
|
|
716
|
+
'<span style="color:var(--red)">Error: ' + escHtml(e.message) + '</span>' +
|
|
717
|
+
_ccRetryControls(connectionHint, isNetworkError && (!dashboardHealth.reachable || dashboardHealth.restarted)));
|
|
671
718
|
}
|
|
672
719
|
} finally {
|
|
673
720
|
if (activeTab) { activeTab._sending = false; activeTab._abortController = null; activeTab._429retries = 0; delete activeTab._streamedText; delete activeTab._toolsUsed; delete activeTab._sendStartedAt; }
|
package/dashboard.js
CHANGED
|
@@ -541,6 +541,7 @@ const ccInFlightTabs = new Map(); // tabId → timestamp — per-tab in-flight t
|
|
|
541
541
|
const ccInFlightAborts = new Map(); // tabId → abortFn — lets a new request kill the stale LLM
|
|
542
542
|
const CC_INFLIGHT_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes — auto-release if request hangs
|
|
543
543
|
const CC_LOCK_WAIT_MS = 200; // grace period for previous handler's finally to release lock
|
|
544
|
+
const CC_STREAM_HEARTBEAT_MS = 15000; // keep streaming responses alive across proxies/restart races
|
|
544
545
|
function _releaseCCTab(tabId) { ccInFlightTabs.delete(tabId); ccInFlightAborts.delete(tabId); }
|
|
545
546
|
function _ccTabIsInFlight(tabId) {
|
|
546
547
|
if (!ccInFlightTabs.has(tabId)) return false;
|
|
@@ -3791,6 +3792,23 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3791
3792
|
async function handleCommandCenterStream(req, res) {
|
|
3792
3793
|
if (checkRateLimit('command-center', 10)) { res.statusCode = 429; res.end('Rate limited'); return; }
|
|
3793
3794
|
let tabId;
|
|
3795
|
+
let _ccStreamAbort = null;
|
|
3796
|
+
let _ccStreamEnded = false;
|
|
3797
|
+
let _ccHeartbeatTimer = null;
|
|
3798
|
+
const writeCcEvent = (payload) => {
|
|
3799
|
+
try {
|
|
3800
|
+
res.write('data: ' + JSON.stringify(payload) + '\n\n');
|
|
3801
|
+
return true;
|
|
3802
|
+
} catch {
|
|
3803
|
+
return false;
|
|
3804
|
+
}
|
|
3805
|
+
};
|
|
3806
|
+
const stopCcHeartbeat = () => {
|
|
3807
|
+
if (_ccHeartbeatTimer) {
|
|
3808
|
+
clearInterval(_ccHeartbeatTimer);
|
|
3809
|
+
_ccHeartbeatTimer = null;
|
|
3810
|
+
}
|
|
3811
|
+
};
|
|
3794
3812
|
try {
|
|
3795
3813
|
const body = await readBody(req);
|
|
3796
3814
|
if (!body.message) { res.statusCode = 400; res.end('message required'); return; }
|
|
@@ -3807,13 +3825,20 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3807
3825
|
ccInFlightTabs.set(tabId, Date.now());
|
|
3808
3826
|
|
|
3809
3827
|
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
|
3810
|
-
|
|
3811
|
-
|
|
3828
|
+
writeCcEvent({ type: 'heartbeat' }); // flush headers quickly and keep intermediaries from idling out
|
|
3829
|
+
_ccHeartbeatTimer = setInterval(() => {
|
|
3830
|
+
if (_ccStreamEnded) {
|
|
3831
|
+
stopCcHeartbeat();
|
|
3832
|
+
return;
|
|
3833
|
+
}
|
|
3834
|
+
if (!writeCcEvent({ type: 'heartbeat' })) stopCcHeartbeat();
|
|
3835
|
+
}, CC_STREAM_HEARTBEAT_MS);
|
|
3812
3836
|
// Kill LLM process immediately if client disconnects mid-stream.
|
|
3813
3837
|
// Guard with !_ccStreamEnded: when the stream ends normally, finally already released the lock;
|
|
3814
3838
|
// without the guard, this close event (which fires after res.end) could wipe a new request's lock.
|
|
3815
3839
|
req.on('close', () => {
|
|
3816
3840
|
if (!_ccStreamEnded) {
|
|
3841
|
+
stopCcHeartbeat();
|
|
3817
3842
|
_releaseCCTab(tabId);
|
|
3818
3843
|
if (_ccStreamAbort) {
|
|
3819
3844
|
console.log(`[CC-stream] Client disconnected for tab ${tabId} — aborting LLM`);
|
|
@@ -3851,10 +3876,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3851
3876
|
onChunk: (text) => {
|
|
3852
3877
|
const actIdx = findCCActionsDelimiter(text);
|
|
3853
3878
|
const display = actIdx >= 0 ? text.slice(0, actIdx).trim() : text;
|
|
3854
|
-
|
|
3879
|
+
writeCcEvent({ type: 'chunk', text: display });
|
|
3855
3880
|
},
|
|
3856
3881
|
onToolUse: (name, input) => {
|
|
3857
|
-
|
|
3882
|
+
writeCcEvent({ type: 'tool', name, input: _lightToolInput(input) });
|
|
3858
3883
|
}
|
|
3859
3884
|
});
|
|
3860
3885
|
_ccStreamAbort = llmPromise.abort;
|
|
@@ -3875,10 +3900,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3875
3900
|
onChunk: (text) => {
|
|
3876
3901
|
const actIdx = findCCActionsDelimiter(text);
|
|
3877
3902
|
const display = actIdx >= 0 ? text.slice(0, actIdx).trim() : text;
|
|
3878
|
-
|
|
3903
|
+
writeCcEvent({ type: 'chunk', text: display });
|
|
3879
3904
|
},
|
|
3880
3905
|
onToolUse: (name, input) => {
|
|
3881
|
-
|
|
3906
|
+
writeCcEvent({ type: 'tool', name, input: _lightToolInput(input) });
|
|
3882
3907
|
}
|
|
3883
3908
|
});
|
|
3884
3909
|
_ccStreamAbort = retryPromise.abort;
|
|
@@ -3896,7 +3921,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3896
3921
|
const stderrTail = (result.stderr || '').trim().split('\n').filter(Boolean).slice(-3).join(' | ');
|
|
3897
3922
|
console.error(`[CC-stream] Failed: code=${result.code}, stderr=${(result.stderr || '').slice(0, 500)}, stdout_tail=${(result.raw || '').slice(-500)}`);
|
|
3898
3923
|
const retryHint = 'Send your message again to retry.';
|
|
3899
|
-
|
|
3924
|
+
writeCcEvent({ type: 'done', text: `I had trouble processing that ${debugInfo}. ${stderrTail ? 'Detail: ' + stderrTail : ''}\n\n${retryHint}`, actions: [], sessionId: null });
|
|
3900
3925
|
_ccStreamEnded = true; res.end();
|
|
3901
3926
|
return;
|
|
3902
3927
|
}
|
|
@@ -3939,7 +3964,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3939
3964
|
}
|
|
3940
3965
|
const donePayload = { type: 'done', text: displayText, actions, actionResults, sessionId: responseSessionId, newSession: !wasResume };
|
|
3941
3966
|
if (sessionReset) donePayload.sessionReset = true;
|
|
3942
|
-
|
|
3967
|
+
writeCcEvent(donePayload);
|
|
3943
3968
|
|
|
3944
3969
|
// Mirror CC response to Teams (non-blocking, skip Teams-originated)
|
|
3945
3970
|
const _streamTabId = body.tabId || 'default';
|
|
@@ -3949,11 +3974,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3949
3974
|
|
|
3950
3975
|
_ccStreamEnded = true; res.end();
|
|
3951
3976
|
} finally {
|
|
3977
|
+
stopCcHeartbeat();
|
|
3952
3978
|
_releaseCCTab(tabId);
|
|
3953
3979
|
}
|
|
3954
3980
|
} catch (e) {
|
|
3981
|
+
stopCcHeartbeat();
|
|
3955
3982
|
_releaseCCTab(tabId);
|
|
3956
|
-
|
|
3983
|
+
writeCcEvent({ type: 'error', error: e.message });
|
|
3957
3984
|
_ccStreamEnded = true; try { res.end(); } catch {}
|
|
3958
3985
|
}
|
|
3959
3986
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1040",
|
|
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"
|