@bridge4dev/runner 0.52.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 +111 -33
- package/dist/adapters/codex-protocol.d.ts +11 -0
- package/dist/adapters/codex-protocol.js +41 -3
- package/dist/adapters/codex.js +14 -26
- package/dist/adapters/types.d.ts +45 -1
- package/dist/adapters/types.js +53 -0
- package/dist/checkpoints.d.ts +62 -0
- package/dist/checkpoints.js +50 -1
- package/dist/git.d.ts +64 -0
- package/dist/git.js +487 -36
- package/dist/gitops.d.ts +5 -0
- package/dist/gitops.js +7 -8
- package/dist/host-load.d.ts +156 -0
- package/dist/host-load.js +223 -0
- package/dist/index.js +190 -40
- package/dist/policy.d.ts +38 -0
- package/dist/policy.js +228 -7
- package/dist/process-priority.d.ts +55 -0
- package/dist/process-priority.js +99 -0
- package/dist/protocol.d.ts +42 -20
- 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 +156 -3
- package/dist/supervisor.js +351 -32
- 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
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { systemdUserEnv } from './environment.js';
|
|
6
|
+
import { readCgroupMemory, readMemoryFacts, readOwnCgroupMemory, readSessionsSliceMemory, SERVICE_NAME, SESSIONS_SLICE, } from './service-unit.js';
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
function bytes(raw) {
|
|
9
|
+
if (raw === undefined || !/^\d+$/.test(raw.trim()))
|
|
10
|
+
return null;
|
|
11
|
+
const value = Number(raw);
|
|
12
|
+
// systemd prints UINT64_MAX for «not set» on some versions, which is above
|
|
13
|
+
// `MAX_SAFE_INTEGER` and would otherwise arrive as a plausible byte count.
|
|
14
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Read the unit's group, never the CLI's own `/proc/self` group. A missing
|
|
18
|
+
* reading is not an empty service: a timed-out user bus can belong to a busy
|
|
19
|
+
* machine. Raw `memory.current` deliberately survives beside the split, so the
|
|
20
|
+
* safety floor never depends on an estimate of how much can be reclaimed.
|
|
21
|
+
*/
|
|
22
|
+
export function unitMemoryReading(output, cgroupRoot = '/sys/fs/cgroup') {
|
|
23
|
+
const properties = new Map(output.split('\n').map((line) => {
|
|
24
|
+
const separator = line.indexOf('=');
|
|
25
|
+
return [line.slice(0, separator), line.slice(separator + 1).trim()];
|
|
26
|
+
}));
|
|
27
|
+
// An empty answer is not a fact about the unit: everything below reads meaning
|
|
28
|
+
// into what systemd said, so there has to have been an answer first. A dead
|
|
29
|
+
// bus prints nothing and stays «unknown». `ControlGroup` is not required to be
|
|
30
|
+
// PRESENT — only to be empty where it decides emptiness below — so a systemd
|
|
31
|
+
// that does not print it leaves «unknown» too, and the filesystem reader
|
|
32
|
+
// behind this one covers that machine.
|
|
33
|
+
if (!properties.has('ActiveState'))
|
|
34
|
+
return null;
|
|
35
|
+
const current = bytes(properties.get('MemoryCurrent'));
|
|
36
|
+
const group = properties.get('ControlGroup');
|
|
37
|
+
if (group !== undefined && group.startsWith('/') && group !== '/') {
|
|
38
|
+
const root = path.resolve(cgroupRoot);
|
|
39
|
+
const directory = path.resolve(root, `.${group}`);
|
|
40
|
+
if (directory.startsWith(`${root}${path.sep}`)) {
|
|
41
|
+
try {
|
|
42
|
+
const measured = readCgroupMemory(directory);
|
|
43
|
+
if (measured !== null)
|
|
44
|
+
return measured;
|
|
45
|
+
const raw = bytes(fs.readFileSync(path.join(directory, 'memory.current'), 'utf8'));
|
|
46
|
+
// Without `memory.stat` the split between «cache we can give back» and
|
|
47
|
+
// «memory that has to be killed for» is unknown, and null says so: the
|
|
48
|
+
// floor then assumes all of it, the headroom none of it.
|
|
49
|
+
if (raw !== null)
|
|
50
|
+
return { currentBytes: raw, unreclaimableBytes: null };
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Unsupported controller, removed cgroup or unreadable file: unknown.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (current !== null)
|
|
58
|
+
return { currentBytes: current, unreclaimableBytes: null };
|
|
59
|
+
// A unit with NO cgroup holds nothing a new ceiling could kill, whatever its
|
|
60
|
+
// ActiveState says — and that is the whole point of not asking for
|
|
61
|
+
// `inactive` here. `failed` and `activating (auto-restart)` report exactly
|
|
62
|
+
// the shape `inactive` does (verified on systemd 255: `ControlGroup=`,
|
|
63
|
+
// `MemoryCurrent=[not set]`), and those are precisely the states a machine is
|
|
64
|
+
// in when someone runs `install.sh --repair` or `doctor --fix` on it. Reading
|
|
65
|
+
// them as «unknown» turned the cure into a refusal on the only machines that
|
|
66
|
+
// need it.
|
|
67
|
+
return group === '' ? { currentBytes: 0, unreclaimableBytes: 0 } : null;
|
|
68
|
+
}
|
|
69
|
+
async function readUnitUsage(unit) {
|
|
70
|
+
try {
|
|
71
|
+
const { stdout } = await execFileAsync('systemctl', ['--user', 'show', unit, '-p', 'MemoryCurrent', '-p', 'ActiveState', '-p', 'ControlGroup'], { timeout: 10_000, env: systemdUserEnv() });
|
|
72
|
+
return unitMemoryReading(stdout);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Both units, measured through systemd, with the filesystem underneath.
|
|
80
|
+
*
|
|
81
|
+
* One helper so `doctor`, `doctor --fix`, `install-service` and the daemon's
|
|
82
|
+
* hourly re-measure cannot drift apart in what they measure — which is how the
|
|
83
|
+
* CLI once computed a ceiling 34 % away from the daemon's and the two rewrote
|
|
84
|
+
* the file forever.
|
|
85
|
+
*
|
|
86
|
+
* The fallback is not a second opinion, it is the same reading taken another
|
|
87
|
+
* way, and it is safe for every caller precisely because each reader
|
|
88
|
+
* self-identifies: `readOwnCgroupMemory` hands back null for any process that
|
|
89
|
+
* is not the service itself, so the daemon measures itself when the bus is
|
|
90
|
+
* unhappy and a CLI never mistakes its own `session-N.scope` for the service.
|
|
91
|
+
* Without it, one slow `systemctl` meant the daemon wrote no policy at all —
|
|
92
|
+
* for an hour, on the overloaded machine this policy exists to protect.
|
|
93
|
+
*/
|
|
94
|
+
export async function readMemoryFactsFromSystemd() {
|
|
95
|
+
const [service, sessions] = await Promise.all([
|
|
96
|
+
readUnitUsage(SERVICE_NAME),
|
|
97
|
+
readUnitUsage(SESSIONS_SLICE),
|
|
98
|
+
]);
|
|
99
|
+
return memoryReadings(service, sessions);
|
|
100
|
+
}
|
|
101
|
+
/** The half of the above that has no bus in it, so it can be tested. */
|
|
102
|
+
export function memoryReadings(service, sessions, fromFilesystem = { own: readOwnCgroupMemory, sessions: readSessionsSliceMemory }) {
|
|
103
|
+
const serviceMemory = service ?? fromFilesystem.own();
|
|
104
|
+
const sessionsMemory = sessions ?? fromFilesystem.sessions();
|
|
105
|
+
return {
|
|
106
|
+
facts: readMemoryFacts(serviceMemory, sessionsMemory),
|
|
107
|
+
// The floor reading, and «unknown» kept apart from «empty»: this number is
|
|
108
|
+
// the only thing standing between a live slice and the ceiling written by
|
|
109
|
+
// a command typed on a machine whose service cannot be measured.
|
|
110
|
+
sessionsUsageBytes: sessionsMemory === null
|
|
111
|
+
? null
|
|
112
|
+
: (sessionsMemory.unreclaimableBytes ?? sessionsMemory.currentBytes),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=systemd-memory.js.map
|
package/dist/verify.js
CHANGED
|
@@ -5,6 +5,8 @@ import { promisify } from 'node:util';
|
|
|
5
5
|
import { log } from './log.js';
|
|
6
6
|
import { maskString } from './policy.js';
|
|
7
7
|
import { evaluateRecipeCommand } from './policy.js';
|
|
8
|
+
import { lowerPriority } from './process-priority.js';
|
|
9
|
+
import { cageSpawn, releaseSessionScope } from './session-cage.js';
|
|
8
10
|
import { recipeFingerprint } from './recipe.js';
|
|
9
11
|
import { StringDecoder } from 'node:string_decoder';
|
|
10
12
|
import { stateDir } from './paths.js';
|
|
@@ -301,6 +303,7 @@ export class VerifyRunner {
|
|
|
301
303
|
logPath,
|
|
302
304
|
logBytes: 0,
|
|
303
305
|
child: null,
|
|
306
|
+
scopeUnit: null,
|
|
304
307
|
cancelled: false,
|
|
305
308
|
branch: input.branch,
|
|
306
309
|
commitSha: input.commitSha,
|
|
@@ -540,6 +543,11 @@ export class VerifyRunner {
|
|
|
540
543
|
settled = true;
|
|
541
544
|
clearTimeout(timer);
|
|
542
545
|
run.child = null;
|
|
546
|
+
// Read why the scope ended and let systemd forget it, in that order —
|
|
547
|
+
// a scope the OOM killer took stays in `failed` and would refuse the
|
|
548
|
+
// very same unit name to the next step (`session-cage.ts`).
|
|
549
|
+
void releaseSessionScope(run.scopeUnit, `verify-${run.runId}`);
|
|
550
|
+
run.scopeUnit = null;
|
|
543
551
|
resolve({
|
|
544
552
|
step: run.currentStep ?? '',
|
|
545
553
|
durationMs: this.now() - startedAt,
|
|
@@ -547,14 +555,26 @@ export class VerifyRunner {
|
|
|
547
555
|
...(timedOut ? { timedOut: true } : {}),
|
|
548
556
|
});
|
|
549
557
|
};
|
|
558
|
+
// A verification step is the heaviest thing on the machine that has an id
|
|
559
|
+
// of its own, so it gets a cage of its own too — keyed by the run rather
|
|
560
|
+
// than by a session, because that is the identity a verify run has. The
|
|
561
|
+
// scope survives `detached: true`: `systemd-run --scope` execs in the
|
|
562
|
+
// same pid, so `setsid` still makes the child its own group leader and
|
|
563
|
+
// `process.kill(-pid)` below still reaches the compilers.
|
|
564
|
+
const caged = cageSpawn({
|
|
565
|
+
id: `verify-${run.runId}`,
|
|
566
|
+
command: '/bin/sh',
|
|
567
|
+
args: ['-c', step.run],
|
|
568
|
+
});
|
|
569
|
+
run.scopeUnit = caged.unit;
|
|
550
570
|
let child;
|
|
551
571
|
try {
|
|
552
572
|
// `detached` so the whole process tree gets the signal: a build script
|
|
553
573
|
// is a shell that spawns compilers, and killing only the shell leaves
|
|
554
574
|
// them running with the disk and the CPU.
|
|
555
|
-
child = spawn(
|
|
575
|
+
child = spawn(caged.command, caged.args, {
|
|
556
576
|
cwd: run.cwd,
|
|
557
|
-
env: buildEnv(step.env),
|
|
577
|
+
env: { ...buildEnv(step.env), ...caged.env },
|
|
558
578
|
detached: true,
|
|
559
579
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
560
580
|
});
|
|
@@ -565,6 +585,12 @@ export class VerifyRunner {
|
|
|
565
585
|
return;
|
|
566
586
|
}
|
|
567
587
|
run.child = child;
|
|
588
|
+
// A recipe step is the heaviest thing the runner starts on its own — a
|
|
589
|
+
// full build or a full test run — and the one with nobody waiting on a
|
|
590
|
+
// keystroke. It goes behind the daemon and level with the agent sessions;
|
|
591
|
+
// the whole process group inherits it, which is the point, because
|
|
592
|
+
// `detached` means the compilers are down there and not here.
|
|
593
|
+
lowerPriority(child.pid);
|
|
568
594
|
// A StringDecoder per stream, not `buffer.toString('utf8')` per chunk.
|
|
569
595
|
//
|
|
570
596
|
// A pipe read ends wherever the kernel filled the buffer, which is
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const RUNNER_VERSION = "0.
|
|
1
|
+
export declare const RUNNER_VERSION = "0.54.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
package/package.json
CHANGED