@yemi33/minions 0.1.1696 → 0.1.1698
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 +5 -0
- package/bin/minions.js +11 -6
- package/dashboard.js +38 -2
- package/engine/cli.js +67 -5
- package/engine/copilot-models.json +1 -1
- package/engine.js +28 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/bin/minions.js
CHANGED
|
@@ -203,6 +203,7 @@ function resolveMinionsHome(forInit = false) {
|
|
|
203
203
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
204
204
|
let force = rest.includes('--force');
|
|
205
205
|
const skipScan = rest.includes('--skip-scan');
|
|
206
|
+
const skipStart = rest.includes('--skip-start') || rest.includes('--no-start');
|
|
206
207
|
const MINIONS_HOME = resolveMinionsHome(cmd === 'init');
|
|
207
208
|
|
|
208
209
|
function isSubpath(parent, child) {
|
|
@@ -339,7 +340,12 @@ function init() {
|
|
|
339
340
|
printPreflight(results, { label: 'Preflight checks' });
|
|
340
341
|
} catch {}
|
|
341
342
|
|
|
342
|
-
|
|
343
|
+
if (isUpgrade && skipStart) {
|
|
344
|
+
console.log(`\n Upgrade complete (${pkgVersion}). Restart skipped by caller.\n`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Auto-start on fresh install; direct force-upgrade restarts automatically.
|
|
343
349
|
if (isUpgrade) {
|
|
344
350
|
try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME }); } catch {}
|
|
345
351
|
}
|
|
@@ -436,7 +442,7 @@ function showVersion() {
|
|
|
436
442
|
if (installed) {
|
|
437
443
|
console.log(` Installed version: ${installed}`);
|
|
438
444
|
if (installed !== pkg) {
|
|
439
|
-
console.log('\n Update available! Run: minions
|
|
445
|
+
console.log('\n Update available! Run: minions update');
|
|
440
446
|
} else {
|
|
441
447
|
console.log(' Up to date.');
|
|
442
448
|
}
|
|
@@ -449,7 +455,7 @@ function showVersion() {
|
|
|
449
455
|
const latest = execSync('npm view @yemi33/minions version', { encoding: 'utf8', timeout: 5000, windowsHide: true }).trim();
|
|
450
456
|
if (latest && latest !== pkg) {
|
|
451
457
|
console.log(`\n Latest on npm: ${latest}`);
|
|
452
|
-
console.log(' To update:
|
|
458
|
+
console.log(' To update: minions update');
|
|
453
459
|
}
|
|
454
460
|
} catch {} // offline or npm not available — skip silently
|
|
455
461
|
|
|
@@ -489,7 +495,7 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
489
495
|
|
|
490
496
|
Setup:
|
|
491
497
|
minions init Bootstrap ~/.minions/ (first time)
|
|
492
|
-
minions update Update to latest version (npm update +
|
|
498
|
+
minions update Update to latest version (npm update + one restart)
|
|
493
499
|
minions version Show installed vs package version
|
|
494
500
|
minions doctor Check prerequisites and runtime health
|
|
495
501
|
minions add <project-dir> Link a project (interactive)
|
|
@@ -538,7 +544,7 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
538
544
|
console.error(' npm update failed:', e.message);
|
|
539
545
|
process.exit(1);
|
|
540
546
|
}
|
|
541
|
-
execSync('minions init --force', { stdio: 'inherit', timeout: 120000 });
|
|
547
|
+
execSync('minions init --force --skip-start', { stdio: 'inherit', timeout: 120000 });
|
|
542
548
|
}
|
|
543
549
|
// Restart engine + dashboard so they pick up the new code
|
|
544
550
|
console.log('\n Restarting engine and dashboard...\n');
|
|
@@ -761,4 +767,3 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
761
767
|
console.log(' Run "minions help" for usage.\n');
|
|
762
768
|
process.exit(1);
|
|
763
769
|
}
|
|
764
|
-
|
package/dashboard.js
CHANGED
|
@@ -226,6 +226,40 @@ function _agentSessionIsDraining(agentId) {
|
|
|
226
226
|
return terminalIdx >= 0 && terminalIdx > lastSteer;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
function _dispatchBranch(item) {
|
|
230
|
+
const branch = item?.meta?.branch || item?.branch || item?.meta?.item?.branch || item?.meta?.item?.featureBranch;
|
|
231
|
+
return branch ? String(branch).replace(/^refs\/heads\//, '') : null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function _hasCachedResumeSession(agentId, activeDispatch, maxAgeMs = 2 * 60 * 60 * 1000) {
|
|
235
|
+
const sessionFile = safeJson(path.join(AGENTS_DIR, agentId, 'session.json'));
|
|
236
|
+
if (!sessionFile?.sessionId || !sessionFile.savedAt) return false;
|
|
237
|
+
const savedAtMs = new Date(sessionFile.savedAt).getTime();
|
|
238
|
+
if (!Number.isFinite(savedAtMs) || Date.now() - savedAtMs >= maxAgeMs) return false;
|
|
239
|
+
const dispatchBranch = _dispatchBranch(activeDispatch);
|
|
240
|
+
const sessionBranch = sessionFile.branch ? String(sessionFile.branch).replace(/^refs\/heads\//, '') : null;
|
|
241
|
+
return !!dispatchBranch && !!sessionBranch && dispatchBranch === sessionBranch;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function _steeringDeliveryState(agentId) {
|
|
245
|
+
const activeDispatch = (getDispatchQueue().active || []).find(d => d.agent === agentId);
|
|
246
|
+
if (!activeDispatch) return { deliveryStatus: 'queued', pendingDelivery: true };
|
|
247
|
+
|
|
248
|
+
const runtimeName = shared.resolveAgentCli(CONFIG.agents?.[agentId], CONFIG.engine);
|
|
249
|
+
try {
|
|
250
|
+
const runtime = require('./engine/runtimes').resolveRuntime(runtimeName);
|
|
251
|
+
if (runtime?.capabilities?.midRunSessionId === false && !_hasCachedResumeSession(agentId, activeDispatch)) {
|
|
252
|
+
return {
|
|
253
|
+
deliveryStatus: 'pending_checkpoint',
|
|
254
|
+
pendingDelivery: true,
|
|
255
|
+
detail: 'Runtime has not emitted a resumable session yet; delivery is pending until the next resumable checkpoint.',
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
} catch { /* unknown runtime: checkSteering will surface retry state */ }
|
|
259
|
+
|
|
260
|
+
return { deliveryStatus: 'queued', pendingDelivery: false };
|
|
261
|
+
}
|
|
262
|
+
|
|
229
263
|
const PLANS_DIR = path.join(MINIONS_DIR, 'plans');
|
|
230
264
|
const TEAMS_INBOX_PATH = path.join(ENGINE_DIR, 'teams-inbox.json');
|
|
231
265
|
|
|
@@ -5778,7 +5812,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5778
5812
|
const engine = getEngineState();
|
|
5779
5813
|
const agents = getAgents();
|
|
5780
5814
|
const health = {
|
|
5781
|
-
status: engine.state === 'running' ? 'healthy' : engine.state === 'paused' ? 'degraded' : 'stopped',
|
|
5815
|
+
status: engine.state === 'running' ? 'healthy' : (engine.state === 'paused' || engine.state === 'stopping') ? 'degraded' : 'stopped',
|
|
5782
5816
|
engine: { state: engine.state, pid: engine.pid },
|
|
5783
5817
|
agents: agents.map(a => ({ id: a.id, name: a.name, status: a.status })),
|
|
5784
5818
|
projects: PROJECTS.map(p => ({ name: p.name, reachable: fs.existsSync(p.localPath) })),
|
|
@@ -6219,6 +6253,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6219
6253
|
}
|
|
6220
6254
|
|
|
6221
6255
|
const entry = steering.writeSteeringMessage(agentId, text);
|
|
6256
|
+
const delivery = _steeringDeliveryState(agentId);
|
|
6222
6257
|
|
|
6223
6258
|
// Also append to live-output.log so it shows in the chat view
|
|
6224
6259
|
const liveLogPath = path.join(agentDir, 'live-output.log');
|
|
@@ -6226,7 +6261,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6226
6261
|
|
|
6227
6262
|
return jsonReply(res, 200, {
|
|
6228
6263
|
ok: true,
|
|
6229
|
-
message: 'Steering message queued',
|
|
6264
|
+
message: delivery.pendingDelivery ? 'Steering message pending delivery' : 'Steering message queued',
|
|
6265
|
+
...delivery,
|
|
6230
6266
|
file: entry?.file || null,
|
|
6231
6267
|
inboxCount: steering.listUnreadSteeringMessages(agentId).length,
|
|
6232
6268
|
});
|
package/engine/cli.js
CHANGED
|
@@ -52,6 +52,43 @@ function isEngineProcessAlive(control) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function createControlOwner(pid = process.pid) {
|
|
56
|
+
return { pid, ownerToken: `${pid}-${shared.uid()}` };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function controlBelongsToOwner(control, owner) {
|
|
60
|
+
return !!(
|
|
61
|
+
control &&
|
|
62
|
+
owner &&
|
|
63
|
+
owner.ownerToken &&
|
|
64
|
+
control.ownerToken === owner.ownerToken &&
|
|
65
|
+
control.pid === owner.pid
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function mutateControlForOwner(owner, mutator) {
|
|
70
|
+
let changed = false;
|
|
71
|
+
const control = mutateControl(current => {
|
|
72
|
+
if (!controlBelongsToOwner(current, owner)) return current;
|
|
73
|
+
changed = true;
|
|
74
|
+
return mutator(current);
|
|
75
|
+
});
|
|
76
|
+
return { changed, control };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function markControlStoppingForOwner(owner, stoppingAt) {
|
|
80
|
+
return mutateControlForOwner(owner, current => ({
|
|
81
|
+
...current,
|
|
82
|
+
state: 'stopping',
|
|
83
|
+
pid: owner.pid,
|
|
84
|
+
stopping_at: stoppingAt,
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function markControlStoppedForOwner(owner, stoppedAt) {
|
|
89
|
+
return mutateControlForOwner(owner, () => ({ state: 'stopped', stopped_at: stoppedAt }));
|
|
90
|
+
}
|
|
91
|
+
|
|
55
92
|
function handleCommand(cmd, args) {
|
|
56
93
|
if (!cmd) {
|
|
57
94
|
return commands.start();
|
|
@@ -320,7 +357,15 @@ const commands = {
|
|
|
320
357
|
}
|
|
321
358
|
let codeCommit = null;
|
|
322
359
|
try { codeCommit = require('child_process').execSync('git rev-parse --short HEAD', { cwd: path.resolve(__dirname, '..'), encoding: 'utf8', timeout: 5000, windowsHide: true }).trim(); } catch {}
|
|
323
|
-
|
|
360
|
+
const controlOwner = createControlOwner();
|
|
361
|
+
mutateControl(() => ({
|
|
362
|
+
state: 'running',
|
|
363
|
+
pid: controlOwner.pid,
|
|
364
|
+
ownerToken: controlOwner.ownerToken,
|
|
365
|
+
started_at: e.ts(),
|
|
366
|
+
codeVersion,
|
|
367
|
+
codeCommit
|
|
368
|
+
}));
|
|
324
369
|
// Keep .minions-version in sync so `minions version` stays accurate after git pulls
|
|
325
370
|
if (codeVersion) {
|
|
326
371
|
try { fs.writeFileSync(path.join(shared.MINIONS_DIR, '.minions-version'), codeVersion); } catch {}
|
|
@@ -688,11 +733,18 @@ const commands = {
|
|
|
688
733
|
clearInterval(fastPollTimer);
|
|
689
734
|
if (teamsInboxTimer) clearInterval(teamsInboxTimer);
|
|
690
735
|
for (const f of _watchedFiles) { try { fs.unwatchFile(f); } catch { /* cleanup */ } }
|
|
691
|
-
|
|
736
|
+
const stoppingAt = e.ts();
|
|
737
|
+
const stoppingWrite = markControlStoppingForOwner(controlOwner, stoppingAt);
|
|
738
|
+
if (!stoppingWrite.changed) {
|
|
739
|
+
e.log('warn', 'Graceful shutdown skipped control.json stopping transition; control file is owned by a different engine process');
|
|
740
|
+
}
|
|
692
741
|
e.log('info', `Graceful shutdown initiated (${signal})`);
|
|
693
742
|
|
|
694
743
|
if (e.activeProcesses.size === 0) {
|
|
695
|
-
|
|
744
|
+
const stoppedWrite = markControlStoppedForOwner(controlOwner, e.ts());
|
|
745
|
+
if (!stoppedWrite.changed) {
|
|
746
|
+
e.log('warn', 'Graceful shutdown skipped control.json stopped transition; control file is owned by a different engine process');
|
|
747
|
+
}
|
|
696
748
|
e.log('info', 'Graceful shutdown complete (no active agents)');
|
|
697
749
|
shared.flushLogs(); // drain buffered log entries before exit
|
|
698
750
|
console.log('No active agents — stopped.');
|
|
@@ -706,7 +758,10 @@ const commands = {
|
|
|
706
758
|
const poll = setInterval(() => {
|
|
707
759
|
if (e.activeProcesses.size === 0) {
|
|
708
760
|
clearInterval(poll);
|
|
709
|
-
|
|
761
|
+
const stoppedWrite = markControlStoppedForOwner(controlOwner, e.ts());
|
|
762
|
+
if (!stoppedWrite.changed) {
|
|
763
|
+
e.log('warn', 'Graceful shutdown skipped control.json stopped transition; control file is owned by a different engine process');
|
|
764
|
+
}
|
|
710
765
|
e.log('info', 'Graceful shutdown complete (all agents finished)');
|
|
711
766
|
shared.flushLogs(); // drain buffered log entries before exit
|
|
712
767
|
console.log('All agents finished — stopped.');
|
|
@@ -714,7 +769,10 @@ const commands = {
|
|
|
714
769
|
}
|
|
715
770
|
if (Date.now() >= deadline) {
|
|
716
771
|
clearInterval(poll);
|
|
717
|
-
|
|
772
|
+
const stoppedWrite = markControlStoppedForOwner(controlOwner, e.ts());
|
|
773
|
+
if (!stoppedWrite.changed) {
|
|
774
|
+
e.log('warn', 'Graceful shutdown skipped control.json stopped transition; control file is owned by a different engine process');
|
|
775
|
+
}
|
|
718
776
|
e.log('warn', `Graceful shutdown timed out after ${timeout / 1000}s with ${e.activeProcesses.size} agent(s) still active`);
|
|
719
777
|
shared.flushLogs(); // drain buffered log entries before exit
|
|
720
778
|
console.log(`Shutdown timeout (${timeout / 1000}s) — force exiting with ${e.activeProcesses.size} agent(s) still running.`);
|
|
@@ -1374,4 +1432,8 @@ module.exports = {
|
|
|
1374
1432
|
_parseRuntimeFlags,
|
|
1375
1433
|
_modelLooksIncompatible,
|
|
1376
1434
|
_applyRuntimeFlags,
|
|
1435
|
+
_createControlOwner: createControlOwner,
|
|
1436
|
+
_controlBelongsToOwner: controlBelongsToOwner,
|
|
1437
|
+
_markControlStoppingForOwner: markControlStoppingForOwner,
|
|
1438
|
+
_markControlStoppedForOwner: markControlStoppedForOwner,
|
|
1377
1439
|
};
|
package/engine.js
CHANGED
|
@@ -1129,14 +1129,35 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1129
1129
|
const procInfo = activeProcesses.get(id);
|
|
1130
1130
|
ackPendingSteeringFiles(agentId, procInfo, stdout);
|
|
1131
1131
|
|
|
1132
|
+
if (procInfo?._deferredSteeringFiles?.length && procInfo.sessionId) {
|
|
1133
|
+
const deferredPaths = new Set(procInfo._deferredSteeringFiles);
|
|
1134
|
+
const pendingDeferred = steering.listUnreadSteeringMessages(agentId)
|
|
1135
|
+
.filter(entry => deferredPaths.has(entry.path) && entry.message.trim());
|
|
1136
|
+
if (pendingDeferred.length > 0) {
|
|
1137
|
+
log('info', `Steering: delivering ${pendingDeferred.length} deferred message(s) for ${agentId} at resumable checkpoint`);
|
|
1138
|
+
procInfo._steeringMessage = pendingDeferred.map(entry => entry.message.trim()).join('\n\n');
|
|
1139
|
+
procInfo._steeringSessionId = procInfo.sessionId;
|
|
1140
|
+
procInfo._steeringEntry = pendingDeferred;
|
|
1141
|
+
procInfo._steeringDeferredCheckpoint = true;
|
|
1142
|
+
delete procInfo._deferredSteeringFiles;
|
|
1143
|
+
} else {
|
|
1144
|
+
delete procInfo._deferredSteeringFiles;
|
|
1145
|
+
}
|
|
1146
|
+
} else if (procInfo?._deferredSteeringFiles?.length) {
|
|
1147
|
+
log('warn', `Steering: ${agentId} exited before a resumable sessionId was available — message remains pending`);
|
|
1148
|
+
try { fs.appendFileSync(liveOutputPath, `\n[steering-pending] Agent exited before a resumable session was available. Your message remains unread and will be retried on the next dispatch.\n`); } catch {}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1132
1151
|
// Check if this was a steering kill — re-spawn with resume
|
|
1133
1152
|
if (procInfo?._steeringMessage) {
|
|
1134
1153
|
const steerMsg = procInfo._steeringMessage;
|
|
1135
1154
|
const steerSessionId = procInfo._steeringSessionId;
|
|
1136
1155
|
const steerEntry = procInfo._steeringEntry;
|
|
1156
|
+
const steeringDeferredCheckpoint = procInfo._steeringDeferredCheckpoint === true;
|
|
1137
1157
|
delete procInfo._steeringMessage;
|
|
1138
1158
|
delete procInfo._steeringSessionId;
|
|
1139
1159
|
delete procInfo._steeringEntry;
|
|
1160
|
+
delete procInfo._steeringDeferredCheckpoint;
|
|
1140
1161
|
|
|
1141
1162
|
// Guard: can't resume without a session
|
|
1142
1163
|
if (!steerSessionId) {
|
|
@@ -1247,9 +1268,13 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1247
1268
|
),
|
|
1248
1269
|
});
|
|
1249
1270
|
|
|
1250
|
-
//
|
|
1251
|
-
|
|
1252
|
-
|
|
1271
|
+
// Live steering kills discard partial old output. Deferred checkpoint
|
|
1272
|
+
// steering keeps the completed turn output so completion parsing still
|
|
1273
|
+
// sees the original work if the follow-up only acknowledges steering.
|
|
1274
|
+
if (!steeringDeferredCheckpoint) {
|
|
1275
|
+
stdout = '';
|
|
1276
|
+
stderr = '';
|
|
1277
|
+
}
|
|
1253
1278
|
sessionCaptureState.sessionLineBuffer = '';
|
|
1254
1279
|
// Re-wire stdout/stderr handlers (same as original)
|
|
1255
1280
|
resumeProc.stdout.on('data', (data) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1698",
|
|
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"
|