@yemi33/minions 0.1.1737 → 0.1.1739
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 +17 -0
- package/dashboard/js/command-center.js +36 -2
- package/dashboard/js/features-client.js +16 -0
- package/dashboard/js/settings.js +68 -2
- package/dashboard-build.js +1 -1
- package/dashboard.js +110 -19
- package/docs/command-center.md +4 -3
- package/engine/cleanup.js +4 -13
- package/engine/copilot-models.json +1 -1
- package/engine/features.js +58 -0
- package/engine/llm.js +38 -2
- package/engine/shared.js +0 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1739 (2026-05-06)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- make Command Center chat sessions non-expiring (#2100)
|
|
7
|
+
|
|
8
|
+
## 0.1.1738 (2026-05-06)
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
- add feature-flag registry for experimental UX gates
|
|
12
|
+
- graceful runtime-switch reset with transcript carryover
|
|
13
|
+
|
|
14
|
+
### Fixes
|
|
15
|
+
- prevent hung callLLM Promises when child stdio doesn't drain
|
|
16
|
+
|
|
17
|
+
### Other
|
|
18
|
+
- chore(gitignore): ignore root-level config.json.backup sidecar
|
|
19
|
+
|
|
3
20
|
## 0.1.1737 (2026-05-05)
|
|
4
21
|
|
|
5
22
|
### Other
|
|
@@ -101,6 +101,26 @@ function _ccActiveTab() {
|
|
|
101
101
|
return _ccTabs.find(function(t) { return t.id === _ccActiveTabId; }) || null;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
// Build a plain-text transcript from a tab's stored messages — sent on every
|
|
105
|
+
// initial request so the server can carry it over if the session has to reset
|
|
106
|
+
// (runtime switch, system-prompt change). Last 20 user/assistant turns only;
|
|
107
|
+
// system/action rows are skipped because they're UI artifacts, not dialog.
|
|
108
|
+
var CC_TRANSCRIPT_MAX_TURNS = 20;
|
|
109
|
+
function _ccBuildTranscript(tab) {
|
|
110
|
+
if (!tab || !Array.isArray(tab.messages) || tab.messages.length === 0) return [];
|
|
111
|
+
var out = [];
|
|
112
|
+
for (var i = 0; i < tab.messages.length; i++) {
|
|
113
|
+
var m = tab.messages[i];
|
|
114
|
+
if (!m || (m.role !== 'user' && m.role !== 'assistant')) continue;
|
|
115
|
+
var html = typeof m.html === 'string' ? m.html : '';
|
|
116
|
+
var tmp = document.createElement('div');
|
|
117
|
+
tmp.innerHTML = html;
|
|
118
|
+
var text = (tmp.textContent || tmp.innerText || '').trim();
|
|
119
|
+
if (text) out.push({ role: m.role, text: text });
|
|
120
|
+
}
|
|
121
|
+
return out.slice(-CC_TRANSCRIPT_MAX_TURNS);
|
|
122
|
+
}
|
|
123
|
+
|
|
104
124
|
function _ccMergeStreamText(prev, incoming) {
|
|
105
125
|
// `prev` is already merged-clean from prior frames (server strips actions
|
|
106
126
|
// before SSE emission, and any leaked partial was sanitized by the previous
|
|
@@ -372,6 +392,12 @@ function ccCloseTab(id) {
|
|
|
372
392
|
closingTab._queue = [];
|
|
373
393
|
_ccSending = (_ccTabs.some(function(t) { return t._sending; }));
|
|
374
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 {}
|
|
375
401
|
_ccTabs.splice(idx, 1);
|
|
376
402
|
if (_ccActiveTabId === id) {
|
|
377
403
|
// Switch to adjacent tab or create new
|
|
@@ -756,7 +782,15 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
756
782
|
terminalEventSeen = true;
|
|
757
783
|
_cleanupStreamDiv();
|
|
758
784
|
if (evt.sessionReset) {
|
|
759
|
-
|
|
785
|
+
var resetText;
|
|
786
|
+
if (evt.sessionResetReason === 'runtimeChanged' && evt.previousRuntime && evt.currentRuntime) {
|
|
787
|
+
resetText = 'Runtime switched (' + escHtml(evt.previousRuntime) + ' → ' + escHtml(evt.currentRuntime) + ') — started a fresh session and carried over recent history.';
|
|
788
|
+
} else if (evt.sessionResetReason === 'promptChanged') {
|
|
789
|
+
resetText = 'Minions was updated — started a fresh session and carried over recent history.';
|
|
790
|
+
} else {
|
|
791
|
+
resetText = 'Minions was updated — started a fresh session with latest context.';
|
|
792
|
+
}
|
|
793
|
+
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">' + resetText + '</div>', false, activeTabId);
|
|
760
794
|
}
|
|
761
795
|
var finalText = _ccMergeStreamText(streamedText, evt.text || '');
|
|
762
796
|
var rendered = renderMd(finalText || streamedText || '');
|
|
@@ -824,7 +858,7 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
824
858
|
while (true) {
|
|
825
859
|
var consume = await _ccConsumeStream(
|
|
826
860
|
reconnectAttempts === 0
|
|
827
|
-
? { message: message, tabId: activeTabId, sessionId: activeTab.sessionId || null }
|
|
861
|
+
? { message: message, tabId: activeTabId, sessionId: activeTab.sessionId || null, transcript: _ccBuildTranscript(activeTab) }
|
|
828
862
|
: { tabId: activeTabId, sessionId: activeTab.sessionId || null, reconnect: true },
|
|
829
863
|
reconnectAttempts > 0
|
|
830
864
|
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// features-client.js — Client-side feature flag accessor.
|
|
2
|
+
// Reads `window.MINIONS_FEATURES` (data injected at HTML build time:
|
|
3
|
+
// { flags: {id: bool}, defaults: {id: bool} }) and exposes synchronous
|
|
4
|
+
// `MinionsFeatures.isOn(id)` so gated render code can branch without async.
|
|
5
|
+
|
|
6
|
+
window.MinionsFeatures = {
|
|
7
|
+
isOn: function(id) {
|
|
8
|
+
var f = window.MINIONS_FEATURES || { flags: {}, defaults: {} };
|
|
9
|
+
if (Object.prototype.hasOwnProperty.call(f.flags, id)) return f.flags[id] === true;
|
|
10
|
+
return f.defaults && f.defaults[id] === true;
|
|
11
|
+
},
|
|
12
|
+
_setLocal: function(id, enabled) {
|
|
13
|
+
if (!window.MINIONS_FEATURES) window.MINIONS_FEATURES = { flags: {}, defaults: {} };
|
|
14
|
+
window.MINIONS_FEATURES.flags[id] = enabled === true;
|
|
15
|
+
},
|
|
16
|
+
};
|
package/dashboard/js/settings.js
CHANGED
|
@@ -28,10 +28,18 @@ async function openSettings() {
|
|
|
28
28
|
|
|
29
29
|
_settingsData = null;
|
|
30
30
|
let data;
|
|
31
|
+
let featuresList = [];
|
|
31
32
|
try {
|
|
32
|
-
const
|
|
33
|
-
|
|
33
|
+
const [settingsRes, featuresRes] = await Promise.all([
|
|
34
|
+
fetch('/api/settings'),
|
|
35
|
+
fetch('/api/features').catch(() => null),
|
|
36
|
+
]);
|
|
37
|
+
data = await settingsRes.json();
|
|
34
38
|
_settingsData = data;
|
|
39
|
+
if (featuresRes && featuresRes.ok) {
|
|
40
|
+
const fjson = await featuresRes.json().catch(() => null);
|
|
41
|
+
if (fjson && Array.isArray(fjson.features)) featuresList = fjson.features;
|
|
42
|
+
}
|
|
35
43
|
} catch (e) { showToast('cmd-toast', 'Failed to load settings: ' + e.message, false); return; }
|
|
36
44
|
|
|
37
45
|
const e = data.engine || {};
|
|
@@ -276,6 +284,36 @@ async function openSettings() {
|
|
|
276
284
|
'<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Routing Table</h3>' +
|
|
277
285
|
'<textarea id="set-routing" rows="12" style="width:100%;padding:8px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-family:monospace;font-size:11px;resize:vertical">' + escHtml(data.routing || '') + '</textarea>' +
|
|
278
286
|
|
|
287
|
+
// Toggles persist immediately via POST /api/features/toggle — no Save needed.
|
|
288
|
+
'<details id="settings-features-details" style="margin-top:16px;border-top:1px solid var(--border);padding-top:12px">' +
|
|
289
|
+
'<summary style="cursor:pointer;font-size:13px;color:var(--blue);user-select:none">Show experimental flags ' +
|
|
290
|
+
'<span style="font-size:10px;color:var(--muted)">(' + featuresList.length + ' registered)</span>' +
|
|
291
|
+
'</summary>' +
|
|
292
|
+
'<div style="font-size:10px;color:var(--muted);margin:8px 0 10px">In-progress UX or behavior gates. Toggles persist immediately. Registry: <code>engine/features.js</code>. Env override: <code>MINIONS_FEATURE_<NAME>=1</code>.</div>' +
|
|
293
|
+
(featuresList.length === 0
|
|
294
|
+
? '<div style="font-size:11px;color:var(--muted);padding:12px;border:1px dashed var(--border);border-radius:4px;text-align:center">No experimental features registered. Add entries to <code>engine/features.js</code> to gate new work.</div>'
|
|
295
|
+
: '<div style="display:flex;flex-direction:column;gap:8px">' +
|
|
296
|
+
featuresList.map(function(f) {
|
|
297
|
+
const checked = f.enabled ? ' checked' : '';
|
|
298
|
+
const expiredBadge = f.expired
|
|
299
|
+
? ' <span style="font-size:9px;padding:1px 5px;background:rgba(220,80,80,0.15);color:var(--red);border-radius:3px;margin-left:4px">EXPIRED</span>'
|
|
300
|
+
: '';
|
|
301
|
+
const meta = [];
|
|
302
|
+
if (f.addedIn) meta.push('added in ' + escHtml(f.addedIn));
|
|
303
|
+
if (f.expires) meta.push('expires ' + escHtml(f.expires));
|
|
304
|
+
meta.push(f.default ? 'default: on' : 'default: off');
|
|
305
|
+
return '<label data-feature-id="' + escHtml(f.id) + '" style="display:flex;align-items:flex-start;gap:8px;padding:8px;border:1px solid var(--border);border-radius:4px;cursor:pointer">' +
|
|
306
|
+
'<input type="checkbox" data-feature-toggle="' + escHtml(f.id) + '"' + checked + ' style="margin-top:3px;cursor:pointer">' +
|
|
307
|
+
'<div style="flex:1">' +
|
|
308
|
+
'<div style="font-size:11px;font-weight:600;color:var(--text)">' + escHtml(f.id) + expiredBadge + '</div>' +
|
|
309
|
+
(f.description ? '<div style="font-size:10px;color:var(--muted);margin-top:2px">' + escHtml(f.description) + '</div>' : '') +
|
|
310
|
+
'<div style="font-size:9px;color:var(--muted);margin-top:3px">' + meta.join(' · ') + '</div>' +
|
|
311
|
+
'</div>' +
|
|
312
|
+
'</label>';
|
|
313
|
+
}).join('') +
|
|
314
|
+
'</div>') +
|
|
315
|
+
'</details>' +
|
|
316
|
+
|
|
279
317
|
'</div>';
|
|
280
318
|
|
|
281
319
|
document.getElementById('modal-title').textContent = 'Settings';
|
|
@@ -312,6 +350,34 @@ async function openSettings() {
|
|
|
312
350
|
// 3. On defaultCli change → re-fetch models so the input never shows stale list.
|
|
313
351
|
// The same pattern wires ccCli → ccModel; ccCli inherits defaultCli when unset.
|
|
314
352
|
initRuntimeFleetUI(e, agents);
|
|
353
|
+
|
|
354
|
+
document.querySelectorAll('input[data-feature-toggle]').forEach(function(input) {
|
|
355
|
+
input.addEventListener('change', async function() {
|
|
356
|
+
const id = input.getAttribute('data-feature-toggle');
|
|
357
|
+
const enabled = input.checked;
|
|
358
|
+
if (window.MinionsFeatures && typeof window.MinionsFeatures._setLocal === 'function') {
|
|
359
|
+
window.MinionsFeatures._setLocal(id, enabled);
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
const r = await fetch('/api/features/toggle', {
|
|
363
|
+
method: 'POST',
|
|
364
|
+
headers: { 'Content-Type': 'application/json' },
|
|
365
|
+
body: JSON.stringify({ id, enabled }),
|
|
366
|
+
});
|
|
367
|
+
if (!r.ok) {
|
|
368
|
+
const j = await r.json().catch(() => ({}));
|
|
369
|
+
throw new Error(j.error || ('HTTP ' + r.status));
|
|
370
|
+
}
|
|
371
|
+
showToast('cmd-toast', 'Feature "' + id + '" ' + (enabled ? 'enabled' : 'disabled'), true);
|
|
372
|
+
} catch (err) {
|
|
373
|
+
input.checked = !enabled;
|
|
374
|
+
if (window.MinionsFeatures && typeof window.MinionsFeatures._setLocal === 'function') {
|
|
375
|
+
window.MinionsFeatures._setLocal(id, !enabled);
|
|
376
|
+
}
|
|
377
|
+
showToast('cmd-toast', 'Failed to toggle "' + id + '": ' + err.message, false);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
});
|
|
315
381
|
}
|
|
316
382
|
|
|
317
383
|
async function initRuntimeFleetUI(engineCfg, agentsCfg) {
|
package/dashboard-build.js
CHANGED
|
@@ -29,7 +29,7 @@ function buildDashboardHtml() {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const jsFiles = [
|
|
32
|
-
'utils', 'state', 'detail-panel', 'live-stream',
|
|
32
|
+
'utils', 'state', 'features-client', 'detail-panel', 'live-stream',
|
|
33
33
|
'render-agents', 'render-dispatch', 'render-work-items', 'render-prd',
|
|
34
34
|
'render-prs', 'render-plans', 'render-inbox', 'render-kb', 'render-skills',
|
|
35
35
|
'render-other', 'render-schedules', 'render-watches', 'render-pipelines', 'render-meetings', 'render-pinned',
|
package/dashboard.js
CHANGED
|
@@ -30,6 +30,7 @@ const playbook = require('./engine/playbook');
|
|
|
30
30
|
const dispatchMod = require('./engine/dispatch');
|
|
31
31
|
const steering = require('./engine/steering');
|
|
32
32
|
const projectDiscovery = require('./engine/project-discovery');
|
|
33
|
+
const features = require('./engine/features');
|
|
33
34
|
const os = require('os');
|
|
34
35
|
|
|
35
36
|
const { safeRead, safeReadDir, safeWrite, safeJson, safeJsonObj, safeJsonArr, safeUnlink, mutateJsonFileLocked, mutateControl, mutateCooldowns, mutateWorkItems, getProjects: _getProjects, DONE_STATUSES, WI_STATUS, WORK_TYPE, reopenWorkItem } = shared;
|
|
@@ -448,7 +449,7 @@ function buildDashboardHtml() {
|
|
|
448
449
|
|
|
449
450
|
// Assemble JS modules (order matters: utils → state → renderers → commands → refresh)
|
|
450
451
|
const jsFiles = [
|
|
451
|
-
'utils', 'state', 'render-utils', 'detail-panel', 'live-stream',
|
|
452
|
+
'utils', 'state', 'features-client', 'render-utils', 'detail-panel', 'live-stream',
|
|
452
453
|
'render-agents', 'render-dispatch', 'render-work-items', 'render-prd',
|
|
453
454
|
'render-prs', 'render-plans', 'render-inbox', 'render-kb', 'render-skills',
|
|
454
455
|
'render-other', 'render-schedules', 'render-watches', 'render-pipelines', 'render-meetings', 'render-pinned',
|
|
@@ -461,10 +462,23 @@ function buildDashboardHtml() {
|
|
|
461
462
|
jsHtml += `\n// ─── ${f}.js ────────────────────────────────────────\n${content}\n`;
|
|
462
463
|
}
|
|
463
464
|
|
|
465
|
+
// Snapshot feature-flag state at build time so the client renders synchronously
|
|
466
|
+
// without an extra /api/features round-trip. Helper itself lives in
|
|
467
|
+
// dashboard/js/features-client.js (auto-concatenated below).
|
|
468
|
+
let featuresBoot = { flags: {}, defaults: {} };
|
|
469
|
+
try {
|
|
470
|
+
for (const f of features.listFeatures(queries.getConfig())) {
|
|
471
|
+
featuresBoot.flags[f.id] = f.enabled;
|
|
472
|
+
featuresBoot.defaults[f.id] = f.default;
|
|
473
|
+
}
|
|
474
|
+
} catch { /* registry empty or config unreadable — ship empty boot state */ }
|
|
475
|
+
|
|
476
|
+
const featuresBootstrap = `window.MINIONS_FEATURES = ${JSON.stringify(featuresBoot)};\n`;
|
|
477
|
+
|
|
464
478
|
return layout
|
|
465
479
|
.replace('/* __CSS__ */', () => css)
|
|
466
480
|
.replace('<!-- __PAGES__ -->', () => pageHtml)
|
|
467
|
-
.replace('/* __JS__ */', () => `window.__MINIONS_HOME = ${JSON.stringify(os.homedir())};\n${jsHtml}`);
|
|
481
|
+
.replace('/* __JS__ */', () => `window.__MINIONS_HOME = ${JSON.stringify(os.homedir())};\n${featuresBootstrap}${jsHtml}`);
|
|
468
482
|
}
|
|
469
483
|
|
|
470
484
|
let HTML_RAW = buildDashboardHtml();
|
|
@@ -918,9 +932,14 @@ setInterval(() => {
|
|
|
918
932
|
|
|
919
933
|
// ── Command Center: session state + helpers ─────────────────────────────────
|
|
920
934
|
|
|
921
|
-
//
|
|
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.
|
|
922
942
|
const CC_SESSION_MAX_TURNS = shared.ENGINE_DEFAULTS.ccMaxTurns;
|
|
923
|
-
const CC_SESSION_TTL_MS = shared.ENGINE_DEFAULTS.ccSessionTtlMs;
|
|
924
943
|
let ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
|
|
925
944
|
const ccInFlightTabs = new Map(); // tabId → timestamp — per-tab in-flight tracking for parallel CC requests
|
|
926
945
|
const ccInFlightAborts = new Map(); // tabId → abortFn — lets a new request kill the stale LLM
|
|
@@ -1056,18 +1075,17 @@ function _ccTabIsInFlight(tabId) {
|
|
|
1056
1075
|
|
|
1057
1076
|
function ccSessionValid() {
|
|
1058
1077
|
if (!ccSession.sessionId) return false;
|
|
1059
|
-
// Invalidate session if system prompt changed (e.g. after code update + restart)
|
|
1078
|
+
// Invalidate session if system prompt changed (e.g. after code update + restart).
|
|
1079
|
+
// This is correctness-driven, not expiration-driven: a session created against
|
|
1080
|
+
// a stale system prompt would carry the old persona/rules into resume turns.
|
|
1060
1081
|
if (ccSession._promptHash && ccSession._promptHash !== _ccPromptHash) {
|
|
1061
1082
|
console.log('[CC] System prompt changed — invalidating stale session');
|
|
1062
1083
|
ccSession = { sessionId: null, createdAt: null, lastActiveAt: null, turnCount: 0 };
|
|
1063
1084
|
return false;
|
|
1064
1085
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
return false;
|
|
1069
|
-
}
|
|
1070
|
-
return ccSession.turnCount < CC_SESSION_MAX_TURNS;
|
|
1086
|
+
// No TTL or turn-count cap — CC chat sessions live until the user explicitly
|
|
1087
|
+
// closes the tab (which calls DELETE /api/cc-sessions/:id).
|
|
1088
|
+
return true;
|
|
1071
1089
|
}
|
|
1072
1090
|
|
|
1073
1091
|
// Static system prompt — baked into session on creation, never changes
|
|
@@ -1106,11 +1124,13 @@ function _sessionExpired(lastActiveAt, ttlMs) {
|
|
|
1106
1124
|
return Date.now() - at > ttlMs;
|
|
1107
1125
|
}
|
|
1108
1126
|
|
|
1127
|
+
// CC tab sessions never auto-expire — only invalid records (missing id /
|
|
1128
|
+
// sessionId) and prompt-hash mismatches are filtered out. Tabs are removed
|
|
1129
|
+
// from disk only when the user explicitly closes them via DELETE
|
|
1130
|
+
// /api/cc-sessions/:id.
|
|
1109
1131
|
function _filterCcTabSessions(sessions) {
|
|
1110
1132
|
return (Array.isArray(sessions) ? sessions : []).filter(s =>
|
|
1111
1133
|
s && s.id && s.sessionId &&
|
|
1112
|
-
(s.turnCount || 0) < CC_SESSION_MAX_TURNS &&
|
|
1113
|
-
!_sessionExpired(s.lastActiveAt || s.createdAt, CC_SESSION_TTL_MS) &&
|
|
1114
1134
|
(!s._promptHash || s._promptHash === _ccPromptHash)
|
|
1115
1135
|
);
|
|
1116
1136
|
}
|
|
@@ -1121,10 +1141,34 @@ function _readCcTabSessions({ prune = true } = {}) {
|
|
|
1121
1141
|
return sessions;
|
|
1122
1142
|
}
|
|
1123
1143
|
|
|
1124
|
-
|
|
1144
|
+
const CC_CARRYOVER_MAX_TURNS = 20;
|
|
1145
|
+
const CC_CARRYOVER_PER_MSG_CHARS = 2000;
|
|
1146
|
+
|
|
1147
|
+
function _buildTranscriptCarryover(transcript, { previousRuntime } = {}) {
|
|
1148
|
+
if (!Array.isArray(transcript) || transcript.length === 0) return '';
|
|
1149
|
+
const filtered = transcript.filter(m => m && (m.role === 'user' || m.role === 'assistant') && typeof m.text === 'string' && m.text.trim());
|
|
1150
|
+
if (filtered.length === 0) return '';
|
|
1151
|
+
const recent = filtered.slice(-CC_CARRYOVER_MAX_TURNS);
|
|
1152
|
+
const truncated = filtered.length > recent.length;
|
|
1153
|
+
const header = previousRuntime
|
|
1154
|
+
? `--- Previous conversation (carried over from ${previousRuntime} session) ---`
|
|
1155
|
+
: `--- Previous conversation (carried over) ---`;
|
|
1156
|
+
const lines = recent.map(m => {
|
|
1157
|
+
const who = m.role === 'user' ? 'User' : 'Assistant';
|
|
1158
|
+
let text = m.text.trim();
|
|
1159
|
+
if (text.length > CC_CARRYOVER_PER_MSG_CHARS) text = text.slice(0, CC_CARRYOVER_PER_MSG_CHARS) + '… [truncated]';
|
|
1160
|
+
return `${who}: ${text}`;
|
|
1161
|
+
});
|
|
1162
|
+
const truncationNote = truncated ? `[earlier messages truncated — showing last ${recent.length} of ${filtered.length}]\n\n` : '';
|
|
1163
|
+
return `${header}\n\n${truncationNote}${lines.join('\n\n')}\n\n--- Current message follows ---`;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// Load persisted CC session on startup. CC chat sessions are non-expiring;
|
|
1167
|
+
// only restore-time validity checks here are sessionId presence (anything
|
|
1168
|
+
// else would auto-expire the user's chat without their consent).
|
|
1125
1169
|
try {
|
|
1126
1170
|
const saved = safeJson(path.join(ENGINE_DIR, 'cc-session.json'));
|
|
1127
|
-
if (saved && saved.sessionId
|
|
1171
|
+
if (saved && saved.sessionId) ccSession = saved;
|
|
1128
1172
|
} catch { /* optional */ }
|
|
1129
1173
|
|
|
1130
1174
|
let _preambleCache = null;
|
|
@@ -5513,7 +5557,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5513
5557
|
// Session management — per-tab: use sessionId from request, don't mutate global ccSession
|
|
5514
5558
|
let tabSessionId = body.sessionId || null;
|
|
5515
5559
|
let sessionReset = false;
|
|
5516
|
-
|
|
5560
|
+
let sessionResetReason = null;
|
|
5561
|
+
let previousRuntime = null;
|
|
5562
|
+
const currentRuntime = shared.resolveCcCli(CONFIG.engine);
|
|
5563
|
+
// If system prompt or runtime changed since this session was created, force a fresh session
|
|
5517
5564
|
if (tabSessionId) {
|
|
5518
5565
|
const sessions = _readCcTabSessions();
|
|
5519
5566
|
const tabEntry = sessions.find(s => s.id === (body.tabId || 'default'));
|
|
@@ -5523,12 +5570,19 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5523
5570
|
} else if (tabEntry._promptHash && tabEntry._promptHash !== _ccPromptHash) {
|
|
5524
5571
|
tabSessionId = null;
|
|
5525
5572
|
sessionReset = true;
|
|
5573
|
+
sessionResetReason = 'promptChanged';
|
|
5574
|
+
} else if (tabEntry.runtime && tabEntry.runtime !== currentRuntime) {
|
|
5575
|
+
tabSessionId = null;
|
|
5576
|
+
sessionReset = true;
|
|
5577
|
+
sessionResetReason = 'runtimeChanged';
|
|
5578
|
+
previousRuntime = tabEntry.runtime;
|
|
5526
5579
|
}
|
|
5527
5580
|
}
|
|
5528
5581
|
const wasResume = !!tabSessionId;
|
|
5529
5582
|
const sessionId = tabSessionId || null;
|
|
5530
5583
|
const preamble = wasResume ? '' : buildCCStatePreamble();
|
|
5531
|
-
const
|
|
5584
|
+
const carryover = sessionReset ? _buildTranscriptCarryover(body.transcript, { previousRuntime }) : '';
|
|
5585
|
+
const prompt = (preamble ? preamble + '\n\n---\n\n' : '') + (carryover ? carryover + '\n\n---\n\n' : '') + body.message;
|
|
5532
5586
|
|
|
5533
5587
|
const { trackEngineUsage: trackUsage } = require('./engine/llm');
|
|
5534
5588
|
const streamModel = CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel;
|
|
@@ -5606,8 +5660,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5606
5660
|
existing.turnCount = sessionReset ? 1 : (existing.turnCount || 0) + 1;
|
|
5607
5661
|
existing.preview = preview;
|
|
5608
5662
|
existing._promptHash = _ccPromptHash;
|
|
5663
|
+
existing.runtime = currentRuntime;
|
|
5609
5664
|
} else {
|
|
5610
|
-
sessions.push({ id: _persistTabId, title: (body.message || 'New chat').slice(0, 40), sessionId: responseSessionId, createdAt: new Date(now).toISOString(), lastActiveAt: new Date(now).toISOString(), turnCount: 1, preview, _promptHash: _ccPromptHash });
|
|
5665
|
+
sessions.push({ id: _persistTabId, title: (body.message || 'New chat').slice(0, 40), sessionId: responseSessionId, createdAt: new Date(now).toISOString(), lastActiveAt: new Date(now).toISOString(), turnCount: 1, preview, _promptHash: _ccPromptHash, runtime: currentRuntime });
|
|
5611
5666
|
}
|
|
5612
5667
|
safeWrite(CC_SESSIONS_PATH, sessions);
|
|
5613
5668
|
} catch { /* non-critical */ }
|
|
@@ -5630,7 +5685,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5630
5685
|
// Issue #1834: surface action JSON parse failures so the UI can warn
|
|
5631
5686
|
// instead of silently dropping. Client renders this as a small notice.
|
|
5632
5687
|
if (_actionParseError) donePayload.actionParseError = _actionParseError;
|
|
5633
|
-
if (sessionReset)
|
|
5688
|
+
if (sessionReset) {
|
|
5689
|
+
donePayload.sessionReset = true;
|
|
5690
|
+
if (sessionResetReason) donePayload.sessionResetReason = sessionResetReason;
|
|
5691
|
+
if (previousRuntime) donePayload.previousRuntime = previousRuntime;
|
|
5692
|
+
donePayload.currentRuntime = currentRuntime;
|
|
5693
|
+
}
|
|
5634
5694
|
liveState.donePayload = donePayload;
|
|
5635
5695
|
if (liveState.writer) liveState.writer(donePayload);
|
|
5636
5696
|
|
|
@@ -6253,6 +6313,33 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6253
6313
|
} catch (e) { return jsonReply(res, e.statusCode || 500, { error: e.message }); }
|
|
6254
6314
|
}
|
|
6255
6315
|
|
|
6316
|
+
async function handleFeaturesList(req, res) {
|
|
6317
|
+
try {
|
|
6318
|
+
return jsonReply(res, 200, { features: features.listFeatures(CONFIG) });
|
|
6319
|
+
} catch (e) { return jsonReply(res, e.statusCode || 500, { error: e.message }); }
|
|
6320
|
+
}
|
|
6321
|
+
|
|
6322
|
+
async function handleFeaturesToggle(req, res) {
|
|
6323
|
+
try {
|
|
6324
|
+
const body = await readBody(req);
|
|
6325
|
+
const id = body && typeof body.id === 'string' ? body.id.trim() : '';
|
|
6326
|
+
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
6327
|
+
if (!features.hasFeature(id)) {
|
|
6328
|
+
return jsonReply(res, 404, { error: `Unknown feature flag: "${id}". Register it in engine/features.js.` });
|
|
6329
|
+
}
|
|
6330
|
+
const enabled = body.enabled === true;
|
|
6331
|
+
const configPath = path.join(MINIONS_DIR, 'config.json');
|
|
6332
|
+
mutateJsonFileLocked(configPath, (cfg) => {
|
|
6333
|
+
if (!cfg.features || typeof cfg.features !== 'object') cfg.features = {};
|
|
6334
|
+
cfg.features[id] = enabled;
|
|
6335
|
+
return cfg;
|
|
6336
|
+
});
|
|
6337
|
+
reloadConfig();
|
|
6338
|
+
invalidateStatusCache();
|
|
6339
|
+
return jsonReply(res, 200, { ok: true, id, enabled });
|
|
6340
|
+
} catch (e) { return jsonReply(res, e.statusCode || 500, { error: e.message }); }
|
|
6341
|
+
}
|
|
6342
|
+
|
|
6256
6343
|
async function handleHealth(req, res) {
|
|
6257
6344
|
const engine = getEngineState();
|
|
6258
6345
|
const agents = getAgents();
|
|
@@ -7043,6 +7130,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7043
7130
|
{ method: 'POST', path: '/api/settings/routing', desc: 'Update routing.md', params: 'content', handler: handleSettingsRouting },
|
|
7044
7131
|
{ method: 'POST', path: '/api/settings/reset', desc: 'Reset engine + claude + agent settings to defaults', handler: handleSettingsReset },
|
|
7045
7132
|
|
|
7133
|
+
// Feature flags (experimental / in-progress UX gates — see engine/features.js)
|
|
7134
|
+
{ method: 'GET', path: '/api/features', desc: 'List registered feature flags with current enabled state', handler: handleFeaturesList },
|
|
7135
|
+
{ method: 'POST', path: '/api/features/toggle', desc: 'Enable/disable a registered feature flag', params: 'id, enabled', handler: handleFeaturesToggle },
|
|
7136
|
+
|
|
7046
7137
|
// Teams Bot Framework webhook
|
|
7047
7138
|
{ method: 'POST', path: '/api/bot', desc: 'Bot Framework webhook for Teams integration', handler: handleTeamsBot },
|
|
7048
7139
|
];
|
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;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine/features.js — Feature flag registry. Recipe in CLAUDE.md → "Feature Flags".
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Entry shape: id → { description, default: bool, addedIn?: version, expires?: ISO-date }
|
|
6
|
+
const FEATURES = {
|
|
7
|
+
// Example:
|
|
8
|
+
// 'ux-sidebar-v2': { description: '…', default: false, addedIn: '0.1.1738', expires: '2026-06-01' },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const ENV_TRUTHY = new Set(['1', 'true', 'on', 'yes']);
|
|
12
|
+
const ENV_FALSY = new Set(['0', 'false', 'off', 'no', '']);
|
|
13
|
+
|
|
14
|
+
function envKey(id) {
|
|
15
|
+
return 'MINIONS_FEATURE_' + String(id).toUpperCase().replace(/-/g, '_');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readEnvOverride(id) {
|
|
19
|
+
const raw = process.env[envKey(id)];
|
|
20
|
+
if (raw === undefined) return undefined;
|
|
21
|
+
const v = String(raw).trim().toLowerCase();
|
|
22
|
+
if (ENV_TRUTHY.has(v)) return true;
|
|
23
|
+
if (ENV_FALSY.has(v)) return false;
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isFeatureOn(id, config, registry = FEATURES) {
|
|
28
|
+
if (!Object.prototype.hasOwnProperty.call(registry, id)) {
|
|
29
|
+
throw new Error(`Unknown feature flag: "${id}". Register it in engine/features.js.`);
|
|
30
|
+
}
|
|
31
|
+
const env = readEnvOverride(id);
|
|
32
|
+
if (env !== undefined) return env;
|
|
33
|
+
const fromConfig = config && config.features && config.features[id];
|
|
34
|
+
if (typeof fromConfig === 'boolean') return fromConfig;
|
|
35
|
+
return registry[id].default === true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function listFeatures(config, registry = FEATURES) {
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
return Object.entries(registry).map(([id, meta]) => {
|
|
41
|
+
const expiresAt = meta.expires ? Date.parse(meta.expires) : NaN;
|
|
42
|
+
return {
|
|
43
|
+
id,
|
|
44
|
+
description: meta.description || '',
|
|
45
|
+
default: meta.default === true,
|
|
46
|
+
enabled: isFeatureOn(id, config, registry),
|
|
47
|
+
addedIn: meta.addedIn || null,
|
|
48
|
+
expires: meta.expires || null,
|
|
49
|
+
expired: expiresAt < now,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hasFeature(id, registry = FEATURES) {
|
|
55
|
+
return Object.prototype.hasOwnProperty.call(registry, id);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { FEATURES, isFeatureOn, listFeatures, hasFeature };
|
package/engine/llm.js
CHANGED
|
@@ -23,6 +23,7 @@ const { resolveRuntime } = require('./runtimes');
|
|
|
23
23
|
const MINIONS_DIR = shared.MINIONS_DIR;
|
|
24
24
|
const ENGINE_DIR = path.join(MINIONS_DIR, 'engine');
|
|
25
25
|
const COPILOT_TASK_COMPLETE_GRACE_MS = 3000;
|
|
26
|
+
const LLM_EXIT_SETTLE_GRACE_MS = 1000;
|
|
26
27
|
const MISSING_RUNTIME_EXIT_CODE = 78;
|
|
27
28
|
|
|
28
29
|
// ─── Engine-Usage Metrics ────────────────────────────────────────────────────
|
|
@@ -609,6 +610,8 @@ function callLLM(promptText, sysPromptText, opts = {}) {
|
|
|
609
610
|
maxBudget, bare, fallbackModel,
|
|
610
611
|
...runtimeFeatureOpts,
|
|
611
612
|
});
|
|
613
|
+
let settled = false;
|
|
614
|
+
let exitSettleTimer = null;
|
|
612
615
|
let taskCompleteTimer = null;
|
|
613
616
|
const scheduleTaskCompleteClose = () => {
|
|
614
617
|
if (taskCompleteTimer) return;
|
|
@@ -635,11 +638,16 @@ function callLLM(promptText, sysPromptText, opts = {}) {
|
|
|
635
638
|
|
|
636
639
|
const timer = setTimeout(() => { shared.killImmediate(proc); }, timeout);
|
|
637
640
|
|
|
638
|
-
|
|
641
|
+
function finish(code) {
|
|
642
|
+
if (settled) return;
|
|
643
|
+
settled = true;
|
|
639
644
|
clearTimeout(timer);
|
|
645
|
+
if (exitSettleTimer) clearTimeout(exitSettleTimer);
|
|
640
646
|
clearTaskCompleteTimer();
|
|
641
647
|
for (const f of cleanupFiles) safeUnlink(f);
|
|
642
648
|
const parsed = acc.finalize();
|
|
649
|
+
try { proc.stdout?.destroy(); } catch {}
|
|
650
|
+
try { proc.stderr?.destroy(); } catch {}
|
|
643
651
|
const durationMs = Date.now() - _startMs;
|
|
644
652
|
const usage = parsed.usage ? { ...parsed.usage, durationMs } : { durationMs };
|
|
645
653
|
// parseError lets the adapter classify obvious failure modes (auth /
|
|
@@ -659,10 +667,19 @@ function callLLM(promptText, sysPromptText, opts = {}) {
|
|
|
659
667
|
runtime: runtime.name,
|
|
660
668
|
errorClass: errInfo.code,
|
|
661
669
|
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
proc.on('close', finish);
|
|
673
|
+
proc.on('exit', (code) => {
|
|
674
|
+
if (settled) return;
|
|
675
|
+
exitSettleTimer = setTimeout(() => finish(code), LLM_EXIT_SETTLE_GRACE_MS);
|
|
662
676
|
});
|
|
663
677
|
|
|
664
678
|
proc.on('error', (err) => {
|
|
679
|
+
if (settled) return;
|
|
680
|
+
settled = true;
|
|
665
681
|
clearTimeout(timer);
|
|
682
|
+
if (exitSettleTimer) clearTimeout(exitSettleTimer);
|
|
666
683
|
clearTaskCompleteTimer();
|
|
667
684
|
for (const f of cleanupFiles) safeUnlink(f);
|
|
668
685
|
shared.log('error', `LLM spawn error (${label}): ${err.message}`);
|
|
@@ -709,6 +726,8 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
|
|
|
709
726
|
maxBudget, bare, fallbackModel,
|
|
710
727
|
...runtimeFeatureOpts,
|
|
711
728
|
});
|
|
729
|
+
let settled = false;
|
|
730
|
+
let exitSettleTimer = null;
|
|
712
731
|
let taskCompleteTimer = null;
|
|
713
732
|
const scheduleTaskCompleteClose = () => {
|
|
714
733
|
if (taskCompleteTimer) return;
|
|
@@ -738,11 +757,16 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
|
|
|
738
757
|
|
|
739
758
|
const timer = setTimeout(() => { shared.killImmediate(proc); }, timeout);
|
|
740
759
|
|
|
741
|
-
|
|
760
|
+
function finish(code) {
|
|
761
|
+
if (settled) return;
|
|
762
|
+
settled = true;
|
|
742
763
|
clearTimeout(timer);
|
|
764
|
+
if (exitSettleTimer) clearTimeout(exitSettleTimer);
|
|
743
765
|
clearTaskCompleteTimer();
|
|
744
766
|
for (const f of cleanupFiles) safeUnlink(f);
|
|
745
767
|
const parsed = acc.finalize();
|
|
768
|
+
try { proc.stdout?.destroy(); } catch {}
|
|
769
|
+
try { proc.stderr?.destroy(); } catch {}
|
|
746
770
|
const durationMs = Date.now() - _startMs;
|
|
747
771
|
const usage = parsed.usage ? { ...parsed.usage, durationMs } : { durationMs };
|
|
748
772
|
const errInfo = code !== 0
|
|
@@ -759,10 +783,22 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
|
|
|
759
783
|
runtime: runtime.name,
|
|
760
784
|
errorClass: errInfo.code,
|
|
761
785
|
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
proc.on('close', finish);
|
|
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.
|
|
793
|
+
if (settled) return;
|
|
794
|
+
exitSettleTimer = setTimeout(() => finish(code), LLM_EXIT_SETTLE_GRACE_MS);
|
|
762
795
|
});
|
|
763
796
|
|
|
764
797
|
proc.on('error', (err) => {
|
|
798
|
+
if (settled) return;
|
|
799
|
+
settled = true;
|
|
765
800
|
clearTimeout(timer);
|
|
801
|
+
if (exitSettleTimer) clearTimeout(exitSettleTimer);
|
|
766
802
|
clearTaskCompleteTimer();
|
|
767
803
|
for (const f of cleanupFiles) safeUnlink(f);
|
|
768
804
|
shared.log('error', `LLM-stream spawn error (${label}): ${err.message}`);
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1739",
|
|
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"
|