@yemi33/minions 0.1.1926 → 0.1.1928

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.
@@ -166,9 +166,15 @@ function renderLlmPerf(metrics) {
166
166
  el.innerHTML = '<p class="empty">No LLM performance data yet.</p>';
167
167
  return;
168
168
  }
169
- let html = '<table class="pr-table"><thead><tr><th>Call Type</th><th>Calls</th><th>Total Time</th><th>Avg Time</th><th>Cost</th></tr></thead><tbody>';
170
- const entries = Object.entries(engine).filter(([k]) => !k.startsWith('test')).sort((a, b) => (b[1].calls || 0) - (a[1].calls || 0));
171
- for (const [type, m] of entries) {
169
+ // Split entries: per-LLM-call rows vs per-agent-task agent-dispatch row.
170
+ // `calls` semantics differ between the two agent-dispatch counts dispatched
171
+ // work items (with runtimeMs = wall-clock of the agent process), the others
172
+ // count LLM API round-trips (with totalDurationMs = sum of per-call latencies).
173
+ const filtered = Object.entries(engine).filter(([k]) => !k.startsWith('test'));
174
+ const perCall = filtered.filter(([k]) => k !== 'agent-dispatch')
175
+ .sort((a, b) => (b[1].calls || 0) - (a[1].calls || 0));
176
+ const perTask = filtered.filter(([k]) => k === 'agent-dispatch');
177
+ function renderRow(type, m) {
172
178
  const calls = m.calls || 0;
173
179
  const totalMs = m.totalDurationMs || 0;
174
180
  const timedCalls = m.timedCalls || 0;
@@ -176,16 +182,48 @@ function renderLlmPerf(metrics) {
176
182
  const fmtTotal = totalMs < 60000 ? Math.round(totalMs / 1000) + 's' : Math.round(totalMs / 60000) + 'm';
177
183
  const fmtAvg = avgMs < 1000 ? Math.round(avgMs) + 'ms' : avgMs < 60000 ? Math.round(avgMs / 1000) + 's' : Math.round(avgMs / 60000) + 'm';
178
184
  const cost = m.costUsd ? '$' + m.costUsd.toFixed(2) : '-';
179
- html += '<tr><td style="font-weight:600">' + escHtml(type) + '</td>' +
185
+ return '<tr><td style="font-weight:600">' + escHtml(type) + '</td>' +
180
186
  '<td>' + calls + '</td>' +
181
187
  '<td style="color:var(--muted)">' + (totalMs ? fmtTotal : '-') + '</td>' +
182
188
  '<td style="color:var(--blue)">' + (avgMs ? fmtAvg : '-') + '</td>' +
183
189
  '<td style="color:var(--muted)">' + cost + '</td></tr>';
184
190
  }
191
+ let html = '<table class="pr-table"><thead><tr><th>Call Type</th><th>Calls</th><th>Total Time</th><th>Avg Time</th><th>Cost</th></tr></thead><tbody>';
192
+ for (const [type, m] of perCall) {
193
+ html += renderRow(type, m);
194
+ }
195
+ if (perTask.length > 0) {
196
+ // Visual divider + caption row to distinguish per-call (above) from
197
+ // per-task (below) — column units differ.
198
+ html += '<tr><td colspan="5" style="border-top:2px solid var(--border);padding-top:6px;font-size:10px;color:var(--muted);font-style:italic">— per agent task (calls = dispatched work items, time = wall-clock of agent process) —</td></tr>';
199
+ for (const [type, m] of perTask) {
200
+ html += renderRow(type + ' (per task)', m);
201
+ }
202
+ }
185
203
  html += '</tbody></table>';
186
204
  el.innerHTML = html;
187
205
  }
188
206
 
207
+ // Aggregate _engine entries for the Token Usage summary tiles.
208
+ // Excludes:
209
+ // - 'agent-dispatch' (already counted in per-agent metrics[agentId] totals;
210
+ // summing both would double-count agent spend in the tile)
211
+ // - 'test-*' / '_test*' (matching engine/llm.js trackEngineUsage filter so
212
+ // stale test categories don't inflate engineCalls)
213
+ function _aggregateEngineUsageForTokenTile(engine) {
214
+ let cost = 0, input = 0, output = 0, cache = 0, calls = 0;
215
+ for (const [k, e] of Object.entries(engine || {})) {
216
+ if (k === 'agent-dispatch') continue;
217
+ if (k.startsWith('test-') || k.startsWith('_test')) continue;
218
+ cost += e.costUsd || 0;
219
+ input += e.inputTokens || 0;
220
+ output += e.outputTokens || 0;
221
+ cache += e.cacheRead || 0;
222
+ calls += e.calls || 0;
223
+ }
224
+ return { cost, input, output, cache, calls };
225
+ }
226
+
189
227
  function renderTokenUsage(metrics) {
190
228
  const el = document.getElementById('token-usage-content');
191
229
  const agents = Object.entries(metrics).filter(([k]) => !k.startsWith('_'));
@@ -201,15 +239,11 @@ function renderTokenUsage(metrics) {
201
239
  agentCache += m.totalCacheRead || 0;
202
240
  }
203
241
 
204
- // Aggregate engine totals
205
- let engineCost = 0, engineInput = 0, engineOutput = 0, engineCache = 0, engineCalls = 0;
206
- for (const [, e] of Object.entries(engine)) {
207
- engineCost += e.costUsd || 0;
208
- engineInput += e.inputTokens || 0;
209
- engineOutput += e.outputTokens || 0;
210
- engineCache += e.cacheRead || 0;
211
- engineCalls += e.calls || 0;
212
- }
242
+ // Aggregate engine totals (excludes agent-dispatch + test-* — see helper)
243
+ const engAgg = _aggregateEngineUsageForTokenTile(engine);
244
+ const engineCost = engAgg.cost, engineInput = engAgg.input,
245
+ engineOutput = engAgg.output, engineCache = engAgg.cache,
246
+ engineCalls = engAgg.calls; // eslint-disable-line no-unused-vars
213
247
 
214
248
  const totalCost = agentCost + engineCost;
215
249
  const totalInput = agentInput + engineInput;
@@ -430,4 +464,4 @@ async function _addSelectedProjects() {
430
464
  }
431
465
  }
432
466
 
433
- window.MinionsOther = { renderProjects, optimisticallyAddProject, projectChipRemove, renderMcpServers, renderMetrics, renderLlmPerf, renderTokenUsage, openScanProjectsModal };
467
+ window.MinionsOther = { renderProjects, optimisticallyAddProject, projectChipRemove, renderMcpServers, renderMetrics, renderLlmPerf, renderTokenUsage, _aggregateEngineUsageForTokenTile, openScanProjectsModal };
@@ -2550,8 +2550,14 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount
2550
2550
  metrics._engine['agent-dispatch'] = { calls: 0, costUsd: 0, inputTokens: 0, outputTokens: 0, cacheRead: 0, cacheCreation: 0, totalDurationMs: 0 };
2551
2551
  }
