@yemi33/minions 0.1.1578 → 0.1.1579
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 +4 -1
- package/dashboard/js/command-center.js +104 -88
- package/dashboard.js +183 -30
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.1579 (2026-04-28)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
6
|
- hash-dedup, compress+normalize pass, dynamic stale-guard, rich result
|
|
7
7
|
|
|
8
|
+
### Other
|
|
9
|
+
- Keep CC streams reconnectable
|
|
10
|
+
|
|
8
11
|
## 0.1.1577 (2026-04-27)
|
|
9
12
|
|
|
10
13
|
### Fixes
|
|
@@ -94,6 +94,13 @@ function _ccFindPinTarget(query) {
|
|
|
94
94
|
function ccAbort() {
|
|
95
95
|
var tab = _ccActiveTab();
|
|
96
96
|
if (tab && tab._abortController) {
|
|
97
|
+
try {
|
|
98
|
+
fetch('/api/command-center/abort', {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: { 'Content-Type': 'application/json' },
|
|
101
|
+
body: JSON.stringify({ tabId: tab.id })
|
|
102
|
+
}).catch(function() {});
|
|
103
|
+
} catch {}
|
|
97
104
|
tab._abortController.abort();
|
|
98
105
|
tab._abortController = null;
|
|
99
106
|
}
|
|
@@ -211,6 +218,13 @@ function ccCloseTab(id) {
|
|
|
211
218
|
var closingTab = _ccTabs.find(function(t) { return t.id === id; });
|
|
212
219
|
if (closingTab && closingTab._sending) {
|
|
213
220
|
if (!confirm('This tab has an active request. Close anyway?')) return;
|
|
221
|
+
try {
|
|
222
|
+
fetch('/api/command-center/abort', {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
headers: { 'Content-Type': 'application/json' },
|
|
225
|
+
body: JSON.stringify({ tabId: id })
|
|
226
|
+
}).catch(function() {});
|
|
227
|
+
} catch {}
|
|
214
228
|
if (closingTab._abortController) { closingTab._abortController.abort(); closingTab._abortController = null; }
|
|
215
229
|
closingTab._sending = false;
|
|
216
230
|
closingTab._queue = [];
|
|
@@ -445,6 +459,8 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
445
459
|
if (existingQueueEl) existingQueueEl.remove();
|
|
446
460
|
|
|
447
461
|
var ccStartTime = Date.now();
|
|
462
|
+
var reconnectAttempts = 0;
|
|
463
|
+
var streamStatusNote = '';
|
|
448
464
|
var phases = [
|
|
449
465
|
[0, 'Thinking...'],
|
|
450
466
|
[3000, 'Reading minions context...'],
|
|
@@ -511,6 +527,9 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
511
527
|
if (streamedText) {
|
|
512
528
|
html += renderMd(streamedText);
|
|
513
529
|
}
|
|
530
|
+
if (streamStatusNote) {
|
|
531
|
+
html += '<div style="margin-top:6px;font-size:10px;color:var(--muted)">' + escHtml(streamStatusNote) + '</div>';
|
|
532
|
+
}
|
|
514
533
|
html += '<div style="margin-top:' + (streamedText ? '6px' : '0') + '">' + _getThinkingHtml() + '</div>';
|
|
515
534
|
streamDiv.innerHTML = html;
|
|
516
535
|
// Re-append queue indicators so they stay below the streaming content
|
|
@@ -531,35 +550,70 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
531
550
|
var phaseTimer = setInterval(updateStreamDiv, 1000);
|
|
532
551
|
updateStreamDiv(); // render proper layout immediately (not raw "Thinking..." text)
|
|
533
552
|
|
|
534
|
-
|
|
535
|
-
// Stream response via SSE — shows text as it arrives
|
|
553
|
+
async function _ccConsumeStream(requestBody, isReconnect) {
|
|
536
554
|
var res = await fetch('/api/command-center/stream', {
|
|
537
555
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
538
|
-
body: JSON.stringify(
|
|
556
|
+
body: JSON.stringify(requestBody),
|
|
539
557
|
signal: activeTab._abortController ? activeTab._abortController.signal : AbortSignal.timeout(960000)
|
|
540
558
|
});
|
|
541
559
|
|
|
542
560
|
if (!res.ok) {
|
|
543
|
-
|
|
544
|
-
if (res.status === 429 && (!activeTab._429retries || activeTab._429retries < 3)) {
|
|
561
|
+
if (!isReconnect && res.status === 429 && (!activeTab._429retries || activeTab._429retries < 3)) {
|
|
545
562
|
activeTab._429retries = (activeTab._429retries || 0) + 1;
|
|
546
|
-
_cleanupStreamDiv(); // remove current thinking div — prevents stacking on each retry
|
|
547
563
|
await new Promise(function(r) { setTimeout(r, 1500); });
|
|
548
|
-
return await
|
|
564
|
+
return await _ccConsumeStream({ message: message, tabId: activeTabId, sessionId: activeTab.sessionId || null }, false);
|
|
549
565
|
}
|
|
550
566
|
activeTab._429retries = 0;
|
|
551
|
-
_cleanupStreamDiv();
|
|
552
567
|
var errText = await res.text();
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
return;
|
|
568
|
+
if (isReconnect && res.status === 409) return { interrupted: true, reconnectable: false, reason: errText || 'No live stream' };
|
|
569
|
+
throw new Error(errText || 'CC error');
|
|
556
570
|
}
|
|
557
571
|
|
|
572
|
+
activeTab._429retries = 0;
|
|
573
|
+
streamStatusNote = '';
|
|
574
|
+
updateStreamDiv();
|
|
575
|
+
|
|
558
576
|
var reader = res.body.getReader();
|
|
559
577
|
var decoder = new TextDecoder();
|
|
560
578
|
var buf = '';
|
|
561
579
|
var terminalEventSeen = false;
|
|
562
580
|
|
|
581
|
+
async function _handleEvent(evt) {
|
|
582
|
+
if (evt.type === 'chunk') {
|
|
583
|
+
streamedText = evt.text;
|
|
584
|
+
if (activeTab) activeTab._streamedText = streamedText;
|
|
585
|
+
updateStreamDiv();
|
|
586
|
+
} else if (evt.type === 'heartbeat') {
|
|
587
|
+
return;
|
|
588
|
+
} else if (evt.type === 'tool') {
|
|
589
|
+
toolsUsed.push({ name: evt.name, input: evt.input || {} });
|
|
590
|
+
if (activeTab) activeTab._toolsUsed = toolsUsed.slice();
|
|
591
|
+
updateStreamDiv();
|
|
592
|
+
if (msgs.scrollHeight - msgs.scrollTop - msgs.clientHeight < 150) msgs.scrollTop = msgs.scrollHeight;
|
|
593
|
+
} else if (evt.type === 'done') {
|
|
594
|
+
terminalEventSeen = true;
|
|
595
|
+
_cleanupStreamDiv();
|
|
596
|
+
if (evt.sessionReset) {
|
|
597
|
+
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);
|
|
598
|
+
}
|
|
599
|
+
var rendered = renderMd(evt.text || streamedText || '');
|
|
600
|
+
addMsg('assistant', rendered + _ccElapsedFooter('{seconds}s'));
|
|
601
|
+
if (evt.sessionId !== undefined) {
|
|
602
|
+
var originTab = _ccTabs.find(function(t) { return t.id === activeTabId; });
|
|
603
|
+
if (originTab) { originTab.sessionId = evt.sessionId || null; }
|
|
604
|
+
ccSaveState(); ccUpdateSessionIndicator();
|
|
605
|
+
}
|
|
606
|
+
if (evt.actions && evt.actions.length > 0) {
|
|
607
|
+
_tagServerExecuted(evt.actions, evt.actionResults);
|
|
608
|
+
for (var ai = 0; ai < evt.actions.length; ai++) { await ccExecuteAction(evt.actions[ai], activeTabId); }
|
|
609
|
+
}
|
|
610
|
+
} else if (evt.type === 'error') {
|
|
611
|
+
terminalEventSeen = true;
|
|
612
|
+
_cleanupStreamDiv();
|
|
613
|
+
addMsg('assistant', '<span style="color:var(--red)">' + escHtml(evt.error) + '</span>');
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
563
617
|
while (true) {
|
|
564
618
|
var readResult = await reader.read();
|
|
565
619
|
if (readResult.done) break;
|
|
@@ -569,93 +623,55 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
569
623
|
for (var li = 0; li < lines.length; li++) {
|
|
570
624
|
var line = lines[li];
|
|
571
625
|
if (!line.startsWith('data: ')) continue;
|
|
572
|
-
try {
|
|
573
|
-
var evt = JSON.parse(line.slice(6));
|
|
574
|
-
if (evt.type === 'chunk') {
|
|
575
|
-
streamedText = evt.text;
|
|
576
|
-
if (activeTab) activeTab._streamedText = streamedText;
|
|
577
|
-
updateStreamDiv();
|
|
578
|
-
} else if (evt.type === 'heartbeat') {
|
|
579
|
-
continue;
|
|
580
|
-
} else if (evt.type === 'tool') {
|
|
581
|
-
toolsUsed.push({ name: evt.name, input: evt.input || {} });
|
|
582
|
-
if (activeTab) activeTab._toolsUsed = toolsUsed.slice();
|
|
583
|
-
updateStreamDiv();
|
|
584
|
-
if (msgs.scrollHeight - msgs.scrollTop - msgs.clientHeight < 150) msgs.scrollTop = msgs.scrollHeight;
|
|
585
|
-
} else if (evt.type === 'done') {
|
|
586
|
-
terminalEventSeen = true;
|
|
587
|
-
_cleanupStreamDiv();
|
|
588
|
-
// If system prompt changed, show a notice before the response
|
|
589
|
-
if (evt.sessionReset) {
|
|
590
|
-
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);
|
|
591
|
-
}
|
|
592
|
-
// placeholder was added with skipSave=true — nothing to pop
|
|
593
|
-
var rendered = renderMd(evt.text || streamedText || '');
|
|
594
|
-
addMsg('assistant', rendered + _ccElapsedFooter('{seconds}s'));
|
|
595
|
-
if (evt.sessionId !== undefined) {
|
|
596
|
-
// Save session to the originating tab, not whichever tab is active now
|
|
597
|
-
var originTab = _ccTabs.find(function(t) { return t.id === activeTabId; });
|
|
598
|
-
if (originTab) { originTab.sessionId = evt.sessionId || null; }
|
|
599
|
-
ccSaveState(); ccUpdateSessionIndicator();
|
|
600
|
-
}
|
|
601
|
-
if (evt.actions && evt.actions.length > 0) {
|
|
602
|
-
_tagServerExecuted(evt.actions, evt.actionResults);
|
|
603
|
-
for (var ai = 0; ai < evt.actions.length; ai++) { await ccExecuteAction(evt.actions[ai], activeTabId); }
|
|
604
|
-
}
|
|
605
|
-
} else if (evt.type === 'error') {
|
|
606
|
-
terminalEventSeen = true;
|
|
607
|
-
_cleanupStreamDiv();
|
|
608
|
-
// placeholder was skipSave — no pop needed
|
|
609
|
-
addMsg('assistant', '<span style="color:var(--red)">' + escHtml(evt.error) + '</span>');
|
|
610
|
-
}
|
|
611
|
-
} catch { /* incomplete JSON */ }
|
|
626
|
+
try { await _handleEvent(JSON.parse(line.slice(6))); } catch {}
|
|
612
627
|
}
|
|
613
628
|
}
|
|
614
|
-
// Process any remaining buffered data after stream ends
|
|
615
629
|
if (buf.trim()) {
|
|
616
630
|
var remainingLines = buf.split('\n');
|
|
617
631
|
for (var ri = 0; ri < remainingLines.length; ri++) {
|
|
618
632
|
var rline = remainingLines[ri];
|
|
619
633
|
if (!rline.startsWith('data: ')) continue;
|
|
620
|
-
try {
|
|
621
|
-
var revt = JSON.parse(rline.slice(6));
|
|
622
|
-
if (revt.type === 'done') {
|
|
623
|
-
terminalEventSeen = true;
|
|
624
|
-
_cleanupStreamDiv();
|
|
625
|
-
// placeholder was skipSave — no pop needed
|
|
626
|
-
var rendered2 = renderMd(revt.text || streamedText || '');
|
|
627
|
-
addMsg('assistant', rendered2 + _ccElapsedFooter('{seconds}s'));
|
|
628
|
-
if (revt.sessionId !== undefined) {
|
|
629
|
-
var originTab2 = _ccTabs.find(function(t) { return t.id === activeTabId; });
|
|
630
|
-
if (originTab2) { originTab2.sessionId = revt.sessionId || null; }
|
|
631
|
-
ccSaveState(); ccUpdateSessionIndicator();
|
|
632
|
-
}
|
|
633
|
-
if (revt.actions && revt.actions.length > 0) {
|
|
634
|
-
_tagServerExecuted(revt.actions, revt.actionResults);
|
|
635
|
-
for (var ai2 = 0; ai2 < revt.actions.length; ai2++) { await ccExecuteAction(revt.actions[ai2], activeTabId); }
|
|
636
|
-
}
|
|
637
|
-
} else if (revt.type === 'heartbeat') {
|
|
638
|
-
continue;
|
|
639
|
-
} else if (revt.type === 'error') {
|
|
640
|
-
terminalEventSeen = true;
|
|
641
|
-
_cleanupStreamDiv();
|
|
642
|
-
addMsg('assistant', '<span style="color:var(--red)">' + escHtml(revt.error) + '</span>');
|
|
643
|
-
} else if (revt.type === 'chunk') {
|
|
644
|
-
streamedText = revt.text;
|
|
645
|
-
updateStreamDiv();
|
|
646
|
-
}
|
|
647
|
-
} catch {}
|
|
634
|
+
try { await _handleEvent(JSON.parse(rline.slice(6))); } catch {}
|
|
648
635
|
}
|
|
649
636
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
637
|
+
return { interrupted: !terminalEventSeen, reconnectable: true };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
while (true) {
|
|
642
|
+
var consume = await _ccConsumeStream(
|
|
643
|
+
reconnectAttempts === 0
|
|
644
|
+
? { message: message, tabId: activeTabId, sessionId: activeTab.sessionId || null }
|
|
645
|
+
: { tabId: activeTabId, sessionId: activeTab.sessionId || null, reconnect: true },
|
|
646
|
+
reconnectAttempts > 0
|
|
647
|
+
);
|
|
648
|
+
if (!consume.interrupted) break;
|
|
649
|
+
if (!consume.reconnectable || reconnectAttempts >= 2) {
|
|
650
|
+
_cleanupStreamDiv();
|
|
651
|
+
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>';
|
|
652
|
+
if (streamedText) {
|
|
653
|
+
addMsg('assistant', renderMd(streamedText) + _ccElapsedFooter('Stream interrupted after {seconds}s') + _ccRetryControls(streamEndedHint, false));
|
|
654
|
+
} else {
|
|
655
|
+
addMsg('assistant', '<span style="color:var(--red)">The response stream ended before completion.</span>' + _ccRetryControls(streamEndedHint, false));
|
|
656
|
+
}
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
var reconnectHealth = await _ccDashboardHealth();
|
|
660
|
+
if (!reconnectHealth.reachable || reconnectHealth.restarted) {
|
|
661
|
+
_cleanupStreamDiv();
|
|
662
|
+
var reconnectHint = reconnectHealth.restarted
|
|
663
|
+
? '<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>'
|
|
664
|
+
: '<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>';
|
|
665
|
+
addMsg('assistant', (streamedText ? renderMd(streamedText) + _ccElapsedFooter('Stream interrupted after {seconds}s') : '') +
|
|
666
|
+
_ccRetryControls(reconnectHint, reconnectHealth.restarted));
|
|
667
|
+
break;
|
|
658
668
|
}
|
|
669
|
+
reconnectAttempts++;
|
|
670
|
+
toolsUsed = [];
|
|
671
|
+
if (activeTab) activeTab._toolsUsed = [];
|
|
672
|
+
streamStatusNote = 'Connection interrupted — reattaching to the live response...';
|
|
673
|
+
updateStreamDiv();
|
|
674
|
+
await new Promise(function(r) { setTimeout(r, 1000 * reconnectAttempts); });
|
|
659
675
|
}
|
|
660
676
|
} catch (e) {
|
|
661
677
|
_cleanupStreamDiv();
|
package/dashboard.js
CHANGED
|
@@ -544,10 +544,86 @@ const CC_SESSION_TTL_MS = shared.ENGINE_DEFAULTS.ccSessionTtlMs;
|
|
|
544
544
|
let ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
|
|
545
545
|
const ccInFlightTabs = new Map(); // tabId → timestamp — per-tab in-flight tracking for parallel CC requests
|
|
546
546
|
const ccInFlightAborts = new Map(); // tabId → abortFn — lets a new request kill the stale LLM
|
|
547
|
+
const ccLiveStreams = new Map(); // tabId → buffered live stream state for reconnect-after-disconnect
|
|
547
548
|
const CC_INFLIGHT_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes — auto-release if request hangs
|
|
548
549
|
const CC_LOCK_WAIT_MS = 200; // grace period for previous handler's finally to release lock
|
|
549
550
|
const CC_STREAM_HEARTBEAT_MS = 15000; // keep streaming responses alive across proxies/restart races
|
|
551
|
+
const CC_STREAM_REATTACH_GRACE_MS = 60000; // keep CC job alive briefly after disconnect so the UI can reattach
|
|
552
|
+
const CC_STREAM_DONE_RETENTION_MS = 30000; // retain final payload briefly so reconnect can still receive it
|
|
550
553
|
function _releaseCCTab(tabId) { ccInFlightTabs.delete(tabId); ccInFlightAborts.delete(tabId); }
|
|
554
|
+
function _getCcLiveStream(tabId) {
|
|
555
|
+
return ccLiveStreams.get(tabId) || null;
|
|
556
|
+
}
|
|
557
|
+
function _clearCcLiveTimers(tabId) {
|
|
558
|
+
const state = _getCcLiveStream(tabId);
|
|
559
|
+
if (!state) return;
|
|
560
|
+
if (state.abortTimer) {
|
|
561
|
+
clearTimeout(state.abortTimer);
|
|
562
|
+
state.abortTimer = null;
|
|
563
|
+
}
|
|
564
|
+
if (state.cleanupTimer) {
|
|
565
|
+
clearTimeout(state.cleanupTimer);
|
|
566
|
+
state.cleanupTimer = null;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function _clearCcLiveStream(tabId) {
|
|
570
|
+
const state = _getCcLiveStream(tabId);
|
|
571
|
+
if (!state) return;
|
|
572
|
+
_clearCcLiveTimers(tabId);
|
|
573
|
+
ccLiveStreams.delete(tabId);
|
|
574
|
+
}
|
|
575
|
+
function _ensureCcLiveStream(tabId) {
|
|
576
|
+
let state = _getCcLiveStream(tabId);
|
|
577
|
+
if (state) return state;
|
|
578
|
+
state = {
|
|
579
|
+
tabId,
|
|
580
|
+
text: '',
|
|
581
|
+
tools: [],
|
|
582
|
+
donePayload: null,
|
|
583
|
+
writer: null,
|
|
584
|
+
endResponse: null,
|
|
585
|
+
abortFn: null,
|
|
586
|
+
abortTimer: null,
|
|
587
|
+
cleanupTimer: null,
|
|
588
|
+
};
|
|
589
|
+
ccLiveStreams.set(tabId, state);
|
|
590
|
+
return state;
|
|
591
|
+
}
|
|
592
|
+
function _attachCcLiveStream(tabId, writer, endResponse) {
|
|
593
|
+
const state = _ensureCcLiveStream(tabId);
|
|
594
|
+
_clearCcLiveTimers(tabId);
|
|
595
|
+
state.writer = writer;
|
|
596
|
+
state.endResponse = endResponse;
|
|
597
|
+
return state;
|
|
598
|
+
}
|
|
599
|
+
function _detachCcLiveStream(tabId, writer) {
|
|
600
|
+
const state = _getCcLiveStream(tabId);
|
|
601
|
+
if (!state) return;
|
|
602
|
+
if (!writer || state.writer === writer) {
|
|
603
|
+
state.writer = null;
|
|
604
|
+
state.endResponse = null;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function _scheduleCcLiveAbort(tabId) {
|
|
608
|
+
const state = _getCcLiveStream(tabId);
|
|
609
|
+
if (!state || state.donePayload) return;
|
|
610
|
+
_clearCcLiveTimers(tabId);
|
|
611
|
+
state.abortTimer = setTimeout(() => {
|
|
612
|
+
const live = _getCcLiveStream(tabId);
|
|
613
|
+
if (!live || live.donePayload || live.writer) return;
|
|
614
|
+
try { if (live.abortFn) live.abortFn(); } catch {}
|
|
615
|
+
}, CC_STREAM_REATTACH_GRACE_MS);
|
|
616
|
+
}
|
|
617
|
+
function _scheduleCcLiveCleanup(tabId, delayMs = CC_STREAM_DONE_RETENTION_MS) {
|
|
618
|
+
const state = _getCcLiveStream(tabId);
|
|
619
|
+
if (!state) return;
|
|
620
|
+
if (state.cleanupTimer) clearTimeout(state.cleanupTimer);
|
|
621
|
+
state.cleanupTimer = setTimeout(() => {
|
|
622
|
+
const live = _getCcLiveStream(tabId);
|
|
623
|
+
if (!live || live.writer) return;
|
|
624
|
+
_clearCcLiveStream(tabId);
|
|
625
|
+
}, delayMs);
|
|
626
|
+
}
|
|
551
627
|
function _ccTabIsInFlight(tabId) {
|
|
552
628
|
if (!ccInFlightTabs.has(tabId)) return false;
|
|
553
629
|
// Auto-release stale locks — if a request has been in-flight longer than CC_INFLIGHT_TIMEOUT_MS,
|
|
@@ -3900,10 +3976,31 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3900
3976
|
async function handleCommandCenterNewSession(req, res) {
|
|
3901
3977
|
ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
|
|
3902
3978
|
ccInFlightTabs.clear(); // Reset all in-flight guards
|
|
3979
|
+
for (const [tabId, live] of ccLiveStreams.entries()) {
|
|
3980
|
+
try { if (live.abortFn) live.abortFn(); } catch {}
|
|
3981
|
+
_clearCcLiveStream(tabId);
|
|
3982
|
+
}
|
|
3903
3983
|
safeWrite(path.join(ENGINE_DIR, 'cc-session.json'), ccSession);
|
|
3904
3984
|
return jsonReply(res, 200, { ok: true });
|
|
3905
3985
|
}
|
|
3906
3986
|
|
|
3987
|
+
async function handleCommandCenterAbort(req, res) {
|
|
3988
|
+
try {
|
|
3989
|
+
const body = await readBody(req);
|
|
3990
|
+
const tabId = body.tabId || 'default';
|
|
3991
|
+
const live = _getCcLiveStream(tabId);
|
|
3992
|
+
if (live?.abortFn) {
|
|
3993
|
+
try { live.abortFn(); } catch {}
|
|
3994
|
+
} else {
|
|
3995
|
+
const abort = ccInFlightAborts.get(tabId);
|
|
3996
|
+
if (abort) { try { abort(); } catch {} }
|
|
3997
|
+
}
|
|
3998
|
+
_clearCcLiveStream(tabId);
|
|
3999
|
+
_releaseCCTab(tabId);
|
|
4000
|
+
return jsonReply(res, 200, { ok: true });
|
|
4001
|
+
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
4002
|
+
}
|
|
4003
|
+
|
|
3907
4004
|
async function handleCCSessionsList(req, res) {
|
|
3908
4005
|
const sessions = _readCcTabSessions();
|
|
3909
4006
|
return jsonReply(res, 200, { sessions });
|
|
@@ -4037,8 +4134,51 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4037
4134
|
};
|
|
4038
4135
|
try {
|
|
4039
4136
|
const body = await readBody(req);
|
|
4040
|
-
if (!body.message) { res.statusCode = 400; res.end('message required'); return; }
|
|
4137
|
+
if (!body.message && !body.reconnect) { res.statusCode = 400; res.end('message required'); return; }
|
|
4041
4138
|
tabId = body.tabId || 'default';
|
|
4139
|
+
if (body.reconnect) {
|
|
4140
|
+
const live = _getCcLiveStream(tabId);
|
|
4141
|
+
if (!live) { res.statusCode = 409; res.end('No live command-center response to reconnect'); return; }
|
|
4142
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
|
4143
|
+
writeCcEvent({ type: 'heartbeat' });
|
|
4144
|
+
_ccHeartbeatTimer = setInterval(() => {
|
|
4145
|
+
if (_ccStreamEnded) {
|
|
4146
|
+
stopCcHeartbeat();
|
|
4147
|
+
return;
|
|
4148
|
+
}
|
|
4149
|
+
if (!writeCcEvent({ type: 'heartbeat' })) stopCcHeartbeat();
|
|
4150
|
+
}, CC_STREAM_HEARTBEAT_MS);
|
|
4151
|
+
let reconnectDone;
|
|
4152
|
+
const reconnectDonePromise = new Promise(resolve => { reconnectDone = resolve; });
|
|
4153
|
+
_attachCcLiveStream(tabId, writeCcEvent, () => {
|
|
4154
|
+
if (_ccStreamEnded) return;
|
|
4155
|
+
_ccStreamEnded = true;
|
|
4156
|
+
stopCcHeartbeat();
|
|
4157
|
+
try { res.end(); } catch {}
|
|
4158
|
+
reconnectDone();
|
|
4159
|
+
});
|
|
4160
|
+
req.on('close', () => {
|
|
4161
|
+
if (_ccStreamEnded) return;
|
|
4162
|
+
stopCcHeartbeat();
|
|
4163
|
+
_detachCcLiveStream(tabId, writeCcEvent);
|
|
4164
|
+
_scheduleCcLiveAbort(tabId);
|
|
4165
|
+
reconnectDone();
|
|
4166
|
+
});
|
|
4167
|
+
for (const tool of live.tools || []) {
|
|
4168
|
+
writeCcEvent({ type: 'tool', name: tool.name, input: _lightToolInput(tool.input) });
|
|
4169
|
+
}
|
|
4170
|
+
if (live.text) writeCcEvent({ type: 'chunk', text: live.text });
|
|
4171
|
+
if (live.donePayload) {
|
|
4172
|
+
writeCcEvent(live.donePayload);
|
|
4173
|
+
_ccStreamEnded = true;
|
|
4174
|
+
stopCcHeartbeat();
|
|
4175
|
+
try { res.end(); } catch {}
|
|
4176
|
+
_scheduleCcLiveCleanup(tabId);
|
|
4177
|
+
return;
|
|
4178
|
+
}
|
|
4179
|
+
await reconnectDonePromise;
|
|
4180
|
+
return;
|
|
4181
|
+
}
|
|
4042
4182
|
if (_ccTabIsInFlight(tabId)) {
|
|
4043
4183
|
// Previous request still in-flight — abort its LLM (handles keep-alive abort where close event didn't fire)
|
|
4044
4184
|
const prevAbort = ccInFlightAborts.get(tabId);
|
|
@@ -4049,6 +4189,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4049
4189
|
}
|
|
4050
4190
|
}
|
|
4051
4191
|
ccInFlightTabs.set(tabId, Date.now());
|
|
4192
|
+
_clearCcLiveStream(tabId);
|
|
4193
|
+
const liveState = _attachCcLiveStream(tabId, writeCcEvent, () => {
|
|
4194
|
+
if (_ccStreamEnded) return;
|
|
4195
|
+
_ccStreamEnded = true;
|
|
4196
|
+
stopCcHeartbeat();
|
|
4197
|
+
try { res.end(); } catch {}
|
|
4198
|
+
});
|
|
4052
4199
|
|
|
4053
4200
|
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
|
4054
4201
|
writeCcEvent({ type: 'heartbeat' }); // flush headers quickly and keep intermediaries from idling out
|
|
@@ -4059,17 +4206,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4059
4206
|
}
|
|
4060
4207
|
if (!writeCcEvent({ type: 'heartbeat' })) stopCcHeartbeat();
|
|
4061
4208
|
}, CC_STREAM_HEARTBEAT_MS);
|
|
4062
|
-
//
|
|
4063
|
-
// Guard with !_ccStreamEnded: when the stream ends normally, finally already released the lock;
|
|
4064
|
-
// without the guard, this close event (which fires after res.end) could wipe a new request's lock.
|
|
4209
|
+
// Keep the LLM alive briefly after disconnect so the UI can reattach to the same in-flight turn.
|
|
4065
4210
|
req.on('close', () => {
|
|
4066
4211
|
if (!_ccStreamEnded) {
|
|
4067
4212
|
stopCcHeartbeat();
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
console.log(`[CC-stream] Client disconnected for tab ${tabId} — aborting LLM`);
|
|
4071
|
-
_ccStreamAbort();
|
|
4072
|
-
}
|
|
4213
|
+
_detachCcLiveStream(tabId, writeCcEvent);
|
|
4214
|
+
_scheduleCcLiveAbort(tabId);
|
|
4073
4215
|
}
|
|
4074
4216
|
});
|
|
4075
4217
|
|
|
@@ -4106,14 +4248,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4106
4248
|
onChunk: (text) => {
|
|
4107
4249
|
const actIdx = findCCActionsDelimiter(text);
|
|
4108
4250
|
const display = actIdx >= 0 ? text.slice(0, actIdx).trim() : text;
|
|
4109
|
-
|
|
4251
|
+
liveState.text = display;
|
|
4252
|
+
if (liveState.writer) liveState.writer({ type: 'chunk', text: display });
|
|
4110
4253
|
},
|
|
4111
4254
|
onToolUse: (name, input) => {
|
|
4112
4255
|
toolUses.push({ name, input: input || {} });
|
|
4113
|
-
|
|
4256
|
+
liveState.tools.push({ name, input: input || {} });
|
|
4257
|
+
if (liveState.writer) liveState.writer({ type: 'tool', name, input: _lightToolInput(input) });
|
|
4114
4258
|
}
|
|
4115
4259
|
});
|
|
4116
4260
|
_ccStreamAbort = llmPromise.abort;
|
|
4261
|
+
liveState.abortFn = _ccStreamAbort;
|
|
4117
4262
|
ccInFlightAborts.set(tabId, _ccStreamAbort);
|
|
4118
4263
|
const result = await llmPromise;
|
|
4119
4264
|
trackUsage('command-center', result.usage);
|
|
@@ -4127,21 +4272,24 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4127
4272
|
toolUses = []; // discard stale metadata from the failed resume attempt
|
|
4128
4273
|
const retryPromise = callLLMStreaming(freshPrompt, CC_STATIC_SYSTEM_PROMPT, {
|
|
4129
4274
|
timeout: 900000, label: 'command-center', model: streamModel, maxTurns: ccMaxTurns,
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4275
|
+
allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
|
|
4276
|
+
effort: streamEffort, direct: true,
|
|
4277
|
+
onChunk: (text) => {
|
|
4278
|
+
const actIdx = findCCActionsDelimiter(text);
|
|
4279
|
+
const display = actIdx >= 0 ? text.slice(0, actIdx).trim() : text;
|
|
4280
|
+
liveState.text = display;
|
|
4281
|
+
if (liveState.writer) liveState.writer({ type: 'chunk', text: display });
|
|
4282
|
+
},
|
|
4283
|
+
onToolUse: (name, input) => {
|
|
4284
|
+
toolUses.push({ name, input: input || {} });
|
|
4285
|
+
liveState.tools.push({ name, input: input || {} });
|
|
4286
|
+
if (liveState.writer) liveState.writer({ type: 'tool', name, input: _lightToolInput(input) });
|
|
4287
|
+
}
|
|
4288
|
+
});
|
|
4289
|
+
_ccStreamAbort = retryPromise.abort;
|
|
4290
|
+
liveState.abortFn = _ccStreamAbort;
|
|
4291
|
+
ccInFlightAborts.set(tabId, _ccStreamAbort);
|
|
4292
|
+
const retryResult = await retryPromise;
|
|
4145
4293
|
trackUsage('command-center', retryResult.usage);
|
|
4146
4294
|
if (retryResult.text) {
|
|
4147
4295
|
// Fresh session succeeded — use retryResult from here
|
|
@@ -4154,8 +4302,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4154
4302
|
const stderrTail = (result.stderr || '').trim().split('\n').filter(Boolean).slice(-3).join(' | ');
|
|
4155
4303
|
console.error(`[CC-stream] Failed: code=${result.code}, stderr=${(result.stderr || '').slice(0, 500)}, stdout_tail=${(result.raw || '').slice(-500)}`);
|
|
4156
4304
|
const retryHint = 'Send your message again to retry.';
|
|
4157
|
-
|
|
4158
|
-
|
|
4305
|
+
liveState.donePayload = { type: 'done', text: `I had trouble processing that ${debugInfo}. ${stderrTail ? 'Detail: ' + stderrTail : ''}\n\n${retryHint}`, actions: [], sessionId: null };
|
|
4306
|
+
if (liveState.writer) liveState.writer(liveState.donePayload);
|
|
4307
|
+
if (liveState.endResponse) liveState.endResponse();
|
|
4308
|
+
_scheduleCcLiveCleanup(tabId);
|
|
4159
4309
|
return;
|
|
4160
4310
|
}
|
|
4161
4311
|
|
|
@@ -4197,7 +4347,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4197
4347
|
}
|
|
4198
4348
|
const donePayload = { type: 'done', text: displayText, actions, actionResults, sessionId: responseSessionId, newSession: !wasResume };
|
|
4199
4349
|
if (sessionReset) donePayload.sessionReset = true;
|
|
4200
|
-
|
|
4350
|
+
liveState.donePayload = donePayload;
|
|
4351
|
+
if (liveState.writer) liveState.writer(donePayload);
|
|
4201
4352
|
|
|
4202
4353
|
// Mirror CC response to Teams (non-blocking, skip Teams-originated)
|
|
4203
4354
|
const _streamTabId = body.tabId || 'default';
|
|
@@ -4205,7 +4356,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4205
4356
|
teams.teamsPostCCResponse(body.message, result.text).catch(() => {});
|
|
4206
4357
|
}
|
|
4207
4358
|
|
|
4208
|
-
|
|
4359
|
+
if (liveState.endResponse) liveState.endResponse();
|
|
4360
|
+
_scheduleCcLiveCleanup(tabId);
|
|
4209
4361
|
} finally {
|
|
4210
4362
|
stopCcHeartbeat();
|
|
4211
4363
|
_releaseCCTab(tabId);
|
|
@@ -5099,6 +5251,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5099
5251
|
|
|
5100
5252
|
// Command Center
|
|
5101
5253
|
{ method: 'POST', path: '/api/command-center/new-session', desc: 'Clear active CC session', handler: handleCommandCenterNewSession },
|
|
5254
|
+
{ method: 'POST', path: '/api/command-center/abort', desc: 'Abort an in-flight CC request for a tab', params: 'tabId?', handler: handleCommandCenterAbort },
|
|
5102
5255
|
{ method: 'POST', path: '/api/command-center', desc: 'Conversational command center with full minions context', params: 'message, sessionId?', handler: handleCommandCenter },
|
|
5103
5256
|
{ method: 'POST', path: '/api/command-center/stream', desc: 'Streaming CC — SSE with text chunks as they arrive', params: 'message, tabId?', handler: handleCommandCenterStream },
|
|
5104
5257
|
{ method: 'GET', path: '/api/cc-sessions', desc: 'List CC session metadata for all tabs', handler: handleCCSessionsList },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1579",
|
|
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"
|