@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 CHANGED
@@ -1,8 +1,9 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.1046 (2026-04-16)
3
+ ## 0.1.1047 (2026-04-16)
4
4
 
5
5
  ### Fixes
6
+ - stamp live-output.log stub before spawn (#1198)
6
7
  - harden settings save and migrate pr poll config
7
8
 
8
9
  ### Other
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
- const proc = runFile(process.execPath, spawnArgs, {
875
- cwd,
876
- stdio: ['pipe', 'pipe', 'pipe'],
877
- env: childEnv,
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
- safeWrite(liveOutputPath, `# Live output for ${agentId} ${id}\n# Started: ${startedAt}\n# Task: ${dispatchItem.task}\n\n`);
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.1046",
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"