@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,30 +1,66 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* engine/process-utils.js — Cross-platform process-management helpers
|
|
2
|
+
* engine/processes/process-utils.js — Cross-platform process-management helpers
|
|
3
3
|
* (kill trees, PID liveness, CPU sampling, process-table enumeration, cwd
|
|
4
4
|
* holder detection, descendant/reachability BFS).
|
|
5
5
|
*
|
|
6
|
-
* Extracted from engine/shared.js (P-mrb8yg9x001k1401-b) — a self-contained
|
|
6
|
+
* Extracted from engine/core/shared.js (P-mrb8yg9x001k1401-b) — a self-contained
|
|
7
7
|
* slice with no dependency on the rest of shared.js's state/JSON machinery.
|
|
8
8
|
* shared.js re-exports these names so no caller needs to change its
|
|
9
|
-
* `require('
|
|
10
|
-
* ENGINE_DEFAULTS from shared.js, it lazily `require('
|
|
9
|
+
* `require('../core/shared')` call. Where a function needs MINIONS_DIR /
|
|
10
|
+
* ENGINE_DEFAULTS from shared.js, it lazily `require('../core/shared')`s inside
|
|
11
11
|
* the function body (not at module top-level) to avoid a load-time
|
|
12
12
|
* circular-require deadlock with shared.js requiring this module back.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
17
|
-
const { execSync: _execSync, execFile: _execFile } = require('child_process');
|
|
17
|
+
const { execSync: _execSync, execFile: _execFile, spawnSync: _spawnSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
// W-ms6dmv99005o26f4 — tri-state PID liveness.
|
|
20
|
+
//
|
|
21
|
+
// `process.kill(pid, 0)` has THREE outcomes, not two:
|
|
22
|
+
// - no throw → the signal was deliverable; the process exists.
|
|
23
|
+
// - ESRCH → the kernel found no such process; genuine, conclusive absence.
|
|
24
|
+
// - EPERM → the kernel FOUND the process and refused the signal. This is
|
|
25
|
+
// positive evidence of existence, and every helper here used to
|
|
26
|
+
// report it as death.
|
|
27
|
+
// - anything else (EINVAL, EACCES on exotic platforms, a thrown validate error
|
|
28
|
+
// from a caller-supplied probe) → we learned nothing.
|
|
29
|
+
//
|
|
30
|
+
// Collapsing all four into `false` is the third link of the 2026-07-29T17:44Z
|
|
31
|
+
// false-orphan storm: in engine/cli.js a `false` here zeroes `agentPid`, which is
|
|
32
|
+
// exactly the `hadPid && !agentPid` condition that flags a dispatch confirmed-dead
|
|
33
|
+
// at restart — bypassing both minAliveBeforeOrphanMs and the engine-restart grace
|
|
34
|
+
// window, so the dispatch is orphaned on the next tick after a few seconds of
|
|
35
|
+
// silence with failure_class `engine-restart`.
|
|
36
|
+
//
|
|
37
|
+
// `conclusive` lets destructive callers require proof before acting; the boolean
|
|
38
|
+
// facades below keep their historical "false == not known to be alive" contract
|
|
39
|
+
// for non-destructive callers.
|
|
40
|
+
function inspectPidLiveness(pid, opts = {}) {
|
|
41
|
+
const value = Number(pid);
|
|
42
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
43
|
+
return { alive: false, conclusive: true, reason: 'invalid-pid' };
|
|
44
|
+
}
|
|
45
|
+
const kill = typeof opts.kill === 'function' ? opts.kill : process.kill.bind(process);
|
|
46
|
+
try {
|
|
47
|
+
kill(value, 0);
|
|
48
|
+
return { alive: true, conclusive: true, reason: 'signalled' };
|
|
49
|
+
} catch (err) {
|
|
50
|
+
const code = String(err && err.code || '');
|
|
51
|
+
if (code === 'ESRCH') return { alive: false, conclusive: true, reason: 'esrch' };
|
|
52
|
+
if (code === 'EPERM') return { alive: true, conclusive: true, reason: 'eperm' };
|
|
53
|
+
return { alive: false, conclusive: false, reason: code ? code.toLowerCase() : 'probe-failed' };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
18
56
|
|
|
19
57
|
// Shared.js-local PID liveness check, moved here unchanged. Avoids a circular
|
|
20
|
-
// require on engine/cli.js (which has its own isPidAlive) and engine/timeout.js
|
|
58
|
+
// require on engine/operations/cli.js (which has its own isPidAlive) and engine/orchestration/timeout.js
|
|
21
59
|
// (which has isOsPidAliveForDispatch — but that one looks up pid from a
|
|
22
60
|
// side-channel pid-file, whereas callers that already have the pid in-hand
|
|
23
61
|
// use this simple check).
|
|
24
|
-
function isPidAlive(pid) {
|
|
25
|
-
|
|
26
|
-
try { process.kill(pid, 0); return true; }
|
|
27
|
-
catch { return false; }
|
|
62
|
+
function isPidAlive(pid, opts = {}) {
|
|
63
|
+
return inspectPidLiveness(pid, opts).alive;
|
|
28
64
|
}
|
|
29
65
|
|
|
30
66
|
// ─── Cross-Platform Process Kill Helpers ─────────────────────────────────────
|
|
@@ -61,9 +97,9 @@ function unrefTimer(timer) {
|
|
|
61
97
|
return timer;
|
|
62
98
|
}
|
|
63
99
|
|
|
64
|
-
function
|
|
100
|
+
function _killGracefully(proc, graceMs = 5000) {
|
|
65
101
|
const pid = normalizeKillPid(proc);
|
|
66
|
-
if (!pid) return;
|
|
102
|
+
if (!pid) return false;
|
|
67
103
|
if (process.platform === 'win32') {
|
|
68
104
|
try { _execSync(`taskkill /PID ${pid} /T`, { stdio: 'pipe', timeout: 3000, windowsHide: true }); } catch { /* process may be dead */ }
|
|
69
105
|
unrefTimer(setTimeout(() => {
|
|
@@ -75,9 +111,10 @@ function killGracefully(proc, graceMs = 5000) {
|
|
|
75
111
|
killUnixProcessTree(pid, 'SIGKILL');
|
|
76
112
|
}, graceMs));
|
|
77
113
|
}
|
|
114
|
+
return true;
|
|
78
115
|
}
|
|
79
116
|
|
|
80
|
-
function
|
|
117
|
+
function _killImmediate(proc) {
|
|
81
118
|
const pid = normalizeKillPid(proc);
|
|
82
119
|
if (!pid) return false;
|
|
83
120
|
if (process.platform === 'win32') {
|
|
@@ -91,12 +128,183 @@ function killImmediate(proc) {
|
|
|
91
128
|
return killUnixProcessTree(pid, 'SIGKILL');
|
|
92
129
|
}
|
|
93
130
|
|
|
131
|
+
// Kill one process without walking its descendants. Daemon replacement uses
|
|
132
|
+
// this because detached agents and managed services intentionally outlive the
|
|
133
|
+
// engine/dashboard process that launched them.
|
|
134
|
+
function _killProcessOnly(proc) {
|
|
135
|
+
const pid = normalizeKillPid(proc);
|
|
136
|
+
if (!pid) return false;
|
|
137
|
+
if (process.platform === 'win32') {
|
|
138
|
+
try {
|
|
139
|
+
_execSync(`taskkill /PID ${pid} /F`, { stdio: 'pipe', timeout: 3000, windowsHide: true });
|
|
140
|
+
return true;
|
|
141
|
+
} catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
process.kill(pid, 'SIGKILL');
|
|
147
|
+
return true;
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function _normalizeProcessScriptPath(value, platform = process.platform) {
|
|
154
|
+
const normalized = String(value || '').replace(/\\/g, '/');
|
|
155
|
+
return platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// `path.basename` binds to the HOST platform, but the helpers below are told
|
|
159
|
+
// which platform's process table they are parsing. Parsing a Windows path with
|
|
160
|
+
// POSIX semantics keeps the backslashes (breaking Windows enumeration from a
|
|
161
|
+
// Linux/macOS engine), and parsing a POSIX path with Windows semantics splits on
|
|
162
|
+
// a backslash that is a legal POSIX filename character. Always pick the flavour
|
|
163
|
+
// that matches the platform being enumerated. Call sites that have already run
|
|
164
|
+
// the raw value through `_normalizeProcessScriptPath` are separator-agnostic
|
|
165
|
+
// today, but they route through here too so the rule holds if that normalization
|
|
166
|
+
// ever stops collapsing backslashes.
|
|
167
|
+
function _processPathFlavor(platform) {
|
|
168
|
+
return platform === 'win32' ? path.win32 : path.posix;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function _tokenizeProcessCommandLine(commandLine) {
|
|
172
|
+
const tokens = [];
|
|
173
|
+
let current = '';
|
|
174
|
+
let quoted = false;
|
|
175
|
+
for (const char of String(commandLine || '')) {
|
|
176
|
+
if (char === '"') {
|
|
177
|
+
quoted = !quoted;
|
|
178
|
+
} else if (!quoted && /\s/.test(char)) {
|
|
179
|
+
if (current) {
|
|
180
|
+
tokens.push(current);
|
|
181
|
+
current = '';
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
current += char;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (current) tokens.push(current);
|
|
188
|
+
return tokens;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function commandLineRunsScript(commandLine, scriptPath, platform = process.platform) {
|
|
192
|
+
const target = _normalizeProcessScriptPath(scriptPath, platform);
|
|
193
|
+
if (!target) return false;
|
|
194
|
+
const tokens = _tokenizeProcessCommandLine(commandLine);
|
|
195
|
+
const executable = _processPathFlavor(platform)
|
|
196
|
+
.basename(_normalizeProcessScriptPath(tokens[0], platform))
|
|
197
|
+
.toLowerCase();
|
|
198
|
+
if (executable !== 'node' && executable !== 'node.exe' && executable !== 'nodejs') return false;
|
|
199
|
+
const windows = platform === 'win32';
|
|
200
|
+
const isEngine = target === 'engine.js' || target.endsWith('/engine.js');
|
|
201
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
202
|
+
const token = _normalizeProcessScriptPath(tokens[i], platform);
|
|
203
|
+
if (!token.endsWith('.js')) continue;
|
|
204
|
+
if (token !== target) {
|
|
205
|
+
if (windows) return false;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
if (!isEngine) return true;
|
|
209
|
+
return _normalizeProcessScriptPath(tokens[i + 1], platform) === 'start';
|
|
210
|
+
}
|
|
211
|
+
// `ps` flattens argv, so an unquoted POSIX path containing spaces cannot be
|
|
212
|
+
// reconstructed by tokenization. Match the exact path in the raw command
|
|
213
|
+
// only when no earlier JavaScript entrypoint exists.
|
|
214
|
+
if (!windows) {
|
|
215
|
+
const command = String(commandLine || '').replace(/\\/g, '/');
|
|
216
|
+
let offset = 0;
|
|
217
|
+
while (offset < command.length) {
|
|
218
|
+
const index = command.indexOf(target, offset);
|
|
219
|
+
if (index < 0) break;
|
|
220
|
+
const end = index + target.length;
|
|
221
|
+
const leftBoundary = index === 0 || /\s|["']/.test(command[index - 1]);
|
|
222
|
+
const rightBoundary = end === command.length || /\s|["']/.test(command[end]);
|
|
223
|
+
if (leftBoundary && rightBoundary) {
|
|
224
|
+
const prefix = command.slice(0, index);
|
|
225
|
+
if (!/\.js(?:\s|["']|$)/.test(prefix)) {
|
|
226
|
+
const suffix = command.slice(end).replace(/^["']/, '').trimStart();
|
|
227
|
+
return isEngine ? /^start(?:\s|$)/.test(suffix) : true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
offset = index + 1;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Return Node PIDs whose executable script token exactly matches any requested
|
|
237
|
+
// path. Enumeration failures fail open: leaking a daemon is safer than killing
|
|
238
|
+
// an unrelated process from another checkout.
|
|
239
|
+
function listNodePidsMatchingScripts(scriptPaths, {
|
|
240
|
+
platform = process.platform,
|
|
241
|
+
spawnSync = _spawnSync,
|
|
242
|
+
execSync = _execSync,
|
|
243
|
+
} = {}) {
|
|
244
|
+
const targets = [...new Set((scriptPaths || []).map(value => String(value || '')).filter(Boolean))];
|
|
245
|
+
if (targets.length === 0) return [];
|
|
246
|
+
const flavor = _processPathFlavor(platform);
|
|
247
|
+
const basenames = [...new Set(targets.map(target => flavor.basename(target)))];
|
|
248
|
+
if (basenames.some(base => !/^[A-Za-z0-9._-]+$/.test(base))) return [];
|
|
249
|
+
let output = '';
|
|
250
|
+
try {
|
|
251
|
+
if (platform === 'win32') {
|
|
252
|
+
const commandLineFilter = basenames
|
|
253
|
+
.map(base => `CommandLine LIKE '%${base.replace(/'/g, "''")}%'`)
|
|
254
|
+
.join(' OR ');
|
|
255
|
+
const ps = `Get-CimInstance Win32_Process -Filter "name='node.exe' AND (${commandLineFilter})" | `
|
|
256
|
+
+ 'ForEach-Object { "$($_.ProcessId)|$($_.CommandLine)" }';
|
|
257
|
+
const result = spawnSync('powershell', [
|
|
258
|
+
'-NoProfile',
|
|
259
|
+
'-NonInteractive',
|
|
260
|
+
'-EncodedCommand',
|
|
261
|
+
Buffer.from(ps, 'utf16le').toString('base64'),
|
|
262
|
+
], {
|
|
263
|
+
encoding: 'utf8',
|
|
264
|
+
timeout: 8000,
|
|
265
|
+
windowsHide: true,
|
|
266
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
267
|
+
});
|
|
268
|
+
if (result.error || result.status !== 0) return [];
|
|
269
|
+
output = String(result.stdout || '');
|
|
270
|
+
} else {
|
|
271
|
+
for (const base of basenames) {
|
|
272
|
+
try {
|
|
273
|
+
output += String(execSync(`ps -eo pid=,args= | grep -F -- '${base}'`, {
|
|
274
|
+
encoding: 'utf8',
|
|
275
|
+
timeout: 8000,
|
|
276
|
+
windowsHide: true,
|
|
277
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
278
|
+
}) || '');
|
|
279
|
+
} catch { /* grep exit 1 means no candidate rows */ }
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const pids = new Set();
|
|
287
|
+
for (const line of output.split(/\r?\n/)) {
|
|
288
|
+
const match = platform === 'win32'
|
|
289
|
+
? line.trim().match(/^(\d+)\|(.*)$/)
|
|
290
|
+
: line.trim().match(/^(\d+)\s+(.*)$/);
|
|
291
|
+
if (!match || !targets.some(target => commandLineRunsScript(match[2], target, platform))) continue;
|
|
292
|
+
const pid = Number(match[1]);
|
|
293
|
+
if (Number.isInteger(pid) && pid > 0) pids.add(pid);
|
|
294
|
+
}
|
|
295
|
+
return [...pids];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function listNodePidsMatchingScript(scriptPath, opts) {
|
|
299
|
+
return listNodePidsMatchingScripts([scriptPath], opts);
|
|
300
|
+
}
|
|
301
|
+
|
|
94
302
|
// Decide whether a Windows `tasklist /FI "PID eq <pid>" /NH` output proves the
|
|
95
303
|
// pid is alive. A bare `out.includes(String(pid))` can read a DEAD pid as alive
|
|
96
304
|
// on a digit-substring collision — the pid appears inside another column (a
|
|
97
305
|
// larger PID, a memory-KB figure, a session id). Require a word-boundary match
|
|
98
306
|
// (`\b<pid>\b`) and, when an `imageName` is supplied, that the expected process
|
|
99
|
-
// image is present too. Mirrors the hardened check in engine/restart-health.js.
|
|
307
|
+
// image is present too. Mirrors the hardened check in engine/recovery/restart-health.js.
|
|
100
308
|
// Pure (no shell-out) so callers can pass captured output and unit tests can
|
|
101
309
|
// feed crafted samples.
|
|
102
310
|
function tasklistOutputShowsPid(out, pid, { imageName } = {}) {
|
|
@@ -160,10 +368,8 @@ function getProcessCpuSeconds(pid) {
|
|
|
160
368
|
return null;
|
|
161
369
|
}
|
|
162
370
|
|
|
163
|
-
// Single-PID kill (no
|
|
164
|
-
|
|
165
|
-
// be a no-op anyway.
|
|
166
|
-
function killByPidImmediate(pid) {
|
|
371
|
+
// Single-PID kill (no tree walk) for identity-verified declarations.
|
|
372
|
+
function _killByPidImmediate(pid) {
|
|
167
373
|
const n = Number(pid);
|
|
168
374
|
if (!Number.isInteger(n) || n <= 0 || n === process.pid) return false;
|
|
169
375
|
if (process.platform === 'win32') {
|
|
@@ -176,7 +382,7 @@ function killByPidImmediate(pid) {
|
|
|
176
382
|
// Batched kill — one OS process for N PIDs. `taskkill` accepts repeated /PID
|
|
177
383
|
// flags natively; on Unix we still loop process.kill, which is in-process and
|
|
178
384
|
// cheap. Returns the count of successful kills.
|
|
179
|
-
function
|
|
385
|
+
function _killByPidsImmediate(pids) {
|
|
180
386
|
const valid = (Array.isArray(pids) ? pids : [])
|
|
181
387
|
.map(Number)
|
|
182
388
|
.filter(n => Number.isInteger(n) && n > 0 && n !== process.pid);
|
|
@@ -186,7 +392,7 @@ function killByPidsImmediate(pids) {
|
|
|
186
392
|
try { _execSync(`taskkill /F ${flags}`, { stdio: 'pipe', timeout: 5000, windowsHide: true }); return valid.length; }
|
|
187
393
|
catch {
|
|
188
394
|
let killed = 0;
|
|
189
|
-
for (const p of valid) { if (
|
|
395
|
+
for (const p of valid) { if (_killByPidImmediate(p)) killed++; }
|
|
190
396
|
return killed;
|
|
191
397
|
}
|
|
192
398
|
}
|
|
@@ -203,7 +409,7 @@ function killByPidsImmediate(pids) {
|
|
|
203
409
|
// an attached child (e.g. managed-spawn's Node launcher shim, P-8a4d6f29): a
|
|
204
410
|
// non-tree kill would reap the shim and orphan/leak the child. Returns the
|
|
205
411
|
// count of pids for which the kill call was issued without throwing.
|
|
206
|
-
function
|
|
412
|
+
function _killByPidTreesImmediate(pids) {
|
|
207
413
|
const valid = (Array.isArray(pids) ? pids : [])
|
|
208
414
|
.map(Number)
|
|
209
415
|
.filter(n => Number.isInteger(n) && n > 0 && n !== process.pid);
|
|
@@ -229,6 +435,184 @@ function killByPidTreesImmediate(pids) {
|
|
|
229
435
|
return killed;
|
|
230
436
|
}
|
|
231
437
|
|
|
438
|
+
const PROCESS_TERMINATION_SITES = Object.freeze({
|
|
439
|
+
'bin.daemon-stop-process': { mode: 'process-only', ownership: 'exact-scripts', description: 'Stop an exact-script Minions daemon without its detached children.' },
|
|
440
|
+
'bin.daemon-stop-tree': { mode: 'tree-immediate', ownership: 'exact-scripts', description: 'Stop an exact-script daemon and its owned descendants during full shutdown.' },
|
|
441
|
+
'dashboard.engine-restart': { mode: 'process-only', ownership: 'exact-script', description: 'Restart the engine while preserving detached agents.' },
|
|
442
|
+
'dashboard.keep-process-manual': { mode: 'pid-immediate', ownership: 'exact-identity', description: 'Operator removes one identity-verified keep-process PID.' },
|
|
443
|
+
'dashboard.tools-inventory-shutdown': { mode: 'tree-immediate', ownership: 'child-handle', description: 'Dashboard shutdown reaps its tools-inventory child.' },
|
|
444
|
+
'dashboard.tools-inventory-watchdog': { mode: 'tree-immediate', ownership: 'child-handle', description: 'Tools-inventory child exceeded its watchdog.' },
|
|
445
|
+
'engine.acp-worker-close': { mode: 'tree-immediate', ownership: 'child-handle', description: 'Close an owned ACP worker and descendants.' },
|
|
446
|
+
'engine.acp-worker-root-close': { mode: 'process-only', ownership: 'child-handle', description: 'Close only an ACP worker root while preserving declared descendants.' },
|
|
447
|
+
'engine.cleanup.stale-agent-pid': { mode: 'tree-immediate', ownership: 'recorded-agent', description: 'Reap an identity-verified stale agent PID file.' },
|
|
448
|
+
'engine.cli.kill-command': { mode: 'tree-immediate', ownership: 'recorded-agent', description: 'Explicit CLI kill of an identity-verified agent PID.' },
|
|
449
|
+
'engine.cli.shutdown-pooled': { mode: 'facade-cancel', ownership: 'facade', description: 'Graceful engine shutdown cancels an active pooled turn.' },
|
|
450
|
+
'engine.cli.unsafe-reattach': { mode: 'tree-immediate', ownership: 'recorded-agent', description: 'Reject an identity-verified agent running from an unsafe execution path.' },
|
|
451
|
+
'engine.dispatch.cancel': { mode: 'tree-graceful', ownership: 'recorded-agent', description: 'Explicitly cancel an identity-verified cold dispatch.' },
|
|
452
|
+
'engine.git-status-probe-cleanup': { mode: 'pids-immediate', ownership: 'exact-identities', description: 'Reap git probes proven to target one owned worktree.' },
|
|
453
|
+
'engine.keep-process.ttl': { mode: 'pid-immediate', ownership: 'declared-process', description: 'Expire a keep-process PID whose identity predates its declaration.' },
|
|
454
|
+
'engine.lifecycle.hook-buffer-limit': { mode: 'tree-immediate', ownership: 'child-handle', description: 'Post-completion hook exceeded its output bound.' },
|
|
455
|
+
'engine.lifecycle.hook-timeout-force': { mode: 'tree-immediate', ownership: 'child-handle', description: 'Post-completion hook ignored graceful timeout.' },
|
|
456
|
+
'engine.lifecycle.hook-timeout-graceful': { mode: 'tree-graceful', ownership: 'child-handle', description: 'Post-completion hook exceeded its timeout.' },
|
|
457
|
+
'engine.llm.abort': { mode: 'tree-immediate', ownership: 'child-handle', description: 'Abort an owned direct LLM child.' },
|
|
458
|
+
'engine.llm.timeout': { mode: 'tree-immediate', ownership: 'child-handle', description: 'Owned direct LLM child exceeded its timeout.' },
|
|
459
|
+
'engine.managed-launcher.self-signal': { mode: 'self-signal', ownership: 'self', description: 'Managed launcher forwards a termination signal to itself.' },
|
|
460
|
+
'engine.managed-spawn.reap-failed': { mode: 'tree-immediate', ownership: 'exact-identity', description: 'A newly spawned managed runtime failed acceptance.' },
|
|
461
|
+
'engine.managed-spawn.remove': { mode: 'tree-immediate', ownership: 'exact-identity', description: 'Remove an identity-verified managed process tree.' },
|
|
462
|
+
'engine.pooled-agent.descendants': { mode: 'pids-immediate', ownership: 'exact-identities', description: 'Reap descendants proven to belong to one pooled session.' },
|
|
463
|
+
'engine.query.git-probe-timeout': { mode: 'tree-immediate', ownership: 'child-handle', description: 'Git probe child exceeded its watchdog.' },
|
|
464
|
+
'engine.spawn-agent.descendants': { mode: 'pids-immediate', ownership: 'exact-identities', description: 'Spawn wrapper reaps its snapshotted runtime descendants.' },
|
|
465
|
+
'engine.spawn-agent.stdin-failure': { mode: 'tree-immediate', ownership: 'child-handle', description: 'Runtime child could not receive its startup prompt.' },
|
|
466
|
+
'engine.spawn-partial-failure': { mode: 'tree-immediate', ownership: 'child-handle', description: 'Agent setup failed after creating a cold child.' },
|
|
467
|
+
'engine.spawn-partial-pooled': { mode: 'facade-cancel', ownership: 'facade', description: 'Agent setup failed after acquiring a pooled turn.' },
|
|
468
|
+
'engine.spawn-phase.stall': { mode: 'tree-immediate', ownership: 'tracked-process', description: 'Tracked cold runtime remained wedged in startup.' },
|
|
469
|
+
'engine.spawn-phase.stall-pooled': { mode: 'facade-cancel', ownership: 'facade', description: 'Tracked pooled turn remained wedged in startup.' },
|
|
470
|
+
'engine.supervisor.daemon-replace': { mode: 'process-only', ownership: 'exact-script', description: 'Supervisor replaces an exact-script daemon without descendants.' },
|
|
471
|
+
'engine.timeout.tracked-cold': { mode: 'tree-immediate', ownership: 'tracked-process', description: 'Timeout, steering, or removed-dispatch cleanup stops a tracked cold runtime.' },
|
|
472
|
+
'engine.timeout.tracked-cold-escalation': { mode: 'tree-immediate', ownership: 'tracked-process', description: 'Steering escalation force-stops a tracked cold runtime.' },
|
|
473
|
+
'engine.timeout.tracked-cold-graceful': { mode: 'tree-graceful', ownership: 'tracked-process', description: 'Tracked cold runtime receives graceful timeout termination.' },
|
|
474
|
+
'engine.timeout.tracked-pooled': { mode: 'facade-cancel', ownership: 'facade', description: 'Timeout or steering cancels a pooled turn without killing its worker.' },
|
|
475
|
+
'engine.worktree-gc.orphan-holder': { mode: 'tree-immediate', ownership: 'exact-identity', description: 'Reap an aged process proven to hold an owned orphan worktree.' },
|
|
476
|
+
'test.helper-child-watchdog': { mode: 'tree-immediate', ownership: 'child-handle', description: 'Shared test helper reaps a child that exceeded its watchdog.' },
|
|
477
|
+
'test.process-descendants': { mode: 'pids-immediate', ownership: 'exact-identities', description: 'Test harness reaps descendants of its own worker process.' },
|
|
478
|
+
'test.unit-worker-timeout': { mode: 'tree-immediate', ownership: 'child-handle', description: 'Parallel unit runner reaps a timed-out worker tree.' },
|
|
479
|
+
'compat.tree-graceful': { mode: 'tree-graceful', ownership: 'compatibility', description: 'Legacy public helper; production call sites are forbidden.' },
|
|
480
|
+
'compat.tree-immediate': { mode: 'tree-immediate', ownership: 'compatibility', description: 'Legacy public helper; production call sites are forbidden.' },
|
|
481
|
+
'compat.process-only': { mode: 'process-only', ownership: 'compatibility', description: 'Legacy public helper; production call sites are forbidden.' },
|
|
482
|
+
'compat.pid-immediate': { mode: 'pid-immediate', ownership: 'compatibility', description: 'Legacy public helper; production call sites are forbidden.' },
|
|
483
|
+
'compat.pids-immediate': { mode: 'pids-immediate', ownership: 'compatibility', description: 'Legacy public helper; production call sites are forbidden.' },
|
|
484
|
+
'compat.trees-immediate': { mode: 'trees-immediate', ownership: 'compatibility', description: 'Legacy public helper; production call sites are forbidden.' },
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
function _terminationOwnershipAllowed(policy, target, opts) {
|
|
488
|
+
if (policy.ownership === 'compatibility') return true;
|
|
489
|
+
if (policy.ownership === 'child-handle') {
|
|
490
|
+
return !!target
|
|
491
|
+
&& typeof target === 'object'
|
|
492
|
+
&& normalizeKillPid(target) !== null
|
|
493
|
+
&& target.killed !== true
|
|
494
|
+
&& (!Object.prototype.hasOwnProperty.call(target, 'exitCode') || target.exitCode === null);
|
|
495
|
+
}
|
|
496
|
+
if (policy.ownership === 'facade') return !!target && typeof target.kill === 'function';
|
|
497
|
+
if (policy.ownership === 'self') return Number(target?.pid) === process.pid;
|
|
498
|
+
if (policy.ownership === 'exact-script') {
|
|
499
|
+
const pid = normalizeKillPid(target);
|
|
500
|
+
return !!pid
|
|
501
|
+
&& typeof opts.scriptPath === 'string'
|
|
502
|
+
&& listNodePidsMatchingScript(opts.scriptPath).includes(pid);
|
|
503
|
+
}
|
|
504
|
+
if (policy.ownership === 'exact-scripts') {
|
|
505
|
+
const pid = normalizeKillPid(target);
|
|
506
|
+
return !!pid
|
|
507
|
+
&& Array.isArray(opts.scriptPaths)
|
|
508
|
+
&& listNodePidsMatchingScripts(opts.scriptPaths).includes(pid);
|
|
509
|
+
}
|
|
510
|
+
if (policy.ownership === 'exact-identity') {
|
|
511
|
+
const pid = normalizeKillPid(target);
|
|
512
|
+
const expected = opts.expectedIdentity;
|
|
513
|
+
if (!pid || !expected || Number(expected.pid) !== pid) return false;
|
|
514
|
+
const processes = Array.isArray(opts.processes) ? opts.processes : listAllProcesses();
|
|
515
|
+
return isSameProcessIdentity(expected, getProcessIdentity(pid, { processes }));
|
|
516
|
+
}
|
|
517
|
+
if (policy.ownership === 'declared-process') {
|
|
518
|
+
const pid = normalizeKillPid(target);
|
|
519
|
+
const processes = Array.isArray(opts.processes) ? opts.processes : listAllProcesses();
|
|
520
|
+
return !!pid
|
|
521
|
+
&& Number(opts.recordedAtMs) > 0
|
|
522
|
+
&& isProcessIdentityCompatible(
|
|
523
|
+
getProcessIdentity(pid, { processes }),
|
|
524
|
+
Number(opts.recordedAtMs),
|
|
525
|
+
opts.skewMs,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
if (policy.ownership === 'tracked-process' || policy.ownership === 'recorded-agent') {
|
|
529
|
+
const processes = Array.isArray(opts.processes) ? opts.processes : listAllProcesses();
|
|
530
|
+
return (policy.ownership !== 'tracked-process' || opts.tracked === true)
|
|
531
|
+
&& Number(opts.recordedAtMs) > 0
|
|
532
|
+
&& isRecordedAgentProcess(normalizeKillPid(target), Number(opts.recordedAtMs), {
|
|
533
|
+
...opts,
|
|
534
|
+
processes,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function _filterExactIdentityTargets(target, opts) {
|
|
541
|
+
const pids = (Array.isArray(target) ? target : [])
|
|
542
|
+
.map(Number)
|
|
543
|
+
.filter(pid => Number.isInteger(pid) && pid > 0);
|
|
544
|
+
const expected = opts.expectedIdentities instanceof Map
|
|
545
|
+
? opts.expectedIdentities
|
|
546
|
+
: new Map();
|
|
547
|
+
if (pids.length === 0 || expected.size === 0) return [];
|
|
548
|
+
const processes = Array.isArray(opts.processes) ? opts.processes : listAllProcesses();
|
|
549
|
+
const currentByPid = new Map(processes.map(entry => [Number(entry.pid), entry]));
|
|
550
|
+
return pids.filter(pid => {
|
|
551
|
+
const startedAt = Number(expected.get(pid));
|
|
552
|
+
const current = currentByPid.get(pid);
|
|
553
|
+
return startedAt > 0
|
|
554
|
+
&& Number(current?.startedAt) > 0
|
|
555
|
+
&& Number(current.startedAt) === startedAt;
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function terminateProcess(site, target, opts = {}) {
|
|
560
|
+
const policy = PROCESS_TERMINATION_SITES[site];
|
|
561
|
+
if (!policy) throw new Error(`Unknown process termination site: ${site}`);
|
|
562
|
+
let ownedTarget = target;
|
|
563
|
+
if (policy.ownership === 'exact-identities') {
|
|
564
|
+
ownedTarget = _filterExactIdentityTargets(target, opts);
|
|
565
|
+
if (ownedTarget.length === 0) return 0;
|
|
566
|
+
} else if (!_terminationOwnershipAllowed(policy, target, opts)) {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
switch (policy.mode) {
|
|
570
|
+
case 'tree-graceful':
|
|
571
|
+
return _killGracefully(ownedTarget, opts.graceMs);
|
|
572
|
+
case 'tree-immediate':
|
|
573
|
+
return _killImmediate(ownedTarget);
|
|
574
|
+
case 'process-only':
|
|
575
|
+
return _killProcessOnly(ownedTarget);
|
|
576
|
+
case 'pid-immediate':
|
|
577
|
+
return _killByPidImmediate(ownedTarget?.pid ?? ownedTarget);
|
|
578
|
+
case 'pids-immediate':
|
|
579
|
+
return _killByPidsImmediate(ownedTarget);
|
|
580
|
+
case 'trees-immediate':
|
|
581
|
+
return _killByPidTreesImmediate(ownedTarget);
|
|
582
|
+
case 'facade-cancel':
|
|
583
|
+
return ownedTarget.kill() !== false;
|
|
584
|
+
case 'self-signal':
|
|
585
|
+
try { process.kill(process.pid, opts.signal); return true; }
|
|
586
|
+
catch { return false; }
|
|
587
|
+
default:
|
|
588
|
+
throw new Error(`Unsupported process termination mode: ${policy.mode}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function killGracefully(proc, graceMs = 5000) {
|
|
593
|
+
return terminateProcess('compat.tree-graceful', proc, { graceMs });
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function killImmediate(proc) {
|
|
597
|
+
return terminateProcess('compat.tree-immediate', proc);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function killProcessOnly(proc) {
|
|
601
|
+
return terminateProcess('compat.process-only', proc);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function killByPidImmediate(pid) {
|
|
605
|
+
return terminateProcess('compat.pid-immediate', pid);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function killByPidsImmediate(pids) {
|
|
609
|
+
return terminateProcess('compat.pids-immediate', pids);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function killByPidTreesImmediate(pids) {
|
|
613
|
+
return terminateProcess('compat.trees-immediate', pids);
|
|
614
|
+
}
|
|
615
|
+
|
|
232
616
|
// ─── Process Table Enumeration ───────────────────────────────────────────────
|
|
233
617
|
// Cross-platform listing of every live process as { pid, ppid, name, cmd? }.
|
|
234
618
|
// `cmd` is best-effort — included on Windows (via wmic) and Linux (/proc); may
|
|
@@ -315,6 +699,56 @@ function listAllProcesses() {
|
|
|
315
699
|
return process.platform === 'win32' ? _winListProcesses() : _unixListProcesses();
|
|
316
700
|
}
|
|
317
701
|
|
|
702
|
+
// W-ms6dmv99005o26f4 — "the enumerator failed" must not read as "those processes
|
|
703
|
+
// are gone".
|
|
704
|
+
//
|
|
705
|
+
// listAllProcesses() collapses EVERY failure — a PowerShell/wmic/ps timeout under
|
|
706
|
+
// fleet load, a maxBuffer overflow, a spawn EAGAIN — into `[]`. Callers that hand
|
|
707
|
+
// that `[]` to the helpers below then get a confident "no such process" for every
|
|
708
|
+
// PID they ask about. engine/cli.js takes exactly one snapshot for the whole fleet
|
|
709
|
+
// at engine start, so a single transient enumeration failure disproved every live
|
|
710
|
+
// agent at once, flagged them all confirmed-dead, and produced the fleet-wide
|
|
711
|
+
// false-orphan storm of 2026-07-29T17:44Z (a dozen dispatches reaped within ~10s,
|
|
712
|
+
// retries consumed and worktrees GC'd — while the agent processes were still
|
|
713
|
+
// running, since nothing had actually killed them).
|
|
714
|
+
//
|
|
715
|
+
// The snapshot is self-validating: a live OS always contains at least the process
|
|
716
|
+
// doing the observing, so a snapshot that is empty, or that omits `selfPid`
|
|
717
|
+
// (truncated read, partially parsed CSV), is known-incomplete and carries no
|
|
718
|
+
// evidence of absence at all.
|
|
719
|
+
const PROCESS_SNAPSHOT_UNAVAILABLE = 'process-snapshot-unavailable';
|
|
720
|
+
|
|
721
|
+
function captureProcessSnapshot(opts = {}) {
|
|
722
|
+
const list = typeof opts.list === 'function' ? opts.list : listAllProcesses;
|
|
723
|
+
const selfPid = Number.isInteger(opts.selfPid) ? opts.selfPid : process.pid;
|
|
724
|
+
let processes = [];
|
|
725
|
+
let error = null;
|
|
726
|
+
try { processes = list(); } catch (err) { error = err; }
|
|
727
|
+
if (!Array.isArray(processes)) processes = [];
|
|
728
|
+
const usable = processes.length > 0 && processes.some(entry => Number(entry && entry.pid) === selfPid);
|
|
729
|
+
if (usable) return { ok: true, processes };
|
|
730
|
+
return {
|
|
731
|
+
ok: false,
|
|
732
|
+
processes: [],
|
|
733
|
+
reason: error ? `${PROCESS_SNAPSHOT_UNAVAILABLE}: ${error.message}` : PROCESS_SNAPSHOT_UNAVAILABLE,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Normalize an `opts.processes` value into a TRUSTED process array, or null when
|
|
738
|
+
// the caller supplied no usable snapshot. Returning null rather than [] is the
|
|
739
|
+
// whole point: [] asserts "these processes do not exist", null admits "unknown".
|
|
740
|
+
//
|
|
741
|
+
// A raw array keeps its historical trusted-input meaning so existing callers
|
|
742
|
+
// (engine/cleanup.js, engine/timeout.js, tests passing synthetic fleets) are
|
|
743
|
+
// unaffected; only a `captureProcessSnapshot()` result can report itself unusable.
|
|
744
|
+
function _resolveSnapshotProcesses(value) {
|
|
745
|
+
if (Array.isArray(value)) return value;
|
|
746
|
+
if (value && typeof value === 'object' && Array.isArray(value.processes)) {
|
|
747
|
+
return value.ok === false ? null : value.processes;
|
|
748
|
+
}
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
|
|
318
752
|
function _parsePowerShellProcesses(out) {
|
|
319
753
|
const parsed = JSON.parse(out);
|
|
320
754
|
const arr = Array.isArray(parsed) ? parsed : [parsed];
|
|
@@ -350,13 +784,41 @@ function _parseUnixProcessesWithStart(out) {
|
|
|
350
784
|
}).filter(Boolean);
|
|
351
785
|
}
|
|
352
786
|
|
|
353
|
-
|
|
787
|
+
// W-ms6dmv99005o26f4 — tri-state identity observation.
|
|
788
|
+
//
|
|
789
|
+
// getProcessIdentity() returns null for three very different situations: the PID
|
|
790
|
+
// is invalid, the process is genuinely absent, and "we could not observe it".
|
|
791
|
+
// Only the first two are evidence. Callers that act destructively on a null —
|
|
792
|
+
// engine/cli.js refuses reattach and marks the dispatch confirmed-dead, which
|
|
793
|
+
// bypasses both minAliveBeforeOrphanMs and the engine-restart grace window in
|
|
794
|
+
// engine/timeout.js — need the distinction.
|
|
795
|
+
function inspectProcessIdentity(pid, opts = {}) {
|
|
354
796
|
const value = Number(pid);
|
|
355
|
-
if (!Number.isInteger(value) || value <= 0)
|
|
356
|
-
|
|
797
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
798
|
+
return { identity: null, conclusive: true, reason: 'invalid-pid' };
|
|
799
|
+
}
|
|
800
|
+
let processes = _resolveSnapshotProcesses(opts.processes);
|
|
801
|
+
if (!processes) {
|
|
802
|
+
// No usable caller snapshot — re-observe live rather than concluding absence.
|
|
803
|
+
const live = captureProcessSnapshot(
|
|
804
|
+
typeof opts._list === 'function' ? { list: opts._list, selfPid: opts.selfPid } : {},
|
|
805
|
+
);
|
|
806
|
+
if (!live.ok) return { identity: null, conclusive: false, reason: live.reason };
|
|
807
|
+
processes = live.processes;
|
|
808
|
+
}
|
|
357
809
|
const match = processes.find(entry => Number(entry && entry.pid) === value);
|
|
358
|
-
|
|
359
|
-
|
|
810
|
+
if (!match) return { identity: null, conclusive: true, reason: 'absent-from-snapshot' };
|
|
811
|
+
const startedAt = Number(match.startedAt);
|
|
812
|
+
if (!(startedAt > 0)) {
|
|
813
|
+
// The process exists but the platform did not report a creation time (e.g. a
|
|
814
|
+
// `ps` variant without lstart). Absence of a timestamp is not a mismatch.
|
|
815
|
+
return { identity: null, conclusive: false, reason: 'start-time-unavailable' };
|
|
816
|
+
}
|
|
817
|
+
return { identity: { pid: value, startedAt }, conclusive: true, reason: 'snapshot' };
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function getProcessIdentity(pid, opts = {}) {
|
|
821
|
+
return inspectProcessIdentity(pid, opts).identity;
|
|
360
822
|
}
|
|
361
823
|
|
|
362
824
|
function isProcessIdentityCompatible(identity, recordedAtMs, skewMs = 1000) {
|
|
@@ -365,6 +827,27 @@ function isProcessIdentityCompatible(identity, recordedAtMs, skewMs = 1000) {
|
|
|
365
827
|
&& identity.startedAt <= recordedAtMs + skewMs;
|
|
366
828
|
}
|
|
367
829
|
|
|
830
|
+
// The fail-closed inverse of isProcessIdentityCompatible is NOT proof of a
|
|
831
|
+
// foreign process: that gate also answers false when the identity could not be
|
|
832
|
+
// resolved or the record carries no usable start time (absence of evidence).
|
|
833
|
+
// This helper answers the narrower question — did we resolve a real creation
|
|
834
|
+
// time that is provably LATER than the child we recorded, i.e. is this PID
|
|
835
|
+
// demonstrably somebody else's? Only that is safe to act on destructively
|
|
836
|
+
// (W-ms8d10y3003t0588).
|
|
837
|
+
function isProcessIdentityConclusiveMismatch(identity, recordedAtMs, skewMs = 1000) {
|
|
838
|
+
const observedAt = Number(identity && identity.startedAt);
|
|
839
|
+
return Number.isFinite(observedAt)
|
|
840
|
+
&& observedAt > 0
|
|
841
|
+
&& Number.isFinite(recordedAtMs)
|
|
842
|
+
// A non-positive recorded time is not a real start time — callers coerce a
|
|
843
|
+
// missing `started_at` through Number(), and Number(null) / Number('') are
|
|
844
|
+
// both 0. Epoch 0 would make EVERY live process look provably later than
|
|
845
|
+
// ours, which is the exact absence-of-evidence collapse this helper exists
|
|
846
|
+
// to prevent.
|
|
847
|
+
&& recordedAtMs > 0
|
|
848
|
+
&& observedAt > recordedAtMs + skewMs;
|
|
849
|
+
}
|
|
850
|
+
|
|
368
851
|
function isSameProcessIdentity(left, right) {
|
|
369
852
|
return !!left
|
|
370
853
|
&& !!right
|
|
@@ -465,9 +948,9 @@ function findProcessesWithCwdInside(dir, opts = {}) {
|
|
|
465
948
|
// Lazy require avoids a load-time circular-require deadlock: shared.js
|
|
466
949
|
// requires this module at top-level, so shared's own module.exports isn't
|
|
467
950
|
// populated yet at that point — but by the time this function actually
|
|
468
|
-
// RUNS, shared.js has finished loading and require('
|
|
951
|
+
// RUNS, shared.js has finished loading and require('../core/shared') resolves
|
|
469
952
|
// to its full, cached exports object.
|
|
470
|
-
const { ENGINE_DEFAULTS } = require('
|
|
953
|
+
const { ENGINE_DEFAULTS } = require('../core/shared');
|
|
471
954
|
const timeoutMs = Number(opts.timeoutMs) > 0
|
|
472
955
|
? Number(opts.timeoutMs)
|
|
473
956
|
: (ENGINE_DEFAULTS.orphanHolderScanTimeoutMs || 5000);
|
|
@@ -747,7 +1230,7 @@ function findProcessCwdHolders(worktreePath, opts = {}) {
|
|
|
747
1230
|
try { resolved = path.resolve(worktreePath); } catch { return []; }
|
|
748
1231
|
if (!resolved) return [];
|
|
749
1232
|
// Lazy require — see comment in findProcessesWithCwdInside above.
|
|
750
|
-
const { ENGINE_DEFAULTS, MINIONS_DIR } = require('
|
|
1233
|
+
const { ENGINE_DEFAULTS, MINIONS_DIR } = require('../core/shared');
|
|
751
1234
|
const timeoutMs = Number(opts.timeoutMs) > 0
|
|
752
1235
|
? Number(opts.timeoutMs)
|
|
753
1236
|
: (ENGINE_DEFAULTS.statusProbeKillTimeoutMs || 12000);
|
|
@@ -779,8 +1262,8 @@ function findProcessCwdHolders(worktreePath, opts = {}) {
|
|
|
779
1262
|
// Cross-check a single PID's command line for a Minions agent invocation,
|
|
780
1263
|
// including the shared spawn wrapper and every bundled runtime CLI. Used by
|
|
781
1264
|
// orphan/recycled-PID safety:
|
|
782
|
-
// - engine/cleanup.js: gate before killing a PID found in engine/tmp/pid-*.pid
|
|
783
|
-
// - engine/timeout.js: gate before parking a dispatch as still-alive when
|
|
1265
|
+
// - engine/orchestration/cleanup.js: gate before killing a PID found in engine/tmp/pid-*.pid
|
|
1266
|
+
// - engine/orchestration/timeout.js: gate before parking a dispatch as still-alive when
|
|
784
1267
|
// the OS PID is alive but may belong to an unrelated recycled-PID process
|
|
785
1268
|
//
|
|
786
1269
|
// Windows: PowerShell Get-CimInstance for the full CommandLine of one PID.
|
|
@@ -802,31 +1285,69 @@ function _commandLineMatchesAgent(cmdline) {
|
|
|
802
1285
|
return AGENT_COMMAND_PATTERNS.some(pattern => pattern.test(value));
|
|
803
1286
|
}
|
|
804
1287
|
|
|
805
|
-
|
|
1288
|
+
// W-ms6dmv99005o26f4 — tri-state command-line observation.
|
|
1289
|
+
//
|
|
1290
|
+
// `conclusive` answers "did we actually observe anything?", separately from
|
|
1291
|
+
// `match`. The old boolean form short-circuited on `Array.isArray(opts.processes)`
|
|
1292
|
+
// and returned false whenever the PID was missing from the array — including when
|
|
1293
|
+
// that array was the `[]` produced by a FAILED enumeration, so the per-PID
|
|
1294
|
+
// fallback probe below never ran and a live agent was reported as not-an-agent.
|
|
1295
|
+
function inspectProcessCommandLineForAgent(pid, opts = {}) {
|
|
806
1296
|
const n = Number(pid);
|
|
807
|
-
if (!Number.isInteger(n) || n <= 0) return false;
|
|
1297
|
+
if (!Number.isInteger(n) || n <= 0) return { match: false, conclusive: true, reason: 'invalid-pid' };
|
|
1298
|
+
const snapshot = _resolveSnapshotProcesses(opts.processes);
|
|
1299
|
+
if (snapshot) {
|
|
1300
|
+
const match = snapshot.find(entry => Number(entry?.pid) === n);
|
|
1301
|
+
if (!match) return { match: false, conclusive: true, reason: 'absent-from-snapshot' };
|
|
1302
|
+
if (match.cmd) {
|
|
1303
|
+
return { match: _commandLineMatchesAgent(match.cmd), conclusive: true, reason: 'snapshot-cmdline' };
|
|
1304
|
+
}
|
|
1305
|
+
// Snapshot has the process but no command line (wmic/ps enumerations omit it)
|
|
1306
|
+
// — fall through to the per-PID probe.
|
|
1307
|
+
}
|
|
808
1308
|
let cmdline = '';
|
|
809
1309
|
try {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1310
|
+
cmdline = typeof opts._probe === 'function'
|
|
1311
|
+
? String(opts._probe(n) || '').trim()
|
|
1312
|
+
: _readProcessCommandLine(n);
|
|
1313
|
+
} catch { return { match: false, conclusive: false, reason: 'probe-failed' }; }
|
|
1314
|
+
if (!cmdline) return { match: false, conclusive: false, reason: 'probe-empty' };
|
|
1315
|
+
return { match: _commandLineMatchesAgent(cmdline), conclusive: true, reason: 'live-cmdline' };
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// Read one PID's full command line, or throw when it cannot be observed.
|
|
1319
|
+
// Windows: PowerShell Get-CimInstance. Linux: /proc/<pid>/cmdline (NUL-separated).
|
|
1320
|
+
// macOS / when /proc isn't available: fallback `ps -p <pid> -o command=`.
|
|
1321
|
+
function _readProcessCommandLine(n) {
|
|
1322
|
+
if (process.platform === 'win32') {
|
|
1323
|
+
const out = _execSync(
|
|
1324
|
+
`powershell -NoProfile -NonInteractive -Command "(Get-CimInstance Win32_Process -Filter 'ProcessId=${n}').CommandLine"`,
|
|
1325
|
+
{ stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000, windowsHide: true, encoding: 'utf8' }
|
|
1326
|
+
);
|
|
1327
|
+
return String(out || '').trim();
|
|
1328
|
+
}
|
|
1329
|
+
try {
|
|
1330
|
+
const buf = fs.readFileSync(`/proc/${n}/cmdline`);
|
|
1331
|
+
return buf.toString('utf8').replace(/\0/g, ' ').trim();
|
|
1332
|
+
} catch {
|
|
1333
|
+
return String(_execSync(`ps -p ${n} -o command=`,
|
|
1334
|
+
{ stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000, encoding: 'utf8' }) || '').trim();
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// Boolean facade over inspectProcessCommandLineForAgent. Returns false when the
|
|
1339
|
+
// PID is invalid, the process doesn't exist, the command line can't be read, or
|
|
1340
|
+
// the cmdline contains no recognized agent marker. False stays the safe default
|
|
1341
|
+
// for both historical call sites: cleanup falls through to "skip kill" and
|
|
1342
|
+
// timeout falls through to "treat PID as dead".
|
|
1343
|
+
function isProcessCommandLineMatchingAgent(pid, opts = {}) {
|
|
1344
|
+
return inspectProcessCommandLineForAgent(pid, opts).match;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function isRecordedAgentProcess(pid, recordedAtMs, opts = {}) {
|
|
1348
|
+
const identity = getProcessIdentity(pid, opts);
|
|
1349
|
+
return isProcessIdentityCompatible(identity, recordedAtMs, opts.skewMs)
|
|
1350
|
+
&& isProcessCommandLineMatchingAgent(pid, opts);
|
|
830
1351
|
}
|
|
831
1352
|
|
|
832
1353
|
function _buildChildMap(processes) {
|
|
@@ -838,6 +1359,14 @@ function _buildChildMap(processes) {
|
|
|
838
1359
|
return childMap;
|
|
839
1360
|
}
|
|
840
1361
|
|
|
1362
|
+
function _isChronologicalChild(parentPid, childPid, byPid) {
|
|
1363
|
+
const parentStartedAt = Number(byPid.get(parentPid)?.startedAt) || 0;
|
|
1364
|
+
const childStartedAt = Number(byPid.get(childPid)?.startedAt) || 0;
|
|
1365
|
+
// Detached children retain their original PPID after the parent exits. If
|
|
1366
|
+
// that PID is later reused, the older process is not a child of the new one.
|
|
1367
|
+
return parentStartedAt <= 0 || childStartedAt <= 0 || childStartedAt >= parentStartedAt;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
841
1370
|
// BFS descendants of rootPid given a process snapshot. `allProcesses` is
|
|
842
1371
|
// injectable for tests and for amortizing one snapshot across multiple
|
|
843
1372
|
// `listProcessDescendants` calls in the same tick.
|
|
@@ -846,6 +1375,7 @@ function listProcessDescendants(rootPid, allProcesses = null) {
|
|
|
846
1375
|
if (!Number.isInteger(root) || root <= 0) return [];
|
|
847
1376
|
const procs = Array.isArray(allProcesses) ? allProcesses : listAllProcesses();
|
|
848
1377
|
const childMap = _buildChildMap(procs);
|
|
1378
|
+
const byPid = new Map(procs.map(entry => [Number(entry.pid), entry]));
|
|
849
1379
|
const result = [];
|
|
850
1380
|
const seen = new Set([root]);
|
|
851
1381
|
const queue = [root];
|
|
@@ -853,7 +1383,7 @@ function listProcessDescendants(rootPid, allProcesses = null) {
|
|
|
853
1383
|
const cur = queue.shift();
|
|
854
1384
|
const children = childMap.get(cur) || [];
|
|
855
1385
|
for (const c of children) {
|
|
856
|
-
if (seen.has(c)) continue;
|
|
1386
|
+
if (seen.has(c) || !_isChronologicalChild(cur, c, byPid)) continue;
|
|
857
1387
|
seen.add(c);
|
|
858
1388
|
result.push(c);
|
|
859
1389
|
queue.push(c);
|
|
@@ -873,6 +1403,7 @@ function listProcessReachable(rootPids, allProcesses = null) {
|
|
|
873
1403
|
if (!roots.length) return new Set();
|
|
874
1404
|
const procs = Array.isArray(allProcesses) ? allProcesses : listAllProcesses();
|
|
875
1405
|
const childMap = _buildChildMap(procs);
|
|
1406
|
+
const byPid = new Map(procs.map(entry => [Number(entry.pid), entry]));
|
|
876
1407
|
const seen = new Set();
|
|
877
1408
|
const queue = [];
|
|
878
1409
|
for (const r of roots) {
|
|
@@ -882,7 +1413,7 @@ function listProcessReachable(rootPids, allProcesses = null) {
|
|
|
882
1413
|
const cur = queue.shift();
|
|
883
1414
|
const children = childMap.get(cur) || [];
|
|
884
1415
|
for (const c of children) {
|
|
885
|
-
if (seen.has(c)) continue;
|
|
1416
|
+
if (seen.has(c) || !_isChronologicalChild(cur, c, byPid)) continue;
|
|
886
1417
|
seen.add(c);
|
|
887
1418
|
queue.push(c);
|
|
888
1419
|
}
|
|
@@ -891,11 +1422,17 @@ function listProcessReachable(rootPids, allProcesses = null) {
|
|
|
891
1422
|
}
|
|
892
1423
|
|
|
893
1424
|
module.exports = {
|
|
1425
|
+
PROCESS_TERMINATION_SITES,
|
|
1426
|
+
terminateProcess,
|
|
894
1427
|
normalizeKillPid,
|
|
895
1428
|
unixChildPids,
|
|
896
1429
|
killUnixProcessTree,
|
|
897
1430
|
killGracefully,
|
|
898
1431
|
killImmediate,
|
|
1432
|
+
killProcessOnly,
|
|
1433
|
+
commandLineRunsScript,
|
|
1434
|
+
listNodePidsMatchingScript,
|
|
1435
|
+
listNodePidsMatchingScripts,
|
|
899
1436
|
killByPidImmediate,
|
|
900
1437
|
killByPidsImmediate,
|
|
901
1438
|
killByPidTreesImmediate,
|
|
@@ -907,10 +1444,15 @@ module.exports = {
|
|
|
907
1444
|
_unixListProcesses,
|
|
908
1445
|
_parseUnixProcessesWithStart, // exported for testing
|
|
909
1446
|
listAllProcesses,
|
|
1447
|
+
captureProcessSnapshot,
|
|
1448
|
+
PROCESS_SNAPSHOT_UNAVAILABLE,
|
|
910
1449
|
listAllProcessesAsync,
|
|
911
1450
|
getProcessIdentity,
|
|
1451
|
+
inspectProcessIdentity,
|
|
912
1452
|
isProcessIdentityCompatible,
|
|
1453
|
+
isProcessIdentityConclusiveMismatch,
|
|
913
1454
|
isSameProcessIdentity,
|
|
1455
|
+
isRecordedAgentProcess,
|
|
914
1456
|
findProcessesWithCwdInside,
|
|
915
1457
|
findProcessesWithCwdInsideAsync,
|
|
916
1458
|
_findWindowsProcessesWithCwdInside,
|
|
@@ -920,9 +1462,11 @@ module.exports = {
|
|
|
920
1462
|
_parseCwdHolderLines,
|
|
921
1463
|
findProcessCwdHolders,
|
|
922
1464
|
isProcessCommandLineMatchingAgent,
|
|
1465
|
+
inspectProcessCommandLineForAgent,
|
|
923
1466
|
_commandLineMatchesAgent, // exported for testing
|
|
924
1467
|
_buildChildMap,
|
|
925
1468
|
listProcessDescendants,
|
|
926
1469
|
listProcessReachable,
|
|
927
1470
|
isPidAlive,
|
|
1471
|
+
inspectPidLiveness,
|
|
928
1472
|
};
|