@yemi33/minions 0.1.1046 → 0.1.1047
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 +2 -1
- package/engine/timeout.js +13 -2
- package/engine.js +38 -17
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/engine/timeout.js
CHANGED
|
@@ -308,16 +308,27 @@ function checkTimeouts(config) {
|
|
|
308
308
|
const procInfo = activeProcesses.get(item.id);
|
|
309
309
|
if (procInfo?._steeringAt && Date.now() - procInfo._steeringAt < 60000) continue;
|
|
310
310
|
|
|
311
|
+
// Capture live-output.log file state for orphan/hung diagnostics (#W-mo248lkjwgsu).
|
|
312
|
+
// Three distinguishable failure modes:
|
|
313
|
+
// logExists=false → spawn call itself threw, no log ever written
|
|
314
|
+
// logExists=true, size~stub → process died at startup (stub-only content)
|
|
315
|
+
// logExists=true, size>stub → genuine hang (process alive, wrote output, then stopped)
|
|
316
|
+
let _logState = 'logExists=false logSize=0';
|
|
317
|
+
try {
|
|
318
|
+
const lst = fs.statSync(liveLogPath);
|
|
319
|
+
_logState = `logExists=true logSize=${lst.size}`;
|
|
320
|
+
} catch { /* ENOENT — keep default */ }
|
|
321
|
+
|
|
311
322
|
if (!hasProcess && silentMs > effectiveTimeout && (Date.now() > engineRestartGraceUntil || engineRestartGraceExempt?.has(item.id))) {
|
|
312
323
|
// No tracked process AND no recent output past effective timeout AND (grace period expired OR confirmed-dead at restart) → orphaned
|
|
313
|
-
log('warn', `Orphan detected: ${item.agent} (${item.id}) — no process tracked, silent for ${silentSec}s${isBlocking ? ' (blocking timeout exceeded)' : ''}`);
|
|
324
|
+
log('warn', `Orphan detected: ${item.agent} (${item.id}) — no process tracked, silent for ${silentSec}s${isBlocking ? ' (blocking timeout exceeded)' : ''} [${_logState}]`);
|
|
314
325
|
dispatch().updateAgentStatus(item.id, AGENT_STATUS.TIMED_OUT, `Orphaned — no process, silent for ${silentSec}s`);
|
|
315
326
|
// Clear session so retry starts fresh
|
|
316
327
|
try { shared.safeUnlink(path.join(AGENTS_DIR, item.agent, 'session.json')); } catch {}
|
|
317
328
|
deadItems.push({ item, reason: `Orphaned — no process, silent for ${silentSec}s` });
|
|
318
329
|
} else if (hasProcess && silentMs > effectiveTimeout) {
|
|
319
330
|
// Has process but no output past effective timeout → hung
|
|
320
|
-
log('warn', `Hung agent: ${item.agent} (${item.id}) — process exists but no output for ${silentSec}s${isBlocking ? ' (blocking timeout exceeded)' : ''}`);
|
|
331
|
+
log('warn', `Hung agent: ${item.agent} (${item.id}) — process exists but no output for ${silentSec}s${isBlocking ? ' (blocking timeout exceeded)' : ''} [${_logState}]`);
|
|
321
332
|
dispatch().updateAgentStatus(item.id, AGENT_STATUS.TIMED_OUT, `Hung — no output for ${silentSec}s`);
|
|
322
333
|
const procInfo = activeProcesses.get(item.id);
|
|
323
334
|
if (procInfo) {
|
package/engine.js
CHANGED
|
@@ -871,25 +871,16 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
871
871
|
const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
|
|
872
872
|
const spawnArgs = [spawnScript, promptPath, sysPromptPath, ...args];
|
|
873
873
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
const MAX_OUTPUT = 1024 * 1024; // 1MB
|
|
881
|
-
let stdout = '';
|
|
882
|
-
let stderr = '';
|
|
883
|
-
let lastOutputAt = Date.now();
|
|
884
|
-
let heartbeatTimer = null;
|
|
885
|
-
let _trustCheckDone = false;
|
|
886
|
-
const _spawnTime = Date.now();
|
|
887
|
-
|
|
888
|
-
// Live output file — written as data arrives so dashboard can tail it
|
|
874
|
+
// Live output file — stamped BEFORE child process is spawned (#W-mo248lkjwgsu).
|
|
875
|
+
// Writing the stub pre-spawn lets the orphan detector distinguish three failure modes
|
|
876
|
+
// that otherwise look identical:
|
|
877
|
+
// 1. No log file → spawn call itself threw before reaching this line
|
|
878
|
+
// 2. Log has stub only → process started but died before its first write
|
|
879
|
+
// 3. Log has stub + ... → process alive but hung (the only case that warrants orphan kill+retry)
|
|
889
880
|
const liveOutputPath = path.join(AGENTS_DIR, agentId, 'live-output.log');
|
|
890
881
|
|
|
891
882
|
// Rotate previous live output to preserve session history (fixes #543: orphan recovery overwrites)
|
|
892
|
-
// Only rotate if the existing file has meaningful content (beyond just the header)
|
|
883
|
+
// Only rotate if the existing file has meaningful content (beyond just the header stub)
|
|
893
884
|
const LIVE_OUTPUT_SPARSE_THRESHOLD = 500; // bytes — header + init JSON is typically < 500
|
|
894
885
|
try {
|
|
895
886
|
if (fs.existsSync(liveOutputPath)) {
|
|
@@ -901,7 +892,37 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
901
892
|
}
|
|
902
893
|
} catch { /* rotation is best-effort — overwrite still happens below */ }
|
|
903
894
|
|
|
904
|
-
|
|
895
|
+
// Stamp the log synchronously before spawn. If the synchronous write throws,
|
|
896
|
+
// we still attempt to spawn (log-file stamp failure must not block the agent),
|
|
897
|
+
// but we note it so the diagnostic trail isn't silent either.
|
|
898
|
+
try {
|
|
899
|
+
safeWrite(liveOutputPath, `# Live output for ${agentId} — ${id}\n# Started: ${startedAt}\n# Task: ${dispatchItem.task}\n[${new Date().toISOString()}] spawn: agent=${agentId} item=${id}\n\n`);
|
|
900
|
+
} catch (stubErr) {
|
|
901
|
+
log('warn', `Failed to stamp live-output stub for ${agentId} (${id}): ${stubErr.message}`);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
let proc;
|
|
905
|
+
try {
|
|
906
|
+
proc = runFile(process.execPath, spawnArgs, {
|
|
907
|
+
cwd,
|
|
908
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
909
|
+
env: childEnv,
|
|
910
|
+
});
|
|
911
|
+
} catch (spawnErr) {
|
|
912
|
+
// Synchronous spawn failure — record it to the (already-stamped) log so the
|
|
913
|
+
// orphan detector's "logSize > stub-only" check can tell this apart from a
|
|
914
|
+
// hung process. Then rethrow so the dispatch loop handles it normally.
|
|
915
|
+
try { fs.appendFileSync(liveOutputPath, `[${new Date().toISOString()}] spawn-failed: ${spawnErr.message}\n[process-exit] spawn-failed\n`); } catch { /* cleanup-only best effort */ }
|
|
916
|
+
throw spawnErr;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const MAX_OUTPUT = 1024 * 1024; // 1MB
|
|
920
|
+
let stdout = '';
|
|
921
|
+
let stderr = '';
|
|
922
|
+
let lastOutputAt = Date.now();
|
|
923
|
+
let heartbeatTimer = null;
|
|
924
|
+
let _trustCheckDone = false;
|
|
925
|
+
const _spawnTime = Date.now();
|
|
905
926
|
|
|
906
927
|
// Keep live log active even when the agent produces no stdout/stderr for long stretches.
|
|
907
928
|
// This makes "silent but running" states visible in the dashboard tail view.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1047",
|
|
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"
|