@yemi33/minions 0.1.518 → 0.1.519

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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.519 (2026-04-07)
4
+
5
+ ### Other
6
+ - perf: fix 5 performance bottlenecks in engine and dashboard
7
+
3
8
  ## 0.1.518 (2026-04-07)
4
9
 
5
10
  ### Fixes
@@ -3,24 +3,26 @@
3
3
  let livePollingInterval = null;
4
4
  let liveEventSource = null;
5
5
  let _steerInFlight = false;
6
+ let _lastRenderedText = '';
6
7
 
7
8
  function renderLiveChatMessage(raw) {
8
9
  const el = document.getElementById('live-messages');
9
10
  if (!el) return;
11
+ const fragments = [];
10
12
 
11
13
  function renderJsonObj(obj) {
12
14
  if (obj.type === 'assistant' && obj.message?.content) {
13
15
  for (const block of obj.message.content) {
14
16
  if (block.type === 'thinking') {
15
- el.innerHTML += '<div style="font-size:10px;color:var(--muted);padding:2px 8px;font-style:italic">\u{1F4AD} Thinking...</div>';
17
+ fragments.push('<div style="font-size:10px;color:var(--muted);padding:2px 8px;font-style:italic">\u{1F4AD} Thinking...</div>');
16
18
  }
17
19
  if (block.type === 'text' && block.text) {
18
- el.innerHTML += '<div style="background:var(--surface2);padding:8px 12px;border-radius:12px 12px 12px 2px;max-width:90%;margin:4px 0;font-size:12px;word-break:break-word">' + renderMd(block.text) + '</div>';
20
+ fragments.push('<div style="background:var(--surface2);padding:8px 12px;border-radius:12px 12px 12px 2px;max-width:90%;margin:4px 0;font-size:12px;word-break:break-word">' + renderMd(block.text) + '</div>');
19
21
  }
20
22
  if (block.type === 'tool_use') {
21
- el.innerHTML += '<div style="background:var(--surface);border:1px solid var(--border);padding:4px 8px;border-radius:4px;margin:2px 0;font-size:10px;color:var(--muted);cursor:pointer" onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display===\'none\'?\'block\':\'none\'">' +
23
+ fragments.push('<div style="background:var(--surface);border:1px solid var(--border);padding:4px 8px;border-radius:4px;margin:2px 0;font-size:10px;color:var(--muted);cursor:pointer" onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display===\'none\'?\'block\':\'none\'">' +
22
24
  '\u{1F527} ' + escHtml(block.name || 'tool') + '</div>' +
23
- '<div style="display:none;background:var(--bg);padding:4px 8px;border-radius:4px;margin:0 0 4px;font-size:10px;font-family:monospace;white-space:pre-wrap;max-height:200px;overflow-y:auto;color:var(--muted)">' + escHtml(JSON.stringify(block.input || {}, null, 2).slice(0, 500)) + '</div>';
25
+ '<div style="display:none;background:var(--bg);padding:4px 8px;border-radius:4px;margin:0 0 4px;font-size:10px;font-family:monospace;white-space:pre-wrap;max-height:200px;overflow-y:auto;color:var(--muted)">' + escHtml(JSON.stringify(block.input || {}, null, 2).slice(0, 500)) + '</div>');
24
26
  }
25
27
  }
26
28
  }
@@ -28,11 +30,11 @@ function renderLiveChatMessage(raw) {
28
30
  const content = obj.message?.content?.[0]?.content || obj.content || '';
29
31
  const text = typeof content === 'string' ? content : JSON.stringify(content);
30
32
  if (text.length > 10) {
31
- el.innerHTML += '<div style="background:var(--bg);border-left:2px solid var(--border);padding:2px 8px;margin:0 0 2px 16px;font-size:9px;font-family:monospace;color:var(--muted);max-height:100px;overflow-y:auto;white-space:pre-wrap;cursor:pointer" onclick="this.style.maxHeight=this.style.maxHeight===\'100px\'?\'none\':\'100px\'">' + escHtml(text.slice(0, 1000)) + (text.length > 1000 ? '...' : '') + '</div>';
33
+ fragments.push('<div style="background:var(--bg);border-left:2px solid var(--border);padding:2px 8px;margin:0 0 2px 16px;font-size:9px;font-family:monospace;color:var(--muted);max-height:100px;overflow-y:auto;white-space:pre-wrap;cursor:pointer" onclick="this.style.maxHeight=this.style.maxHeight===\'100px\'?\'none\':\'100px\'">' + escHtml(text.slice(0, 1000)) + (text.length > 1000 ? '...' : '') + '</div>');
32
34
  }
33
35
  }
34
36
  if (obj.type === 'result') {
35
- el.innerHTML += '<div style="background:rgba(63,185,80,0.1);border:1px solid var(--green);padding:8px 12px;border-radius:8px;margin:8px 0;font-size:12px;color:var(--green)">\u2713 Task complete</div>';
37
+ fragments.push('<div style="background:rgba(63,185,80,0.1);border:1px solid var(--green);padding:8px 12px;border-radius:8px;margin:8px 0;font-size:12px;color:var(--green)">\u2713 Task complete</div>');
36
38
  }
37
39
  }
38
40
 
@@ -41,47 +43,36 @@ function renderLiveChatMessage(raw) {
41
43
  const trimmed = line.trim();
42
44
  if (!trimmed || trimmed.startsWith('#')) continue;
43
45
 
44
- // Human steering messages
45
46
  if (trimmed.startsWith('[human-steering]')) {
46
47
  const msg = trimmed.replace('[human-steering] ', '');
47
- el.innerHTML += '<div style="align-self:flex-end;background:var(--blue);color:#fff;padding:6px 12px;border-radius:12px 12px 2px 12px;max-width:80%;margin:4px 0;font-size:12px">' + escHtml(msg) +
48
- '<div style="font-size:9px;opacity:0.7;margin-top:2px">\u2713 Queued</div></div>';
48
+ fragments.push('<div style="align-self:flex-end;background:var(--blue);color:#fff;padding:6px 12px;border-radius:12px 12px 2px 12px;max-width:80%;margin:4px 0;font-size:12px">' + escHtml(msg) +
49
+ '<div style="font-size:9px;opacity:0.7;margin-top:2px">\u2713 Queued</div></div>');
49
50
  continue;
50
51
  }
51
-
52
- // Heartbeat lines
53
- if (trimmed.startsWith('[heartbeat]')) {
54
- continue;
55
- }
56
-
57
- // Steering failure notices
52
+ if (trimmed.startsWith('[heartbeat]')) continue;
58
53
  if (trimmed.startsWith('[steering-failed]')) {
59
54
  const msg = trimmed.replace('[steering-failed] ', '');
60
- el.innerHTML += '<div style="background:rgba(248,81,73,0.1);border:1px solid var(--red);color:var(--red);padding:6px 12px;border-radius:8px;margin:4px 0;font-size:11px">\u26A0 ' + escHtml(msg) + '</div>';
55
+ fragments.push('<div style="background:rgba(248,81,73,0.1);border:1px solid var(--red);color:var(--red);padding:6px 12px;border-radius:8px;margin:4px 0;font-size:11px">\u26A0 ' + escHtml(msg) + '</div>');
61
56
  continue;
62
57
  }
63
-
64
- // JSON array format (--output-format json)
65
58
  if (trimmed.startsWith('[')) {
66
59
  try {
67
60
  const arr = JSON.parse(trimmed);
68
61
  if (Array.isArray(arr)) { for (const obj of arr) renderJsonObj(obj); continue; }
69
- } catch { /* fall through to raw text */ }
62
+ } catch { /* fall through */ }
70
63
  }
71
-
72
- // Single JSON object (--output-format stream-json)
73
64
  if (trimmed.startsWith('{')) {
74
65
  try { renderJsonObj(JSON.parse(trimmed)); continue; } catch { /* fall through */ }
75
66
  }
76
-
77
- // Fallback: raw text (stderr, non-JSON lines)
78
67
  if (trimmed.startsWith('[stderr]')) {
79
- el.innerHTML += '<div style="font-size:9px;color:var(--red);font-family:monospace;padding:1px 4px">' + escHtml(trimmed) + '</div>';
68
+ fragments.push('<div style="font-size:9px;color:var(--red);font-family:monospace;padding:1px 4px">' + escHtml(trimmed) + '</div>');
80
69
  } else {
81
- el.innerHTML += '<div style="font-size:10px;color:var(--muted);font-family:monospace;padding:1px 4px">' + escHtml(trimmed) + '</div>';
70
+ fragments.push('<div style="font-size:10px;color:var(--muted);font-family:monospace;padding:1px 4px">' + escHtml(trimmed) + '</div>');
82
71
  }
83
72
  }
