@bridge4dev/runner 0.57.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/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 +223 -33
- package/dist/supervisor.js +845 -94
- 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
package/dist/session-cage.d.ts
CHANGED
|
@@ -94,6 +94,14 @@ export interface SessionCageFacts {
|
|
|
94
94
|
* `--scope` was off by default there anyway.
|
|
95
95
|
*/
|
|
96
96
|
expandEnvironmentFlag: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Does this systemd take `MemoryLow=` on a scope? (#398 S3)
|
|
99
|
+
*
|
|
100
|
+
* False means the machine keeps its cage and loses only the guarantees — the
|
|
101
|
+
* brake and the wall are unaffected. Probed rather than assumed, because a
|
|
102
|
+
* `-p` systemd refuses makes `systemd-run` spawn nothing at all.
|
|
103
|
+
*/
|
|
104
|
+
memoryLowFlag: boolean;
|
|
97
105
|
}
|
|
98
106
|
/**
|
|
99
107
|
* The FLOOR the plan's formula does not have, and the reason it needs one.
|
|
@@ -118,6 +126,36 @@ export interface SessionCageFacts {
|
|
|
118
126
|
* (QA-2026-09-07 MINOR-6, where the earlier wording promised the opposite).
|
|
119
127
|
*/
|
|
120
128
|
export declare const SESSION_MEMORY_HIGH_MIN_BYTES: number;
|
|
129
|
+
/**
|
|
130
|
+
* The gap between the brake and the wall, and never less than this (#398 S2).
|
|
131
|
+
*
|
|
132
|
+
* The brake is where the kernel starts reclaiming; the wall is where it kills.
|
|
133
|
+
* With the two at the same number there is no brake at all — that is the 0.54.0
|
|
134
|
+
* kill line, and #387 is the whole story of what it costs. So every path that
|
|
135
|
+
* computes the pair keeps at least a band between them, and the band is also
|
|
136
|
+
* the step by which the stall detector may raise a brake.
|
|
137
|
+
*
|
|
138
|
+
* 256 MiB is sized against what one `tsc` of this monorepo holds (~400 MB
|
|
139
|
+
* measured) divided by the overshoot a build makes in one 5 s window: enough
|
|
140
|
+
* room for the allocation that was refused, not enough to hide a runaway.
|
|
141
|
+
*
|
|
142
|
+
* {@link sessionMemoryBandBytes} widens it on the machines where reaching the
|
|
143
|
+
* wall costs the whole session rather than one process — see there.
|
|
144
|
+
*/
|
|
145
|
+
export declare const SESSION_MEMORY_BAND_BYTES: number;
|
|
146
|
+
/**
|
|
147
|
+
* The band on THIS machine.
|
|
148
|
+
*
|
|
149
|
+
* Where the probe found no `OOMPolicy=continue` (systemd 249 and friends), a
|
|
150
|
+
* kill inside the scope stops the whole scope: the session dies, not one
|
|
151
|
+
* process. The band there is not a nicety but the only warning the session
|
|
152
|
+
* gets, so it is a quarter of the brake and never under 512 MiB.
|
|
153
|
+
*
|
|
154
|
+
* Two of the fleet's fifteen caged machines answer `sessionOomContinue: false`
|
|
155
|
+
* (`search aigain`, `trizailab`, measured 10.09.2026), so this is not a
|
|
156
|
+
* hypothetical branch.
|
|
157
|
+
*/
|
|
158
|
+
export declare function sessionMemoryBandBytes(brakeBytes: number, oomContinue: boolean): number;
|
|
121
159
|
/**
|
|
122
160
|
* The wall, when nothing on the machine can say where it should be.
|
|
123
161
|
*
|
|
@@ -293,6 +331,7 @@ export interface CageProbe {
|
|
|
293
331
|
probeMemoryMax: (options: {
|
|
294
332
|
expandEnvironmentFlag: boolean;
|
|
295
333
|
oomPolicyFlag: boolean;
|
|
334
|
+
memoryLowFlag: boolean;
|
|
296
335
|
}) => Promise<CageProbeResult>;
|
|
297
336
|
/** `MemoryMax` of the runner service in bytes; null for `infinity`. */
|
|
298
337
|
serviceMemoryMax: () => Promise<number | null>;
|
|
@@ -328,6 +367,8 @@ export declare function detectSessionCage(probe?: CageProbe): Promise<SessionCag
|
|
|
328
367
|
* must not be retried into a cage with a weaker policy.
|
|
329
368
|
*/
|
|
330
369
|
export declare function refusesOomPolicy(error: string): boolean;
|
|
370
|
+
/** The same question for `MemoryLow=`, and the same wording from systemd. */
|
|
371
|
+
export declare function refusesMemoryLow(error: string): boolean;
|
|
331
372
|
/**
|
|
332
373
|
* Probe once, at daemon start, and remember the answer.
|
|
333
374
|
*
|
|
@@ -335,7 +376,9 @@ export declare function refusesOomPolicy(error: string): boolean;
|
|
|
335
376
|
* three sessions starting at once would mean three throwaway scopes before the
|
|
336
377
|
* first agent got a word out.
|
|
337
378
|
*/
|
|
338
|
-
export declare function initSessionCage(probe?: CageProbe
|
|
379
|
+
export declare function initSessionCage(probe?: CageProbe, options?: {
|
|
380
|
+
keepCageIfWorse?: boolean;
|
|
381
|
+
}): Promise<SessionCageFacts>;
|
|
339
382
|
/** What the last {@link initSessionCage} found; the safe default before it ran. */
|
|
340
383
|
export declare function sessionCage(): SessionCageFacts;
|
|
341
384
|
export interface CagedSpawn {
|
|
@@ -351,6 +394,10 @@ export interface CagedSpawn {
|
|
|
351
394
|
}
|
|
352
395
|
/** The scope a live caged id runs in, or null when it is not caged (or gone). */
|
|
353
396
|
export declare function sessionScopeUnitOf(id: string): string | null;
|
|
397
|
+
/** Remember which process is the agent of this session. */
|
|
398
|
+
export declare function noteSessionAgentPid(id: string, pid: number | undefined): void;
|
|
399
|
+
/** The agent's pid for a live session, or null when nothing has said. */
|
|
400
|
+
export declare function sessionAgentPid(id: string): number | null;
|
|
354
401
|
/**
|
|
355
402
|
* Wrap a command in its session's cage, or hand it back untouched.
|
|
356
403
|
*
|
|
@@ -363,12 +410,30 @@ export declare function sessionScopeUnitOf(id: string): string | null;
|
|
|
363
410
|
* spawn, because `systemd-run --scope` execs into the SAME pid — verified in the
|
|
364
411
|
* spike, including `detached: true` + `process.kill(-pid)` in `verify.ts`
|
|
365
412
|
* (`pgid === child.pid` still holds).
|
|
413
|
+
*
|
|
414
|
+
* Untouched is also the answer for a process that has no right to put units on
|
|
415
|
+
* this machine (#403). It is the mildest of the doors in `cage-authority.ts`:
|
|
416
|
+
* the command still runs, it simply runs uncaged, which is exactly what happens
|
|
417
|
+
* on every machine without cgroups.
|
|
366
418
|
*/
|
|
367
419
|
export declare function cageSpawn(input: {
|
|
368
420
|
id: string;
|
|
369
421
|
command: string;
|
|
370
422
|
args: string[];
|
|
371
423
|
}): CagedSpawn;
|
|
424
|
+
/**
|
|
425
|
+
* The command line itself, with no question of who is allowed to run it.
|
|
426
|
+
*
|
|
427
|
+
* Split out from {@link cageSpawn} so the shape of the line stays testable: the
|
|
428
|
+
* suite proves what `systemd-run` would be handed, and the door above proves
|
|
429
|
+
* who may hand it. One function could not do both — a test process legitimately
|
|
430
|
+
* needs the first answer and must never get the second.
|
|
431
|
+
*/
|
|
432
|
+
export declare function buildCagedSpawn(input: {
|
|
433
|
+
id: string;
|
|
434
|
+
command: string;
|
|
435
|
+
args: string[];
|
|
436
|
+
}): CagedSpawn;
|
|
372
437
|
/**
|
|
373
438
|
* Did this process die in the window before `systemd-run` handed over?
|
|
374
439
|
*
|
|
@@ -424,6 +489,14 @@ export declare function parseScopeMemoryStatus(files: {
|
|
|
424
489
|
events: string;
|
|
425
490
|
swapCurrent?: string;
|
|
426
491
|
}): ScopeMemoryStatus | null;
|
|
492
|
+
/**
|
|
493
|
+
* Where a session scope's cgroup lives, or null off a user manager.
|
|
494
|
+
*
|
|
495
|
+
* Exported for `session-stall.ts`, which reads three more files out of the same
|
|
496
|
+
* directory (`memory.pressure`, `memory.stat`, `cgroup.procs`). One resolver,
|
|
497
|
+
* so the two modules can never disagree about which cgroup a session is in.
|
|
498
|
+
*/
|
|
499
|
+
export declare function scopeCgroupDir(unit: string): string | null;
|
|
427
500
|
/**
|
|
428
501
|
* What one live session's cgroup holds and has been through. Null when the
|
|
429
502
|
* cgroup is not there (the scope ended, or this machine has no cage).
|
|
@@ -432,6 +505,122 @@ export declare function parseScopeMemoryStatus(files: {
|
|
|
432
505
|
* every live session, and the bus is the thing that took 2.7 s under load.
|
|
433
506
|
*/
|
|
434
507
|
export declare function readScopeMemoryStatus(unit: string, readFile?: (p: string) => string): ScopeMemoryStatus | null;
|
|
508
|
+
/** What one session's cgroup holds, split into «can be given back» and «cannot». */
|
|
509
|
+
export interface ScopeHold {
|
|
510
|
+
/** `memory.current`. */
|
|
511
|
+
currentBytes: number;
|
|
512
|
+
/** `memory.current − (file − shmem)` — the part a lower limit cannot reclaim. */
|
|
513
|
+
holdBytes: number;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* What a live session holds, for the allocator (#398 S3).
|
|
517
|
+
*
|
|
518
|
+
* `readCgroupMemory` rather than a second parser: it already takes page cache
|
|
519
|
+
* out and puts `shmem` back, which is the difference between «cache we can give
|
|
520
|
+
* back» and «memory something would have to be killed for», and getting that
|
|
521
|
+
* wrong inflates every number by whatever the machine happened to have cached
|
|
522
|
+
* (measured at +19 % on this host).
|
|
523
|
+
*/
|
|
524
|
+
export declare function readScopeHold(unit: string): ScopeHold | null;
|
|
525
|
+
/** What systemd has in force over ALL sessions together, read live off cgroupfs. */
|
|
526
|
+
export interface SliceLimits {
|
|
527
|
+
/** `memory.max` of the slice — the pot. Null for `max`. */
|
|
528
|
+
potBytes: number | null;
|
|
529
|
+
/**
|
|
530
|
+
* `memory.high` of the slice — the COLLECTIVE brake, and the invariant the
|
|
531
|
+
* allocator's overselling rests on. Null for `max`, which means there is none.
|
|
532
|
+
*/
|
|
533
|
+
collectiveBrakeBytes: number | null;
|
|
534
|
+
/**
|
|
535
|
+
* `memory.low` of the slice — is the GUARANTEE in force at all (#398 S7, B3)?
|
|
536
|
+
*
|
|
537
|
+
* The card and the agent's prompt both promise every session two gigabytes
|
|
538
|
+
* that will not be reclaimed, and nothing on this machine ever checked that
|
|
539
|
+
* the promise was written: measured on vmi3201430 on 10.09.2026, this file
|
|
540
|
+
* held `0` while the chip said «2.0 GB guaranteed». Zero is not «unknown» —
|
|
541
|
+
* it is the kernel's way of saying there is no protection here.
|
|
542
|
+
*/
|
|
543
|
+
guaranteeBytes: number | null;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* The slice's own two limits, from the filesystem rather than the bus.
|
|
547
|
+
*
|
|
548
|
+
* Live on purpose. What the probe found at daemon start is a snapshot, and the
|
|
549
|
+
* pot moves: the hourly `repairResourceLimits` rewrites it from a fresh
|
|
550
|
+
* measurement, and a machine whose neighbours grew or shrank gets a different
|
|
551
|
+
* number. Reading two small files on the tick costs nothing and cannot be stale;
|
|
552
|
+
* a `systemctl show` would cost a bus call measured at 2.7 s under load.
|
|
553
|
+
*/
|
|
554
|
+
export declare function readSliceLimits(readFile?: (p: string) => string): SliceLimits | null;
|
|
555
|
+
/**
|
|
556
|
+
* Where `devbridge-sessions.slice` lives on this machine.
|
|
557
|
+
*
|
|
558
|
+
* Two ways of answering, and the second is what makes this usable from a
|
|
559
|
+
* command the operator typed. The first is the reader's own cgroup, which is
|
|
560
|
+
* exact whenever the reader is under the user manager. The second builds the
|
|
561
|
+
* path from the user id, exactly as `userManagerCgroupControllers()` already
|
|
562
|
+
* does — a `systemd-run` scope, a login session and a bare shell all get the
|
|
563
|
+
* right answer that way, and the daemon gets the same one either way.
|
|
564
|
+
*/
|
|
565
|
+
export declare function sessionsSliceCgroupDir(io?: {
|
|
566
|
+
selfCgroup?: () => string | null;
|
|
567
|
+
exists?: (p: string) => boolean;
|
|
568
|
+
/** The uid of THIS process — injected so a test does not depend on who runs it. */
|
|
569
|
+
selfUid?: () => number;
|
|
570
|
+
ownerUid?: () => number;
|
|
571
|
+
}): string | null;
|
|
572
|
+
/**
|
|
573
|
+
* The three numbers a session's scope should be started with, when something
|
|
574
|
+
* knows better than the daemon-start snapshot (#398 S3, work 9).
|
|
575
|
+
*
|
|
576
|
+
* Registered by the supervisor, which owns the allocator. Without it every new
|
|
577
|
+
* session would spend its first half-minute on the old `pot / 3` formula and
|
|
578
|
+
* then jump to the new one on the first tick — visible, unexplainable, and
|
|
579
|
+
* exactly the kind of thing that gets reported as a bug.
|
|
580
|
+
*
|
|
581
|
+
* A plain module-level hook rather than an import, because the allocator imports
|
|
582
|
+
* THIS module and the cycle would be real.
|
|
583
|
+
*/
|
|
584
|
+
export interface LiveLadder {
|
|
585
|
+
highBytes: number;
|
|
586
|
+
maxBytes: number;
|
|
587
|
+
swapBytes: number;
|
|
588
|
+
guaranteedBytes: number;
|
|
589
|
+
}
|
|
590
|
+
export declare function setLiveLadderSource(source: ((id: string) => LiveLadder | null) | null): void;
|
|
591
|
+
/**
|
|
592
|
+
* The two numbers this session may be TOLD about (#398 S5).
|
|
593
|
+
*
|
|
594
|
+
* The guarantee and the ceiling, and deliberately not the live share. The share
|
|
595
|
+
* is recomputed every 30 s, so any value of it put into an environment variable
|
|
596
|
+
* or into a system prompt is stale within half a minute — and a number the agent
|
|
597
|
+
* believes and acts on after it has stopped being true is worse than no number.
|
|
598
|
+
* The guarantee is a constant of the formula and the ceiling barely moves.
|
|
599
|
+
*
|
|
600
|
+
* Null on a machine with no cage: there is nothing to promise there, and «нет
|
|
601
|
+
* cgroup — держим ноль» (#257) means we say nothing rather than invent a limit.
|
|
602
|
+
*/
|
|
603
|
+
export declare function sessionMemoryFor(id: string): {
|
|
604
|
+
guaranteedBytes: number;
|
|
605
|
+
maxBytes: number;
|
|
606
|
+
swapBytes: number;
|
|
607
|
+
} | null;
|
|
608
|
+
/**
|
|
609
|
+
* What the agent is told, and how — one text for both CLIs (#398 S5).
|
|
610
|
+
*
|
|
611
|
+
* A shared builder rather than two literals for the reason `DIRECT_BRANCH_RULE`
|
|
612
|
+
* in `adapters/types.ts` is shared: the two system-prompt appends are hand-kept
|
|
613
|
+
* copies of one another, and a third place for them to diverge was not worth
|
|
614
|
+
* having.
|
|
615
|
+
*
|
|
616
|
+
* The live share is named as something the agent CANNOT know, on purpose. Half
|
|
617
|
+
* of the incident this whole plan comes from was an agent raising its own heap
|
|
618
|
+
* twice — to 4 GB, then to 6 GB, against a wall of 4296 MB — because nothing
|
|
619
|
+
* had ever told it there was a wall.
|
|
620
|
+
*/
|
|
621
|
+
export declare function sessionMemoryEnv(id: string): Record<string, string>;
|
|
622
|
+
/** The sentence for the system prompt, or null on a machine with no cage. */
|
|
623
|
+
export declare function sessionMemoryPromptLine(id: string): string | null;
|
|
435
624
|
export declare function markScopeOomKillsSeen(id: string, oomKills: number): void;
|
|
436
625
|
/**
|
|
437
626
|
* «a process in it was stopped» / «3 processes in it were stopped», so the verb
|
|
@@ -503,8 +692,46 @@ export declare function listSessionScopes(systemctl?: Systemctl): Promise<Sessio
|
|
|
503
692
|
* `liveIds` exists so the same sweep can run later without killing work in
|
|
504
693
|
* progress; only the CURRENT scope of a live session is spared, because an
|
|
505
694
|
* earlier attempt of the same session is exactly the leftover we are here for.
|
|
695
|
+
*
|
|
696
|
+
* **Everything in {@link liveUnits} is spared too, whatever the caller passed.**
|
|
697
|
+
* A session is not the only thing that puts a scope under this prefix: a
|
|
698
|
+
* `verify` run cages itself as `verify-<runId>` (`verify.ts`), and a caller
|
|
699
|
+
* that hands over its list of SESSIONS would have the sweep stop a check that
|
|
700
|
+
* is running — a new bug in place of the old one. `liveUnits` is filled by
|
|
701
|
+
* `cageSpawn` and emptied by `releaseSessionScope`, so it is the full register
|
|
702
|
+
* of live cages regardless of who opened them; at daemon start it is empty by
|
|
703
|
+
* construction, which is exactly why the sweep there still takes everything.
|
|
704
|
+
*/
|
|
705
|
+
/**
|
|
706
|
+
* Who, if anybody, is alive inside a cage — asked of the kernel rather than of
|
|
707
|
+
* this process's own memory (#403).
|
|
708
|
+
*
|
|
709
|
+
* The sweep used to decide by `liveUnits`, which is a register of what THIS
|
|
710
|
+
* process started. In the daemon that is right; in any other process it is
|
|
711
|
+
* empty by construction, and «not in my register» then means «the whole
|
|
712
|
+
* machine». That inversion is what stopped three people's sessions on
|
|
713
|
+
* 10.09.2026.
|
|
714
|
+
*
|
|
715
|
+
* The cgroup answers the question properly, and it answers the same for
|
|
716
|
+
* everybody:
|
|
717
|
+
*
|
|
718
|
+
* - `empty` — no processes at all: nothing to stop, only a name to
|
|
719
|
+
* free (`reset-failed`).
|
|
720
|
+
* - `orphaned` — processes whose parent is gone (`PPid` 1 or 0, or a
|
|
721
|
+
* parent that no longer exists). This is the shape the
|
|
722
|
+
* sweep exists for: a scope that outlived the daemon
|
|
723
|
+
* that started it, carrying a `ugrep` for 10 h 51 min.
|
|
724
|
+
* - `owned` — a live parent outside the cage is supervising them.
|
|
725
|
+
* Somebody's session, and not ours to end.
|
|
726
|
+
* - `unknown` — `/proc` would not say. Left alone, loudly.
|
|
506
727
|
*/
|
|
507
|
-
export
|
|
728
|
+
export type ScopeLiveness = 'empty' | 'orphaned' | 'owned' | 'unknown';
|
|
729
|
+
export declare function scopeOwnerLiveness(unit: string, io?: {
|
|
730
|
+
readFile?: (p: string) => string;
|
|
731
|
+
cgroupDir?: (unit: string) => string | null;
|
|
732
|
+
isAlive?: (pid: number) => boolean;
|
|
733
|
+
}): ScopeLiveness;
|
|
734
|
+
export declare function sweepOrphanSessionScopes(liveIds?: Iterable<string>, systemctl?: Systemctl, liveness?: (unit: string) => ScopeLiveness): Promise<string[]>;
|
|
508
735
|
/**
|
|
509
736
|
* The slice names and the CPU share — re-exported so a caller that reasons about
|
|
510
737
|
* the cage needs one import, while the values themselves stay next to the
|