@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.
- package/dist/adapters/claude.js +23 -4
- package/dist/adapters/codex-protocol.js +6 -1
- package/dist/adapters/codex.js +11 -2
- package/dist/adapters/questions.d.ts +15 -0
- package/dist/adapters/questions.js +32 -0
- package/dist/adapters/types.js +14 -0
- package/dist/cage-authority.d.ts +118 -0
- package/dist/cage-authority.js +241 -0
- package/dist/config.d.ts +83 -5
- package/dist/config.js +59 -1
- package/dist/daemon-lock.d.ts +43 -0
- package/dist/daemon-lock.js +107 -0
- package/dist/host-load.d.ts +9 -0
- package/dist/host-load.js +9 -0
- package/dist/index.js +222 -20
- package/dist/policy.d.ts +9 -0
- package/dist/policy.js +68 -0
- package/dist/protocol.d.ts +43 -27
- package/dist/recipe-schema.d.ts +12 -12
- package/dist/self-update.js +22 -1
- package/dist/service-unit.d.ts +35 -3
- package/dist/service-unit.js +82 -5
- package/dist/session-allocator.d.ts +259 -0
- package/dist/session-allocator.js +492 -0
- package/dist/session-cage.d.ts +229 -2
- package/dist/session-cage.js +590 -40
- package/dist/session-limits.d.ts +71 -0
- package/dist/session-limits.js +93 -0
- package/dist/session-stall.d.ts +353 -0
- package/dist/session-stall.js +760 -0
- package/dist/supervisor.d.ts +235 -33
- package/dist/supervisor.js +1178 -265
- package/dist/systemd-memory.js +2 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much memory each live session may use, decided from what is actually free
|
|
3
|
+
* (#398 S3, ticket #398).
|
|
4
|
+
*
|
|
5
|
+
* The number this replaces was `max(pot / 3, 2 GiB)`, computed ONCE at daemon
|
|
6
|
+
* start and never again. The divisor came from `maxSessions` defaulting to three
|
|
7
|
+
* and from nothing else, so a lone session on an idle machine got a third of the
|
|
8
|
+
* pot and the other two thirds sat unused — the owner's complaint in #398, word
|
|
9
|
+
* for word: «ночью машина свободна ~9 ГБ, а сессии всё равно нельзя выйти за
|
|
10
|
+
* 2.5 ГБ».
|
|
11
|
+
*
|
|
12
|
+
* **The decisive idea is three levels, and it has to be understood before any of
|
|
13
|
+
* the arithmetic below.** Today a session has two numbers, the brake and the
|
|
14
|
+
* wall, and both of the jobs — «do not crowd your neighbour» and «do not kill
|
|
15
|
+
* the machine» — are being asked of them at once. That is unsatisfiable: for a
|
|
16
|
+
* lone session to take nearly the whole pot its brake must be nearly the whole
|
|
17
|
+
* pot, and then two such sessions overflow the pot between them without either
|
|
18
|
+
* being braked — and what overflows is the SLICE, where the victim is chosen by
|
|
19
|
+
* the kernel and the one that dies is not the one at fault (measured, #387).
|
|
20
|
+
*
|
|
21
|
+
* So there are three, the way every grown-up scheduler has three:
|
|
22
|
+
*
|
|
23
|
+
* - **the guarantee** (`MemoryLow`) — small and untouchable. The sum of the
|
|
24
|
+
* guarantees has to fit in the pot, and this is the ONLY place a sum is
|
|
25
|
+
* checked. It answers «how much will not be taken from me».
|
|
26
|
+
* - **the brake** (`MemoryHigh`) — large and DELIBERATELY oversold. Every
|
|
27
|
+
* session may grow into everything that is free right now. The sum of the
|
|
28
|
+
* brakes is greater than the pot, and that is not a mistake.
|
|
29
|
+
* - **the collective brake on the slice** (`MemoryHigh` on
|
|
30
|
+
* `devbridge-sessions.slice`, `service-unit.ts`) — the thing that makes the
|
|
31
|
+
* overselling safe. As the sessions together approach the pot the kernel
|
|
32
|
+
* slows all of them and reclaims first from whoever is above their guarantee,
|
|
33
|
+
* so the hard ceiling — where the OOM killer picks — is never reached.
|
|
34
|
+
* - **the wall** (`MemoryMax`) — still one guarantee below the pot, so a lone
|
|
35
|
+
* runaway meets ITS OWN wall before the shared pot overflows (rule of #387).
|
|
36
|
+
*
|
|
37
|
+
* Overselling the brakes WITHOUT the collective brake on the slice is a bug, not
|
|
38
|
+
* a solution. The two halves are one change, and {@link allocateSessionMemory}
|
|
39
|
+
* refuses to oversell when the caller reports that the slice has no live
|
|
40
|
+
* `MemoryHigh` — see {@link AllocatorMachine.collectiveBrake}.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* What one session is guaranteed and will not have taken away.
|
|
44
|
+
*
|
|
45
|
+
* 2 GiB, sized over the 1571 MB peak measured for a workspace `pnpm typecheck`
|
|
46
|
+
* in this monorepo. A dev server that cannot run one of those is not a dev
|
|
47
|
+
* server, so this is the number below which a session is not worth starting.
|
|
48
|
+
*/
|
|
49
|
+
export declare const SESSION_GUARANTEE_BYTES: number;
|
|
50
|
+
/**
|
|
51
|
+
* Growth allowance over what a session already holds.
|
|
52
|
+
*
|
|
53
|
+
* The same 1.25 `service-unit.ts` uses for the ceiling it writes over a cgroup's
|
|
54
|
+
* current usage, and for the same reason: a limit at exactly what is held is a
|
|
55
|
+
* limit that is already breached.
|
|
56
|
+
*/
|
|
57
|
+
export declare const SESSION_HEADROOM_OVER_HOLD = 1.25;
|
|
58
|
+
/**
|
|
59
|
+
* The floor under the BRAKE, over what a session already holds — 1.05, and it
|
|
60
|
+
* is a different number from the one above on purpose (#398 §8, #403).
|
|
61
|
+
*
|
|
62
|
+
* The wall may sit a quarter above the hold: it is a hard limit, and headroom
|
|
63
|
+
* there costs nothing. The brake may not, because it is followed every tick: at
|
|
64
|
+
* 1.25 a session that merely holds its ground has its brake RAISED on every
|
|
65
|
+
* pass, `addMemoryTo` reads that as «the machine found it more memory», and the
|
|
66
|
+
* stall deadline resets for ever — measured in the suite, four tests waiting
|
|
67
|
+
* for a stop that could no longer come.
|
|
68
|
+
*
|
|
69
|
+
* 1.05 is enough for the one thing this floor is for: `memory.current` counts
|
|
70
|
+
* page cache, so a brake at exactly the anonymous hold is already breached.
|
|
71
|
+
*/
|
|
72
|
+
export declare const SESSION_BRAKE_OVER_HOLD = 1.05;
|
|
73
|
+
/**
|
|
74
|
+
* The step the DIVIDED guarantee is rounded down to — 64 MiB (#403).
|
|
75
|
+
*
|
|
76
|
+
* `MemoryLow` is the one number here with no dead zone of its own, on purpose:
|
|
77
|
+
* a guarantee is what a person is promised, and a promise that lags a tick
|
|
78
|
+
* behind the machine is worse than one that follows it. But the divided
|
|
79
|
+
* guarantee is `pool / n`, and the pool follows `MemAvailable`, which breathes:
|
|
80
|
+
* measured on a hungry machine with two sessions, a pot of 3800 MiB and 11 MiB
|
|
81
|
+
* of breathing wrote `MemoryLow` on ten ticks out of ten where the code before
|
|
82
|
+
* these fixes wrote it on one.
|
|
83
|
+
*
|
|
84
|
+
* Rounding down rather than to nearest, because the sum of the guarantees is
|
|
85
|
+
* the one sum in this module that has to fit inside the pot.
|
|
86
|
+
*/
|
|
87
|
+
export declare const GUARANTEE_STEP_BYTES: number;
|
|
88
|
+
/** Below this pot the machine is honestly single-session — see the formula. */
|
|
89
|
+
export declare function hungryPotBytes(baseBand: number): number;
|
|
90
|
+
/** The band this machine uses before any session-specific widening. */
|
|
91
|
+
export declare function baseBandBytes(oomContinue: boolean): number;
|
|
92
|
+
/** One live session, as the allocator needs to see it. */
|
|
93
|
+
export interface AllocatorSession {
|
|
94
|
+
id: string;
|
|
95
|
+
/**
|
|
96
|
+
* `memory.current − (file − shmem)`: what the session holds and cannot simply
|
|
97
|
+
* give back. Page cache is excluded because lowering a limit over clean file
|
|
98
|
+
* pages makes the kernel reclaim them, not kill anything — counting them would
|
|
99
|
+
* inflate every number by whatever the machine happened to have cached.
|
|
100
|
+
*/
|
|
101
|
+
holdBytes: number;
|
|
102
|
+
/** `memory.current`, the whole reading — what `MemAvailable` cannot see. */
|
|
103
|
+
currentBytes: number;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* What the machine's OWNER has said, in `config.toml` (#398 S6).
|
|
107
|
+
*
|
|
108
|
+
* Every field optional, and every absent one means «the shipped default». The
|
|
109
|
+
* knobs exist because this formula travels to dev servers nobody here has ever
|
|
110
|
+
* seen: the two previous memory rules were both reasonable on the author's
|
|
111
|
+
* machine and destructive somewhere else, and an owner who has to get work done
|
|
112
|
+
* tonight needs a way back that is not «downgrade the runner».
|
|
113
|
+
*/
|
|
114
|
+
export interface AllocatorKnobs {
|
|
115
|
+
/** `false` — the pre-0.58.0 fixed third, computed once and never moved. */
|
|
116
|
+
adaptive?: boolean;
|
|
117
|
+
/** The guarantee, bytes. Default {@link SESSION_GUARANTEE_BYTES}. */
|
|
118
|
+
guaranteeBytes?: number;
|
|
119
|
+
/** A ceiling on the ceiling, bytes: no session grows past this whatever is free. */
|
|
120
|
+
sessionMaxBytes?: number;
|
|
121
|
+
}
|
|
122
|
+
/** What the machine says about itself right now. */
|
|
123
|
+
export interface AllocatorMachine {
|
|
124
|
+
/** `MemoryMax` systemd has in force on the sessions slice. Never rewritten live. */
|
|
125
|
+
potBytes: number | null;
|
|
126
|
+
/** `MemAvailable`. */
|
|
127
|
+
availableBytes: number;
|
|
128
|
+
/** `MemTotal`. */
|
|
129
|
+
totalBytes: number;
|
|
130
|
+
/** What the machine keeps for itself — `machineReserveBytes(totalBytes)`. */
|
|
131
|
+
reserveBytes: number;
|
|
132
|
+
/** Does a kill inside a scope cost one process or the whole session? */
|
|
133
|
+
oomContinue: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Is there a LIVE `MemoryHigh` on the sessions slice, read back off systemd?
|
|
136
|
+
*
|
|
137
|
+
* The hard invariant of this module. Overselling the brakes is safe only while
|
|
138
|
+
* the collective brake exists; without it, overflowing the slice ends in a
|
|
139
|
+
* kill rather than in throttling, and the one killed is the neighbour sitting
|
|
140
|
+
* under its own limit — measured on this host, victim `memory.events`:
|
|
141
|
+
* `high 0, max 0, oom 0, oom_kill 1`.
|
|
142
|
+
*/
|
|
143
|
+
collectiveBrake: boolean;
|
|
144
|
+
/** Swap one session may push into, as `session-cage.ts` computes it. */
|
|
145
|
+
swapBytes: number;
|
|
146
|
+
}
|
|
147
|
+
/** The three numbers of one session, plus the band they were built with. */
|
|
148
|
+
export interface SessionAllocation {
|
|
149
|
+
id: string;
|
|
150
|
+
guaranteedBytes: number;
|
|
151
|
+
brakeBytes: number;
|
|
152
|
+
wallBytes: number;
|
|
153
|
+
swapBytes: number;
|
|
154
|
+
bandBytes: number;
|
|
155
|
+
/** This session's guarantee does not fit in the pot beside the others (D9). */
|
|
156
|
+
tight: boolean;
|
|
157
|
+
}
|
|
158
|
+
export interface Allocation {
|
|
159
|
+
sessions: SessionAllocation[];
|
|
160
|
+
/** The live pot — what is actually available to sessions right now. */
|
|
161
|
+
poolBytes: number;
|
|
162
|
+
/** What the sessions hold between them. */
|
|
163
|
+
heldBytes: number;
|
|
164
|
+
/** What nobody holds. */
|
|
165
|
+
freeBytes: number;
|
|
166
|
+
/** How many guarantees fit in the live pot. */
|
|
167
|
+
guaranteesFit: number;
|
|
168
|
+
/** The conservative formula was used because the slice has no collective brake. */
|
|
169
|
+
conservative: boolean;
|
|
170
|
+
/** The machine is honestly single-session: the pot is below `2G + band`. */
|
|
171
|
+
hungry: boolean;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The whole decision, as a pure function of the numbers.
|
|
175
|
+
*
|
|
176
|
+
* Kept pure and kept here, away from every timer and socket, because what it has
|
|
177
|
+
* to be is CORRECT on machines nobody has: the formula travels with the runner
|
|
178
|
+
* to every customer's dev server, and the last two times a memory rule shipped
|
|
179
|
+
* it was reasonable on the author's machine and destructive somewhere else
|
|
180
|
+
* (#387: the victim was the neighbour; `service-unit.ts:314`: a 4 GB machine got
|
|
181
|
+
* `MemoryHigh == MemoryMax`). So the fleet's profiles are fixtures, and the
|
|
182
|
+
* eight properties below are tests rather than comments.
|
|
183
|
+
*/
|
|
184
|
+
export declare function allocateSessionMemory(machine: AllocatorMachine, sessions: AllocatorSession[], knobs?: AllocatorKnobs): Allocation;
|
|
185
|
+
/**
|
|
186
|
+
* What was last written to one session's scope, and how long it has been asking
|
|
187
|
+
* to shrink.
|
|
188
|
+
*
|
|
189
|
+
* The asymmetry below is not caution, it is a measurement: `MemoryMax` lowered
|
|
190
|
+
* below what a process holds ends it in seconds (verified on this host — a scope
|
|
191
|
+
* holding 200 MiB, `MemoryMax` set to 64 MiB, the process read «Killed» and the
|
|
192
|
+
* unit went `inactive`). An oversold limit is safe; a killed honest session is
|
|
193
|
+
* not recoverable.
|
|
194
|
+
*/
|
|
195
|
+
export interface WrittenLimits {
|
|
196
|
+
brakeBytes: number;
|
|
197
|
+
wallBytes: number;
|
|
198
|
+
guaranteedBytes: number;
|
|
199
|
+
/**
|
|
200
|
+
* `MemorySwapMax`, which moves only when the MACHINE changes under us.
|
|
201
|
+
*
|
|
202
|
+
* It is here because of a live example: on 09.09.2026 a swap file appeared on
|
|
203
|
+
* vmi3219930 and every session on it went on being told `MemorySwapMax=0`,
|
|
204
|
+
* because the cage measures the machine once at daemon start. Without swap the
|
|
205
|
+
* brake stops instead of slowing, so that machine kept the exact failure this
|
|
206
|
+
* whole plan is about while the means to avoid it sat unused on its disk.
|
|
207
|
+
*/
|
|
208
|
+
swapBytes: number;
|
|
209
|
+
/** Consecutive ticks on which the session's own numbers asked to come down. */
|
|
210
|
+
shrinkTicks: number;
|
|
211
|
+
/**
|
|
212
|
+
* What the session held when these numbers were written.
|
|
213
|
+
*
|
|
214
|
+
* The difference between «the session shrank» and «a neighbour arrived», and
|
|
215
|
+
* without it there was no difference at all: the wall came down whenever the
|
|
216
|
+
* formula wanted less, whatever the reason — which the design and the comment
|
|
217
|
+
* beside it both forbid. Found by the independent review of 10.09.2026.
|
|
218
|
+
*/
|
|
219
|
+
holdBytes: number;
|
|
220
|
+
}
|
|
221
|
+
/** Ticks in a row a session must ask to shrink before its wall comes down. */
|
|
222
|
+
export declare const SHRINK_TICKS_TO_LOWER = 4;
|
|
223
|
+
/**
|
|
224
|
+
* Neither number moves for less than this — `max(256 MiB, 10 %)`.
|
|
225
|
+
*
|
|
226
|
+
* Every write is a `systemctl` call, and the bus was measured at 2.7 s under
|
|
227
|
+
* load. A dead zone is what keeps a pot that breathes by a few megabytes from
|
|
228
|
+
* costing a bus call per session per tick.
|
|
229
|
+
*/
|
|
230
|
+
export declare function deadZoneBytes(value: number): number;
|
|
231
|
+
export interface LimitMove {
|
|
232
|
+
brakeBytes: number;
|
|
233
|
+
wallBytes: number;
|
|
234
|
+
guaranteedBytes: number;
|
|
235
|
+
swapBytes: number;
|
|
236
|
+
holdBytes: number;
|
|
237
|
+
shrinkTicks: number;
|
|
238
|
+
/** Properties to write, empty when nothing moved enough to be worth a call. */
|
|
239
|
+
properties: string[];
|
|
240
|
+
/** The brake came DOWN — the stall detector has to be muted for a while. */
|
|
241
|
+
lowered: boolean;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* What to actually write, given what is already in force.
|
|
245
|
+
*
|
|
246
|
+
* Up is free and immediate: memory nobody is using costs nobody anything.
|
|
247
|
+
* Down is where every rule lives.
|
|
248
|
+
*/
|
|
249
|
+
export declare function planLimitMove(written: WrittenLimits | null, wanted: SessionAllocation, holdBytes: number,
|
|
250
|
+
/**
|
|
251
|
+
* Does this systemd take `MemoryLow=` at all? (#398 S3)
|
|
252
|
+
*
|
|
253
|
+
* False costs the machine its guarantees and nothing else. It has to be
|
|
254
|
+
* honoured HERE and not only at spawn, because every property goes to systemd
|
|
255
|
+
* in ONE `set-property` call: a single directive this systemd refuses would
|
|
256
|
+
* take the brake and the wall down with it, every tick, for every session.
|
|
257
|
+
*/
|
|
258
|
+
memoryLow?: boolean): LimitMove;
|
|
259
|
+
//# sourceMappingURL=session-allocator.d.ts.map
|