@bridge4dev/runner 0.56.0 → 0.58.1

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.
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The live memory numbers this machine reports (#398 S4).
3
+ *
4
+ * Mirror of `packages/shared/src/schemas/session-limits.ts` — the DevBridge side
5
+ * is the source of truth, exactly like `host-load.ts` mirrors the load frame and
6
+ * `levels.ts` mirrors the level thresholds. Copied rather than imported on
7
+ * purpose: this package is published to npm on its own and installed on machines
8
+ * that have never heard of `@devbridge/shared`.
9
+ *
10
+ * CHECKED: `session-limits.test.ts` reads the shared file and compares every
11
+ * field below against it, so a change on one side that is not made on the other
12
+ * turns a test red instead of turning a card blank.
13
+ */
14
+ /** One session's three numbers, as they leave the machine. */
15
+ export interface SessionLimitsEntry {
16
+ sessionId: string;
17
+ guaranteedBytes: number;
18
+ brakeBytes: number;
19
+ wallBytes: number;
20
+ holdBytes: number;
21
+ /** This session's guarantee does not fit in the pot beside the others (D9). */
22
+ tight: boolean;
23
+ /** Milliseconds until its biggest command is stopped; null when it is moving. */
24
+ stallRemainingMs: number | null;
25
+ /** Why its limits last moved. */
26
+ reason: 'added' | 'stopped-command' | 'nothing-to-stop' | 'report-only' | null;
27
+ }
28
+ export interface SessionLimitsFrame {
29
+ at: string;
30
+ poolBytes: number;
31
+ heldBytes: number;
32
+ otherHeldBytes: number;
33
+ /** What one session is guaranteed here — the constant, so a card with no
34
+ * session on it can still name the number. */
35
+ guaranteeBytes: number;
36
+ /** The brake a session starting right now would get — the card cannot derive it. */
37
+ sessionCeilingBytes: number;
38
+ guaranteesFit: number;
39
+ seats: number;
40
+ swapPerSessionBytes: number;
41
+ collectiveBrake: boolean;
42
+ conservative: boolean;
43
+ hungry: boolean;
44
+ sessions: SessionLimitsEntry[];
45
+ }
46
+ /** Mirror of `SESSION_LIMITS_HEARTBEAT_MS`. */
47
+ export declare const SESSION_LIMITS_HEARTBEAT_MS = 60000;
48
+ /** Mirror of `SESSION_LIMITS_MIN_DELTA_RATIO`. */
49
+ export declare const SESSION_LIMITS_MIN_DELTA_RATIO = 0.1;
50
+ /**
51
+ * Is this frame different enough from the last one to be worth the socket?
52
+ *
53
+ * The allocator runs every 30 s and its numbers breathe with the machine, so
54
+ * without a gate this would be a write per machine per half-minute for a card
55
+ * nobody is looking at. What counts as a change:
56
+ *
57
+ * - anything about WHO is on the machine — a session appearing or leaving, or
58
+ * a session's limits actually being rewritten. Those are the moments a person
59
+ * would notice, and no ratio must be able to hide them;
60
+ * - a state that a person acts on — the deadline appearing or disappearing, the
61
+ * crowding flag, the collective brake going missing;
62
+ * - and otherwise a tenth of the pot, which is the same dead zone the allocator
63
+ * uses before it will write to a scope at all.
64
+ */
65
+ export declare function sessionLimitsChangedEnough(previous: SessionLimitsFrame | null, next: SessionLimitsFrame): boolean;
66
+ /**
67
+ * Is a heartbeat due? Same shape and same reason as `hostLoadHeartbeatDue`: a
68
+ * backwards clock step is «due», not «early».
69
+ */
70
+ export declare function sessionLimitsHeartbeatDue(sinceLastSentMs: number, heartbeatMs: number): boolean;
71
+ //# sourceMappingURL=session-limits.d.ts.map
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The live memory numbers this machine reports (#398 S4).
3
+ *
4
+ * Mirror of `packages/shared/src/schemas/session-limits.ts` — the DevBridge side
5
+ * is the source of truth, exactly like `host-load.ts` mirrors the load frame and
6
+ * `levels.ts` mirrors the level thresholds. Copied rather than imported on
7
+ * purpose: this package is published to npm on its own and installed on machines
8
+ * that have never heard of `@devbridge/shared`.
9
+ *
10
+ * CHECKED: `session-limits.test.ts` reads the shared file and compares every
11
+ * field below against it, so a change on one side that is not made on the other
12
+ * turns a test red instead of turning a card blank.
13
+ */
14
+ /** Mirror of `SESSION_LIMITS_HEARTBEAT_MS`. */
15
+ export const SESSION_LIMITS_HEARTBEAT_MS = 60_000;
16
+ /** Mirror of `SESSION_LIMITS_MIN_DELTA_RATIO`. */
17
+ export const SESSION_LIMITS_MIN_DELTA_RATIO = 0.1;
18
+ /**
19
+ * Is this frame different enough from the last one to be worth the socket?
20
+ *
21
+ * The allocator runs every 30 s and its numbers breathe with the machine, so
22
+ * without a gate this would be a write per machine per half-minute for a card
23
+ * nobody is looking at. What counts as a change:
24
+ *
25
+ * - anything about WHO is on the machine — a session appearing or leaving, or
26
+ * a session's limits actually being rewritten. Those are the moments a person
27
+ * would notice, and no ratio must be able to hide them;
28
+ * - a state that a person acts on — the deadline appearing or disappearing, the
29
+ * crowding flag, the collective brake going missing;
30
+ * - and otherwise a tenth of the pot, which is the same dead zone the allocator
31
+ * uses before it will write to a scope at all.
32
+ */
33
+ export function sessionLimitsChangedEnough(previous, next) {
34
+ if (previous === null)
35
+ return true;
36
+ if (previous.sessions.length !== next.sessions.length)
37
+ return true;
38
+ if (previous.collectiveBrake !== next.collectiveBrake ||
39
+ previous.conservative !== next.conservative ||
40
+ previous.hungry !== next.hungry ||
41
+ previous.guaranteeBytes !== next.guaranteeBytes ||
42
+ // Exact, and it can be: since #403 this is the ceiling of an IDLE machine,
43
+ // computed from the pot and the owner's knobs alone. It does not follow
44
+ // `MemAvailable`, so it does not breathe, and any move in it is a fact
45
+ // worth a frame. (While it was the live ceiling of the current crowd, this
46
+ // line sent one every thirty seconds from every machine.)
47
+ previous.sessionCeilingBytes !== next.sessionCeilingBytes ||
48
+ previous.guaranteesFit !== next.guaranteesFit ||
49
+ previous.seats !== next.seats ||
50
+ previous.swapPerSessionBytes !== next.swapPerSessionBytes) {
51
+ return true;
52
+ }
53
+ const before = new Map(previous.sessions.map((s) => [s.sessionId, s]));
54
+ for (const session of next.sessions) {
55
+ const was = before.get(session.sessionId);
56
+ if (!was)
57
+ return true;
58
+ if (was.tight !== session.tight ||
59
+ was.reason !== session.reason ||
60
+ // «A countdown is running» and «it is not» are different states; the exact
61
+ // number of seconds left is not, or the strip would repaint every tick.
62
+ (was.stallRemainingMs === null) !== (session.stallRemainingMs === null)) {
63
+ return true;
64
+ }
65
+ if (was.guaranteedBytes !== session.guaranteedBytes ||
66
+ was.brakeBytes !== session.brakeBytes ||
67
+ was.wallBytes !== session.wallBytes) {
68
+ // The allocator has its own dead zone before it writes, so a limit that
69
+ // moved at all is a limit worth reporting.
70
+ return true;
71
+ }
72
+ }
73
+ const base = Math.max(1, previous.poolBytes);
74
+ if (Math.abs(next.poolBytes - previous.poolBytes) / base >= SESSION_LIMITS_MIN_DELTA_RATIO) {
75
+ return true;
76
+ }
77
+ if (Math.abs(next.heldBytes - previous.heldBytes) / base >= SESSION_LIMITS_MIN_DELTA_RATIO) {
78
+ return true;
79
+ }
80
+ if (Math.abs(next.otherHeldBytes - previous.otherHeldBytes) / base >=
81
+ SESSION_LIMITS_MIN_DELTA_RATIO) {
82
+ return true;
83
+ }
84
+ return false;
85
+ }
86
+ /**
87
+ * Is a heartbeat due? Same shape and same reason as `hostLoadHeartbeatDue`: a
88
+ * backwards clock step is «due», not «early».
89
+ */
90
+ export function sessionLimitsHeartbeatDue(sinceLastSentMs, heartbeatMs) {
91
+ return sinceLastSentMs >= heartbeatMs || sinceLastSentMs < 0;
92
+ }
93
+ //# sourceMappingURL=session-limits.js.map
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Telling «this session is standing still» from «this session is slow» — and
3
+ * doing something about it before the night is over (#398 S2, gotcha §480).
4
+ *
5
+ * Above `MemoryHigh` the kernel does not kill, it puts the allocating thread to
6
+ * sleep on the way back to userspace; with `MemorySwapMax=0` there is nothing to
7
+ * push out, so the penalty is up to 2 s per 256 KB and the process never reaches
8
+ * `MemoryMax` at all — it simply stops. On 09.09.2026 that cost one session an
9
+ * hour of silence on a build that printed nothing but its startup warnings.
10
+ * Ten of the fleet's fifteen caged machines have a session swap of zero, so this
11
+ * is the ordinary case and not the exotic one.
12
+ *
13
+ * The measure is ONE number and it is not the one three earlier drafts reached
14
+ * for.
15
+ *
16
+ * - **Not CPU.** A process wedged in direct reclaim burns system time in bursts:
17
+ * the same stuck process was measured at 3.6 %, 2.9 %, 6.9 %, 18.7 %, 98 %,
18
+ * 53 %, 22 %, 67 % of one core on consecutive seconds. Any threshold over it
19
+ * both misfires and misses.
20
+ * - **Not swap.** The first draft counted growth in `memory.swap.current` and
21
+ * `pgmajfault` as evidence of progress, out of a fear of killing honest work
22
+ * on a machine that swaps. The measurement refuted it: a live session with
23
+ * 232 MB of swap and 1.3 M major faults stood still for 2 h 15 min out of 18.
24
+ * Swap does not separate useful paging from thrashing.
25
+ * - **Pressure does.** `memory.pressure`, line `full`, field `total=`, is
26
+ * cumulative microseconds during which NO task in the cgroup could run for
27
+ * want of memory. Measured on this host: a wedged process, 27.9 s of stall in
28
+ * a 27.5 s window (~100 %); a healthy live session, 10.7 s in six hours
29
+ * (0.05 %); a session thrashing swap, 8093 s in 18 hours with the share close
30
+ * to 1 inside the bad stretches. Three orders of magnitude between honest
31
+ * work and a stall, in one number, on every machine that has PSI.
32
+ *
33
+ * The second condition is a cheap fuse and it is also a measurement: on a live
34
+ * scope of this host `anon` was 308 MB against a `memory.current` of 1033 MB
35
+ * with 9246 brake events — i.e. the brake very often catches page cache, which
36
+ * the kernel takes back with no stall at all. Those episodes must not reach the
37
+ * counter, let alone the deadline.
38
+ */
39
+ /**
40
+ * One detector window.
41
+ *
42
+ * Five seconds, not the watch's thirty: the deadline below is three minutes, and
43
+ * a measure that needs two consecutive windows would spend a third of the
44
+ * deadline deciding at the 30 s cadence. Five seconds of `full` stall is also
45
+ * long enough that a single scheduling hiccup cannot fill it.
46
+ */
47
+ export declare const STALL_WINDOW_MS = 5000;
48
+ /**
49
+ * Share of the window spent with nothing in the cgroup able to run, above which
50
+ * the window counts as a standing one.
51
+ *
52
+ * **A fifth, not a half, and the difference was found by measurement.** The plan
53
+ * this module implements said 50 %, taken from a hard stall that read ~100 %.
54
+ * Reproducing the incident itself — a cage with a 64 MiB brake and no swap, a
55
+ * process touching what it allocates — gave four consecutive windows of
56
+ * **0.766, 0.484, 0.443, 0.542** (10.09.2026, this host). Two of the four sit
57
+ * BELOW a half: at that line the very shape this mechanism exists for would
58
+ * trip and clear in turn and never reach its deadline.
59
+ *
60
+ * There is room to move the line because the gap is enormous. Measured on the
61
+ * same host: a healthy live session, 0.0005; honest disk-bound work under the
62
+ * same 64 MiB brake writing 6.3 GB at 739 MB/s, exactly **0.0** with the brake
63
+ * never firing at all. A fifth is four hundred times what healthy work shows and
64
+ * half of what the mildest stalled window did.
65
+ */
66
+ export declare const STALL_SHARE = 0.2;
67
+ /**
68
+ * …and the share it has to fall BELOW before a running deadline is cleared.
69
+ *
70
+ * Two lines, not one, for the reason the cage watch already had `calmTicks`: a
71
+ * genuine stall is not a flat number, it breathes. One line means one dip
72
+ * cancels three minutes of standing still. Half the trip line, and still two
73
+ * hundred times the 0.0005 healthy work shows.
74
+ */
75
+ export declare const STALL_SHARE_CLEAR = 0.1;
76
+ /** Consecutive windows over the line before the session counts as standing still. */
77
+ export declare const STALL_WINDOWS_TO_TRIP = 2;
78
+ /**
79
+ * `(anon + shmem) / memory.current` above which the pressure is about memory the
80
+ * kernel cannot simply drop. Below it the brake is grinding page cache.
81
+ */
82
+ export declare const STALL_ANON_SHARE = 0.9;
83
+ /**
84
+ * How long a standing session is given before its biggest command is stopped
85
+ * (decision D6 of the plan).
86
+ *
87
+ * Three minutes: long enough for a person who is watching to intervene, short
88
+ * enough that a session which stalls at 02:00 is not still standing at 08:00.
89
+ * Doubled on a machine without PSI, where the fallback measure below is weaker.
90
+ */
91
+ export declare const STALL_GRACE_MS = 180000;
92
+ /**
93
+ * A drop of this much in `memory.current` counts as real progress and clears the
94
+ * deadline.
95
+ *
96
+ * A new line of output does NOT: in the incident the build printed startup
97
+ * warnings for seventeen minutes while allocating nothing.
98
+ */
99
+ export declare const STALL_PROGRESS_DROP_BYTES: number;
100
+ /**
101
+ * How long the detector stays quiet after WE lowered this session's brake.
102
+ *
103
+ * Measured on this host: a process honestly writing a file at 6.4 MB/s was
104
+ * stopped dead the instant its `MemoryHigh` was narrowed on the live scope. Any
105
+ * lowering therefore manufactures exactly the signal this module looks for, and
106
+ * without the mute the mechanism would catch its own edit and stop a command for
107
+ * it. One window plus the deadline, with room to spare.
108
+ */
109
+ export declare const STALL_MUTE_AFTER_LOWER_MS = 240000;
110
+ /**
111
+ * Between SIGTERM and SIGKILL for the subtree being stopped.
112
+ *
113
+ * Thirty seconds, not five: neither `pnpm install` nor `docker build` can put
114
+ * its own house in order in five, and half-written state costs the next turn
115
+ * more than the wait costs this one.
116
+ */
117
+ export declare const STALL_SIGKILL_AFTER_MS = 30000;
118
+ /**
119
+ * Growth under which the PSI-less fallback still calls a session stuck:
120
+ * `min(1 % of the brake, 16 MiB)` per window.
121
+ */
122
+ export declare const STALL_FALLBACK_GROWTH_BYTES: number;
123
+ /** What one read of a session's cgroup says about whether it is moving. */
124
+ export interface StallSample {
125
+ /**
126
+ * `memory.pressure`, line `full`, `total=` — cumulative microseconds with no
127
+ * task in the cgroup able to run. Null on a kernel built without `CONFIG_PSI`.
128
+ */
129
+ pressureFullUs: number | null;
130
+ /** `memory.events` `high`, cumulative — the fallback measure. */
131
+ highEvents: number;
132
+ /** `memory.current`. */
133
+ currentBytes: number;
134
+ /** `anon` + `shmem` from `memory.stat` — what cannot simply be dropped. */
135
+ anonBytes: number;
136
+ /** `memory.high`, null for `max`. */
137
+ brakeBytes: number | null;
138
+ /** When this sample was taken. */
139
+ at: number;
140
+ }
141
+ /** The detector's memory of one session. Deliberately its own, not `cageWatch`. */
142
+ export interface StallState {
143
+ /**
144
+ * Which cgroup these numbers came from. A relaunch gets the same unit NAME
145
+ * back once the failed unit is reset, so the counters are checked as well:
146
+ * anything that went backwards is a new cgroup, not a counter that shrank.
147
+ */
148
+ unit: string;
149
+ last: StallSample | null;
150
+ /** Consecutive windows over the line. */
151
+ hotWindows: number;
152
+ /** When the deadline started running; null = this session is not stuck. */
153
+ stalledSince: number | null;
154
+ /** `memory.current` when the deadline started — the yardstick for «it moved». */
155
+ markBytes: number;
156
+ /** Detection is off until this moment, because we moved the brake ourselves. */
157
+ mutedUntil: number;
158
+ }
159
+ export declare function freshStallState(unit: string): StallState;
160
+ /**
161
+ * The pure half: five files' text in, a sample out. Null when `memory.current`
162
+ * could not be read, which means the cgroup is gone.
163
+ */
164
+ export declare function parseStallSample(files: {
165
+ current: string;
166
+ high: string;
167
+ events: string;
168
+ stat: string;
169
+ pressure?: string;
170
+ }, at: number): StallSample | null;
171
+ /** Read one sample off a live scope. Null when the cgroup is not there. */
172
+ export declare function readStallSample(unit: string, at: number, readFile?: (p: string) => string): StallSample | null;
173
+ /** What the detector decided about one window. */
174
+ export interface StallVerdict {
175
+ state: StallState;
176
+ /** Is this session standing still right now? */
177
+ stuck: boolean;
178
+ /** Milliseconds left before its biggest command is stopped; null when not stuck. */
179
+ remainingMs: number | null;
180
+ /** The deadline has run out — the caller stops a command now. */
181
+ expired: boolean;
182
+ /** PSI was unavailable, so the weaker measure was used and the grace doubled. */
183
+ degraded: boolean;
184
+ }
185
+ /**
186
+ * One window of the detector, as a pure function of the previous state.
187
+ *
188
+ * Everything the caller must not get wrong lives here: two windows before the
189
+ * counter trips, the anon fuse, the mute after our own edit, and a reset that
190
+ * only real progress can buy.
191
+ */
192
+ export declare function stallStep(previous: StallState, sample: StallSample, options?: {
193
+ graceMs?: number;
194
+ now?: number;
195
+ windowMs?: number;
196
+ /**
197
+ * Is this the only session in the slice right now (#398 S7, B9)?
198
+ *
199
+ * When it is, «somebody else's brake» has no somebody else: the collective
200
+ * brake on the slice can only have been filled by this session, and the
201
+ * discriminator below must not read that as innocence.
202
+ */
203
+ soleSession?: boolean;
204
+ }): StallVerdict;
205
+ /** One process of a session's cgroup, as `/proc` describes it. */
206
+ export interface ScopeProcess {
207
+ pid: number;
208
+ ppid: number;
209
+ /**
210
+ * Field 22 of `/proc/<pid>/stat` — the moment this process started, in clock
211
+ * ticks since boot.
212
+ *
213
+ * The identity of a pid, and the reason it is carried: pids are REUSED. A
214
+ * subtree is asked to stop with SIGTERM and killed thirty seconds later, and
215
+ * on a busy machine a pid freed in between can belong to something else
216
+ * entirely by then — so the second signal is checked against this before it
217
+ * is sent. Found by the independent review of 10.09.2026, which called it the
218
+ * most dangerous line in the change, and it was right.
219
+ */
220
+ startedAtTicks: number;
221
+ /** `/proc/<pid>/comm` — the executable's name, never its arguments. */
222
+ comm: string;
223
+ /** Resident set, bytes, from `/proc/<pid>/statm`. */
224
+ rssBytes: number;
225
+ /** Does the command line name an MCP server? Then it is the agent's, not work. */
226
+ mcp: boolean;
227
+ }
228
+ /** The chosen victim: a whole subtree, named by its heaviest process. */
229
+ export interface KillCandidate {
230
+ /** Root of the subtree — the process whose whole tree is stopped. */
231
+ rootPid: number;
232
+ /** Every pid in that subtree, deepest last. */
233
+ pids: number[];
234
+ /**
235
+ * The same pids with the moment each one started, so the SIGKILL thirty
236
+ * seconds later can prove it is still signalling the same processes.
237
+ */
238
+ identities: Array<{
239
+ pid: number;
240
+ startedAtTicks: number;
241
+ }>;
242
+ /** The heaviest process in it, by name only — arguments can carry a token. */
243
+ name: string;
244
+ /** What the whole subtree holds. */
245
+ rssBytes: number;
246
+ }
247
+ /**
248
+ * Anything whose command line says «MCP server» is the agent's own plumbing.
249
+ *
250
+ * Deliberately generous, because the direction of a mistake matters: a false
251
+ * positive costs us a candidate (we stop something smaller, or nothing), a false
252
+ * negative costs the session its tools mid-turn.
253
+ *
254
+ * **Widened after the independent review of 10.09.2026, which found the first
255
+ * version matched nothing real.** The command lines actually on this machine are
256
+ * `npm exec @playwright/mcp@latest …` and `node …/playwright-mcp --output-dir …`
257
+ * — in the first `mcp` is followed by `@`, in the second preceded by `-`, and
258
+ * the original character classes allowed neither. So the rule is now simply
259
+ * «`mcp` as a word», which does match both.
260
+ *
261
+ * The same review found the mirror-image defect, and it was the worse of the
262
+ * two: the AGENT's own command line contains `--mcp-config …`, so the agent
263
+ * matched, and protection used to be spread from anything that matched — which
264
+ * protected the agent's ENTIRE subtree and made every Claude session return no
265
+ * candidate at all. Protection is no longer spread from a tree root: a root is
266
+ * protected on its own account (it is the agent), and spreading from it would
267
+ * protect the very builds this mechanism exists to stop.
268
+ */
269
+ export declare const MCP_MARKER: RegExp;
270
+ export declare function residentPageSize(readFile?: (p: string) => string, procDir?: string): number;
271
+ /**
272
+ * Read `/proc` for every process in one session's cgroup.
273
+ *
274
+ * **The exception to the rule in `host-load.ts`**, and the only one: that module
275
+ * states in full what this runner may read for telemetry, and says «no
276
+ * `/proc/<pid>` walk, no `ps`». This is not telemetry — nothing here leaves the
277
+ * machine except one command NAME — and choosing a victim cannot be done from
278
+ * two summary files. The rule and this exception are noted in both places.
279
+ *
280
+ * `ppid` is taken from after the LAST `)` of `/proc/<pid>/stat`, never by field
281
+ * number: a process name may contain spaces and parentheses (`(npm exec
282
+ * @playw)`), which moves every field after it (gotcha §441).
283
+ */
284
+ export declare function readScopeProcesses(unit: string, readFile?: (p: string) => string, procDir?: string): ScopeProcess[];
285
+ /**
286
+ * The biggest command of this session, defined by the letter and not by «the
287
+ * fattest thing on the machine».
288
+ *
289
+ * Every exclusion below is a measurement or an incident:
290
+ *
291
+ * - **only pids of THIS cgroup.** By RSS the fattest processes on a dev server
292
+ * are `dockerd`, the production API and other people's agents; a candidate
293
+ * chosen machine-wide would stop one of those.
294
+ * - **never the agent.** The agent is the one process in the scope whose parent
295
+ * is OUTSIDE it — `systemd-run --scope` execs into it and the runner spawned
296
+ * it. That is a structural fact and needs no pid handed down through two
297
+ * adapters. Stopping the agent is what the top «Stop» button is for; this
298
+ * mechanism has no right to it (D6).
299
+ * - **never an MCP server**, nor anything under one: those are the agent's
300
+ * tools, not its work.
301
+ * - **never `git`.** It is never the fattest thing, and a killed `git` leaves
302
+ * `.git/index.lock` behind, which then costs the session its restore points
303
+ * and its next turns.
304
+ * - **the whole subtree**, not one pid: over a `tsc` there is a `pnpm`, and
305
+ * over that a `sh`, and killing the leaf leaves the parent to start another.
306
+ */
307
+ export declare function pickKillCandidate(processes: ScopeProcess[], agentPid?: number | null): KillCandidate | null;
308
+ /**
309
+ * Is this pid still the process it was when we decided to stop it?
310
+ *
311
+ * `starttime` never repeats for one pid, so comparing it is the cheap and exact
312
+ * answer. Anything unreadable is «not the same», which is the safe direction: a
313
+ * signal not sent costs a command another thirty seconds, a signal sent to the
314
+ * wrong process costs somebody something we know nothing about.
315
+ */
316
+ export declare function isSameProcess(identity: {
317
+ pid: number;
318
+ startedAtTicks: number;
319
+ }, readFile?: (p: string) => string, procDir?: string): boolean;
320
+ /**
321
+ * Send one signal to every pid of a subtree. Processes that ended are skipped.
322
+ *
323
+ * When identities are given, each pid is checked against the process it was
324
+ * when the subtree was chosen — see {@link isSameProcess}. The SIGKILL that
325
+ * follows a SIGTERM thirty seconds later is the call that needs it: on a busy
326
+ * machine a pid freed in between can belong to something else by then.
327
+ */
328
+ export declare function signalSubtree(pids: Array<number | {
329
+ pid: number;
330
+ startedAtTicks: number;
331
+ }>, signal: NodeJS.Signals, kill?: (pid: number, signal: NodeJS.Signals) => void, sameProcess?: (identity: {
332
+ pid: number;
333
+ startedAtTicks: number;
334
+ }) => boolean): number;
335
+ /**
336
+ * Raise this session's brake on the live scope.
337
+ *
338
+ * `systemctl --user set-property --runtime`, never a write into the cgroup
339
+ * files: a direct write is undone by the next `daemon-reload`, and the runner
340
+ * calls one itself every hour. `--runtime` is not optional either — without it
341
+ * systemd persists the property into `~/.config/systemd/user.control/`, which
342
+ * OUTRANKS the drop-in this runner ships and which `doctor` reports as an
343
+ * emergency override (`index.ts`).
344
+ *
345
+ * Never throws. A failure here must not take down the tick, let alone the
346
+ * daemon: the daemon carries a fatal `unhandledRejection` handler, so one
347
+ * unhandled write error would end every live session on the machine. A
348
+ * `set-property` on a unit that ended between the measurement and the write
349
+ * exits 1 with «Unit … not found» — the ordinary outcome of a race, not an
350
+ * error worth a loud line.
351
+ */
352
+ export declare function setScopeProperties(unit: string, properties: string[], run?: (args: string[]) => Promise<unknown>): Promise<boolean>;
353
+ //# sourceMappingURL=session-stall.d.ts.map