84
73
 
74
+ if (fragments.length > 0) el.innerHTML += fragments.join('');
75
+
85
76
  // Auto-scroll
86
77
  if (el.scrollHeight - el.scrollTop - el.clientHeight < 150) {
87
78
  el.scrollTop = el.scrollHeight;
@@ -94,6 +85,7 @@ function startLiveStream(agentId) {
94
85
 
95
86
  const msgEl = document.getElementById('live-messages');
96
87
  if (msgEl) msgEl.innerHTML = '';
88
+ _lastRenderedText = '';
97
89
 
98
90
  // Use polling instead of SSE to avoid HTTP/1.1 connection exhaustion
99
91
  // (SSE holds a persistent connection, blocking CC and other API calls)
@@ -126,8 +118,14 @@ async function refreshLiveOutput() {
126
118
  const el = document.getElementById('live-messages');
127
119
  if (el) {
128
120
  const wasAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 150;
129
- el.innerHTML = '';
130
- renderLiveChatMessage(text);
121
+ // Incremental render: only parse new content if text is an extension of previous
122
+ if (_lastRenderedText && text.length > _lastRenderedText.length && text.startsWith(_lastRenderedText.slice(0, 200))) {
123
+ renderLiveChatMessage(text.slice(_lastRenderedText.length));
124
+ } else {
125
+ el.innerHTML = '';
126
+ renderLiveChatMessage(text);
127
+ }
128
+ _lastRenderedText = text;
131
129
  if (wasAtBottom) el.scrollTop = el.scrollHeight;
132
130
  }
133
131
  } catch (e) { console.error('live-stream reload:', e.message); }
@@ -25,6 +25,15 @@ function _detectPageChanges(data) {
25
25
  return changes;
26
26
  }
27
27
 
28
+ // Change detection — skip renders for sections that haven't changed since last refresh
29
+ const _sectionCache = {};
30
+ function _changed(key, value) {
31
+ var json = JSON.stringify(value);
32
+ if (_sectionCache[key] === json) return false;
33
+ _sectionCache[key] = json;
34
+ return true;
35
+ }
36
+
28
37
  function _processStatusUpdate(data) {
29
38
  // Detect fresh install — clear stale browser state if install ID changed
30
39
  if (data.installId) {
@@ -35,45 +44,43 @@ function _processStatusUpdate(data) {
35
44
  }
36
45
  localStorage.setItem('minions-install-id', data.installId);
37
46
  }
47
+ // Always update cheap elements
38
48
  document.getElementById('ts').textContent = new Date(data.timestamp).toLocaleTimeString();
39
49
  const engineState = (data.engine && data.engine.state) ? data.engine.state : 'stopped';
40
50
  document.getElementById('setup-banner').style.display = (!data.initialized && engineState !== 'stopped') ? 'block' : 'none';
41
- renderAgents(data.agents);
42
- renderPrdProgress(data.prdProgress);
43
- _cachePrdItems(data.prdProgress);
44
- renderInbox(data.inbox || []);
45
- cmdUpdateAgentList(data.agents);
46
- cmdUpdateProjectList(data.projects || []);
47
- renderNotes(data.notes);
48
- renderPrd(data.prd, data.prdProgress);
49
- // Auto-approve badge
50
51
  const autoEl = document.getElementById('auto-approve-badge');
51
52
  if (autoEl) autoEl.innerHTML = data.autoMode?.approvePlans
52
53
  ? '<span style="font-size:9px;font-weight:600;padding:1px 6px;border-radius:3px;background:rgba(63,185,80,0.15);color:var(--green);border:1px solid rgba(63,185,80,0.3)">AUTO-APPROVE</span>'
53
54
  : '';
54
- // Inbox consolidation threshold from config
55
55
  const threshEl = document.getElementById('inbox-threshold');
56
56
  if (threshEl && data.autoMode?.inboxThreshold) threshEl.textContent = data.autoMode.inboxThreshold;
57
- renderPrs(data.pullRequests || []);
58
- renderArchiveButtons(data.archivedPrds || []);
59
- renderEngineStatus(data.engine);
60
- renderVersionBanner(data.version);
61
- renderDispatch(data.dispatch);
57
+
58
+ // Render only changed sections
59
+ if (_changed('agents', data.agents)) { renderAgents(data.agents); cmdUpdateAgentList(data.agents); }
60
+ if (_changed('prdProgress', data.prdProgress)) { renderPrdProgress(data.prdProgress); _cachePrdItems(data.prdProgress); }
61
+ if (_changed('inbox', data.inbox)) renderInbox(data.inbox || []);
62
+ if (_changed('projects', data.projects)) { cmdUpdateProjectList(data.projects || []); renderProjects(data.projects || []); }
63
+ if (_changed('notes', data.notes)) renderNotes(data.notes);
64
+ if (_changed('prd', [data.prd, data.prdProgress])) renderPrd(data.prd, data.prdProgress);
65
+ if (_changed('prs', data.pullRequests)) renderPrs(data.pullRequests || []);
66
+ if (_changed('archivedPrds', data.archivedPrds)) renderArchiveButtons(data.archivedPrds || []);
67
+ if (_changed('engine', data.engine)) renderEngineStatus(data.engine);
68
+ if (_changed('version', data.version)) renderVersionBanner(data.version);
69
+ if (_changed('dispatch', data.dispatch)) renderDispatch(data.dispatch);
62
70
  window._lastDispatch = data.dispatch;
63
71
  window._lastWorkItems = data.workItems || [];
64
72
  window._lastStatus = data;
65
73
  prunePrdRequeueState(window._lastWorkItems);
66
- renderEngineLog(data.engineLog || []);
67
- renderProjects(data.projects || []);
68
- renderMetrics(data.metrics || {});
69
- renderWorkItems(data.workItems || []);
70
- renderSkills(data.skills || []);
71
- renderMcpServers(data.mcpServers || []);
72
- renderSchedules(data.schedules || []);
73
- renderMeetings(data.meetings || []);
74
- if (typeof renderPipelines === 'function') renderPipelines(data.pipelines || []);
75
- renderPinned(data.pinned || []);
76
- // Update sidebar counts
74
+ if (_changed('engineLog', data.engineLog)) renderEngineLog(data.engineLog || []);
75
+ if (_changed('metrics', data.metrics)) renderMetrics(data.metrics || {});
76
+ if (_changed('workItems', data.workItems)) renderWorkItems(data.workItems || []);
77
+ if (_changed('skills', data.skills)) renderSkills(data.skills || []);
78
+ if (_changed('mcpServers', data.mcpServers)) renderMcpServers(data.mcpServers || []);
79
+ if (_changed('schedules', data.schedules)) renderSchedules(data.schedules || []);
80
+ if (_changed('meetings', data.meetings)) renderMeetings(data.meetings || []);
81
+ if (_changed('pipelines', data.pipelines) && typeof renderPipelines === 'function') renderPipelines(data.pipelines || []);
82
+ if (_changed('pinned', data.pinned)) renderPinned(data.pinned || []);
83
+ // Sidebar counts (cheap)
77
84
  const swi = document.getElementById('sidebar-wi');
78
85
  if (swi) swi.textContent = (data.workItems || []).length || '';
79
86
  const spr = document.getElementById('sidebar-pr');
@@ -277,6 +277,7 @@ function checkPlanCompletion(meta, config) {
277
277
  // Archive deferred until verify completes
278
278
 
279
279
  log('info', `PRD ${planFile} completed: ${doneItems.length} done, ${failedItems.length} failed, runtime ${runtimeMin}m`);
280
+ return true;
280
281
  }
281
282
 
282
283
  // ─── Archive Plan ───────────────────────────────────────────────────────────
package/engine.js CHANGED
@@ -1492,6 +1492,7 @@ function discoverFromWorkItems(config, project) {
1492
1492
  const prdSyncQueue = [];
1493
1493
  const skipped = { gated: 0, noAgent: 0 };
1494
1494
  let needsWrite = false;
1495
+ const selfHealKeys = new Set(); // Collect keys for batched self-heal (1 lock instead of N)
1495
1496
 
1496
1497
  for (const item of items) {
1497
1498
  try {
@@ -1526,25 +1527,13 @@ function discoverFromWorkItems(config, project) {
1526
1527
  }
1527
1528
 
1528
1529
  const key = `work-${project?.name || 'default'}-${item.id}`;
1529
- // Self-heal: if an item is pending, stale completed/cooldown markers must not gate redispatch.
1530
- // This protects against persisted state drift from old runtime versions.
1531
- try {
1532
- mutateDispatch((dp) => {
1533
- const prev = Array.isArray(dp.completed) ? dp.completed : [];
1534
- const next = [];
1535
- for (let i = 0; i < prev.length; i++) {
1536
- if (prev[i].meta?.dispatchKey !== key) next.push(prev[i]);
1537
- }
1538
- dp.completed = next;
1539
- return dp;
1540
- });
1541
- dispatchCooldowns.delete(key);
1542
- } catch (e) { log('warn', 'self-heal dispatch state: ' + e.message); }
1543
- // Cooldown bypass for resumed items — clear in-memory cooldown so they dispatch immediately
1530
+ // Self-heal: collect keys for batched dispatch.json cleanup (after the loop)
1531
+ selfHealKeys.add(key);
1532
+ dispatchCooldowns.delete(key);
1533
+ // Cooldown bypass for resumed items
1544
1534
  if (item._resumedAt) {
1545
- dispatchCooldowns.delete(key);
1546
1535
  delete item._resumedAt;
1547
- safeWrite(projectWorkItemsPath(project), items);
1536
+ needsWrite = true;
1548
1537
  }
1549
1538
  if (isAlreadyDispatched(key)) {
1550
1539
  if (item.status === WI_STATUS.PENDING) { item.status = WI_STATUS.DISPATCHED; needsWrite = true; }
@@ -1696,6 +1685,16 @@ function discoverFromWorkItems(config, project) {
1696
1685
  } catch (err) { log('warn', `discoverFromWorkItems: skipping ${item.id}: ${err.message}`); }
1697
1686
  }
1698
1687
 
1688
+ // Batched self-heal: clear all stale completed entries in ONE lock acquisition
1689
+ if (selfHealKeys.size > 0) {
1690
+ try {
1691
+ mutateDispatch((dp) => {
1692
+ dp.completed = (Array.isArray(dp.completed) ? dp.completed : []).filter(d => !selfHealKeys.has(d.meta?.dispatchKey));
1693
+ return dp;
1694
+ });
1695
+ } catch (e) { log('warn', 'batched self-heal: ' + e.message); }
1696
+ }
1697
+
1699
1698
  // Auto-promote decomposed parents to done when all sub-tasks complete
1700
1699
  for (const item of items) {
1701
1700
  if (item.status !== WI_STATUS.DECOMPOSED || !item._subItemIds?.length) continue;
@@ -2264,10 +2263,8 @@ function discoverWork(config) {
2264
2263
  }
2265
2264
  if (plan.status !== 'approved' && plan.status !== 'active') continue;
2266
2265
  // Simulate the meta object checkPlanCompletion expects
2267
- lifecycle.checkPlanCompletion({ item: { sourcePlan: f } }, config);
2268
- // If plan transitioned to completed, cache it
2269
- const after = safeJson(path.join(prdDir, f));
2270
- if (after?.status === 'completed') completedPlanCache.add(f);
2266
+ const completed = lifecycle.checkPlanCompletion({ item: { sourcePlan: f } }, config);
2267
+ if (completed) completedPlanCache.add(f);
2271
2268
  }
2272
2269
  }
2273
2270
  } catch (e) { log('warn', 'plan completion sweep: ' + e.message); }
@@ -2379,10 +2376,8 @@ async function tickInner() {
2379
2376
  if (completedPlanCache.has(file)) continue;
2380
2377
  const plan = safeJson(path.join(PRD_DIR, file));
2381
2378
  if (plan && plan.missing_features && plan.status !== 'completed') {
2382
- checkPlanCompletion({ item: { sourcePlan: file } }, config);
2383
- // If plan transitioned to completed, cache it
2384
- const after = safeJson(path.join(PRD_DIR, file));
2385
- if (after?.status === 'completed') completedPlanCache.add(file);
2379
+ const completed = checkPlanCompletion({ item: { sourcePlan: file } }, config);
2380
+ if (completed) completedPlanCache.add(file);
2386
2381
  } else if (plan?.status === 'completed') {
2387
2382
  completedPlanCache.add(file);
2388
2383
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.518",
3
+ "version": "0.1.519",
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"