@yemi33/minions 0.1.1696 → 0.1.1697

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,8 +1,9 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.1696 (2026-05-04)
3
+ ## 0.1.1697 (2026-05-04)
4
4
 
5
5
  ### Features
6
+ - guard engine shutdown ownership (#2021)
6
7
  - preserve doc-chat final payload (#2019)
7
8
 
8
9
  ## 0.1.1695 (2026-05-04)
package/dashboard.js CHANGED
@@ -5778,7 +5778,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5778
5778
  const engine = getEngineState();
5779
5779
  const agents = getAgents();
5780
5780
  const health = {
5781
- status: engine.state === 'running' ? 'healthy' : engine.state === 'paused' ? 'degraded' : 'stopped',
5781
+ status: engine.state === 'running' ? 'healthy' : (engine.state === 'paused' || engine.state === 'stopping') ? 'degraded' : 'stopped',
5782
5782
  engine: { state: engine.state, pid: engine.pid },
5783
5783
  agents: agents.map(a => ({ id: a.id, name: a.name, status: a.status })),
5784
5784
  projects: PROJECTS.map(p => ({ name: p.name, reachable: fs.existsSync(p.localPath) })),
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:13:46.106Z"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1696",
3
+ "version": "0.1.1697",
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"