2552
2552
  const eng = metrics._engine['agent-dispatch'];
2553
- eng.calls++;
2553
+ // Gate calls on runtimeMs > 0 so pre-spawn skips (deps unmet, cooldown,
2554
+ // classified-fail) don't inflate the dispatch-tile call count. `calls` and
2555
+ // `timedCalls` advance together for agent-dispatch; both fields are kept
2556
+ // for schema parity with other _engine entries (CC/doc-chat/consolidation/
2557
+ // kb-sweep) where calls = LLM round-trips and timedCalls subset = ones with
2558
+ // a measured durationMs.
2554
2559
  if (runtimeMs > 0) {
2560
+ eng.calls++;
2555
2561
  eng.totalDurationMs = (eng.totalDurationMs || 0) + runtimeMs;
2556
2562
  eng.timedCalls = (eng.timedCalls || 0) + 1;
2557
2563
  }
package/engine.js CHANGED
@@ -823,7 +823,9 @@ async function spawnAgent(dispatchItem, config) {
823
823
  shared.assertWorktreeOutsideProject(worktreePath, rootDir);
824
824
 
825
825
  // If branch is already checked out in an existing worktree, reuse it
826
+ _phaseT.findExistingStart = Date.now();
826
827
  const existingWt = await findExistingWorktree(rootDir, branchName);
828
+ _phaseT.findExistingEnd = Date.now();
827
829
  if (existingWt) {
828
830
  // Same guard for reuse — a previously-created bad worktree must not
829
831
  // be silently reused either; the cleanup sweep flags these so the
@@ -834,13 +836,16 @@ async function spawnAgent(dispatchItem, config) {
834
836
  // Probe origin first — locally-created branches that were never pushed
835
837
  // (orphan/timeout retry before first push) would otherwise emit a
836
838
  // "couldn't find remote ref" warn pair on every reuse.
839
+ _phaseT.reuseSyncStart = Date.now();
837
840
  await syncReusedWorktree(rootDir, existingWt, branchName, _gitOpts);
841
+ _phaseT.reuseSyncEnd = Date.now();
838
842
  } else if (READ_ONLY_ROOT_TASK_TYPES.has(type) && !isPipelineBranchName(branchName)) {
839
843
  // Read-only tasks — no worktree needed, run in rootDir
840
844
  log('info', `${type}: read-only task, no worktree needed — running in rootDir`);
841
845
  branchName = null;
842
846
  worktreePath = null;
843
847
  } else {
848
+ _phaseT.createWorktreeStart = Date.now();
844
849
  try {
845
850
  if (!fs.existsSync(worktreePath)) {
846
851
  const isSharedBranch = meta?.branchStrategy === 'shared-branch' || meta?.useExistingBranch;
@@ -965,6 +970,7 @@ async function spawnAgent(dispatchItem, config) {
965
970
  return null;
966
971
  }
967
972
  }
973
+ _phaseT.createWorktreeEnd = Date.now();
968
974
  }
969
975
 
970
976
  // Shared-branch preflight (#2439): refuse to dispatch into a dirty shared
@@ -974,7 +980,9 @@ async function spawnAgent(dispatchItem, config) {
974
980
  // fires AFTER spawn — converting orchestration hygiene into fake
975
981
  // implementation failures and cascading dependent items.
976
982
  if (worktreePath && fs.existsSync(worktreePath) && meta?.branchStrategy === 'shared-branch' && branchName) {
983
+ _phaseT.cleanCheckStart = Date.now();
977
984
  const cleanResult = await assertCleanSharedWorktree(rootDir, worktreePath, branchName, id, _gitOpts);
985
+ _phaseT.cleanCheckEnd = Date.now();
978
986
  if (!cleanResult.clean) {
979
987
  const previewFiles = (cleanResult.dirtyFiles || []).slice(0, 5).join(', ');
980
988
  const reasonMsg = `DIRTY_WORKTREE: shared branch ${branchName} worktree at ${worktreePath} is dirty (${cleanResult.reason}); ${cleanResult.dirtyFiles?.length || 0} file(s)${previewFiles ? ': ' + previewFiles : ''}`;
@@ -1281,6 +1289,7 @@ async function spawnAgent(dispatchItem, config) {
1281
1289
  // Inject dirty file list when worktree has uncommitted changes (e.g., max_turns retry)
1282
1290
  // This signals to the respawned agent that prior work exists in the worktree (#960)
1283
1291
  if (worktreePath && fs.existsSync(worktreePath)) {
1292
+ _phaseT.dirtyProbeStart = Date.now();
1284
1293
  try {
1285
1294
  const dirtyResult = await execAsync('git status --porcelain', { ..._gitOpts, cwd: worktreePath, timeout: 10000 });
1286
1295
  const dirtyOutput = (dirtyResult.stdout || '').trim();
@@ -1298,6 +1307,7 @@ async function spawnAgent(dispatchItem, config) {
1298
1307
  log('info', `Injected ${dirtyFiles.length} dirty files into prompt for ${id}`);
1299
1308
  }
1300
1309
  } catch (e) { log('warn', `git status --porcelain for dirty files: ${e.message}`); }
1310
+ _phaseT.dirtyProbeEnd = Date.now();
1301
1311
  }
1302
1312
 
1303
1313
  // Safety check: warn if a write-capable task is running in the main repo without a worktree
@@ -1504,6 +1514,11 @@ async function spawnAgent(dispatchItem, config) {
1504
1514
  const timings = {
1505
1515
  prompt: _diff('start', 'afterPrompt'),
1506
1516
  worktree_total: _diff('afterPrompt', 'afterWorktree'),
1517
+ wt_find_existing: _diff('findExistingStart', 'findExistingEnd'),
1518
+ wt_reuse_sync: _diff('reuseSyncStart', 'reuseSyncEnd'),
1519
+ wt_create: _diff('createWorktreeStart', 'createWorktreeEnd'),
1520
+ wt_clean_check: _diff('cleanCheckStart', 'cleanCheckEnd'),
1521
+ wt_dirty_probe: _diff('dirtyProbeStart', 'dirtyProbeEnd'),
1507
1522
  dep_fetch: _diff('depFetchStart', 'depFetchEnd'),
1508
1523
  dep_preflight: _diff('depPreflightStart', 'depPreflightEnd'),
1509
1524
  dep_merge: _diff('depMergeStart', 'depMergeEnd'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1926",
3
+ "version": "0.1.1928",
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"