@bridge4dev/runner 0.53.0 → 0.54.0
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 +96 -1
- package/dist/adapters/codex-protocol.d.ts +11 -0
- package/dist/adapters/codex-protocol.js +41 -3
- package/dist/adapters/codex.js +4 -0
- package/dist/host-load.d.ts +156 -0
- package/dist/host-load.js +223 -0
- package/dist/index.js +190 -40
- package/dist/process-priority.d.ts +55 -0
- package/dist/process-priority.js +99 -0
- package/dist/protocol.d.ts +25 -3
- package/dist/recipe-schema.d.ts +6 -6
- package/dist/self-update.js +43 -2
- package/dist/service-unit.d.ts +232 -10
- package/dist/service-unit.js +372 -43
- package/dist/session-cage.d.ts +297 -0
- package/dist/session-cage.js +755 -0
- package/dist/supervisor.d.ts +57 -0
- package/dist/supervisor.js +72 -0
- package/dist/systemd-memory.d.ts +35 -0
- package/dist/systemd-memory.js +115 -0
- package/dist/verify.js +28 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/self-update.js
CHANGED
|
@@ -4,6 +4,7 @@ import os from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { findClaudeCli, USE_BUNDLED_CLAUDE } from './agent-binary.js';
|
|
7
|
+
import { systemdUserEnv } from './environment.js';
|
|
7
8
|
import { log } from './log.js';
|
|
8
9
|
import { stateDir } from './paths.js';
|
|
9
10
|
import { RUNNER_VERSION } from './version.js';
|
|
@@ -330,6 +331,8 @@ async function installGlobal(exec, source, prefix) {
|
|
|
330
331
|
* npm needs PATH/HOME and a writable cache; everything else is stripped, both to
|
|
331
332
|
* keep provider credentials out of a child process that touches the network and
|
|
332
333
|
* to stop a stray `npm_config_*` from redirecting the install.
|
|
334
|
+
*
|
|
335
|
+
* NOT for `systemctl` — see `systemctlEnv()` below.
|
|
333
336
|
*/
|
|
334
337
|
function npmEnv() {
|
|
335
338
|
const env = {
|
|
@@ -344,6 +347,44 @@ function npmEnv() {
|
|
|
344
347
|
env['XDG_CACHE_HOME'] = process.env['XDG_CACHE_HOME'];
|
|
345
348
|
return env;
|
|
346
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* What `systemctl --user` needs, and nothing more.
|
|
352
|
+
*
|
|
353
|
+
* `npmEnv()` was used here too, and that was the whole defect (plan §5.5): it
|
|
354
|
+
* strips `XDG_RUNTIME_DIR`, which is how the client finds this user's D-Bus
|
|
355
|
+
* socket. Without it systemd prints «Failed to connect to bus: No medium found»
|
|
356
|
+
* and exits **0** — so every self-update reported a `daemon-reload` that never
|
|
357
|
+
* happened, and the memory ceiling written into the drop-in a line earlier never
|
|
358
|
+
* came into effect. In the file 7680M, in effect 6.0G, and no error anywhere.
|
|
359
|
+
*
|
|
360
|
+
* The two environments are merged rather than swapped, because each one answers
|
|
361
|
+
* a question the other does not:
|
|
362
|
+
*
|
|
363
|
+
* - `systemdUserEnv()` alone would work, and is what the rest of the runner
|
|
364
|
+
* passes to `systemctl`. But it starts from the full `process.env`, and in
|
|
365
|
+
* the daemon that includes the agent provider keys. This module deliberately
|
|
366
|
+
* hands no child a credential it has no use for (there is a test for exactly
|
|
367
|
+
* that), and `systemctl` has no use for one.
|
|
368
|
+
* - `npmEnv()` alone is the bug.
|
|
369
|
+
*
|
|
370
|
+
* So: the minimal shape of `npmEnv()` — PATH to find the binary, HOME for
|
|
371
|
+
* completeness — plus the two bus variables `systemdUserEnv()` resolves. The
|
|
372
|
+
* `npm_config_*` half is left out; it means nothing to systemctl.
|
|
373
|
+
*/
|
|
374
|
+
function systemctlEnv() {
|
|
375
|
+
const resolved = systemdUserEnv();
|
|
376
|
+
const env = {
|
|
377
|
+
PATH: process.env['PATH'] ?? '/usr/local/bin:/usr/bin:/bin',
|
|
378
|
+
HOME: process.env['HOME'] ?? '',
|
|
379
|
+
};
|
|
380
|
+
const runtimeDir = resolved['XDG_RUNTIME_DIR'];
|
|
381
|
+
if (runtimeDir)
|
|
382
|
+
env['XDG_RUNTIME_DIR'] = runtimeDir;
|
|
383
|
+
const busAddress = resolved['DBUS_SESSION_BUS_ADDRESS'];
|
|
384
|
+
if (busAddress)
|
|
385
|
+
env['DBUS_SESSION_BUS_ADDRESS'] = busAddress;
|
|
386
|
+
return env;
|
|
387
|
+
}
|
|
347
388
|
export async function selfUpdate(options) {
|
|
348
389
|
const exec = options.exec ??
|
|
349
390
|
((file, args, opts) => execFileAsync(file, args, { timeout: opts.timeout, env: opts.env, maxBuffer: 4_000_000 }));
|
|
@@ -501,7 +542,7 @@ export async function selfUpdate(options) {
|
|
|
501
542
|
fs.writeFileSync(unitPath(), buildUnit(installed.command));
|
|
502
543
|
await exec('systemctl', ['--user', 'daemon-reload'], {
|
|
503
544
|
timeout: VERIFY_TIMEOUT_MS,
|
|
504
|
-
env:
|
|
545
|
+
env: systemctlEnv(),
|
|
505
546
|
});
|
|
506
547
|
log.warn('self-update: the service unit pointed at the previous location — rewritten', {
|
|
507
548
|
execStart: installed.command,
|
|
@@ -525,7 +566,7 @@ export async function selfUpdate(options) {
|
|
|
525
566
|
if ((options.writeLimits ?? writeLimitsOverride)()) {
|
|
526
567
|
await exec('systemctl', ['--user', 'daemon-reload'], {
|
|
527
568
|
timeout: VERIFY_TIMEOUT_MS,
|
|
528
|
-
env:
|
|
569
|
+
env: systemctlEnv(),
|
|
529
570
|
});
|
|
530
571
|
log.warn('self-update: resource limits drop-in written', {
|
|
531
572
|
path: limitsOverridePath(),
|
package/dist/service-unit.d.ts
CHANGED
|
@@ -57,8 +57,45 @@ export declare function buildUnit(execStart?: string, nodeBinary?: string): stri
|
|
|
57
57
|
* which is the only way to fix the servers that already have the bad numbers
|
|
58
58
|
* baked in — and it never overwrites a unit the operator edited by hand.
|
|
59
59
|
*/
|
|
60
|
-
export declare const LIMITS_VERSION =
|
|
60
|
+
export declare const LIMITS_VERSION = 4;
|
|
61
|
+
/**
|
|
62
|
+
* Where the agent sessions live once they have a cage of their own.
|
|
63
|
+
*
|
|
64
|
+
* A `systemd-run --scope` is a SIBLING of the service, not a child of it: a
|
|
65
|
+
* session started that way leaves the service's `MemoryMax`, `CPUQuota`,
|
|
66
|
+
* `OOMPolicy=continue` and `KillMode=control-group` behind entirely. So the
|
|
67
|
+
* collective ceiling has to move with them, onto the slice — otherwise the cage
|
|
68
|
+
* per session would arrive at the price of the ceiling over all of them.
|
|
69
|
+
*
|
|
70
|
+
* The dash is systemd's hierarchy separator: `devbridge-sessions.slice` is a
|
|
71
|
+
* child of `devbridge.slice`, which is where the CPU share for everything the
|
|
72
|
+
* agents run is set.
|
|
73
|
+
*/
|
|
74
|
+
export declare const SESSIONS_SLICE = "devbridge-sessions.slice";
|
|
75
|
+
export declare const DEVBRIDGE_SLICE = "devbridge.slice";
|
|
76
|
+
/**
|
|
77
|
+
* Half the default weight, on `devbridge.slice` and on every session scope.
|
|
78
|
+
*
|
|
79
|
+
* Lives here rather than in `session-cage.ts` because the number has to be the
|
|
80
|
+
* SAME in both places — the slice sets the share of the agents against the
|
|
81
|
+
* daemon, the scope sets one session's share against another's — and a
|
|
82
|
+
* duplicated literal is how those two drift apart. Why 50 and what it replaces:
|
|
83
|
+
* `buildDevbridgeSliceOverride` below.
|
|
84
|
+
*/
|
|
85
|
+
export declare const SESSION_CPU_WEIGHT = 50;
|
|
61
86
|
export declare function limitsOverridePath(home?: string): string;
|
|
87
|
+
/**
|
|
88
|
+
* Drop-in path for a slice unit that has no unit FILE at all.
|
|
89
|
+
*
|
|
90
|
+
* systemd synthesises `devbridge-sessions.slice` the first time something asks
|
|
91
|
+
* for it, and it reads drop-ins for the synthesised unit exactly as for a real
|
|
92
|
+
* one — verified on this machine: a `[Slice] MemoryMax=64M` drop-in with no
|
|
93
|
+
* fragment gave `MemoryMax=67108864` on the live slice. So there is no unit file
|
|
94
|
+
* to write and no unit file to keep in sync; the policy is the drop-in.
|
|
95
|
+
*/
|
|
96
|
+
export declare function sliceOverridePath(slice: string, home?: string): string;
|
|
97
|
+
export declare function sessionsSliceOverridePath(home?: string): string;
|
|
98
|
+
export declare function devbridgeSliceOverridePath(home?: string): string;
|
|
62
99
|
/**
|
|
63
100
|
* What the memory policy needs to know about this machine. Read once and passed
|
|
64
101
|
* in, so the policy itself is a pure function that a test can drive with the
|
|
@@ -76,6 +113,43 @@ export interface MemoryFacts {
|
|
|
76
113
|
* as the floor under the ceiling; see `memoryPolicy`.
|
|
77
114
|
*/
|
|
78
115
|
ownUsageBytes: number;
|
|
116
|
+
/**
|
|
117
|
+
* What a LOWER ceiling could not reclaim its way out of — the number the
|
|
118
|
+
* ceiling has to clear, and not the same question as the one above.
|
|
119
|
+
*
|
|
120
|
+
* Equal to `ownUsageBytes` whenever the split is known, which is every path
|
|
121
|
+
* that can read the cgroup's own `memory.stat`. It differs only where the
|
|
122
|
+
* split is unknown — systemd answered `MemoryCurrent` but the files behind it
|
|
123
|
+
* could not be read — and there it is the FULL reading: a ceiling written
|
|
124
|
+
* under an unknown cgroup must assume none of it can be given back.
|
|
125
|
+
*
|
|
126
|
+
* Page cache is deliberately NOT part of it when the split IS known: lowering
|
|
127
|
+
* `MemoryMax` under clean file pages makes the kernel reclaim them, not kill
|
|
128
|
+
* anything, so counting them would inflate every ceiling by whatever the
|
|
129
|
+
* machine happened to have cached (measured: +87 % on this host) — and the
|
|
130
|
+
* floor is applied AFTER the 85 %-of-total cap, so that inflation walks the
|
|
131
|
+
* ceiling straight past the cap this policy exists to enforce.
|
|
132
|
+
*/
|
|
133
|
+
ownFloorBytes?: number;
|
|
134
|
+
/**
|
|
135
|
+
* The same reading for `devbridge-sessions.slice`, and the reason it is a
|
|
136
|
+
* separate number rather than part of the one above.
|
|
137
|
+
*
|
|
138
|
+
* Since 0.54.0 the agents live in that slice, NOT in the service's cgroup, and
|
|
139
|
+
* the ceiling this policy computes is written to both units. So both cgroups
|
|
140
|
+
* are invisible to `MemAvailable` and both have to survive the write — a
|
|
141
|
+
* policy that measures only the daemon computes a ceiling from a machine it
|
|
142
|
+
* cannot see and then applies it to one it can kill (QA-2026-09-07 BLOCKER-1:
|
|
143
|
+
* the hourly re-measure wrote 2 GiB onto a slice holding 6 GiB, with
|
|
144
|
+
* `MemorySwapMax=0`, which is every session on the machine).
|
|
145
|
+
*
|
|
146
|
+
* 0 is the honest answer on a machine where the cage never took: there the
|
|
147
|
+
* sessions are still inside the service's cgroup and `ownUsageBytes` already
|
|
148
|
+
* counts them.
|
|
149
|
+
*/
|
|
150
|
+
sessionsUsageBytes: number;
|
|
151
|
+
/** The same distinction as {@link MemoryFacts.ownFloorBytes}, for the slice. */
|
|
152
|
+
sessionsFloorBytes?: number;
|
|
79
153
|
/** Seconds since boot. Below `BOOT_SETTLE_SEC` the measurement is a lie. */
|
|
80
154
|
uptimeSec: number;
|
|
81
155
|
}
|
|
@@ -102,6 +176,8 @@ export interface MemoryPolicy {
|
|
|
102
176
|
* produces a ceiling that protects nothing.
|
|
103
177
|
*/
|
|
104
178
|
export declare const BOOT_SETTLE_SEC = 600;
|
|
179
|
+
/** What the ceiling about to be written has to clear, in either cgroup. */
|
|
180
|
+
export declare function managedUsageFloorBytes(facts: MemoryFacts): number;
|
|
105
181
|
/**
|
|
106
182
|
* The two numbers, and the incident that decides them.
|
|
107
183
|
*
|
|
@@ -122,8 +198,10 @@ export declare const BOOT_SETTLE_SEC = 600;
|
|
|
122
198
|
*
|
|
123
199
|
* So the percentage has to be of what the machine can SPARE, not of what it has:
|
|
124
200
|
*
|
|
125
|
-
* headroom = MemAvailable +
|
|
126
|
-
* rewrite would walk the ceiling down by what we already hold
|
|
201
|
+
* headroom = MemAvailable + everything WE hold (ours is added back, or every
|
|
202
|
+
* rewrite would walk the ceiling down by what we already hold —
|
|
203
|
+
* and since 0.54.0 «ours» is the daemon's cgroup PLUS the sessions
|
|
204
|
+
* slice, because the agents moved out of the daemon's one)
|
|
127
205
|
* MemoryMax = headroom − reserve
|
|
128
206
|
* MemoryHigh = 80 % of MemoryMax (reclaim and throttle first, kill last)
|
|
129
207
|
*
|
|
@@ -143,6 +221,7 @@ export declare const BOOT_SETTLE_SEC = 600;
|
|
|
143
221
|
* machine, and a ceiling of «everything» is the bug this function exists to fix.
|
|
144
222
|
*/
|
|
145
223
|
export declare function memoryPolicy(facts: MemoryFacts, minCeilingBytes?: number): MemoryPolicy;
|
|
224
|
+
export declare function readOwnCgroupMemory(): CgroupMemory | null;
|
|
146
225
|
/**
|
|
147
226
|
* What the cgroup holds that `MemAvailable` has NOT already counted.
|
|
148
227
|
*
|
|
@@ -154,17 +233,92 @@ export declare function memoryPolicy(facts: MemoryFacts, minCeilingBytes?: numbe
|
|
|
154
233
|
* precisely when memory is tightest. Subtracting `file` keeps the part we really
|
|
155
234
|
* do hold and cannot give back on demand.
|
|
156
235
|
*/
|
|
236
|
+
export interface CgroupMemory {
|
|
237
|
+
/** Everything the cgroup holds, page cache included. */
|
|
238
|
+
currentBytes: number;
|
|
239
|
+
/**
|
|
240
|
+
* The part of it a lower ceiling could not reclaim its way out of, or null
|
|
241
|
+
* when only the total is known — systemd answered `MemoryCurrent` but the
|
|
242
|
+
* cgroup's own files could not be read. Null is «unknown», never «zero»:
|
|
243
|
+
* the two lead to different ceilings, and only one of them is safe.
|
|
244
|
+
*/
|
|
245
|
+
unreclaimableBytes: number | null;
|
|
246
|
+
}
|
|
247
|
+
export declare function readCgroupMemory(dir: string): CgroupMemory | null;
|
|
157
248
|
export declare function readCgroupUnreclaimable(dir: string): number | null;
|
|
249
|
+
/**
|
|
250
|
+
* Where a slice unit's cgroup lives, worked out from the cgroup we are in.
|
|
251
|
+
*
|
|
252
|
+
* systemd's dash rule spells the hierarchy out: `devbridge-sessions.slice` sits
|
|
253
|
+
* inside `devbridge.slice`, which sits directly under the user manager's own
|
|
254
|
+
* cgroup — verified on this host with a throwaway `--slice=dbqa-probe-sub.slice`,
|
|
255
|
+
* which landed in `user@0.service/dbqa.slice/dbqa-probe.slice/dbqa-probe-sub.slice`.
|
|
256
|
+
*
|
|
257
|
+
* The user manager's cgroup is found rather than assembled: the runner runs as
|
|
258
|
+
* root and as a dedicated user (`user@0.service`, `user@1001.service`), and the
|
|
259
|
+
* same guess-the-path mistake that `readOwnCgroupUsage` avoids would silently
|
|
260
|
+
* return 0 here — which is exactly the blindness BLOCKER-1 was.
|
|
261
|
+
*
|
|
262
|
+
* Null means «there is no user manager above us», and then there is no
|
|
263
|
+
* `--user` slice for the sessions to be in either.
|
|
264
|
+
*/
|
|
265
|
+
export declare function sliceCgroupPath(selfCgroup: string, slice: string): string | null;
|
|
266
|
+
/**
|
|
267
|
+
* What the agents are holding right now, outside the daemon's own cgroup — or
|
|
268
|
+
* null where this machine cannot say.
|
|
269
|
+
*
|
|
270
|
+
* Read from the filesystem rather than through `systemctl show`, because this is
|
|
271
|
+
* the hourly path inside the daemon and it has to stay synchronous — the same
|
|
272
|
+
* reason `readOwnCgroupUsage` reads `/proc`. Callers that are not the daemon can
|
|
273
|
+
* pass the number in; see {@link readMemoryFacts}.
|
|
274
|
+
*
|
|
275
|
+
* A missing directory is 0 and not «unknown»: systemd removes the cgroup of an
|
|
276
|
+
* empty slice, so «no directory» means «no session is holding anything». The two
|
|
277
|
+
* are kept apart because they now lead to opposite decisions — 0 lets a blind
|
|
278
|
+
* ceiling be written onto the slice, «unknown» forbids it (see
|
|
279
|
+
* {@link buildSessionsSliceOverride}).
|
|
280
|
+
*/
|
|
281
|
+
export declare function readSessionsSliceMemory(): CgroupMemory | null;
|
|
282
|
+
/**
|
|
283
|
+
* The same tri-state, read out of `systemctl show` instead of the filesystem —
|
|
284
|
+
* the authoritative source for the paths a person types (`doctor --fix`,
|
|
285
|
+
* `install-service`), which run outside the daemon's cgroup.
|
|
286
|
+
*
|
|
287
|
+
* `[not set]` is the ambiguous answer and the reason `activeState` is asked for
|
|
288
|
+
* as well: systemd prints it for a slice that has no cgroup (nothing has ever
|
|
289
|
+
* run there) AND for one whose accounting is off, and those two must not lead to
|
|
290
|
+
* the same decision. Only «systemd loaded the unit and it is not even active» is
|
|
291
|
+
* a positive statement that nothing can be killed by what we write; everything
|
|
292
|
+
* else is «unknown», which writes no ceiling at all.
|
|
293
|
+
*
|
|
294
|
+
* A failed `systemctl` call is null on both counts — including the 10-second
|
|
295
|
+
* timeout, which fires exactly on the overloaded machine this policy protects.
|
|
296
|
+
*/
|
|
297
|
+
export declare function parseSliceUsage(memoryCurrent: string | null, activeState: string | null): number | null;
|
|
298
|
+
/**
|
|
299
|
+
* The slice's number for the callers that take one, and «unknown» kept apart
|
|
300
|
+
* from «empty»: an unreadable live slice must never be replaced with zero.
|
|
301
|
+
*
|
|
302
|
+
* The FLOOR reading, not the headroom one — this feeds a ceiling that has to
|
|
303
|
+
* clear what the slice holds, and where the split is unknown the whole reading
|
|
304
|
+
* has to be assumed unreclaimable.
|
|
305
|
+
*/
|
|
306
|
+
export declare function readSessionsSliceUsageOrNull(): number | null;
|
|
158
307
|
/**
|
|
159
308
|
* Everything `memoryPolicy` needs, straight off this machine.
|
|
160
309
|
*
|
|
161
|
-
*
|
|
162
|
-
* `install-service` run in the operator's own
|
|
163
|
-
* service's usage from `/proc/self
|
|
164
|
-
*
|
|
165
|
-
*
|
|
310
|
+
* Both readings are passed in by callers that are not the daemon — `doctor` and
|
|
311
|
+
* `install-service` run in the operator's own `session-N.scope` and cannot read
|
|
312
|
+
* the service's usage from `/proc/self`; they ask systemd instead
|
|
313
|
+
* (`readMemoryFactsFromSystemd`). Returns null when either cgroup is unknowable,
|
|
314
|
+
* because guessing there is the one dangerous direction: it removes the floor
|
|
315
|
+
* that stops a live session from being killed on the next `daemon-reload`.
|
|
316
|
+
*
|
|
317
|
+
* The default reads both from the filesystem, which is right for the daemon:
|
|
318
|
+
* its own cgroup through `/proc/self`, and the sessions slice through the user
|
|
319
|
+
* manager's cgroup, which sits above the daemon and the CLI alike.
|
|
166
320
|
*/
|
|
167
|
-
export declare function readMemoryFacts(
|
|
321
|
+
export declare function readMemoryFacts(own?: CgroupMemory | null, sessions?: CgroupMemory | null): MemoryFacts | null;
|
|
168
322
|
/**
|
|
169
323
|
* `CPUQuota` worth keeping: enough headroom that a runaway build cannot make the
|
|
170
324
|
* box unreachable, but never so little that ordinary work is throttled.
|
|
@@ -176,6 +330,74 @@ export declare function readMemoryFacts(ownUsageBytes?: number | null): MemoryFa
|
|
|
176
330
|
*/
|
|
177
331
|
export declare function cpuQuotaPercent(cpuCount?: number): number | null;
|
|
178
332
|
export declare function buildLimitsOverride(cpuCount?: number, facts?: MemoryFacts | null): string;
|
|
333
|
+
/**
|
|
334
|
+
* The ceiling over ALL sessions, on the slice they were moved into.
|
|
335
|
+
*
|
|
336
|
+
* Same number as the service's, and deliberately so — but it is a COPY, not a
|
|
337
|
+
* move, and that is deliberate too. The plan asked for the ceiling to be carried
|
|
338
|
+
* across; shrinking the service's to «what the daemon alone needs» would be
|
|
339
|
+
* correct only on a machine where the cage actually took. On cgroup v1, without
|
|
340
|
+
* a user bus, under a foreign supervisor — every `nice-only` machine — the
|
|
341
|
+
* sessions are still CHILDREN of the service, and a service ceiling sized for
|
|
342
|
+
* the daemon would cap all of them at a few hundred MB. This file is written
|
|
343
|
+
* before anything has probed which of the two machines this is, so the safe
|
|
344
|
+
* shape is the same number twice: on a caged machine the slice is the ceiling
|
|
345
|
+
* that binds, on an uncaged one the service is, and neither machine is ever
|
|
346
|
+
* left with a ceiling that is too small for what is under it.
|
|
347
|
+
*
|
|
348
|
+
* The price is that a caged machine formally permits `service + slice`. It is
|
|
349
|
+
* not the guarantee `memoryPolicy` computes, and it is written down here rather
|
|
350
|
+
* than glossed over (QA-2026-09-07 MINOR-4).
|
|
351
|
+
*
|
|
352
|
+
* `MemorySwapMax=0` for the same reason it is on every scope: a ceiling on
|
|
353
|
+
* resident memory alone is not a ceiling, it is a swap pump (a 200 MB cage
|
|
354
|
+
* allocated 2 GB and drained the host's swap during the spike).
|
|
355
|
+
*
|
|
356
|
+
* No `MemoryHigh` here either. Soft pressure on the slice would throttle every
|
|
357
|
+
* session on the machine to keep one runaway alive a little longer — the exact
|
|
358
|
+
* trade the spike measured and rejected: 60 seconds of delays instead of a
|
|
359
|
+
* 380 ms honest death.
|
|
360
|
+
*
|
|
361
|
+
* `sessionsUsageBytes` is the door that used to lead around all of the above.
|
|
362
|
+
* `facts` is null whenever the SERVICE's `MemoryCurrent` is unreadable — a
|
|
363
|
+
* stopped service, or a runner under a foreign supervisor — and this file then
|
|
364
|
+
* fell back to a flat `MemoryMax=55%`. But the sessions are SIBLINGS of the
|
|
365
|
+
* service, not its children: they survive `systemctl --user stop
|
|
366
|
+
* devbridge-runner`, so «the service is not running» says nothing at all about
|
|
367
|
+
* what the slice is holding, and `doctor --fix` on such a machine wrote 55 % of
|
|
368
|
+
* total onto a live slice and then called `daemon-reload` — the collective kill
|
|
369
|
+
* of QA-2026-09-07 BLOCKER-1 arriving through a door with no policy behind it.
|
|
370
|
+
*
|
|
371
|
+
* So the slice's own usage is read separately, and the promise made on
|
|
372
|
+
* `buildLimitsOverride` («the floor belongs to the policy, so every caller gets
|
|
373
|
+
* it and none can opt out») holds on this path too:
|
|
374
|
+
* - a number → the ceiling clears it by the same 1.25 the policy uses, and
|
|
375
|
+
* never drops below what one agent needs;
|
|
376
|
+
* - 0 → nothing is running there, so the blind fraction can kill
|
|
377
|
+
* nothing and stays;
|
|
378
|
+
* - null → this machine could not say, and NO ceiling is written at all.
|
|
379
|
+
* Leaving whatever is in force in force is strictly better than
|
|
380
|
+
* applying an unfounded number to a cgroup that may be full: the
|
|
381
|
+
* daemon rewrites the file with a measured ceiling the moment it
|
|
382
|
+
* can measure one (the drift check treats a file with no
|
|
383
|
+
* `MemoryMax` as outdated).
|
|
384
|
+
*/
|
|
385
|
+
export declare function buildSessionsSliceOverride(facts?: MemoryFacts | null, sessionsUsageBytes?: number | null): string;
|
|
386
|
+
/**
|
|
387
|
+
* The CPU share of everything the agents run, against the daemon's own.
|
|
388
|
+
*
|
|
389
|
+
* `nice(2)` orders tasks INSIDE one cgroup. The moment a session gets a scope of
|
|
390
|
+
* its own it is no longer inside the service's cgroup, and the split between the
|
|
391
|
+
* two is decided by `cpu.weight` — which is 100 everywhere by default,
|
|
392
|
+
* `app.slice` (where the service lives) included. Without this file the cage
|
|
393
|
+
* would silently undo stage 1a and hand back the failure of 16.08: the daemon
|
|
394
|
+
* starved by its own children, four missed heartbeats, the server Offline and
|
|
395
|
+
* 504 on every session. 50 against 100 leaves the daemon two thirds.
|
|
396
|
+
*
|
|
397
|
+
* `process-priority.ts` stays exactly as it is: it is what protects the daemon
|
|
398
|
+
* on cgroup v1 and on every machine where the cage does not apply.
|
|
399
|
+
*/
|
|
400
|
+
export declare function buildDevbridgeSliceOverride(): string;
|
|
179
401
|
/**
|
|
180
402
|
* Is the shipped resource policy missing or from an older runner?
|
|
181
403
|
*
|
|
@@ -195,7 +417,7 @@ export declare function limitsOverrideIsOutdated(readFile?: (p: string) => strin
|
|
|
195
417
|
* arrive at the same number — otherwise a write whose floor was binding would be
|
|
196
418
|
* seen as drifted on the very next call and rewritten forever.
|
|
197
419
|
*/
|
|
198
|
-
export declare function writeLimitsOverride(force?: boolean, home?: string, facts?: MemoryFacts | null): boolean;
|
|
420
|
+
export declare function writeLimitsOverride(force?: boolean, home?: string, facts?: MemoryFacts | null, sessionsUsageBytes?: number | null): boolean;
|
|
199
421
|
/**
|
|
200
422
|
* Does the installed unit point at something that no longer exists?
|
|
201
423
|
*
|