@bridge4dev/runner 0.54.0 → 0.55.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.
- package/dist/adapters/claude.js +10 -2
- package/dist/adapters/codex.js +33 -0
- package/dist/index.js +25 -4
- package/dist/service-unit.d.ts +45 -9
- package/dist/service-unit.js +71 -21
- package/dist/session-cage.d.ts +223 -6
- package/dist/session-cage.js +473 -30
- package/dist/supervisor.d.ts +49 -0
- package/dist/supervisor.js +170 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/session-cage.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { DEVBRIDGE_SLICE, SESSION_CPU_WEIGHT, SESSIONS_SLICE } from './service-unit.js';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* The honest share of one session — where the BRAKE starts, not where it dies.
|
|
4
|
+
*
|
|
5
|
+
* Until #387 this was `MemoryMax`, the kill line. It is the same number and the
|
|
6
|
+
* same reasoning, moved one step down the ladder: above it the kernel reclaims
|
|
7
|
+
* and throttles (`MemoryHigh`), and the kill line is {@link sessionMemoryMaxBytes}.
|
|
4
8
|
*
|
|
5
9
|
* `min(2.5 GiB, service ceiling / 2)` — §3 of
|
|
6
|
-
* `docs/plans/
|
|
10
|
+
* `docs/plans/shipped/agent-sessions-host-resources.md`. Both halves matter:
|
|
7
11
|
*
|
|
8
12
|
* - the absolute number is sized against the measurement, not against a round
|
|
9
13
|
* figure. The heaviest ordinary thing a session runs is a workspace
|
|
@@ -20,7 +24,7 @@ import { DEVBRIDGE_SLICE, SESSION_CPU_WEIGHT, SESSIONS_SLICE } from './service-u
|
|
|
20
24
|
* protects the machine is what systemd has in force, not what the drop-in on
|
|
21
25
|
* disk says — those two have already drifted apart once (§5.5 of the plan).
|
|
22
26
|
*/
|
|
23
|
-
export declare const
|
|
27
|
+
export declare const SESSION_MEMORY_HIGH_ABSOLUTE_BYTES: number;
|
|
24
28
|
/**
|
|
25
29
|
* Processes and threads per session.
|
|
26
30
|
*
|
|
@@ -50,8 +54,26 @@ export interface SessionCageFacts {
|
|
|
50
54
|
mode: SessionCageMode;
|
|
51
55
|
/** Why not `scope`. Empty string when it is. */
|
|
52
56
|
reason: string;
|
|
53
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* The brake — `MemoryHigh` on the scope: past it the kernel reclaims and slows
|
|
59
|
+
* the session down instead of killing anything. Null unless `mode === 'scope'`.
|
|
60
|
+
*/
|
|
61
|
+
memoryHighBytes: number | null;
|
|
62
|
+
/**
|
|
63
|
+
* The wall — `MemoryMax` on the scope, far above the brake. Reaching it costs
|
|
64
|
+
* the session its hungriest process (and, without `oomContinue`, the session).
|
|
65
|
+
* Null unless `mode === 'scope'`.
|
|
66
|
+
*/
|
|
54
67
|
memoryMaxBytes: number | null;
|
|
68
|
+
/** Swap one session may use, bytes. 0 = none. Null unless `mode === 'scope'`. */
|
|
69
|
+
swapMaxBytes: number | null;
|
|
70
|
+
/**
|
|
71
|
+
* `OOMPolicy=continue` is on the scope: a kill inside it takes one process,
|
|
72
|
+
* not the session. False on a systemd whose scopes do not take the setting
|
|
73
|
+
* (Ubuntu 22.04 is 249, and there the whole scope still stops on any kill —
|
|
74
|
+
* which is why the wall has to be far away on every machine, not only here).
|
|
75
|
+
*/
|
|
76
|
+
oomContinue: boolean;
|
|
55
77
|
/** The service ceiling systemd has in force, as measured. Null = infinity. */
|
|
56
78
|
serviceMemoryMaxBytes: number | null;
|
|
57
79
|
/**
|
|
@@ -95,7 +117,30 @@ export interface SessionCageFacts {
|
|
|
95
117
|
* cost the one thing it does buy: the runaway dying instead of its neighbours
|
|
96
118
|
* (QA-2026-09-07 MINOR-6, where the earlier wording promised the opposite).
|
|
97
119
|
*/
|
|
98
|
-
export declare const
|
|
120
|
+
export declare const SESSION_MEMORY_HIGH_MIN_BYTES: number;
|
|
121
|
+
/**
|
|
122
|
+
* The wall, when nothing on the machine can say where it should be.
|
|
123
|
+
*
|
|
124
|
+
* Every real path reads the wall off systemd (the slice, else the service) or
|
|
125
|
+
* measures it (`memoryPolicy`). This is the last resort for a machine that gave
|
|
126
|
+
* neither — twice the honest share, so that even blind the wall sits above the
|
|
127
|
+
* brake by a margin an honest build fits into, and a runaway still meets one.
|
|
128
|
+
*/
|
|
129
|
+
export declare const SESSION_MEMORY_WALL_FALLBACK_FACTOR = 2;
|
|
130
|
+
/**
|
|
131
|
+
* The pool is cut into this many honest shares. Three, because that is what
|
|
132
|
+
* `maxSessions` defaults to on a dev server, so three sessions can sit at their
|
|
133
|
+
* brakes without the pool itself overflowing — and one runaway then meets its
|
|
134
|
+
* own wall (`ceiling − share`) while a neighbour on its share still fits.
|
|
135
|
+
*/
|
|
136
|
+
export declare const SESSION_SHARE_DIVISOR = 3;
|
|
137
|
+
/**
|
|
138
|
+
* On a machine too small to leave a share of room, the brake is this fraction
|
|
139
|
+
* of the wall. The same 0.8 `memoryPolicy` puts between the service's own
|
|
140
|
+
* `MemoryHigh` and `MemoryMax`, for the same reason: a band, however narrow,
|
|
141
|
+
* beats a bare kill line.
|
|
142
|
+
*/
|
|
143
|
+
export declare const SESSION_BRAKE_OF_WALL = 0.8;
|
|
99
144
|
/**
|
|
100
145
|
* `clamp(ceiling / 2, 2 GiB, 2.5 GiB)`, never above the ceiling itself.
|
|
101
146
|
*
|
|
@@ -114,7 +159,83 @@ export declare const SESSION_MEMORY_MIN_BYTES: number;
|
|
|
114
159
|
* hand, and then «never more than the slice» has to still be true
|
|
115
160
|
* (QA-2026-09-07 MINOR-5).
|
|
116
161
|
*/
|
|
117
|
-
export declare function
|
|
162
|
+
export declare function sessionMemoryHighBytes(containingMemoryMaxBytes: number | null): number;
|
|
163
|
+
/** The brake and the wall, decided together — see {@link sessionMemoryLadder}. */
|
|
164
|
+
export interface SessionMemoryLadder {
|
|
165
|
+
highBytes: number;
|
|
166
|
+
maxBytes: number;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The two numbers of the cage, computed in ONE place because each is wrong
|
|
170
|
+
* without the other (QA of #387 found both halves broken separately).
|
|
171
|
+
*
|
|
172
|
+
* ```
|
|
173
|
+
* brake = max(containing / 3, 2 GiB) capped by the containing ceiling
|
|
174
|
+
* wall = containing − brake never at or below the brake
|
|
175
|
+
* ```
|
|
176
|
+
*
|
|
177
|
+
* **Why a third, and no upper cap.** The owner's second note on #387: «ночью
|
|
178
|
+
* машина свободна (~9 ГБ), а сессии всё равно нельзя выйти за 2.5 ГБ… граница
|
|
179
|
+
* должна смотреть на то, сколько реально свободно». A fixed 2.5 GiB was right
|
|
180
|
+
* as a KILL line and is wrong as a brake: on a big machine it throttles honest
|
|
181
|
+
* work that the machine could have absorbed. A third of the pool is the largest
|
|
182
|
+
* share that still lets three sessions sit at their brakes inside the ceiling,
|
|
183
|
+
* which is what `maxSessions` defaults to. The 2 GiB floor stays — it is sized
|
|
184
|
+
* over the 1571 MB measured peak of a workspace `pnpm typecheck`, and a dev
|
|
185
|
+
* server that cannot run one of those is not a dev server.
|
|
186
|
+
*
|
|
187
|
+
* **Why the wall is `containing − brake` and not the ceiling itself.** 0.55.0's
|
|
188
|
+
* first shape put the wall AT the slice ceiling, and that is not a per-session
|
|
189
|
+
* wall at all: the kernel charges a leaf and every ancestor, so with any
|
|
190
|
+
* neighbour the SLICE overflows first, and then the victim is chosen across the
|
|
191
|
+
* whole slice. Measured on this host (a 300 MB slice, two 250 MB scopes): the
|
|
192
|
+
* scope that crept up survived to the end, and the **neighbour holding 200 MB
|
|
193
|
+
* under its own wall was killed** — slice `max 19, oom 1, oom_kill 1`, victim
|
|
194
|
+
* `max 0, oom 0, oom_kill 1`. Leaving one honest share of room under the
|
|
195
|
+
* ceiling is what makes the runaway meet its OWN wall first while a neighbour
|
|
196
|
+
* on its share still fits.
|
|
197
|
+
*
|
|
198
|
+
* **The tiny machine.** When the ceiling is so low that `containing − brake`
|
|
199
|
+
* would land at or under the brake, there is no room for isolation at all: the
|
|
200
|
+
* wall becomes the ceiling and the BRAKE is lowered to 80 % of it, so that
|
|
201
|
+
* there is still a braking band (`memoryPolicy` uses the same 0.8 for the
|
|
202
|
+
* service). Without this, a 4 GB machine got `MemoryHigh == MemoryMax` — the
|
|
203
|
+
* 0.54.0 kill line back in place, with the card claiming a brake.
|
|
204
|
+
*/
|
|
205
|
+
export declare function sessionMemoryLadder(containingMemoryMaxBytes: number | null, measuredCeilingBytes?: number | null): SessionMemoryLadder;
|
|
206
|
+
/**
|
|
207
|
+
* The wall — `MemoryMax` on the scope — and why it is the WHOLE containing
|
|
208
|
+
* ceiling and not a fraction of it (#387).
|
|
209
|
+
*
|
|
210
|
+
* The owner's rule: the wall is protection against a runaway, not the norm of
|
|
211
|
+
* work. Where the norm lives is {@link sessionMemoryHighBytes}; the wall has to
|
|
212
|
+
* be far enough above it that honest work never touches it, and «far» on a dev
|
|
213
|
+
* server means «what the machine can actually spare» — which is exactly the
|
|
214
|
+
* number the slice ceiling already is (`memoryPolicy`: `MemAvailable` plus what
|
|
215
|
+
* we hold, minus a reserve). Giving a session less than that is the 2.5 GiB
|
|
216
|
+
* mistake with a different number.
|
|
217
|
+
*
|
|
218
|
+
* With `OOMPolicy=continue` on the scope, reaching the wall costs a runaway its
|
|
219
|
+
* fattest process and nothing else; the neighbours are protected by the slice,
|
|
220
|
+
* which holds the same number over all of them together.
|
|
221
|
+
*
|
|
222
|
+
* `containing` is what systemd has in force on the slice (else the service);
|
|
223
|
+
* `measured` is what the policy would write there; the factor is the last
|
|
224
|
+
* resort. Never below the brake — a wall under the brake is a kill line again.
|
|
225
|
+
*/
|
|
226
|
+
export declare function sessionMemoryMaxBytes(containingMemoryMaxBytes: number | null, measuredCeilingBytes?: number | null): number;
|
|
227
|
+
/**
|
|
228
|
+
* Swap one session may push into — the reason the brake slows instead of stalls.
|
|
229
|
+
*
|
|
230
|
+
* Half of what the slice may use, the same «no single session takes the
|
|
231
|
+
* collective allowance» rule as the memory share. When systemd reports no
|
|
232
|
+
* bound on the slice (the drop-in never landed, MAJOR-3 shape) the share is cut
|
|
233
|
+
* from the machine's `SwapTotal` directly, with the same fraction the slice
|
|
234
|
+
* would have had, so the per-scope line is a real bound on its own. A machine
|
|
235
|
+
* with no swap gets 0, which is exactly 0.54.0's line — and exactly right:
|
|
236
|
+
* there is nothing to share.
|
|
237
|
+
*/
|
|
238
|
+
export declare function sessionSwapMaxBytes(sliceSwapMaxBytes: number | null, hostSwapTotalBytes: number | null): number;
|
|
118
239
|
/**
|
|
119
240
|
* The session id as systemd will accept it.
|
|
120
241
|
*
|
|
@@ -171,11 +292,18 @@ export interface CageProbe {
|
|
|
171
292
|
/** The one-shot scope that reads its own `memory.max`, built like a real one. */
|
|
172
293
|
probeMemoryMax: (options: {
|
|
173
294
|
expandEnvironmentFlag: boolean;
|
|
295
|
+
oomPolicyFlag: boolean;
|
|
174
296
|
}) => Promise<CageProbeResult>;
|
|
175
297
|
/** `MemoryMax` of the runner service in bytes; null for `infinity`. */
|
|
176
298
|
serviceMemoryMax: () => Promise<number | null>;
|
|
177
299
|
/** `MemoryMax` in force on `devbridge-sessions.slice`; null for `infinity`. */
|
|
178
300
|
sessionsSliceMemoryMax: () => Promise<number | null>;
|
|
301
|
+
/** `MemorySwapMax` in force on the slice; 0 is a real answer, null = `infinity`/unknown. */
|
|
302
|
+
sessionsSliceSwapMax: () => Promise<number | null>;
|
|
303
|
+
/** `SwapTotal` of the machine, or null where `/proc/meminfo` will not say. */
|
|
304
|
+
hostSwapTotalBytes: () => number | null;
|
|
305
|
+
/** The ceiling `memoryPolicy` would write today, or null on an unmeasurable machine. */
|
|
306
|
+
machineCeilingBytes: () => number | null;
|
|
179
307
|
/** Can this kernel renice at all — the fallback's own precondition. */
|
|
180
308
|
canRenice: () => boolean;
|
|
181
309
|
}
|
|
@@ -191,6 +319,15 @@ export interface CageProbe {
|
|
|
191
319
|
export declare function cageEnv(): Record<string, string>;
|
|
192
320
|
export declare const defaultCageProbe: CageProbe;
|
|
193
321
|
export declare function detectSessionCage(probe?: CageProbe): Promise<SessionCageFacts>;
|
|
322
|
+
/**
|
|
323
|
+
* Did `systemd-run` refuse the command line BECAUSE of `OOMPolicy=`?
|
|
324
|
+
*
|
|
325
|
+
* systemd's wording for a property a unit type does not take is «Unknown
|
|
326
|
+
* assignment: OOMPolicy=continue» (`bus_append_unit_property_assignment`); a
|
|
327
|
+
* refusal that does not name the property is some other machine's problem and
|
|
328
|
+
* must not be retried into a cage with a weaker policy.
|
|
329
|
+
*/
|
|
330
|
+
export declare function refusesOomPolicy(error: string): boolean;
|
|
194
331
|
/**
|
|
195
332
|
* Probe once, at daemon start, and remember the answer.
|
|
196
333
|
*
|
|
@@ -212,6 +349,8 @@ export interface CagedSpawn {
|
|
|
212
349
|
/** The scope unit this start will live in, or null when nothing was wrapped. */
|
|
213
350
|
unit: string | null;
|
|
214
351
|
}
|
|
352
|
+
/** The scope a live caged id runs in, or null when it is not caged (or gone). */
|
|
353
|
+
export declare function sessionScopeUnitOf(id: string): string | null;
|
|
215
354
|
/**
|
|
216
355
|
* Wrap a command in its session's cage, or hand it back untouched.
|
|
217
356
|
*
|
|
@@ -246,6 +385,77 @@ export declare function killedBeforeExec(info: {
|
|
|
246
385
|
sawOutput: boolean;
|
|
247
386
|
caged: boolean;
|
|
248
387
|
}): boolean;
|
|
388
|
+
/** The counters of one session's cgroup, read straight off the filesystem. */
|
|
389
|
+
export interface ScopeMemoryStatus {
|
|
390
|
+
/** `memory.current`. */
|
|
391
|
+
currentBytes: number;
|
|
392
|
+
/** `memory.high` — null for `max`. */
|
|
393
|
+
highBytes: number | null;
|
|
394
|
+
/** `memory.max` — null for `max`. */
|
|
395
|
+
maxBytes: number | null;
|
|
396
|
+
/** `memory.swap.current`, 0 where the file is missing. */
|
|
397
|
+
swapCurrentBytes: number;
|
|
398
|
+
/** `memory.events` `high`: how many times the brake engaged. Cumulative. */
|
|
399
|
+
highEvents: number;
|
|
400
|
+
/** `memory.events` `oom_kill`: processes the kernel killed in here. Cumulative. */
|
|
401
|
+
oomKills: number;
|
|
402
|
+
/**
|
|
403
|
+
* `memory.events` `oom`: how many times THIS cgroup's own limit was reached
|
|
404
|
+
* and an allocation was about to fail.
|
|
405
|
+
*
|
|
406
|
+
* The discriminator between «this session hit its wall» and «the pool over
|
|
407
|
+
* all sessions ran out and the kernel picked a victim anywhere in it».
|
|
408
|
+
* Measured on this host: the innocent neighbour that was killed read
|
|
409
|
+
* `max 0, oom 0, oom_kill 1`, while the slice read `max 19, oom 1,
|
|
410
|
+
* oom_kill 1`. Without it every kill was reported to the session it landed
|
|
411
|
+
* on as «you went over your ceiling», which for a neighbour is false.
|
|
412
|
+
*/
|
|
413
|
+
ownLimitOom: number;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* The pure half of {@link readScopeMemoryStatus}: the four files' text in, the
|
|
417
|
+
* status out. `memory.swap.current` is optional (no swap controller, cgroup v2
|
|
418
|
+
* without swap accounting); the rest are not.
|
|
419
|
+
*/
|
|
420
|
+
export declare function parseScopeMemoryStatus(files: {
|
|
421
|
+
current: string;
|
|
422
|
+
high: string;
|
|
423
|
+
max: string;
|
|
424
|
+
events: string;
|
|
425
|
+
swapCurrent?: string;
|
|
426
|
+
}): ScopeMemoryStatus | null;
|
|
427
|
+
/**
|
|
428
|
+
* What one live session's cgroup holds and has been through. Null when the
|
|
429
|
+
* cgroup is not there (the scope ended, or this machine has no cage).
|
|
430
|
+
*
|
|
431
|
+
* Filesystem, not `systemctl show`: this runs on the supervisor's 30 s tick for
|
|
432
|
+
* every live session, and the bus is the thing that took 2.7 s under load.
|
|
433
|
+
*/
|
|
434
|
+
export declare function readScopeMemoryStatus(unit: string, readFile?: (p: string) => string): ScopeMemoryStatus | null;
|
|
435
|
+
export declare function markScopeOomKillsSeen(id: string, oomKills: number): void;
|
|
436
|
+
/**
|
|
437
|
+
* «a process in it was stopped» / «3 processes in it were stopped», so the verb
|
|
438
|
+
* agrees. Shared with the supervisor's feed notices, which count the same
|
|
439
|
+
* thing and must not word it differently.
|
|
440
|
+
*/
|
|
441
|
+
export declare function stoppedProcesses(count: number, where?: 'it' | 'this one'): string;
|
|
442
|
+
/**
|
|
443
|
+
* Snapshot the cgroup's verdict before systemd can take it away.
|
|
444
|
+
*
|
|
445
|
+
* Exported for the test seam only (`readStatus`): the real caller passes
|
|
446
|
+
* nothing and reads the live cgroup.
|
|
447
|
+
*/
|
|
448
|
+
export declare function rememberDeath(id: string, unit: string, readStatus?: (unit: string) => ScopeMemoryStatus | null): void;
|
|
449
|
+
/**
|
|
450
|
+
* One sentence for the error the person reads, when the kernel had a hand in
|
|
451
|
+
* this death — or null when it had not. Consumed: a session restarted after an
|
|
452
|
+
* OOM must not carry the old sentence into its next, unrelated failure.
|
|
453
|
+
*
|
|
454
|
+
* The text that reached people in 0.54.0 was «exited with code 143» once and
|
|
455
|
+
* «terminated by signal SIGKILL» the next time, for the same cause, and neither
|
|
456
|
+
* said the word memory. This is that word.
|
|
457
|
+
*/
|
|
458
|
+
export declare function explainMemoryDeath(id: string): string | null;
|
|
249
459
|
/** So the sweeper and the reader can be tested without a machine under them. */
|
|
250
460
|
export type Systemctl = (args: string[]) => Promise<{
|
|
251
461
|
stdout: string;
|
|
@@ -263,6 +473,13 @@ export type Systemctl = (args: string[]) => Promise<{
|
|
|
263
473
|
* startable in the meantime.
|
|
264
474
|
*/
|
|
265
475
|
export declare function releaseSessionScope(unit: string | null, id?: string, systemctl?: Systemctl): Promise<string | null>;
|
|
476
|
+
/**
|
|
477
|
+
* The sentence for a death, once the verdict is in. Null when the kernel had no
|
|
478
|
+
* hand in it. Waits for the release of this session's scope, but never longer
|
|
479
|
+
* than `capMs`: an answer that arrives after the person has read the error is
|
|
480
|
+
* worth nothing, and a hung `systemctl` must not hold a failing session open.
|
|
481
|
+
*/
|
|
482
|
+
export declare function memoryDeathSentence(id: string, capMs?: number): Promise<string | null>;
|
|
266
483
|
/** Unit names of every `devbridge-session-*.scope` systemd still knows about. */
|
|
267
484
|
export declare function listSessionScopeUnits(systemctl?: Systemctl): Promise<string[]>;
|
|
268
485
|
export interface SessionScopeInfo {
|