@yemi33/minions 0.1.1972 → 0.1.1973

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/engine/cli.js CHANGED
@@ -521,7 +521,14 @@ const commands = {
521
521
  const savedBranch = normalizeSessionBranch(sj?.branch);
522
522
  if (sj?.sessionId && (!expectedBranch || savedBranch === expectedBranch)) {
523
523
  sessionId = sj.sessionId;
524
- } else if (sj?.sessionId && expectedBranch) {
524
+ } else if (sj?.sessionId && expectedBranch && sj?.dispatchId === item.id) {
525
+ // Only warn when the saved session is for THIS dispatch but on the
526
+ // wrong branch — that's a true anomaly worth flagging. The common
527
+ // case — leftover session.json from a previous (now-completed)
528
+ // dispatch on a different branch — is expected and silent, since
529
+ // the engine writes session.json on completion of each dispatch
530
+ // and a fresh dispatch may run on a different branch before
531
+ // saveSession overwrites it (W-mpbn93ou000611b3).
525
532
  shared.log('warn', `Reattach: ignoring session for ${agentId} on branch ${savedBranch || 'unknown'}; expected ${expectedBranch}`);
526
533
  }
527
534
  } catch {}
@@ -529,9 +529,44 @@ function main() {
529
529
  clearTimeout(startupTimer);
530
530
  clearTimeout(initialSnapshotTimer);
531
531
  clearInterval(descTimer);
532
+
533
+ // Compute the exit code and write the [process-exit] sentinel FIRST,
534
+ // before the descendant snapshot/reap. The engine's orphan reaper uses
535
+ // the sentinel as the single signal that "the runtime exited cleanly
536
+ // with code N"; if we delay it behind `snapshotDescendants()` (which
537
+ // shells out to `Get-CimInstance` on Windows and can block 1-5+s),
538
+ // there's a window where the runtime PID we track is already dead but
539
+ // no sentinel exists yet. After an engine restart that path triggers
540
+ // `canReapDeadProcess` and the dispatch gets auto-retried as orphaned
541
+ // even though it completed normally. See W-mpbn93ou000611b3 / the
542
+ // 2026-05-18 ripley-explore regression.
543
+ //
544
+ // Prefer the 'exit' event's code/signal when present (Node's 'close'
545
+ // event can report code=0 on Windows when the OS-level exit was
546
+ // non-zero — see the long-form note above the exit handler).
547
+ const effectiveCode = (realExitFromEvent != null) ? realExitFromEvent : code;
548
+ const effectiveSignal = realSignalFromEvent || signal;
549
+ const exitCode = normalizeRuntimeExit(effectiveCode, effectiveSignal);
550
+ if (sentinelWritten) {
551
+ // Defense-in-depth: never write a duplicate sentinel. We observed pairs
552
+ // of [process-exit] code=0 lines in live-output.log across many failed
553
+ // runs, which suggests close has fired twice in some edge cases (e.g.,
554
+ // shim re-launch on Windows). One sentinel per spawn is the contract.
555
+ // Skip descendant reap on the duplicate close too — the first close
556
+ // already handled it (reaping the same PIDs again is a no-op at best,
557
+ // but skipping is faster and matches the prior early-return contract).
558
+ fs.appendFileSync(debugPath, `EXIT (duplicate close, skipping sentinel): code=${exitCode}${effectiveSignal ? ` signal=${effectiveSignal}` : ''}\n`);
559
+ process.exit(exitCode);
560
+ return;
561
+ }
562
+ sentinelWritten = true;
563
+ const sentinelResult = writeProcessExitSentinel({ exitCode, signal: effectiveSignal });
564
+
532
565
  // Final snapshot + reap, but only when the runtime actually spawned
533
566
  // children. Read-only / very short agents (exit before the 3s initial
534
- // snapshot fires) skip the wmic shell-out entirely.
567
+ // snapshot fires) skip the wmic shell-out entirely. Runs AFTER the
568
+ // sentinel write so a slow Get-CimInstance call can't gate completion
569
+ // detection — see the hoist note above.
535
570
  if (trackedDescendants.size || gotFirstOutput) {
536
571
  snapshotDescendants();
537
572
  if (trackedDescendants.size) {
@@ -580,21 +615,6 @@ function main() {
580
615
  try { fs.appendFileSync(debugPath, `DESCENDANTS reaped=${reaped}/${toKillPids.length} kept=${kept.length}\n`); } catch {}
581
616
  }
582
617
  }
583
- // Prefer the 'exit' event's code/signal when present — see note above.
584
- const effectiveCode = (realExitFromEvent != null) ? realExitFromEvent : code;
585
- const effectiveSignal = realSignalFromEvent || signal;
586
- const exitCode = normalizeRuntimeExit(effectiveCode, effectiveSignal);
587
- if (sentinelWritten) {
588
- // Defense-in-depth: never write a duplicate sentinel. We observed pairs
589
- // of [process-exit] code=0 lines in live-output.log across many failed
590
- // runs, which suggests close has fired twice in some edge cases (e.g.,
591
- // shim re-launch on Windows). One sentinel per spawn is the contract.
592
- fs.appendFileSync(debugPath, `EXIT (duplicate close, skipping sentinel): code=${exitCode}${effectiveSignal ? ` signal=${effectiveSignal}` : ''}\n`);
593
- process.exit(exitCode);
594
- return;
595
- }
596
- sentinelWritten = true;
597
- const sentinelResult = writeProcessExitSentinel({ exitCode, signal: effectiveSignal });
598
618
  fs.appendFileSync(debugPath, `EXIT: code=${exitCode}${effectiveSignal ? ` signal=${effectiveSignal}` : ''} (close=${code} exit=${realExitFromEvent})\nSTDERR: ${stderrBuf.slice(0, 500)}\n`);
599
619
  if (!sentinelResult.fileWritten) {
600
620
  fs.appendFileSync(debugPath, `EXIT SENTINEL: file write failed for ${process.env.MINIONS_LIVE_OUTPUT_PATH}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1972",
3
+ "version": "0.1.1973",
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"