@yemi33/minions 0.1.2308 → 0.1.2309

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/bin/minions.js CHANGED
@@ -391,6 +391,19 @@ function _resolveRestartHealthTimeoutMs() {
391
391
  return Math.min(300000, Math.max(15000, resolved));
392
392
  }
393
393
 
394
+ /**
395
+ * Read config.json best-effort for callers that spawn engine.js before any
396
+ * config is loaded into memory. Mirrors _resolveRestartHealthTimeoutMs's
397
+ * inline read.
398
+ */
399
+ function _readConfigJsonSafe() {
400
+ const home = process.env.MINIONS_HOME || (typeof MINIONS_HOME !== 'undefined' ? MINIONS_HOME : null);
401
+ try {
402
+ if (home) return JSON.parse(fs.readFileSync(path.join(home, 'config.json'), 'utf8'));
403
+ } catch { /* config may not exist yet */ }
404
+ return null;
405
+ }
406
+
394
407
  /**
395
408
  * Spawn engine + dashboard + supervisor, verify health, optionally open browser.
396
409
  * Shared by `minions start` and `minions restart` — restart layers a kill phase
@@ -401,8 +414,14 @@ function _resolveRestartHealthTimeoutMs() {
401
414
  function spawnFullStackAndVerify({ rest, forceOpen, dashWasUp, restartStartMs }) {
402
415
  const engineOut = _openStdioLog('engine-stdio.log');
403
416
  const engineErr = _openStdioLog('engine-stdio.log');
417
+ // W-mr2c4i8m0004da94: enable Node's crash-diagnostics report for the
418
+ // engine.js process so a future silent death (the class investigated in
419
+ // W-mr2azk6i) leaves a JSON report under engine/diagnostics/crash-reports
420
+ // instead of vanishing with no trace.
421
+ const crashDiagEnv = shared.getEngineCrashDiagnosticsEnv(_readConfigJsonSafe());
404
422
  const engineProc = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_HOME, 'engine.js'), 'start', ...rest], {
405
- cwd: MINIONS_HOME, stdio: ['ignore', engineOut, engineErr], detached: true, windowsHide: true
423
+ cwd: MINIONS_HOME, stdio: ['ignore', engineOut, engineErr], detached: true, windowsHide: true,
424
+ env: { ...process.env, ...crashDiagEnv }
406
425
  });
407
426
  engineProc.unref();
408
427
  console.log(`\n Engine started (PID: ${engineProc.pid})`);
@@ -934,7 +953,8 @@ function init() {
934
953
  ? `\n Upgrade complete (${pkgVersion}). Restarting engine and dashboard...\n`
935
954
  : '\n Starting engine and dashboard...\n');
936
955
  const engineProc = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_HOME, 'engine.js'), 'start'], {
937
- cwd: MINIONS_HOME, stdio: 'ignore', detached: true, windowsHide: true
956
+ cwd: MINIONS_HOME, stdio: 'ignore', detached: true, windowsHide: true,
957
+ env: { ...process.env, ...shared.getEngineCrashDiagnosticsEnv(_readConfigJsonSafe()) }
938
958
  });
939
959
  engineProc.unref();
940
960
  console.log(` Engine started (PID: ${engineProc.pid})`);
package/docs/README.md CHANGED
@@ -61,6 +61,7 @@ Operational runbooks for engine operators and fleet maintainers.
61
61
  - [notes.md](notes.md) — Consolidated team knowledge: patterns, conventions, bugs, and build findings accumulated from agent inbox notes. Read by the engine and injected into agent prompts as shared context.
62
62
  - [auto-discovery.md](auto-discovery.md) — Auto-discovery and execution pipeline: the per-tick orchestration loop and the four work-discovery sources.
63
63
  - [diagnostics-memory.md](diagnostics-memory.md) — Operator runbook for the in-process memory + perf observability surface: `/api/diagnostics/memory[/history]`, `/api/diagnostics/heap-snapshot` guard-token capture, `MEMORY_BASELINE` log emissions, `--cpu-prof`/`--heap-prof` capture, and the `test/perf/soak.test.js` heap-growth regression gate.
64
+ - [diagnostics-crash-reports.md](diagnostics-crash-reports.md) — Proactive Node crash-diagnostics reports (`--report-on-fatalerror --report-on-signal --diagnostic-dir=...`) for the engine.js process on Windows: where reports land, config/opt-out, and retention.
64
65
  - [engine-restart.md](engine-restart.md) — How agents survive an engine restart: state persistence, the 20-minute startup grace period, and orphan reattachment via PID files and `live-output.log`.
65
66
  - [human-vs-automated.md](human-vs-automated.md) — Quick reference table of which features humans start, run, decide, and recover, and the two human approval gates.
66
67
  - [kb-sweep.md](kb-sweep.md) — Knowledge-base sweep runbook: how `engine/kb-sweep.js` consolidates `notes/inbox/` into `knowledge/` and survives `minions restart`.
@@ -0,0 +1,132 @@
1
+ # Diagnostics: crash reports for the engine.js process (Windows)
2
+
3
+ Operator runbook for the proactive crash-diagnostics feature shipped for
4
+ `W-mr2c4i8m0004da94`. Follow-up from `W-mr2azk6i` (2026-07-01 engine.js
5
+ crash-loop investigation): a run of 4 silent `engine.js` deaths left no
6
+ exception/stack trace in `engine-stdio.log`, no Windows Event Viewer
7
+ entries (Application/System logs were empty for the window), and no crash
8
+ dumps in `C:\Windows\Minidump` or `%LOCALAPPDATA%\CrashDumps`. The box had
9
+ no crash-event logging wired up at all, so a genuine native/access-violation
10
+ crash of `node.exe` would have vanished without a trace.
11
+
12
+ ## What this does
13
+
14
+ Every spawn of `engine.js` — via `minions start`/`restart` (`bin/minions.js`)
15
+ **and** the supervisor's respawn-on-death path (`engine/supervisor.js`) —
16
+ now gets Node's own diagnostic-report flags folded into `NODE_OPTIONS`:
17
+
18
+ ```
19
+ --report-on-fatalerror --report-on-signal --diagnostic-dir=<MINIONS_HOME>/engine/diagnostics/crash-reports
20
+ ```
21
+
22
+ This is a zero-dependency, built-in Node feature (no Sysinternals ProcDump
23
+ or other external tool needed):
24
+
25
+ - **`--report-on-fatalerror`** — writes a JSON diagnostic report the moment
26
+ the process hits an unrecoverable V8 error: out-of-memory, JS stack
27
+ overflow, or a native-addon crash. This is exactly the class of death a
28
+ silent `node.exe` disappearance on Windows would otherwise hide.
29
+ - **`--report-on-signal`** — lets an operator request a report on demand by
30
+ sending the configured signal to the running engine PID: `SIGUSR2` on
31
+ POSIX, `SIGBREAK` on Windows (Node's default `--report-signal`). Useful to
32
+ capture a report while a hang is in progress, before deciding to kill it.
33
+ - **`--diagnostic-dir=...`** — where both of the above land.
34
+
35
+ Reports are plain JSON (`report.<timestamp>.<pid>.<seq>.json`) containing
36
+ the JS/native stack, loaded modules, resource usage, and libuv handle
37
+ summary at the moment of the event — see [Node's diagnostic report
38
+ docs](https://nodejs.org/api/report.html) for the full schema.
39
+
40
+ ## Where reports land
41
+
42
+ `<MINIONS_HOME>/engine/diagnostics/crash-reports/` — a sibling of the
43
+ existing `engine/diagnostics/` directory used for heap snapshots and
44
+ CPU/heap profiles (see [docs/diagnostics-memory.md](diagnostics-memory.md)
45
+ §3–4), in its own subfolder so retention sweeps don't collide. The directory
46
+ is gitignored and created on first spawn if missing
47
+ (`getEngineCrashDiagnosticsEnv`, [`engine/shared.js`](../engine/shared.js)).
48
+
49
+ ## Config + opt-out
50
+
51
+ ```json
52
+ {
53
+ "engine": {
54
+ "crashDiagnostics": {
55
+ "enabled": true,
56
+ "retainCount": 20
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ - `enabled` (default `true`) — set `false` to disable the NODE_OPTIONS
63
+ injection entirely (falls back to the pre-existing no-diagnostics
64
+ behavior).
65
+ - `retainCount` (default `20`) — how many `report.*.json` files are kept
66
+ before the oldest are pruned. Restart the engine (`minions restart`)
67
+ after editing `enabled`, since it only takes effect at the next spawn.
68
+
69
+ Both live in `ENGINE_DEFAULTS.crashDiagnostics`
70
+ ([`engine/shared.js`](../engine/shared.js)).
71
+
72
+ ## Retention / cleanup
73
+
74
+ Reports are pruned by `pruneCrashDiagnosticsReports` (`engine/shared.js`),
75
+ called from `engine/cleanup.js#runCleanup` — the same periodic cleanup pass
76
+ that already runs every `ENGINE_DEFAULTS.cleanupEvery` ticks (default 60
77
+ ticks ≈ 10 min at the default 10 s tick interval). It keeps the newest
78
+ `retainCount` files by mtime and deletes the rest, mirroring the existing
79
+ heap-snapshot retention pattern in `dashboard.js#_heapSnapshotPrune`. Pruning
80
+ is best-effort: a missing directory or a transient Windows file lock is
81
+ swallowed and retried on the next cleanup pass.
82
+
83
+ ## Idempotency across the CLI → supervisor respawn chain
84
+
85
+ Both `bin/minions.js` (initial spawn) and `engine/supervisor.js` (respawn on
86
+ death) call the same `getEngineCrashDiagnosticsEnv(config)` helper. It checks
87
+ whether the caller's `NODE_OPTIONS` already contains `--diagnostic-dir=`
88
+ (which a spawned child inherits from its parent's env) and returns `{}`
89
+ unchanged if so — so a CLI-spawned engine that later gets respawned by the
90
+ supervisor never accumulates duplicate flags.
91
+
92
+ ## Inspecting a report after a crash
93
+
94
+ ```powershell
95
+ Get-ChildItem "$env:USERPROFILE\.minions\engine\diagnostics\crash-reports" |
96
+ Sort-Object LastWriteTime -Descending | Select-Object -First 5
97
+
98
+ Get-Content "$env:USERPROFILE\.minions\engine\diagnostics\crash-reports\report.<ts>.<pid>.001.json" |
99
+ ConvertFrom-Json | Select-Object javascriptStack, header
100
+ ```
101
+
102
+ Key fields to check first: `header.event` (what triggered the report,
103
+ e.g. `"FatalError"` or `"Signal"`), `javascriptStack`, `nativeStack`, and
104
+ `resourceUsage` (memory/CPU at the moment of the event).
105
+
106
+ ## Requesting a report on demand (hang investigation)
107
+
108
+ ```powershell
109
+ # Find the engine PID from engine/control.json, then:
110
+ # Windows: send SIGBREAK-equivalent via node's report trigger utility, or
111
+ # use Node's process.report.writeReport() interactively if you have a REPL
112
+ # attached. The simplest cross-platform trigger is `process.kill(pid, 'SIGBREAK')`
113
+ # from another Node process on Windows.
114
+ node -e "process.kill(<pid>, 'SIGBREAK')"
115
+ ```
116
+
117
+ A fresh `report.*.json` should appear in the crash-reports directory within
118
+ a second or two, without killing the process.
119
+
120
+ ## Related modules and tests
121
+
122
+ - Module: [`engine/shared.js`](../engine/shared.js) — `CRASH_REPORTS_DIR`,
123
+ `getEngineCrashDiagnosticsEnv`, `pruneCrashDiagnosticsReports`,
124
+ `ENGINE_DEFAULTS.crashDiagnostics`.
125
+ - Spawn wiring: [`bin/minions.js`](../bin/minions.js)
126
+ (`spawnFullStackAndVerify`, the post-install/upgrade auto-start path) and
127
+ [`engine/supervisor.js`](../engine/supervisor.js) (`spawnEngine`).
128
+ - Cleanup wiring: [`engine/cleanup.js`](../engine/cleanup.js) `runCleanup`.
129
+ - Tests: [`test/unit/crash-diagnostics.test.js`](../test/unit/crash-diagnostics.test.js).
130
+ - Related: [docs/diagnostics-memory.md](diagnostics-memory.md) (memory/GC/heap
131
+ observability — the sibling diagnostics surface this doc's directory lives
132
+ next to).
package/engine/cleanup.js CHANGED
@@ -1633,6 +1633,19 @@ async function runCleanup(config, verbose = false) {
1633
1633
  cleaned.knowledgeScratch = reapKnowledgeScratch();
1634
1634
  } catch (e) { log('warn', `reapKnowledgeScratch: ${e.message}`); }
1635
1635
 
1636
+ // 18. Prune old Node crash-diagnostics reports (W-mr2c4i8m0004da94). Node
1637
+ // writes `report.*.json` to CRASH_REPORTS_DIR on a fatal V8 error or
1638
+ // `--report-on-signal` request — see getEngineCrashDiagnosticsEnv. Keep
1639
+ // only the newest config.engine.crashDiagnostics.retainCount (default 20)
1640
+ // so a crash-loop or repeated on-demand reports don't accumulate unbounded.
1641
+ cleaned.crashReports = 0;
1642
+ try {
1643
+ cleaned.crashReports = shared.pruneCrashDiagnosticsReports(config);
1644
+ if (cleaned.crashReports > 0) {
1645
+ log('info', `cleanup: pruned ${cleaned.crashReports} old crash-diagnostics report(s)`);
1646
+ }
1647
+ } catch (e) { log('warn', `pruneCrashDiagnosticsReports: ${e.message}`); }
1648
+
1636
1649
  return cleaned;
1637
1650
  }
1638
1651
 
package/engine/shared.js CHANGED
@@ -58,6 +58,13 @@ const MINIONS_DIR = process.env.MINIONS_TEST_DIR || resolveMinionsHome(false, {
58
58
  preferSourceCheckout: true,
59
59
  });
60
60
  const ENGINE_DIR = path.join(MINIONS_DIR, 'engine');
61
+ // W-mr2c4i8m0004da94: Node's built-in `--diagnostic-dir` target for the
62
+ // engine.js process's crash-diagnostics report (see
63
+ // getEngineCrashDiagnosticsEnv / pruneCrashDiagnosticsReports below and
64
+ // docs/diagnostics-crash-reports.md). Sibling of the existing
65
+ // engine/diagnostics/ heap-snapshot dir, own subfolder so retention sweeps
66
+ // don't collide with heap-snapshot pruning.
67
+ const CRASH_REPORTS_DIR = path.join(ENGINE_DIR, 'diagnostics', 'crash-reports');
61
68
  const CONTROL_PATH = path.join(ENGINE_DIR, 'control.json');
62
69
  const COOLDOWNS_PATH = path.join(ENGINE_DIR, 'cooldowns.json');
63
70
  // W-mp60tw0u000j3931: Persistent cross-restart engine state (migration markers,
@@ -1633,6 +1640,69 @@ function openAppendLogFd(name, dir, opts) {
1633
1640
  }
1634
1641
  }
1635
1642
 
1643
+ // W-mr2c4i8m0004da94: proactive crash diagnostics for the engine.js node
1644
+ // process on Windows. Follow-up from W-mr2azk6i (2026-07-01 crash-loop
1645
+ // investigation): 4 silent engine.js deaths with no exception in
1646
+ // engine-stdio.log, no Windows Event Viewer entries, and no crash dumps —
1647
+ // this box had no crash-event logging wired up for a genuine native/AV
1648
+ // crash. Node's own diagnostic-report feature is a zero-dependency fix:
1649
+ // `--report-on-fatalerror` writes a JSON report on unrecoverable V8 errors
1650
+ // (OOM, stack overflow, native-addon segfault) and `--report-on-signal`
1651
+ // lets an operator request one on demand (SIGUSR2 on POSIX, SIGBREAK on
1652
+ // Windows) without installing anything (e.g. Sysinternals ProcDump).
1653
+ //
1654
+ // Callers: bin/minions.js (CLI `start`/`restart` spawn) and
1655
+ // engine/supervisor.js (respawn-on-death). Both spawn engine.js as a fresh
1656
+ // process before any config is loaded into memory, so this reads config.json
1657
+ // directly rather than expecting an already-parsed config object — pass the
1658
+ // parsed object in if the caller already has it (e.g. from its own
1659
+ // best-effort config.json read), or `null`/`undefined` to use the defaults.
1660
+ //
1661
+ // Idempotent: if the caller's NODE_OPTIONS already carries
1662
+ // `--diagnostic-dir=`, returns `{}` unchanged so the CLI's spawn -> a later
1663
+ // supervisor respawn -> another respawn chain never piles up duplicate
1664
+ // flags (child processes inherit NODE_OPTIONS from their parent env).
1665
+ function getEngineCrashDiagnosticsEnv(config, baseEnv) {
1666
+ const env = baseEnv || process.env;
1667
+ const cfg = (config && config.engine && config.engine.crashDiagnostics) || {};
1668
+ const defaults = ENGINE_DEFAULTS.crashDiagnostics || {};
1669
+ const enabled = cfg.enabled !== undefined ? cfg.enabled !== false : defaults.enabled !== false;
1670
+ if (!enabled) return {};
1671
+ const existing = String(env.NODE_OPTIONS || '');
1672
+ if (existing.includes('--diagnostic-dir=')) return {};
1673
+ try { fs.mkdirSync(CRASH_REPORTS_DIR, { recursive: true }); } catch { /* best effort — Node still tries to write on crash */ }
1674
+ const flags = `--report-on-fatalerror --report-on-signal --diagnostic-dir=${CRASH_REPORTS_DIR}`;
1675
+ return { NODE_OPTIONS: existing ? `${existing} ${flags}` : flags };
1676
+ }
1677
+
1678
+ // Prune `report.*.json` files in CRASH_REPORTS_DIR down to the most-recent
1679
+ // `retainCount` (config.engine.crashDiagnostics.retainCount, default 20) by
1680
+ // mtime, mirroring dashboard.js's `_heapSnapshotPrune` retention pattern so
1681
+ // a leak-hunt session (or a genuine crash-loop) doesn't fill the disk with
1682
+ // reports unbounded. Best-effort: missing dir / unreadable stat / failed
1683
+ // unlink are swallowed. Called from engine/cleanup.js#runCleanup (fires
1684
+ // every ENGINE_DEFAULTS.cleanupEvery ticks, ~10 min at default tick
1685
+ // interval) so retention happens automatically without a dedicated sweep.
1686
+ function pruneCrashDiagnosticsReports(config) {
1687
+ const cfg = (config && config.engine && config.engine.crashDiagnostics) || {};
1688
+ const defaults = ENGINE_DEFAULTS.crashDiagnostics || {};
1689
+ const retain = Number.isFinite(cfg.retainCount) ? cfg.retainCount : (defaults.retainCount || 20);
1690
+ let names;
1691
+ try { names = fs.readdirSync(CRASH_REPORTS_DIR); } catch { return 0; }
1692
+ const candidates = names.filter(n => n.startsWith('report.') && n.endsWith('.json'));
1693
+ if (candidates.length <= retain) return 0;
1694
+ const stamped = candidates.map(n => {
1695
+ try { return { n, mtime: fs.statSync(path.join(CRASH_REPORTS_DIR, n)).mtimeMs }; }
1696
+ catch { return { n, mtime: 0 }; }
1697
+ });
1698
+ stamped.sort((a, b) => b.mtime - a.mtime); // newest first
1699
+ let removed = 0;
1700
+ for (const s of stamped.slice(retain)) {
1701
+ try { fs.unlinkSync(path.join(CRASH_REPORTS_DIR, s.n)); removed++; } catch { /* best effort */ }
1702
+ }
1703
+ return removed;
1704
+ }
1705
+
1636
1706
  function withFileLock(lockPath, fn, {
1637
1707
  timeoutMs = 5000,
1638
1708
  retryDelayMs = 25,
@@ -3156,6 +3226,19 @@ const ENGINE_DEFAULTS = {
3156
3226
  // at the default 10s tickInterval. Set to 0 (or any non-positive integer)
3157
3227
  // to disable both the log emission and sidecar write cleanly (operator opt-out).
3158
3228
  memoryBaselineEveryTicks: 6,
3229
+ // W-mr2c4i8m0004da94: proactive crash diagnostics for the engine.js
3230
+ // node.exe process. When enabled (default), every spawn of engine.js
3231
+ // (bin/minions.js `start`/`restart` AND engine/supervisor.js respawn-on-
3232
+ // death) gets NODE_OPTIONS += `--report-on-fatalerror --report-on-signal
3233
+ // --diagnostic-dir=<CRASH_REPORTS_DIR>` so a fatal V8 error (OOM, stack
3234
+ // overflow, native-addon crash) or an operator-sent signal
3235
+ // (SIGUSR2 POSIX / SIGBREAK Windows) leaves a JSON diagnostic report
3236
+ // instead of a silent death. See getEngineCrashDiagnosticsEnv,
3237
+ // pruneCrashDiagnosticsReports, and docs/diagnostics-crash-reports.md.
3238
+ crashDiagnostics: {
3239
+ enabled: true,
3240
+ retainCount: 20, // report.*.json files kept in CRASH_REPORTS_DIR; oldest pruned during runCleanup (cleanupEvery ticks)
3241
+ },
3159
3242
  stalledDispatchSweepEvery: 120, // stalled-dispatch retry sweep (~20 min at default 10s tick) — only fires when all agents idle
3160
3243
  // W-mp5trwh60008386d: per-PR 404 must repeat across N consecutive successful base-repo probes
3161
3244
  // before flipping a PR to `abandoned`. A single 404 on `repos/{slug}/pulls/{n}` can be a transient
@@ -9091,6 +9174,9 @@ function createBackoffTracker({ baseMs = 30000, maxMs = 10 * 60 * 1000 } = {}) {
9091
9174
  module.exports = {
9092
9175
  MINIONS_DIR,
9093
9176
  ENGINE_DIR,
9177
+ CRASH_REPORTS_DIR,
9178
+ getEngineCrashDiagnosticsEnv,
9179
+ pruneCrashDiagnosticsReports,
9094
9180
  resolveEngineCacheDir,
9095
9181
  openUrlInBrowser,
9096
9182
  CONTROL_PATH,
@@ -347,6 +347,18 @@ function _sqliteSpawnFlags() {
347
347
  return ['--experimental-sqlite'];
348
348
  }
349
349
 
350
+ // W-mr2c4i8m0004da94: crash-diagnostics env additions for a supervisor-
351
+ // initiated respawn. Best-effort config.json read — the supervisor spawns
352
+ // engine.js before any config is loaded into memory, mirroring the same
353
+ // read bin/minions.js does for its own engine spawns.
354
+ function _engineCrashDiagnosticsEnv() {
355
+ const shared = _sharedOrNull();
356
+ if (!shared || typeof shared.getEngineCrashDiagnosticsEnv !== 'function') return {};
357
+ let cfg = null;
358
+ try { cfg = JSON.parse(fs.readFileSync(path.join(MINIONS_DIR, 'config.json'), 'utf8')); } catch { /* config may not exist */ }
359
+ try { return shared.getEngineCrashDiagnosticsEnv(cfg); } catch { return {}; }
360
+ }
361
+
350
362
  function spawnEngine() {
351
363
  const out = openAppendFd('engine-stdio.log');
352
364
  const err = openAppendFd('engine-stdio.log');
@@ -355,6 +367,7 @@ function spawnEngine() {
355
367
  stdio: ['ignore', out, err],
356
368
  detached: true,
357
369
  windowsHide: true,
370
+ env: { ...process.env, ..._engineCrashDiagnosticsEnv() },
358
371
  });
359
372
  proc.unref();
360
373
  return proc.pid;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2308",
3
+ "version": "0.1.2309",
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"