@yemi33/minions 0.1.1814 → 0.1.1816

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1816 (2026-05-09)
4
+
5
+ ### Features
6
+ - correlate CC action failures (#2254)
7
+
8
+ ## 0.1.1815 (2026-05-09)
9
+
10
+ ### Features
11
+ - verify restart health (#2258)
12
+
3
13
  ## 0.1.1814 (2026-05-09)
4
14
 
5
15
  ### 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')) {
@@ -826,11 +826,15 @@ async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
826
826
  var failures = evt.actionResults.filter(function(r) { return r && r.error; });
827
827
  var warnings = evt.actionResults.filter(function(r) { return r && r.warning; });
828
828
  if (failures.length > 0) {
829
- var failHtml = failures.map(function(r) { return '<li>' + escHtml(r.type || 'action') + ': ' + escHtml(r.error) + '</li>'; }).join('');
829
+ var failHtml = failures.map(function(r) {
830
+ return '<li>' + escHtml(r.type || (r.actionContext && r.actionContext.type) || 'action') + ': ' + escHtml(r.error) + _ccActionContextSuffix(r.actionContext) + '</li>';
831
+ }).join('');
830
832
  addMsg('system', '<div style="padding:6px 12px;font-size:11px;color:var(--red);background:var(--surface2);border-radius:6px;margin:4px 0">⚠️ ' + failures.length + ' action' + (failures.length > 1 ? 's' : '') + ' failed:<ul style="margin:4px 0 0 16px;padding:0">' + failHtml + '</ul></div>', false, activeTabId);
831
833
  }
832
834
  if (warnings.length > 0) {
833
- var warnHtml = warnings.map(function(r) { return '<li>' + escHtml(r.type || 'action') + ': ' + escHtml(r.warning) + '</li>'; }).join('');
835
+ var warnHtml = warnings.map(function(r) {
836
+ return '<li>' + escHtml(r.type || (r.actionContext && r.actionContext.type) || 'action') + ': ' + escHtml(r.warning) + _ccActionContextSuffix(r.actionContext) + '</li>';
837
+ }).join('');
834
838
  addMsg('system', '<div style="padding:6px 12px;font-size:11px;color:var(--orange);background:var(--surface2);border-radius:6px;margin:4px 0">ℹ️ ' + warnings.length + ' action' + (warnings.length > 1 ? '' : '') + ' completed with warnings:<ul style="margin:4px 0 0 16px;padding:0">' + warnHtml + '</ul></div>', false, activeTabId);
835
839
  }
836
840
  }
@@ -1012,6 +1016,28 @@ async function _ccFetch(url, body, method) {
1012
1016
  return res;
1013
1017
  }
1014
1018
 
1019
+ var CC_ACTION_CONTEXT_STALE_MS = 2 * 60 * 1000;
1020
+ function _ccActionContextIsStale(ctx, nowMs) {
1021
+ if (!ctx || !ctx.requestedAt) return false;
1022
+ var ts = Date.parse(ctx.requestedAt);
1023
+ if (!Number.isFinite(ts)) return false;
1024
+ var now = Number.isFinite(nowMs) ? nowMs : Date.now();
1025
+ return now - ts > CC_ACTION_CONTEXT_STALE_MS;
1026
+ }
1027
+
1028
+ function _ccActionContextSuffix(ctx, nowMs) {
1029
+ if (!ctx || typeof ctx !== 'object') return '';
1030
+ var parts = [];
1031
+ if (ctx.title) parts.push('action "' + escHtml(ctx.title) + '"');
1032
+ if (ctx.request) parts.push('request "' + escHtml(ctx.request) + '"');
1033
+ if (ctx.requestedAt) parts.push('started ' + escHtml(ctx.requestedAt));
1034
+ if (parts.length === 0) return '';
1035
+ var stale = _ccActionContextIsStale(ctx, nowMs)
1036
+ ? ' <strong style="color:var(--orange)">possibly stale</strong>'
1037
+ : '';
1038
+ return '<div style="font-size:10px;color:var(--muted);margin-top:2px">Context: ' + parts.join(' | ') + stale + '</div>';
1039
+ }
1040
+
1015
1041
  // Tag actions that the server already executed so ccExecuteAction skips the API call
1016
1042
  function _tagServerExecuted(actions, actionResults) {
1017
1043
  if (!actionResults || !Array.isArray(actionResults)) return;
@@ -1022,10 +1048,12 @@ function _tagServerExecuted(actions, actionResults) {
1022
1048
  if (r.id) actions[i]._serverId = r.id;
1023
1049
  if (r.warning) actions[i]._serverWarning = r.warning;
1024
1050
  if (r.duplicate) actions[i]._serverDuplicate = true;
1051
+ if (r.actionContext) actions[i]._serverContext = r.actionContext;
1025
1052
  if (r.reusedFromAction !== undefined) actions[i]._serverHidden = true;
1026
1053
  } else if (r && r.error) {
1027
1054
  actions[i]._serverExecuted = true;
1028
1055
  actions[i]._serverError = r.error;
1056
+ if (r.actionContext) actions[i]._serverContext = r.actionContext;
1029
1057
  }
1030
1058
  // clientExecuted: false means server didn't handle it — frontend must execute
1031
1059
  }
@@ -1040,7 +1068,8 @@ async function ccExecuteAction(action, targetTabId) {
1040
1068
  if (action._serverExecuted) {
1041
1069
  if (action._serverHidden) return;
1042
1070
  if (action._serverError) {
1043
- status.innerHTML = '&#10007; ' + escHtml(action.type) + ' failed: ' + escHtml(action._serverError);
1071
+ status.innerHTML = '&#10007; ' + escHtml(action.type) + ' failed: ' + escHtml(action._serverError) +
1072
+ _ccActionContextSuffix(action._serverContext);
1044
1073
  status.style.color = 'var(--red)';
1045
1074
  } else {
1046
1075
  var label = action._serverId ? escHtml(action._serverId) : escHtml(action.title || action.type);
@@ -1048,7 +1077,8 @@ async function ccExecuteAction(action, targetTabId) {
1048
1077
  var successLabel = serverActionType === 'dispatch' ? 'Dispatched' : serverActionType;
1049
1078
  status.innerHTML = '&#10003; ' + escHtml(successLabel) + ': <strong>' + label + '</strong>' +
1050
1079
  (action._serverDuplicate ? '<div style="font-size:10px;color:var(--orange);margin-top:2px">Already existed from a previous request; no duplicate work item was created.</div>' : '') +
1051
- (action._serverWarning ? '<div style="font-size:10px;color:var(--muted);margin-top:2px">' + escHtml(action._serverWarning) + '</div>' : '');
1080
+ (action._serverWarning ? '<div style="font-size:10px;color:var(--muted);margin-top:2px">' + escHtml(action._serverWarning) + '</div>' : '') +
1081
+ _ccActionContextSuffix(action._serverContext);
1052
1082
  status.style.color = 'var(--green)';
1053
1083
  }
1054
1084
  ccAddMessage('action', status.outerHTML, false, targetTabId);
package/dashboard.js CHANGED
@@ -2202,6 +2202,34 @@ function _actionsWithIntentFallback(actions, opts = {}) {
2202
2202
  return [action];
2203
2203
  }
2204
2204
 
2205
+ function _ccActionContextValue(action) {
2206
+ if (!action || typeof action !== 'object') return '';
2207
+ const candidates = [action.title, action.id, action.file, action.target, action.pr, action.endpoint];
2208
+ for (const value of candidates) {
2209
+ const cleaned = _ccCleanIntentString(value, 160);
2210
+ if (cleaned) return cleaned;
2211
+ }
2212
+ return '';
2213
+ }
2214
+
2215
+ function _annotateCCActionResults(actions, actionResults, opts = {}) {
2216
+ if (!Array.isArray(actionResults)) return actionResults;
2217
+ const requestedAt = _ccCleanIntentString(opts.requestedAt || new Date().toISOString(), 80);
2218
+ const request = _ccCleanIntentString(opts.message, 180);
2219
+ return actionResults.map((result, idx) => {
2220
+ if (!result || typeof result !== 'object') return result;
2221
+ const action = Array.isArray(actions) ? normalizeCCAction(actions[idx]) : null;
2222
+ const context = {
2223
+ type: _ccCleanIntentString(result.type || action?.type || 'action', 80) || 'action',
2224
+ };
2225
+ const title = _ccActionContextValue(action);
2226
+ if (title) context.title = title;
2227
+ if (request) context.request = request;
2228
+ if (requestedAt) context.requestedAt = requestedAt;
2229
+ return { ...result, actionContext: context };
2230
+ });
2231
+ }
2232
+
2205
2233
  function parseCCActions(text) {
2206
2234
  let actions = [];
2207
2235
  let displayText = stripCCActionsForDisplay(text);
@@ -7100,6 +7128,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7100
7128
  try {
7101
7129
  const body = await readBody(req);
7102
7130
  if (!body.message) return jsonReply(res, 400, { error: 'message required' });
7131
+ const actionRequestedAt = new Date().toISOString();
7103
7132
 
7104
7133
  // Per-tab concurrency guard
7105
7134
  tabId = body.tabId || 'default';
@@ -7155,7 +7184,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7155
7184
  { message: body.message, intentMetadata: body.intentMetadata, source: 'command-center', answerText: parsed.text, toolUses }
7156
7185
  );
7157
7186
  if (parsed.actions.length > 0) {
7158
- parsed.actionResults = await executeCCActions(parsed.actions);
7187
+ parsed.actionResults = _annotateCCActionResults(
7188
+ parsed.actions,
7189
+ await executeCCActions(parsed.actions),
7190
+ { message: body.message, requestedAt: actionRequestedAt }
7191
+ );
7159
7192
  }
7160
7193
  // Mirror only user-facing text to Teams; never send the internal action block.
7161
7194
  if (!tabId.startsWith('teams-')) {
@@ -7259,6 +7292,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7259
7292
  const body = await readBody(req);
7260
7293
  if (!body.message && !body.reconnect) { res.statusCode = 400; res.end('message required'); return; }
7261
7294
  tabId = body.tabId || 'default';
7295
+ const actionRequestedAt = new Date().toISOString();
7262
7296
  if (body.reconnect) {
7263
7297
  const live = _getCcLiveStream(tabId);
7264
7298
  if (!live) { res.statusCode = 409; res.end('No live command-center response to reconnect'); return; }
@@ -7487,7 +7521,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7487
7521
  );
7488
7522
  let actionResults;
7489
7523
  if (actions.length > 0) {
7490
- actionResults = await executeCCActions(actions);
7524
+ actionResults = _annotateCCActionResults(
7525
+ actions,
7526
+ await executeCCActions(actions),
7527
+ { message: body.message, requestedAt: actionRequestedAt }
7528
+ );
7491
7529
  }
7492
7530
  const donePayload = { type: 'done', text: displayText, actions, actionResults, sessionId: responseSessionId, newSession: !wasResume };
7493
7531
  // Issue #1834: surface action JSON parse failures so the UI can warn
@@ -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:27:55.877Z"
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.1816",
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"