@bridge4dev/runner 0.46.1 → 0.47.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/README.md +44 -2
- package/dist/adapters/claude.js +6 -2
- package/dist/agent-auto-update.d.ts +75 -0
- package/dist/agent-auto-update.js +134 -0
- package/dist/agent-binary.d.ts +48 -0
- package/dist/agent-binary.js +55 -0
- package/dist/agent-cleanup.d.ts +78 -0
- package/dist/agent-cleanup.js +184 -0
- package/dist/agent-install.d.ts +140 -0
- package/dist/agent-install.js +475 -0
- package/dist/agent-registry.d.ts +223 -0
- package/dist/agent-registry.js +131 -0
- package/dist/agent-versions.d.ts +93 -0
- package/dist/agent-versions.js +157 -0
- package/dist/auth-relay.d.ts +9 -1
- package/dist/auth-relay.js +3 -1
- package/dist/commit-message.js +5 -0
- package/dist/config.d.ts +44 -4
- package/dist/config.js +43 -0
- package/dist/index.js +46 -12
- package/dist/protocol.d.ts +155 -28
- package/dist/protocol.js +33 -1
- package/dist/recipe-schema.d.ts +12 -12
- package/dist/self-update.d.ts +14 -0
- package/dist/self-update.js +45 -13
- package/dist/supervisor.d.ts +155 -2
- package/dist/supervisor.js +393 -8
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,16 +69,58 @@ it. If you would rather run it in the foreground, or under your own supervisor,
|
|
|
69
69
|
|
|
70
70
|
Both honour `XDG_CONFIG_HOME` / `XDG_STATE_HOME`.
|
|
71
71
|
|
|
72
|
+
## What the machine's owner can switch off
|
|
73
|
+
|
|
74
|
+
`config.toml` is read by the runner and cannot be changed from the dashboard — these are
|
|
75
|
+
the owner's vetoes, and they survive re-pairing. A capability switched off here is not
|
|
76
|
+
announced to the server at all, so the dashboard draws no control rather than a control
|
|
77
|
+
that answers «refused».
|
|
78
|
+
|
|
79
|
+
```toml
|
|
80
|
+
[verify]
|
|
81
|
+
enabled = true # running the project's own build/test recipe
|
|
82
|
+
|
|
83
|
+
[checkpoints]
|
|
84
|
+
enabled = true # restore points (copies of the working tree, on this disk)
|
|
85
|
+
|
|
86
|
+
[agents]
|
|
87
|
+
install_enabled = true # installing and updating the agent CLIs (0.47.0)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`[agents] install_enabled = false` covers both routes at once: the **Install** / **Update**
|
|
91
|
+
buttons on the server card and the automatic update a session start would otherwise
|
|
92
|
+
trigger. It is the same download onto the same disk either way.
|
|
93
|
+
|
|
94
|
+
## Agent CLIs
|
|
95
|
+
|
|
96
|
+
Since 0.47.0 the runner does not carry a Claude binary and the installer does not install
|
|
97
|
+
agents. Both `claude` and `codex` are ordinary system installs, and the server card shows
|
|
98
|
+
each one with its version:
|
|
99
|
+
|
|
100
|
+
- **Install / Update** run the vendor's own installer for that agent, at a version the
|
|
101
|
+
card picked from the registry. Free space is checked first, the new binary is smoke-tested
|
|
102
|
+
by running it, and a build that will not run is rolled back. **The daemon does not
|
|
103
|
+
restart** — sessions already running keep the file they opened, and the new version takes
|
|
104
|
+
effect for the next one.
|
|
105
|
+
- **Auto-update agents** (on by default) does the same thing on its own when a session
|
|
106
|
+
starts, for that session's agent only, at most once a day per agent.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
claude --version
|
|
110
|
+
codex --version
|
|
111
|
+
```
|
|
112
|
+
|
|
72
113
|
## Updating
|
|
73
114
|
|
|
74
115
|
From the dashboard: the server card offers **Update runner** once a newer version is
|
|
75
116
|
available, and the runner replaces itself — smoke-testing the new build before it
|
|
76
|
-
restarts, and rolling back to the previous tarball if that test fails.
|
|
117
|
+
restarts, and rolling back to the previous tarball if that test fails. **Updating the
|
|
118
|
+
runner disconnects every session running on this machine**; updating an agent does not.
|
|
77
119
|
|
|
78
120
|
By hand:
|
|
79
121
|
|
|
80
122
|
```bash
|
|
81
|
-
npm install -g @bridge4dev/runner && systemctl --user restart devbridge-runner
|
|
123
|
+
npm install -g --omit=optional @bridge4dev/runner && systemctl --user restart devbridge-runner
|
|
82
124
|
```
|
|
83
125
|
|
|
84
126
|
## License
|
package/dist/adapters/claude.js
CHANGED
|
@@ -8,7 +8,7 @@ import { evaluateToolUse, maskSecrets, maskString, } from '../policy.js';
|
|
|
8
8
|
import { availableModes, MODE_REFUSED_TEXT, MODE_WITHDRAWN_TEXT, DEVBRIDGE_MCP_SERVER_NAME, } from './types.js';
|
|
9
9
|
import { percentFromUtilization, RATE_WINDOW_MINUTES, rateWindowKey } from './rate-limits.js';
|
|
10
10
|
import { applyUsagePercentages, parseUsageText, probeUsageText } from './claude-usage.js';
|
|
11
|
-
import {
|
|
11
|
+
import { claudeExecutableOption, sessionClaudePath } from '../agent-binary.js';
|
|
12
12
|
/** How often `/usage` may be read. Free, but still a process. */
|
|
13
13
|
const USAGE_PROBE_INTERVAL_MS = 3 * 60 * 1000;
|
|
14
14
|
import { answerSummary, answerValue, discussMessage, invalidationMessage, mirrorOptions, newAskId, MAX_OPTIONS, MAX_QUESTIONS, OPTION_TEXT_LIMIT, QUESTION_TEXT_LIMIT, } from './questions.js';
|
|
@@ -501,6 +501,10 @@ class ClaudeSession {
|
|
|
501
501
|
const options = {
|
|
502
502
|
cwd: spec.cwd,
|
|
503
503
|
env: agentEnv(this.mode),
|
|
504
|
+
// Empty while `USE_BUNDLED_CLAUDE` — the SDK keeps resolving its own
|
|
505
|
+
// bundled binary, exactly as before. After C3 this pins the system
|
|
506
|
+
// `claude`, which is the file the card measures and the button installs.
|
|
507
|
+
...claudeExecutableOption(),
|
|
504
508
|
/**
|
|
505
509
|
* Everything the machine's own Claude has (owner's call, 2026-07-30).
|
|
506
510
|
*
|
|
@@ -785,7 +789,7 @@ class ClaudeSession {
|
|
|
785
789
|
if (this.usageProbedAt !== 0 && Date.now() - this.usageProbedAt < USAGE_PROBE_INTERVAL_MS) {
|
|
786
790
|
return;
|
|
787
791
|
}
|
|
788
|
-
const binary =
|
|
792
|
+
const binary = sessionClaudePath();
|
|
789
793
|
if (!binary)
|
|
790
794
|
return;
|
|
791
795
|
this.usageProbedAt = Date.now();
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type AgentRuntime } from './agent-registry.js';
|
|
2
|
+
import type { MeasuredAgentVersion } from './agent-versions.js';
|
|
3
|
+
/**
|
|
4
|
+
* Moving an agent CLI forward on its own, when a session on that agent starts
|
|
5
|
+
* (Р13, §4.8 of the plan).
|
|
6
|
+
*
|
|
7
|
+
* Not a scheduler and not our idea of a policy — it is the update path both
|
|
8
|
+
* vendors already have, reconnected to the way DevBridge actually launches
|
|
9
|
+
* them. Claude updates itself when a human types `claude` in a terminal; Codex
|
|
10
|
+
* only prints a banner, and only in its terminal UI. On a dev server nobody
|
|
11
|
+
* types either: the runner starts `claude` through the SDK and `codex
|
|
12
|
+
* app-server`, and in that mode NEITHER agent ever moves. Left alone, a machine
|
|
13
|
+
* stays on whatever version the day of its last manual install left behind.
|
|
14
|
+
*
|
|
15
|
+
* Two files' worth of care go into what this must NOT do:
|
|
16
|
+
*
|
|
17
|
+
* - **it must not delay a session.** The person asked for a session, not for
|
|
18
|
+
* an update. The install runs behind the start and the current session
|
|
19
|
+
* finishes on the file it already opened — a running process keeps its own
|
|
20
|
+
* inode, so replacing the binary underneath it is invisible to it;
|
|
21
|
+
* - **it must not try again every time.** Once a day per (agent, machine), and
|
|
22
|
+
* the mark is written on disk BEFORE the attempt, so a machine with no disk
|
|
23
|
+
* space and no network gives up for the day instead of downloading nothing
|
|
24
|
+
* over and over at every session start.
|
|
25
|
+
*/
|
|
26
|
+
/** One attempt per agent per day, whatever the outcome. */
|
|
27
|
+
export declare const AUTO_UPDATE_WINDOW_MS: number;
|
|
28
|
+
/**
|
|
29
|
+
* Take today's single attempt for this agent, or refuse.
|
|
30
|
+
*
|
|
31
|
+
* Records the attempt itself rather than trusting the caller to, and records it
|
|
32
|
+
* **before** the install runs — a download that dies halfway has still used the
|
|
33
|
+
* day. `auto-resume.ts` claims the same way and for the same reason: a ceiling
|
|
34
|
+
* that only counts successes is not a ceiling.
|
|
35
|
+
*
|
|
36
|
+
* On disk, not in memory: a runner restarted by a crash loop would otherwise
|
|
37
|
+
* start every session with a fresh allowance.
|
|
38
|
+
*
|
|
39
|
+
* The window is measured on the ABSOLUTE distance, which is what makes a clock
|
|
40
|
+
* that moved matter correctly in both directions. A VM resumed from a snapshot
|
|
41
|
+
* or an ntp step back an hour must not hand out a second attempt — that is the
|
|
42
|
+
* common case, and `now - last` alone would let it through. A mark that is a
|
|
43
|
+
* year in the future is a machine whose clock was simply wrong when it was
|
|
44
|
+
* written, and freezing auto-update on it until the calendar catches up would
|
|
45
|
+
* be worse than one extra install.
|
|
46
|
+
*/
|
|
47
|
+
export declare function claimAgentAutoUpdate(wireKey: string, now?: number): boolean;
|
|
48
|
+
/** Test seam — the ledger is a file, and tests need it empty. */
|
|
49
|
+
export declare function forgetAgentAutoUpdates(): void;
|
|
50
|
+
/**
|
|
51
|
+
* Why we are not updating this agent right now — or `null`, meaning go ahead.
|
|
52
|
+
*
|
|
53
|
+
* A string rather than a boolean so the log line says which of the six it was.
|
|
54
|
+
* Every one of them is «leave the machine exactly as it is», and four of them
|
|
55
|
+
* are refusals the plan states outright:
|
|
56
|
+
*
|
|
57
|
+
* - **not installed** — installing an absent agent is a person's decision (Р3);
|
|
58
|
+
* - **somebody else's package manager** — replacing a Homebrew copy would leave
|
|
59
|
+
* two installations fighting over one command name;
|
|
60
|
+
* - **the probe failed** — the binary is there and will not say what it is, so
|
|
61
|
+
* there is no version to roll back to if the new one is broken;
|
|
62
|
+
* - **already current or ahead** — a machine ahead of our snapshot is a machine
|
|
63
|
+
* whose owner updated it by hand an hour ago, and walking it back would be a
|
|
64
|
+
* downgrade nobody asked for.
|
|
65
|
+
*
|
|
66
|
+
* Deliberately pure: no clock, no disk, no processes. The claim and the lock are
|
|
67
|
+
* the caller's, because only the caller can hold them for the length of an
|
|
68
|
+
* install.
|
|
69
|
+
*/
|
|
70
|
+
export declare function autoUpdateRefusal(input: {
|
|
71
|
+
runtime: AgentRuntime;
|
|
72
|
+
measured: MeasuredAgentVersion;
|
|
73
|
+
latest: string;
|
|
74
|
+
}): string | null;
|
|
75
|
+
//# sourceMappingURL=agent-auto-update.d.ts.map
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { compareAgentVersions } from './agent-registry.js';
|
|
4
|
+
import { stateDir } from './paths.js';
|
|
5
|
+
/**
|
|
6
|
+
* Moving an agent CLI forward on its own, when a session on that agent starts
|
|
7
|
+
* (Р13, §4.8 of the plan).
|
|
8
|
+
*
|
|
9
|
+
* Not a scheduler and not our idea of a policy — it is the update path both
|
|
10
|
+
* vendors already have, reconnected to the way DevBridge actually launches
|
|
11
|
+
* them. Claude updates itself when a human types `claude` in a terminal; Codex
|
|
12
|
+
* only prints a banner, and only in its terminal UI. On a dev server nobody
|
|
13
|
+
* types either: the runner starts `claude` through the SDK and `codex
|
|
14
|
+
* app-server`, and in that mode NEITHER agent ever moves. Left alone, a machine
|
|
15
|
+
* stays on whatever version the day of its last manual install left behind.
|
|
16
|
+
*
|
|
17
|
+
* Two files' worth of care go into what this must NOT do:
|
|
18
|
+
*
|
|
19
|
+
* - **it must not delay a session.** The person asked for a session, not for
|
|
20
|
+
* an update. The install runs behind the start and the current session
|
|
21
|
+
* finishes on the file it already opened — a running process keeps its own
|
|
22
|
+
* inode, so replacing the binary underneath it is invisible to it;
|
|
23
|
+
* - **it must not try again every time.** Once a day per (agent, machine), and
|
|
24
|
+
* the mark is written on disk BEFORE the attempt, so a machine with no disk
|
|
25
|
+
* space and no network gives up for the day instead of downloading nothing
|
|
26
|
+
* over and over at every session start.
|
|
27
|
+
*/
|
|
28
|
+
/** One attempt per agent per day, whatever the outcome. */
|
|
29
|
+
export const AUTO_UPDATE_WINDOW_MS = 24 * 60 * 60 * 1_000;
|
|
30
|
+
function ledgerPath() {
|
|
31
|
+
return path.join(stateDir(), 'agent-auto-update.json');
|
|
32
|
+
}
|
|
33
|
+
function read() {
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(fs.readFileSync(ledgerPath(), 'utf8'));
|
|
36
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
37
|
+
return {};
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Missing, unreadable or half-written — «nobody has tried today» is the
|
|
42
|
+
// answer that keeps the feature working rather than the one that is safe,
|
|
43
|
+
// and the cost of being wrong here is one extra install per day.
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function write(ledger) {
|
|
48
|
+
try {
|
|
49
|
+
const dir = stateDir();
|
|
50
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
51
|
+
const tmp = path.join(dir, `.agent-auto-update.json.${process.pid}.tmp`);
|
|
52
|
+
fs.writeFileSync(tmp, JSON.stringify(ledger), { mode: 0o600 });
|
|
53
|
+
fs.renameSync(tmp, ledgerPath());
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// A machine that cannot write its state directory has bigger problems than
|
|
57
|
+
// a missed throttle, and failing to record an attempt must never be the
|
|
58
|
+
// thing that stops a session from starting.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Take today's single attempt for this agent, or refuse.
|
|
63
|
+
*
|
|
64
|
+
* Records the attempt itself rather than trusting the caller to, and records it
|
|
65
|
+
* **before** the install runs — a download that dies halfway has still used the
|
|
66
|
+
* day. `auto-resume.ts` claims the same way and for the same reason: a ceiling
|
|
67
|
+
* that only counts successes is not a ceiling.
|
|
68
|
+
*
|
|
69
|
+
* On disk, not in memory: a runner restarted by a crash loop would otherwise
|
|
70
|
+
* start every session with a fresh allowance.
|
|
71
|
+
*
|
|
72
|
+
* The window is measured on the ABSOLUTE distance, which is what makes a clock
|
|
73
|
+
* that moved matter correctly in both directions. A VM resumed from a snapshot
|
|
74
|
+
* or an ntp step back an hour must not hand out a second attempt — that is the
|
|
75
|
+
* common case, and `now - last` alone would let it through. A mark that is a
|
|
76
|
+
* year in the future is a machine whose clock was simply wrong when it was
|
|
77
|
+
* written, and freezing auto-update on it until the calendar catches up would
|
|
78
|
+
* be worse than one extra install.
|
|
79
|
+
*/
|
|
80
|
+
export function claimAgentAutoUpdate(wireKey, now = Date.now()) {
|
|
81
|
+
const ledger = read();
|
|
82
|
+
const last = ledger[wireKey]?.at;
|
|
83
|
+
if (typeof last === 'number' && Math.abs(now - last) < AUTO_UPDATE_WINDOW_MS)
|
|
84
|
+
return false;
|
|
85
|
+
ledger[wireKey] = { at: now };
|
|
86
|
+
write(ledger);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
/** Test seam — the ledger is a file, and tests need it empty. */
|
|
90
|
+
export function forgetAgentAutoUpdates() {
|
|
91
|
+
try {
|
|
92
|
+
fs.rmSync(ledgerPath(), { force: true });
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
/* nothing to forget */
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Why we are not updating this agent right now — or `null`, meaning go ahead.
|
|
100
|
+
*
|
|
101
|
+
* A string rather than a boolean so the log line says which of the six it was.
|
|
102
|
+
* Every one of them is «leave the machine exactly as it is», and four of them
|
|
103
|
+
* are refusals the plan states outright:
|
|
104
|
+
*
|
|
105
|
+
* - **not installed** — installing an absent agent is a person's decision (Р3);
|
|
106
|
+
* - **somebody else's package manager** — replacing a Homebrew copy would leave
|
|
107
|
+
* two installations fighting over one command name;
|
|
108
|
+
* - **the probe failed** — the binary is there and will not say what it is, so
|
|
109
|
+
* there is no version to roll back to if the new one is broken;
|
|
110
|
+
* - **already current or ahead** — a machine ahead of our snapshot is a machine
|
|
111
|
+
* whose owner updated it by hand an hour ago, and walking it back would be a
|
|
112
|
+
* downgrade nobody asked for.
|
|
113
|
+
*
|
|
114
|
+
* Deliberately pure: no clock, no disk, no processes. The claim and the lock are
|
|
115
|
+
* the caller's, because only the caller can hold them for the length of an
|
|
116
|
+
* install.
|
|
117
|
+
*/
|
|
118
|
+
export function autoUpdateRefusal(input) {
|
|
119
|
+
const { runtime, measured, latest } = input;
|
|
120
|
+
if (measured.probeFailed)
|
|
121
|
+
return 'the version probe failed';
|
|
122
|
+
if (measured.version === null)
|
|
123
|
+
return 'not installed here';
|
|
124
|
+
if (measured.managedBy !== runtime.install.managedBy) {
|
|
125
|
+
return `installed with ${measured.managedBy}`;
|
|
126
|
+
}
|
|
127
|
+
const behind = compareAgentVersions(measured.version, latest);
|
|
128
|
+
if (behind === null)
|
|
129
|
+
return 'the versions are not comparable';
|
|
130
|
+
if (behind >= 0)
|
|
131
|
+
return 'already up to date';
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=agent-auto-update.js.map
|
package/dist/agent-binary.d.ts
CHANGED
|
@@ -25,4 +25,52 @@ export declare function findClaudeCli(packageDir: string): string | null;
|
|
|
25
25
|
* platform package is reachable from the SDK and from nowhere else.
|
|
26
26
|
*/
|
|
27
27
|
export declare function claudeCliPath(): string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Which Claude the SESSIONS run on.
|
|
30
|
+
*
|
|
31
|
+
* `true` — the binary bundled inside this runner's own installation, as an
|
|
32
|
+
* optional platform package of the SDK. That is how it has always worked, and
|
|
33
|
+
* it is why «update Claude» has meant «reinstall the runner», which kills every
|
|
34
|
+
* live session on the machine (гоча #414).
|
|
35
|
+
*
|
|
36
|
+
* `false` — the system `claude` on PATH: installed, measured and updated like
|
|
37
|
+
* any other agent, by the same button, without touching the daemon (Р1, Р10).
|
|
38
|
+
*
|
|
39
|
+
* **Flipped to `false` in 0.47.0** (#371, stage C3). It stayed `true` through
|
|
40
|
+
* the whole implementation on purpose: the API image packs this worktree into
|
|
41
|
+
* the tarball every dev server updates from, so an early flip would have moved
|
|
42
|
+
* the fleet onto a system binary half of them did not have — before the button
|
|
43
|
+
* that installs it existed. That button ships in the same release, the card
|
|
44
|
+
* says «Install» on any machine that needs it, and Р11 is the owner's decision
|
|
45
|
+
* that there are no other safety nets: every server is one we can reach.
|
|
46
|
+
*
|
|
47
|
+
* The bundled code stays compiled in rather than deleted, so the way back is
|
|
48
|
+
* this one value rather than a revert. Two things must move with it if it ever
|
|
49
|
+
* goes back to `true`: the `--omit=optional` in `self-update.ts` (the binary
|
|
50
|
+
* would stop being installed at all) and the guard test below it.
|
|
51
|
+
*/
|
|
52
|
+
export declare const USE_BUNDLED_CLAUDE = false;
|
|
53
|
+
/**
|
|
54
|
+
* The Claude executable a session should be started with, or null when there is
|
|
55
|
+
* none to start.
|
|
56
|
+
*
|
|
57
|
+
* The two answers are genuinely different files of different versions on the
|
|
58
|
+
* same machine — measured seven versions apart on this very dev server — so
|
|
59
|
+
* every caller must go through here rather than pick one.
|
|
60
|
+
*
|
|
61
|
+
* The parameter exists so both branches can be tested without mocking a module
|
|
62
|
+
* the tests also need the real behaviour of; production always takes the default.
|
|
63
|
+
*/
|
|
64
|
+
export declare function sessionClaudePath(useBundled?: boolean): string | null;
|
|
65
|
+
/**
|
|
66
|
+
* The SDK option that pins the executable, for both places that build `Options`.
|
|
67
|
+
*
|
|
68
|
+
* Empty while the bundled binary is in use: the SDK then resolves its own, which
|
|
69
|
+
* is exactly today's behaviour and keeps `main` neutral. Empty also when the
|
|
70
|
+
* system binary is missing — passing a path we know is not there would turn a
|
|
71
|
+
* clear «Claude is not installed» into an SDK spawn error.
|
|
72
|
+
*/
|
|
73
|
+
export declare function claudeExecutableOption(useBundled?: boolean): {
|
|
74
|
+
pathToClaudeCodeExecutable?: string;
|
|
75
|
+
};
|
|
28
76
|
//# sourceMappingURL=agent-binary.d.ts.map
|
package/dist/agent-binary.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { AGENT_RUNTIMES } from './agent-registry.js';
|
|
5
|
+
import { whichExecutable } from './environment.js';
|
|
4
6
|
/**
|
|
5
7
|
* Is the binary this runner would actually launch Claude with present?
|
|
6
8
|
*
|
|
@@ -96,4 +98,57 @@ function resolveFromSdkEntry(sdkEntry) {
|
|
|
96
98
|
}
|
|
97
99
|
return null;
|
|
98
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Which Claude the SESSIONS run on.
|
|
103
|
+
*
|
|
104
|
+
* `true` — the binary bundled inside this runner's own installation, as an
|
|
105
|
+
* optional platform package of the SDK. That is how it has always worked, and
|
|
106
|
+
* it is why «update Claude» has meant «reinstall the runner», which kills every
|
|
107
|
+
* live session on the machine (гоча #414).
|
|
108
|
+
*
|
|
109
|
+
* `false` — the system `claude` on PATH: installed, measured and updated like
|
|
110
|
+
* any other agent, by the same button, without touching the daemon (Р1, Р10).
|
|
111
|
+
*
|
|
112
|
+
* **Flipped to `false` in 0.47.0** (#371, stage C3). It stayed `true` through
|
|
113
|
+
* the whole implementation on purpose: the API image packs this worktree into
|
|
114
|
+
* the tarball every dev server updates from, so an early flip would have moved
|
|
115
|
+
* the fleet onto a system binary half of them did not have — before the button
|
|
116
|
+
* that installs it existed. That button ships in the same release, the card
|
|
117
|
+
* says «Install» on any machine that needs it, and Р11 is the owner's decision
|
|
118
|
+
* that there are no other safety nets: every server is one we can reach.
|
|
119
|
+
*
|
|
120
|
+
* The bundled code stays compiled in rather than deleted, so the way back is
|
|
121
|
+
* this one value rather than a revert. Two things must move with it if it ever
|
|
122
|
+
* goes back to `true`: the `--omit=optional` in `self-update.ts` (the binary
|
|
123
|
+
* would stop being installed at all) and the guard test below it.
|
|
124
|
+
*/
|
|
125
|
+
export const USE_BUNDLED_CLAUDE = false;
|
|
126
|
+
/**
|
|
127
|
+
* The Claude executable a session should be started with, or null when there is
|
|
128
|
+
* none to start.
|
|
129
|
+
*
|
|
130
|
+
* The two answers are genuinely different files of different versions on the
|
|
131
|
+
* same machine — measured seven versions apart on this very dev server — so
|
|
132
|
+
* every caller must go through here rather than pick one.
|
|
133
|
+
*
|
|
134
|
+
* The parameter exists so both branches can be tested without mocking a module
|
|
135
|
+
* the tests also need the real behaviour of; production always takes the default.
|
|
136
|
+
*/
|
|
137
|
+
export function sessionClaudePath(useBundled = USE_BUNDLED_CLAUDE) {
|
|
138
|
+
return useBundled ? claudeCliPath() : whichExecutable(AGENT_RUNTIMES.claude.bin);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The SDK option that pins the executable, for both places that build `Options`.
|
|
142
|
+
*
|
|
143
|
+
* Empty while the bundled binary is in use: the SDK then resolves its own, which
|
|
144
|
+
* is exactly today's behaviour and keeps `main` neutral. Empty also when the
|
|
145
|
+
* system binary is missing — passing a path we know is not there would turn a
|
|
146
|
+
* clear «Claude is not installed» into an SDK spawn error.
|
|
147
|
+
*/
|
|
148
|
+
export function claudeExecutableOption(useBundled = USE_BUNDLED_CLAUDE) {
|
|
149
|
+
if (useBundled)
|
|
150
|
+
return {};
|
|
151
|
+
const binary = sessionClaudePath(useBundled);
|
|
152
|
+
return binary ? { pathToClaudeCodeExecutable: binary } : {};
|
|
153
|
+
}
|
|
99
154
|
//# sourceMappingURL=agent-binary.js.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throwing away the Claude versions nobody will run again (Р12, §4.7).
|
|
3
|
+
*
|
|
4
|
+
* Anthropic's native installer keeps **every** version it has ever downloaded
|
|
5
|
+
* as a separate 200–330 MB executable in `~/.local/share/claude/versions/` and
|
|
6
|
+
* deletes none of them; the launcher on PATH is a symlink pointing at one. On
|
|
7
|
+
* this very dev server that was five copies and 1.4 GB with the disk 80 % full
|
|
8
|
+
* (гоча #414) — and stage D now moves the version forward by itself, so what
|
|
9
|
+
* used to grow by a file a month grows by a file a day.
|
|
10
|
+
*
|
|
11
|
+
* Three files are untouchable, and each is a different way to break a machine:
|
|
12
|
+
*
|
|
13
|
+
* 1. **whatever the launcher points at.** Delete it and `claude` on PATH is a
|
|
14
|
+
* dangling symlink: no sessions, no sign-in, and nothing on the card
|
|
15
|
+
* explaining it — `measureAgent` would report a probe failure, which stage 2
|
|
16
|
+
* already knows is a state with no way out from the interface.
|
|
17
|
+
* 2. **the version a rollback would restore.** A failed install puts the
|
|
18
|
+
* previous version back by pointing the launcher at its file, so «keep two»
|
|
19
|
+
* is not an arbitrary number: it is current + the one behind it.
|
|
20
|
+
* 3. **a version some process is still executing.** Claude Code launches its
|
|
21
|
+
* own helpers by its own path, and a session that started two updates ago is
|
|
22
|
+
* running the third file from the end. Unlinking a mapped executable does
|
|
23
|
+
* not kill it on Linux, but it does make every later `exec` of that path
|
|
24
|
+
* fail — and the caller's own gate (sweep only when this runner is tracking
|
|
25
|
+
* no sessions at all) cannot see a process somebody started by hand.
|
|
26
|
+
*/
|
|
27
|
+
/** Current + the one a rollback would restore. Р12 says two, and two it is. */
|
|
28
|
+
export declare const KEEP_VERSIONS = 2;
|
|
29
|
+
export interface VersionSweepResult {
|
|
30
|
+
/** Version numbers whose files were unlinked. */
|
|
31
|
+
removed: string[];
|
|
32
|
+
/** Freed bytes, as measured before the unlink. */
|
|
33
|
+
freedBytes: number;
|
|
34
|
+
/** Versions left alone, with the reason — the log line worth having. */
|
|
35
|
+
kept: Array<{
|
|
36
|
+
version: string;
|
|
37
|
+
reason: string;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The version the launcher on PATH resolves to, or null.
|
|
42
|
+
*
|
|
43
|
+
* Read by resolving the symlink rather than by running `claude --version`: the
|
|
44
|
+
* question is «which FILE», and this runs on a timer where spawning a 300 MB
|
|
45
|
+
* binary would be a poor way to ask. `realpath` follows a chain of links, so a
|
|
46
|
+
* distribution that put its own wrapper in between still answers correctly.
|
|
47
|
+
*/
|
|
48
|
+
export declare function currentNativeVersion(homeDir: string): string | null;
|
|
49
|
+
/**
|
|
50
|
+
* Version files at least one live process is executing right now, best effort.
|
|
51
|
+
*
|
|
52
|
+
* `/proc/<pid>/exe` is a symlink to the executable's real path, so this is an
|
|
53
|
+
* exact answer for every process this user may look at — and an empty answer on
|
|
54
|
+
* a platform without `/proc`. Unreadable entries are SKIPPED rather than treated
|
|
55
|
+
* as a match: another user's process cannot be running a file out of this user's
|
|
56
|
+
* home, and refusing to sweep whenever `/proc` holds anything opaque would mean
|
|
57
|
+
* never sweeping on a busy machine.
|
|
58
|
+
*/
|
|
59
|
+
export declare function versionsInUse(homeDir: string): Set<string>;
|
|
60
|
+
export interface VersionSweepOptions {
|
|
61
|
+
/** The daemon user's home. Only ever this one — never another account's. */
|
|
62
|
+
homeDir?: string;
|
|
63
|
+
keep?: number;
|
|
64
|
+
/** Test seam: which versions a process holds. */
|
|
65
|
+
inUse?: (homeDir: string) => Set<string>;
|
|
66
|
+
/** Test seam: which version the launcher points at. */
|
|
67
|
+
current?: (homeDir: string) => string | null;
|
|
68
|
+
/** Test seam: actually unlink. */
|
|
69
|
+
remove?: (file: string) => void;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Sweep, and say what happened.
|
|
73
|
+
*
|
|
74
|
+
* Never throws: this runs on a timer beside live sessions, and a permission
|
|
75
|
+
* error on one file must cost that file, not the daemon.
|
|
76
|
+
*/
|
|
77
|
+
export declare function pruneNativeClaudeVersions(options?: VersionSweepOptions): VersionSweepResult;
|
|
78
|
+
//# sourceMappingURL=agent-cleanup.d.ts.map
|