@yemi33/minions 0.1.163 → 0.1.165
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 +10 -0
- package/engine/lifecycle.js +35 -36
- package/engine.js +39 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/engine/lifecycle.js
CHANGED
|
@@ -1188,6 +1188,11 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1188
1188
|
? path.join(MINIONS_DIR, 'work-items.json')
|
|
1189
1189
|
: meta.project?.name ? path.join(MINIONS_DIR, 'projects', meta.project.name, 'work-items.json') : null;
|
|
1190
1190
|
if (wiPath) {
|
|
1191
|
+
// Cost ceiling circuit breaker config — resolved outside lock for clarity
|
|
1192
|
+
const engineCfg = config?.engine || {};
|
|
1193
|
+
const evalMaxCost = engineCfg.evalMaxCost != null ? engineCfg.evalMaxCost : shared.ENGINE_DEFAULTS.evalMaxCost;
|
|
1194
|
+
|
|
1195
|
+
// Single lock: accumulate cost AND check ceiling atomically (no TOCTOU)
|
|
1191
1196
|
mutateJsonFileLocked(wiPath, (items) => {
|
|
1192
1197
|
if (!Array.isArray(items)) return items;
|
|
1193
1198
|
const wi = items.find(i => i.id === meta.item.id);
|
|
@@ -1195,29 +1200,17 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1195
1200
|
wi._totalCostUsd = (wi._totalCostUsd || 0) + (taskUsage.costUsd || 0);
|
|
1196
1201
|
wi._totalInputTokens = (wi._totalInputTokens || 0) + (taskUsage.inputTokens || 0);
|
|
1197
1202
|
wi._totalOutputTokens = (wi._totalOutputTokens || 0) + (taskUsage.outputTokens || 0);
|
|
1203
|
+
|
|
1204
|
+
// Cost ceiling circuit breaker — treat like evalMaxIterations exceeded
|
|
1205
|
+
if (evalMaxCost != null && evalMaxCost > 0 &&
|
|
1206
|
+
wi._totalCostUsd > evalMaxCost && wi.status !== 'needs-human-review') {
|
|
1207
|
+
wi.status = 'needs-human-review';
|
|
1208
|
+
wi.failReason = `Cumulative cost $${wi._totalCostUsd.toFixed(2)} exceeds evalMaxCost ceiling $${evalMaxCost.toFixed(2)}`;
|
|
1209
|
+
log('warn', `Work item ${meta.item.id} exceeded cost ceiling ($${wi._totalCostUsd.toFixed(2)} > $${evalMaxCost.toFixed(2)}) — needs-human-review`);
|
|
1210
|
+
}
|
|
1198
1211
|
}
|
|
1199
1212
|
return items;
|
|
1200
1213
|
}, { defaultValue: [] });
|
|
1201
|
-
|
|
1202
|
-
// Cost ceiling circuit breaker — treat like evalMaxIterations exceeded
|
|
1203
|
-
const engineCfg = config?.engine || {};
|
|
1204
|
-
const evalMaxCost = engineCfg.evalMaxCost != null ? engineCfg.evalMaxCost : shared.ENGINE_DEFAULTS.evalMaxCost;
|
|
1205
|
-
if (evalMaxCost != null && evalMaxCost > 0) {
|
|
1206
|
-
const freshItems = safeJson(wiPath) || [];
|
|
1207
|
-
const wi = freshItems.find(i => i.id === meta.item.id);
|
|
1208
|
-
if (wi && wi._totalCostUsd > evalMaxCost && wi.status !== 'needs-human-review') {
|
|
1209
|
-
mutateJsonFileLocked(wiPath, (items) => {
|
|
1210
|
-
if (!Array.isArray(items)) return items;
|
|
1211
|
-
const target = items.find(i => i.id === meta.item.id);
|
|
1212
|
-
if (target) {
|
|
1213
|
-
target.status = 'needs-human-review';
|
|
1214
|
-
target.failReason = `Cumulative cost $${wi._totalCostUsd.toFixed(2)} exceeds evalMaxCost ceiling $${evalMaxCost.toFixed(2)}`;
|
|
1215
|
-
log('warn', `Work item ${meta.item.id} exceeded cost ceiling ($${wi._totalCostUsd.toFixed(2)} > $${evalMaxCost.toFixed(2)}) — needs-human-review`);
|
|
1216
|
-
}
|
|
1217
|
-
return items;
|
|
1218
|
-
}, { defaultValue: [] });
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
1214
|
}
|
|
1222
1215
|
} catch (err) { log('warn', `Cost accumulation: ${err.message}`); }
|
|
1223
1216
|
}
|
|
@@ -1358,15 +1351,15 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1358
1351
|
}
|
|
1359
1352
|
|
|
1360
1353
|
if (!isSuccess && meta?.item?.id) {
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1354
|
+
// Auto-retry with atomic read-modify-write (no TOCTOU race)
|
|
1355
|
+
try {
|
|
1356
|
+
const wiPath = resolveWiPath(meta);
|
|
1357
|
+
if (wiPath) {
|
|
1358
|
+
let retriesExhausted = false;
|
|
1365
1359
|
mutateJsonFileLocked(wiPath, (items) => {
|
|
1366
1360
|
if (!Array.isArray(items)) return items;
|
|
1367
1361
|
const wi = items.find(i => i.id === meta.item.id);
|
|
1368
1362
|
if (!wi) return items;
|
|
1369
|
-
|
|
1370
1363
|
const retries = wi._retryCount || 0;
|
|
1371
1364
|
if (retries < 3) {
|
|
1372
1365
|
log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/3`);
|
|
@@ -1374,22 +1367,28 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
|
|
|
1374
1367
|
wi.status = 'pending';
|
|
1375
1368
|
delete wi.dispatched_at;
|
|
1376
1369
|
delete wi.dispatched_to;
|
|
1377
|
-
|
|
1370
|
+
if (type === 'decompose') delete wi._decomposing;
|
|
1378
1371
|
} else {
|
|
1379
|
-
|
|
1380
|
-
wi.failReason = 'Agent failed (3 retries exhausted)';
|
|
1381
|
-
wi.failedAt = ts();
|
|
1382
|
-
finalStatus = 'failed';
|
|
1372
|
+
retriesExhausted = true;
|
|
1383
1373
|
}
|
|
1374
|
+
// Clear _decomposing flag on any decompose failure to prevent permanent stuck
|
|
1384
1375
|
if (type === 'decompose') delete wi._decomposing;
|
|
1385
1376
|
return items;
|
|
1386
|
-
});
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1377
|
+
}, { defaultValue: [] });
|
|
1378
|
+
if (retriesExhausted) {
|
|
1379
|
+
updateWorkItemStatus(meta, 'failed', 'Agent failed (3 retries exhausted)');
|
|
1380
|
+
}
|
|
1381
|
+
} else {
|
|
1382
|
+
// No wiPath — can't read retries, fall back to meta snapshot
|
|
1383
|
+
const retries = meta.item._retryCount || 0;
|
|
1384
|
+
if (retries < 3) {
|
|
1385
|
+
log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/3`);
|
|
1386
|
+
updateWorkItemStatus(meta, 'pending', '');
|
|
1387
|
+
} else {
|
|
1388
|
+
updateWorkItemStatus(meta, 'failed', 'Agent failed (3 retries exhausted)');
|
|
1389
|
+
}
|
|
1391
1390
|
}
|
|
1392
|
-
}
|
|
1391
|
+
} catch (err) { log('warn', `Retry/decompose update: ${err.message}`); }
|
|
1393
1392
|
}
|
|
1394
1393
|
// Meeting post-completion: collect findings/debate/conclusion
|
|
1395
1394
|
if (type === 'meeting' && meta?.meetingId) {
|
package/engine.js
CHANGED
|
@@ -1802,6 +1802,41 @@ function discoverCentralWorkItems(config) {
|
|
|
1802
1802
|
assignedProject: projects.length > 0 ? projects[i % projects.length] : null
|
|
1803
1803
|
}));
|
|
1804
1804
|
|
|
1805
|
+
// Inject checkpoint context if agent left a checkpoint.json from a prior run
|
|
1806
|
+
let fanOutCheckpointContext = '';
|
|
1807
|
+
try {
|
|
1808
|
+
const fanFirstProject = projects[0];
|
|
1809
|
+
const fanBranch = item.branch || `work/${item.id}`;
|
|
1810
|
+
const fanWtPath = fanFirstProject?.localPath
|
|
1811
|
+
? path.resolve(fanFirstProject.localPath, config.engine?.worktreeRoot || '../worktrees', fanBranch)
|
|
1812
|
+
: '';
|
|
1813
|
+
const fanCpPath = fanWtPath ? path.join(fanWtPath, 'checkpoint.json') : '';
|
|
1814
|
+
if (fanCpPath && fs.existsSync(fanCpPath)) {
|
|
1815
|
+
const fanCpData = JSON.parse(fs.readFileSync(fanCpPath, 'utf8'));
|
|
1816
|
+
const fanCpCount = (item._checkpointCount || 0) + 1;
|
|
1817
|
+
if (fanCpCount > 3) {
|
|
1818
|
+
log('warn', `Work item ${item.id} exceeded 3 checkpoint-resumes — marking as needs-human-review`);
|
|
1819
|
+
item.status = 'needs-human-review';
|
|
1820
|
+
item._checkpointCount = fanCpCount;
|
|
1821
|
+
needsWrite = true;
|
|
1822
|
+
continue;
|
|
1823
|
+
}
|
|
1824
|
+
item._checkpointCount = fanCpCount;
|
|
1825
|
+
const fanCpSummary = [
|
|
1826
|
+
`## Checkpoint (Resume #${fanCpCount}/3)`,
|
|
1827
|
+
'',
|
|
1828
|
+
'A previous agent run timed out but left a checkpoint. Continue from where it left off.',
|
|
1829
|
+
'',
|
|
1830
|
+
Array.isArray(fanCpData.completed) && fanCpData.completed.length > 0 ? `### Completed\n${fanCpData.completed.map(s => '- ' + s).join('\n')}` : '',
|
|
1831
|
+
Array.isArray(fanCpData.remaining) && fanCpData.remaining.length > 0 ? `### Remaining\n${fanCpData.remaining.map(s => '- ' + s).join('\n')}` : '',
|
|
1832
|
+
Array.isArray(fanCpData.blockers) && fanCpData.blockers.length > 0 ? `### Blockers\n${fanCpData.blockers.map(s => '- ' + s).join('\n')}` : '',
|
|
1833
|
+
fanCpData.branch_state ? `### Branch State\n${fanCpData.branch_state}` : '',
|
|
1834
|
+
].filter(Boolean).join('\n');
|
|
1835
|
+
fanOutCheckpointContext = fanCpSummary;
|
|
1836
|
+
log('info', `Injecting checkpoint context for ${item.id} (resume #${fanCpCount})`);
|
|
1837
|
+
}
|
|
1838
|
+
} catch (e) { log('warn', `checkpoint read for ${item.id}: ${e.message}`); }
|
|
1839
|
+
|
|
1805
1840
|
for (const { agent, assignedProject } of assignments) {
|
|
1806
1841
|
const fanKey = `${key}-${agent.id}`;
|
|
1807
1842
|
if (isAlreadyDispatched(fanKey)) continue;
|
|
@@ -1833,6 +1868,10 @@ function discoverCentralWorkItems(config) {
|
|
|
1833
1868
|
? '## Push Branch\n\n**PR creation is skipped for this work item.** Push your branch and report the branch name.\n\n```bash\ngit push -u origin ' + fanBranch + '\n```\n\nInclude the branch name in your completion summary.'
|
|
1834
1869
|
: '## Create PR (MANDATORY)\n\n**Your task is NOT complete until a pull request exists.** If PR creation fails, retry up to 3 times before reporting the error.\n\n{{pr_create_instructions}}\n- sourceRefName: `refs/heads/' + fanBranch + '`\n- targetRefName: `refs/heads/{{main_branch}}`\n- title: `{{commit_message}}`\n- labels: `["minions:{{agent_id}}"]`\n\nInclude in the PR description:\n- What was built and why\n- Files changed\n- How to build and test, browser URL if applicable\n- Test plan\n\n## Post self-review on PR\n\n{{pr_comment_instructions}}\n- pullRequestId: `<from PR creation>`\n- Re-read your own diff critically before posting\n- Sign: `Built by Minions ({{agent_name}} — {{agent_role}})`';
|
|
1835
1870
|
|
|
1871
|
+
// Inject checkpoint context (computed once above the loop)
|
|
1872
|
+
vars.checkpoint_context = fanOutCheckpointContext;
|
|
1873
|
+
|
|
1874
|
+
|
|
1836
1875
|
if (workType === 'ask') {
|
|
1837
1876
|
vars.question = item.title + (item.description ? '\n\n' + item.description : '');
|
|
1838
1877
|
vars.task_id = item.id;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.165",
|
|
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"
|