@bridge4dev/runner 0.46.1 → 0.48.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 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
@@ -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 { claudeCliPath } from '../agent-binary.js';
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 = claudeCliPath();
792
+ const binary = sessionClaudePath();
789
793
  if (!binary)
790
794
  return;
791
795
  this.usageProbedAt = Date.now();
@@ -1157,10 +1157,13 @@ class CodexSession {
1157
1157
  }
1158
1158
  return;
1159
1159
  }
1160
- // #279. Removed from `OPT_OUT_NOTIFICATIONS` on purpose: it is not a
1161
- // delta stream — Codex sends it when a turn changes the account's usage,
1162
- // which is orders of magnitude rarer than the text deltas that list
1163
- // defends against.
1160
+ // #279. Kept out of `OPT_OUT_NOTIFICATIONS` on purpose, but NOT because
1161
+ // it is rare: Codex sends it after every model request, as often as
1162
+ // `thread/tokenUsage/updated` (production count 56 591 against 57 008,
1163
+ // 05.09.2026 – #366). What keeps it off the wire is the supervisor's
1164
+ // level gate, which forwards a snapshot only when it changed, carries a
1165
+ // refusal (`blocked`), or is older than the resend floor. Opting the
1166
+ // notification out here instead would silence the refusal too.
1164
1167
  case 'account/rateLimits/updated': {
1165
1168
  this.onRateLimits(params);
1166
1169
  return;
@@ -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
@@ -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
@@ -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