@yemi33/minions 0.1.1925 → 0.1.1927

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 };
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-14T02:03:25.197Z"
4
+ "cachedAt": "2026-05-14T02:06:41.197Z"
5
5
  }
@@ -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
@@ -735,6 +735,11 @@ async function spawnAgent(dispatchItem, config) {
735
735
  const claudeConfig = config.claude || {};
736
736
  const engineConfig = config.engine || {};
737
737
  const startedAt = ts();
738
+ // Phase timing — raw timestamps, emitted as one structured `[spawn-timing]`
739
+ // log line after spawn succeeds. Use Date.now() not ts() so deltas are plain
740
+ // milliseconds. Keys are stamped at phase boundaries below.
741
+ const _phaseT = { start: Date.now() };
742
+ let _depCountForLog = 0;
738
743
 
739
744
  updateAgentStatus(id, AGENT_STATUS.SPAWNING, `Preparing ${type} task for ${agentId}`);
740
745
 
@@ -802,6 +807,7 @@ async function spawnAgent(dispatchItem, config) {
802
807
  const sysPromptPath = path.join(tmpDir, `sysprompt-${safeId}.md`);
803
808
  safeWrite(sysPromptPath, systemPrompt);
804
809
  const _cleanupPromptFiles = () => { safeUnlink(promptPath); safeUnlink(sysPromptPath); };
810
+ _phaseT.afterPrompt = Date.now();
805
811
 
806
812
  if (branchName) {
807
813
  updateAgentStatus(id, AGENT_STATUS.WORKTREE_SETUP, `Setting up worktree for branch ${branchName}`);
@@ -990,7 +996,9 @@ async function spawnAgent(dispatchItem, config) {
990
996
  if (worktreePath && fs.existsSync(worktreePath)) {
991
997
  cwd = worktreePath;
992
998
  const depIds = meta?.item?.depends_on || [];
999
+ _depCountForLog = depIds.length;
993
1000
  if (depIds.length > 0) {
1001
+ _phaseT.depFetchStart = Date.now();
994
1002
  try {
995
1003
  const depBranches = resolveDependencyBranches(depIds, meta?.item?.sourcePlan, project, config);
996
1004
  let depMergeFailed = false;
@@ -1047,6 +1055,8 @@ async function spawnAgent(dispatchItem, config) {
1047
1055
  depMergeFailed = true;
1048
1056
  }
1049
1057
  }
1058
+ _phaseT.depFetchEnd = Date.now();
1059
+ _phaseT.depPreflightStart = _phaseT.depFetchEnd;
1050
1060
  // Merge successfully-fetched + recovered (local-only pushed) branches sequentially
1051
1061
  const fetched = fetchable.filter((_, i) => fetchResults[i].status === 'fulfilled' || recoveredBranches.has(fetchable[i].branch));
1052
1062
  // Ancestor pruning: remove dep branches already contained in another (#958)
@@ -1098,6 +1108,8 @@ async function spawnAgent(dispatchItem, config) {
1098
1108
  log('warn', `Pre-flight simulation failed, proceeding with real merge: ${e.message}`);
1099
1109
  }
1100
1110
  }
1111
+ _phaseT.depPreflightEnd = Date.now();
1112
+ _phaseT.depMergeStart = _phaseT.depPreflightEnd;
1101
1113
  // Stash uncommitted changes before dep merge if worktree is dirty (#973)
1102
1114
  let stashed = false;
1103
1115
  if (!depMergeFailed && !skipDepMerge && prunedDeps.length > 0) {
@@ -1182,6 +1194,7 @@ async function spawnAgent(dispatchItem, config) {
1182
1194
  log('warn', `git stash pop failed in ${branchName}: ${popErr.message} — stash preserved for agent`);
1183
1195
  }
1184
1196
  }
1197
+ _phaseT.depMergeEnd = Date.now();
1185
1198
  if (depMergeFailed) {
1186
1199
  _cleanupPromptFiles();
1187
1200
  // Build actionable failReason identifying the conflicting branch and files (#958)
@@ -1291,6 +1304,7 @@ async function spawnAgent(dispatchItem, config) {
1291
1304
  if (cwd === rootDir && ['implement', 'implement:large', 'fix', 'test', 'verify', 'plan-to-prd'].includes(type)) {
1292
1305
  log('warn', `Agent ${agentId} running ${type} task in main repo (no worktree) for ${id} — changes may land on master directly`);
1293
1306
  }
1307
+ _phaseT.afterWorktree = Date.now();
1294
1308
 
1295
1309
  // ── Stale-HEAD guard for fix-task pushes (P-c8f2d5e3) ────────────────────
1296
1310
  // When a PR branch is rebased upstream (force-push), a reused worktree can
@@ -1302,6 +1316,7 @@ async function spawnAgent(dispatchItem, config) {
1302
1316
  // Read-only and non-fix dispatches are out of scope — implement tasks cut
1303
1317
  // their own branch from main, and review/verify don't push.
1304
1318
  if (type === WORK_TYPE.FIX && branchName && worktreePath && cwd === worktreePath) {
1319
+ _phaseT.staleHeadStart = Date.now();
1305
1320
  try {
1306
1321
  const guard = await assertStaleHeadOk({
1307
1322
  branch: branchName,
@@ -1325,6 +1340,7 @@ async function spawnAgent(dispatchItem, config) {
1325
1340
  // to skipped:'fetch-failed'). Log and continue.
1326
1341
  log('warn', `Stale-HEAD guard error for ${id} (${branchName}): ${err.message}`);
1327
1342
  }
1343
+ _phaseT.staleHeadEnd = Date.now();
1328
1344
  }
1329
1345
 
1330
1346
  // ── Runtime + opts resolution (P-2a6d9c4f) ────────────────────────────────
@@ -1371,6 +1387,7 @@ async function spawnAgent(dispatchItem, config) {
1371
1387
 
1372
1388
  // MCP servers: agents inherit from ~/.claude.json directly as Claude Code processes.
1373
1389
  // No --mcp-config needed — avoids redundant config and ensures agents always have latest servers.
1390
+ _phaseT.afterRuntime = Date.now();
1374
1391
 
1375
1392
  log('info', `Spawning agent: ${agentId} (${id}) in ${cwd}`);
1376
1393
  log('info', `Task type: ${type} | Branch: ${branchName || 'none'}`);
@@ -1428,6 +1445,7 @@ async function spawnAgent(dispatchItem, config) {
1428
1445
  }
1429
1446
 
1430
1447
  let proc;
1448
+ _phaseT.spawnCallStart = Date.now();
1431
1449
  try {
1432
1450
  // `detached: true` puts the agent in its own process group (POSIX) / job
1433
1451
  // object (Windows), so when the engine dies — gracefully via stop, abruptly
@@ -1441,6 +1459,7 @@ async function spawnAgent(dispatchItem, config) {
1441
1459
  env: childEnv,
1442
1460
  detached: true,
1443
1461
  });
1462
+ _phaseT.spawnCallEnd = Date.now();
1444
1463
  } catch (spawnErr) {
1445
1464
  // Synchronous spawn failure — record it to the (already-stamped) log so the
1446
1465
  // orphan detector's "logSize > stub-only" check can tell this apart from a
@@ -1476,6 +1495,26 @@ async function spawnAgent(dispatchItem, config) {
1476
1495
  };
1477
1496
  activeProcesses.set(id, initialProcInfo);
1478
1497
 
1498
+ // Emit per-phase timing for spawn-latency analysis. One structured line per
1499
+ // dispatch; grep `[spawn-timing]` to aggregate. Null phases didn't run for
1500
+ // this dispatch (e.g. stale_head only runs for fix tasks; dep_* only when
1501
+ // the item has depends_on).
1502
+ try {
1503
+ const _diff = (a, b) => (_phaseT[a] != null && _phaseT[b] != null) ? (_phaseT[b] - _phaseT[a]) : null;
1504
+ const timings = {
1505
+ prompt: _diff('start', 'afterPrompt'),
1506
+ worktree_total: _diff('afterPrompt', 'afterWorktree'),
1507
+ dep_fetch: _diff('depFetchStart', 'depFetchEnd'),
1508
+ dep_preflight: _diff('depPreflightStart', 'depPreflightEnd'),
1509
+ dep_merge: _diff('depMergeStart', 'depMergeEnd'),
1510
+ stale_head: _diff('staleHeadStart', 'staleHeadEnd'),
1511
+ runtime: _diff('afterWorktree', 'afterRuntime'),
1512
+ spawn_call: _diff('spawnCallStart', 'spawnCallEnd'),
1513
+ total: _diff('start', 'spawnCallEnd'),
1514
+ };
1515
+ log('info', `[spawn-timing] id=${id} agent=${agentId} type=${type} runtime=${runtimeName} branch=${branchName || '-'} deps=${_depCountForLog} ${JSON.stringify(timings)}`);
1516
+ } catch { /* telemetry is best-effort */ }
1517
+
1479
1518
  const MAX_OUTPUT = 1024 * 1024; // 1MB
1480
1519
  let stdout = '';
1481
1520
  let stderr = '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1925",
3
+ "version": "0.1.1927",
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"