@yemi33/minions 0.1.1738 → 0.1.1740
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 +10 -0
- package/dashboard/js/command-center.js +6 -0
- package/dashboard.js +128 -76
- package/docs/command-center.md +4 -3
- package/engine/cleanup.js +4 -13
- package/engine/copilot-models.json +1 -1
- package/engine/llm.js +3 -0
- package/engine/runtimes/claude.js +23 -1
- package/engine/runtimes/copilot.js +23 -1
- package/engine/shared.js +0 -1
- package/engine/spawn-agent.js +8 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1740 (2026-05-06)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- doc-chat resume retry + 1h timeout + surfaced stderr (#2101)
|
|
7
|
+
- make Command Center chat sessions non-expiring (#2100)
|
|
8
|
+
|
|
9
|
+
### Fixes
|
|
10
|
+
- invalidate stale session IDs on runtime switch (#2102)
|
|
11
|
+
- drop 5s setTimeout that nuked sysprompt .tmp before Claude could read it (#2105)
|
|
12
|
+
|
|
3
13
|
## 0.1.1738 (2026-05-06)
|
|
4
14
|
|
|
5
15
|
### Features
|
|
@@ -392,6 +392,12 @@ function ccCloseTab(id) {
|
|
|
392
392
|
closingTab._queue = [];
|
|
393
393
|
_ccSending = (_ccTabs.some(function(t) { return t._sending; }));
|
|
394
394
|
}
|
|
395
|
+
// Tabs are non-expiring on the server — explicit close is the only path that
|
|
396
|
+
// removes the persisted session. Fire-and-forget DELETE so closing a tab
|
|
397
|
+
// also evicts the server-side cc-sessions.json entry.
|
|
398
|
+
try {
|
|
399
|
+
fetch('/api/cc-sessions/' + encodeURIComponent(id), { method: 'DELETE' }).catch(function() {});
|
|
400
|
+
} catch {}
|
|
395
401
|
_ccTabs.splice(idx, 1);
|
|
396
402
|
if (_ccActiveTabId === id) {
|
|
397
403
|
// Switch to adjacent tab or create new
|
package/dashboard.js
CHANGED
|
@@ -932,9 +932,14 @@ setInterval(() => {
|
|
|
932
932
|
|
|
933
933
|
// ── Command Center: session state + helpers ─────────────────────────────────
|
|
934
934
|
|
|
935
|
-
//
|
|
935
|
+
// CC chat sessions do NOT auto-expire. A tab is removed only via explicit user
|
|
936
|
+
// deletion (DELETE /api/cc-sessions/:id, wired from ccCloseTab). Doc-chat
|
|
937
|
+
// sessions keep their own TTL (DOC_SESSION_TTL_MS) — that's a separate store.
|
|
938
|
+
//
|
|
939
|
+
// CC_SESSION_MAX_TURNS is reused by the doc-chat session pruner to cap
|
|
940
|
+
// per-session turn growth there; CC chat sessions are not capped because
|
|
941
|
+
// users are expected to keep long-running tabs alive indefinitely.
|
|
936
942
|
const CC_SESSION_MAX_TURNS = shared.ENGINE_DEFAULTS.ccMaxTurns;
|
|
937
|
-
const CC_SESSION_TTL_MS = shared.ENGINE_DEFAULTS.ccSessionTtlMs;
|
|
938
943
|
let ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
|
|
939
944
|
const ccInFlightTabs = new Map(); // tabId → timestamp — per-tab in-flight tracking for parallel CC requests
|
|
940
945
|
const ccInFlightAborts = new Map(); // tabId → abortFn — lets a new request kill the stale LLM
|
|
@@ -947,9 +952,10 @@ const CC_STREAM_REATTACH_GRACE_MS = 60000; // keep CC job alive briefly after di
|
|
|
947
952
|
const CC_STREAM_DONE_RETENTION_MS = 30000; // retain final payload briefly so reconnect can still receive it
|
|
948
953
|
const CC_LIVE_STREAM_MAX_AGE_MS = shared.ENGINE_DEFAULTS.ccLiveStreamMaxAgeMs;
|
|
949
954
|
// Doc-chat is interactive — long-doc edits with multi-step Read+Write tool use can run
|
|
950
|
-
//
|
|
951
|
-
// edits mid-stream
|
|
952
|
-
|
|
955
|
+
// well past 5 min on `canEdit:true` paths. Bumped to 1 hour (matching CC) so legitimate
|
|
956
|
+
// edits aren't killed mid-stream and the backend timeout never beats the user's reading
|
|
957
|
+
// time. The doc-chat handlers still abort on client disconnect.
|
|
958
|
+
const DOC_CHAT_TIMEOUT_MS = 60 * 60 * 1000;
|
|
953
959
|
function _releaseCCTab(tabId) { ccInFlightTabs.delete(tabId); ccInFlightAborts.delete(tabId); }
|
|
954
960
|
function _getCcLiveStream(tabId) {
|
|
955
961
|
return ccLiveStreams.get(tabId) || null;
|
|
@@ -1070,18 +1076,17 @@ function _ccTabIsInFlight(tabId) {
|
|
|
1070
1076
|
|
|
1071
1077
|
function ccSessionValid() {
|
|
1072
1078
|
if (!ccSession.sessionId) return false;
|
|
1073
|
-
// Invalidate session if system prompt changed (e.g. after code update + restart)
|
|
1079
|
+
// Invalidate session if system prompt changed (e.g. after code update + restart).
|
|
1080
|
+
// This is correctness-driven, not expiration-driven: a session created against
|
|
1081
|
+
// a stale system prompt would carry the old persona/rules into resume turns.
|
|
1074
1082
|
if (ccSession._promptHash && ccSession._promptHash !== _ccPromptHash) {
|
|
1075
1083
|
console.log('[CC] System prompt changed — invalidating stale session');
|
|
1076
1084
|
ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
|
|
1077
1085
|
return false;
|
|
1078
1086
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
return false;
|
|
1083
|
-
}
|
|
1084
|
-
return ccSession.turnCount < CC_SESSION_MAX_TURNS;
|
|
1087
|
+
// No TTL or turn-count cap — CC chat sessions live until the user explicitly
|
|
1088
|
+
// closes the tab (which calls DELETE /api/cc-sessions/:id).
|
|
1089
|
+
return true;
|
|
1085
1090
|
}
|
|
1086
1091
|
|
|
1087
1092
|
// Static system prompt — baked into session on creation, never changes
|
|
@@ -1120,11 +1125,13 @@ function _sessionExpired(lastActiveAt, ttlMs) {
|
|
|
1120
1125
|
return Date.now() - at > ttlMs;
|
|
1121
1126
|
}
|
|
1122
1127
|
|
|
1128
|
+
// CC tab sessions never auto-expire — only invalid records (missing id /
|
|
1129
|
+
// sessionId) and prompt-hash mismatches are filtered out. Tabs are removed
|
|
1130
|
+
// from disk only when the user explicitly closes them via DELETE
|
|
1131
|
+
// /api/cc-sessions/:id.
|
|
1123
1132
|
function _filterCcTabSessions(sessions) {
|
|
1124
1133
|
return (Array.isArray(sessions) ? sessions : []).filter(s =>
|
|
1125
1134
|
s && s.id && s.sessionId &&
|
|
1126
|
-
(s.turnCount || 0) < CC_SESSION_MAX_TURNS &&
|
|
1127
|
-
!_sessionExpired(s.lastActiveAt || s.createdAt, CC_SESSION_TTL_MS) &&
|
|
1128
1135
|
(!s._promptHash || s._promptHash === _ccPromptHash)
|
|
1129
1136
|
);
|
|
1130
1137
|
}
|
|
@@ -1157,10 +1164,12 @@ function _buildTranscriptCarryover(transcript, { previousRuntime } = {}) {
|
|
|
1157
1164
|
return `${header}\n\n${truncationNote}${lines.join('\n\n')}\n\n--- Current message follows ---`;
|
|
1158
1165
|
}
|
|
1159
1166
|
|
|
1160
|
-
// Load persisted CC session on startup
|
|
1167
|
+
// Load persisted CC session on startup. CC chat sessions are non-expiring;
|
|
1168
|
+
// only restore-time validity checks here are sessionId presence (anything
|
|
1169
|
+
// else would auto-expire the user's chat without their consent).
|
|
1161
1170
|
try {
|
|
1162
1171
|
const saved = safeJson(path.join(ENGINE_DIR, 'cc-session.json'));
|
|
1163
|
-
if (saved && saved.sessionId
|
|
1172
|
+
if (saved && saved.sessionId) ccSession = saved;
|
|
1164
1173
|
} catch { /* optional */ }
|
|
1165
1174
|
|
|
1166
1175
|
let _preambleCache = null;
|
|
@@ -2458,6 +2467,56 @@ function _formatDocChatContext({ document, title, filePath, selection, canEdit,
|
|
|
2458
2467
|
return context;
|
|
2459
2468
|
}
|
|
2460
2469
|
|
|
2470
|
+
// Build the doc-chat extraContext for a single ccCall pass — refreshed on retry
|
|
2471
|
+
// so a fresh-session retry includes the full document instead of relying on the
|
|
2472
|
+
// dead session's prior turn for context.
|
|
2473
|
+
function _buildDocChatPass({ docSlice, title, filePath, selection, canEdit, isJson, sessionKey, freshSession }) {
|
|
2474
|
+
const existing = freshSession ? null : resolveSession('doc', sessionKey);
|
|
2475
|
+
const docHash = require('crypto').createHash('md5').update(docSlice).digest('hex').slice(0, 8);
|
|
2476
|
+
const docUnchanged = existing?.sessionId && existing._docHash === docHash;
|
|
2477
|
+
const extraContext = _formatDocChatContext({
|
|
2478
|
+
document: docSlice, title, filePath, selection, canEdit, isJson, docUnchanged,
|
|
2479
|
+
});
|
|
2480
|
+
return { extraContext, docHash, hadSession: !!existing?.sessionId };
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
// One-shot recovery from a stuck resumed session: if the initial call rode an
|
|
2484
|
+
// existing session and came back empty / non-zero, invalidate the persisted
|
|
2485
|
+
// session and re-run via the supplied closure. ccCall already retries fresh
|
|
2486
|
+
// when the runtime can't extract a sessionId, but on adapter-still-alive
|
|
2487
|
+
// failures it preserves the session — and from doc-chat's perspective that's
|
|
2488
|
+
// the failure mode the user actually sees ("Failed to process request").
|
|
2489
|
+
async function _retryDocChatAfterResumeFailure({ result, initialPass, freshSession, sessionKey, runOnce }) {
|
|
2490
|
+
if (!initialPass.hadSession || freshSession || result.missingRuntime) return result;
|
|
2491
|
+
if (result.code === 0 && result.text) return result;
|
|
2492
|
+
console.log(`[doc-chat] Resumed session call failed (code=${result.code}, empty=${!result.text}) — invalidating session and retrying fresh for ${sessionKey || 'untitled'}`);
|
|
2493
|
+
if (sessionKey) {
|
|
2494
|
+
docSessions.delete(sessionKey);
|
|
2495
|
+
schedulePersistDocSessions();
|
|
2496
|
+
}
|
|
2497
|
+
return runOnce();
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
// Build the {error} envelope returned to the dashboard when doc-chat ultimately
|
|
2501
|
+
// fails. Keeps the polite user-facing message but exposes the runtime's real
|
|
2502
|
+
// stderr / exit code / errorClass so the UI can render the cause and so future
|
|
2503
|
+
// failures are debuggable from logs.
|
|
2504
|
+
function _docChatFailureResponse(label, filePath, result) {
|
|
2505
|
+
const stderrTail = (result.stderr || '').slice(-2048);
|
|
2506
|
+
console.error(`[${label}] Failed: code=${result.code}, empty=${!result.text}, filePath=${filePath}, errorClass=${result.errorClass || 'null'}, stderr=${stderrTail.slice(0, 200)}`);
|
|
2507
|
+
return {
|
|
2508
|
+
answer: 'Failed to process request. Try again.',
|
|
2509
|
+
content: null,
|
|
2510
|
+
actions: [],
|
|
2511
|
+
error: {
|
|
2512
|
+
code: result.code ?? null,
|
|
2513
|
+
stderr: stderrTail,
|
|
2514
|
+
errorClass: result.errorClass || null,
|
|
2515
|
+
runtime: result.runtime || null,
|
|
2516
|
+
},
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2461
2520
|
// Doc-specific wrapper — adds document context, parses ---DOCUMENT---
|
|
2462
2521
|
async function ccDocCall({ message, document, title, filePath, selection, canEdit, isJson, model, freshSession, onAbortReady }) {
|
|
2463
2522
|
const sessionKey = filePath || title;
|
|
@@ -2471,34 +2530,30 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
|
|
|
2471
2530
|
// Skip persistDocSessions() here — the post-call cleanup below handles persistence.
|
|
2472
2531
|
}
|
|
2473
2532
|
|
|
2474
|
-
// Skip re-sending full document on session resume if content unchanged
|
|
2475
|
-
const docHash = require('crypto').createHash('md5').update(docSlice).digest('hex').slice(0, 8);
|
|
2476
|
-
const existing = freshSession ? null : resolveSession('doc', sessionKey);
|
|
2477
|
-
const docUnchanged = existing?.sessionId && existing._docHash === docHash;
|
|
2478
|
-
|
|
2479
|
-
const docContext = _formatDocChatContext({
|
|
2480
|
-
document: docSlice,
|
|
2481
|
-
title,
|
|
2482
|
-
filePath,
|
|
2483
|
-
selection,
|
|
2484
|
-
canEdit,
|
|
2485
|
-
isJson,
|
|
2486
|
-
docUnchanged,
|
|
2487
|
-
});
|
|
2488
2533
|
const allowActions = _messageRequestsOrchestration(message);
|
|
2489
2534
|
|
|
2490
|
-
const
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2535
|
+
const runOnce = async () => {
|
|
2536
|
+
const { extraContext } = _buildDocChatPass({
|
|
2537
|
+
docSlice, title, filePath, selection, canEdit, isJson, sessionKey, freshSession,
|
|
2538
|
+
});
|
|
2539
|
+
return ccCall(message, {
|
|
2540
|
+
store: 'doc', sessionKey,
|
|
2541
|
+
extraContext, label: 'doc-chat',
|
|
2542
|
+
timeout: DOC_CHAT_TIMEOUT_MS,
|
|
2543
|
+
allowedTools: canEdit ? 'Read,Write,Edit,Glob,Grep' : 'Read,Glob,Grep',
|
|
2544
|
+
maxTurns: canEdit ? 25 : 10,
|
|
2545
|
+
skipStatePreamble: true,
|
|
2546
|
+
systemPrompt: DOC_CHAT_SYSTEM_PROMPT,
|
|
2547
|
+
...(model ? { model } : {}),
|
|
2548
|
+
onAbortReady,
|
|
2549
|
+
});
|
|
2550
|
+
};
|
|
2551
|
+
|
|
2552
|
+
const initialPass = _buildDocChatPass({
|
|
2553
|
+
docSlice, title, filePath, selection, canEdit, isJson, sessionKey, freshSession,
|
|
2501
2554
|
});
|
|
2555
|
+
let result = await runOnce();
|
|
2556
|
+
result = await _retryDocChatAfterResumeFailure({ result, initialPass, freshSession, sessionKey, runOnce });
|
|
2502
2557
|
|
|
2503
2558
|
if (freshSession && sessionKey) {
|
|
2504
2559
|
// One-shot call — discard the session ccCall just stored so it cannot
|
|
@@ -2508,7 +2563,7 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
|
|
|
2508
2563
|
} else if (result.code === 0 && result.sessionId) {
|
|
2509
2564
|
// Store doc hash for next call's unchanged check
|
|
2510
2565
|
const session = resolveSession('doc', sessionKey);
|
|
2511
|
-
if (session) session._docHash = docHash;
|
|
2566
|
+
if (session) session._docHash = initialPass.docHash;
|
|
2512
2567
|
}
|
|
2513
2568
|
|
|
2514
2569
|
if (result.missingRuntime) {
|
|
@@ -2516,8 +2571,7 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
|
|
|
2516
2571
|
}
|
|
2517
2572
|
|
|
2518
2573
|
if (result.code !== 0 || !result.text) {
|
|
2519
|
-
|
|
2520
|
-
return { answer: 'Failed to process request. Try again.', content: null, actions: [] };
|
|
2574
|
+
return _docChatFailureResponse('doc-chat', filePath, result);
|
|
2521
2575
|
}
|
|
2522
2576
|
|
|
2523
2577
|
return _parseDocChatResultText(result.text, { allowActions });
|
|
@@ -2531,42 +2585,39 @@ async function ccDocCallStreaming({ message, document, title, filePath, selectio
|
|
|
2531
2585
|
docSessions.delete(sessionKey);
|
|
2532
2586
|
}
|
|
2533
2587
|
|
|
2534
|
-
const docHash = require('crypto').createHash('md5').update(docSlice).digest('hex').slice(0, 8);
|
|
2535
|
-
const existing = freshSession ? null : resolveSession('doc', sessionKey);
|
|
2536
|
-
const docUnchanged = existing?.sessionId && existing._docHash === docHash;
|
|
2537
|
-
|
|
2538
|
-
const docContext = _formatDocChatContext({
|
|
2539
|
-
document: docSlice,
|
|
2540
|
-
title,
|
|
2541
|
-
filePath,
|
|
2542
|
-
selection,
|
|
2543
|
-
canEdit,
|
|
2544
|
-
isJson,
|
|
2545
|
-
docUnchanged,
|
|
2546
|
-
});
|
|
2547
2588
|
const allowActions = _messageRequestsOrchestration(message);
|
|
2548
2589
|
|
|
2549
|
-
const
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2590
|
+
const runOnce = async () => {
|
|
2591
|
+
const { extraContext } = _buildDocChatPass({
|
|
2592
|
+
docSlice, title, filePath, selection, canEdit, isJson, sessionKey, freshSession,
|
|
2593
|
+
});
|
|
2594
|
+
return ccCallStreaming(message, {
|
|
2595
|
+
store: 'doc', sessionKey,
|
|
2596
|
+
extraContext, label: 'doc-chat',
|
|
2597
|
+
timeout: DOC_CHAT_TIMEOUT_MS,
|
|
2598
|
+
allowedTools: canEdit ? 'Read,Write,Edit,Glob,Grep' : 'Read,Glob,Grep',
|
|
2599
|
+
maxTurns: canEdit ? 25 : 10,
|
|
2600
|
+
skipStatePreamble: true,
|
|
2601
|
+
systemPrompt: DOC_CHAT_SYSTEM_PROMPT,
|
|
2602
|
+
...(model ? { model } : {}),
|
|
2603
|
+
onAbortReady,
|
|
2604
|
+
onChunk: (text) => { if (onChunk) onChunk(_docChatDisplayText(text, { allowActions })); },
|
|
2605
|
+
onToolUse,
|
|
2606
|
+
});
|
|
2607
|
+
};
|
|
2608
|
+
|
|
2609
|
+
const initialPass = _buildDocChatPass({
|
|
2610
|
+
docSlice, title, filePath, selection, canEdit, isJson, sessionKey, freshSession,
|
|
2562
2611
|
});
|
|
2612
|
+
let result = await runOnce();
|
|
2613
|
+
result = await _retryDocChatAfterResumeFailure({ result, initialPass, freshSession, sessionKey, runOnce });
|
|
2563
2614
|
|
|
2564
2615
|
if (freshSession && sessionKey) {
|
|
2565
2616
|
docSessions.delete(sessionKey);
|
|
2566
2617
|
schedulePersistDocSessions();
|
|
2567
2618
|
} else if (result.code === 0 && result.sessionId) {
|
|
2568
2619
|
const session = resolveSession('doc', sessionKey);
|
|
2569
|
-
if (session) session._docHash = docHash;
|
|
2620
|
+
if (session) session._docHash = initialPass.docHash;
|
|
2570
2621
|
}
|
|
2571
2622
|
|
|
2572
2623
|
if (result.missingRuntime) {
|
|
@@ -2574,8 +2625,7 @@ async function ccDocCallStreaming({ message, document, title, filePath, selectio
|
|
|
2574
2625
|
}
|
|
2575
2626
|
|
|
2576
2627
|
if (result.code !== 0 || !result.text) {
|
|
2577
|
-
|
|
2578
|
-
return { answer: 'Failed to process request. Try again.', content: null, actions: [] };
|
|
2628
|
+
return _docChatFailureResponse('doc-chat-stream', filePath, result);
|
|
2579
2629
|
}
|
|
2580
2630
|
|
|
2581
2631
|
return _parseDocChatResultText(result.text, { allowActions });
|
|
@@ -4672,7 +4722,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4672
4722
|
}
|
|
4673
4723
|
}
|
|
4674
4724
|
|
|
4675
|
-
const { answer, content, actions, actionParseError } = await ccDocCall({
|
|
4725
|
+
const { answer, content, actions, actionParseError, error: ccError } = await ccDocCall({
|
|
4676
4726
|
message: body.message, document: currentContent, title: body.title,
|
|
4677
4727
|
filePath: body.filePath, selection: body.selection, canEdit, isJson,
|
|
4678
4728
|
model: body.model || undefined,
|
|
@@ -4681,11 +4731,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4681
4731
|
});
|
|
4682
4732
|
const actionResults = await executeDocChatActions(actions);
|
|
4683
4733
|
const baseReply = (extra = {}) => ({
|
|
4684
|
-
ok:
|
|
4734
|
+
ok: !ccError,
|
|
4685
4735
|
answer,
|
|
4686
4736
|
actions,
|
|
4687
4737
|
...(actionResults ? { actionResults } : {}),
|
|
4688
4738
|
...(actionParseError ? { actionParseError } : {}),
|
|
4739
|
+
...(ccError ? { error: ccError } : {}),
|
|
4689
4740
|
...extra,
|
|
4690
4741
|
});
|
|
4691
4742
|
|
|
@@ -4787,7 +4838,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4787
4838
|
|
|
4788
4839
|
try {
|
|
4789
4840
|
|
|
4790
|
-
const { answer, content, actions, actionParseError } = await ccDocCallStreaming({
|
|
4841
|
+
const { answer, content, actions, actionParseError, error: ccError } = await ccDocCallStreaming({
|
|
4791
4842
|
message: body.message, document: currentContent, title: body.title,
|
|
4792
4843
|
filePath: body.filePath, selection: body.selection, canEdit, isJson,
|
|
4793
4844
|
model: body.model || undefined,
|
|
@@ -4803,6 +4854,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4803
4854
|
actions,
|
|
4804
4855
|
...(actionResults ? { actionResults } : {}),
|
|
4805
4856
|
...(actionParseError ? { actionParseError } : {}),
|
|
4857
|
+
...(ccError ? { error: ccError } : {}),
|
|
4806
4858
|
...extra,
|
|
4807
4859
|
});
|
|
4808
4860
|
|
package/docs/command-center.md
CHANGED
|
@@ -13,11 +13,12 @@ CC maintains a true multi-turn session using Claude CLI's `--resume` flag. Unlik
|
|
|
13
13
|
**Session lifecycle:**
|
|
14
14
|
- **Created** on first message (or after the system prompt changes, or when you click **New Session**)
|
|
15
15
|
- **Resumed** on subsequent messages via `--resume <sessionId>`
|
|
16
|
-
- **Invalidated** when the CC system prompt changes — detected by hashing `CC_STATIC_SYSTEM_PROMPT` into `_ccPromptHash` and comparing on each call.
|
|
17
|
-
- **Persisted** to `engine/cc-session.json` —
|
|
16
|
+
- **Invalidated** only when the CC system prompt changes — detected by hashing `CC_STATIC_SYSTEM_PROMPT` into `_ccPromptHash` and comparing on each call. CC chat sessions do **not** auto-expire by TTL or turn count; `ENGINE_DEFAULTS.ccMaxTurns` (default 50) is a per-call tool-use cap inside the Claude CLI, not a session lifetime cap.
|
|
17
|
+
- **Persisted** to `engine/cc-session.json` (legacy global session) and `engine/cc-sessions.json` (per-tab sessions) — both survive dashboard restarts and engine cleanup ticks
|
|
18
18
|
- **Frontend messages** saved to `localStorage` — survive page refresh
|
|
19
|
+
- **Removed** only when the user explicitly closes a tab via the **×** button on the tab strip — that fires `DELETE /api/cc-sessions/:id` to evict the persisted session
|
|
19
20
|
|
|
20
|
-
Click **New Session** in the drawer header to start fresh.
|
|
21
|
+
Click **New Session** in the drawer header to start fresh; click the **×** on a tab to remove it permanently.
|
|
21
22
|
|
|
22
23
|
### Fresh State Each Turn
|
|
23
24
|
|
package/engine/cleanup.js
CHANGED
|
@@ -681,20 +681,11 @@ async function runCleanup(config, verbose = false) {
|
|
|
681
681
|
}
|
|
682
682
|
} catch (e) { log('warn', 'orphan PRD status reset: ' + e.message); }
|
|
683
683
|
|
|
684
|
-
// 10.
|
|
684
|
+
// 10. CC tab sessions are non-expiring by design — they persist until the
|
|
685
|
+
// user explicitly closes the tab (which fires DELETE /api/cc-sessions/:id).
|
|
686
|
+
// Cleanup intentionally does NOT prune cc-sessions.json; doing so would
|
|
687
|
+
// silently invalidate live chat tabs the user expects to keep.
|
|
685
688
|
cleaned.ccSessions = 0;
|
|
686
|
-
try {
|
|
687
|
-
const ccSessionsPath = path.join(ENGINE_DIR, 'cc-sessions.json');
|
|
688
|
-
const sessions = shared.safeJsonArr(ccSessionsPath);
|
|
689
|
-
const CC_SESSIONS_CAP = 50;
|
|
690
|
-
if (sessions.length > CC_SESSIONS_CAP) {
|
|
691
|
-
// Sort by lastActiveAt descending, keep newest
|
|
692
|
-
sessions.sort((a, b) => new Date(b.lastActiveAt || 0) - new Date(a.lastActiveAt || 0));
|
|
693
|
-
const pruned = sessions.slice(0, CC_SESSIONS_CAP);
|
|
694
|
-
cleaned.ccSessions = sessions.length - pruned.length;
|
|
695
|
-
safeWrite(ccSessionsPath, pruned);
|
|
696
|
-
}
|
|
697
|
-
} catch (e) { log('warn', 'prune cc-sessions: ' + e.message); }
|
|
698
689
|
|
|
699
690
|
// 10b. Prune doc-chat sessions — cap at 100 entries, remove oldest beyond cap
|
|
700
691
|
cleaned.docSessions = 0;
|
package/engine/llm.js
CHANGED
|
@@ -787,6 +787,9 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
|
|
|
787
787
|
|
|
788
788
|
proc.on('close', finish);
|
|
789
789
|
proc.on('exit', (code) => {
|
|
790
|
+
// 'close' waits for stdio to close. If the runtime spawned a detached
|
|
791
|
+
// grandchild that inherited stdout/stderr, the OS pipe stays open and
|
|
792
|
+
// 'close' may never fire. Fall back to 'exit' after a drain window.
|
|
790
793
|
if (settled) return;
|
|
791
794
|
exitSettleTimer = setTimeout(() => finish(code), LLM_EXIT_SETTLE_GRACE_MS);
|
|
792
795
|
});
|
|
@@ -287,12 +287,33 @@ function getSkillWriteTargets({ homeDir = os.homedir(), project = null } = {}) {
|
|
|
287
287
|
};
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
// Stamped into every session.json this adapter writes so the pre-spawn resume
|
|
291
|
+
// path can detect "session was produced by a different runtime" — Claude
|
|
292
|
+
// rejects Copilot session IDs (and vice versa) with "No conversation found",
|
|
293
|
+
// which would otherwise burn a retry slot before the post-failure cleanup at
|
|
294
|
+
// engine.js:1195 fires. See W-mot9fwya000d09cb.
|
|
295
|
+
const RUNTIME_NAME = 'claude';
|
|
296
|
+
|
|
290
297
|
function getResumeSessionId({ agentId, branchName, agentsDir, maxAgeMs = 2 * 60 * 60 * 1000, logger = console } = {}) {
|
|
291
298
|
if (!agentId || agentId.startsWith('temp-') || !agentsDir) return null;
|
|
299
|
+
const sessionPath = path.join(agentsDir, agentId, 'session.json');
|
|
292
300
|
try {
|
|
293
|
-
const sessionPath = path.join(agentsDir, agentId, 'session.json');
|
|
294
301
|
const sessionFile = _safeJson(sessionPath);
|
|
295
302
|
if (!sessionFile?.sessionId || !sessionFile.savedAt) return null;
|
|
303
|
+
|
|
304
|
+
// Runtime-mismatch invalidation. Distinct from stale-by-age: the session is
|
|
305
|
+
// structurally unusable on this runtime, so drop it AND clear session.json
|
|
306
|
+
// so the next dispatch starts fresh instead of failing with --resume.
|
|
307
|
+
// Legacy sessions (no `runtime` field) are treated as compatible — opt-in
|
|
308
|
+
// check, no false invalidations on first deploy.
|
|
309
|
+
if (sessionFile.runtime && sessionFile.runtime !== RUNTIME_NAME) {
|
|
310
|
+
if (logger && typeof logger.info === 'function') {
|
|
311
|
+
logger.info(`Skipping resume for ${agentId}: runtime mismatch (session: ${sessionFile.runtime}, current: ${RUNTIME_NAME}) — clearing session.json`);
|
|
312
|
+
}
|
|
313
|
+
try { fs.unlinkSync(sessionPath); } catch {}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
296
317
|
const sessionAge = Date.now() - new Date(sessionFile.savedAt).getTime();
|
|
297
318
|
const sameBranch = branchName && sessionFile.branch && sessionFile.branch === branchName;
|
|
298
319
|
if (sessionAge < maxAgeMs && sameBranch) {
|
|
@@ -315,6 +336,7 @@ function saveSession({ agentId, dispatchId, branch, sessionId, agentsDir, now =
|
|
|
315
336
|
dispatchId,
|
|
316
337
|
savedAt: typeof now === 'function' ? now() : new Date().toISOString(),
|
|
317
338
|
branch: branch || null,
|
|
339
|
+
runtime: RUNTIME_NAME,
|
|
318
340
|
});
|
|
319
341
|
return true;
|
|
320
342
|
} catch (err) {
|
|
@@ -306,12 +306,33 @@ function getSkillWriteTargets({ homeDir = os.homedir(), project = null } = {}) {
|
|
|
306
306
|
};
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
// Stamped into every session.json this adapter writes so the pre-spawn resume
|
|
310
|
+
// path can detect "session was produced by a different runtime" — Copilot
|
|
311
|
+
// rejects Claude session IDs (and vice versa) with "No conversation found",
|
|
312
|
+
// which would otherwise burn a retry slot before the post-failure cleanup at
|
|
313
|
+
// engine.js:1195 fires. See W-mot9fwya000d09cb.
|
|
314
|
+
const RUNTIME_NAME = 'copilot';
|
|
315
|
+
|
|
309
316
|
function getResumeSessionId({ agentId, branchName, agentsDir, maxAgeMs = 2 * 60 * 60 * 1000, logger = console } = {}) {
|
|
310
317
|
if (!agentId || agentId.startsWith('temp-') || !agentsDir) return null;
|
|
318
|
+
const sessionPath = path.join(agentsDir, agentId, 'session.json');
|
|
311
319
|
try {
|
|
312
|
-
const sessionPath = path.join(agentsDir, agentId, 'session.json');
|
|
313
320
|
const sessionFile = _safeJson(sessionPath);
|
|
314
321
|
if (!sessionFile?.sessionId || !sessionFile.savedAt) return null;
|
|
322
|
+
|
|
323
|
+
// Runtime-mismatch invalidation. Distinct from stale-by-age: the session is
|
|
324
|
+
// structurally unusable on this runtime, so drop it AND clear session.json
|
|
325
|
+
// so the next dispatch starts fresh instead of failing with --resume.
|
|
326
|
+
// Legacy sessions (no `runtime` field) are treated as compatible — opt-in
|
|
327
|
+
// check, no false invalidations on first deploy.
|
|
328
|
+
if (sessionFile.runtime && sessionFile.runtime !== RUNTIME_NAME) {
|
|
329
|
+
if (logger && typeof logger.info === 'function') {
|
|
330
|
+
logger.info(`Skipping resume for ${agentId}: runtime mismatch (session: ${sessionFile.runtime}, current: ${RUNTIME_NAME}) — clearing session.json`);
|
|
331
|
+
}
|
|
332
|
+
try { fs.unlinkSync(sessionPath); } catch {}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
315
336
|
const sessionAge = Date.now() - new Date(sessionFile.savedAt).getTime();
|
|
316
337
|
const sameBranch = branchName && sessionFile.branch && sessionFile.branch === branchName;
|
|
317
338
|
if (sessionAge < maxAgeMs && sameBranch) {
|
|
@@ -334,6 +355,7 @@ function saveSession({ agentId, dispatchId, branch, sessionId, agentsDir, now =
|
|
|
334
355
|
dispatchId,
|
|
335
356
|
savedAt: typeof now === 'function' ? now() : new Date().toISOString(),
|
|
336
357
|
branch: branch || null,
|
|
358
|
+
runtime: RUNTIME_NAME,
|
|
337
359
|
});
|
|
338
360
|
return true;
|
|
339
361
|
} catch (err) {
|
package/engine/shared.js
CHANGED
|
@@ -914,7 +914,6 @@ const ENGINE_DEFAULTS = {
|
|
|
914
914
|
removeWorktreeFailureTtlMs: 24 * 60 * 60 * 1000, // stale failed paths are forgotten after a day
|
|
915
915
|
removeWorktreeFailureMaxEntries: 1000, // bound failed-worktree retry suppression cache
|
|
916
916
|
ccMaxTurns: 50, // max tool-use turns for CC/doc-chat before CLI stops
|
|
917
|
-
ccSessionTtlMs: 7 * 24 * 60 * 60 * 1000, // 7d — keep chats resumable after breaks, still bounded by turn cap
|
|
918
917
|
docSessionTtlMs: 7 * 24 * 60 * 60 * 1000, // 7d — longer-lived doc sessions, still bounded
|
|
919
918
|
docSessionMaxEntries: 200, // cap doc-chat session map/disk store by least-recent activity
|
|
920
919
|
ccLiveStreamMaxAgeMs: 30 * 60 * 1000, // hard cap reconnect buffers if abort/cleanup stalls
|
package/engine/spawn-agent.js
CHANGED
|
@@ -355,11 +355,14 @@ function main() {
|
|
|
355
355
|
|
|
356
356
|
// Clean up sys tmp (only created for non-resume sessions on adapters that
|
|
357
357
|
// use --system-prompt-file).
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
//
|
|
358
|
+
//
|
|
359
|
+
// We deliberately DO NOT use a fixed-delay setTimeout here. Claude CLI reads
|
|
360
|
+
// `--system-prompt-file` lazily during init; on cold starts (MCP boot,
|
|
361
|
+
// `--add-dir` traversal, Windows process startup) the read can land well
|
|
362
|
+
// after any short timer fires, leaving Claude to bail with
|
|
363
|
+
// "System prompt file not found". The exit/SIGTERM handler below — plus the
|
|
364
|
+
// matching unlinks in engine/dispatch.js and engine/meeting.js on dispatch
|
|
365
|
+
// completion — are sufficient to keep tmp files from leaking.
|
|
363
366
|
function _cleanupSpawnTempFiles() {
|
|
364
367
|
if (wantsSystemPromptFile) {
|
|
365
368
|
try { fs.unlinkSync(sysTmpPath); } catch { /* may already be cleaned */ }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1740",
|
|
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"
|