@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
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AccountLoginCodeResult, AccountLoginTarget, LoginStartResult } from './auth-relay.js';
|
|
2
|
+
/**
|
|
3
|
+
* Several logins of one agent, on the wire (#422 S2, plan §8).
|
|
4
|
+
*
|
|
5
|
+
* Five commands, each a thin door over what `claude-homes.ts` already does on
|
|
6
|
+
* the machine. New NAMES rather than new arguments of `login_start`: an older
|
|
7
|
+
* runner silently ignores an argument it does not know and would sign in over
|
|
8
|
+
* the machine login while the API believed a saved account was being added; an
|
|
9
|
+
* unknown name it answers «Unknown command» at once.
|
|
10
|
+
*
|
|
11
|
+
* The API refuses to send any of these to a runner whose `capabilities.commands`
|
|
12
|
+
* does not name them, and `capabilities.accounts` says for which agents they
|
|
13
|
+
* work. Both lists come from here, so the announcement and the handler cannot
|
|
14
|
+
* disagree.
|
|
15
|
+
*
|
|
16
|
+
* The shapes are a hand-written copy of `@devbridge/shared` `agent-accounts.ts`
|
|
17
|
+
* (this package cannot import it); `account-commands.test.ts` parses real
|
|
18
|
+
* answers through the shared schemas.
|
|
19
|
+
*/
|
|
20
|
+
export declare const ACCOUNT_COMMANDS: readonly ["agent_accounts", "agent_account_login_start", "agent_account_login_code", "agent_account_activate", "agent_account_forget"];
|
|
21
|
+
export type AccountCommand = (typeof ACCOUNT_COMMANDS)[number];
|
|
22
|
+
/**
|
|
23
|
+
* The agents this runner keeps several logins for (`capabilities.accounts`,
|
|
24
|
+
* R11). Codex joins in S4; until then its account commands are refused with a
|
|
25
|
+
* sentence rather than half-served.
|
|
26
|
+
*/
|
|
27
|
+
export declare const ACCOUNT_AGENTS: readonly ["claude"];
|
|
28
|
+
/** What `supervisor.ts` sends back as `command_result`, minus the envelope. */
|
|
29
|
+
export type AccountCommandReply = {
|
|
30
|
+
ok: true;
|
|
31
|
+
result: unknown;
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
error: string;
|
|
35
|
+
result?: unknown;
|
|
36
|
+
};
|
|
37
|
+
/** The two sign-in steps the commands drive – `AuthRelay`, or a test double. */
|
|
38
|
+
export interface AccountSignIn {
|
|
39
|
+
startAccountLogin(target: AccountLoginTarget): Promise<LoginStartResult>;
|
|
40
|
+
submitAccountCode(code: string): Promise<AccountLoginCodeResult>;
|
|
41
|
+
}
|
|
42
|
+
export declare function isAccountCommand(name: string): name is AccountCommand;
|
|
43
|
+
export declare function runAccountCommand(name: AccountCommand, args: Record<string, unknown>, signIn: AccountSignIn): Promise<AccountCommandReply>;
|
|
44
|
+
//# sourceMappingURL=account-commands.d.ts.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { AccountError, forgetAccount, isAccountId, listAccounts, setActiveAccount, } from './claude-homes.js';
|
|
2
|
+
import { MACHINE_ACCOUNT_ID } from './login-marks.js';
|
|
3
|
+
import { maskString } from './policy.js';
|
|
4
|
+
/**
|
|
5
|
+
* Several logins of one agent, on the wire (#422 S2, plan §8).
|
|
6
|
+
*
|
|
7
|
+
* Five commands, each a thin door over what `claude-homes.ts` already does on
|
|
8
|
+
* the machine. New NAMES rather than new arguments of `login_start`: an older
|
|
9
|
+
* runner silently ignores an argument it does not know and would sign in over
|
|
10
|
+
* the machine login while the API believed a saved account was being added; an
|
|
11
|
+
* unknown name it answers «Unknown command» at once.
|
|
12
|
+
*
|
|
13
|
+
* The API refuses to send any of these to a runner whose `capabilities.commands`
|
|
14
|
+
* does not name them, and `capabilities.accounts` says for which agents they
|
|
15
|
+
* work. Both lists come from here, so the announcement and the handler cannot
|
|
16
|
+
* disagree.
|
|
17
|
+
*
|
|
18
|
+
* The shapes are a hand-written copy of `@devbridge/shared` `agent-accounts.ts`
|
|
19
|
+
* (this package cannot import it); `account-commands.test.ts` parses real
|
|
20
|
+
* answers through the shared schemas.
|
|
21
|
+
*/
|
|
22
|
+
export const ACCOUNT_COMMANDS = [
|
|
23
|
+
'agent_accounts',
|
|
24
|
+
'agent_account_login_start',
|
|
25
|
+
'agent_account_login_code',
|
|
26
|
+
'agent_account_activate',
|
|
27
|
+
'agent_account_forget',
|
|
28
|
+
];
|
|
29
|
+
/**
|
|
30
|
+
* The agents this runner keeps several logins for (`capabilities.accounts`,
|
|
31
|
+
* R11). Codex joins in S4; until then its account commands are refused with a
|
|
32
|
+
* sentence rather than half-served.
|
|
33
|
+
*/
|
|
34
|
+
export const ACCOUNT_AGENTS = ['claude'];
|
|
35
|
+
export function isAccountCommand(name) {
|
|
36
|
+
return ACCOUNT_COMMANDS.includes(name);
|
|
37
|
+
}
|
|
38
|
+
function refusal(error) {
|
|
39
|
+
// `AccountError` is written for a person and carries no path (S1); anything
|
|
40
|
+
// else is masked like every other command error on this wire.
|
|
41
|
+
const message = error instanceof AccountError
|
|
42
|
+
? error.message
|
|
43
|
+
: maskString(String(error instanceof Error ? error.message : error));
|
|
44
|
+
return { ok: false, error: message.slice(0, 500) };
|
|
45
|
+
}
|
|
46
|
+
function accountIdArg(value) {
|
|
47
|
+
return value === MACHINE_ACCOUNT_ID || isAccountId(value) ? value : null;
|
|
48
|
+
}
|
|
49
|
+
export async function runAccountCommand(name, args, signIn) {
|
|
50
|
+
const agent = args['agent'];
|
|
51
|
+
if (agent === 'codex') {
|
|
52
|
+
return { ok: false, error: 'several Codex logins are not supported by this runner yet' };
|
|
53
|
+
}
|
|
54
|
+
if (agent !== 'claude')
|
|
55
|
+
return { ok: false, error: 'agent must be claude or codex' };
|
|
56
|
+
try {
|
|
57
|
+
switch (name) {
|
|
58
|
+
case 'agent_accounts':
|
|
59
|
+
// The one place `claude auth status` runs on request (R14 a) – home by
|
|
60
|
+
// home, only where the known identity is stale.
|
|
61
|
+
return { ok: true, result: await listAccounts({ probeIdentity: true }) };
|
|
62
|
+
case 'agent_account_login_start': {
|
|
63
|
+
const target = args['target'];
|
|
64
|
+
if (target !== 'machine' && target !== 'saved') {
|
|
65
|
+
return { ok: false, error: 'target must be machine or saved' };
|
|
66
|
+
}
|
|
67
|
+
return { ok: true, result: await signIn.startAccountLogin(target) };
|
|
68
|
+
}
|
|
69
|
+
case 'agent_account_login_code': {
|
|
70
|
+
const code = args['code'];
|
|
71
|
+
if (typeof code !== 'string' || !code.trim()) {
|
|
72
|
+
return { ok: false, error: 'code is required' };
|
|
73
|
+
}
|
|
74
|
+
const result = await signIn.submitAccountCode(code);
|
|
75
|
+
return result.ok ? { ok: true, result } : { ok: false, error: result.detail, result };
|
|
76
|
+
}
|
|
77
|
+
case 'agent_account_activate': {
|
|
78
|
+
const id = accountIdArg(args['id']);
|
|
79
|
+
if (!id)
|
|
80
|
+
return { ok: false, error: 'no such account on this server' };
|
|
81
|
+
// The usage cache is keyed by subscription, so a switch leaves it alone:
|
|
82
|
+
// the new row's figures are already its own (R16, §8).
|
|
83
|
+
return { ok: true, result: { active: setActiveAccount(id) } };
|
|
84
|
+
}
|
|
85
|
+
case 'agent_account_forget': {
|
|
86
|
+
const id = accountIdArg(args['id']);
|
|
87
|
+
if (!id)
|
|
88
|
+
return { ok: false, error: 'no such account on this server' };
|
|
89
|
+
// Drops the forgotten row's usage slot itself (`forgetAccount`).
|
|
90
|
+
const { active } = forgetAccount(id);
|
|
91
|
+
return { ok: true, result: { ok: true, active } };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
return refusal(error);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=account-commands.js.map
|
|
@@ -59,7 +59,9 @@ export declare function applyUsagePercentages(windows: AgentRateLimitWindow[], r
|
|
|
59
59
|
/**
|
|
60
60
|
* How long the probe may run before it is written off (#390).
|
|
61
61
|
*
|
|
62
|
-
* Ninety seconds, and the number is measured rather than chosen.
|
|
62
|
+
* Ninety seconds, and the number is measured rather than chosen. (The docblock
|
|
63
|
+
* said ninety from #390 on while the constant still read 20 000, and about half
|
|
64
|
+
* the probes died at the ceiling – owner's decision D5 of #422 closes that.) `/usage` was
|
|
63
65
|
* 3.7s when the old ceiling of 20s was set on 2026-08-15; it is not that
|
|
64
66
|
* command any more. Since then the CLI prints «What's contributing to your
|
|
65
67
|
* limits usage?», computed from the machine's OWN session history — 1.4 GB and
|
|
@@ -79,7 +81,7 @@ export declare function applyUsagePercentages(windows: AgentRateLimitWindow[], r
|
|
|
79
81
|
* runs in the background, and since #390 there is at most ONE of it per
|
|
80
82
|
* machine. The cost of NOT waiting was the panel losing two of its three rows.
|
|
81
83
|
*/
|
|
82
|
-
export declare const USAGE_PROBE_TIMEOUT_MS =
|
|
84
|
+
export declare const USAGE_PROBE_TIMEOUT_MS = 90000;
|
|
83
85
|
/**
|
|
84
86
|
* How long a successful reading is believed — the throttle for the whole box.
|
|
85
87
|
*
|
|
@@ -100,7 +102,7 @@ export declare const USAGE_CACHE_MS: number;
|
|
|
100
102
|
* the worst case is a single ~90s process a minute apart, not a pile.
|
|
101
103
|
*/
|
|
102
104
|
export declare const USAGE_FAILURE_COOLDOWN_MS: number;
|
|
103
|
-
export type UsageProbeRunner = (binary: string, cwd: string) => Promise<string | null>;
|
|
105
|
+
export type UsageProbeRunner = (binary: string, cwd: string, env?: NodeJS.ProcessEnv) => Promise<string | null>;
|
|
104
106
|
/**
|
|
105
107
|
* Run `/usage` in a throwaway process and return what it printed.
|
|
106
108
|
*
|
|
@@ -128,17 +130,66 @@ export type UsageProbeRunner = (binary: string, cwd: string) => Promise<string |
|
|
|
128
130
|
* usage at all, because it reads neither OAuth nor the keychain — on the
|
|
129
131
|
* subscription this whole file exists to report, it has nothing to say.
|
|
130
132
|
*/
|
|
131
|
-
export declare function probeUsageText(binary: string, cwd: string, timeoutMs?: number): Promise<string | null>;
|
|
132
|
-
/**
|
|
133
|
+
export declare function probeUsageText(binary: string, cwd: string, timeoutMs?: number, env?: NodeJS.ProcessEnv): Promise<string | null>;
|
|
134
|
+
/**
|
|
135
|
+
* Whose numbers a reading is (#422 R16).
|
|
136
|
+
*
|
|
137
|
+
* The machine used to have one account, so «the machine's reading» was enough
|
|
138
|
+
* of a name. With several Claude homes it is not: a session measures under the
|
|
139
|
+
* home it STARTED with, and the cache is kept per subscription – `orgId` when
|
|
140
|
+
* the home's identity is known, the account id until then.
|
|
141
|
+
*/
|
|
142
|
+
export interface UsageAccount {
|
|
143
|
+
id: string;
|
|
144
|
+
/** `CLAUDE_CONFIG_DIR` of a saved account; null for the machine login. */
|
|
145
|
+
home: string | null;
|
|
146
|
+
orgId?: string;
|
|
147
|
+
}
|
|
148
|
+
/** The machine login, for callers that predate #422 and name no account. */
|
|
149
|
+
export declare const MACHINE_USAGE_ACCOUNT: UsageAccount;
|
|
150
|
+
/** The cache slot a reading of this account lives in. */
|
|
151
|
+
export declare function usageCacheKey(account: {
|
|
152
|
+
id: string;
|
|
153
|
+
orgId?: string;
|
|
154
|
+
}): string;
|
|
155
|
+
/** Who a reading belongs to, read off the home right after it was measured. */
|
|
156
|
+
export interface UsageSignature {
|
|
157
|
+
accountUuid?: string;
|
|
158
|
+
orgId?: string;
|
|
159
|
+
}
|
|
160
|
+
/** A reading of one account's plan, with the moment it was taken. */
|
|
133
161
|
export interface UsageReading {
|
|
134
162
|
rows: UsageRow[];
|
|
135
163
|
measuredAtMs: number;
|
|
164
|
+
/** The account the CLI measured – from the home's own `.claude.json`. */
|
|
165
|
+
accountUuid?: string;
|
|
166
|
+
/** The subscription the numbers belong to. */
|
|
167
|
+
orgId?: string;
|
|
136
168
|
}
|
|
137
169
|
export declare function setUsageProbeRunner(run: UsageProbeRunner | null): void;
|
|
138
|
-
/** Forget everything measured. For tests, and for a re-login that changes account. */
|
|
139
|
-
export declare function invalidateUsageCache(): void;
|
|
140
170
|
/**
|
|
141
|
-
*
|
|
171
|
+
* Forget what was measured for one account (#422 R16).
|
|
172
|
+
*
|
|
173
|
+
* `forget` drops the forgotten row's slot, a replaced login drops the replaced
|
|
174
|
+
* row's, a new sign-in to the machine drops the machine's; `activate` drops
|
|
175
|
+
* nothing – the keys already keep accounts apart.
|
|
176
|
+
*
|
|
177
|
+
* Never cancels a probe that is running. Nulling «a probe is running» used to
|
|
178
|
+
* let the very next caller start a SECOND process beside the first, which then
|
|
179
|
+
* finished and wrote its figures under the account that had just been switched
|
|
180
|
+
* to. The running probe now finishes into the slot it was started for; a
|
|
181
|
+
* dropped slot is simply not where anybody reads any more, and the next probe
|
|
182
|
+
* waits in the queue behind it.
|
|
183
|
+
*
|
|
184
|
+
* Called with no account it forgets EVERYTHING, the queue included – a clean
|
|
185
|
+
* slate for a test. The product always names an account.
|
|
186
|
+
*/
|
|
187
|
+
export declare function invalidateUsageCache(account?: {
|
|
188
|
+
id: string;
|
|
189
|
+
orgId?: string;
|
|
190
|
+
}): void;
|
|
191
|
+
/**
|
|
192
|
+
* What the machine last read for this account, WITHOUT starting a measurement.
|
|
142
193
|
*
|
|
143
194
|
* The reason this exists is the defect #390 was actually about. A session's
|
|
144
195
|
* window map starts empty, and its first `rate_limit_event` arrives 12-28
|
|
@@ -152,7 +203,10 @@ export declare function invalidateUsageCache(): void;
|
|
|
152
203
|
* the machine already knows in its very first snapshot, and the probe that
|
|
153
204
|
* follows only refreshes it.
|
|
154
205
|
*/
|
|
155
|
-
export declare function lastUsageRows(
|
|
206
|
+
export declare function lastUsageRows(account?: {
|
|
207
|
+
id: string;
|
|
208
|
+
orgId?: string;
|
|
209
|
+
}): UsageReading | null;
|
|
156
210
|
export interface ReadUsageOptions {
|
|
157
211
|
binary: string;
|
|
158
212
|
/**
|
|
@@ -166,21 +220,31 @@ export interface ReadUsageOptions {
|
|
|
166
220
|
cwd?: string;
|
|
167
221
|
/** Fixed clock for tests; both the throttle and the stamp read it. */
|
|
168
222
|
now?: number;
|
|
223
|
+
/** Whose plan to measure. Absent: the machine login, as before #422. */
|
|
224
|
+
account?: UsageAccount;
|
|
225
|
+
/** The probe's environment – the account's home, without the operator token. */
|
|
226
|
+
env?: NodeJS.ProcessEnv;
|
|
227
|
+
/**
|
|
228
|
+
* Read who the home says it is, right after the probe. A reading whose
|
|
229
|
+
* subscription is not the account's own is thrown away rather than filed under
|
|
230
|
+
* the wrong name – the exact wrong statement #380 was about.
|
|
231
|
+
*/
|
|
232
|
+
sign?: () => UsageSignature;
|
|
169
233
|
}
|
|
170
234
|
/**
|
|
171
|
-
*
|
|
235
|
+
* An account's plan percentages — measured at most once per {@link USAGE_CACHE_MS}.
|
|
172
236
|
*
|
|
173
|
-
*
|
|
174
|
-
* measuring again too SOON; the
|
|
175
|
-
*
|
|
176
|
-
* sessions reaching a turn end together used to start five copies of a
|
|
237
|
+
* Three guards, and they answer different questions. The throttle stops us
|
|
238
|
+
* measuring the same account again too SOON; the per-account join stops us
|
|
239
|
+
* measuring it twice AT ONCE; the queue stops two accounts measuring at once.
|
|
240
|
+
* Five sessions reaching a turn end together used to start five copies of a
|
|
177
241
|
* 20-second CPU-bound process, each of which made the others slower — the same
|
|
178
242
|
* self-sustaining shape `gitops.ts` writes about for `git_status`.
|
|
179
243
|
*
|
|
180
244
|
* Always resolves to the best reading we have, never to «nothing» just because
|
|
181
245
|
* this attempt failed. A stale true number beats an empty panel — and the
|
|
182
246
|
* reading carries the moment it was TAKEN, so the caller can tell a fresh
|
|
183
|
-
* measurement from the one it already folded in
|
|
247
|
+
* measurement from the one it already folded in.
|
|
184
248
|
*/
|
|
185
249
|
export declare function readUsageRows(options: ReadUsageOptions): Promise<UsageReading | null>;
|
|
186
250
|
//# sourceMappingURL=claude-usage.d.ts.map
|
|
@@ -232,7 +232,9 @@ export function applyUsagePercentages(windows, rows, options = {}) {
|
|
|
232
232
|
/**
|
|
233
233
|
* How long the probe may run before it is written off (#390).
|
|
234
234
|
*
|
|
235
|
-
* Ninety seconds, and the number is measured rather than chosen.
|
|
235
|
+
* Ninety seconds, and the number is measured rather than chosen. (The docblock
|
|
236
|
+
* said ninety from #390 on while the constant still read 20 000, and about half
|
|
237
|
+
* the probes died at the ceiling – owner's decision D5 of #422 closes that.) `/usage` was
|
|
236
238
|
* 3.7s when the old ceiling of 20s was set on 2026-08-15; it is not that
|
|
237
239
|
* command any more. Since then the CLI prints «What's contributing to your
|
|
238
240
|
* limits usage?», computed from the machine's OWN session history — 1.4 GB and
|
|
@@ -252,7 +254,7 @@ export function applyUsagePercentages(windows, rows, options = {}) {
|
|
|
252
254
|
* runs in the background, and since #390 there is at most ONE of it per
|
|
253
255
|
* machine. The cost of NOT waiting was the panel losing two of its three rows.
|
|
254
256
|
*/
|
|
255
|
-
export const USAGE_PROBE_TIMEOUT_MS =
|
|
257
|
+
export const USAGE_PROBE_TIMEOUT_MS = 90_000;
|
|
256
258
|
/**
|
|
257
259
|
* How long a successful reading is believed — the throttle for the whole box.
|
|
258
260
|
*
|
|
@@ -333,7 +335,7 @@ function warnProbeFailure(error, timeoutMs, now) {
|
|
|
333
335
|
* usage at all, because it reads neither OAuth nor the keychain — on the
|
|
334
336
|
* subscription this whole file exists to report, it has nothing to say.
|
|
335
337
|
*/
|
|
336
|
-
export function probeUsageText(binary, cwd, timeoutMs = USAGE_PROBE_TIMEOUT_MS) {
|
|
338
|
+
export function probeUsageText(binary, cwd, timeoutMs = USAGE_PROBE_TIMEOUT_MS, env) {
|
|
337
339
|
return new Promise((resolve) => {
|
|
338
340
|
let settled = false;
|
|
339
341
|
const finish = (text) => {
|
|
@@ -345,6 +347,12 @@ export function probeUsageText(binary, cwd, timeoutMs = USAGE_PROBE_TIMEOUT_MS)
|
|
|
345
347
|
};
|
|
346
348
|
const child = execFile(binary, ['--strict-mcp-config', '-p', '/usage', '--output-format', 'text'], {
|
|
347
349
|
cwd,
|
|
350
|
+
// The account's home and nothing that outranks it (#422 S1 item 5): a
|
|
351
|
+
// probe run under the daemon's environment measures whichever login the
|
|
352
|
+
// DAEMON has, and with a saved account active that is somebody else's
|
|
353
|
+
// plan printed under this account's name. Absent only for a caller
|
|
354
|
+
// that measures the machine login exactly as before.
|
|
355
|
+
...(env ? { env } : {}),
|
|
348
356
|
timeout: timeoutMs,
|
|
349
357
|
maxBuffer: 1024 * 1024,
|
|
350
358
|
// SIGTERM can be trapped, SIGKILL cannot. A probe that ignored the
|
|
@@ -411,33 +419,41 @@ function probeDir() {
|
|
|
411
419
|
return null;
|
|
412
420
|
}
|
|
413
421
|
}
|
|
422
|
+
/** The machine login, for callers that predate #422 and name no account. */
|
|
423
|
+
export const MACHINE_USAGE_ACCOUNT = { id: 'machine', home: null };
|
|
424
|
+
/** The cache slot a reading of this account lives in. */
|
|
425
|
+
export function usageCacheKey(account) {
|
|
426
|
+
return account.orgId ? `org:${account.orgId}` : `account:${account.id}`;
|
|
427
|
+
}
|
|
428
|
+
const slots = new Map();
|
|
414
429
|
/**
|
|
415
|
-
* The
|
|
430
|
+
* The tail of the one-at-a-time queue: «a probe is running» is ONE per machine.
|
|
416
431
|
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* When {@link lastRows} was MEASURED — not when it was last handed out.
|
|
424
|
-
*
|
|
425
|
-
* Load-bearing rather than diagnostic (QA-390 MAJOR). A reading may be up to
|
|
426
|
-
* {@link USAGE_CACHE_MS} old, and it is handed to a session at the end of every
|
|
427
|
-
* turn. Meanwhile a `rate_limit_event` can have put a NEWER percentage into
|
|
428
|
-
* that session's window, and `applyUsagePercentages` overwrites percentages
|
|
429
|
-
* without asking how old they are — so an unqualified reading rolled 95% back
|
|
430
|
-
* to the 60% the machine measured three minutes ago, on every turn. The stamp
|
|
431
|
-
* is what lets the adapter tell «this is news» from «this is what you already
|
|
432
|
-
* folded in».
|
|
432
|
+
* Not one per account. Two live sessions on two accounts would otherwise take
|
|
433
|
+
* turns throwing away each other's reading and starting their own — the pile-up
|
|
434
|
+
* of #390, rebuilt out of accounts instead of sessions (R16). A second account
|
|
435
|
+
* waits for the first probe to finish and then measures; nobody measures twice
|
|
436
|
+
* at once.
|
|
433
437
|
*/
|
|
434
|
-
let
|
|
435
|
-
|
|
436
|
-
let
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
438
|
+
let probeQueue = Promise.resolve();
|
|
439
|
+
function slotFor(key) {
|
|
440
|
+
let slot = slots.get(key);
|
|
441
|
+
if (!slot) {
|
|
442
|
+
slot = { rows: null, measuredAtMs: 0, attemptedAtMs: 0, lastAttemptOk: false, pending: null };
|
|
443
|
+
slots.set(key, slot);
|
|
444
|
+
}
|
|
445
|
+
return slot;
|
|
446
|
+
}
|
|
447
|
+
function readingOf(slot) {
|
|
448
|
+
if (!slot?.rows)
|
|
449
|
+
return null;
|
|
450
|
+
return {
|
|
451
|
+
rows: slot.rows,
|
|
452
|
+
measuredAtMs: slot.measuredAtMs,
|
|
453
|
+
...(slot.accountUuid ? { accountUuid: slot.accountUuid } : {}),
|
|
454
|
+
...(slot.orgId ? { orgId: slot.orgId } : {}),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
441
457
|
/**
|
|
442
458
|
* The probe the cache runs. Replaced only by tests.
|
|
443
459
|
*
|
|
@@ -450,21 +466,41 @@ let inFlight = null;
|
|
|
450
466
|
* `cwd` did not exist, and this change stopped the probe using the session's
|
|
451
467
|
* `cwd` at all.
|
|
452
468
|
*/
|
|
453
|
-
|
|
469
|
+
const defaultProbeRunner = (binary, cwd, env) => probeUsageText(binary, cwd, USAGE_PROBE_TIMEOUT_MS, env);
|
|
470
|
+
let probeRunner = defaultProbeRunner;
|
|
454
471
|
export function setUsageProbeRunner(run) {
|
|
455
|
-
probeRunner = run ??
|
|
472
|
+
probeRunner = run ?? defaultProbeRunner;
|
|
456
473
|
}
|
|
457
|
-
/**
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
474
|
+
/**
|
|
475
|
+
* Forget what was measured for one account (#422 R16).
|
|
476
|
+
*
|
|
477
|
+
* `forget` drops the forgotten row's slot, a replaced login drops the replaced
|
|
478
|
+
* row's, a new sign-in to the machine drops the machine's; `activate` drops
|
|
479
|
+
* nothing – the keys already keep accounts apart.
|
|
480
|
+
*
|
|
481
|
+
* Never cancels a probe that is running. Nulling «a probe is running» used to
|
|
482
|
+
* let the very next caller start a SECOND process beside the first, which then
|
|
483
|
+
* finished and wrote its figures under the account that had just been switched
|
|
484
|
+
* to. The running probe now finishes into the slot it was started for; a
|
|
485
|
+
* dropped slot is simply not where anybody reads any more, and the next probe
|
|
486
|
+
* waits in the queue behind it.
|
|
487
|
+
*
|
|
488
|
+
* Called with no account it forgets EVERYTHING, the queue included – a clean
|
|
489
|
+
* slate for a test. The product always names an account.
|
|
490
|
+
*/
|
|
491
|
+
export function invalidateUsageCache(account) {
|
|
492
|
+
if (account === undefined) {
|
|
493
|
+
slots.clear();
|
|
494
|
+
warnedAtMs.clear();
|
|
495
|
+
// A probe a previous test left hanging must not hold every later one.
|
|
496
|
+
// Production probes settle on their own (the watchdog in `probeUsageText`).
|
|
497
|
+
probeQueue = Promise.resolve();
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
slots.delete(usageCacheKey(account));
|
|
465
501
|
}
|
|
466
502
|
/**
|
|
467
|
-
* What the machine last read, WITHOUT starting a measurement.
|
|
503
|
+
* What the machine last read for this account, WITHOUT starting a measurement.
|
|
468
504
|
*
|
|
469
505
|
* The reason this exists is the defect #390 was actually about. A session's
|
|
470
506
|
* window map starts empty, and its first `rate_limit_event` arrives 12-28
|
|
@@ -478,61 +514,101 @@ export function invalidateUsageCache() {
|
|
|
478
514
|
* the machine already knows in its very first snapshot, and the probe that
|
|
479
515
|
* follows only refreshes it.
|
|
480
516
|
*/
|
|
481
|
-
export function lastUsageRows() {
|
|
482
|
-
return
|
|
517
|
+
export function lastUsageRows(account = MACHINE_USAGE_ACCOUNT) {
|
|
518
|
+
return readingOf(slots.get(usageCacheKey(account)));
|
|
483
519
|
}
|
|
484
520
|
/**
|
|
485
|
-
*
|
|
521
|
+
* An account's plan percentages — measured at most once per {@link USAGE_CACHE_MS}.
|
|
486
522
|
*
|
|
487
|
-
*
|
|
488
|
-
* measuring again too SOON; the
|
|
489
|
-
*
|
|
490
|
-
* sessions reaching a turn end together used to start five copies of a
|
|
523
|
+
* Three guards, and they answer different questions. The throttle stops us
|
|
524
|
+
* measuring the same account again too SOON; the per-account join stops us
|
|
525
|
+
* measuring it twice AT ONCE; the queue stops two accounts measuring at once.
|
|
526
|
+
* Five sessions reaching a turn end together used to start five copies of a
|
|
491
527
|
* 20-second CPU-bound process, each of which made the others slower — the same
|
|
492
528
|
* self-sustaining shape `gitops.ts` writes about for `git_status`.
|
|
493
529
|
*
|
|
494
530
|
* Always resolves to the best reading we have, never to «nothing» just because
|
|
495
531
|
* this attempt failed. A stale true number beats an empty panel — and the
|
|
496
532
|
* reading carries the moment it was TAKEN, so the caller can tell a fresh
|
|
497
|
-
* measurement from the one it already folded in
|
|
533
|
+
* measurement from the one it already folded in.
|
|
498
534
|
*/
|
|
499
535
|
export function readUsageRows(options) {
|
|
536
|
+
const account = options.account ?? MACHINE_USAGE_ACCOUNT;
|
|
537
|
+
const key = usageCacheKey(account);
|
|
538
|
+
const slot = slotFor(key);
|
|
500
539
|
const now = options.now ?? Date.now();
|
|
501
|
-
const interval = lastAttemptOk ? USAGE_CACHE_MS : USAGE_FAILURE_COOLDOWN_MS;
|
|
502
|
-
if (attemptedAtMs !== 0 && now - attemptedAtMs < interval) {
|
|
503
|
-
return Promise.resolve(
|
|
540
|
+
const interval = slot.lastAttemptOk ? USAGE_CACHE_MS : USAGE_FAILURE_COOLDOWN_MS;
|
|
541
|
+
if (slot.attemptedAtMs !== 0 && now - slot.attemptedAtMs < interval) {
|
|
542
|
+
return Promise.resolve(readingOf(slot));
|
|
504
543
|
}
|
|
505
|
-
if (
|
|
506
|
-
return
|
|
544
|
+
if (slot.pending)
|
|
545
|
+
return slot.pending;
|
|
507
546
|
const cwd = options.cwd ?? probeDir();
|
|
508
547
|
if (cwd === null)
|
|
509
|
-
return Promise.resolve(
|
|
510
|
-
|
|
548
|
+
return Promise.resolve(readingOf(slot));
|
|
549
|
+
// After the slot was dropped (a forgotten or replaced account) the reading
|
|
550
|
+
// lands in the orphan and nobody sees it; whoever asks gets the slot that is
|
|
551
|
+
// current now.
|
|
552
|
+
const answer = () => slots.get(key) === slot ? readingOf(slot) : lastUsageRows(account);
|
|
553
|
+
const pending = probeQueue
|
|
554
|
+
.then(() =>
|
|
555
|
+
// Waited in the queue behind somebody else's probe: the account may have
|
|
556
|
+
// been forgotten or replaced meanwhile. A probe of a slot nobody reads, or
|
|
557
|
+
// under a home that is gone, would only let the CLI recreate that home.
|
|
558
|
+
slots.get(key) !== slot || (account.home !== null && !fs.existsSync(account.home))
|
|
559
|
+
? null
|
|
560
|
+
: probeRunner(options.binary, cwd, options.env))
|
|
511
561
|
.then((text) => {
|
|
512
562
|
const rows = text ? parseUsageText(text) : [];
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
//
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
563
|
+
const signature = rows.length > 0 ? signReading(options.sign) : {};
|
|
564
|
+
// The home answered for a different subscription than the account we
|
|
565
|
+
// measured for – a re-login under our feet. Not this account's numbers.
|
|
566
|
+
const foreign = account.orgId !== undefined &&
|
|
567
|
+
signature.orgId !== undefined &&
|
|
568
|
+
signature.orgId !== account.orgId;
|
|
569
|
+
slot.attemptedAtMs = options.now ?? Date.now();
|
|
570
|
+
slot.lastAttemptOk = rows.length > 0 && !foreign;
|
|
571
|
+
if (foreign) {
|
|
572
|
+
log.warn('claude: /usage measured a different subscription than the account — dropped', {
|
|
573
|
+
account: account.id,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
else if (rows.length > 0) {
|
|
577
|
+
// Zero rows is not an answer worth keeping: it means the wording moved
|
|
578
|
+
// or the CLI said nothing, and the previous true reading is still the
|
|
579
|
+
// best thing we have to show.
|
|
580
|
+
slot.rows = rows;
|
|
581
|
+
slot.measuredAtMs = slot.attemptedAtMs;
|
|
582
|
+
slot.accountUuid = signature.accountUuid;
|
|
583
|
+
slot.orgId = account.orgId ?? signature.orgId;
|
|
521
584
|
}
|
|
522
|
-
return
|
|
585
|
+
return answer();
|
|
523
586
|
})
|
|
524
587
|
.catch((error) => {
|
|
525
588
|
// `probeUsageText` resolves rather than rejects, so this is a seat that
|
|
526
589
|
// threw or a parser that did — either way the reading is unchanged.
|
|
527
590
|
log.debug('claude: /usage probe threw', { error: String(error) });
|
|
528
|
-
attemptedAtMs = options.now ?? Date.now();
|
|
529
|
-
lastAttemptOk = false;
|
|
530
|
-
return
|
|
591
|
+
slot.attemptedAtMs = options.now ?? Date.now();
|
|
592
|
+
slot.lastAttemptOk = false;
|
|
593
|
+
return answer();
|
|
531
594
|
})
|
|
532
595
|
.finally(() => {
|
|
533
|
-
|
|
596
|
+
slot.pending = null;
|
|
534
597
|
});
|
|
535
|
-
|
|
598
|
+
slot.pending = pending;
|
|
599
|
+
probeQueue = pending.catch(() => undefined);
|
|
536
600
|
return pending;
|
|
537
601
|
}
|
|
602
|
+
function signReading(sign) {
|
|
603
|
+
if (!sign)
|
|
604
|
+
return {};
|
|
605
|
+
try {
|
|
606
|
+
return sign();
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
// A home we could not read signs nothing; the reading still belongs to the
|
|
610
|
+
// account it was measured for.
|
|
611
|
+
return {};
|
|
612
|
+
}
|
|
613
|
+
}
|
|
538
614
|
//# sourceMappingURL=claude-usage.js.map
|
|
@@ -1,8 +1,37 @@
|
|
|
1
1
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
-
import { type AgentAdapter, type AgentQuestion, type AgentSession, type SessionSpec } from './types.js';
|
|
2
|
+
import { type AgentAdapter, type AgentMode, type AgentQuestion, type AgentSession, type SessionSpec } from './types.js';
|
|
3
|
+
import { type SessionAccount } from '../claude-homes.js';
|
|
3
4
|
export declare function truncate(text: string, limit?: number): string;
|
|
4
5
|
/** Exported for the one-shot commit-message run (session 14), same rules. */
|
|
5
6
|
export declare function scrubbedEnv(): Record<string, string>;
|
|
7
|
+
/**
|
|
8
|
+
* The one env var the allowlist above refuses, added back for exactly one mode
|
|
9
|
+
* (ticket #156).
|
|
10
|
+
*
|
|
11
|
+
* The CLI's own guard, read out of the binary verbatim:
|
|
12
|
+
*
|
|
13
|
+
* ```js
|
|
14
|
+
* if (t === "bypassPermissions" || r) {
|
|
15
|
+
* if (typeof process.getuid === "function" && process.getuid() === 0
|
|
16
|
+
* && process.env.IS_SANDBOX !== "1" && !Z.CLAUDE_CODE_BUBBLEWRAP)
|
|
17
|
+
* console.error("--dangerously-skip-permissions cannot be used with root/sudo privileges …"),
|
|
18
|
+
* process.exit(1)
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* The runner's default install is root (`install.sh`), so on an ordinary
|
|
23
|
+
* machine «Unrestricted» did not degrade — it killed the session at launch with
|
|
24
|
+
* exit code 1. On the machine this was found on it happened to start, because
|
|
25
|
+
* `~/.claude/settings.json` carries `env: { IS_SANDBOX: "1" }` and this adapter
|
|
26
|
+
* loads user settings; that is a hole in the scrub covering a bug, not a fix.
|
|
27
|
+
*
|
|
28
|
+
* The condition here is the CLI's own, no wider: `full` AND uid 0. It is safe
|
|
29
|
+
* precisely where it is applied — `bypassPermissions` is the mode in which the
|
|
30
|
+
* SDK never calls `canUseTool` (it says so itself), so layer 1 is already inert
|
|
31
|
+
* and there is nothing left for `IS_SANDBOX` to weaken. In every other mode the
|
|
32
|
+
* scrub stands, which is what the comment on `ENV_ALLOWLIST` has always meant.
|
|
33
|
+
*/
|
|
34
|
+
export declare function agentEnv(mode: AgentMode, sessionId: string, account?: Pick<SessionAccount, 'kind' | 'home'>): Record<string, string>;
|
|
6
35
|
/**
|
|
7
36
|
* The whole `AskUserQuestion` payload, not just its first line.
|
|
8
37
|
*
|