@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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1698 (2026-05-04)
4
+
5
+ ### Features
6
+ - fix update restart and Copilot steering (#2025)
7
+
3
8
  ## 0.1.1696 (2026-05-04)
4
9
 
5
10
  ### Features
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
- // Auto-start on fresh install; force-upgrade restarts automatically.
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 init --force');
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: npm update -g @yemi33/minions && minions init --force');
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 + init --force)
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
- mutateControl(() => ({ state: 'running', pid: process.pid, started_at: e.ts(), codeVersion, codeCommit }));
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
- mutateControl(() => ({ state: 'stopping', pid: process.pid, stopping_at: e.ts() }));
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
- mutateControl(() => ({ state: 'stopped', stopped_at: e.ts() }));
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
- mutateControl(() => ({ state: 'stopped', stopped_at: e.ts() }));
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
- mutateControl(() => ({ state: 'stopped', stopped_at: e.ts() }));
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
  };
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-04T06:12:43.776Z"
4
+ "cachedAt": "2026-05-04T06:31:58.762Z"
5
5
  }
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
- // Reset output buffers so post-completion parsing only sees the resumed session
1251
- stdout = '';
1252
- stderr = '';
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.1696",
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"