@phnx-labs/agents-cli 1.22.45 → 1.22.47
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 +303 -0
- package/LICENSE +102 -182
- package/README.md +54 -17
- package/dist/cli/command-registry.d.ts +2 -1
- package/dist/cli/command-registry.js +6 -2
- package/dist/commands/accounts.js +56 -12
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +229 -0
- package/dist/commands/browser-sessions-picker.js +9 -7
- package/dist/commands/browser.js +266 -15
- package/dist/commands/config.js +26 -4
- package/dist/commands/cost.d.ts +1 -1
- package/dist/commands/detach.d.ts +13 -0
- package/dist/commands/detach.js +11 -8
- package/dist/commands/exec.js +55 -38
- package/dist/commands/feed-watch.d.ts +2 -0
- package/dist/commands/feed-watch.js +28 -0
- package/dist/commands/feed.js +27 -0
- package/dist/commands/feedback.js +3 -19
- package/dist/commands/insights.d.ts +1 -1
- package/dist/commands/insights.js +2 -2
- package/dist/commands/lease.js +3 -3
- package/dist/commands/routines.js +1 -1
- package/dist/commands/sessions-picker-factory.d.ts +2 -1
- package/dist/commands/sessions-picker-factory.js +1 -1
- package/dist/commands/sessions-stop.d.ts +24 -0
- package/dist/commands/sessions-stop.js +66 -0
- package/dist/commands/sessions-trace.d.ts +82 -0
- package/dist/commands/sessions-trace.js +387 -0
- package/dist/commands/sessions.js +4 -0
- package/dist/commands/ssh.d.ts +15 -0
- package/dist/commands/ssh.js +194 -16
- package/dist/commands/teams.js +34 -2
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +19 -32
- package/dist/commands/watchdog.js +9 -14
- package/dist/lib/account-capabilities.d.ts +4 -0
- package/dist/lib/account-capabilities.js +13 -9
- package/dist/lib/account-registry.d.ts +2 -0
- package/dist/lib/account-registry.js +30 -2
- package/dist/lib/accounting/rotate.d.ts +9 -0
- package/dist/lib/accounting/rotate.js +12 -2
- package/dist/lib/accounting/usage.d.ts +92 -6
- package/dist/lib/accounting/usage.js +329 -75
- package/dist/lib/agent-modes.js +29 -1
- package/dist/lib/agent-spec/agents.d.ts +4 -3
- package/dist/lib/agent-spec/agents.js +20 -7
- package/dist/lib/analytics/mix-commands.d.ts +1 -1
- package/dist/lib/analytics/mix-commands.js +2 -2
- package/dist/lib/auth-health.d.ts +13 -3
- package/dist/lib/auth-health.js +50 -8
- package/dist/lib/browser/devices.d.ts +19 -0
- package/dist/lib/browser/devices.js +23 -0
- package/dist/lib/browser/ipc.d.ts +1 -1
- package/dist/lib/browser/ipc.js +33 -7
- package/dist/lib/browser/profiles.d.ts +106 -1
- package/dist/lib/browser/profiles.js +300 -17
- package/dist/lib/browser/remote-control.d.ts +26 -2
- package/dist/lib/browser/remote-control.js +33 -3
- package/dist/lib/browser/runtime-state.d.ts +9 -0
- package/dist/lib/browser/runtime-state.js +24 -8
- package/dist/lib/browser/service.d.ts +37 -0
- package/dist/lib/browser/service.js +78 -20
- package/dist/lib/browser/sessions-list.d.ts +1 -3
- package/dist/lib/browser/sessions-list.js +3 -16
- package/dist/lib/browser/types.d.ts +12 -1
- package/dist/lib/cli-resources.js +17 -15
- package/dist/lib/codex-policy.d.ts +11 -1
- package/dist/lib/codex-policy.js +20 -5
- package/dist/lib/config-keys.d.ts +4 -3
- package/dist/lib/config-keys.js +17 -7
- package/dist/lib/daemon/runner.js +15 -2
- package/dist/lib/daemon-ticks.js +2 -2
- package/dist/lib/device-config.d.ts +25 -8
- package/dist/lib/device-config.js +90 -17
- package/dist/lib/devices/config-migration.d.ts +7 -0
- package/dist/lib/devices/config-migration.js +67 -3
- package/dist/lib/devices/harness-inventory.js +20 -3
- package/dist/lib/devices/health.d.ts +27 -2
- package/dist/lib/devices/health.js +39 -9
- package/dist/lib/devices/interactive-host.d.ts +15 -0
- package/dist/lib/devices/interactive-host.js +53 -0
- package/dist/lib/devices/registry.d.ts +61 -4
- package/dist/lib/devices/registry.js +94 -38
- package/dist/lib/devices/stats-cache.d.ts +6 -0
- package/dist/lib/devices/stats-cache.js +10 -2
- package/dist/lib/exec.d.ts +25 -3
- package/dist/lib/exec.js +75 -22
- package/dist/lib/feed/answer.d.ts +29 -0
- package/dist/lib/feed/answer.js +145 -0
- package/dist/lib/feed/attention.d.ts +83 -0
- package/dist/lib/feed/attention.js +0 -0
- package/dist/lib/feed/feed.d.ts +109 -0
- package/dist/lib/feed/feed.js +154 -3
- package/dist/lib/feed/pr-status.d.ts +14 -0
- package/dist/lib/feed/pr-status.js +47 -0
- package/dist/lib/feed/watch.d.ts +59 -0
- package/dist/lib/feed/watch.js +180 -0
- package/dist/lib/fleet/capture.js +6 -0
- package/dist/lib/fleet/remote-login.js +8 -3
- package/dist/lib/fleet/types.d.ts +17 -0
- package/dist/lib/harness/adapter.d.ts +11 -0
- package/dist/lib/harness/adapters/claude.js +38 -24
- package/dist/lib/harness/adapters/codex.js +2 -4
- package/dist/lib/hooks/cache.js +8 -0
- package/dist/lib/hooks/match.d.ts +3 -0
- package/dist/lib/hooks/match.js +25 -0
- package/dist/lib/hosts/passthrough.js +16 -0
- package/dist/lib/hosts/registry.js +7 -0
- package/dist/lib/identity/client.d.ts +53 -0
- package/dist/lib/identity/client.js +106 -0
- package/dist/lib/identity/index.d.ts +115 -0
- package/dist/lib/identity/index.js +82 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +13 -0
- package/dist/lib/menubar/install-menubar.js +26 -2
- package/dist/lib/open-url.d.ts +60 -2
- package/dist/lib/open-url.js +167 -12
- package/dist/lib/probe.d.ts +8 -0
- package/dist/lib/probe.js +105 -0
- package/dist/lib/refresh-coordinator.d.ts +1 -1
- package/dist/lib/refresh-coordinator.js +1 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/session/active.d.ts +67 -0
- package/dist/lib/session/active.js +69 -1
- package/dist/lib/session/db.js +4 -2
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +12 -6
- package/dist/lib/session/prompt.d.ts +35 -0
- package/dist/lib/session/prompt.js +56 -0
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +6 -1
- package/dist/lib/session/render.js +7 -3
- package/dist/lib/session/session-cache.js +5 -1
- package/dist/lib/session/trajectory-compare.d.ts +47 -0
- package/dist/lib/session/trajectory-compare.js +116 -0
- package/dist/lib/session/trajectory-html.d.ts +15 -0
- package/dist/lib/session/trajectory-html.js +750 -0
- package/dist/lib/session/trajectory-lineage.d.ts +92 -0
- package/dist/lib/session/trajectory-lineage.js +195 -0
- package/dist/lib/session/trajectory-text.d.ts +37 -0
- package/dist/lib/session/trajectory-text.js +277 -0
- package/dist/lib/session/trajectory.d.ts +102 -0
- package/dist/lib/session/trajectory.js +335 -0
- package/dist/lib/startup/command-registry.d.ts +5 -2
- package/dist/lib/startup/command-registry.js +8 -5
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +4 -2
- package/dist/lib/teams/agents.js +9 -0
- package/dist/lib/terminal/inject.d.ts +19 -0
- package/dist/lib/terminal/inject.js +19 -9
- package/dist/lib/tmux/session.d.ts +46 -13
- package/dist/lib/tmux/session.js +173 -27
- package/dist/lib/types.d.ts +16 -0
- package/dist/lib/usage-backoff.d.ts +2 -1
- package/dist/lib/usage-backoff.js +60 -19
- package/dist/lib/usage-refresh.d.ts +17 -3
- package/dist/lib/usage-refresh.js +57 -11
- package/dist/lib/view-types.d.ts +2 -2
- package/dist/lib/watchdog/log.d.ts +1 -1
- package/dist/lib/watchdog/log.js +1 -1
- package/dist/lib/watchdog/runner.d.ts +36 -45
- package/dist/lib/watchdog/runner.js +150 -119
- package/dist/lib/watchdog/service.d.ts +0 -1
- package/dist/lib/watchdog/service.js +0 -1
- package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
- package/dist/lib/watchdog/watchdog-agent.js +59 -0
- package/dist/lib/watchdog/watchdog.d.ts +11 -2
- package/dist/lib/watchdog/watchdog.js +37 -104
- package/package.json +2 -2
- package/dist/commands/usage.d.ts +0 -27
- package/dist/commands/usage.js +0 -96
|
@@ -118,6 +118,57 @@ export function usageUnreachableError(agent, cause) {
|
|
|
118
118
|
? `${agent} usage read failed: ${detail}`
|
|
119
119
|
: `${agent} usage read failed.`;
|
|
120
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Marker for a log-based (`network: false`) provider — Codex, Grok — that has
|
|
123
|
+
* simply never recorded a rate-limit event on this machine yet: no session
|
|
124
|
+
* log exists, or no session in it carries usage data. Distinct on purpose from
|
|
125
|
+
* `usageUnreachableError`: that one means the local log COULDN'T be read (a
|
|
126
|
+
* real failure worth surfacing distinctly); this one means there is nothing to
|
|
127
|
+
* read because the account has not run here, which is expected for a fresh
|
|
128
|
+
* install and should render as a benign state, not an error (RUSH-3040).
|
|
129
|
+
*/
|
|
130
|
+
export const USAGE_NO_RECENT_USAGE_MARKER = 'no usage recorded yet';
|
|
131
|
+
export const USAGE_BENIGN_STATE = Symbol('usageBenignState');
|
|
132
|
+
/**
|
|
133
|
+
* Shared error-classification + 429 backoff for a networked usage fetch whose
|
|
134
|
+
* only signal is an HTTP status (or none at all, on a network failure) —
|
|
135
|
+
* Antigravity's :retrieveUserQuota and Muse's Meta Model API probe are both
|
|
136
|
+
* this shape. They were added to `USAGE_SOURCES` after the four original
|
|
137
|
+
* `usageXError` constructors and did not get their own scheme (usage.ts's
|
|
138
|
+
* error handling was written for "four networked providers"; RUSH-3040).
|
|
139
|
+
* Route every no-snapshot outcome for either through this one function so a
|
|
140
|
+
* future entry cannot be added second-class again — it owns noting the 429
|
|
141
|
+
* backoff, so callers must NOT also call {@link noteUsageRateLimited} for the
|
|
142
|
+
* same response.
|
|
143
|
+
*/
|
|
144
|
+
export function classifyUsageFetchFailure(agent, agentId, status, retryAfterHeader, usageScope) {
|
|
145
|
+
if (status === 429) {
|
|
146
|
+
noteUsageRateLimited(agentId, retryAfterHeader ?? null, { account: usageScope });
|
|
147
|
+
return usageRejectedError(agent, 429);
|
|
148
|
+
}
|
|
149
|
+
if (status !== null)
|
|
150
|
+
return usageRejectedError(agent, status);
|
|
151
|
+
return usageUnreachableError(agent);
|
|
152
|
+
}
|
|
153
|
+
/** Classify a `UsageInfo.error` string into its {@link UsageErrorKind}, or null when there is no error. */
|
|
154
|
+
export function classifyUsageErrorKind(error) {
|
|
155
|
+
if (!error)
|
|
156
|
+
return null;
|
|
157
|
+
if (isUsageHeadlessScopeError(error))
|
|
158
|
+
return 'headless-scope';
|
|
159
|
+
if (error.startsWith('No readable '))
|
|
160
|
+
return 'no-credential';
|
|
161
|
+
if (error.includes('credential expired'))
|
|
162
|
+
return 'expired-credential';
|
|
163
|
+
if (error.includes('rate-limited this machine') || error.includes('is rate-limiting the usage endpoint')) {
|
|
164
|
+
return 'rate-limited';
|
|
165
|
+
}
|
|
166
|
+
if (error.includes('rejected the usage read'))
|
|
167
|
+
return 'rejected';
|
|
168
|
+
if (error.includes('usage read failed'))
|
|
169
|
+
return 'unreachable';
|
|
170
|
+
return 'rejected';
|
|
171
|
+
}
|
|
121
172
|
/**
|
|
122
173
|
* True when a Claude OAuth access token is within the refresh leeway of expiry
|
|
123
174
|
* (or already expired) — i.e. it "would need a refresh" before the next use.
|
|
@@ -165,6 +216,14 @@ const COMPACT_BAR_LEN = 5;
|
|
|
165
216
|
const USAGE_BAR_LEN = 10;
|
|
166
217
|
const FULL = '\u2588';
|
|
167
218
|
const EMPTY = '\u2591';
|
|
219
|
+
/** Construct the benign no-local-log result without overloading `error`. */
|
|
220
|
+
export function usageNoRecentUsageInfo() {
|
|
221
|
+
return { snapshot: null, error: null, [USAGE_BENIGN_STATE]: 'no-recent-usage' };
|
|
222
|
+
}
|
|
223
|
+
/** Read a benign state for the human renderer; symbols are omitted by JSON serialization. */
|
|
224
|
+
export function getUsageBenignState(info) {
|
|
225
|
+
return info[USAGE_BENIGN_STATE] ?? null;
|
|
226
|
+
}
|
|
168
227
|
/** The single registry of agent usage sources and their transport. */
|
|
169
228
|
const USAGE_SOURCES = {
|
|
170
229
|
claude: { fetch: getClaudeUsageInfo, network: true },
|
|
@@ -333,6 +392,9 @@ async function fetchLiveUsageDeduped(input, usageKey, cached, fileOnly) {
|
|
|
333
392
|
home: input.home,
|
|
334
393
|
cliVersion: input.cliVersion,
|
|
335
394
|
organizationId: input.info.organizationId,
|
|
395
|
+
// Scope this fetch's 429 backoff to the account being fetched, so one
|
|
396
|
+
// throttled account cannot park the whole provider (RUSH-3036).
|
|
397
|
+
usageScope: usageKey,
|
|
336
398
|
fileOnly,
|
|
337
399
|
});
|
|
338
400
|
if (usage.snapshot) {
|
|
@@ -391,6 +453,26 @@ export function pickCompactUsageWindows(windows, maxWindows) {
|
|
|
391
453
|
}
|
|
392
454
|
return chosen;
|
|
393
455
|
}
|
|
456
|
+
/** Human label for a classified usage error, for the no-bars branch of {@link formatUsageSummary}. */
|
|
457
|
+
function formatUsageErrorKindLabel(kind, detail) {
|
|
458
|
+
switch (kind) {
|
|
459
|
+
case 'no-credential':
|
|
460
|
+
return 'sign in / provision token';
|
|
461
|
+
case 'expired-credential':
|
|
462
|
+
return 're-auth for usage';
|
|
463
|
+
case 'rate-limited': {
|
|
464
|
+
const retryHint = detail?.match(/not retrying for (.+)\.$/)?.[1] ?? null;
|
|
465
|
+
return retryHint ? `rate-limited (retry ~${retryHint})` : 'rate-limited';
|
|
466
|
+
}
|
|
467
|
+
case 'rejected':
|
|
468
|
+
case 'unreachable':
|
|
469
|
+
case 'headless-scope':
|
|
470
|
+
case null:
|
|
471
|
+
case undefined:
|
|
472
|
+
default:
|
|
473
|
+
return 'usage unavailable';
|
|
474
|
+
}
|
|
475
|
+
}
|
|
394
476
|
/** Format a one-line usage summary with compact bars for inline display. */
|
|
395
477
|
export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
|
|
396
478
|
const parts = [];
|
|
@@ -398,7 +480,10 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
|
|
|
398
480
|
parts.push(chalk.gray(plan.padEnd(planWidth)));
|
|
399
481
|
}
|
|
400
482
|
if (snapshot) {
|
|
401
|
-
if (snapshot.unavailable?.reason === '
|
|
483
|
+
if (snapshot.unavailable?.reason === 'out_of_credits') {
|
|
484
|
+
parts.push(chalk.red('out of credits'));
|
|
485
|
+
}
|
|
486
|
+
else if (snapshot.unavailable?.reason === 'session_limit' && snapshot.unavailable.resetsAt) {
|
|
402
487
|
parts.push(chalk.yellow(`session-limited (${formatResetHint(snapshot.unavailable.resetsAt)})`));
|
|
403
488
|
}
|
|
404
489
|
// Compact rows show BLOCKING windows — the same set
|
|
@@ -442,11 +527,16 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
|
|
|
442
527
|
// mistaken for a missing setup-token or seeding failure (RUSH-2392).
|
|
443
528
|
parts.push(chalk.dim(USAGE_HEADLESS_SCOPE_MARKER));
|
|
444
529
|
}
|
|
530
|
+
else if (opts?.benignState === 'no-recent-usage') {
|
|
531
|
+
parts.push(chalk.dim(USAGE_NO_RECENT_USAGE_MARKER));
|
|
532
|
+
}
|
|
445
533
|
else if (opts?.unavailable) {
|
|
446
534
|
// Signed-in account we could NOT fetch usage for (no live token in a reachable
|
|
447
535
|
// home / org mismatch / fetch error). Say so explicitly instead of drawing a
|
|
448
|
-
// blank gauge that reads like "0% used"
|
|
449
|
-
|
|
536
|
+
// blank gauge that reads like "0% used" — and name the SPECIFIC cause when
|
|
537
|
+
// the caller passed one, rather than the generic bucket that used to cover
|
|
538
|
+
// ~6 different failures (RUSH-3040).
|
|
539
|
+
parts.push(chalk.dim(formatUsageErrorKindLabel(opts.errorKind, opts.errorDetail)));
|
|
450
540
|
}
|
|
451
541
|
return parts.join(' ');
|
|
452
542
|
}
|
|
@@ -472,8 +562,14 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
|
|
|
472
562
|
export function deriveUsageStatusFromSnapshot(snapshot) {
|
|
473
563
|
if (!snapshot)
|
|
474
564
|
return null;
|
|
475
|
-
if (snapshot.unavailable
|
|
476
|
-
|
|
565
|
+
if (snapshot.unavailable) {
|
|
566
|
+
// out_of_credits has no clock — it stays blocking until a successful run
|
|
567
|
+
// clears it. session_limit blocks only until its reset time.
|
|
568
|
+
if (snapshot.unavailable.reason === 'out_of_credits')
|
|
569
|
+
return 'rate_limited';
|
|
570
|
+
if (snapshot.unavailable.resetsAt && snapshot.unavailable.resetsAt.getTime() > Date.now()) {
|
|
571
|
+
return 'rate_limited';
|
|
572
|
+
}
|
|
477
573
|
}
|
|
478
574
|
if (snapshot.windows.length === 0)
|
|
479
575
|
return null;
|
|
@@ -589,11 +685,17 @@ async function getCodexUsageInfo(options) {
|
|
|
589
685
|
return { snapshot: null, error: null };
|
|
590
686
|
}
|
|
591
687
|
const files = collectCodexSessionFiles(options?.home, sinceMs);
|
|
688
|
+
const now = new Date();
|
|
592
689
|
for (const filePath of files) {
|
|
593
690
|
const match = await readLatestCodexRateLimits(filePath);
|
|
594
691
|
if (!match)
|
|
595
692
|
continue;
|
|
596
|
-
|
|
693
|
+
// Same freshness filter Grok already applies (RUSH-3040): a window whose
|
|
694
|
+
// reset time or windowMinutes-derived expiry has passed is a STALE read,
|
|
695
|
+
// not a current one — rendering it as-is is how a codex bar kept showing
|
|
696
|
+
// "100% used" past its own reset. Try the next-older session file rather
|
|
697
|
+
// than surfacing a stale bar.
|
|
698
|
+
const windows = normalizeCodexWindows(match.rateLimits).filter((window) => isCachedUsageWindowFresh(window, match.capturedAt, now));
|
|
597
699
|
if (windows.length === 0)
|
|
598
700
|
continue;
|
|
599
701
|
return {
|
|
@@ -606,10 +708,14 @@ async function getCodexUsageInfo(options) {
|
|
|
606
708
|
error: null,
|
|
607
709
|
};
|
|
608
710
|
}
|
|
609
|
-
|
|
711
|
+
// No session ever recorded a rate-limit event on this machine (or none of
|
|
712
|
+
// the ones found were still fresh) — a benign "nothing to show yet", not a
|
|
713
|
+
// failure (RUSH-3040). Distinct from the outer catch below, which is a
|
|
714
|
+
// genuine read/parse failure.
|
|
715
|
+
return usageNoRecentUsageInfo();
|
|
610
716
|
}
|
|
611
|
-
catch {
|
|
612
|
-
return { snapshot: null, error:
|
|
717
|
+
catch (err) {
|
|
718
|
+
return { snapshot: null, error: usageUnreachableError('Codex', err) };
|
|
613
719
|
}
|
|
614
720
|
}
|
|
615
721
|
/**
|
|
@@ -662,7 +768,7 @@ async function getClaudeUsageInfo(options) {
|
|
|
662
768
|
}
|
|
663
769
|
// Honour a live Retry-After rather than re-arming the penalty (see
|
|
664
770
|
// usage-backoff.ts). No request at all while the window is open.
|
|
665
|
-
const throttledUntil = usageRateLimitedUntil('claude');
|
|
771
|
+
const throttledUntil = usageRateLimitedUntil('claude', Date.now(), options?.usageScope);
|
|
666
772
|
if (throttledUntil) {
|
|
667
773
|
return { snapshot: null, error: usageThrottledError('Claude', throttledUntil) };
|
|
668
774
|
}
|
|
@@ -678,7 +784,7 @@ async function getClaudeUsageInfo(options) {
|
|
|
678
784
|
});
|
|
679
785
|
if (!response.ok) {
|
|
680
786
|
if (response.status === 429) {
|
|
681
|
-
noteUsageRateLimited('claude', response.headers.get('retry-after'));
|
|
787
|
+
noteUsageRateLimited('claude', response.headers.get('retry-after'), { account: options?.usageScope });
|
|
682
788
|
}
|
|
683
789
|
// Setup-token is user:inference only; usage needs user:profile → 403
|
|
684
790
|
// with a scope-requirement body. Distinct from a real rejection so the
|
|
@@ -771,7 +877,7 @@ async function getKimiUsageInfo(options) {
|
|
|
771
877
|
}
|
|
772
878
|
// Honour a live Retry-After rather than re-arming the penalty (see
|
|
773
879
|
// usage-backoff.ts). No request at all while the window is open.
|
|
774
|
-
const throttledUntil = usageRateLimitedUntil('kimi');
|
|
880
|
+
const throttledUntil = usageRateLimitedUntil('kimi', Date.now(), options?.usageScope);
|
|
775
881
|
if (throttledUntil) {
|
|
776
882
|
return { snapshot: null, error: usageThrottledError('Kimi', throttledUntil) };
|
|
777
883
|
}
|
|
@@ -787,7 +893,7 @@ async function getKimiUsageInfo(options) {
|
|
|
787
893
|
// way there are no bars to draw, and the status is what tells them apart.
|
|
788
894
|
if (!response.ok) {
|
|
789
895
|
if (response.status === 429) {
|
|
790
|
-
noteUsageRateLimited('kimi', response.headers.get('retry-after'));
|
|
896
|
+
noteUsageRateLimited('kimi', response.headers.get('retry-after'), { account: options?.usageScope });
|
|
791
897
|
}
|
|
792
898
|
return { snapshot: null, error: usageRejectedError('Kimi', response.status) };
|
|
793
899
|
}
|
|
@@ -905,7 +1011,7 @@ async function getDroidUsageInfo(options) {
|
|
|
905
1011
|
}
|
|
906
1012
|
// Honour a live Retry-After rather than re-arming the penalty (see
|
|
907
1013
|
// usage-backoff.ts). No request at all while the window is open.
|
|
908
|
-
const throttledUntil = usageRateLimitedUntil('droid');
|
|
1014
|
+
const throttledUntil = usageRateLimitedUntil('droid', Date.now(), options?.usageScope);
|
|
909
1015
|
if (throttledUntil) {
|
|
910
1016
|
return { snapshot: null, error: usageThrottledError('Droid', throttledUntil) };
|
|
911
1017
|
}
|
|
@@ -920,7 +1026,7 @@ async function getDroidUsageInfo(options) {
|
|
|
920
1026
|
// 401 => revoked/expired token. No bars to draw, and the status says why.
|
|
921
1027
|
if (!response.ok) {
|
|
922
1028
|
if (response.status === 429) {
|
|
923
|
-
noteUsageRateLimited('droid', response.headers.get('retry-after'));
|
|
1029
|
+
noteUsageRateLimited('droid', response.headers.get('retry-after'), { account: options?.usageScope });
|
|
924
1030
|
}
|
|
925
1031
|
return { snapshot: null, error: usageRejectedError('Droid', response.status) };
|
|
926
1032
|
}
|
|
@@ -947,7 +1053,7 @@ async function getDroidUsageInfo(options) {
|
|
|
947
1053
|
}
|
|
948
1054
|
}
|
|
949
1055
|
/** Probe Claude's OAuth token against the usage endpoint. Never refreshes — reports `expired` for a near-expiry token; see the comment below (RUSH-1822). */
|
|
950
|
-
export async function probeClaudeStatus(home, cliVersion) {
|
|
1056
|
+
export async function probeClaudeStatus(home, cliVersion, usageScope) {
|
|
951
1057
|
// accessTokenCache: the daemon warms this probe every ~3 min per account, so it
|
|
952
1058
|
// reads ONLY the file-based setup-token and never the interactive login —
|
|
953
1059
|
// transmitting that ACL-bound token to the usage API from a background loop is
|
|
@@ -972,7 +1078,7 @@ export async function probeClaudeStatus(home, cliVersion) {
|
|
|
972
1078
|
// window is open, report the throttle from the recorded state instead of
|
|
973
1079
|
// firing again and re-arming it (usage-backoff.ts). This 3-min-cadence
|
|
974
1080
|
// probe is what created the loop it now respects.
|
|
975
|
-
if (usageRateLimitedUntil('claude'))
|
|
1081
|
+
if (usageRateLimitedUntil('claude', Date.now(), usageScope))
|
|
976
1082
|
return { status: 429, token: 'present' };
|
|
977
1083
|
try {
|
|
978
1084
|
const response = await fetch(CLAUDE_USAGE_URL, {
|
|
@@ -986,7 +1092,7 @@ export async function probeClaudeStatus(home, cliVersion) {
|
|
|
986
1092
|
signal: AbortSignal.timeout(8000),
|
|
987
1093
|
});
|
|
988
1094
|
if (response.status === 429) {
|
|
989
|
-
noteUsageRateLimited('claude', response.headers.get('retry-after'));
|
|
1095
|
+
noteUsageRateLimited('claude', response.headers.get('retry-after'), { account: usageScope });
|
|
990
1096
|
}
|
|
991
1097
|
// Setup-token is user:inference only; usage needs user:profile → 403.
|
|
992
1098
|
// That is NOT a revocation — the account still runs (RUSH-2392).
|
|
@@ -1014,7 +1120,7 @@ export async function probeClaudeStatus(home, cliVersion) {
|
|
|
1014
1120
|
}
|
|
1015
1121
|
}
|
|
1016
1122
|
/** Probe Kimi's OAuth token against the /usages endpoint. Never refreshes (single-use rotation — see getKimiUsageInfo). */
|
|
1017
|
-
export async function probeKimiStatus(home) {
|
|
1123
|
+
export async function probeKimiStatus(home, usageScope) {
|
|
1018
1124
|
const credPath = resolveKimiCredentialPath(home);
|
|
1019
1125
|
if (!credPath)
|
|
1020
1126
|
return { status: null, token: 'missing' };
|
|
@@ -1038,7 +1144,7 @@ export async function probeKimiStatus(home) {
|
|
|
1038
1144
|
// is what created the loop it now respects. It sits AFTER the local
|
|
1039
1145
|
// missing/expired checks — as in probeClaudeStatus and probeDroidStatus — so
|
|
1040
1146
|
// a genuinely broken credential is never misreported as merely throttled.
|
|
1041
|
-
if (usageRateLimitedUntil('kimi'))
|
|
1147
|
+
if (usageRateLimitedUntil('kimi', Date.now(), usageScope))
|
|
1042
1148
|
return { status: 429, token: 'present' };
|
|
1043
1149
|
try {
|
|
1044
1150
|
const response = await fetch(KIMI_USAGES_URL, {
|
|
@@ -1047,7 +1153,7 @@ export async function probeKimiStatus(home) {
|
|
|
1047
1153
|
signal: AbortSignal.timeout(8000),
|
|
1048
1154
|
});
|
|
1049
1155
|
if (response.status === 429) {
|
|
1050
|
-
noteUsageRateLimited('kimi', response.headers.get('retry-after'));
|
|
1156
|
+
noteUsageRateLimited('kimi', response.headers.get('retry-after'), { account: usageScope });
|
|
1051
1157
|
}
|
|
1052
1158
|
return { status: response.status, token: 'present' };
|
|
1053
1159
|
}
|
|
@@ -1056,7 +1162,7 @@ export async function probeKimiStatus(home) {
|
|
|
1056
1162
|
}
|
|
1057
1163
|
}
|
|
1058
1164
|
/** Probe Droid's WorkOS token against the billing-limits endpoint. Never refreshes (single-use rotation — see getDroidUsageInfo). */
|
|
1059
|
-
export async function probeDroidStatus(home) {
|
|
1165
|
+
export async function probeDroidStatus(home, usageScope) {
|
|
1060
1166
|
const cred = decryptDroidAuthPayload(home || os.homedir());
|
|
1061
1167
|
const accessToken = cred?.access_token;
|
|
1062
1168
|
if (typeof accessToken !== 'string' || !accessToken)
|
|
@@ -1068,7 +1174,7 @@ export async function probeDroidStatus(home) {
|
|
|
1068
1174
|
// window is open, report the throttle from the recorded state instead of
|
|
1069
1175
|
// firing again and re-arming it (usage-backoff.ts). This 3-min-cadence
|
|
1070
1176
|
// probe is what created the loop it now respects.
|
|
1071
|
-
if (usageRateLimitedUntil('droid'))
|
|
1177
|
+
if (usageRateLimitedUntil('droid', Date.now(), usageScope))
|
|
1072
1178
|
return { status: 429, token: 'present' };
|
|
1073
1179
|
try {
|
|
1074
1180
|
const response = await fetch(DROID_USAGE_URL, {
|
|
@@ -1077,7 +1183,7 @@ export async function probeDroidStatus(home) {
|
|
|
1077
1183
|
signal: AbortSignal.timeout(8000),
|
|
1078
1184
|
});
|
|
1079
1185
|
if (response.status === 429) {
|
|
1080
|
-
noteUsageRateLimited('droid', response.headers.get('retry-after'));
|
|
1186
|
+
noteUsageRateLimited('droid', response.headers.get('retry-after'), { account: usageScope });
|
|
1081
1187
|
}
|
|
1082
1188
|
return { status: response.status, token: 'present' };
|
|
1083
1189
|
}
|
|
@@ -1487,12 +1593,9 @@ export function writeClaudeUsageCache(usageKey, snapshot, cachePath = getClaudeU
|
|
|
1487
1593
|
// refresh cannot drop another account's row (lost update).
|
|
1488
1594
|
const cache = readClaudeUsageCacheFile(cachePath);
|
|
1489
1595
|
const prior = cache[usageKey];
|
|
1490
|
-
const priorReset = parseDateValue(prior?.unavailable?.resetsAt);
|
|
1491
1596
|
cache[usageKey] = serializeClaudeUsageSnapshot({
|
|
1492
1597
|
...snapshot,
|
|
1493
|
-
unavailable:
|
|
1494
|
-
? { reason: 'session_limit', resetsAt: priorReset }
|
|
1495
|
-
: snapshot.unavailable,
|
|
1598
|
+
unavailable: carryForwardUnavailable(prior?.unavailable, snapshot.unavailable),
|
|
1496
1599
|
});
|
|
1497
1600
|
atomicWriteFileSync(cachePath, JSON.stringify(cache, null, 2), 'utf-8');
|
|
1498
1601
|
});
|
|
@@ -1530,7 +1633,10 @@ function serializeClaudeUsageSnapshot(snapshot) {
|
|
|
1530
1633
|
capturedAt: snapshot.capturedAt?.toISOString() || null,
|
|
1531
1634
|
plan: snapshot.plan ?? null,
|
|
1532
1635
|
unavailable: snapshot.unavailable
|
|
1533
|
-
? {
|
|
1636
|
+
? {
|
|
1637
|
+
reason: snapshot.unavailable.reason,
|
|
1638
|
+
resetsAt: snapshot.unavailable.resetsAt?.toISOString(),
|
|
1639
|
+
}
|
|
1534
1640
|
: undefined,
|
|
1535
1641
|
windows: snapshot.windows.map((window) => ({
|
|
1536
1642
|
key: window.key,
|
|
@@ -1552,6 +1658,10 @@ function serializeClaudeUsageSnapshot(snapshot) {
|
|
|
1552
1658
|
* candidate (RUSH-2858). Dropping mirrors the Grok collector, and an all-expired
|
|
1553
1659
|
* snapshot deserializes to null so `readClaudeUsageCache` deletes the entry and
|
|
1554
1660
|
* callers surface "usage unavailable" plus the recorded throttle reason.
|
|
1661
|
+
*
|
|
1662
|
+
* A row that carries a plan survives even with no fresh windows: the plan is a
|
|
1663
|
+
* truthful reading in its own right, and losing it is what made the cached view
|
|
1664
|
+
* contradict the refreshed one for meterless harnesses. See the guard below.
|
|
1555
1665
|
*/
|
|
1556
1666
|
function deserializeClaudeUsageSnapshot(snapshot, now) {
|
|
1557
1667
|
const capturedAt = parseDateValue(snapshot.capturedAt);
|
|
@@ -1565,11 +1675,18 @@ function deserializeClaudeUsageSnapshot(snapshot, now) {
|
|
|
1565
1675
|
windowMinutes: window.windowMinutes,
|
|
1566
1676
|
}))
|
|
1567
1677
|
.filter((window) => isCachedUsageWindowFresh(window, capturedAt, now));
|
|
1568
|
-
const
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1678
|
+
const unavailable = deserializeUnavailable(snapshot.unavailable, now);
|
|
1679
|
+
// A windowless row is not automatically worthless. Grok's collector reports
|
|
1680
|
+
// the subscription tier and no meters at all, so treating "no fresh windows"
|
|
1681
|
+
// as "nothing cached" deleted the only truthful thing we knew about the
|
|
1682
|
+
// account: `--refresh` wrote {plan: 'SuperGrok Heavy', windows: []}, the very
|
|
1683
|
+
// next plain `agents view` deserialized it to null, `readClaudeUsageCache`
|
|
1684
|
+
// pruned the row, and the row rendered "usage unavailable" one read after a
|
|
1685
|
+
// successful refresh. Keep a plan-bearing row — it renders as the plan alone,
|
|
1686
|
+
// and `deriveUsageStatusFromSnapshot` still returns null for zero windows, so
|
|
1687
|
+
// it can never read as a 0% bar or an "available" badge (the RUSH-2858
|
|
1688
|
+
// property that made expired windows drop in the first place).
|
|
1689
|
+
if (windows.length === 0 && !unavailable && !snapshot.plan) {
|
|
1573
1690
|
return null;
|
|
1574
1691
|
}
|
|
1575
1692
|
return {
|
|
@@ -1581,6 +1698,82 @@ function deserializeClaudeUsageSnapshot(snapshot, now) {
|
|
|
1581
1698
|
unavailable,
|
|
1582
1699
|
};
|
|
1583
1700
|
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Carry a prior refusal marker forward across a daemon usage refresh, and drop
|
|
1703
|
+
* an expired one. A live `snapshot.unavailable` (a refusal just observed) wins.
|
|
1704
|
+
* `out_of_credits` survives refreshes with no reset — only a successful run
|
|
1705
|
+
* clears it (clearClaudeAccountRefusal). A `session_limit` survives only while
|
|
1706
|
+
* its reset time is still in the future.
|
|
1707
|
+
*/
|
|
1708
|
+
function carryForwardUnavailable(prior, live) {
|
|
1709
|
+
if (live)
|
|
1710
|
+
return live;
|
|
1711
|
+
if (!prior)
|
|
1712
|
+
return undefined;
|
|
1713
|
+
if (prior.reason === 'out_of_credits')
|
|
1714
|
+
return { reason: 'out_of_credits' };
|
|
1715
|
+
const reset = parseDateValue(prior.resetsAt);
|
|
1716
|
+
return reset && reset.getTime() > Date.now()
|
|
1717
|
+
? { reason: 'session_limit', resetsAt: reset }
|
|
1718
|
+
: undefined;
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* Deserialize a cached `unavailable` marker, dropping an expired session_limit
|
|
1722
|
+
* but keeping a clock-less out_of_credits.
|
|
1723
|
+
*/
|
|
1724
|
+
function deserializeUnavailable(cached, now) {
|
|
1725
|
+
if (!cached)
|
|
1726
|
+
return undefined;
|
|
1727
|
+
if (cached.reason === 'out_of_credits')
|
|
1728
|
+
return { reason: 'out_of_credits' };
|
|
1729
|
+
const reset = parseDateValue(cached.resetsAt);
|
|
1730
|
+
return reset && reset.getTime() > now.getTime()
|
|
1731
|
+
? { reason: 'session_limit', resetsAt: reset }
|
|
1732
|
+
: undefined;
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Persist a Claude tokens/credits exhaustion (`out of usage credits` / `monthly
|
|
1736
|
+
* spend limit`) from a real run. Unlike a rate/session limit this does NOT reset
|
|
1737
|
+
* on a clock, so no reset time is stored — rotation excludes the account until a
|
|
1738
|
+
* later successful run clears it via {@link clearClaudeAccountRefusal}.
|
|
1739
|
+
*/
|
|
1740
|
+
export function noteClaudeOutOfCredits(usageKey, cachePath = getClaudeUsageCachePath()) {
|
|
1741
|
+
try {
|
|
1742
|
+
ensureLockTarget(cachePath, '{}');
|
|
1743
|
+
withFileLock(cachePath, () => {
|
|
1744
|
+
const cache = readClaudeUsageCacheFile(cachePath);
|
|
1745
|
+
const existing = cache[usageKey] ?? { capturedAt: null, windows: [] };
|
|
1746
|
+
cache[usageKey] = { ...existing, unavailable: { reason: 'out_of_credits' } };
|
|
1747
|
+
atomicWriteFileSync(cachePath, JSON.stringify(cache, null, 2), 'utf-8');
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
catch {
|
|
1751
|
+
/* best-effort cache write — lock busy or disk full */
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Clear any persisted refusal marker for an account after a run SUCCEEDS on it.
|
|
1756
|
+
* This is the recovery path for `out_of_credits` (which has no clock) and also
|
|
1757
|
+
* proactively clears a stale `session_limit` the moment the account serves again.
|
|
1758
|
+
*/
|
|
1759
|
+
export function clearClaudeAccountRefusal(usageKey, cachePath = getClaudeUsageCachePath()) {
|
|
1760
|
+
try {
|
|
1761
|
+
if (!fs.existsSync(cachePath))
|
|
1762
|
+
return;
|
|
1763
|
+
withFileLock(cachePath, () => {
|
|
1764
|
+
const cache = readClaudeUsageCacheFile(cachePath);
|
|
1765
|
+
const existing = cache[usageKey];
|
|
1766
|
+
if (!existing?.unavailable)
|
|
1767
|
+
return;
|
|
1768
|
+
const { unavailable: _drop, ...rest } = existing;
|
|
1769
|
+
cache[usageKey] = rest;
|
|
1770
|
+
atomicWriteFileSync(cachePath, JSON.stringify(cache, null, 2), 'utf-8');
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
catch {
|
|
1774
|
+
/* best-effort cache write */
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1584
1777
|
/**
|
|
1585
1778
|
* Persist a Claude session-limit refusal from a real run until its stated reset.
|
|
1586
1779
|
* This quota is not part of Anthropic's five-hour/weekly usage response.
|
|
@@ -1873,11 +2066,12 @@ async function getGrokUsageInfo(options) {
|
|
|
1873
2066
|
try {
|
|
1874
2067
|
const base = options?.home || os.homedir();
|
|
1875
2068
|
const logPath = path.join(base, '.grok', 'logs', 'unified.jsonl');
|
|
2069
|
+
// No log yet: a benign "nothing recorded here", not a failure (RUSH-3040).
|
|
1876
2070
|
if (!fs.existsSync(logPath))
|
|
1877
|
-
return
|
|
2071
|
+
return usageNoRecentUsageInfo();
|
|
1878
2072
|
const match = await readLatestGrokBilling(logPath);
|
|
1879
2073
|
if (!match)
|
|
1880
|
-
return
|
|
2074
|
+
return usageNoRecentUsageInfo();
|
|
1881
2075
|
// Grok has no live usage API (`network: false`) — bars are last-seen from
|
|
1882
2076
|
// this machine's unified.jsonl only. Drop windows whose billing period has
|
|
1883
2077
|
// already ended so a stale 100% does not paint "rate-limited" after reset,
|
|
@@ -1886,6 +2080,10 @@ async function getGrokUsageInfo(options) {
|
|
|
1886
2080
|
// (see readLatestGrokBilling).
|
|
1887
2081
|
const now = new Date();
|
|
1888
2082
|
const windows = match.windows.filter((window) => isCachedUsageWindowFresh(window, match.capturedAt, now));
|
|
2083
|
+
// NOTE: an empty `windows` here (all windows expired) still returns a real
|
|
2084
|
+
// snapshot, not the no-recent-usage marker — the plan/tier is still valid
|
|
2085
|
+
// and callers (e.g. `formatUsageSummary`) render "no bars" correctly from
|
|
2086
|
+
// an empty array. The marker above is only for "nothing was ever recorded".
|
|
1889
2087
|
return {
|
|
1890
2088
|
snapshot: {
|
|
1891
2089
|
source: 'last_seen',
|
|
@@ -1897,8 +2095,8 @@ async function getGrokUsageInfo(options) {
|
|
|
1897
2095
|
error: null,
|
|
1898
2096
|
};
|
|
1899
2097
|
}
|
|
1900
|
-
catch {
|
|
1901
|
-
return { snapshot: null, error:
|
|
2098
|
+
catch (err) {
|
|
2099
|
+
return { snapshot: null, error: usageUnreachableError('Grok', err) };
|
|
1902
2100
|
}
|
|
1903
2101
|
}
|
|
1904
2102
|
/**
|
|
@@ -1912,16 +2110,40 @@ async function getGrokUsageInfo(options) {
|
|
|
1912
2110
|
async function getMuseUsageInfo(options) {
|
|
1913
2111
|
try {
|
|
1914
2112
|
const base = options?.home || os.homedir();
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
2113
|
+
// Honour a live Retry-After rather than re-arming the penalty (see
|
|
2114
|
+
// usage-backoff.ts). The local log fallback still works while the live
|
|
2115
|
+
// probe is throttled — only report the throttle when there is truly
|
|
2116
|
+
// nothing else to show.
|
|
2117
|
+
const throttledUntil = usageRateLimitedUntil('muse', Date.now(), options?.usageScope);
|
|
2118
|
+
if (throttledUntil) {
|
|
2119
|
+
const local = await readMuseLocalSessionUsage(base);
|
|
2120
|
+
if (local)
|
|
2121
|
+
return { snapshot: local, error: null };
|
|
2122
|
+
return { snapshot: null, error: usageThrottledError('Muse', throttledUntil) };
|
|
2123
|
+
}
|
|
2124
|
+
const probe = await probeMuseRateLimits(base);
|
|
2125
|
+
if (probe.snapshot)
|
|
2126
|
+
return { snapshot: probe.snapshot, error: null };
|
|
1918
2127
|
const local = await readMuseLocalSessionUsage(base);
|
|
1919
|
-
if (
|
|
1920
|
-
return { snapshot:
|
|
1921
|
-
|
|
2128
|
+
if (local)
|
|
2129
|
+
return { snapshot: local, error: null };
|
|
2130
|
+
// No live snapshot and no local log — every source came up empty. Only
|
|
2131
|
+
// now does the probe's own outcome become the reported error, mirroring
|
|
2132
|
+
// Cursor's "surface the last resort's failure" pattern above.
|
|
2133
|
+
if (!probe.hasKey)
|
|
2134
|
+
return { snapshot: null, error: usageNoCredentialError('Muse') };
|
|
2135
|
+
if (probe.noHeaders)
|
|
2136
|
+
return usageNoRecentUsageInfo();
|
|
2137
|
+
return {
|
|
2138
|
+
snapshot: null,
|
|
2139
|
+
error: classifyUsageFetchFailure('Muse', 'muse', probe.status, probe.retryAfter, options?.usageScope),
|
|
2140
|
+
};
|
|
1922
2141
|
}
|
|
1923
|
-
catch {
|
|
1924
|
-
|
|
2142
|
+
catch (err) {
|
|
2143
|
+
// A thrown request (timeout, DNS, TLS, a malformed payload) is a failed
|
|
2144
|
+
// read like any other — staying silent here would hand the caller a stale
|
|
2145
|
+
// snapshot to render as confirmed (RUSH-3040).
|
|
2146
|
+
return { snapshot: null, error: usageUnreachableError('Muse', err) };
|
|
1925
2147
|
}
|
|
1926
2148
|
}
|
|
1927
2149
|
/** Resolve a Muse API key from env or auth.json without logging the value. */
|
|
@@ -1955,25 +2177,28 @@ function resolveMuseApiKey(base) {
|
|
|
1955
2177
|
}
|
|
1956
2178
|
/**
|
|
1957
2179
|
* Probe Meta Model API for rate-limit headers. Uses GET /v1/models (no token
|
|
1958
|
-
* spend).
|
|
2180
|
+
* spend). The 429 backoff is noted by the CALLER via
|
|
2181
|
+
* {@link classifyUsageFetchFailure}, not here, so a throttled read is recorded
|
|
2182
|
+
* exactly once regardless of which branch of `getMuseUsageInfo` observes it.
|
|
1959
2183
|
*/
|
|
1960
2184
|
async function probeMuseRateLimits(base) {
|
|
1961
|
-
if (usageRateLimitedUntil('muse'))
|
|
1962
|
-
return null;
|
|
1963
2185
|
const key = resolveMuseApiKey(base);
|
|
1964
2186
|
if (!key)
|
|
1965
|
-
return null;
|
|
2187
|
+
return { snapshot: null, hasKey: false, status: null, retryAfter: null, noHeaders: false };
|
|
1966
2188
|
try {
|
|
1967
2189
|
const response = await fetch('https://api.meta.ai/v1/models', {
|
|
1968
2190
|
headers: { Authorization: `Bearer ${key}` },
|
|
1969
2191
|
signal: AbortSignal.timeout(8_000),
|
|
1970
2192
|
});
|
|
1971
|
-
if (response.
|
|
1972
|
-
|
|
1973
|
-
|
|
2193
|
+
if (!response.ok) {
|
|
2194
|
+
return {
|
|
2195
|
+
snapshot: null,
|
|
2196
|
+
hasKey: true,
|
|
2197
|
+
status: response.status,
|
|
2198
|
+
retryAfter: response.headers.get('retry-after'),
|
|
2199
|
+
noHeaders: false,
|
|
2200
|
+
};
|
|
1974
2201
|
}
|
|
1975
|
-
if (!response.ok)
|
|
1976
|
-
return null;
|
|
1977
2202
|
const limitTokens = headerNumber(response.headers, 'x-ratelimit-limit-tokens');
|
|
1978
2203
|
const remainingTokens = headerNumber(response.headers, 'x-ratelimit-remaining-tokens');
|
|
1979
2204
|
const limitRequests = headerNumber(response.headers, 'x-ratelimit-limit-requests');
|
|
@@ -2001,18 +2226,25 @@ async function probeMuseRateLimits(base) {
|
|
|
2001
2226
|
windowMinutes: 1,
|
|
2002
2227
|
});
|
|
2003
2228
|
}
|
|
2004
|
-
if (windows.length === 0)
|
|
2005
|
-
return null;
|
|
2229
|
+
if (windows.length === 0) {
|
|
2230
|
+
return { snapshot: null, hasKey: true, status: response.status, retryAfter: null, noHeaders: true };
|
|
2231
|
+
}
|
|
2006
2232
|
return {
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2233
|
+
snapshot: {
|
|
2234
|
+
source: 'live',
|
|
2235
|
+
sourceLabel: 'Meta Model API rate limits',
|
|
2236
|
+
capturedAt: new Date(),
|
|
2237
|
+
windows,
|
|
2238
|
+
plan: 'Meta Model API',
|
|
2239
|
+
},
|
|
2240
|
+
hasKey: true,
|
|
2241
|
+
status: response.status,
|
|
2242
|
+
retryAfter: null,
|
|
2243
|
+
noHeaders: false,
|
|
2012
2244
|
};
|
|
2013
2245
|
}
|
|
2014
2246
|
catch {
|
|
2015
|
-
return null;
|
|
2247
|
+
return { snapshot: null, hasKey: true, status: null, retryAfter: null, noHeaders: false };
|
|
2016
2248
|
}
|
|
2017
2249
|
}
|
|
2018
2250
|
function headerNumber(headers, name) {
|
|
@@ -2578,10 +2810,14 @@ async function refreshAntigravityAccessToken(refreshToken) {
|
|
|
2578
2810
|
}
|
|
2579
2811
|
/**
|
|
2580
2812
|
* POST :retrieveUserQuota against the Code Assist endpoints in order, returning
|
|
2581
|
-
* the first successful bucket list. null when every endpoint rejects
|
|
2582
|
-
* token, no quota API for the account) or the network fails
|
|
2813
|
+
* the first successful bucket list. `buckets: null` when every endpoint rejects
|
|
2814
|
+
* (expired token, no quota API for the account) or the network fails — `status`
|
|
2815
|
+
* carries the LAST rejection's HTTP status (or null when every attempt threw)
|
|
2816
|
+
* so the caller can classify the failure instead of it reading as silence.
|
|
2583
2817
|
*/
|
|
2584
2818
|
async function fetchAntigravityQuota(accessToken) {
|
|
2819
|
+
let lastStatus = null;
|
|
2820
|
+
let lastRetryAfter = null;
|
|
2585
2821
|
for (const url of ANTIGRAVITY_QUOTA_URLS) {
|
|
2586
2822
|
try {
|
|
2587
2823
|
const response = await fetch(url, {
|
|
@@ -2594,16 +2830,19 @@ async function fetchAntigravityQuota(accessToken) {
|
|
|
2594
2830
|
body: '{}',
|
|
2595
2831
|
signal: AbortSignal.timeout(5000),
|
|
2596
2832
|
});
|
|
2597
|
-
if (!response.ok)
|
|
2833
|
+
if (!response.ok) {
|
|
2834
|
+
lastStatus = response.status;
|
|
2835
|
+
lastRetryAfter = response.headers.get('retry-after');
|
|
2598
2836
|
continue;
|
|
2837
|
+
}
|
|
2599
2838
|
const data = (await response.json());
|
|
2600
|
-
return Array.isArray(data?.buckets) ? data.buckets : [];
|
|
2839
|
+
return { buckets: Array.isArray(data?.buckets) ? data.buckets : [], status: response.status, retryAfter: null };
|
|
2601
2840
|
}
|
|
2602
2841
|
catch {
|
|
2603
2842
|
continue;
|
|
2604
2843
|
}
|
|
2605
2844
|
}
|
|
2606
|
-
return null;
|
|
2845
|
+
return { buckets: null, status: lastStatus, retryAfter: lastRetryAfter };
|
|
2607
2846
|
}
|
|
2608
2847
|
/** Compact model tag for the inline bar — 'gemini-2.5-flash-lite' => '2.5FL'. */
|
|
2609
2848
|
export function antigravityModelShortLabel(modelId) {
|
|
@@ -2665,16 +2904,28 @@ async function getAntigravityUsageInfo(options) {
|
|
|
2665
2904
|
try {
|
|
2666
2905
|
const token = await loadAntigravityOauth(options?.home);
|
|
2667
2906
|
if (!token)
|
|
2668
|
-
return { snapshot: null, error:
|
|
2907
|
+
return { snapshot: null, error: usageNoCredentialError('Antigravity') };
|
|
2669
2908
|
let accessToken = normalizeString(token.access_token);
|
|
2670
2909
|
if ((!accessToken || antigravityTokenNeedsRefresh(token.expiry)) && token.refresh_token) {
|
|
2671
2910
|
accessToken = await refreshAntigravityAccessToken(token.refresh_token);
|
|
2672
2911
|
}
|
|
2673
2912
|
if (!accessToken)
|
|
2674
|
-
return { snapshot: null, error:
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2913
|
+
return { snapshot: null, error: usageExpiredCredentialError('Antigravity') };
|
|
2914
|
+
// Honour a live Retry-After rather than re-arming the penalty (see
|
|
2915
|
+
// usage-backoff.ts). No request at all while the window is open. Antigravity
|
|
2916
|
+
// previously had NO rate-limit backoff at all (RUSH-3040) — every refresh
|
|
2917
|
+
// re-hit a throttled endpoint.
|
|
2918
|
+
const throttledUntil = usageRateLimitedUntil('antigravity', Date.now(), options?.usageScope);
|
|
2919
|
+
if (throttledUntil) {
|
|
2920
|
+
return { snapshot: null, error: usageThrottledError('Antigravity', throttledUntil) };
|
|
2921
|
+
}
|
|
2922
|
+
const { buckets, status, retryAfter } = await fetchAntigravityQuota(accessToken);
|
|
2923
|
+
if (!buckets) {
|
|
2924
|
+
return {
|
|
2925
|
+
snapshot: null,
|
|
2926
|
+
error: classifyUsageFetchFailure('Antigravity', 'antigravity', status, retryAfter, options?.usageScope),
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2678
2929
|
const windows = normalizeAntigravityWindows(buckets);
|
|
2679
2930
|
if (windows.length === 0)
|
|
2680
2931
|
return { snapshot: null, error: null };
|
|
@@ -2688,7 +2939,10 @@ async function getAntigravityUsageInfo(options) {
|
|
|
2688
2939
|
error: null,
|
|
2689
2940
|
};
|
|
2690
2941
|
}
|
|
2691
|
-
catch {
|
|
2692
|
-
|
|
2942
|
+
catch (err) {
|
|
2943
|
+
// A thrown request (timeout, DNS, TLS, a malformed payload) is a failed
|
|
2944
|
+
// read like any other — staying silent here would hand the caller a stale
|
|
2945
|
+
// snapshot to render as confirmed (RUSH-3040).
|
|
2946
|
+
return { snapshot: null, error: usageUnreachableError('Antigravity', err) };
|
|
2693
2947
|
}
|
|
2694
2948
|
}
|