@yemi33/minions 0.1.2448 → 0.1.2449
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/cli-api-client.js +1 -1
- package/bin/install-internal-minions.js +1382 -44
- package/bin/install-layout.js +150 -0
- package/bin/minions.js +460 -167
- package/dashboard/docs/typography.md +65 -12
- package/dashboard/js/command-center.js +58 -4
- package/dashboard/js/detail-panel.js +36 -0
- package/dashboard/js/memory-panel.js +59 -12
- package/dashboard/js/qa.js +179 -20
- package/dashboard/js/refresh.js +148 -12
- package/dashboard/js/render-dispatch.js +3 -4
- package/dashboard/js/render-inbox.js +2 -2
- package/dashboard/js/render-other.js +3 -3
- package/dashboard/js/render-pipelines.js +14 -0
- package/dashboard/js/render-plans.js +57 -9
- package/dashboard/js/render-prd.js +132 -23
- package/dashboard/js/render-prs.js +195 -166
- package/dashboard/js/render-schedules.js +63 -3
- package/dashboard/js/render-utils.js +3 -3
- package/dashboard/js/render-watches.js +19 -3
- package/dashboard/js/render-work-items.js +238 -30
- package/dashboard/js/settings.js +205 -54
- package/dashboard/js/utils.js +51 -1
- package/dashboard/pages/home.html +1 -1
- package/dashboard/pages/qa.html +1 -16
- package/dashboard/pages/work.html +40 -0
- package/dashboard/shared/cc-limits.js +79 -0
- package/dashboard/shared/pr-filters.js +21 -38
- package/dashboard/shared/project-git-summary.js +1 -1
- package/dashboard/shared/record-filters.js +169 -0
- package/dashboard/shared/watches-source.js +1 -1
- package/dashboard/shared/welcome-popup.js +1 -1
- package/dashboard/shared/wi-filters.js +302 -0
- package/dashboard/slim/body.html +1 -0
- package/dashboard/slim/js/command-send.js +26 -0
- package/dashboard/slim/js/modals-tiles.js +380 -39
- package/dashboard/slim/js/status.js +13 -21
- package/dashboard/slim/layout.html +1 -0
- package/dashboard/slim/panel-bootstrap.js +6 -2
- package/dashboard/slim/styles.css +38 -0
- package/dashboard/styles.css +159 -55
- package/dashboard-build.js +13 -2
- package/dashboard.js +956 -423
- package/docs/README.md +11 -6
- package/docs/api-errors.md +2 -2
- package/docs/architecture-review-2026-07-09.md +1 -1
- package/docs/architecture.excalidraw +2 -2
- package/docs/auto-discovery.md +18 -9
- package/docs/branch-derivation.md +4 -4
- package/docs/capture-demos.js +39 -2
- package/docs/ci-runner-canary.md +123 -0
- package/docs/claude-md-propagation.md +2 -2
- package/docs/cloud-agent-dispatch.md +204 -0
- package/docs/command-center.md +7 -7
- package/docs/completion-reports.md +43 -20
- package/docs/constants.md +10 -3
- package/docs/constellation-bridge.md +134 -6
- package/docs/constellation-style-telemetry.md +4 -4
- package/docs/contracts/capability-protocol.v1.json +165 -0
- package/docs/cooldown-merge-semantics.md +12 -12
- package/docs/copilot-cli-schema.md +7 -7
- package/docs/cross-repo-plans.md +10 -10
- package/docs/dead-code-audit-retractions.md +5 -5
- package/docs/default-branch-ci.md +173 -0
- package/docs/deprecated.json +31 -31
- package/docs/design-inbox-entries-schema.md +3 -3
- package/docs/design-language.md +1051 -0
- package/docs/design-state-storage.md +11 -11
- package/docs/diagnostics-crash-reports.md +9 -9
- package/docs/diagnostics-memory.md +5 -5
- package/docs/documentation-audit-2026-07-09.md +7 -7
- package/docs/engine-restart.md +90 -5
- package/docs/harness-mode.md +1 -1
- package/docs/internal-install.md +338 -39
- package/docs/kb-dedup-duplicate-pair-investigation.md +5 -5
- package/docs/kb-pr3223-cascade-archiving.md +1 -1
- package/docs/kb-pr696-merge-conflict-docs.md +6 -6
- package/docs/kb-sweep.md +35 -35
- package/docs/keep-processes.md +1 -1
- package/docs/live-checkout-mode.md +30 -30
- package/docs/managed-spawn.md +18 -14
- package/docs/named-agents.md +7 -7
- package/docs/plan-lifecycle.md +69 -2
- package/docs/pr-author-identity.md +114 -0
- package/docs/pr-auto-fix-dispatch.md +19 -4
- package/docs/pr-comment-followup.md +6 -6
- package/docs/pr-review-fix-loop.md +59 -10
- package/docs/process-termination.md +40 -0
- package/docs/proposals/repo-pool-for-live-checkout.md +13 -13
- package/docs/qa-runbook-lifecycle.md +367 -17
- package/docs/qa-runbooks.md +3 -3
- package/docs/rfc-completion-json.md +18 -18
- package/docs/runtime-adapters.md +26 -21
- package/docs/security.md +6 -6
- package/docs/self-improvement.md +4 -4
- package/docs/shared-lifecycle-module-map.md +473 -472
- package/docs/skills.md +52 -3
- package/docs/slim-ux/concepts.md +121 -116
- package/docs/specs/agent-configurability.md +18 -18
- package/docs/specs/agent-rename.md +18 -18
- package/docs/team-memory.md +38 -21
- package/docs/timeouts-and-liveness.md +118 -10
- package/docs/tutorials/01-install-and-connect.md +1 -1
- package/docs/watches.md +40 -39
- package/docs/workspace-manifests.md +4 -4
- package/docs/worktree-lifecycle.md +293 -14
- package/engine/README.md +46 -0
- package/engine/{ado-comment.js → ado/comment.js} +8 -8
- package/engine/{ado-git-auth.js → ado/git-auth.js} +4 -4
- package/engine/{ado.js → ado/index.js} +417 -63
- package/engine/{ado-status.js → ado/status.js} +6 -8
- package/engine/{ado-token.js → ado/token.js} +1 -1
- package/engine/{acp-transport.js → agents/acp-transport.js} +62 -22
- package/engine/{agent-worker-pool.js → agents/agent-worker-pool.js} +17 -8
- package/engine/{cc-worker-pool.js → agents/cc-worker-pool.js} +16 -6
- package/engine/{claude-md-context.js → agents/claude-md-context.js} +5 -5
- package/engine/{harness-context.js → agents/harness-context.js} +5 -5
- package/engine/{harness.js → agents/harness.js} +3 -3
- package/engine/{llm.js → agents/llm.js} +18 -14
- package/engine/{model-discovery.js → agents/model-discovery.js} +2 -2
- package/engine/{playbook.js → agents/playbook.js} +155 -22
- package/engine/{pooled-agent-process.js → agents/pooled-agent-process.js} +14 -12
- package/engine/{preflight.js → agents/preflight.js} +29 -10
- package/engine/{spawn-agent.js → agents/spawn-agent.js} +25 -14
- package/engine/{spawn-phase-watchdog.js → agents/spawn-phase-watchdog.js} +16 -7
- package/engine/{steering.js → agents/steering.js} +5 -5
- package/engine/{tools-inventory.js → agents/tools-inventory.js} +2 -2
- package/engine/{agent-api-validation.js → api/agent-api-validation.js} +2 -2
- package/engine/{api-validation.js → api/api-validation.js} +1 -1
- package/engine/api/bridge.js +787 -0
- package/engine/{cc-api-validation.js → api/cc-api-validation.js} +1 -1
- package/engine/api/companion.js +560 -0
- package/engine/{content-api-validation.js → api/content-api-validation.js} +2 -2
- package/engine/{pr-issue-validation.js → api/pr-issue-validation.js} +33 -6
- package/engine/{settings-validation.js → api/settings-validation.js} +32 -4
- package/engine/api-contracts/agent-content.js +4 -4
- package/engine/api-contracts/capability-manifest.js +236 -0
- package/engine/api-contracts/capability-protocol.js +333 -0
- package/engine/api-contracts/cc-ops.js +1 -1
- package/engine/api-contracts/config-runtime.js +5 -0
- package/engine/api-contracts/core.js +28 -1
- package/engine/api-contracts/index.js +100 -0
- package/engine/api-contracts/orchestration.js +18 -5
- package/engine/api-contracts/pull-requests.js +37 -6
- package/engine/api-contracts/qa-process.js +29 -6
- package/engine/api-contracts/work-plan-prd.js +21 -1
- package/engine/cloud/contract.js +212 -0
- package/engine/cloud/index.js +159 -0
- package/engine/{execution-model.js → core/execution-model.js} +1 -1
- package/engine/{features.js → core/features.js} +4 -4
- package/engine/{operator-identity.js → core/operator-identity.js} +1 -1
- package/engine/{queries.js → core/queries.js} +201 -36
- package/engine/{safe-expr.js → core/safe-expr.js} +1 -1
- package/engine/{shared.js → core/shared.js} +1637 -175
- package/engine/{stdio-timestamps.js → core/stdio-timestamps.js} +1 -1
- package/engine/{untrusted-fence.js → core/untrusted-fence.js} +3 -3
- package/engine/db/index.js +11 -2
- package/engine/db/migrations/002-dispatches.js +3 -3
- package/engine/db/migrations/003-work-items.js +1 -1
- package/engine/db/migrations/004-pull-requests.js +1 -1
- package/engine/db/migrations/006-metrics.js +1 -1
- package/engine/db/migrations/007-watches.js +2 -2
- package/engine/db/migrations/008-small-state.js +1 -1
- package/engine/db/migrations/009-qa.js +1 -1
- package/engine/db/migrations/010-pr-links.js +1 -1
- package/engine/db/migrations/011-remaining-state.js +1 -1
- package/engine/db/migrations/012-steering-deliveries.js +2 -2
- package/engine/db/migrations/013-backfill-broken-note-links.js +1 -1
- package/engine/db/migrations/014-pr-fix-target-prefs.js +2 -2
- package/engine/db/migrations/015-plans-prds.js +0 -0
- package/engine/db/migrations/018-sql-only-cutover.js +2 -2
- package/engine/db/migrations/021-archived-work-items.js +1 -1
- package/engine/db/migrations/022-global-cc-session.js +1 -1
- package/engine/db/migrations/023-engine-state.js +1 -1
- package/engine/db/migrations/025-malformed-work-item-phantoms.js +1 -1
- package/engine/db/migrations/027-review-learning-lifecycle.js +1 -1
- package/engine/db/migrations/029-repair-reused-versions.js +20 -0
- package/engine/db/migrations/031-pr-author-identity.js +137 -0
- package/engine/{consolidation.js → memory/consolidation.js} +6 -6
- package/engine/{kb-sweep-runner.js → memory/kb-sweep-runner.js} +2 -2
- package/engine/{kb-sweep.js → memory/kb-sweep.js} +9 -7
- package/engine/{memory-retrieval.js → memory/memory-retrieval.js} +46 -4
- package/engine/{memory-store.js → memory/memory-store.js} +3 -3
- package/engine/{promotion.js → memory/promotion.js} +3 -3
- package/engine/{review-learning-backfill.js → memory/review-learning-backfill.js} +6 -6
- package/engine/{review-learning.js → memory/review-learning.js} +10 -5
- package/engine/{diagnostics-memory.js → observability/diagnostics-memory.js} +1 -1
- package/engine/{logs-store.js → observability/logs-store.js} +5 -5
- package/engine/{metrics-store.js → observability/metrics-store.js} +4 -4
- package/engine/{check-status.js → operations/check-status.js} +3 -3
- package/engine/{cli.js → operations/cli.js} +271 -113
- package/engine/{distribution.js → operations/distribution.js} +5 -6
- package/engine/{cleanup.js → orchestration/cleanup.js} +72 -45
- package/engine/{cooldown.js → orchestration/cooldown.js} +5 -5
- package/engine/{dispatch-events.js → orchestration/dispatch-events.js} +2 -2
- package/engine/{dispatch.js → orchestration/dispatch.js} +129 -36
- package/engine/orchestration/failed-scheduled-cleanup.js +274 -0
- package/engine/{lifecycle.js → orchestration/lifecycle.js} +198 -90
- package/engine/{meeting.js → orchestration/meeting.js} +6 -16
- package/engine/{pipeline.js → orchestration/pipeline.js} +12 -12
- package/engine/{pre-dispatch-eval.js → orchestration/pre-dispatch-eval.js} +10 -9
- package/engine/{routing.js → orchestration/routing.js} +3 -3
- package/engine/{schedule-bootstrap.js → orchestration/schedule-bootstrap.js} +4 -4
- package/engine/{scheduler.js → orchestration/scheduler.js} +38 -8
- package/engine/{timeout.js → orchestration/timeout.js} +158 -109
- package/engine/{db-events.js → persistence/db-events.js} +2 -2
- package/engine/{dispatch-store.js → persistence/dispatch-store.js} +7 -7
- package/engine/{inbox-store.js → persistence/inbox-store.js} +2 -2
- package/engine/{note-link-backfill.js → persistence/note-link-backfill.js} +4 -4
- package/engine/{pr-fix-target-store.js → persistence/pr-fix-target-store.js} +8 -8
- package/engine/{pull-requests-store.js → persistence/pull-requests-store.js} +21 -7
- package/engine/{small-state-store.js → persistence/small-state-store.js} +31 -31
- package/engine/persistence/state-operations.js +350 -0
- package/engine/{steering-store.js → persistence/steering-store.js} +6 -6
- package/engine/{issues.js → planning/issues.js} +2 -2
- package/engine/{plan-prd-validation.js → planning/plan-prd-validation.js} +8 -2
- package/engine/planning/prd-result-sidecar.js +190 -0
- package/engine/{prd-store.js → planning/prd-store.js} +17 -17
- package/engine/{project-discovery.js → planning/project-discovery.js} +5 -5
- package/engine/{projects.js → planning/projects.js} +10 -10
- package/engine/{resolve-area.js → planning/resolve-area.js} +1 -1
- package/engine/{work-item-validation.js → planning/work-item-validation.js} +39 -3
- package/engine/{work-items-store.js → planning/work-items-store.js} +29 -21
- package/engine/{keep-process-sweep.js → processes/keep-process-sweep.js} +57 -17
- package/engine/{managed-spawn-launcher.js → processes/managed-spawn-launcher.js} +3 -3
- package/engine/{managed-spawn.js → processes/managed-spawn.js} +97 -46
- package/engine/{process-utils.js → processes/process-utils.js} +599 -55
- package/engine/{abandoned-pr-reconciliation.js → providers/abandoned-pr-reconciliation.js} +17 -7
- package/engine/{comment-classifier.js → providers/comment-classifier.js} +85 -17
- package/engine/{comment-format.js → providers/comment-format.js} +5 -5
- package/engine/{gh-comment.js → providers/gh-comment.js} +15 -15
- package/engine/{gh-token.js → providers/gh-token.js} +4 -4
- package/engine/{github.js → providers/github.js} +131 -54
- package/engine/{pr-action.js → providers/pr-action.js} +13 -12
- package/engine/{pr-clone-keep.js → providers/pr-clone-keep.js} +7 -7
- package/engine/{pr-devbox.js → providers/pr-devbox.js} +6 -6
- package/engine/{pr-fix-target.js → providers/pr-fix-target.js} +13 -13
- package/engine/{pr-remote-patch.js → providers/pr-remote-patch.js} +4 -4
- package/engine/{pr-resolve.js → providers/pr-resolve.js} +7 -7
- package/engine/{pr-temp-clone.js → providers/pr-temp-clone.js} +5 -5
- package/engine/{pr-track.js → providers/pr-track.js} +11 -13
- package/engine/{shared-branch-pr-reconcile.js → providers/shared-branch-pr-reconcile.js} +4 -4
- package/engine/qa/auto-prd-qa.js +313 -0
- package/engine/{qa-from-prd.js → qa/from-prd.js} +42 -12
- package/engine/qa/prd-session.js +240 -0
- package/engine/{qa-process-validation.js → qa/process-validation.js} +14 -9
- package/engine/{qa-runbooks.js → qa/runbooks.js} +1 -1
- package/engine/{qa-runs.js → qa/runs.js} +286 -15
- package/engine/{qa-sessions.js → qa/sessions.js} +595 -49
- package/engine/qa/visual-journey.js +654 -0
- package/engine/{qa-runners.js → qa-runners/index.js} +7 -7
- package/engine/qa-runners/maestro.js +3 -3
- package/engine/qa-runners/playwright.js +2 -2
- package/engine/{restart-health.js → recovery/restart-health.js} +48 -4
- package/engine/recovery/stop-stack.js +607 -0
- package/engine/{supervisor.js → recovery/supervisor.js} +105 -175
- package/engine/{watchdog.js → recovery/watchdog.js} +136 -13
- package/engine/runtimes/claude.js +14 -12
- package/engine/runtimes/codex.js +8 -6
- package/engine/runtimes/copilot.js +17 -16
- package/engine/{watch-actions.js → watches/actions.js} +13 -13
- package/engine/{watches.js → watches/index.js} +43 -32
- package/engine/{watches-store.js → watches/store.js} +4 -4
- package/engine/{create-pr-worktree.js → worktrees/create-pr.js} +1 -1
- package/engine/{worktree-gc.js → worktrees/gc.js} +70 -22
- package/engine/worktrees/inventory.js +671 -0
- package/engine/{live-checkout.js → worktrees/live-checkout.js} +4 -4
- package/engine/{worktree-pool.js → worktrees/pool.js} +2 -2
- package/engine/{worktree-preflight.js → worktrees/preflight.js} +1 -0
- package/engine/worktrees/quarantine-refs.js +173 -0
- package/engine.js +1137 -208
- package/minions.js +147 -77
- package/package.json +10 -6
- package/playbooks/_pr-description-audit.md +110 -78
- package/playbooks/build-fix-complex.md +2 -0
- package/playbooks/fix.md +16 -12
- package/playbooks/implement-shared.md +2 -0
- package/playbooks/implement.md +19 -20
- package/playbooks/plan-to-prd.md +18 -3
- package/playbooks/qa-session-draft.md +136 -1
- package/playbooks/qa-session-execute.md +80 -2
- package/playbooks/qa-session-setup.md +17 -1
- package/playbooks/qa-validate.md +1 -1
- package/playbooks/setup.md +2 -0
- package/playbooks/shared-rules.md +25 -32
- package/playbooks/templates/followup-dispatch.md +4 -3
- package/playbooks/verify.md +1 -1
- package/prompts/cc-system.md +19 -27
- package/watch-plugins/README.md +92 -0
- package/watch-plugins/ado-author-prs.js +336 -0
- package/watch-plugins/gh-author-prs.js +375 -0
- package/watch-plugins/http.js +474 -0
- package/watch-plugins/teams-channel.js +869 -0
- package/docs/dev-composite-workflow.md +0 -101
- package/docs/pr-screenshots/pr-886/after-single-header.png +0 -0
- package/docs/pr-screenshots/pr-886/before-duplicate-header.png +0 -0
- package/docs/pr-screenshots/pr-895/01-cancellation-reason-detail.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-901/projects-tab-default.png +0 -0
- package/docs/pr-screenshots/pr-901/projects-tab-fmf-selected.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-AFTER-crop.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-BEFORE-crop.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-dropdown-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
- package/docs/visual-evidence-ci.md +0 -103
- package/engine/bridge.js +0 -379
- package/engine/quarantine-refs.js +0 -103
- package/engine/state-operations.js +0 -178
- /package/engine/{steering-constraints.js → agents/steering-constraints.js} +0 -0
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* engine/supervisor.js — External watchdog for engine + dashboard.
|
|
3
|
+
* engine/recovery/supervisor.js — External watchdog for engine + dashboard.
|
|
4
4
|
*
|
|
5
5
|
* Spawned `detached: true` by `minions start`/`restart` after the engine and
|
|
6
6
|
* dashboard are up. Polls every SUPERVISOR_INTERVAL_MS:
|
|
7
7
|
* - engine PID from `engine/control.json`
|
|
8
|
-
* - dashboard via a port-listener probe (port 7331 by default)
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* counts as "responsive" (and why engine 'degraded'/'stopped' does NOT mean
|
|
15
|
-
* the dashboard is frozen) is documented at _probeDashboardHealth.
|
|
8
|
+
* - dashboard via both a port-listener probe (port 7331 by default) and HTTP
|
|
9
|
+
* `/api/health`. Either can false-negative on a loaded Windows host, so a
|
|
10
|
+
* responsive HTTP result overrides a missed port probe and replacement
|
|
11
|
+
* requires DASH_HEALTH_MAX_FAILS consecutive ticks where neither signal
|
|
12
|
+
* proves health. What counts as responsive is documented at
|
|
13
|
+
* _probeDashboardHealth.
|
|
16
14
|
*
|
|
17
15
|
* When either is dead AND the stop-intent flag is NOT set, respawns the dead
|
|
18
16
|
* one in the same way the CLI does (detached, stdio routed to the engine-
|
|
@@ -29,25 +27,30 @@
|
|
|
29
27
|
* Windows Service via nssm — see docs/supervisor.md.
|
|
30
28
|
*/
|
|
31
29
|
|
|
32
|
-
require('
|
|
30
|
+
require('../core/stdio-timestamps').installIfNotInstalled();
|
|
33
31
|
|
|
34
32
|
const fs = require('fs');
|
|
35
33
|
const path = require('path');
|
|
36
34
|
const os = require('os');
|
|
37
35
|
const { spawn, spawnSync, execSync } = require('child_process');
|
|
36
|
+
const {
|
|
37
|
+
terminateProcess,
|
|
38
|
+
commandLineRunsScript,
|
|
39
|
+
listNodePidsMatchingScript,
|
|
40
|
+
} = require('../processes/process-utils');
|
|
38
41
|
|
|
39
|
-
// Lazy path getters route through engine/shared.js when available so the
|
|
42
|
+
// Lazy path getters route through engine/core/shared.js when available so the
|
|
40
43
|
// supervisor's notion of "the engine dir" honors MINIONS_TEST_DIR (used by
|
|
41
44
|
// the unit tests). Fall back to __dirname-based resolution when shared.js
|
|
42
45
|
// isn't loadable (e.g. early load / bad upgrade) so the supervisor still
|
|
43
46
|
// boots in production.
|
|
44
|
-
const MINIONS_DIR = path.resolve(__dirname, '..');
|
|
47
|
+
const MINIONS_DIR = path.resolve(__dirname, '..', '..');
|
|
45
48
|
function _sharedOrNull() {
|
|
46
|
-
try { return require('
|
|
49
|
+
try { return require('../core/shared'); } catch { return null; }
|
|
47
50
|
}
|
|
48
51
|
// Crash-loop counter (W-mr2c46590003e3ee) — best-effort. shared.js owns the
|
|
49
|
-
// counter/threshold/inbox-alert logic (engine/shared.js#recordEngineRespawn)
|
|
50
|
-
// so supervisor.js, engine/watchdog.js, and dashboard.js's in-process
|
|
52
|
+
// counter/threshold/inbox-alert logic (engine/core/shared.js#recordEngineRespawn)
|
|
53
|
+
// so supervisor.js, engine/recovery/watchdog.js, and dashboard.js's in-process
|
|
51
54
|
// watchdog all feed the same rolling window in control.json instead of each
|
|
52
55
|
// keeping (and silently ignoring) their own. Falls through to a no-op when
|
|
53
56
|
// shared.js can't load, matching this file's existing fail-open posture.
|
|
@@ -56,8 +59,8 @@ function _recordCrashLoopRespawn(source) {
|
|
|
56
59
|
if (!shared || typeof shared.recordEngineRespawn !== 'function') return;
|
|
57
60
|
try { shared.recordEngineRespawn(source); } catch { /* best-effort */ }
|
|
58
61
|
}
|
|
59
|
-
function _engineDir() { return _sharedOrNull()?.ENGINE_DIR || __dirname; }
|
|
60
|
-
const STATIC_ENGINE_DIR = __dirname; // for require-time paths that can't be lazy
|
|
62
|
+
function _engineDir() { return _sharedOrNull()?.ENGINE_DIR || path.resolve(__dirname, '..'); }
|
|
63
|
+
const STATIC_ENGINE_DIR = path.resolve(__dirname, '..'); // for require-time paths that can't be lazy
|
|
61
64
|
function CONTROL_PATH_FN() { return path.join(_engineDir(), 'control.json'); }
|
|
62
65
|
function STOP_INTENT_PATH_FN() { return _sharedOrNull()?.STOP_INTENT_PATH || path.join(_engineDir(), 'stop-intent.json'); }
|
|
63
66
|
function SUPERVISOR_PID_PATH_FN() { return path.join(_engineDir(), 'supervisor.pid'); }
|
|
@@ -94,14 +97,15 @@ function _resolveDashPort() {
|
|
|
94
97
|
const POST_SPAWN_GRACE_MS = Number(process.env.MINIONS_SUPERVISOR_GRACE_MS) || 15000;
|
|
95
98
|
// #421 — heartbeat age threshold before supervisor considers the engine event loop frozen.
|
|
96
99
|
const SUPERVISOR_STALE_ENGINE_HEARTBEAT_MS = Number(process.env.MINIONS_SUPERVISOR_STALE_HEARTBEAT_MS) || 180000; // 3 min
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
100
|
+
// A recent shell-free heartbeat vetoes a failed PID shell-out. This is shorter
|
|
101
|
+
// than the stale-heartbeat watchdog but long enough to absorb several missed
|
|
102
|
+
// 15s heartbeat writes under host load.
|
|
103
|
+
const SUPERVISOR_FRESH_HEARTBEAT_MS = Number(process.env.MINIONS_SUPERVISOR_FRESH_HEARTBEAT_MS) || 60000;
|
|
104
|
+
// Dashboard liveness needs two independent signals. A frozen event loop can
|
|
105
|
+
// retain its listening socket, while netstat can time out or return no rows on
|
|
106
|
+
// a loaded Windows host even though HTTP still works. Only respawn after both
|
|
107
|
+
// fail for DASH_HEALTH_MAX_FAILS consecutive ticks. With the default 30s
|
|
108
|
+
// interval, 3 strikes is about 90s of continuous unresponsiveness.
|
|
105
109
|
const DASH_HEALTH_TIMEOUT_MS = Number(process.env.MINIONS_SUPERVISOR_DASH_HEALTH_TIMEOUT_MS) || 5000;
|
|
106
110
|
const DASH_HEALTH_MAX_FAILS = Number(process.env.MINIONS_SUPERVISOR_DASH_HEALTH_FAILS) || 3;
|
|
107
111
|
const REAP_WAIT_TIMEOUT_MS = Number(process.env.MINIONS_SUPERVISOR_REAP_TIMEOUT_MS) || 10000;
|
|
@@ -112,15 +116,15 @@ function safeReadJson(p) {
|
|
|
112
116
|
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
|
|
113
117
|
}
|
|
114
118
|
|
|
115
|
-
// Single source of truth lives in engine/shared.js (writeStopIntent /
|
|
119
|
+
// Single source of truth lives in engine/core/shared.js (writeStopIntent /
|
|
116
120
|
// clearStopIntent / isStopIntentSet). The supervisor used to re-implement
|
|
117
121
|
// this locally; centralizing it ensures dashboard.js, bin/minions.js,
|
|
118
|
-
// engine/cli.js, and the supervisor all read/write through one contract.
|
|
122
|
+
// engine/operations/cli.js, and the supervisor all read/write through one contract.
|
|
119
123
|
// Fall back to a local file-presence check if shared.js fails to load (e.g.
|
|
120
124
|
// during a bad upgrade) — we'd rather fail-closed than crash the supervisor.
|
|
121
125
|
function isStopIntentSet() {
|
|
122
126
|
try {
|
|
123
|
-
const shared = require('
|
|
127
|
+
const shared = require('../core/shared');
|
|
124
128
|
if (shared && typeof shared.isStopIntentSet === 'function') {
|
|
125
129
|
return shared.isStopIntentSet();
|
|
126
130
|
}
|
|
@@ -151,7 +155,7 @@ function isPidAlive(pid, {
|
|
|
151
155
|
const out = execSync(`tasklist /FI "PID eq ${n}" /NH`, {
|
|
152
156
|
encoding: 'utf8', timeout: 3000, windowsHide: true,
|
|
153
157
|
});
|
|
154
|
-
// Word-boundary + image-name match (mirrors engine/restart-health.js) so a
|
|
158
|
+
// Word-boundary + image-name match (mirrors engine/recovery/restart-health.js) so a
|
|
155
159
|
// digit-substring collision in another column can't read a dead pid as
|
|
156
160
|
// alive. Fall back to an inline regex if shared.js isn't loadable.
|
|
157
161
|
const shared = _sharedOrNull();
|
|
@@ -206,125 +210,12 @@ function listeningPidsForPort(port) {
|
|
|
206
210
|
// checkout on the same machine is never touched. Opt out via
|
|
207
211
|
// MINIONS_SUPERVISOR_REAP=0.
|
|
208
212
|
const REAP_ENABLED = process.env.MINIONS_SUPERVISOR_REAP !== '0';
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// tell which token is the *script* being executed.
|
|
216
|
-
function _tokenizeCmdline(cmdline) {
|
|
217
|
-
const toks = [];
|
|
218
|
-
let cur = '';
|
|
219
|
-
let inQuote = false;
|
|
220
|
-
for (const ch of String(cmdline)) {
|
|
221
|
-
if (ch === '"') { inQuote = !inQuote; continue; }
|
|
222
|
-
if (!inQuote && (ch === ' ' || ch === '\t')) { if (cur) { toks.push(cur); cur = ''; } continue; }
|
|
223
|
-
cur += ch;
|
|
224
|
-
}
|
|
225
|
-
if (cur) toks.push(cur);
|
|
226
|
-
return toks;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// True iff `cmdline` RUNS `target` as its script — not merely mentions it as an
|
|
230
|
-
// argument. The script is the first `.js` token after the node executable, so
|
|
231
|
-
// `node .../engine.js start` matches but `node eslint.js .../engine.js` (where
|
|
232
|
-
// engine.js is an argument to a different script) does NOT. Exact, normalized
|
|
233
|
-
// equality also rejects longer-name false positives (`.../engine.js.bak`,
|
|
234
|
-
// `.../engine.json`) that a plain substring `includes` would catch. `target` is
|
|
235
|
-
// already normalized by the caller.
|
|
236
|
-
//
|
|
237
|
-
// This is intentionally stricter than substring matching: every orphan the
|
|
238
|
-
// supervisor needs to reap was spawned by spawnEngine()/spawnDashboard() with a
|
|
239
|
-
// byte-identical `path.join(MINIONS_DIR, '<script>.js')`, so exact script-token
|
|
240
|
-
// match catches all of them while a foreign process that only *names* the path
|
|
241
|
-
// is left alone (fail-safe: never kill on a loose match).
|
|
242
|
-
//
|
|
243
|
-
// ENGINE subcommand gate: for an `engine.js` target we additionally require the
|
|
244
|
-
// token AFTER the script to be `start`, so a short-lived `node engine.js stop`
|
|
245
|
-
// one-shot (spawned by `minions restart`/`down`/`update` and the `stop` verb
|
|
246
|
-
// during the restart window) is NEVER reaped. The long-lived daemon is ALWAYS
|
|
247
|
-
// spawned with `'start'` (supervisor/dashboard spawnEngine + bin/minions.js), so
|
|
248
|
-
// the requirement loses zero real orphans. The `dashboard.js` match stays BARE
|
|
249
|
-
// (no subcommand) — only the engine candidate gets the `start` requirement.
|
|
250
|
-
function _cmdRunsScript(cmdline, target) {
|
|
251
|
-
const toks = _tokenizeCmdline(cmdline);
|
|
252
|
-
const isEngineTarget = target === 'engine.js' || target.endsWith('/engine.js');
|
|
253
|
-
// toks[0] is the node executable; the script is the first later .js token.
|
|
254
|
-
for (let i = 1; i < toks.length; i++) {
|
|
255
|
-
const norm = _normPath(toks[i]);
|
|
256
|
-
if (norm.endsWith('.js')) {
|
|
257
|
-
if (norm !== target) return false;
|
|
258
|
-
if (isEngineTarget) {
|
|
259
|
-
// Only the daemon (`engine.js start`) is reapable; one-shots like
|
|
260
|
-
// `engine.js stop` are left alone.
|
|
261
|
-
const next = toks[i + 1] ? _normPath(toks[i + 1]) : '';
|
|
262
|
-
return next === 'start';
|
|
263
|
-
}
|
|
264
|
-
return true;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// PIDs of `node` processes that RUN `needle` (a script path) as their script.
|
|
271
|
-
// Cross-platform: PowerShell CIM on Windows, `ps` elsewhere. Best effort —
|
|
272
|
-
// returns [] on any enumeration failure (fail-open: leak rather than risk
|
|
273
|
-
// killing the wrong process).
|
|
274
|
-
//
|
|
275
|
-
// The OS does the coarse filtering (WQL `CommandLine LIKE` / a `grep` on the
|
|
276
|
-
// script basename) so only candidate rows — not the machine's entire node/
|
|
277
|
-
// process table — cross into the JS heap; `_cmdRunsScript` then applies the
|
|
278
|
-
// exact script-position match. On a busy host (CI runners, many node procs)
|
|
279
|
-
// this keeps the buffered stdout to a handful of rows instead of megabytes.
|
|
280
|
-
// `maxBuffer` caps the read defensively in case the coarse filter still matches
|
|
281
|
-
// a lot. The basename is an internal constant (`engine.js`/`dashboard.js`), so
|
|
282
|
-
// it carries no shell/WQL-injection risk.
|
|
283
|
-
const _ENUM_MAX_BUFFER = 8 * 1024 * 1024;
|
|
284
|
-
function listNodePidsMatching(needle) {
|
|
285
|
-
const target = _normPath(needle);
|
|
286
|
-
const base = path.basename(needle); // coarse prefilter, e.g. 'engine.js'
|
|
287
|
-
const pids = [];
|
|
288
|
-
try {
|
|
289
|
-
let out = '';
|
|
290
|
-
if (isWin) {
|
|
291
|
-
// spawnSync (no shell) + -EncodedCommand sidesteps cmd.exe quoting — an
|
|
292
|
-
// inline `powershell -Command "... $(...) | ..."` via execSync gets its
|
|
293
|
-
// `$(...)`/`|` mangled by cmd.exe before PowerShell ever sees them.
|
|
294
|
-
const ps = `Get-CimInstance Win32_Process -Filter "name='node.exe' AND CommandLine LIKE '%${base}%'" | `
|
|
295
|
-
+ 'ForEach-Object { "$($_.ProcessId)|$($_.CommandLine)" }';
|
|
296
|
-
const res = spawnSync('powershell',
|
|
297
|
-
['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(ps, 'utf16le').toString('base64')],
|
|
298
|
-
{ encoding: 'utf8', timeout: 8000, windowsHide: true, maxBuffer: _ENUM_MAX_BUFFER });
|
|
299
|
-
out = (res && res.stdout) || '';
|
|
300
|
-
} else {
|
|
301
|
-
// grep prefilters to candidate rows; no match exits 1 -> execSync throws
|
|
302
|
-
// -> caught below -> [] (correct: nothing to reap).
|
|
303
|
-
out = execSync(`ps -eo pid=,args= | grep -iF -- '${base}'`,
|
|
304
|
-
{ encoding: 'utf8', timeout: 8000, maxBuffer: _ENUM_MAX_BUFFER });
|
|
305
|
-
}
|
|
306
|
-
for (const line of out.split('\n')) {
|
|
307
|
-
const trimmed = line.trim();
|
|
308
|
-
if (!trimmed) continue;
|
|
309
|
-
// Windows rows are "PID|<full command line>"; ps rows are "PID <args>".
|
|
310
|
-
const m = isWin ? trimmed.match(/^(\d+)\|(.*)$/) : trimmed.match(/^(\d+)\s+(.*)$/);
|
|
311
|
-
if (!m) continue;
|
|
312
|
-
const pid = Number(m[1]);
|
|
313
|
-
if (pid && _cmdRunsScript(m[2], target)) pids.push(pid);
|
|
314
|
-
}
|
|
315
|
-
} catch { /* enumeration failed — fail-open with whatever we collected */ }
|
|
316
|
-
return pids;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function _killHard(pid) {
|
|
320
|
-
const shared = _sharedOrNull();
|
|
321
|
-
try {
|
|
322
|
-
if (shared && typeof shared.killImmediate === 'function') { shared.killImmediate({ pid }); return; }
|
|
323
|
-
} catch { /* fall through to platform kill */ }
|
|
324
|
-
try {
|
|
325
|
-
if (isWin) execSync(`taskkill /F /T /PID ${pid}`, { timeout: 5000, windowsHide: true, stdio: 'ignore' });
|
|
326
|
-
else process.kill(pid, 'SIGKILL');
|
|
327
|
-
} catch { /* already gone */ }
|
|
213
|
+
const _cmdRunsScript = commandLineRunsScript;
|
|
214
|
+
const listNodePidsMatching = listNodePidsMatchingScript;
|
|
215
|
+
function _killHard(pid, scriptPath) {
|
|
216
|
+
terminateProcess('engine.supervisor.daemon-replace', { pid }, {
|
|
217
|
+
scriptPath,
|
|
218
|
+
});
|
|
328
219
|
}
|
|
329
220
|
|
|
330
221
|
function _sleepSync(ms) {
|
|
@@ -357,9 +248,17 @@ function reapStrayProcesses(scriptPath, label, options = {}) {
|
|
|
357
248
|
const listPids = options.listPids || listNodePidsMatching;
|
|
358
249
|
const kill = options.kill || _killHard;
|
|
359
250
|
const pids = listPids(scriptPath).filter(p => p !== process.pid);
|
|
251
|
+
const expectedPids = (options.expectedPids || [])
|
|
252
|
+
.map(Number)
|
|
253
|
+
.filter(pid => Number.isInteger(pid) && pid > 0 && pid !== process.pid);
|
|
254
|
+
const expectedIsAlive = options.isAlive || isPidAlive;
|
|
255
|
+
const unproven = expectedPids.filter(pid => expectedIsAlive(pid) && !pids.includes(pid));
|
|
256
|
+
if (unproven.length > 0) {
|
|
257
|
+
throw new Error(`Refusing to respawn ${label}: live PID ownership was not proven for ${unproven.join(', ')}`);
|
|
258
|
+
}
|
|
360
259
|
if (!pids.length) return 0;
|
|
361
260
|
console.log(`[supervisor] Reaping ${pids.length} stray ${label} process(es) before respawn: ${pids.join(', ')}`);
|
|
362
|
-
for (const pid of pids) kill(pid);
|
|
261
|
+
for (const pid of pids) kill(pid, scriptPath);
|
|
363
262
|
const waited = waitForPidsToExit(pids, options);
|
|
364
263
|
if (!waited.ok) {
|
|
365
264
|
throw new Error(`Refusing to respawn ${label}: stale PID(s) still alive after ${options.timeoutMs || REAP_WAIT_TIMEOUT_MS}ms: ${waited.remaining.join(', ')}`);
|
|
@@ -381,7 +280,7 @@ function openAppendFd(name) {
|
|
|
381
280
|
// still serves HTTP 200, but you can't see anything it logs).
|
|
382
281
|
const dir = _engineDir();
|
|
383
282
|
try {
|
|
384
|
-
const shared = require('
|
|
283
|
+
const shared = require('../core/shared');
|
|
385
284
|
if (shared && typeof shared.openAppendLogFd === 'function') {
|
|
386
285
|
return shared.openAppendLogFd(name, dir, { fallback: 'ignore' }).fd;
|
|
387
286
|
}
|
|
@@ -507,6 +406,12 @@ function _isRespawnEligibleEngineState(state) {
|
|
|
507
406
|
return state === 'running' || state === 'degraded';
|
|
508
407
|
}
|
|
509
408
|
|
|
409
|
+
function _engineHeartbeatAgeMs(control, now) {
|
|
410
|
+
const heartbeat = Number(control?.heartbeat);
|
|
411
|
+
if (!heartbeat || !Number.isFinite(heartbeat)) return null;
|
|
412
|
+
return now - heartbeat;
|
|
413
|
+
}
|
|
414
|
+
|
|
510
415
|
const ENGINE_WATCHDOG_OPERATION_NAMES = [
|
|
511
416
|
'isPidAlive',
|
|
512
417
|
'reapStrayProcesses',
|
|
@@ -564,6 +469,15 @@ function checkEngine(now, operations) {
|
|
|
564
469
|
const ops = _resolveEngineWatchdogOperations(operations);
|
|
565
470
|
if (control.pid && ops.isPidAlive(control.pid)) return;
|
|
566
471
|
|
|
472
|
+
const heartbeatAgeMs = _engineHeartbeatAgeMs(control, now);
|
|
473
|
+
if (control.pid && heartbeatAgeMs !== null && heartbeatAgeMs < SUPERVISOR_FRESH_HEARTBEAT_MS) {
|
|
474
|
+
console.log(
|
|
475
|
+
`[supervisor] Engine PID ${control.pid} reported dead by the liveness probe but its heartbeat is ` +
|
|
476
|
+
`${Math.round(heartbeatAgeMs / 1000)}s old — treating as up (false-negative suppressed)`,
|
|
477
|
+
);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
567
481
|
console.log(`[supervisor] Engine PID ${control.pid || '(none)'} is dead — respawning...`);
|
|
568
482
|
// Reap any orphan engines first so a stale control.json PID can't snowball
|
|
569
483
|
// into an unbounded pile of contending engine processes.
|
|
@@ -578,7 +492,7 @@ function checkEngine(now, operations) {
|
|
|
578
492
|
// event loop has frozen (sync busy-loop, blocked system call). Unlike
|
|
579
493
|
// checkEngine (which only fires when the PID is dead), this fires when the PID
|
|
580
494
|
// is alive but control.heartbeat has not advanced for SUPERVISOR_STALE_ENGINE_HEARTBEAT_MS.
|
|
581
|
-
// The heartbeat is written every ~15s by a dedicated timer in engine/cli.js; if
|
|
495
|
+
// The heartbeat is written every ~15s by a dedicated timer in engine/operations/cli.js; if
|
|
582
496
|
// it goes stale while the PID persists, the event loop is stuck.
|
|
583
497
|
function checkEngineHung(now, operations) {
|
|
584
498
|
if (now - _lastEngineRespawnAt < POST_SPAWN_GRACE_MS) return;
|
|
@@ -605,17 +519,26 @@ function checkEngineHung(now, operations) {
|
|
|
605
519
|
`(${Math.round(heartbeatAge / 1000)}s old, threshold ${SUPERVISOR_STALE_ENGINE_HEARTBEAT_MS / 1000}s) — ` +
|
|
606
520
|
`event loop likely frozen; restarting engine`,
|
|
607
521
|
);
|
|
608
|
-
ops.reapStrayProcesses(path.join(MINIONS_DIR, 'engine.js'), 'engine'
|
|
522
|
+
ops.reapStrayProcesses(path.join(MINIONS_DIR, 'engine.js'), 'engine', {
|
|
523
|
+
expectedPids: [control.pid],
|
|
524
|
+
isAlive: ops.isPidAlive,
|
|
525
|
+
});
|
|
609
526
|
const newPid = ops.spawnEngine();
|
|
610
527
|
_lastEngineRespawnAt = now;
|
|
611
528
|
ops.recordCrashLoopRespawn('supervisor:stale-heartbeat');
|
|
612
529
|
console.log(`[supervisor] Engine restarted due to stale heartbeat (new PID: ${newPid})`);
|
|
613
530
|
}
|
|
614
531
|
|
|
615
|
-
async function checkDashboard(now) {
|
|
532
|
+
async function checkDashboard(now, operations = {}) {
|
|
616
533
|
if (now - _lastDashboardRespawnAt < POST_SPAWN_GRACE_MS) { _dashHealthFailStreak = 0; return; }
|
|
617
|
-
const
|
|
618
|
-
const
|
|
534
|
+
const resolveDashPort = operations.resolveDashPort || _resolveDashPort;
|
|
535
|
+
const findListeningPids = operations.listeningPidsForPort || listeningPidsForPort;
|
|
536
|
+
const probeDashboardHealth = operations.probeDashboardHealth || _probeDashboardHealth;
|
|
537
|
+
const reap = operations.reapStrayProcesses || reapStrayProcesses;
|
|
538
|
+
const spawnDash = operations.spawnDashboard || spawnDashboard;
|
|
539
|
+
const pidAlive = operations.isPidAlive || isPidAlive;
|
|
540
|
+
const dashPort = resolveDashPort();
|
|
541
|
+
const pids = findListeningPids(dashPort);
|
|
619
542
|
|
|
620
543
|
const respawn = (why) => {
|
|
621
544
|
console.log(`[supervisor] Dashboard ${why} on port ${dashPort} — respawning...`);
|
|
@@ -624,38 +547,43 @@ async function checkDashboard(now) {
|
|
|
624
547
|
// the canonical port for the fresh one whose beacon _resolveDashPort() reads.
|
|
625
548
|
// Critically, for the frozen-but-listening case the reap also KILLS the wedged
|
|
626
549
|
// process still holding the port so spawnDashboard() can bind it.
|
|
627
|
-
|
|
628
|
-
|
|
550
|
+
reap(path.join(MINIONS_DIR, 'dashboard.js'), 'dashboard', {
|
|
551
|
+
expectedPids: pids,
|
|
552
|
+
isAlive: pidAlive,
|
|
553
|
+
});
|
|
554
|
+
const newPid = spawnDash(dashPort);
|
|
629
555
|
_lastDashboardRespawnAt = now;
|
|
630
556
|
_dashHealthFailStreak = 0;
|
|
631
557
|
console.log(`[supervisor] Dashboard respawned (new PID: ${newPid})`);
|
|
558
|
+
return { status: 'respawned', pid: newPid, port: dashPort };
|
|
632
559
|
};
|
|
633
560
|
|
|
634
|
-
|
|
635
|
-
if (
|
|
561
|
+
const healthy = await probeDashboardHealth(dashPort);
|
|
562
|
+
if (healthy === true) {
|
|
563
|
+
if (pids.length === 0) {
|
|
564
|
+
console.log(
|
|
565
|
+
`[supervisor] Dashboard port probe missed ${dashPort}, but /api/health responded — ` +
|
|
566
|
+
'treating as up (false-negative suppressed)',
|
|
567
|
+
);
|
|
568
|
+
}
|
|
636
569
|
_dashHealthFailStreak = 0;
|
|
637
|
-
|
|
638
|
-
return;
|
|
570
|
+
return { status: 'healthy', port: dashPort };
|
|
639
571
|
}
|
|
640
572
|
|
|
641
|
-
|
|
642
|
-
// listening dashboard keeps the socket LISTENING yet serves the in-browser
|
|
643
|
-
// "unreachable" banner. Probe /api/health; only respawn after a streak of
|
|
644
|
-
// failures so a legitimately busy getStatus() rebuild isn't mistaken for a hang.
|
|
645
|
-
const healthy = await _probeDashboardHealth(dashPort);
|
|
646
|
-
if (healthy === null) {
|
|
647
|
-
// Probe helper unavailable — fall back to port-only liveness (the pre-existing
|
|
648
|
-
// behavior). Don't count this as a failure; we won't respawn a bound dashboard
|
|
649
|
-
// we can't actually prove is frozen.
|
|
573
|
+
if (healthy === null && pids.length > 0) {
|
|
650
574
|
_dashHealthFailStreak = 0;
|
|
651
|
-
return;
|
|
575
|
+
return { status: 'listening', port: dashPort };
|
|
652
576
|
}
|
|
653
|
-
if (healthy) { _dashHealthFailStreak = 0; return; }
|
|
654
577
|
|
|
655
578
|
_dashHealthFailStreak++;
|
|
656
|
-
console.log(
|
|
657
|
-
|
|
658
|
-
|
|
579
|
+
console.log(
|
|
580
|
+
`[supervisor] Dashboard has no responsive health signal on port ${dashPort} ` +
|
|
581
|
+
`(${_dashHealthFailStreak}/${DASH_HEALTH_MAX_FAILS} strikes; listeners=${pids.length})`,
|
|
582
|
+
);
|
|
583
|
+
if (_dashHealthFailStreak < DASH_HEALTH_MAX_FAILS) {
|
|
584
|
+
return { status: 'unhealthy', strikes: _dashHealthFailStreak, port: dashPort };
|
|
585
|
+
}
|
|
586
|
+
return respawn(pids.length > 0 ? 'frozen (health probe failed)' : 'unreachable');
|
|
659
587
|
}
|
|
660
588
|
|
|
661
589
|
let _tickInFlight = false;
|
|
@@ -747,6 +675,8 @@ module.exports = {
|
|
|
747
675
|
_isRespawnEligibleEngineState,
|
|
748
676
|
checkDashboard,
|
|
749
677
|
_probeDashboardHealth,
|
|
678
|
+
_killHard,
|
|
679
|
+
SUPERVISOR_FRESH_HEARTBEAT_MS,
|
|
750
680
|
SUPERVISOR_STALE_ENGINE_HEARTBEAT_MS,
|
|
751
681
|
DASH_HEALTH_MAX_FAILS,
|
|
752
682
|
tick,
|