@phnx-labs/agents-cli 1.22.104 → 1.22.106
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/CHANGELOG.md +18 -0
- package/README.md +19 -1
- package/dist/browser.js +0 -0
- package/dist/commands/exec.js +135 -233
- package/dist/commands/resume.d.ts +6 -21
- package/dist/commands/resume.js +18 -55
- package/dist/commands/run-device-picker.d.ts +58 -0
- package/dist/commands/run-device-picker.js +222 -0
- package/dist/commands/sessions-resume.d.ts +4 -0
- package/dist/commands/sessions-resume.js +132 -49
- package/dist/commands/sessions.js +32 -5
- package/dist/commands/setup-computer.js +2 -2
- package/dist/index.js +0 -0
- package/dist/lib/accounting/account-launch.d.ts +54 -0
- package/dist/lib/accounting/account-launch.js +117 -0
- package/dist/lib/accounting/account-pool-collect.js +2 -1
- package/dist/lib/accounting/account-pool.d.ts +2 -0
- package/dist/lib/accounting/account-pool.js +1 -0
- package/dist/lib/accounting/rotate.d.ts +32 -6
- package/dist/lib/accounting/rotate.js +70 -42
- package/dist/lib/accounting/usage.d.ts +71 -0
- package/dist/lib/accounting/usage.js +160 -11
- package/dist/lib/exec-account-home.d.ts +3 -1
- package/dist/lib/exec-account-home.js +2 -2
- package/dist/lib/exec.d.ts +27 -1
- package/dist/lib/exec.js +150 -27
- package/dist/lib/models.d.ts +1 -1
- package/dist/lib/models.js +4 -4
- package/dist/lib/session/actor-sidecar.d.ts +3 -11
- package/dist/lib/session/actor-sidecar.js +3 -0
- package/dist/lib/session/claude-accounts.d.ts +12 -73
- package/dist/lib/session/claude-accounts.js +32 -70
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +18 -5
- package/dist/lib/session/discover.d.ts +4 -0
- package/dist/lib/session/discover.js +116 -15
- package/dist/lib/session/recovery.d.ts +30 -34
- package/dist/lib/session/recovery.js +212 -76
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/teams/placement-probe.js +1 -1
- package/dist/session-tracker/dist/adapters/claude.d.ts +10 -0
- package/dist/session-tracker/dist/adapters/claude.js +45 -0
- package/dist/session-tracker/dist/hook.sh +191 -0
- package/dist/session-tracker/dist/index.d.ts +19 -0
- package/dist/session-tracker/dist/index.js +67 -0
- package/dist/session-tracker/dist/install-hook.d.ts +19 -0
- package/dist/session-tracker/dist/install-hook.js +245 -0
- package/dist/session-tracker/dist/prune-state.d.ts +2 -0
- package/dist/session-tracker/dist/prune-state.js +7 -0
- package/dist/session-tracker/dist/reader.d.ts +7 -0
- package/dist/session-tracker/dist/reader.js +151 -0
- package/dist/session-tracker/dist/state-file.d.ts +10 -0
- package/dist/session-tracker/dist/state-file.js +119 -0
- package/dist/session-tracker/dist/types.d.ts +32 -0
- package/dist/session-tracker/dist/types.js +1 -0
- package/dist/session-tracker/dist/writer.d.ts +12 -0
- package/dist/session-tracker/dist/writer.js +27 -0
- package/package.json +1 -1
|
@@ -68,6 +68,9 @@ export interface RotateCandidate {
|
|
|
68
68
|
* binary (managed install) and is no longer the account identity.
|
|
69
69
|
*/
|
|
70
70
|
nativeAccount?: string;
|
|
71
|
+
/** Stable registry id for {@link nativeAccount}; never inferred from version. */
|
|
72
|
+
nativeAccountId?: string;
|
|
73
|
+
providerAccountId?: string;
|
|
71
74
|
/** Slot dir when this candidate is a slot — the spawn HOME. */
|
|
72
75
|
slotDir?: string;
|
|
73
76
|
/** True when this row came from `deviceAccounts.slots`, not a version home. */
|
|
@@ -242,7 +245,7 @@ export type AccountReadiness = {
|
|
|
242
245
|
ready: true;
|
|
243
246
|
} | {
|
|
244
247
|
ready: false;
|
|
245
|
-
reason: 'rate_limited' | 'out_of_credits' | 'signed_out' | 'revoked';
|
|
248
|
+
reason: 'rate_limited' | 'out_of_credits' | 'signed_out' | 'revoked' | 'model_limited';
|
|
246
249
|
email: string | null;
|
|
247
250
|
};
|
|
248
251
|
/**
|
|
@@ -254,8 +257,19 @@ export type AccountReadiness = {
|
|
|
254
257
|
* snapshot never carries). When a live snapshot exists it wins over the cached
|
|
255
258
|
* status — matching the gate — so a stale `out_of_credits` cache is not
|
|
256
259
|
* reported while the account is actually serving requests.
|
|
257
|
-
|
|
258
|
-
|
|
260
|
+
*
|
|
261
|
+
* `model`, when supplied, additionally consults a per-(account, model)
|
|
262
|
+
* refusal Claude can surface on ONE model family ("You've reached your Fable
|
|
263
|
+
* limit…") while the account's other models and its global usage windows stay
|
|
264
|
+
* healthy — a global rate_limited/out_of_credits marker would wrongly exclude
|
|
265
|
+
* the whole account for an unrelated model. Keyed on the candidate's stable
|
|
266
|
+
* native account id (never the org-shared usageKey) via
|
|
267
|
+
* {@link candidateAccountKey}, so a model-only limit on one login can never
|
|
268
|
+
* poison a sibling account that merely shares the same org usage bucket.
|
|
269
|
+
* Omitting `model` (every existing caller) leaves generic account status
|
|
270
|
+
* completely unaffected — the model check runs only when a caller opts in.
|
|
271
|
+
*/
|
|
272
|
+
export declare function readinessFromCandidate(candidate: RotateCandidate, now?: number, model?: string): AccountReadiness;
|
|
259
273
|
/**
|
|
260
274
|
* Whether a human sitting at a terminal can clear this exclusion by launching
|
|
261
275
|
* the agent and signing in. The two unhealthy classes are opposites, and the
|
|
@@ -287,6 +301,14 @@ export declare function signInRecoverableCandidates(candidates: RotateCandidate[
|
|
|
287
301
|
* than the version home carries), so neither is checkable here.
|
|
288
302
|
*/
|
|
289
303
|
export declare function checkRunAccountReadiness(agent: AgentId, version: string): Promise<AccountReadiness>;
|
|
304
|
+
/**
|
|
305
|
+
* Identity a candidate dedups on. Quota is tracked per-org, so two versions
|
|
306
|
+
* that share an org are the same rate-limit bucket and must collapse — but two
|
|
307
|
+
* orgs under the same email (e.g. Enterprise + Personal on one Google identity)
|
|
308
|
+
* are genuinely separate buckets and must stay distinct. Prefer the org usage
|
|
309
|
+
* key; fall back to email only when no usage identity is available.
|
|
310
|
+
*/
|
|
311
|
+
export declare function candidateAccountKey(c: RotateCandidate): string;
|
|
290
312
|
/**
|
|
291
313
|
* Pick a healthy candidate using weighted random by remaining capacity.
|
|
292
314
|
*
|
|
@@ -311,15 +333,19 @@ export declare function checkRunAccountReadiness(agent: AgentId, version: string
|
|
|
311
333
|
*
|
|
312
334
|
* Returns null if no candidate is eligible — callers fall back to the pinned
|
|
313
335
|
* version so behavior stays predictable.
|
|
336
|
+
*
|
|
337
|
+
* `model`, when supplied, additionally excludes an account carrying a live
|
|
338
|
+
* per-model refusal for that exact model (see {@link readinessFromCandidate}).
|
|
339
|
+
* Omitted (every pre-existing caller), eligibility is unchanged.
|
|
314
340
|
*/
|
|
315
|
-
export declare function pickBalancedCandidate(candidates: RotateCandidate[], nowMs?: number): RotateResult | null;
|
|
341
|
+
export declare function pickBalancedCandidate(candidates: RotateCandidate[], nowMs?: number, model?: string): RotateResult | null;
|
|
316
342
|
export { PROJECTION_HORIZON_MIN, capacityWeight };
|
|
317
343
|
/**
|
|
318
344
|
* Pick an available candidate. Prefers the configured pinned version when that
|
|
319
345
|
* version has usage available; otherwise routes to the candidate with the most
|
|
320
346
|
* usage headroom.
|
|
321
347
|
*/
|
|
322
|
-
export declare function pickAvailableCandidate(candidates: RotateCandidate[], preferredVersion?: string | null, nowMs?: number): RotateResult | null;
|
|
348
|
+
export declare function pickAvailableCandidate(candidates: RotateCandidate[], preferredVersion?: string | null, nowMs?: number, model?: string): RotateResult | null;
|
|
323
349
|
/**
|
|
324
350
|
* Per-harness routing summary for `agents run auto` — the cross-harness layer
|
|
325
351
|
* that sits above `pickBalancedCandidate` (which is strictly per-harness).
|
|
@@ -463,7 +489,7 @@ export declare function buildRotationDecisionEvent(rotation: RotateResult, agent
|
|
|
463
489
|
* else is healthy, `exhausted` is set so the caller fails loud instead of
|
|
464
490
|
* spawning into a credential-less home.
|
|
465
491
|
*/
|
|
466
|
-
export declare function resolveRunVersion(agent: AgentId, strategy: RunStrategy, cwd?: string, collect?: (agent: AgentId) => Promise<RotateCandidate[]
|
|
492
|
+
export declare function resolveRunVersion(agent: AgentId, strategy: RunStrategy, cwd?: string, collect?: (agent: AgentId) => Promise<RotateCandidate[]>, model?: string): Promise<{
|
|
467
493
|
version: string | null;
|
|
468
494
|
rotation: RotateResult | null;
|
|
469
495
|
/**
|
|
@@ -9,16 +9,18 @@ import * as path from 'path';
|
|
|
9
9
|
import { PROJECTION_HORIZON_MIN, capacityWeight } from './capacity.js';
|
|
10
10
|
import { accountDisplayLabel, getAccountInfo, credentialPresence, ALL_AGENT_IDS, } from '../agents.js';
|
|
11
11
|
import { readMeta, writeMeta, getHelpersDir } from '../state.js';
|
|
12
|
+
import { resolveConfiguredModel } from '../models.js';
|
|
13
|
+
import { isTierToken, resolveTier } from '../model-tiers.js';
|
|
12
14
|
import { listInstalledVersions, getVersionHomePath, resolveVersion } from '../installations/versions.js';
|
|
13
15
|
import { resolveManagedInstallation } from '../installations/store.js';
|
|
14
16
|
import { listNativeAccounts } from '../account-registry.js';
|
|
15
|
-
import {
|
|
17
|
+
import { resolveNativeSpawnHome } from '../exec-account-home.js';
|
|
16
18
|
import { getProjectRunConfigs } from '../run-config.js';
|
|
17
19
|
import { emit } from '../feed/events.js';
|
|
18
|
-
import { getUsageInfoByIdentity, getUsageLookupKey, deriveUsageStatusFromSnapshot, } from './usage.js';
|
|
20
|
+
import { getUsageInfoByIdentity, getUsageLookupKey, deriveUsageStatusFromSnapshot, getClaudeModelRefusal, claudeModelRefusalKey, } from './usage.js';
|
|
19
21
|
import { readAccountHeadroom } from '../fleet-cache.js';
|
|
20
22
|
import { machineId } from '../machine-id.js';
|
|
21
|
-
import { AUTH_PROBE_MAX_AGE_MS, readAuthHealthCache, authCacheKey, isDeadVerdict } from '../auth-health.js';
|
|
23
|
+
import { AUTH_PROBE_MAX_AGE_MS, readAuthHealthCache, authCacheKey, slotAuthVersionKey, isDeadVerdict } from '../auth-health.js';
|
|
22
24
|
function getRotateDir() {
|
|
23
25
|
const dir = path.join(getHelpersDir(), 'rotate');
|
|
24
26
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -82,10 +84,10 @@ const LAUNCHABLE_SLOT_VERDICTS = new Set(['live', 'unverified']);
|
|
|
82
84
|
function isLaunchableSlotVerdict(verdict) {
|
|
83
85
|
return verdict !== null && LAUNCHABLE_SLOT_VERDICTS.has(verdict);
|
|
84
86
|
}
|
|
85
|
-
function isRotationEligible(candidate) {
|
|
87
|
+
function isRotationEligible(candidate, nowMs = Date.now(), model) {
|
|
86
88
|
if (candidate.fromSlot && !isLaunchableSlotVerdict(candidate.authVerdict))
|
|
87
89
|
return false;
|
|
88
|
-
return readinessFromCandidate(candidate).ready;
|
|
90
|
+
return readinessFromCandidate(candidate, nowMs, model).ready;
|
|
89
91
|
}
|
|
90
92
|
/**
|
|
91
93
|
* Whether a version home can actually authenticate a launch.
|
|
@@ -128,9 +130,6 @@ export async function isVersionLaunchableHere(agent, version) {
|
|
|
128
130
|
const launchable = isLaunchableSignedIn(info.signedIn, credentialPresence(agent, home));
|
|
129
131
|
return { launchable, email: launchable ? info.email : null };
|
|
130
132
|
}
|
|
131
|
-
function isAvailableEligible(candidate) {
|
|
132
|
-
return isRotationEligible(candidate);
|
|
133
|
-
}
|
|
134
133
|
/**
|
|
135
134
|
* How old a usage snapshot may be and still settle a routing DECISION.
|
|
136
135
|
*
|
|
@@ -238,8 +237,19 @@ function hasUsageAvailable(candidate) {
|
|
|
238
237
|
* snapshot never carries). When a live snapshot exists it wins over the cached
|
|
239
238
|
* status — matching the gate — so a stale `out_of_credits` cache is not
|
|
240
239
|
* reported while the account is actually serving requests.
|
|
240
|
+
*
|
|
241
|
+
* `model`, when supplied, additionally consults a per-(account, model)
|
|
242
|
+
* refusal Claude can surface on ONE model family ("You've reached your Fable
|
|
243
|
+
* limit…") while the account's other models and its global usage windows stay
|
|
244
|
+
* healthy — a global rate_limited/out_of_credits marker would wrongly exclude
|
|
245
|
+
* the whole account for an unrelated model. Keyed on the candidate's stable
|
|
246
|
+
* native account id (never the org-shared usageKey) via
|
|
247
|
+
* {@link candidateAccountKey}, so a model-only limit on one login can never
|
|
248
|
+
* poison a sibling account that merely shares the same org usage bucket.
|
|
249
|
+
* Omitting `model` (every existing caller) leaves generic account status
|
|
250
|
+
* completely unaffected — the model check runs only when a caller opts in.
|
|
241
251
|
*/
|
|
242
|
-
export function readinessFromCandidate(candidate, now = Date.now()) {
|
|
252
|
+
export function readinessFromCandidate(candidate, now = Date.now(), model) {
|
|
243
253
|
if (!candidate.signedIn) {
|
|
244
254
|
return { ready: false, reason: 'signed_out', email: candidate.email };
|
|
245
255
|
}
|
|
@@ -252,6 +262,16 @@ export function readinessFromCandidate(candidate, now = Date.now()) {
|
|
|
252
262
|
if (authFresh && candidate.authVerdict !== null && isDeadVerdict(candidate.authVerdict)) {
|
|
253
263
|
return { ready: false, reason: 'revoked', email: candidate.email };
|
|
254
264
|
}
|
|
265
|
+
const modelKey = claudeModelRefusalKey(candidate.nativeAccountId ?? candidate.providerAccountId, candidate.providerAccount ? undefined : candidate.slotDir ?? getVersionHomePath(candidate.agent, candidate.version));
|
|
266
|
+
if (candidate.agent === 'claude' && modelKey) {
|
|
267
|
+
const requested = model ?? resolveConfiguredModel(candidate.agent, candidate.version, candidate.slotDir)?.model;
|
|
268
|
+
const concrete = requested && isTierToken(requested)
|
|
269
|
+
? resolveTier(candidate.agent, candidate.version, requested).model
|
|
270
|
+
: requested;
|
|
271
|
+
if (concrete && getClaudeModelRefusal(modelKey, concrete, now)) {
|
|
272
|
+
return { ready: false, reason: 'model_limited', email: candidate.email };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
255
275
|
if (hasUsageAvailable(candidate)) {
|
|
256
276
|
return { ready: true };
|
|
257
277
|
}
|
|
@@ -332,13 +352,17 @@ function compareCandidates(a, b) {
|
|
|
332
352
|
* are genuinely separate buckets and must stay distinct. Prefer the org usage
|
|
333
353
|
* key; fall back to email only when no usage identity is available.
|
|
334
354
|
*/
|
|
335
|
-
function
|
|
336
|
-
|
|
355
|
+
export function candidateAccountKey(c) {
|
|
356
|
+
if (c.nativeAccountId)
|
|
357
|
+
return `native:${c.nativeAccountId}`;
|
|
358
|
+
if (c.providerAccount)
|
|
359
|
+
return `provider:${c.providerAccount}`;
|
|
360
|
+
return c.usageKey ?? c.accountKey ?? c.email ?? `${c.agent}:unregistered:${c.accountLabel || c.version}`;
|
|
337
361
|
}
|
|
338
362
|
function dedupeAndSortCandidates(candidates) {
|
|
339
363
|
const byIdentity = new Map();
|
|
340
364
|
for (const c of candidates) {
|
|
341
|
-
const id =
|
|
365
|
+
const id = candidateAccountKey(c);
|
|
342
366
|
const existing = byIdentity.get(id);
|
|
343
367
|
if (!existing) {
|
|
344
368
|
byIdentity.set(id, c);
|
|
@@ -373,12 +397,16 @@ function dedupeAndSortCandidates(candidates) {
|
|
|
373
397
|
*
|
|
374
398
|
* Returns null if no candidate is eligible — callers fall back to the pinned
|
|
375
399
|
* version so behavior stays predictable.
|
|
400
|
+
*
|
|
401
|
+
* `model`, when supplied, additionally excludes an account carrying a live
|
|
402
|
+
* per-model refusal for that exact model (see {@link readinessFromCandidate}).
|
|
403
|
+
* Omitted (every pre-existing caller), eligibility is unchanged.
|
|
376
404
|
*/
|
|
377
|
-
export function pickBalancedCandidate(candidates, nowMs = Date.now()) {
|
|
405
|
+
export function pickBalancedCandidate(candidates, nowMs = Date.now(), model) {
|
|
378
406
|
const healthy = [];
|
|
379
407
|
const excluded = [];
|
|
380
408
|
for (const c of candidates) {
|
|
381
|
-
if (!isRotationEligible(c)) {
|
|
409
|
+
if (!isRotationEligible(c, nowMs, model)) {
|
|
382
410
|
excluded.push(c);
|
|
383
411
|
continue;
|
|
384
412
|
}
|
|
@@ -489,11 +517,11 @@ function weightedRandomByCapacity(sorted, nowMs = Date.now()) {
|
|
|
489
517
|
* version has usage available; otherwise routes to the candidate with the most
|
|
490
518
|
* usage headroom.
|
|
491
519
|
*/
|
|
492
|
-
export function pickAvailableCandidate(candidates, preferredVersion, nowMs = Date.now()) {
|
|
520
|
+
export function pickAvailableCandidate(candidates, preferredVersion, nowMs = Date.now(), model) {
|
|
493
521
|
const healthy = [];
|
|
494
522
|
const excluded = [];
|
|
495
523
|
for (const c of candidates) {
|
|
496
|
-
if (!
|
|
524
|
+
if (!isRotationEligible(c, nowMs, model)) {
|
|
497
525
|
excluded.push(c);
|
|
498
526
|
continue;
|
|
499
527
|
}
|
|
@@ -538,7 +566,7 @@ export function pickAvailableCandidate(candidates, preferredVersion, nowMs = Dat
|
|
|
538
566
|
export function classifyHarnessCandidates(byHarness, nowMs = Date.now()) {
|
|
539
567
|
const summaries = [];
|
|
540
568
|
for (const [agent, candidates] of byHarness) {
|
|
541
|
-
const eligible = candidates.filter(isRotationEligible);
|
|
569
|
+
const eligible = candidates.filter((c) => isRotationEligible(c));
|
|
542
570
|
if (eligible.length === 0) {
|
|
543
571
|
const counts = new Map();
|
|
544
572
|
for (const c of candidates) {
|
|
@@ -697,22 +725,25 @@ export async function collectRunCandidates(agent) {
|
|
|
697
725
|
const meta = readMeta();
|
|
698
726
|
const binaryLabel = resolveManagedInstallation(agent)?.label ?? versions[0];
|
|
699
727
|
const slotRows = [];
|
|
700
|
-
const slotIdentities = new Set();
|
|
701
728
|
const slotDirs = new Set();
|
|
702
729
|
if (binaryLabel) {
|
|
703
|
-
const slots = readSlots(meta);
|
|
704
730
|
const natives = listNativeAccounts(meta).filter((row) => row.agent === agent);
|
|
705
731
|
const probed = await Promise.all(natives.map(async (account) => {
|
|
706
|
-
const
|
|
707
|
-
if (!
|
|
732
|
+
const resolved = await resolveNativeSpawnHome(agent, account, meta, { readOnly: true }).catch(() => null);
|
|
733
|
+
if (!resolved)
|
|
708
734
|
return null;
|
|
709
|
-
const
|
|
735
|
+
const slot = resolved.slot;
|
|
736
|
+
const home = resolved.execHome;
|
|
737
|
+
const version = resolved.label ?? binaryLabel;
|
|
738
|
+
const cachedHealth = authCache[authCacheKey(localHost, agent, slot ? slotAuthVersionKey(account.id) : version)];
|
|
739
|
+
const authHealth = cachedHealth && Date.now() - cachedHealth.checkedAt <= AUTH_PROBE_MAX_AGE_MS ? cachedHealth : undefined;
|
|
740
|
+
const effectiveVerdict = authHealth?.verdict ?? slot?.verdict ?? null;
|
|
710
741
|
const info = await getAccountInfo(agent, home);
|
|
711
742
|
const launchable = isLaunchableSignedIn(info.signedIn, credentialPresence(agent, home));
|
|
712
|
-
const slotOk = isLaunchableSlotVerdict(
|
|
743
|
+
const slotOk = !slot || isLaunchableSlotVerdict(effectiveVerdict);
|
|
713
744
|
return {
|
|
714
745
|
agent,
|
|
715
|
-
version
|
|
746
|
+
version,
|
|
716
747
|
home,
|
|
717
748
|
info,
|
|
718
749
|
accountKey: launchable ? info.accountKey : null,
|
|
@@ -721,38 +752,33 @@ export async function collectRunCandidates(agent) {
|
|
|
721
752
|
usageStatus: launchable ? info.usageStatus : null,
|
|
722
753
|
plan: launchable ? info.plan : null,
|
|
723
754
|
signedIn: launchable && slotOk,
|
|
724
|
-
authVerdict:
|
|
755
|
+
authVerdict: effectiveVerdict,
|
|
725
756
|
authCheckedAt: (() => {
|
|
726
|
-
|
|
757
|
+
if (authHealth)
|
|
758
|
+
return authHealth.checkedAt;
|
|
759
|
+
const ts = slot?.checkedAt ? Date.parse(slot.checkedAt) : NaN;
|
|
727
760
|
return Number.isFinite(ts) ? ts : null;
|
|
728
761
|
})(),
|
|
729
762
|
lastActive: info.lastActive,
|
|
730
763
|
nativeAccount: account.name,
|
|
764
|
+
nativeAccountId: account.id,
|
|
731
765
|
slotDir: home,
|
|
732
|
-
fromSlot:
|
|
766
|
+
fromSlot: !!slot,
|
|
733
767
|
};
|
|
734
768
|
}));
|
|
735
769
|
for (const row of probed) {
|
|
736
770
|
if (!row)
|
|
737
771
|
continue;
|
|
738
772
|
slotRows.push(row);
|
|
739
|
-
slotDirs.add(
|
|
740
|
-
if (row.info.accountKey)
|
|
741
|
-
slotIdentities.add(row.info.accountKey);
|
|
742
|
-
if (row.email)
|
|
743
|
-
slotIdentities.add(row.email.toLowerCase());
|
|
773
|
+
slotDirs.add(fs.realpathSync(row.home));
|
|
744
774
|
}
|
|
745
775
|
}
|
|
746
776
|
// Legacy per-account installations (`acct-*` homes) until T7 migrates them.
|
|
747
777
|
const versionRows = await Promise.all(versions.map(async (version) => {
|
|
748
778
|
const home = getVersionHomePath(agent, version);
|
|
749
|
-
if (slotDirs.has(
|
|
779
|
+
if (fs.existsSync(home) && slotDirs.has(fs.realpathSync(home)))
|
|
750
780
|
return null;
|
|
751
781
|
const info = await getAccountInfo(agent, home);
|
|
752
|
-
if (info.accountKey && slotIdentities.has(info.accountKey))
|
|
753
|
-
return null;
|
|
754
|
-
if (info.email && slotIdentities.has(info.email.toLowerCase()))
|
|
755
|
-
return null;
|
|
756
782
|
// We used to additionally call isClaudeAuthValid(home), which reads
|
|
757
783
|
// "Claude Code-credentials-<hash>" from the system keychain. That item is
|
|
758
784
|
// written by Claude Code itself with its own process in the ACL, so our
|
|
@@ -849,7 +875,9 @@ export function matchAccountCandidate(candidates, account, preferredLabel) {
|
|
|
849
875
|
const matching = candidates.filter((c) => c.signedIn &&
|
|
850
876
|
(c.email?.toLowerCase() === needle
|
|
851
877
|
|| c.accountKey?.toLowerCase() === needle
|
|
852
|
-
|| c.nativeAccount?.toLowerCase() === needle
|
|
878
|
+
|| c.nativeAccount?.toLowerCase() === needle
|
|
879
|
+
|| c.nativeAccountId?.toLowerCase() === needle
|
|
880
|
+
|| c.providerAccount?.toLowerCase() === needle));
|
|
853
881
|
return matching.find(candidate => candidate.version === preferredLabel) ?? matching[0] ?? null;
|
|
854
882
|
}
|
|
855
883
|
export function matchAccountVersion(candidates, account, preferredLabel) {
|
|
@@ -1064,7 +1092,7 @@ function emitRotationDecision(event, rotation, agent, strategy, extra = {}) {
|
|
|
1064
1092
|
* else is healthy, `exhausted` is set so the caller fails loud instead of
|
|
1065
1093
|
* spawning into a credential-less home.
|
|
1066
1094
|
*/
|
|
1067
|
-
export async function resolveRunVersion(agent, strategy, cwd = process.cwd(), collect = collectRunCandidates) {
|
|
1095
|
+
export async function resolveRunVersion(agent, strategy, cwd = process.cwd(), collect = collectRunCandidates, model) {
|
|
1068
1096
|
const fallback = resolveVersion(agent, cwd);
|
|
1069
1097
|
const candidates = await collect(agent);
|
|
1070
1098
|
// Entirely stale usage (PHNX-2526): every eligible account carries a
|
|
@@ -1089,7 +1117,7 @@ export async function resolveRunVersion(agent, strategy, cwd = process.cwd(), co
|
|
|
1089
1117
|
// Auth-blocked pin: the home cannot authenticate, so launching it is a
|
|
1090
1118
|
// guaranteed miss. Prefer a signed-in sibling on this device.
|
|
1091
1119
|
if (pinnedCandidate && isSignInRecoverable(readinessFromCandidate(pinnedCandidate))) {
|
|
1092
|
-
const rotation = pickAvailableCandidate(candidates, fallback);
|
|
1120
|
+
const rotation = pickAvailableCandidate(candidates, fallback, undefined, model);
|
|
1093
1121
|
// The auth-blocked pin rotates to a sibling — an initial selection, so it
|
|
1094
1122
|
// gets the same verified-only gate as balanced/available. Without this, a
|
|
1095
1123
|
// revoked pin with only stale siblings launched one blind (the yosemite-s1
|
|
@@ -1109,8 +1137,8 @@ export async function resolveRunVersion(agent, strategy, cwd = process.cwd(), co
|
|
|
1109
1137
|
return { version: fallback, rotation: null };
|
|
1110
1138
|
}
|
|
1111
1139
|
const rotation = strategy === 'available'
|
|
1112
|
-
? pickAvailableCandidate(candidates, fallback)
|
|
1113
|
-
: pickBalancedCandidate(candidates);
|
|
1140
|
+
? pickAvailableCandidate(candidates, fallback, undefined, model)
|
|
1141
|
+
: pickBalancedCandidate(candidates, undefined, model);
|
|
1114
1142
|
if (rotation && rotation.noVerifiedUsage)
|
|
1115
1143
|
return refuseStaleUsage(rotation);
|
|
1116
1144
|
if (rotation) {
|
|
@@ -284,6 +284,20 @@ interface CachedUsageWindow {
|
|
|
284
284
|
resetsAt: string | null;
|
|
285
285
|
windowMinutes: number | null;
|
|
286
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* A model-specific refusal ("You've reached your Fable limit…") observed from
|
|
289
|
+
* a real run. Independent of {@link CachedUsageSnapshot.unavailable}: Claude
|
|
290
|
+
* can block ONE model family while the account's other models and its global
|
|
291
|
+
* usage windows stay healthy, so this must never fold into the account-wide
|
|
292
|
+
* marker (that would wrongly exclude every model on the account for a limit
|
|
293
|
+
* that only ever named one). No invented reset — `resetsAt` is present only
|
|
294
|
+
* when the refusal text itself carried a clock; absent means the marker is
|
|
295
|
+
* sticky until a later successful run on this exact (account, model) clears it.
|
|
296
|
+
*/
|
|
297
|
+
interface CachedModelRefusal {
|
|
298
|
+
family?: string;
|
|
299
|
+
resetsAt?: string;
|
|
300
|
+
}
|
|
287
301
|
/** Serialized usage snapshot for the on-disk cache. */
|
|
288
302
|
export interface CachedUsageSnapshot {
|
|
289
303
|
capturedAt: string | null;
|
|
@@ -294,6 +308,15 @@ export interface CachedUsageSnapshot {
|
|
|
294
308
|
reason: 'session_limit' | 'out_of_credits';
|
|
295
309
|
resetsAt?: string;
|
|
296
310
|
};
|
|
311
|
+
/**
|
|
312
|
+
* Per-model refusal markers, keyed by the exact model name a refusal was
|
|
313
|
+
* observed against. Keyed on the account row (itself keyed by a stable
|
|
314
|
+
* accountId-preferring key — see {@link noteClaudeModelRefusal}), not on
|
|
315
|
+
* the org-shared usage key alone, so a per-model limit on one login cannot
|
|
316
|
+
* be misread as blocking a sibling account that merely shares the same org
|
|
317
|
+
* usage bucket.
|
|
318
|
+
*/
|
|
319
|
+
modelRefusals?: Record<string, CachedModelRefusal>;
|
|
297
320
|
}
|
|
298
321
|
/** The single registry of agent usage sources and their transport. */
|
|
299
322
|
declare const USAGE_SOURCES: {
|
|
@@ -762,6 +785,54 @@ export declare function clearClaudeAccountRefusal(usageKey: string, cachePath?:
|
|
|
762
785
|
* This quota is not part of Anthropic's five-hour/weekly usage response.
|
|
763
786
|
*/
|
|
764
787
|
export declare function noteClaudeSessionLimit(usageKey: string, resetsAt: Date, cachePath?: string): void;
|
|
788
|
+
/**
|
|
789
|
+
* Persist a Claude MODEL-specific refusal — "You've reached your Fable limit.
|
|
790
|
+
* Run /usage-credits to continue or switch models with /model." — a distinct
|
|
791
|
+
* class from {@link noteClaudeOutOfCredits} / {@link noteClaudeSessionLimit}:
|
|
792
|
+
* those exclude the whole account, this excludes only ONE model on it (an
|
|
793
|
+
* organization quota group can meter models separately). `accountKey` MUST be
|
|
794
|
+
* the candidate's stable native-account key (see `candidateAccountKey` in
|
|
795
|
+
* rotate.ts), never the org-shared `usageKey` — using the org key here would
|
|
796
|
+
* poison every sibling account under that org for a limit that named one
|
|
797
|
+
* model on one login. No invented reset: `resetsAt` is written only when the
|
|
798
|
+
* refusal text carried one; otherwise the marker is sticky until a later
|
|
799
|
+
* successful run on this exact (account, model) clears it via
|
|
800
|
+
* {@link clearClaudeModelRefusal}.
|
|
801
|
+
*/
|
|
802
|
+
export declare function claudeModelRefusalKey(accountId?: string | null, home?: string | null): string | undefined;
|
|
803
|
+
export declare function noteClaudeModelRefusal(accountKey: string, model: string, refusal: {
|
|
804
|
+
family?: string;
|
|
805
|
+
resetsAt?: Date;
|
|
806
|
+
}, cachePath?: string): void;
|
|
807
|
+
/**
|
|
808
|
+
* Clear a persisted model-refusal marker for exactly ONE (account, model)
|
|
809
|
+
* pair after a run SUCCEEDS on that same account+model. Never clears a
|
|
810
|
+
* sibling model on the same account, and never fires for an interactive
|
|
811
|
+
* detach or an unknown outcome — the caller must have demonstrated an actual
|
|
812
|
+
* completed success on this exact model before calling this.
|
|
813
|
+
*/
|
|
814
|
+
export declare function clearClaudeModelRefusal(accountKey: string, model: string, cachePath?: string): void;
|
|
815
|
+
/**
|
|
816
|
+
* Read a live (non-expired) model-refusal marker for (accountKey, model), or
|
|
817
|
+
* null when none is recorded or the recorded one has passed its clock. A
|
|
818
|
+
* marker with no `resetsAt` never expires here — it is sticky until
|
|
819
|
+
* {@link clearClaudeModelRefusal} observes a real success.
|
|
820
|
+
*/
|
|
821
|
+
export declare function getClaudeModelRefusal(accountKey: string, model: string, nowMs?: number, cachePath?: string): {
|
|
822
|
+
family?: string;
|
|
823
|
+
resetsAt: Date | null;
|
|
824
|
+
} | null;
|
|
825
|
+
/**
|
|
826
|
+
* Parse Claude's model-specific refusal — the CLI's own phrasing when ONE
|
|
827
|
+
* model's quota is exhausted while the account otherwise keeps serving:
|
|
828
|
+
* "You've reached your Fable limit. Run /usage-credits to continue or switch
|
|
829
|
+
* models with /model." Deliberately narrow (unlike the broad RATE_LIMIT_PATTERNS
|
|
830
|
+
* scan) so a session that merely discusses `/usage-credits` cannot false-positive.
|
|
831
|
+
* Tolerates both a straight and curly apostrophe.
|
|
832
|
+
*/
|
|
833
|
+
export declare function parseClaudeModelRefusal(text: string): {
|
|
834
|
+
family: string;
|
|
835
|
+
} | null;
|
|
765
836
|
/** Parse Claude's `hit your session limit · resets …` refusal. */
|
|
766
837
|
export declare function parseClaudeSessionLimitReset(text: string, nowMs?: number): Date | null;
|
|
767
838
|
/**
|
|
@@ -1790,10 +1790,17 @@ export function writeClaudeUsageCache(usageKey, snapshot, cachePath = getClaudeU
|
|
|
1790
1790
|
// refresh cannot drop another account's row (lost update).
|
|
1791
1791
|
const cache = readClaudeUsageCacheFile(cachePath);
|
|
1792
1792
|
const prior = cache[usageKey];
|
|
1793
|
-
cache[usageKey] =
|
|
1794
|
-
...
|
|
1795
|
-
|
|
1796
|
-
|
|
1793
|
+
cache[usageKey] = {
|
|
1794
|
+
...serializeClaudeUsageSnapshot({
|
|
1795
|
+
...snapshot,
|
|
1796
|
+
unavailable: carryForwardUnavailable(prior?.unavailable, snapshot.unavailable),
|
|
1797
|
+
}),
|
|
1798
|
+
// A per-model refusal has its own independent lifecycle (see
|
|
1799
|
+
// noteClaudeModelRefusal / clearClaudeModelRefusal) — a global usage
|
|
1800
|
+
// write must never drop it, the same reason `unavailable` is carried
|
|
1801
|
+
// forward above rather than overwritten.
|
|
1802
|
+
modelRefusals: prior?.modelRefusals,
|
|
1803
|
+
};
|
|
1797
1804
|
atomicWriteFileSync(cachePath, JSON.stringify(cache, null, 2), 'utf-8');
|
|
1798
1805
|
});
|
|
1799
1806
|
}
|
|
@@ -1814,12 +1821,16 @@ export function mergeClaudeUsageCacheWindows(usageKey, snapshot, cachePath = get
|
|
|
1814
1821
|
const windows = new Map(priorSnapshot?.windows.map((window) => [window.key, window]) ?? []);
|
|
1815
1822
|
for (const window of snapshot.windows)
|
|
1816
1823
|
windows.set(window.key, window);
|
|
1817
|
-
cache[usageKey] =
|
|
1818
|
-
...
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1824
|
+
cache[usageKey] = {
|
|
1825
|
+
...serializeClaudeUsageSnapshot({
|
|
1826
|
+
...snapshot,
|
|
1827
|
+
windows: [...windows.values()],
|
|
1828
|
+
plan: snapshot.plan ?? priorSnapshot?.plan ?? null,
|
|
1829
|
+
unavailable: carryForwardUnavailable(prior?.unavailable, snapshot.unavailable),
|
|
1830
|
+
}),
|
|
1831
|
+
// See writeClaudeUsageCache: a per-model refusal survives a windows-only merge too.
|
|
1832
|
+
modelRefusals: prior?.modelRefusals,
|
|
1833
|
+
};
|
|
1823
1834
|
atomicWriteFileSync(cachePath, JSON.stringify(cache, null, 2), 'utf-8');
|
|
1824
1835
|
});
|
|
1825
1836
|
}
|
|
@@ -2004,7 +2015,8 @@ function deserializeClaudeUsageSnapshot(snapshot, now) {
|
|
|
2004
2015
|
staleWindows.length === 0 &&
|
|
2005
2016
|
!unavailable &&
|
|
2006
2017
|
!snapshot.plan &&
|
|
2007
|
-
!snapshot.refreshHint
|
|
2018
|
+
!snapshot.refreshHint &&
|
|
2019
|
+
!hasLiveModelRefusal(snapshot.modelRefusals, now)) {
|
|
2008
2020
|
return null;
|
|
2009
2021
|
}
|
|
2010
2022
|
return {
|
|
@@ -2051,6 +2063,17 @@ function deserializeUnavailable(cached, now) {
|
|
|
2051
2063
|
? { reason: 'session_limit', resetsAt: reset }
|
|
2052
2064
|
: undefined;
|
|
2053
2065
|
}
|
|
2066
|
+
/** Whether any per-model refusal in the row is still live (not clock-expired). */
|
|
2067
|
+
function hasLiveModelRefusal(modelRefusals, now) {
|
|
2068
|
+
if (!modelRefusals)
|
|
2069
|
+
return false;
|
|
2070
|
+
return Object.values(modelRefusals).some((entry) => {
|
|
2071
|
+
if (!entry.resetsAt)
|
|
2072
|
+
return true; // no clock given — sticky until cleared
|
|
2073
|
+
const reset = parseDateValue(entry.resetsAt);
|
|
2074
|
+
return !reset || reset.getTime() > now.getTime();
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2054
2077
|
/**
|
|
2055
2078
|
* Persist a Claude tokens/credits exhaustion (`out of usage credits` / `monthly
|
|
2056
2079
|
* spend limit`) from a real run. Unlike a rate/session limit this does NOT reset
|
|
@@ -2115,6 +2138,132 @@ export function noteClaudeSessionLimit(usageKey, resetsAt, cachePath = getClaude
|
|
|
2115
2138
|
/* best-effort cache write — lock busy or disk full */
|
|
2116
2139
|
}
|
|
2117
2140
|
}
|
|
2141
|
+
/**
|
|
2142
|
+
* Persist a Claude MODEL-specific refusal — "You've reached your Fable limit.
|
|
2143
|
+
* Run /usage-credits to continue or switch models with /model." — a distinct
|
|
2144
|
+
* class from {@link noteClaudeOutOfCredits} / {@link noteClaudeSessionLimit}:
|
|
2145
|
+
* those exclude the whole account, this excludes only ONE model on it (an
|
|
2146
|
+
* organization quota group can meter models separately). `accountKey` MUST be
|
|
2147
|
+
* the candidate's stable native-account key (see `candidateAccountKey` in
|
|
2148
|
+
* rotate.ts), never the org-shared `usageKey` — using the org key here would
|
|
2149
|
+
* poison every sibling account under that org for a limit that named one
|
|
2150
|
+
* model on one login. No invented reset: `resetsAt` is written only when the
|
|
2151
|
+
* refusal text carried one; otherwise the marker is sticky until a later
|
|
2152
|
+
* successful run on this exact (account, model) clears it via
|
|
2153
|
+
* {@link clearClaudeModelRefusal}.
|
|
2154
|
+
*/
|
|
2155
|
+
export function claudeModelRefusalKey(accountId, home) {
|
|
2156
|
+
if (accountId)
|
|
2157
|
+
return `account:${accountId}`;
|
|
2158
|
+
if (!home)
|
|
2159
|
+
return undefined;
|
|
2160
|
+
try {
|
|
2161
|
+
return `context:${fs.realpathSync(home)}`;
|
|
2162
|
+
}
|
|
2163
|
+
catch {
|
|
2164
|
+
return undefined;
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
function modelFamily(model) {
|
|
2168
|
+
const normalized = model.trim().toLowerCase();
|
|
2169
|
+
return /(?:^|[-\s])(fable|sonnet|opus|haiku)(?:$|[-\s\d])/.exec(normalized)?.[1] ?? normalized;
|
|
2170
|
+
}
|
|
2171
|
+
export function noteClaudeModelRefusal(accountKey, model, refusal, cachePath = getClaudeUsageCachePath()) {
|
|
2172
|
+
try {
|
|
2173
|
+
ensureLockTarget(cachePath, '{}');
|
|
2174
|
+
withFileLock(cachePath, () => {
|
|
2175
|
+
const cache = readClaudeUsageCacheFile(cachePath);
|
|
2176
|
+
const existing = cache[accountKey] ?? { capturedAt: null, windows: [] };
|
|
2177
|
+
const modelRefusals = { ...(existing.modelRefusals ?? {}) };
|
|
2178
|
+
modelRefusals[modelFamily(refusal.family ?? model)] = {
|
|
2179
|
+
family: refusal.family,
|
|
2180
|
+
resetsAt: refusal.resetsAt?.toISOString(),
|
|
2181
|
+
};
|
|
2182
|
+
cache[accountKey] = { ...existing, modelRefusals };
|
|
2183
|
+
atomicWriteFileSync(cachePath, JSON.stringify(cache, null, 2), 'utf-8');
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2186
|
+
catch {
|
|
2187
|
+
/* best-effort cache write — lock busy or disk full */
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* Clear a persisted model-refusal marker for exactly ONE (account, model)
|
|
2192
|
+
* pair after a run SUCCEEDS on that same account+model. Never clears a
|
|
2193
|
+
* sibling model on the same account, and never fires for an interactive
|
|
2194
|
+
* detach or an unknown outcome — the caller must have demonstrated an actual
|
|
2195
|
+
* completed success on this exact model before calling this.
|
|
2196
|
+
*/
|
|
2197
|
+
export function clearClaudeModelRefusal(accountKey, model, cachePath = getClaudeUsageCachePath()) {
|
|
2198
|
+
model = modelFamily(model);
|
|
2199
|
+
try {
|
|
2200
|
+
if (!fs.existsSync(cachePath))
|
|
2201
|
+
return;
|
|
2202
|
+
withFileLock(cachePath, () => {
|
|
2203
|
+
const cache = readClaudeUsageCacheFile(cachePath);
|
|
2204
|
+
const existing = cache[accountKey];
|
|
2205
|
+
if (!existing?.modelRefusals?.[model])
|
|
2206
|
+
return;
|
|
2207
|
+
const modelRefusals = { ...existing.modelRefusals };
|
|
2208
|
+
delete modelRefusals[model];
|
|
2209
|
+
const rest = { ...existing, modelRefusals };
|
|
2210
|
+
if (Object.keys(modelRefusals).length === 0)
|
|
2211
|
+
delete rest.modelRefusals;
|
|
2212
|
+
cache[accountKey] = rest;
|
|
2213
|
+
atomicWriteFileSync(cachePath, JSON.stringify(cache, null, 2), 'utf-8');
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2216
|
+
catch {
|
|
2217
|
+
/* best-effort cache write */
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
/**
|
|
2221
|
+
* Read a live (non-expired) model-refusal marker for (accountKey, model), or
|
|
2222
|
+
* null when none is recorded or the recorded one has passed its clock. A
|
|
2223
|
+
* marker with no `resetsAt` never expires here — it is sticky until
|
|
2224
|
+
* {@link clearClaudeModelRefusal} observes a real success.
|
|
2225
|
+
*/
|
|
2226
|
+
export function getClaudeModelRefusal(accountKey, model, nowMs = Date.now(), cachePath = getClaudeUsageCachePath()) {
|
|
2227
|
+
const cache = readClaudeUsageCacheFile(cachePath);
|
|
2228
|
+
const entry = cache[accountKey]?.modelRefusals?.[modelFamily(model)];
|
|
2229
|
+
if (!entry)
|
|
2230
|
+
return null;
|
|
2231
|
+
if (entry.resetsAt) {
|
|
2232
|
+
const reset = parseDateValue(entry.resetsAt);
|
|
2233
|
+
if (reset && reset.getTime() <= nowMs)
|
|
2234
|
+
return null;
|
|
2235
|
+
return { family: entry.family, resetsAt: reset };
|
|
2236
|
+
}
|
|
2237
|
+
return { family: entry.family, resetsAt: null };
|
|
2238
|
+
}
|
|
2239
|
+
/**
|
|
2240
|
+
* Parse Claude's model-specific refusal — the CLI's own phrasing when ONE
|
|
2241
|
+
* model's quota is exhausted while the account otherwise keeps serving:
|
|
2242
|
+
* "You've reached your Fable limit. Run /usage-credits to continue or switch
|
|
2243
|
+
* models with /model." Deliberately narrow (unlike the broad RATE_LIMIT_PATTERNS
|
|
2244
|
+
* scan) so a session that merely discusses `/usage-credits` cannot false-positive.
|
|
2245
|
+
* Tolerates both a straight and curly apostrophe.
|
|
2246
|
+
*/
|
|
2247
|
+
export function parseClaudeModelRefusal(text) {
|
|
2248
|
+
const candidates = [text];
|
|
2249
|
+
for (const line of text.split('\n')) {
|
|
2250
|
+
try {
|
|
2251
|
+
const row = JSON.parse(line);
|
|
2252
|
+
if (row.type === 'result' && row.is_error && typeof row.result === 'string')
|
|
2253
|
+
candidates.push(row.result);
|
|
2254
|
+
if (row.type === 'assistant' && row.isApiErrorMessage && Array.isArray(row.message?.content)) {
|
|
2255
|
+
candidates.push(row.message.content.filter((block) => block.type === 'text').map((block) => block.text ?? '').join('\n'));
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
catch { /* Plain terminal output is checked directly. */ }
|
|
2259
|
+
}
|
|
2260
|
+
for (const candidate of candidates) {
|
|
2261
|
+
const match = /(?:^|\n)\s*You['’]ve reached your ([^\n.]+) limit\.\s*Run \/usage-credits to continue or switch models with \/model\./i.exec(candidate);
|
|
2262
|
+
if (match)
|
|
2263
|
+
return { family: match[1].trim() };
|
|
2264
|
+
}
|
|
2265
|
+
return null;
|
|
2266
|
+
}
|
|
2118
2267
|
/** Parse Claude's `hit your session limit · resets …` refusal. */
|
|
2119
2268
|
export function parseClaudeSessionLimitReset(text, nowMs = Date.now()) {
|
|
2120
2269
|
if (!/hit your session limit/i.test(text))
|
|
@@ -54,4 +54,6 @@ export declare function resolveNativeSpawnHome(agent: AgentId, account: {
|
|
|
54
54
|
id: string;
|
|
55
55
|
name: string;
|
|
56
56
|
agent: AgentId;
|
|
57
|
-
}, meta?: Pick<Meta, 'accounts' | 'deviceAccounts'
|
|
57
|
+
}, meta?: Pick<Meta, 'accounts' | 'deviceAccounts'>, options?: {
|
|
58
|
+
readOnly?: boolean;
|
|
59
|
+
}): Promise<NativeSpawnHome>;
|