@yemi33/minions 0.1.1814 → 0.1.1815

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.1815 (2026-05-09)
4
+
5
+ ### Features
6
+ - verify restart health (#2258)
7
+
3
8
  ## 0.1.1814 (2026-05-09)
4
9
 
5
10
  ### Features
package/bin/minions.js CHANGED
@@ -40,6 +40,7 @@ const { spawn, spawnSync, execSync } = require('child_process');
40
40
 
41
41
  const PKG_ROOT = path.resolve(__dirname, '..');
42
42
  const shared = require(path.join(PKG_ROOT, 'engine', 'shared'));
43
+ const { waitForRestartHealth, formatRestartHealthError } = require(path.join(PKG_ROOT, 'engine', 'restart-health'));
43
44
  const DASH_PORT = 7331;
44
45
  const DASHBOARD_BROWSER_PRESENCE_MAX_AGE_MS = 45000;
45
46
 
@@ -708,7 +709,22 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
708
709
  console.log(`\n Engine started (PID: ${engineProc.pid})`);
709
710
  const dashProc = spawnDashboard(suppressDashboardOpen);
710
711
  console.log(` Dashboard started (PID: ${dashProc.pid})`);
711
- console.log(` Dashboard: http://localhost:${DASH_PORT}\n`);
712
+ console.log(` Dashboard: http://localhost:${DASH_PORT}`);
713
+ console.log(' Verifying restart health...');
714
+ void (async () => {
715
+ const result = await waitForRestartHealth({
716
+ minionsHome: MINIONS_HOME,
717
+ dashboardUrl: `http://127.0.0.1:${DASH_PORT}/api/health`,
718
+ });
719
+ if (!result.ok) {
720
+ console.error(formatRestartHealthError(result));
721
+ process.exit(1);
722
+ }
723
+ console.log(` Restart verified: engine PID ${result.engine.pid}; dashboard healthy.\n`);
724
+ })().catch(err => {
725
+ console.error(`\n ERROR: Restart verification failed: ${err.message}\n`);
726
+ process.exit(1);
727
+ });
712
728
  } else if (cmd === 'nuke') {
713
729
  ensureInstalled();
714
730
  if (!rest.includes('--confirm')) {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-09T15:01:20.730Z"
4
+ "cachedAt": "2026-05-09T15:24:57.566Z"
5
5
  }
@@ -0,0 +1,169 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const http = require('http');
4
+ const https = require('https');
5
+ const { execSync } = require('child_process');
6
+
7
+ const DEFAULT_RESTART_HEALTH_TIMEOUT_MS = 15000;
8
+ const DEFAULT_RESTART_HEALTH_INTERVAL_MS = 250;
9
+
10
+ function sleep(ms) {
11
+ return new Promise(resolve => setTimeout(resolve, ms));
12
+ }
13
+
14
+ function readEngineControl(minionsHome) {
15
+ try {
16
+ return JSON.parse(fs.readFileSync(path.join(minionsHome, 'engine', 'control.json'), 'utf8'));
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ function normalizePid(pid) {
23
+ const n = Number(pid);
24
+ return Number.isInteger(n) && n > 0 ? n : null;
25
+ }
26
+
27
+ function isProcessAlive(pid) {
28
+ const n = normalizePid(pid);
29
+ if (!n || n === process.pid) return false;
30
+ try {
31
+ if (process.platform === 'win32') {
32
+ const out = execSync(`tasklist /FI "PID eq ${n}" /NH`, {
33
+ encoding: 'utf8',
34
+ windowsHide: true,
35
+ timeout: 3000,
36
+ });
37
+ return new RegExp(`\\b${n}\\b`).test(out) && out.toLowerCase().includes('node');
38
+ }
39
+ process.kill(n, 0);
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ function httpGetJson(url, timeoutMs = 1000) {
47
+ return new Promise(resolve => {
48
+ let settled = false;
49
+ const parsed = new URL(url);
50
+ const client = parsed.protocol === 'https:' ? https : http;
51
+ const req = client.get(parsed, { timeout: timeoutMs }, res => {
52
+ let body = '';
53
+ res.setEncoding('utf8');
54
+ res.on('data', chunk => { body += chunk; });
55
+ res.on('end', () => {
56
+ if (settled) return;
57
+ settled = true;
58
+ let json = null;
59
+ try { json = body ? JSON.parse(body) : null; }
60
+ catch (err) {
61
+ resolve({ ok: false, statusCode: res.statusCode, error: err, body });
62
+ return;
63
+ }
64
+ resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, statusCode: res.statusCode, json, body });
65
+ });
66
+ });
67
+ req.on('timeout', () => {
68
+ if (settled) return;
69
+ settled = true;
70
+ req.destroy();
71
+ resolve({ ok: false, error: new Error(`timed out after ${timeoutMs}ms`) });
72
+ });
73
+ req.on('error', err => {
74
+ if (settled) return;
75
+ settled = true;
76
+ resolve({ ok: false, error: err });
77
+ });
78
+ });
79
+ }
80
+
81
+ async function checkRestartHealth(options = {}) {
82
+ const {
83
+ minionsHome,
84
+ dashboardUrl = 'http://127.0.0.1:7331/api/health',
85
+ readControl = readEngineControl,
86
+ isProcessAlive: isAlive = isProcessAlive,
87
+ httpGetJson: getJson = httpGetJson,
88
+ } = options;
89
+
90
+ const control = readControl(minionsHome);
91
+ const pid = normalizePid(control && control.pid);
92
+ const engineAlive = pid ? isAlive(pid) : false;
93
+ const engineOk = control && control.state === 'running' && engineAlive;
94
+
95
+ const dashboard = await getJson(dashboardUrl, 1000);
96
+ const dashboardStatus = dashboard && dashboard.json && dashboard.json.status;
97
+ const dashboardOk = !!(dashboard && dashboard.ok && dashboardStatus === 'healthy');
98
+
99
+ const errors = [];
100
+ if (!engineOk) {
101
+ const state = control && control.state ? control.state : 'unknown';
102
+ const pidLabel = pid || 'none';
103
+ errors.push(`Engine is not healthy (state=${state}, pid=${pidLabel}, alive=${engineAlive ? 'yes' : 'no'})`);
104
+ }
105
+ if (!dashboardOk) {
106
+ const detail = dashboard && dashboard.error
107
+ ? dashboard.error.message
108
+ : dashboard && dashboard.statusCode
109
+ ? `HTTP ${dashboard.statusCode}${dashboardStatus ? `, status=${dashboardStatus}` : ''}`
110
+ : 'no response';
111
+ errors.push(`Dashboard failed health check at ${dashboardUrl} (${detail})`);
112
+ }
113
+
114
+ return {
115
+ ok: engineOk && dashboardOk,
116
+ engine: { state: control && control.state, pid, alive: engineAlive },
117
+ dashboard: { url: dashboardUrl, ok: !!(dashboard && dashboard.ok), statusCode: dashboard && dashboard.statusCode, status: dashboardStatus },
118
+ errors,
119
+ };
120
+ }
121
+
122
+ async function waitForRestartHealth(options = {}) {
123
+ const timeoutMs = options.timeoutMs ?? DEFAULT_RESTART_HEALTH_TIMEOUT_MS;
124
+ const intervalMs = options.intervalMs ?? DEFAULT_RESTART_HEALTH_INTERVAL_MS;
125
+ const maxAttempts = normalizePid(options.maxAttempts);
126
+ const started = Date.now();
127
+ let attempts = 0;
128
+ let last = null;
129
+
130
+ while (true) {
131
+ attempts++;
132
+ last = await checkRestartHealth(options);
133
+ last.attempts = attempts;
134
+ last.elapsedMs = Date.now() - started;
135
+ if (last.ok) return last;
136
+ if (maxAttempts && attempts >= maxAttempts) break;
137
+ const remainingMs = timeoutMs - (Date.now() - started);
138
+ if (!maxAttempts && remainingMs <= 0) break;
139
+ await sleep(maxAttempts ? intervalMs : Math.min(intervalMs, remainingMs));
140
+ }
141
+
142
+ return last || {
143
+ ok: false,
144
+ attempts,
145
+ elapsedMs: Date.now() - started,
146
+ errors: ['Restart health check did not run'],
147
+ };
148
+ }
149
+
150
+ function formatRestartHealthError(result) {
151
+ const elapsed = typeof result.elapsedMs === 'number' ? `${result.elapsedMs}ms` : 'unknown time';
152
+ const attempts = result.attempts || 0;
153
+ const details = (result.errors || ['Unknown restart verification failure']).map(err => ` - ${err}`).join('\n');
154
+ return `\n ERROR: Restart verification failed after ${elapsed} (${attempts} attempt${attempts === 1 ? '' : 's'}).\n${details}\n`;
155
+ }
156
+
157
+ module.exports = {
158
+ DEFAULT_RESTART_HEALTH_TIMEOUT_MS,
159
+ DEFAULT_RESTART_HEALTH_INTERVAL_MS,
160
+ checkRestartHealth,
161
+ waitForRestartHealth,
162
+ formatRestartHealthError,
163
+ _private: {
164
+ httpGetJson,
165
+ isProcessAlive,
166
+ readEngineControl,
167
+ normalizePid,
168
+ },
169
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1814",
3
+ "version": "0.1.1815",
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"