@yemi33/minions 0.1.1684 → 0.1.1686
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 +10 -0
- package/engine/copilot-models.json +1 -1
- package/engine/spawn-agent.js +82 -5
- package/engine/timeout.js +38 -0
- package/engine.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/engine/spawn-agent.js
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
const fs = require('fs');
|
|
36
36
|
const os = require('os');
|
|
37
37
|
const path = require('path');
|
|
38
|
+
const { execSync } = require('child_process');
|
|
38
39
|
const { runFile, cleanChildEnv, killGracefully, killImmediate, ts } = require('./shared');
|
|
39
40
|
const { resolveRuntime } = require('./runtimes');
|
|
40
41
|
|
|
@@ -128,6 +129,78 @@ function normalizeRuntimeExit(code, signal) {
|
|
|
128
129
|
return 1;
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
function injectAdoTokenEnv(env, { execSync: _execSync = execSync, warn = (msg) => process.stderr.write(msg + '\n') } = {}) {
|
|
133
|
+
let token;
|
|
134
|
+
try {
|
|
135
|
+
token = String(_execSync('azureauth ado token --mode iwa --mode broker --output token --timeout 1', {
|
|
136
|
+
encoding: 'utf8',
|
|
137
|
+
timeout: 30000,
|
|
138
|
+
windowsHide: true,
|
|
139
|
+
}) || '').trim();
|
|
140
|
+
} catch (err) {
|
|
141
|
+
warn(`spawn-agent.js: ADO token fetch failed: ${err.message}`);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
if (!token || !token.startsWith('eyJ')) {
|
|
145
|
+
warn('spawn-agent.js: invalid ADO token from azureauth; continuing without Azure DevOps PAT env');
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
env.AZURE_DEVOPS_EXT_PAT = token;
|
|
149
|
+
env.AZURE_DEVOPS_EXT_AZURE_RM_PAT = token;
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const PROCESS_EXIT_SENTINEL_FLUSH_TIMEOUT_MS = 2000;
|
|
154
|
+
|
|
155
|
+
function formatProcessExitSentinel(exitCode, signal) {
|
|
156
|
+
return `\n[process-exit] code=${exitCode}${signal ? ` signal=${signal}` : ''}\n`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function _appendSentinelFallback(outputPath, sentinel) {
|
|
160
|
+
if (!outputPath) return false;
|
|
161
|
+
try {
|
|
162
|
+
fs.appendFileSync(outputPath, sentinel);
|
|
163
|
+
return true;
|
|
164
|
+
} catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function _writeStdoutWithTimeout(stdout, sentinel, timeoutMs) {
|
|
170
|
+
return new Promise((resolve) => {
|
|
171
|
+
let settled = false;
|
|
172
|
+
const finish = (flushed) => {
|
|
173
|
+
if (settled) return;
|
|
174
|
+
settled = true;
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
resolve(flushed);
|
|
177
|
+
};
|
|
178
|
+
const timer = setTimeout(() => finish(false), Math.max(0, timeoutMs));
|
|
179
|
+
try {
|
|
180
|
+
if (!stdout || typeof stdout.write !== 'function') {
|
|
181
|
+
finish(false);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
stdout.write(sentinel, () => finish(true));
|
|
185
|
+
} catch {
|
|
186
|
+
finish(false);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function writeProcessExitSentinel({
|
|
192
|
+
exitCode,
|
|
193
|
+
signal = null,
|
|
194
|
+
stdout = process.stdout,
|
|
195
|
+
outputPath = process.env.MINIONS_LIVE_OUTPUT_PATH,
|
|
196
|
+
timeoutMs = PROCESS_EXIT_SENTINEL_FLUSH_TIMEOUT_MS,
|
|
197
|
+
} = {}) {
|
|
198
|
+
const sentinel = formatProcessExitSentinel(exitCode, signal);
|
|
199
|
+
const stdoutFlushed = await _writeStdoutWithTimeout(stdout, sentinel, timeoutMs);
|
|
200
|
+
const outputPathWritten = stdoutFlushed ? false : _appendSentinelFallback(outputPath, sentinel);
|
|
201
|
+
return { sentinel, stdoutFlushed, outputPathWritten };
|
|
202
|
+
}
|
|
203
|
+
|
|
131
204
|
// ─── Main script execution ──────────────────────────────────────────────────
|
|
132
205
|
|
|
133
206
|
function _installHint(name, runtime) {
|
|
@@ -149,6 +222,7 @@ function main() {
|
|
|
149
222
|
const { promptFile, sysPromptFile, runtimeName, opts, passthrough } = parsed;
|
|
150
223
|
|
|
151
224
|
const env = cleanChildEnv();
|
|
225
|
+
injectAdoTokenEnv(env);
|
|
152
226
|
|
|
153
227
|
let runtime;
|
|
154
228
|
try { runtime = resolveRuntime(runtimeName); }
|
|
@@ -287,20 +361,23 @@ function main() {
|
|
|
287
361
|
}, MCP_STARTUP_TIMEOUT);
|
|
288
362
|
proc.stdout.once('data', () => { gotFirstOutput = true; clearTimeout(startupTimer); });
|
|
289
363
|
|
|
290
|
-
proc.on('close', (code, signal) => {
|
|
364
|
+
proc.on('close', async (code, signal) => {
|
|
291
365
|
clearTimeout(startupTimer);
|
|
292
366
|
const exitCode = normalizeRuntimeExit(code, signal);
|
|
293
|
-
|
|
294
|
-
try { process.stdout.write(`\n[process-exit] code=${exitCode}${signal ? ` signal=${signal}` : ''}\n`); } catch { /* stdout may be closed */ }
|
|
367
|
+
const sentinelResult = await writeProcessExitSentinel({ exitCode, signal });
|
|
295
368
|
fs.appendFileSync(debugPath, `EXIT: code=${exitCode}${signal ? ` signal=${signal}` : ''}\nSTDERR: ${stderrBuf.slice(0, 500)}\n`);
|
|
369
|
+
if (!sentinelResult.stdoutFlushed && sentinelResult.outputPathWritten) {
|
|
370
|
+
fs.appendFileSync(debugPath, `EXIT SENTINEL FALLBACK: ${process.env.MINIONS_LIVE_OUTPUT_PATH}\n`);
|
|
371
|
+
}
|
|
296
372
|
process.exit(exitCode);
|
|
297
373
|
});
|
|
298
|
-
proc.on('error', (err) => {
|
|
374
|
+
proc.on('error', async (err) => {
|
|
299
375
|
fs.appendFileSync(debugPath, `ERROR: ${err.message}\n`);
|
|
376
|
+
await writeProcessExitSentinel({ exitCode: 1 });
|
|
300
377
|
process.exit(1);
|
|
301
378
|
});
|
|
302
379
|
}
|
|
303
380
|
|
|
304
|
-
module.exports = { parseSpawnArgs, buildSpawnInvocation, normalizeRuntimeExit };
|
|
381
|
+
module.exports = { parseSpawnArgs, buildSpawnInvocation, normalizeRuntimeExit, injectAdoTokenEnv, writeProcessExitSentinel };
|
|
305
382
|
|
|
306
383
|
if (require.main === module) main();
|
package/engine/timeout.js
CHANGED
|
@@ -217,6 +217,38 @@ function parseProcessExitCode(logText) {
|
|
|
217
217
|
return lastMatch[1] === 'spawn-failed' ? -1 : parseInt(lastMatch[1], 10);
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
function terminalResultIndicatesError(obj) {
|
|
221
|
+
const subtype = String(obj?.subtype || '');
|
|
222
|
+
const terminalReason = String(obj?.terminal_reason || obj?.terminalReason || '');
|
|
223
|
+
return obj?.is_error === true ||
|
|
224
|
+
/^error/i.test(subtype) ||
|
|
225
|
+
/max[_-]?turns|error|fail|cancel|timeout/i.test(terminalReason);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function parseTerminalResultFallbackExitCode(logText) {
|
|
229
|
+
if (!logText) return null;
|
|
230
|
+
let exitCode = null;
|
|
231
|
+
for (const line of String(logText).split(/\r?\n/)) {
|
|
232
|
+
const trimmed = line.trim();
|
|
233
|
+
if (!trimmed || !trimmed.includes('"result"') ||
|
|
234
|
+
(!trimmed.includes('terminal_reason') && !trimmed.includes('terminalReason'))) continue;
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const obj = JSON.parse(trimmed);
|
|
238
|
+
if (obj?.type === 'result' && (obj.terminal_reason || obj.terminalReason) && terminalResultIndicatesError(obj)) {
|
|
239
|
+
exitCode = 1;
|
|
240
|
+
}
|
|
241
|
+
continue;
|
|
242
|
+
} catch { /* fall through to regex fallback for diagnostic-prefixed JSON */ }
|
|
243
|
+
|
|
244
|
+
if (/"type"\s*:\s*"result"/.test(trimmed) &&
|
|
245
|
+
/"terminal_?reason"\s*:\s*"[^"]*(?:max[_-]?turns|error|fail|cancel|timeout)[^"]*"/i.test(trimmed)) {
|
|
246
|
+
exitCode = 1;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return exitCode;
|
|
250
|
+
}
|
|
251
|
+
|
|
220
252
|
function checkTimeouts(config) {
|
|
221
253
|
const activeProcesses = engine().activeProcesses;
|
|
222
254
|
const engineRestartGraceUntil = engine().engineRestartGraceUntil;
|
|
@@ -411,6 +443,12 @@ function checkTimeouts(config) {
|
|
|
411
443
|
completeFromOutput(item, liveLogPath, processExitCode, fullLog, hasProcess);
|
|
412
444
|
continue;
|
|
413
445
|
}
|
|
446
|
+
const terminalResultExitCode = parseTerminalResultFallbackExitCode(fullLog);
|
|
447
|
+
if (terminalResultExitCode !== null) {
|
|
448
|
+
log('info', `Agent ${item.agent} (${item.id}) completed via stale terminal result fallback (exit code ${terminalResultExitCode})`);
|
|
449
|
+
completeFromOutput(item, liveLogPath, terminalResultExitCode, fullLog, hasProcess);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
414
452
|
} catch (e) { log('warn', 'orphan final output completion scan: ' + e.message); }
|
|
415
453
|
|
|
416
454
|
// No tracked process AND no recent output past stale-orphan timeout AND (grace period expired OR confirmed-dead at restart) → orphaned
|
package/engine.js
CHANGED
|
@@ -995,6 +995,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
995
995
|
// 2. Log has stub only → process started but died before its first write
|
|
996
996
|
// 3. Log has stub + ... → process alive but hung (the only case that warrants orphan kill+retry)
|
|
997
997
|
const liveOutputPath = path.join(AGENTS_DIR, agentId, 'live-output.log');
|
|
998
|
+
childEnv.MINIONS_LIVE_OUTPUT_PATH = liveOutputPath;
|
|
998
999
|
|
|
999
1000
|
// Rotate previous live output to preserve session history (fixes #543: orphan recovery overwrites)
|
|
1000
1001
|
// Only rotate if the existing file has meaningful content (beyond just the header stub)
|
|
@@ -1186,6 +1187,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1186
1187
|
const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
|
|
1187
1188
|
const childEnv = shared.cleanChildEnv();
|
|
1188
1189
|
if (completionReportPath) childEnv.MINIONS_COMPLETION_REPORT = completionReportPath;
|
|
1190
|
+
childEnv.MINIONS_LIVE_OUTPUT_PATH = liveOutputPath;
|
|
1189
1191
|
// Inject cached ADO token for steering session too (#998)
|
|
1190
1192
|
try {
|
|
1191
1193
|
const adoToken = await getAdoToken();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1686",
|
|
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"
|