@yemi33/minions 0.1.517 → 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,15 @@
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
+
8
+ ## 0.1.518 (2026-04-07)
9
+
10
+ ### Fixes
11
+ - decomposed parents auto-promote to done, sub-task PRs aggregate to parent
12
+
3
13
  ## 0.1.517 (2026-04-07)
4
14
 
5
15
  ### Features
@@ -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/queries.js CHANGED
@@ -777,6 +777,22 @@ function getPrdInfo(config) {
777
777
  const url = pr?.url || (project?.prUrlBase ? project.prUrlBase + wi._pr.replace('PR-', '') : '');
778
778
  prdToPr[wi.id] = [{ id: wi._pr, url, title: pr?.title || '', status: pr?.status || 'active', _project: project?.name || '' }];
779
779
  }
780
+ // Aggregate sub-task PRs to decomposed parent (sub-tasks aren't PRD items but their PRs should show)
781
+ for (const pr of allPrs) {
782
+ for (const itemId of (pr.prdItems || [])) {
783
+ // Find if this is a sub-task with a parent
784
+ const allItems = Object.values(wiById);
785
+ const wi = allItems.find(w => w.id === itemId && w.parent_id);
786
+ if (!wi) continue;
787
+ const parentId = wi.parent_id;
788
+ if (!prdToPr[parentId]) prdToPr[parentId] = [];
789
+ if (!prdToPr[parentId].some(p => p.id === pr.id)) {
790
+ const project = projects.find(p => p.name === pr._project) || projects[0] || null;
791
+ const url = pr.url || (project?.prUrlBase ? project.prUrlBase + pr.id.replace('PR-', '') : '');
792
+ prdToPr[parentId].push({ id: pr.id, url, title: pr.title || '', status: pr.status || 'active', _project: pr._project || '' });
793
+ }
794
+ }
795
+ }
780
796
 
781
797
  // PRD JSON status is the source of truth — kept in sync with work item by syncPrdItemStatus.
782
798
  // Map from PRD JSON values to display values (pending → missing for undispatched items)
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,32 @@ 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
+
1698
+ // Auto-promote decomposed parents to done when all sub-tasks complete
1699
+ for (const item of items) {
1700
+ if (item.status !== WI_STATUS.DECOMPOSED || !item._subItemIds?.length) continue;
1701
+ const allSubsDone = item._subItemIds.every(sid => {
1702
+ const sub = items.find(i => i.id === sid);
1703
+ return sub && DONE_STATUSES.has(sub.status);
1704
+ });
1705
+ if (allSubsDone) {
1706
+ item.status = WI_STATUS.DONE;
1707
+ if (!item.completedAt) item.completedAt = ts();
1708
+ needsWrite = true;
1709
+ log('info', `Decomposed parent ${item.id} → done (all ${item._subItemIds.length} sub-tasks complete)`);
1710
+ if (item.sourcePlan) syncPrdItemStatus(item.id, WI_STATUS.DONE, item.sourcePlan);
1711
+ }
1712
+ }
1713
+
1699
1714
  // Write back updated statuses (always, since we mark items dispatched before newWork check)
1700
1715
  if (newWork.length > 0) {
1701
1716
  const workItemsPath = projectWorkItemsPath(project);
@@ -2248,10 +2263,8 @@ function discoverWork(config) {
2248
2263
  }
2249
2264
  if (plan.status !== 'approved' && plan.status !== 'active') continue;
2250
2265
  // Simulate the meta object checkPlanCompletion expects
2251
- lifecycle.checkPlanCompletion({ item: { sourcePlan: f } }, config);
2252
- // If plan transitioned to completed, cache it
2253
- const after = safeJson(path.join(prdDir, f));
2254
- if (after?.status === 'completed') completedPlanCache.add(f);
2266
+ const completed = lifecycle.checkPlanCompletion({ item: { sourcePlan: f } }, config);
2267
+ if (completed) completedPlanCache.add(f);
2255
2268
  }
2256
2269
  }
2257
2270
  } catch (e) { log('warn', 'plan completion sweep: ' + e.message); }
@@ -2363,10 +2376,8 @@ async function tickInner() {
2363
2376
  if (completedPlanCache.has(file)) continue;
2364
2377
  const plan = safeJson(path.join(PRD_DIR, file));
2365
2378
  if (plan && plan.missing_features && plan.status !== 'completed') {
2366
- checkPlanCompletion({ item: { sourcePlan: file } }, config);
2367
- // If plan transitioned to completed, cache it
2368
- const after = safeJson(path.join(PRD_DIR, file));
2369
- if (after?.status === 'completed') completedPlanCache.add(file);
2379
+ const completed = checkPlanCompletion({ item: { sourcePlan: file } }, config);
2380
+ if (completed) completedPlanCache.add(file);
2370
2381
  } else if (plan?.status === 'completed') {
2371
2382
  completedPlanCache.add(file);
2372
2383
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.517",
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"