@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
package/dist/supervisor.d.ts
CHANGED
|
@@ -5,7 +5,10 @@ import { installAgent } from './agent-install.js';
|
|
|
5
5
|
import { pruneNativeClaudeVersions } from './agent-cleanup.js';
|
|
6
6
|
import { type AgentVersionsMeasurement } from './agent-versions.js';
|
|
7
7
|
import { type HostLoadFrame } from './host-load.js';
|
|
8
|
-
import { type ScopeMemoryStatus } from './session-cage.js';
|
|
8
|
+
import { type ScopeHold, type ScopeMemoryStatus, type SliceLimits } from './session-cage.js';
|
|
9
|
+
import { type AllocatorKnobs } from './session-allocator.js';
|
|
10
|
+
import { type ScopeProcess, type StallSample } from './session-stall.js';
|
|
11
|
+
import { createCheckpoint } from './checkpoints.js';
|
|
9
12
|
import type { RunnerWsClient } from './ws-client.js';
|
|
10
13
|
import type { SessionDescriptor } from './protocol.js';
|
|
11
14
|
import type { AgentAdapter } from './adapters/types.js';
|
|
@@ -50,6 +53,17 @@ export interface SupervisorOptions {
|
|
|
50
53
|
verifyEnabled?: boolean;
|
|
51
54
|
/** Test seam for the one-shot commit-message run. */
|
|
52
55
|
proposeCommitMessage?: typeof proposeCommitMessage;
|
|
56
|
+
/**
|
|
57
|
+
* Test seam for taking a restore point — #401.
|
|
58
|
+
*
|
|
59
|
+
* A seam and not a detail: the opening restore point is the whole window this
|
|
60
|
+
* ticket is about. It runs between «the working folder is ready» and «the
|
|
61
|
+
* first process exists», it takes SECONDS on a dirty tree, and a message that
|
|
62
|
+
* arrives inside it used to open a second door to `launchAgent`. Timing that
|
|
63
|
+
* window from the outside is guesswork; holding it open from a test is the
|
|
64
|
+
* only way the race is reproducible on demand.
|
|
65
|
+
*/
|
|
66
|
+
createCheckpoint?: typeof createCheckpoint;
|
|
53
67
|
/**
|
|
54
68
|
* `[checkpoints] enabled` from the runner's own config (ticket #126), by the
|
|
55
69
|
* same rule as `[verify] enabled`: restore points are copies of the working
|
|
@@ -153,8 +167,49 @@ export interface SupervisorOptions {
|
|
|
153
167
|
*/
|
|
154
168
|
sessionScopeUnitOf?: (id: string) => string | null;
|
|
155
169
|
readScopeMemoryStatus?: (unit: string) => ScopeMemoryStatus | null;
|
|
156
|
-
/**
|
|
157
|
-
|
|
170
|
+
/**
|
|
171
|
+
* #398 S1: the orphan sweep, so a test can prove the tick calls it — and that
|
|
172
|
+
* it never calls it twice at once. Production sweeps for real.
|
|
173
|
+
*/
|
|
174
|
+
sweepOrphanSessionScopes?: (liveIds: Iterable<string>) => Promise<string[]>;
|
|
175
|
+
/**
|
|
176
|
+
* #398 S2: the stall mechanism's four doors to the machine.
|
|
177
|
+
*
|
|
178
|
+
* All four are seams for the same reason the cage's two are: putting a real
|
|
179
|
+
* session into a real stall needs a real kernel refusing real memory, and no
|
|
180
|
+
* suite can arrange that. Production reads `/sys` and `/proc` and talks to
|
|
181
|
+
* systemd.
|
|
182
|
+
*/
|
|
183
|
+
readStallSample?: (unit: string, at: number) => StallSample | null;
|
|
184
|
+
readScopeProcesses?: (unit: string) => ScopeProcess[];
|
|
185
|
+
setScopeProperties?: (unit: string, properties: string[]) => Promise<boolean>;
|
|
186
|
+
signalSubtree?: (pids: Array<number | {
|
|
187
|
+
pid: number;
|
|
188
|
+
startedAtTicks: number;
|
|
189
|
+
}>, signal: NodeJS.Signals) => number;
|
|
190
|
+
/** The detector's cadence, ms. Real one is 5 s — no suite waits that out. */
|
|
191
|
+
stallSampleMs?: number;
|
|
192
|
+
/** How long a standing session is given. Real one is 3 minutes (D6). */
|
|
193
|
+
stallGraceMs?: number;
|
|
194
|
+
/**
|
|
195
|
+
* #398 S6: what the machine's owner asked for in `config.toml`.
|
|
196
|
+
*
|
|
197
|
+
* Read once in `cmdDaemon` and passed down, which is this package's idiom for
|
|
198
|
+
* config — nothing outside `index.ts` imports `config.js`, so a module that
|
|
199
|
+
* read the file itself would be the first exception and the first place for
|
|
200
|
+
* two answers about one setting to appear.
|
|
201
|
+
*/
|
|
202
|
+
memoryKnobs?: AllocatorKnobs;
|
|
203
|
+
/** `report-only` leaves the deadline running and stops nothing (#398 S6). */
|
|
204
|
+
memoryStallAction?: 'stop-command' | 'report-only';
|
|
205
|
+
/**
|
|
206
|
+
* #398 S3: the allocator's two readings of the machine, so a test can put a
|
|
207
|
+
* session on a crowded or an empty box without one. Production reads cgroupfs.
|
|
208
|
+
*/
|
|
209
|
+
readSliceLimits?: () => SliceLimits | null;
|
|
210
|
+
readScopeHold?: (unit: string) => ScopeHold | null;
|
|
211
|
+
/** #398 S4: the heartbeat for the memory frame. Real one is a minute. */
|
|
212
|
+
sessionLimitsHeartbeatMs?: number;
|
|
158
213
|
}
|
|
159
214
|
export declare class Supervisor {
|
|
160
215
|
private readonly ws;
|
|
@@ -233,23 +288,6 @@ export declare class Supervisor {
|
|
|
233
288
|
private readonly verify;
|
|
234
289
|
private readonly verifyReports;
|
|
235
290
|
constructor(ws: RunnerWsClient, opts: SupervisorOptions);
|
|
236
|
-
/**
|
|
237
|
-
* How long a session may sit over its brake before the feed says more than
|
|
238
|
-
* «slower» (#387, step 3). Five minutes is a starting point, not a
|
|
239
|
-
* measurement: an honest `pnpm typecheck` overshoots for tens of seconds, a
|
|
240
|
-
* runaway for ever, and the number that tells them apart on real machines is
|
|
241
|
-
* still to be read off `memory.events`. Sized so the first warning is never
|
|
242
|
-
* the only one a night-time run leaves behind.
|
|
243
|
-
*/
|
|
244
|
-
private static readonly CAGE_BRAKE_LONG_MS;
|
|
245
|
-
/**
|
|
246
|
-
* Calm ticks before the feed says the braking is over.
|
|
247
|
-
*
|
|
248
|
-
* `memory.high` holds usage AT the line, so a single sample under it means
|
|
249
|
-
* nothing. Two (a minute at the 30 s cadence) is the difference between an
|
|
250
|
-
* episode that ended and a build that breathed.
|
|
251
|
-
*/
|
|
252
|
-
private static readonly CAGE_CALM_TICKS_TO_RELEASE;
|
|
253
291
|
/** How often the agent versions are re-derived. See the constructor. */
|
|
254
292
|
private static readonly AGENT_VERSIONS_INTERVAL_MS;
|
|
255
293
|
private readonly agentVersionsTimer;
|
|
@@ -401,6 +439,13 @@ export declare class Supervisor {
|
|
|
401
439
|
/** The heartbeat window actually used — the constant, or a test's own. */
|
|
402
440
|
private readonly hostLoadHeartbeatMs;
|
|
403
441
|
private readonly hostLoadTimer;
|
|
442
|
+
/** #398 S2: the stall detector's clock, and the deadline it enforces. */
|
|
443
|
+
private readonly stallTimer;
|
|
444
|
+
private readonly stallGraceMs;
|
|
445
|
+
private readonly stallSampleMs;
|
|
446
|
+
private readonly limitsHeartbeatMs;
|
|
447
|
+
/** A stall tick is in flight — the next one waits rather than overlaps. */
|
|
448
|
+
private watchingStalls;
|
|
404
449
|
/** The last measurement the API actually took from us, or `null` for «nothing yet». */
|
|
405
450
|
private lastPublishedHostLoad;
|
|
406
451
|
/** When that frame went out, by this machine's clock. `0` = never. */
|
|
@@ -431,28 +476,185 @@ export declare class Supervisor {
|
|
|
431
476
|
* it had one.
|
|
432
477
|
*/
|
|
433
478
|
/**
|
|
434
|
-
* Tell the person
|
|
479
|
+
* Tell the person when the KERNEL killed something inside a caged session.
|
|
435
480
|
*
|
|
436
|
-
*
|
|
481
|
+
* One thing, since #398 S2 — `oom_kill` moved: the kernel stopped a process
|
|
482
|
+
* inside the session. With `OOMPolicy=continue` the session itself is still
|
|
483
|
+
* here, and the person has to be told that the command that was running is
|
|
484
|
+
* probably what died.
|
|
437
485
|
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
* - the brake released: said once, so a person who saw the warning knows the
|
|
443
|
-
* wait is over without reading the numbers;
|
|
444
|
-
* - `oom_kill` moved: the kernel killed a process inside the session. With
|
|
445
|
-
* `OOMPolicy=continue` the session itself is still here, and the person
|
|
446
|
-
* has to be told that the command that was running is probably what died.
|
|
486
|
+
* **The three braking lines this tick used to write are gone** (decision D8 of
|
|
487
|
+
* `session-memory-fair-share.md`): «has hit its memory share», «has been over
|
|
488
|
+
* its memory share for 5 min», and the all-clear. Two reasons, and the second
|
|
489
|
+
* is the decisive one:
|
|
447
490
|
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
491
|
+
* - they were state, not events. Being over the brake is a condition that
|
|
492
|
+
* comes and goes; its place is the activity strip under the session and the
|
|
493
|
+
* machine card, not the history a person scrolls back through;
|
|
494
|
+
* - they disagreed with the mechanism that replaced them. The deadline in
|
|
495
|
+
* `session-stall.ts` runs out after three minutes, so the five-minute line
|
|
496
|
+
* could never have been printed at all.
|
|
497
|
+
*
|
|
498
|
+
* What a person sees instead: nothing while the machine copes, and exactly one
|
|
499
|
+
* line when a command was actually stopped.
|
|
450
500
|
*
|
|
451
501
|
* A parked session has no process and no cgroup, and is skipped without a
|
|
452
502
|
* read. A session whose cgroup cannot be read (no cage on this machine, or
|
|
453
503
|
* the scope already gone) is skipped the same way: the watch never guesses.
|
|
454
504
|
*/
|
|
455
505
|
private watchSessionCages;
|
|
506
|
+
/** A sweep is in flight — see {@link sweepOrphanCages}. */
|
|
507
|
+
private sweepingCages;
|
|
508
|
+
/**
|
|
509
|
+
* Stop every session scope with nobody behind it, on the watch tick (#398 S1).
|
|
510
|
+
*
|
|
511
|
+
* The sweep itself spares every LIVE cage by its own register, sessions and
|
|
512
|
+
* `verify` runs alike (`session-cage.ts`), so what is passed here is belt and
|
|
513
|
+
* braces rather than the guard — but passing it says at the call site which
|
|
514
|
+
* ids this supervisor believes are alive, and a divergence between the two
|
|
515
|
+
* would be a bug worth seeing in a test.
|
|
516
|
+
*
|
|
517
|
+
* Re-entrancy matters more than it looks: the sweep is a `systemctl` call and
|
|
518
|
+
* the bus was measured at 2.7 s under load, so on a machine where the tick is
|
|
519
|
+
* 30 s and the bus is slow two sweeps could otherwise overlap and each would
|
|
520
|
+
* see the other's half-stopped units.
|
|
521
|
+
*
|
|
522
|
+
* Never fatal, and deliberately silent on failure: the daemon carries a fatal
|
|
523
|
+
* `unhandledRejection` handler, and losing every session because a bus call
|
|
524
|
+
* failed would be a far worse trade than an orphan living one more tick.
|
|
525
|
+
*/
|
|
526
|
+
private sweepOrphanCages;
|
|
527
|
+
/**
|
|
528
|
+
* The stall mechanism, once per window (#398 S2, D6, gotcha §480).
|
|
529
|
+
*
|
|
530
|
+
* The order is the whole design, and each step exists because the one before
|
|
531
|
+
* it was not enough:
|
|
532
|
+
*
|
|
533
|
+
* 1. **T0 — add.** If the machine has memory free, the brake goes up and the
|
|
534
|
+
* session carries on. Nothing is written to the feed: the mechanism
|
|
535
|
+
* worked, nothing changed for the person, the build is still going (D8).
|
|
536
|
+
* 2. **T1 — the deadline.** Three minutes, announced as state (the strip
|
|
537
|
+
* under the session says how long is left) and never as a line in the
|
|
538
|
+
* history. This is the window in which a person can step in without
|
|
539
|
+
* paying for it with an entry they will scroll past for ever.
|
|
540
|
+
* 3. **T2 — stop the biggest command.** Not the session, not the agent: the
|
|
541
|
+
* command. The agent stays alive, sees its command stopped, is told why,
|
|
542
|
+
* and answers the person itself. Waiting for a human instead is what cost
|
|
543
|
+
* the incident its hour — at night nobody comes.
|
|
544
|
+
* 4. **T3 — nothing to stop.** When all that is left in the cage is the agent
|
|
545
|
+
* and its tools, this mechanism has no right to any of them. State again,
|
|
546
|
+
* no line: the person decides with «Stop» or «Pause».
|
|
547
|
+
*
|
|
548
|
+
* Never throws out of here. The daemon carries a fatal `unhandledRejection`
|
|
549
|
+
* handler, so one unhandled error would end every session on the machine.
|
|
550
|
+
*/
|
|
551
|
+
private watchSessionStalls;
|
|
552
|
+
private stepOneStall;
|
|
553
|
+
/**
|
|
554
|
+
* Ask the allocator for more, now, because this session has stopped moving (T0).
|
|
555
|
+
*
|
|
556
|
+
* Since S3 there is no separate ladder here: `MemoryHigh` has exactly TWO
|
|
557
|
+
* writers, and they are the same code — the allocator on its tick and the
|
|
558
|
+
* allocator on this signal. Two independent writers of one number is how a
|
|
559
|
+
* limit ends up being whatever the last tick happened to think.
|
|
560
|
+
*
|
|
561
|
+
* The allocator needs no special case for a stall: a standing session's `hold`
|
|
562
|
+
* is by definition all the way up against its brake, so `hold + free` asks for
|
|
563
|
+
* everything the machine has spare. If there is nothing spare, nothing moves,
|
|
564
|
+
* and the deadline below is the answer instead.
|
|
565
|
+
*/
|
|
566
|
+
private addMemoryTo;
|
|
567
|
+
/** T2 and T3 — stop the biggest command of this session, or nothing at all. */
|
|
568
|
+
private stopBiggestCommand;
|
|
569
|
+
/**
|
|
570
|
+
* Say something to the agent on the runner's own initiative, without stepping
|
|
571
|
+
* on a card the person has not answered (#398 S7, B7).
|
|
572
|
+
*
|
|
573
|
+
* `session.send()` looks like a neutral channel and is not one: with a
|
|
574
|
+
* question open, `ClaudeSession.send` answers it «discuss» and the API
|
|
575
|
+
* records `question_resolved {source: 'user'}` — a decision attributed to a
|
|
576
|
+
* person who never made it — while `CodexSession.send` declines a held plan
|
|
577
|
+
* the same way. Every other initiative of this supervisor already checks for
|
|
578
|
+
* an open card; the memory mechanism was the one that did not.
|
|
579
|
+
*
|
|
580
|
+
* Held rather than dropped: the agent is parked on the card, so it cannot run
|
|
581
|
+
* anything until the person answers anyway, and once they do the note is the
|
|
582
|
+
* first thing it reads.
|
|
583
|
+
*/
|
|
584
|
+
private tellAgent;
|
|
585
|
+
/** Deliver what waited for a card to close — see {@link tellAgent}. */
|
|
586
|
+
private flushPendingAgentNote;
|
|
587
|
+
/** How many sessions are running in a cage right now (#398 S7, B9). */
|
|
588
|
+
private liveCagedSessions;
|
|
589
|
+
/** SIGKILL for a subtree that did not take SIGTERM within the grace. */
|
|
590
|
+
private escalatePendingKill;
|
|
591
|
+
/** An allocator pass is in flight — the next one waits rather than overlaps. */
|
|
592
|
+
private allocating;
|
|
593
|
+
/** The last pass's machine-wide numbers, for the card and the session banner. */
|
|
594
|
+
private lastAllocation;
|
|
595
|
+
/**
|
|
596
|
+
* The machine the last allocation was computed for, kept so the card's own
|
|
597
|
+
* number can be derived without measuring the machine a SECOND time (#403).
|
|
598
|
+
*/
|
|
599
|
+
private lastAllocatorMachine;
|
|
600
|
+
/** The last frame the API actually took from us, or null for «nothing yet». */
|
|
601
|
+
private lastPublishedLimits;
|
|
602
|
+
/** When that frame went out, by this machine's clock. `0` = never. */
|
|
603
|
+
private lastLimitsSentAt;
|
|
604
|
+
/**
|
|
605
|
+
* Decide, and write, how much memory each live session may use (#398 S3).
|
|
606
|
+
*
|
|
607
|
+
* Runs on the 30 s watch tick and on the stall detector's signal, and nowhere
|
|
608
|
+
* else: `MemoryHigh` must have exactly one implementation of «what should this
|
|
609
|
+
* be», or the two writers disagree and the limit becomes whichever ran last.
|
|
610
|
+
*
|
|
611
|
+
* Everything that could be wrong about the machine is read fresh here rather
|
|
612
|
+
* than taken from the daemon-start probe: the pot moves when the hourly
|
|
613
|
+
* re-measure rewrites the slice, the collective brake decides whether
|
|
614
|
+
* overselling is even legal, and a neighbouring application growing is
|
|
615
|
+
* precisely the case #398 was opened about.
|
|
616
|
+
*/
|
|
617
|
+
private runAllocator;
|
|
618
|
+
/** The machine as the allocator needs to see it, from live readings only. */
|
|
619
|
+
private allocatorMachine;
|
|
620
|
+
/**
|
|
621
|
+
* What a session STARTING right now should be given (#398 S3, work 9).
|
|
622
|
+
*
|
|
623
|
+
* The allocator is asked to compute as though this session already existed,
|
|
624
|
+
* so a session born at 03:00 gets the number its first tick will confirm
|
|
625
|
+
* rather than the one the daemon measured whenever it last started.
|
|
626
|
+
*/
|
|
627
|
+
private ladderForNewSession;
|
|
628
|
+
/**
|
|
629
|
+
* Tell the API how this machine is dividing its memory — when it moved (#398 S4).
|
|
630
|
+
*
|
|
631
|
+
* Built from the pass the allocator has just finished rather than measured
|
|
632
|
+
* again: two readings of a moving machine taken a moment apart would let the
|
|
633
|
+
* card and the scope disagree about the same session, and «the card said 4 GB
|
|
634
|
+
* and the cgroup said 2» is the exact shape of the bug that made
|
|
635
|
+
* `limitsCurrent` untrustworthy once already.
|
|
636
|
+
*
|
|
637
|
+
* Recorded ONLY when the socket took it, same as `publishHostLoad`: a frame
|
|
638
|
+
* dropped by a dead socket must not be remembered as sent.
|
|
639
|
+
*/
|
|
640
|
+
/**
|
|
641
|
+
* The number on the machine card: what ONE session gets when the machine is
|
|
642
|
+
* free (decision D4 of `runner-cage-authority.md`, 10.09.2026).
|
|
643
|
+
*
|
|
644
|
+
* The card used to carry the ceiling a newcomer would get RIGHT NOW, from the
|
|
645
|
+
* current crowd — which is honest per second and useless per person: on a
|
|
646
|
+
* 12 GiB machine with two sessions of 3 GiB it read ~6 GiB, it moved whenever
|
|
647
|
+
* a neighbour started or stopped, and the sentence beside it said «up to X on
|
|
648
|
+
* an idle machine», which is a different number entirely. A card is read to
|
|
649
|
+
* answer «what does this machine give a session», and that question has a
|
|
650
|
+
* still answer.
|
|
651
|
+
*
|
|
652
|
+
* Computed, not measured: the pot and the owner's knobs are all it takes, so
|
|
653
|
+
* this makes no second reading of a moving machine — the one the shape of
|
|
654
|
+
* `publishSessionLimits` warns against in its own header.
|
|
655
|
+
*/
|
|
656
|
+
private idleSessionCeiling;
|
|
657
|
+
private publishSessionLimits;
|
|
456
658
|
private publishHostLoad;
|
|
457
659
|
private onFrame;
|
|
458
660
|
private startSession;
|