pi-crew 0.11.38 → 0.11.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/dist/index.mjs +522 -518
- package/package.json +2 -2
- package/skills/real-test-pi-crew/SKILL.md +9 -1
- package/src/config/env-allowlist-registry.ts +14 -1
- package/src/config/env-vars.ts +12 -0
- package/src/extension/registration/lifecycle-handlers.ts +12 -0
- package/src/extension/team-tool/doctor.ts +125 -1
- package/src/runtime/child-pi/child-pi-kill.ts +68 -2
- package/src/runtime/child-pi/child-pi-spawn.ts +4 -0
- package/src/runtime/heartbeat/worker-heartbeat.ts +4 -0
- package/src/runtime/process/orphan-reaper.ts +197 -22
- package/src/runtime/process/proc-stat.ts +8 -0
- package/src/runtime/process/reap-group.ts +499 -0
- package/src/runtime/process/run-descendant-sweep.ts +744 -0
- package/src/runtime/recovery/crash-recovery.ts +202 -0
- package/src/runtime/rpc/frame-client.ts +21 -1
- package/src/runtime/stale-reconciler.ts +343 -2
- package/src/runtime/task-runner/child-executor.ts +12 -1
- package/src/runtime/task-runner/state-helpers.ts +28 -1
- package/src/state/stores/state-store.ts +8 -0
- package/src/state/types.ts +42 -0
|
@@ -10,21 +10,52 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Safety model (mirrors zombie-scanner.ts): this module NEVER kills on a bare
|
|
12
12
|
* pid-liveness guess. A pid is reaped only when it is (a) recorded as the
|
|
13
|
-
* worker of a run whose reconcile verdict was already "stale", AND (b)
|
|
14
|
-
* by the AUTHORITATIVE zombie signal — `PI_CREW_KIND=subagent` with a
|
|
15
|
-
* `PI_CREW_PARENT_PID`
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* worker of a run whose reconcile verdict was already "stale", AND (b) either
|
|
14
|
+
* confirmed by the AUTHORITATIVE zombie signal — `PI_CREW_KIND=subagent` with a
|
|
15
|
+
* DEAD `PI_CREW_PARENT_PID` — or IDENTITY-PROVEN (wave 11a: the recorded
|
|
16
|
+
* starttime matches the live /proc value, so we know it is the exact process we
|
|
17
|
+
* spawned, PID reuse excluded). A worker whose parent is still alive and whose
|
|
18
|
+
* identity we cannot prove is never a candidate, so a healthy in-flight worker
|
|
19
|
+
* cannot be killed by accident. All IO is injectable for unit testing (no real
|
|
20
|
+
* kills in tests).
|
|
21
|
+
*
|
|
22
|
+
* Wave 11a (F2) changes, in one place each:
|
|
23
|
+
* - evidence carries the persisted pgid/starttime/uid (GAP-3 fix feeds it),
|
|
24
|
+
* - a known pgid routes through reap-group.ts (guarded ladder, SIGTERM →
|
|
25
|
+
* SIGKILL escalation) instead of a single fire-and-forget pid SIGTERM,
|
|
26
|
+
* - the `scanZombieSubagents()` gate is FAIL-OPEN: a scan throw no longer
|
|
27
|
+
* disables reaping wholesale — identity-proven candidates still reap,
|
|
28
|
+
* - `PI_CREW_REAP_GROUP=0` degrades every path to the pre-wave pid-only kill.
|
|
29
|
+
*
|
|
30
|
+
* Wave 11a review fixes (round 1):
|
|
31
|
+
* - identity proof now ALSO requires leader-proof (`evidence.pgid ===
|
|
32
|
+
* evidence.pid`) — a non-leader pgid can be the HOST's group when the child
|
|
33
|
+
* was spawned with `useGroup=false` (MAJOR #2).
|
|
34
|
+
* - single accounting: a pid lands in `reaped` XOR `skipped` (MINOR #6).
|
|
35
|
+
* - an escalation promise is never dropped, and `escalate:false` wraps it with
|
|
36
|
+
* a settle-only catch so Node 22 cannot see an unhandled rejection (MINOR #5).
|
|
18
37
|
*/
|
|
19
38
|
|
|
20
39
|
import type { TeamTaskState } from "../../state/types.ts";
|
|
40
|
+
import { logInternalError } from "../../utils/internal-error.ts";
|
|
21
41
|
import { checkProcessLiveness } from "../process-status.ts";
|
|
42
|
+
import { type ProcIdentity, readProcIdentity, reapGroup, resolveReapGroupEnabled } from "./reap-group.ts";
|
|
22
43
|
import { scanZombieSubagents } from "./zombie-scanner.ts";
|
|
23
44
|
|
|
24
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* A recorded worker process for a run task (from heartbeat.pid /
|
|
47
|
+
* checkpoint.childPid) plus, when the record carries it, the spawn-time
|
|
48
|
+
* identity that makes the pid reuse-proof.
|
|
49
|
+
*/
|
|
25
50
|
export interface WorkerPidEvidence {
|
|
26
51
|
taskId: string;
|
|
27
52
|
pid: number;
|
|
53
|
+
/** Process-group id recorded at spawn (heartbeat.pgid / checkpoint.childPgid). */
|
|
54
|
+
pgid?: number;
|
|
55
|
+
/** `/proc/<pid>/stat` field 22 at spawn — survives PID reuse. */
|
|
56
|
+
starttime?: string;
|
|
57
|
+
/** uid at spawn — a group we may not signal is refused. */
|
|
58
|
+
uid?: number;
|
|
28
59
|
}
|
|
29
60
|
|
|
30
61
|
export interface ReapOrphanOptions {
|
|
@@ -32,6 +63,14 @@ export interface ReapOrphanOptions {
|
|
|
32
63
|
isAlive?: (pid: number) => boolean;
|
|
33
64
|
/** Signal sender (default: process.kill). Injected in tests. */
|
|
34
65
|
kill?: (pid: number, signal: NodeJS.Signals) => void;
|
|
66
|
+
/** Group signal sender for the reap-group path (default: process.kill(-pgid, sig)). */
|
|
67
|
+
killGroup?: (pgid: number, signal: NodeJS.Signals) => void;
|
|
68
|
+
/**
|
|
69
|
+
* Group liveness probe for the reap-group path (default: `process.kill(-pgid, 0)`).
|
|
70
|
+
* Test seam: without it a unit test would have to pick a pgid that must not
|
|
71
|
+
* exist in the host's process table before it could observe the refusal path.
|
|
72
|
+
*/
|
|
73
|
+
groupAlive?: (pgid: number) => boolean;
|
|
35
74
|
/** Signal to send first (default SIGTERM — graceful). */
|
|
36
75
|
signal?: NodeJS.Signals;
|
|
37
76
|
/** Pids that must never be touched (self/ancestors). */
|
|
@@ -40,6 +79,27 @@ export interface ReapOrphanOptions {
|
|
|
40
79
|
allowedPids?: ReadonlySet<number>;
|
|
41
80
|
/** Report candidates without killing (used by doctor/dry-run). */
|
|
42
81
|
dryRun?: boolean;
|
|
82
|
+
/** Grace window between SIGTERM and SIGKILL on the group path (ms). */
|
|
83
|
+
gracefulMs?: number;
|
|
84
|
+
/** Hard cap after SIGKILL on the group path (ms). */
|
|
85
|
+
capMs?: number;
|
|
86
|
+
/** Collect the async escalation promises in `pending` (default true). */
|
|
87
|
+
escalate?: boolean;
|
|
88
|
+
/** Let identity-proven pids bypass the `allowedPids` gate (default true). */
|
|
89
|
+
allowIdentityProven?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* When set, a pid that is NOT identity-proven is skipped even with no
|
|
92
|
+
* `allowedPids` gate at all. Used by the fail-open branch below: a scan that
|
|
93
|
+
* throws must not degrade into "SIGTERM every recorded pid" (a recycled pid
|
|
94
|
+
* would then be a stranger's process) — only identity-proven candidates pass.
|
|
95
|
+
*/
|
|
96
|
+
requireIdentityProof?: boolean;
|
|
97
|
+
/** Platform override for the reap-group guard ladder (tests). */
|
|
98
|
+
platform?: NodeJS.Platform;
|
|
99
|
+
/** /proc identity reader override (tests). */
|
|
100
|
+
readIdentity?: (pid: number) => ProcIdentity | undefined;
|
|
101
|
+
/** Zombie-scan override (tests force the fail-open path). */
|
|
102
|
+
scanZombies?: () => { zombies: { pid: number }[] };
|
|
43
103
|
}
|
|
44
104
|
|
|
45
105
|
export interface ReapOrphanResult {
|
|
@@ -47,23 +107,43 @@ export interface ReapOrphanResult {
|
|
|
47
107
|
skipped: { pid: number; reason: string }[];
|
|
48
108
|
/** Every distinct pid considered. */
|
|
49
109
|
candidates: number[];
|
|
110
|
+
/**
|
|
111
|
+
* In-flight group escalations (SIGKILL ladder). The caller that owns the
|
|
112
|
+
* process lifetime must await these — `stale-reconciler.ts` does — so the
|
|
113
|
+
* host does not exit before SIGKILL has been delivered.
|
|
114
|
+
*/
|
|
115
|
+
pending: Promise<unknown>[];
|
|
50
116
|
}
|
|
51
117
|
|
|
52
118
|
/**
|
|
53
119
|
* Collect the recorded worker pids of a run's in-flight tasks. A `running` or
|
|
54
120
|
* `waiting` task that recorded its child pid (heartbeat or checkpoint) is the
|
|
55
|
-
* evidence we reap against. Deduped by pid.
|
|
121
|
+
* evidence we reap against. Deduped by pid. Optional identity fields are copied
|
|
122
|
+
* verbatim — a legacy record simply yields the pre-wave shape.
|
|
56
123
|
*/
|
|
57
124
|
export function collectWorkerPidEvidence(tasks: TeamTaskState[]): WorkerPidEvidence[] {
|
|
58
125
|
const seen = new Set<number>();
|
|
59
126
|
const evidence: WorkerPidEvidence[] = [];
|
|
60
127
|
for (const task of tasks) {
|
|
61
128
|
if (task.status !== "running" && task.status !== "waiting") continue;
|
|
62
|
-
const
|
|
129
|
+
const heartbeat = task.heartbeat;
|
|
130
|
+
const checkpoint = task.checkpoint;
|
|
131
|
+
const pid = heartbeat?.pid ?? checkpoint?.childPid;
|
|
63
132
|
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 1) continue;
|
|
64
133
|
if (seen.has(pid)) continue;
|
|
65
134
|
seen.add(pid);
|
|
66
|
-
|
|
135
|
+
// Wave 11a (F3): the persisted identity, spread only when present so a
|
|
136
|
+
// legacy record keeps its exact pre-wave shape.
|
|
137
|
+
const pgid = heartbeat?.pgid ?? checkpoint?.childPgid;
|
|
138
|
+
const starttime = heartbeat?.starttime ?? checkpoint?.childStartTime;
|
|
139
|
+
const uid = heartbeat?.uid ?? checkpoint?.childUid;
|
|
140
|
+
evidence.push({
|
|
141
|
+
taskId: task.id,
|
|
142
|
+
pid,
|
|
143
|
+
...(pgid !== undefined ? { pgid } : {}),
|
|
144
|
+
...(starttime !== undefined ? { starttime } : {}),
|
|
145
|
+
...(uid !== undefined ? { uid } : {}),
|
|
146
|
+
});
|
|
67
147
|
}
|
|
68
148
|
return evidence;
|
|
69
149
|
}
|
|
@@ -72,21 +152,29 @@ export function collectWorkerPidEvidence(tasks: TeamTaskState[]): WorkerPidEvide
|
|
|
72
152
|
* Reap orphaned worker processes. Conservative by construction:
|
|
73
153
|
* * invalid / pid<=1 → skipped,
|
|
74
154
|
* * protected (self/ancestors) → skipped,
|
|
75
|
-
* *
|
|
76
|
-
*
|
|
77
|
-
* *
|
|
155
|
+
* * `allowedPids` set → any pid outside it is skipped UNLESS identity-proven
|
|
156
|
+
* (wave 11a fail-open: a matching starttime proves it is our process),
|
|
157
|
+
* * `dryRun` → skipped,
|
|
158
|
+
* * known pgid + kill switch on → guarded group reap (SIGTERM → escalation),
|
|
159
|
+
* falling back to the pid-only SIGTERM when the guard ladder refuses,
|
|
160
|
+
* * otherwise the pre-wave pid-only SIGTERM.
|
|
78
161
|
*/
|
|
79
162
|
export function reapOrphanWorkers(evidences: WorkerPidEvidence[], opts: ReapOrphanOptions = {}): ReapOrphanResult {
|
|
163
|
+
const readIdentity = opts.readIdentity ?? readProcIdentity;
|
|
80
164
|
const isAlive = opts.isAlive ?? ((pid: number) => checkProcessLiveness(pid).alive);
|
|
81
165
|
const kill = opts.kill ?? ((pid: number, signal: NodeJS.Signals) => process.kill(pid, signal));
|
|
82
166
|
const protectedPids = opts.protectedPids ?? new Set<number>([process.pid, process.ppid]);
|
|
83
167
|
const signal = opts.signal ?? "SIGTERM";
|
|
168
|
+
const escalate = opts.escalate ?? true;
|
|
169
|
+
const allowIdentityProven = opts.allowIdentityProven ?? true;
|
|
84
170
|
const reaped: number[] = [];
|
|
85
171
|
const skipped: { pid: number; reason: string }[] = [];
|
|
86
172
|
const candidates: number[] = [];
|
|
173
|
+
const pending: Promise<unknown>[] = [];
|
|
87
174
|
const seen = new Set<number>();
|
|
88
175
|
|
|
89
|
-
for (const
|
|
176
|
+
for (const evidence of evidences) {
|
|
177
|
+
const { pid } = evidence;
|
|
90
178
|
if (seen.has(pid)) continue;
|
|
91
179
|
seen.add(pid);
|
|
92
180
|
candidates.push(pid);
|
|
@@ -98,18 +186,94 @@ export function reapOrphanWorkers(evidences: WorkerPidEvidence[], opts: ReapOrph
|
|
|
98
186
|
skipped.push({ pid, reason: "protected-self-or-ancestor" });
|
|
99
187
|
continue;
|
|
100
188
|
}
|
|
101
|
-
|
|
189
|
+
const alive = isAlive(pid);
|
|
190
|
+
// Wave 11a review (MAJOR #2): a recorded starttime proves this is the very
|
|
191
|
+
// incarnation we spawned ONLY when it also proves LEADERSHIP
|
|
192
|
+
// (`evidence.pgid === evidence.pid`). A non-leader pgid cannot be reasoned
|
|
193
|
+
// about from a snapshot: it may be the group of the host that spawned the
|
|
194
|
+
// worker (rpc frames are `detached` but reuse the parent group when
|
|
195
|
+
// useGroup=false). Those candidates are refused here and are the target of
|
|
196
|
+
// the marker-based sweep in wave 11b. An already-dead pid needs no liveness
|
|
197
|
+
// cross-check (nothing can be reused), a live one must match exactly.
|
|
198
|
+
const liveIdentity = alive ? readIdentity(pid) : undefined;
|
|
199
|
+
const leaderProven = evidence.pgid !== undefined && evidence.pgid === pid;
|
|
200
|
+
const identityProven =
|
|
201
|
+
allowIdentityProven &&
|
|
202
|
+
leaderProven &&
|
|
203
|
+
evidence.starttime !== undefined &&
|
|
204
|
+
(alive ? liveIdentity?.starttime !== undefined && liveIdentity.starttime === evidence.starttime : true);
|
|
205
|
+
if (opts.allowedPids && !opts.allowedPids.has(pid) && !identityProven) {
|
|
102
206
|
skipped.push({ pid, reason: "not-an-authoritative-orphan" });
|
|
103
207
|
continue;
|
|
104
208
|
}
|
|
105
|
-
if (!
|
|
106
|
-
skipped.push({ pid, reason: "
|
|
209
|
+
if (opts.requireIdentityProof && !identityProven) {
|
|
210
|
+
skipped.push({ pid, reason: "not-an-authoritative-orphan" });
|
|
107
211
|
continue;
|
|
108
212
|
}
|
|
109
213
|
if (opts.dryRun) {
|
|
110
214
|
skipped.push({ pid, reason: "dry-run" });
|
|
111
215
|
continue;
|
|
112
216
|
}
|
|
217
|
+
const pgid = evidence.pgid ?? liveIdentity?.pgid;
|
|
218
|
+
if (pgid !== undefined && Number.isInteger(pgid) && pgid > 1) {
|
|
219
|
+
if (!resolveReapGroupEnabled()) {
|
|
220
|
+
// Kill switch on ⇒ degrade to the pre-wave pid-only path (the pid is
|
|
221
|
+
// still SIGTERMed below). Not recorded as skipped: a pid only ever
|
|
222
|
+
// appears in ONE of the two result arrays (MINOR #6 — double
|
|
223
|
+
// accounting made `reaped` and `skipped` overlap).
|
|
224
|
+
logInternalError(
|
|
225
|
+
"orphan-reaper.group-disabled",
|
|
226
|
+
new Error(`PI_CREW_REAP_GROUP=0: pgid ${pgid} of pid=${pid} left untouched, pid-only kill path`),
|
|
227
|
+
`task=${evidence.taskId}`,
|
|
228
|
+
"debug",
|
|
229
|
+
);
|
|
230
|
+
} else {
|
|
231
|
+
const expected: ProcIdentity | undefined =
|
|
232
|
+
evidence.starttime !== undefined ? { pid, pgid, starttime: evidence.starttime, uid: evidence.uid } : undefined;
|
|
233
|
+
const groupResult = reapGroup(pgid, pid, {
|
|
234
|
+
...(opts.gracefulMs !== undefined ? { gracefulMs: opts.gracefulMs } : {}),
|
|
235
|
+
...(opts.capMs !== undefined ? { capMs: opts.capMs } : {}),
|
|
236
|
+
...(opts.platform !== undefined ? { platform: opts.platform } : {}),
|
|
237
|
+
readIdentity,
|
|
238
|
+
...(opts.killGroup !== undefined ? { signalGroup: opts.killGroup } : {}),
|
|
239
|
+
...(opts.groupAlive !== undefined ? { groupAlive: opts.groupAlive } : {}),
|
|
240
|
+
...(expected !== undefined ? { expected } : {}),
|
|
241
|
+
name: `orphan-reaper:${pid}`,
|
|
242
|
+
});
|
|
243
|
+
if (groupResult.refusal === undefined) {
|
|
244
|
+
// MINOR #5: never DROP an escalation promise. `escalate:false` callers
|
|
245
|
+
// (doctor/dry-run) still get it collected, with a settle-only catch so
|
|
246
|
+
// an unexpected rejection cannot become a fatal unhandled rejection.
|
|
247
|
+
pending.push(
|
|
248
|
+
escalate
|
|
249
|
+
? groupResult.pending
|
|
250
|
+
: groupResult.pending.catch(() => {
|
|
251
|
+
/* settle-only: a dry-run/doctor caller has no verdict to act on */
|
|
252
|
+
}),
|
|
253
|
+
);
|
|
254
|
+
reaped.push(pid);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (!alive) {
|
|
258
|
+
// Refused group + dead pid ⇒ nothing left to signal: this pid is
|
|
259
|
+
// SKIPPED, and only skipped (never also `reaped`).
|
|
260
|
+
skipped.push({ pid, reason: `group-refused:${groupResult.refusal}` });
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
// Refused group + live pid ⇒ the pid-only fallback below still runs and
|
|
264
|
+
// will decide the single accounting slot.
|
|
265
|
+
logInternalError(
|
|
266
|
+
"orphan-reaper.group-refused",
|
|
267
|
+
new Error(`group reap refused for pid=${pid} pgid=${pgid} (${groupResult.refusal}); pid-only fallback`),
|
|
268
|
+
`task=${evidence.taskId}`,
|
|
269
|
+
"debug",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (!alive) {
|
|
274
|
+
skipped.push({ pid, reason: "already-dead" });
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
113
277
|
try {
|
|
114
278
|
kill(pid, signal);
|
|
115
279
|
reaped.push(pid);
|
|
@@ -117,7 +281,7 @@ export function reapOrphanWorkers(evidences: WorkerPidEvidence[], opts: ReapOrph
|
|
|
117
281
|
skipped.push({ pid, reason: error instanceof Error ? error.message : String(error) });
|
|
118
282
|
}
|
|
119
283
|
}
|
|
120
|
-
return { reaped, skipped, candidates };
|
|
284
|
+
return { reaped, skipped, candidates, pending };
|
|
121
285
|
}
|
|
122
286
|
|
|
123
287
|
/**
|
|
@@ -129,15 +293,26 @@ export function reapOrphanWorkers(evidences: WorkerPidEvidence[], opts: ReapOrph
|
|
|
129
293
|
* yields exactly the unreapable orphans, with no false-positive kill risk for a
|
|
130
294
|
* live worker. Linux-only (the scanner needs /proc); on other platforms the
|
|
131
295
|
* scan yields no zombies and this is a no-op. Best-effort: never throws.
|
|
296
|
+
*
|
|
297
|
+
* Wave 11a (F2) FAIL-OPEN: a scanner throw used to disable the whole pass
|
|
298
|
+
* ("zombie-scan-unavailable"). It now only removes the `allowedPids` gate and
|
|
299
|
+
* flips on `requireIdentityProof`, so IDENTITY-PROVEN candidates (recorded
|
|
300
|
+
* starttime === live starttime) are still reaped while unproven ones stay
|
|
301
|
+
* untouched (no blanket "SIGTERM every recorded pid" degrade).
|
|
132
302
|
*/
|
|
133
303
|
export function reapRecordedWorkerOrphans(tasks: TeamTaskState[], opts: ReapOrphanOptions = {}): ReapOrphanResult {
|
|
134
304
|
const evidence = collectWorkerPidEvidence(tasks);
|
|
135
|
-
if (evidence.length === 0) return { reaped: [], skipped: [], candidates: [] };
|
|
136
|
-
|
|
305
|
+
if (evidence.length === 0) return { reaped: [], skipped: [], candidates: [], pending: [] };
|
|
306
|
+
const scanZombies = opts.scanZombies ?? scanZombieSubagents;
|
|
307
|
+
let allowedPids: Set<number> | undefined;
|
|
308
|
+
let requireIdentityProof = opts.requireIdentityProof ?? false;
|
|
137
309
|
try {
|
|
138
|
-
|
|
310
|
+
allowedPids = new Set(scanZombies().zombies.map((zombie) => zombie.pid));
|
|
139
311
|
} catch {
|
|
140
|
-
|
|
312
|
+
// FAIL-OPEN (wave 11a): no authoritative set at all — but NOT a licence to
|
|
313
|
+
// signal every recorded pid. Only identity-proven candidates pass.
|
|
314
|
+
allowedPids = undefined;
|
|
315
|
+
requireIdentityProof = true;
|
|
141
316
|
}
|
|
142
|
-
return reapOrphanWorkers(evidence, { ...opts, allowedPids
|
|
317
|
+
return reapOrphanWorkers(evidence, { ...opts, allowedPids, requireIdentityProof });
|
|
143
318
|
}
|
|
@@ -20,6 +20,14 @@ export const PROC_STAT_STARTTIME_INDEX = 19;
|
|
|
20
20
|
export const PROC_STAT_STATE_INDEX = 0;
|
|
21
21
|
/** Field 4 (ppid) → second entry after the comm cut. */
|
|
22
22
|
export const PROC_STAT_PPID_INDEX = 1;
|
|
23
|
+
/** Field 5 (pgrp / pgid) → third entry after the comm cut (reap-group.ts). */
|
|
24
|
+
export const PROC_STAT_PGID_INDEX = 2;
|
|
25
|
+
/**
|
|
26
|
+
* Field 7 (tty_nr) → fifth entry after the comm cut (run-descendant-sweep.ts).
|
|
27
|
+
* `0` means "no controlling terminal": a heartbeat/daemon shape. Anything else
|
|
28
|
+
* means the process is attached to a terminal — i.e. an interactive shell.
|
|
29
|
+
*/
|
|
30
|
+
export const PROC_STAT_TTY_INDEX = 4;
|
|
23
31
|
|
|
24
32
|
/**
|
|
25
33
|
* Fields of a `/proc/<pid>/stat` line AFTER the comm column (paren-aware).
|