@bridge4dev/runner 0.65.0 → 0.66.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/account-commands.d.ts +44 -0
- package/dist/account-commands.js +99 -0
- package/dist/adapters/claude-usage.d.ts +79 -15
- package/dist/adapters/claude-usage.js +143 -67
- package/dist/adapters/claude.d.ts +30 -1
- package/dist/adapters/claude.js +171 -11
- package/dist/adapters/codex.js +25 -2
- package/dist/adapters/types.d.ts +22 -0
- package/dist/agent-auth.d.ts +87 -0
- package/dist/agent-auth.js +249 -15
- package/dist/auth-relay.d.ts +110 -26
- package/dist/auth-relay.js +339 -232
- package/dist/claude-homes.d.ts +436 -0
- package/dist/claude-homes.js +1567 -0
- package/dist/commit-message.js +4 -1
- package/dist/config.d.ts +14 -14
- package/dist/daemon-exit.d.ts +40 -0
- package/dist/daemon-exit.js +29 -0
- package/dist/index.js +68 -18
- package/dist/login-marks.d.ts +31 -0
- package/dist/login-marks.js +47 -0
- package/dist/protocol.d.ts +38 -38
- package/dist/protocol.js +3 -1
- package/dist/session-cage.d.ts +49 -0
- package/dist/session-cage.js +238 -12
- package/dist/session-stall.d.ts +25 -0
- package/dist/session-stall.js +84 -0
- package/dist/supervisor.d.ts +66 -2
- package/dist/supervisor.js +181 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/ws-client.js +14 -0
- package/package.json +1 -1
package/dist/supervisor.js
CHANGED
|
@@ -19,6 +19,7 @@ import { applySession, gitBranches, gitCommit, gitDiff, gitLog, gitPush, gitRefs
|
|
|
19
19
|
import { fsView } from './fsview.js';
|
|
20
20
|
import { publishFile } from './file-publish.js';
|
|
21
21
|
import { agentAuthStatuses, AuthRelay, clearAgentAuthFailure, noteAgentAuthFailure, } from './auth-relay.js';
|
|
22
|
+
import { isAccountCommand, runAccountCommand } from './account-commands.js';
|
|
22
23
|
import { isSupervisedProcess, selfUpdate } from './self-update.js';
|
|
23
24
|
import { installAgent } from './agent-install.js';
|
|
24
25
|
import { autoUpdateRefusal, claimAgentAutoUpdate } from './agent-auto-update.js';
|
|
@@ -28,9 +29,9 @@ import { cageAuthority } from './cage-authority.js';
|
|
|
28
29
|
import { invalidateAgentVersions, measureAgentVersions, } from './agent-versions.js';
|
|
29
30
|
import { rememberWorkspacePath } from './environment.js';
|
|
30
31
|
import { hostLoadChangedEnough, hostLoadHeartbeatDue, readHostLoad, HOST_LOAD_HEARTBEAT_MS, HOST_LOAD_SAMPLE_INTERVAL_MS, } from './host-load.js';
|
|
31
|
-
import { markScopeOomKillsSeen, readScopeHold, readScopeMemoryStatus, readSliceLimits, sessionAgentPid, sessionCage, sessionScopeUnitOf, setLiveLadderSource, stoppedProcesses, sweepOrphanSessionScopes, } from './session-cage.js';
|
|
32
|
+
import { markScopeOomKillsSeen, readScopeHold, readScopeMemoryStatus, readSliceLimits, restartStoppedCommands, sessionAgentPid, sessionCage, sessionScopeUnitOf, setLiveLadderSource, stopSessionScope, stoppedProcesses, sweepOrphanSessionScopes, } from './session-cage.js';
|
|
32
33
|
import { allocateSessionMemory, planLimitMove, SESSION_GUARANTEE_BYTES, } from './session-allocator.js';
|
|
33
|
-
import { freshStallState, pickKillCandidate, readScopeProcesses, readStallSample, setScopeProperties, signalSubtree, stallStep, STALL_GRACE_MS, STALL_MUTE_AFTER_LOWER_MS, STALL_SIGKILL_AFTER_MS, STALL_WINDOW_MS, } from './session-stall.js';
|
|
34
|
+
import { countRunningCommands, freshStallState, pickKillCandidate, readScopeProcesses, readStallSample, setScopeProperties, signalSubtree, stallStep, STALL_GRACE_MS, STALL_MUTE_AFTER_LOWER_MS, STALL_SIGKILL_AFTER_MS, STALL_WINDOW_MS, } from './session-stall.js';
|
|
34
35
|
import { machineReserveBytes } from './service-unit.js';
|
|
35
36
|
import { sessionLimitsChangedEnough, sessionLimitsHeartbeatDue, SESSION_LIMITS_HEARTBEAT_MS, } from './session-limits.js';
|
|
36
37
|
import { composeMessageWithAttachments, saveAttachments, } from './attachments.js';
|
|
@@ -1693,7 +1694,28 @@ export class Supervisor {
|
|
|
1693
1694
|
this.lastHostLoadSentAt = now;
|
|
1694
1695
|
}
|
|
1695
1696
|
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Set by {@link shutdown}, and read wherever this supervisor could still be
|
|
1699
|
+
* ASKED to do something after it has let go of everything (#431).
|
|
1700
|
+
*
|
|
1701
|
+
* It exists because the daemon now outlives `shutdown()` by up to three
|
|
1702
|
+
* seconds — the time its cages are given to stop. Until that change every
|
|
1703
|
+
* caller exited in the same tick and «after shutdown» was not a state
|
|
1704
|
+
* anything could be in.
|
|
1705
|
+
*/
|
|
1706
|
+
shuttingDown = false;
|
|
1696
1707
|
async onFrame(frame) {
|
|
1708
|
+
/**
|
|
1709
|
+
* Nothing from the wire is answered once the shutdown has started.
|
|
1710
|
+
*
|
|
1711
|
+
* The socket is closed right after the synchronous half of `shutdown()`,
|
|
1712
|
+
* but closing is a handshake: a frame already in flight still lands. The
|
|
1713
|
+
* client refuses to deliver it (`ws-client.ts`) and this is the second
|
|
1714
|
+
* door, on the side that would act on it — an `event_ack` here deletes the
|
|
1715
|
+
* journal of a session this supervisor has already released.
|
|
1716
|
+
*/
|
|
1717
|
+
if (this.shuttingDown)
|
|
1718
|
+
return;
|
|
1697
1719
|
switch (frame.type) {
|
|
1698
1720
|
case 'hello_ack':
|
|
1699
1721
|
this.helloAcked = true;
|
|
@@ -1866,6 +1888,21 @@ export class Supervisor {
|
|
|
1866
1888
|
* the ask any more.
|
|
1867
1889
|
*/
|
|
1868
1890
|
withdrawReason = 'not_held') {
|
|
1891
|
+
/**
|
|
1892
|
+
* Nothing is started once this runner is going down (#431).
|
|
1893
|
+
*
|
|
1894
|
+
* The wire is already shut by then, but this door has a second caller: the
|
|
1895
|
+
* tail of `pumpEvents`, which runs a microtask after the agent's stream
|
|
1896
|
+
* ends — and since the shutdown waits for its cages, the process is still
|
|
1897
|
+
* here to run it. What it would build first is a worktree and a branch, and
|
|
1898
|
+
* what it would build last is a fresh cage, moments before `process.exit`.
|
|
1899
|
+
*/
|
|
1900
|
+
if (this.shuttingDown) {
|
|
1901
|
+
log.warn('supervisor: not starting a session, this runner is going down', {
|
|
1902
|
+
sessionId: descriptor.id,
|
|
1903
|
+
});
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1869
1906
|
const existing = this.sessions.get(descriptor.id);
|
|
1870
1907
|
if (existing) {
|
|
1871
1908
|
// A resume (higher epoch) can land while the previous life is still
|
|
@@ -2192,6 +2229,22 @@ export class Supervisor {
|
|
|
2192
2229
|
});
|
|
2193
2230
|
return LAUNCH_REFUSED;
|
|
2194
2231
|
}
|
|
2232
|
+
/**
|
|
2233
|
+
* …and no agent at all once the daemon is on its way out (#431).
|
|
2234
|
+
*
|
|
2235
|
+
* The five one-shot relaunches live in the tail of `pumpEvents`, which runs
|
|
2236
|
+
* a microtask after the event stream of the dying process ends — and since
|
|
2237
|
+
* the shutdown waits for its cages, the process is still here to run it.
|
|
2238
|
+
* A launch there would put a NEW cage on the machine moments before
|
|
2239
|
+
* `process.exit`, leaving behind exactly the orphan this ticket is about.
|
|
2240
|
+
* Found by the independent review of #431.
|
|
2241
|
+
*/
|
|
2242
|
+
if (this.shuttingDown) {
|
|
2243
|
+
log.warn('supervisor: not launching an agent, this runner is going down', {
|
|
2244
|
+
sessionId: descriptor.id,
|
|
2245
|
+
});
|
|
2246
|
+
return LAUNCH_REFUSED;
|
|
2247
|
+
}
|
|
2195
2248
|
// An exhausted USD budget must not relaunch $0.01-floor processes (QA-96 F4).
|
|
2196
2249
|
// Codex reports no cost at all, so its costUsd never leaves 0 — gating on it
|
|
2197
2250
|
// would be a limit that can never fire while the UI shows $0.00. Those
|
|
@@ -2650,7 +2703,7 @@ export class Supervisor {
|
|
|
2650
2703
|
const { descriptor } = running;
|
|
2651
2704
|
try {
|
|
2652
2705
|
for await (const event of session.events) {
|
|
2653
|
-
this.forwardEvent(running, event);
|
|
2706
|
+
this.forwardEvent(running, event, session);
|
|
2654
2707
|
}
|
|
2655
2708
|
}
|
|
2656
2709
|
catch (error) {
|
|
@@ -3566,7 +3619,14 @@ export class Supervisor {
|
|
|
3566
3619
|
this.syncBudgetClock(running);
|
|
3567
3620
|
this.reportStatus(running.descriptor.id, 'RUNNING', {});
|
|
3568
3621
|
}
|
|
3569
|
-
forwardEvent(running, event
|
|
3622
|
+
forwardEvent(running, event,
|
|
3623
|
+
/**
|
|
3624
|
+
* The process this event came from (#422). A refusal and a working turn are
|
|
3625
|
+
* attributed to the account THAT process started under – read off the
|
|
3626
|
+
* session that produced the event, not off whatever holds the slot by the
|
|
3627
|
+
* time the event is handled.
|
|
3628
|
+
*/
|
|
3629
|
+
source = running.session) {
|
|
3570
3630
|
const { descriptor } = running;
|
|
3571
3631
|
// Ticket #225: ANY event is the process proving it came up — its own
|
|
3572
3632
|
// capabilities probe answers within about two seconds of a healthy launch,
|
|
@@ -3760,7 +3820,7 @@ export class Supervisor {
|
|
|
3760
3820
|
if (event.code === 'auth_expired') {
|
|
3761
3821
|
const refused = relayAgent(String(descriptor.agent).toLowerCase());
|
|
3762
3822
|
if (refused)
|
|
3763
|
-
noteAgentAuthFailure(refused);
|
|
3823
|
+
noteAgentAuthFailure(refused, source?.accountId);
|
|
3764
3824
|
}
|
|
3765
3825
|
// Only `auth_missing` is worth a relaunch, and the distinction is the
|
|
3766
3826
|
// adapter's own (`probeAuth`), not a guess from prose.
|
|
@@ -3865,7 +3925,7 @@ export class Supervisor {
|
|
|
3865
3925
|
if (event.role === 'assistant') {
|
|
3866
3926
|
const working = relayAgent(String(descriptor.agent).toLowerCase());
|
|
3867
3927
|
if (working)
|
|
3868
|
-
clearAgentAuthFailure(working);
|
|
3928
|
+
clearAgentAuthFailure(working, source?.accountId);
|
|
3869
3929
|
}
|
|
3870
3930
|
this.sendEvent(running, 'message', { role: event.role, text: event.text });
|
|
3871
3931
|
return;
|
|
@@ -7431,6 +7491,11 @@ export class Supervisor {
|
|
|
7431
7491
|
});
|
|
7432
7492
|
}
|
|
7433
7493
|
default:
|
|
7494
|
+
// #422: the five account commands – their own module, one door each
|
|
7495
|
+
// over `claude-homes.ts` (plan §8).
|
|
7496
|
+
if (isAccountCommand(frame.name)) {
|
|
7497
|
+
return void reply(await runAccountCommand(frame.name, frame.args ?? {}, this.authRelay));
|
|
7498
|
+
}
|
|
7434
7499
|
return void reply({ ok: false, error: `Unknown command` });
|
|
7435
7500
|
}
|
|
7436
7501
|
}
|
|
@@ -7676,8 +7741,40 @@ export class Supervisor {
|
|
|
7676
7741
|
// changed, and the interval in the constructor is the net under both.
|
|
7677
7742
|
this.publishSlots();
|
|
7678
7743
|
}
|
|
7679
|
-
/**
|
|
7680
|
-
|
|
7744
|
+
/**
|
|
7745
|
+
* How long the whole fleet of cages is given to stop before this process goes
|
|
7746
|
+
* anyway (#431).
|
|
7747
|
+
*
|
|
7748
|
+
* The bus has been measured at 2.7 s under load and the stops run in
|
|
7749
|
+
* parallel, so three seconds is room for all of them on a bad day. Going over
|
|
7750
|
+
* it costs nothing: `stop` has been ACCEPTED by then, and systemd finishes
|
|
7751
|
+
* the job whether or not anybody is still here to watch. systemd's own
|
|
7752
|
+
* patience with this service is `TimeoutStopSec`, far above this.
|
|
7753
|
+
*/
|
|
7754
|
+
static CAGE_STOP_BUDGET_MS = 3_000;
|
|
7755
|
+
/**
|
|
7756
|
+
* Graceful daemon shutdown: kill agents, take their cages with them, keep
|
|
7757
|
+
* sessions resumable server-side.
|
|
7758
|
+
*
|
|
7759
|
+
* **Everything that has to survive this process happens BEFORE the first
|
|
7760
|
+
* `await`** — the withdrawn cards, the feed line, and the word to each agent
|
|
7761
|
+
* that it is over (a signal for Codex; for Claude, stdin closed and the SDK's
|
|
7762
|
+
* own `close()`, whose SIGTERM is a timer this process will not live to see —
|
|
7763
|
+
* on a caged machine what actually ends it is the stop below). A caller that
|
|
7764
|
+
* does not wait (the suite does not) gets exactly the shutdown it always got;
|
|
7765
|
+
* what it misses is only the stop of the cgroups. Written this way rather
|
|
7766
|
+
* than as two methods because «kill the agent» and «put its cage out» are one
|
|
7767
|
+
* action, and the shape of the code is what stops them drifting apart again
|
|
7768
|
+
* (грабля §479).
|
|
7769
|
+
*
|
|
7770
|
+
* What the caller owes in return: close the socket between the two halves.
|
|
7771
|
+
* The events above need it open, and the seconds below must not be a window
|
|
7772
|
+
* in which a `session_start` arrives at a supervisor that has already let go
|
|
7773
|
+
* of everything (`index.ts`).
|
|
7774
|
+
*/
|
|
7775
|
+
async shutdown() {
|
|
7776
|
+
// First line: every door that could put work back on this machine reads it.
|
|
7777
|
+
this.shuttingDown = true;
|
|
7681
7778
|
clearInterval(this.slotsTimer);
|
|
7682
7779
|
clearInterval(this.hostLoadTimer);
|
|
7683
7780
|
clearInterval(this.stallTimer);
|
|
@@ -7686,6 +7783,10 @@ export class Supervisor {
|
|
|
7686
7783
|
clearTimeout(this.agentCleanupFirstTimer);
|
|
7687
7784
|
clearInterval(this.agentCleanupTimer);
|
|
7688
7785
|
this.authRelay.cancel();
|
|
7786
|
+
const unitOf = this.opts.sessionScopeUnitOf ?? sessionScopeUnitOf;
|
|
7787
|
+
const readProcesses = this.opts.readScopeProcesses ?? readScopeProcesses;
|
|
7788
|
+
const agentPidOf = this.opts.sessionAgentPid ?? sessionAgentPid;
|
|
7789
|
+
const cages = [];
|
|
7689
7790
|
for (const running of this.sessions.values()) {
|
|
7690
7791
|
this.clearBudgetTimers(running);
|
|
7691
7792
|
// BEFORE stop(), and from here rather than from the adapter: the adapter
|
|
@@ -7707,15 +7808,86 @@ export class Supervisor {
|
|
|
7707
7808
|
error: String(error),
|
|
7708
7809
|
});
|
|
7709
7810
|
}
|
|
7811
|
+
/**
|
|
7812
|
+
* What this restart costs, in the person's units, before anything is
|
|
7813
|
+
* killed (#431).
|
|
7814
|
+
*
|
|
7815
|
+
* The count comes from the cage itself rather than from anything this
|
|
7816
|
+
* process believes: the runner does not track what the agent started —
|
|
7817
|
+
* that is the whole shape of the problem — and the cgroup does. Read
|
|
7818
|
+
* BEFORE `stop()`, because a moment later there is nothing left to count.
|
|
7819
|
+
*
|
|
7820
|
+
* Silent when the number is zero: an agent that was only thinking loses
|
|
7821
|
+
* nothing, and «0 commands were stopped» is a line a person has to read
|
|
7822
|
+
* and cannot use (D8).
|
|
7823
|
+
*/
|
|
7824
|
+
const unit = unitOf(running.descriptor.id);
|
|
7825
|
+
if (unit !== null) {
|
|
7826
|
+
cages.push(unit);
|
|
7827
|
+
try {
|
|
7828
|
+
const commands = countRunningCommands(readProcesses(unit), agentPidOf(running.descriptor.id));
|
|
7829
|
+
if (commands > 0) {
|
|
7830
|
+
this.sendEvent(running, 'notice', {
|
|
7831
|
+
level: 'warn',
|
|
7832
|
+
text: restartStoppedCommands(commands),
|
|
7833
|
+
});
|
|
7834
|
+
}
|
|
7835
|
+
}
|
|
7836
|
+
catch (error) {
|
|
7837
|
+
// /proc said nothing readable. The cage is still stopped below — the
|
|
7838
|
+
// person simply does not get the number.
|
|
7839
|
+
log.warn('supervisor: could not count what this session was running', {
|
|
7840
|
+
sessionId: running.descriptor.id,
|
|
7841
|
+
error: String(error),
|
|
7842
|
+
});
|
|
7843
|
+
}
|
|
7844
|
+
}
|
|
7710
7845
|
// The reason matters here: this path also runs for «Update runner», and a
|
|
7711
7846
|
// card that vanishes during an update must say why (session 12).
|
|
7712
|
-
|
|
7847
|
+
//
|
|
7848
|
+
// Guarded like its neighbours in this loop: one adapter that throws on
|
|
7849
|
+
// its way out must not cost every OTHER session the stop of its cage —
|
|
7850
|
+
// the lines below it are what puts them out (independent review of #431).
|
|
7851
|
+
try {
|
|
7852
|
+
running.session?.stop('runner_restarted');
|
|
7853
|
+
}
|
|
7854
|
+
catch (error) {
|
|
7855
|
+
log.warn('supervisor: an agent did not take its stop cleanly', {
|
|
7856
|
+
sessionId: running.descriptor.id,
|
|
7857
|
+
error: String(error),
|
|
7858
|
+
});
|
|
7859
|
+
}
|
|
7713
7860
|
}
|
|
7714
7861
|
this.sessions.clear();
|
|
7715
7862
|
// A build that was mid-flight is abandoned, not judged: its row stays
|
|
7716
7863
|
// RUNNING until the API's sweep turns it into LOST. A verdict nobody
|
|
7717
7864
|
// observed must never become PASSED.
|
|
7718
7865
|
this.verify.shutdown();
|
|
7866
|
+
await this.stopCages(cages);
|
|
7867
|
+
}
|
|
7868
|
+
/**
|
|
7869
|
+
* Put out every cage this daemon was holding, in parallel and on a clock.
|
|
7870
|
+
*
|
|
7871
|
+
* Until 0.65.0 nothing did this: `releaseSessionScope` hangs off the agent
|
|
7872
|
+
* process's `exit`, and on a restart the daemon is gone before that event
|
|
7873
|
+
* fires. What stayed behind was a whole tree — a build, its docker client,
|
|
7874
|
+
* their children — holding the name of a scope systemd will then refuse to
|
|
7875
|
+
* reuse, so the FIRST start of that session after the restart died as
|
|
7876
|
+
* «exited before the session was ready (code 1)» (#431).
|
|
7877
|
+
*/
|
|
7878
|
+
async stopCages(units) {
|
|
7879
|
+
if (units.length === 0)
|
|
7880
|
+
return;
|
|
7881
|
+
const stop = this.opts.stopSessionScope ?? stopSessionScope;
|
|
7882
|
+
log.info('supervisor: stopping the cages of this runner', { count: units.length });
|
|
7883
|
+
const all = Promise.allSettled(units.map((unit) => stop(unit))).then(() => undefined);
|
|
7884
|
+
let timer;
|
|
7885
|
+
const budget = new Promise((resolve) => {
|
|
7886
|
+
timer = setTimeout(resolve, this.opts.cageStopBudgetMs ?? Supervisor.CAGE_STOP_BUDGET_MS);
|
|
7887
|
+
});
|
|
7888
|
+
await Promise.race([all, budget]);
|
|
7889
|
+
if (timer)
|
|
7890
|
+
clearTimeout(timer);
|
|
7719
7891
|
}
|
|
7720
7892
|
}
|
|
7721
7893
|
/**
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const RUNNER_VERSION = "0.
|
|
1
|
+
export declare const RUNNER_VERSION = "0.66.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
package/dist/ws-client.js
CHANGED
|
@@ -137,6 +137,20 @@ export class RunnerWsClient {
|
|
|
137
137
|
this.lastInboundAt = Date.now();
|
|
138
138
|
});
|
|
139
139
|
socket.on('message', (data) => {
|
|
140
|
+
/**
|
|
141
|
+
* A client that was stopped answers nothing, in either direction (#431).
|
|
142
|
+
*
|
|
143
|
+
* `close()` is the START of a handshake, not a cut: the socket lives on
|
|
144
|
+
* for a round trip, and this listener used to keep delivering frames off
|
|
145
|
+
* it. That was harmless while every caller of `stop()` exited in the same
|
|
146
|
+
* tick; since the daemon waits for its cages to be put out, the window is
|
|
147
|
+
* seconds wide, and the supervisor on the other side of `frame` has
|
|
148
|
+
* already let go of every session. An `event_ack` landing there DELETES
|
|
149
|
+
* the journal of a live session (found by the independent review of
|
|
150
|
+
* #431); a `session_start` starts an agent nobody will ever stop.
|
|
151
|
+
*/
|
|
152
|
+
if (this.stopped)
|
|
153
|
+
return;
|
|
140
154
|
this.lastInboundAt = Date.now();
|
|
141
155
|
let parsed;
|
|
142
156
|
try {
|
package/package.json
CHANGED