@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.
- package/dashboard/js/render-other.js +48 -14
- package/engine/copilot-models.json +1 -1
- package/engine/lifecycle.js +7 -1
- package/engine.js +39 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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 };
|
package/engine/lifecycle.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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"
|