@phnx-labs/agents-cli 1.22.46 → 1.22.49

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.
Files changed (348) hide show
  1. package/CHANGELOG.md +663 -0
  2. package/LICENSE +102 -182
  3. package/README.md +96 -49
  4. package/dist/bootstrap.js +4 -2
  5. package/dist/cli/command-registry.d.ts +2 -1
  6. package/dist/cli/command-registry.js +6 -2
  7. package/dist/commands/accounts.d.ts +58 -0
  8. package/dist/commands/accounts.js +243 -25
  9. package/dist/commands/artifacts.js +7 -6
  10. package/dist/commands/attach.js +1 -1
  11. package/dist/commands/auth.js +7 -5
  12. package/dist/commands/browser-sessions-picker.js +9 -7
  13. package/dist/commands/browser.js +711 -62
  14. package/dist/commands/config.d.ts +0 -1
  15. package/dist/commands/config.js +26 -24
  16. package/dist/commands/cost.d.ts +1 -1
  17. package/dist/commands/daemon.d.ts +21 -0
  18. package/dist/commands/daemon.js +147 -12
  19. package/dist/commands/detach.d.ts +13 -0
  20. package/dist/commands/detach.js +11 -8
  21. package/dist/commands/exec.js +107 -53
  22. package/dist/commands/feed-watch.d.ts +2 -0
  23. package/dist/commands/feed-watch.js +28 -0
  24. package/dist/commands/feed.js +27 -0
  25. package/dist/commands/feedback.js +3 -19
  26. package/dist/commands/focus.d.ts +14 -0
  27. package/dist/commands/focus.js +48 -5
  28. package/dist/commands/go.d.ts +3 -0
  29. package/dist/commands/go.js +16 -3
  30. package/dist/commands/insights.d.ts +1 -1
  31. package/dist/commands/insights.js +2 -2
  32. package/dist/commands/inspect.js +2 -2
  33. package/dist/commands/lease.d.ts +1 -1
  34. package/dist/commands/lease.js +7 -6
  35. package/dist/commands/mailboxes.js +4 -3
  36. package/dist/commands/menubar.js +11 -4
  37. package/dist/commands/projects.d.ts +16 -1
  38. package/dist/commands/projects.js +58 -64
  39. package/dist/commands/resume.js +1 -1
  40. package/dist/commands/route.js +4 -4
  41. package/dist/commands/routines.js +4 -3
  42. package/dist/commands/run-broadcast.d.ts +1 -1
  43. package/dist/commands/run-broadcast.js +1 -1
  44. package/dist/commands/sessions-picker-factory.d.ts +2 -1
  45. package/dist/commands/sessions-picker-factory.js +1 -1
  46. package/dist/commands/sessions-picker.d.ts +22 -0
  47. package/dist/commands/sessions-picker.js +48 -9
  48. package/dist/commands/sessions-stop.d.ts +24 -0
  49. package/dist/commands/sessions-stop.js +66 -0
  50. package/dist/commands/sessions-trace.d.ts +82 -0
  51. package/dist/commands/sessions-trace.js +387 -0
  52. package/dist/commands/sessions.js +5 -0
  53. package/dist/commands/setup-secrets.js +23 -0
  54. package/dist/commands/share.d.ts +20 -5
  55. package/dist/commands/share.js +152 -52
  56. package/dist/commands/ssh.d.ts +15 -0
  57. package/dist/commands/ssh.js +243 -18
  58. package/dist/commands/teams.js +34 -2
  59. package/dist/commands/traces.d.ts +2 -0
  60. package/dist/commands/traces.js +184 -0
  61. package/dist/commands/view.d.ts +2 -0
  62. package/dist/commands/view.js +32 -62
  63. package/dist/commands/watchdog.js +9 -14
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js +5 -0
  66. package/dist/lib/account-capabilities.d.ts +4 -0
  67. package/dist/lib/account-capabilities.js +13 -9
  68. package/dist/lib/account-registry.d.ts +2 -0
  69. package/dist/lib/account-registry.js +30 -2
  70. package/dist/lib/accounting/rotate.d.ts +9 -0
  71. package/dist/lib/accounting/rotate.js +12 -2
  72. package/dist/lib/accounting/usage.d.ts +84 -6
  73. package/dist/lib/accounting/usage.js +293 -75
  74. package/dist/lib/agent-modes.js +29 -1
  75. package/dist/lib/agent-spec/agents.d.ts +4 -3
  76. package/dist/lib/agent-spec/agents.js +25 -15
  77. package/dist/lib/analytics/mix-commands.d.ts +1 -1
  78. package/dist/lib/analytics/mix-commands.js +2 -2
  79. package/dist/lib/auth-health.d.ts +13 -3
  80. package/dist/lib/auth-health.js +50 -8
  81. package/dist/lib/browser/devices.d.ts +19 -0
  82. package/dist/lib/browser/devices.js +23 -0
  83. package/dist/lib/browser/drivers/ssh.d.ts +16 -2
  84. package/dist/lib/browser/drivers/ssh.js +36 -9
  85. package/dist/lib/browser/ipc.d.ts +1 -1
  86. package/dist/lib/browser/ipc.js +40 -9
  87. package/dist/lib/browser/profiles.d.ts +104 -65
  88. package/dist/lib/browser/profiles.js +277 -165
  89. package/dist/lib/browser/registry.d.ts +39 -0
  90. package/dist/lib/browser/registry.js +149 -0
  91. package/dist/lib/browser/remote-control.d.ts +28 -2
  92. package/dist/lib/browser/remote-control.js +35 -3
  93. package/dist/lib/browser/resolve-target.d.ts +68 -0
  94. package/dist/lib/browser/resolve-target.js +250 -0
  95. package/dist/lib/browser/runtime-state.d.ts +44 -11
  96. package/dist/lib/browser/runtime-state.js +98 -15
  97. package/dist/lib/browser/service.d.ts +65 -9
  98. package/dist/lib/browser/service.js +196 -61
  99. package/dist/lib/browser/sessions-list.d.ts +1 -3
  100. package/dist/lib/browser/sessions-list.js +3 -16
  101. package/dist/lib/browser/task-index.d.ts +70 -0
  102. package/dist/lib/browser/task-index.js +180 -0
  103. package/dist/lib/browser/types.d.ts +12 -1
  104. package/dist/lib/byok-usage.js +2 -2
  105. package/dist/lib/claude-statusline.d.ts +29 -0
  106. package/dist/lib/claude-statusline.js +139 -0
  107. package/dist/lib/cloud/host.d.ts +1 -1
  108. package/dist/lib/cloud/host.js +1 -1
  109. package/dist/lib/cloud/rush.js +1 -1
  110. package/dist/lib/codex-policy.d.ts +11 -1
  111. package/dist/lib/codex-policy.js +20 -5
  112. package/dist/lib/computer/computer-rpc.js +2 -2
  113. package/dist/lib/computer/download.d.ts +15 -15
  114. package/dist/lib/computer/download.js +35 -98
  115. package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
  116. package/dist/lib/computer/ssh-tunnel.js +12 -22
  117. package/dist/lib/config-keys.d.ts +6 -11
  118. package/dist/lib/config-keys.js +18 -19
  119. package/dist/lib/crabbox/cli.d.ts +1 -1
  120. package/dist/lib/crabbox/cli.js +2 -2
  121. package/dist/lib/crabbox/lease.d.ts +9 -0
  122. package/dist/lib/crabbox/lease.js +70 -15
  123. package/dist/lib/crabbox/runtimes.js +1 -1
  124. package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
  125. package/dist/lib/daemon/account-state-daemon-service.js +26 -0
  126. package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
  127. package/dist/lib/daemon/browser-ipc-service.js +46 -0
  128. package/dist/lib/daemon/daemon.d.ts +3 -0
  129. package/dist/lib/daemon/daemon.js +212 -444
  130. package/dist/lib/daemon/device-probe-service.d.ts +26 -0
  131. package/dist/lib/daemon/device-probe-service.js +47 -0
  132. package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
  133. package/dist/lib/daemon/keychain-reap-service.js +32 -0
  134. package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
  135. package/dist/lib/daemon/monitor-engine-service.js +29 -0
  136. package/dist/lib/daemon/runner.js +15 -2
  137. package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
  138. package/dist/lib/daemon/secrets-broker-service.js +66 -0
  139. package/dist/lib/daemon/self-heal-service.d.ts +27 -0
  140. package/dist/lib/daemon/self-heal-service.js +48 -0
  141. package/dist/lib/daemon/service.d.ts +102 -0
  142. package/dist/lib/daemon/service.js +98 -0
  143. package/dist/lib/daemon/session-index-service.d.ts +22 -0
  144. package/dist/lib/daemon/session-index-service.js +40 -0
  145. package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
  146. package/dist/lib/daemon/state-dir-check-service.js +63 -0
  147. package/dist/lib/daemon/supervisor.d.ts +87 -0
  148. package/dist/lib/daemon/supervisor.js +298 -0
  149. package/dist/lib/daemon/watchdog-service.d.ts +19 -0
  150. package/dist/lib/daemon/watchdog-service.js +40 -0
  151. package/dist/lib/daemon-health.d.ts +15 -0
  152. package/dist/lib/daemon-health.js +29 -5
  153. package/dist/lib/daemon-services.d.ts +8 -1
  154. package/dist/lib/daemon-services.js +55 -0
  155. package/dist/lib/daemon-ticks.d.ts +13 -2
  156. package/dist/lib/daemon-ticks.js +22 -20
  157. package/dist/lib/device-config.d.ts +25 -10
  158. package/dist/lib/device-config.js +90 -39
  159. package/dist/lib/devices/config-migration.d.ts +7 -0
  160. package/dist/lib/devices/config-migration.js +67 -3
  161. package/dist/lib/devices/health.d.ts +27 -2
  162. package/dist/lib/devices/health.js +39 -9
  163. package/dist/lib/devices/interactive-host.d.ts +15 -0
  164. package/dist/lib/devices/interactive-host.js +53 -0
  165. package/dist/lib/devices/registry.d.ts +61 -4
  166. package/dist/lib/devices/registry.js +94 -38
  167. package/dist/lib/devices/stats-cache.d.ts +26 -0
  168. package/dist/lib/devices/stats-cache.js +59 -7
  169. package/dist/lib/devices/worker-pick.d.ts +48 -0
  170. package/dist/lib/devices/worker-pick.js +113 -0
  171. package/dist/lib/exec.d.ts +70 -14
  172. package/dist/lib/exec.js +110 -38
  173. package/dist/lib/feed/answer.d.ts +29 -0
  174. package/dist/lib/feed/answer.js +145 -0
  175. package/dist/lib/feed/attention.d.ts +83 -0
  176. package/dist/lib/feed/attention.js +0 -0
  177. package/dist/lib/feed/feed.d.ts +109 -0
  178. package/dist/lib/feed/feed.js +154 -3
  179. package/dist/lib/feed/pr-status.d.ts +14 -0
  180. package/dist/lib/feed/pr-status.js +47 -0
  181. package/dist/lib/feed/watch.d.ts +59 -0
  182. package/dist/lib/feed/watch.js +180 -0
  183. package/dist/lib/fleet/capture.js +6 -0
  184. package/dist/lib/fleet/remote-login.js +8 -3
  185. package/dist/lib/fleet/types.d.ts +17 -0
  186. package/dist/lib/harness/adapter.d.ts +11 -0
  187. package/dist/lib/harness/adapters/claude.js +38 -24
  188. package/dist/lib/harness/adapters/codex.js +2 -4
  189. package/dist/lib/harness/adapters/cursor.js +8 -11
  190. package/dist/lib/helper-download.d.ts +108 -0
  191. package/dist/lib/helper-download.js +194 -0
  192. package/dist/lib/helper-versions.d.ts +52 -0
  193. package/dist/lib/helper-versions.js +58 -0
  194. package/dist/lib/hooks/cache.js +19 -0
  195. package/dist/lib/hooks/install.js +2 -2
  196. package/dist/lib/hooks/match.d.ts +3 -0
  197. package/dist/lib/hooks/match.js +25 -0
  198. package/dist/lib/hosts/credentials.d.ts +1 -1
  199. package/dist/lib/hosts/credentials.js +1 -1
  200. package/dist/lib/hosts/dispatch.d.ts +7 -1
  201. package/dist/lib/hosts/dispatch.js +24 -5
  202. package/dist/lib/hosts/passthrough.d.ts +8 -15
  203. package/dist/lib/hosts/passthrough.js +35 -16
  204. package/dist/lib/hosts/providers/devices.d.ts +1 -1
  205. package/dist/lib/hosts/providers/devices.js +1 -1
  206. package/dist/lib/hosts/reconnect.d.ts +179 -21
  207. package/dist/lib/hosts/reconnect.js +266 -45
  208. package/dist/lib/hosts/registry.js +7 -0
  209. package/dist/lib/installations/migrate.d.ts +4 -5
  210. package/dist/lib/installations/migrate.js +6 -7
  211. package/dist/lib/installations/shims.d.ts +1 -1
  212. package/dist/lib/installations/shims.js +23 -13
  213. package/dist/lib/installations/versions.js +25 -2
  214. package/dist/lib/linear-autoclose.d.ts +1 -1
  215. package/dist/lib/linear-autoclose.js +1 -1
  216. package/dist/lib/loop.d.ts +2 -2
  217. package/dist/lib/loop.js +1 -1
  218. package/dist/lib/menubar/download-menubar.d.ts +47 -0
  219. package/dist/lib/menubar/download-menubar.js +60 -0
  220. package/dist/lib/menubar/install-menubar.d.ts +44 -7
  221. package/dist/lib/menubar/install-menubar.js +105 -23
  222. package/dist/lib/menubar/notify-desktop.d.ts +3 -3
  223. package/dist/lib/menubar/notify-desktop.js +6 -6
  224. package/dist/lib/model-tiers.d.ts +1 -1
  225. package/dist/lib/open-url.d.ts +60 -2
  226. package/dist/lib/open-url.js +167 -12
  227. package/dist/lib/project-probe.d.ts +1 -1
  228. package/dist/lib/project-probe.js +1 -1
  229. package/dist/lib/project-pull.d.ts +1 -1
  230. package/dist/lib/project-pull.js +2 -2
  231. package/dist/lib/projects.d.ts +1 -1
  232. package/dist/lib/projects.js +1 -1
  233. package/dist/lib/refresh-coordinator.d.ts +1 -1
  234. package/dist/lib/refresh-coordinator.js +1 -1
  235. package/dist/lib/sandbox.js +6 -7
  236. package/dist/lib/secrets/download-keychain.d.ts +47 -0
  237. package/dist/lib/secrets/download-keychain.js +70 -0
  238. package/dist/lib/secrets/install-helper.d.ts +20 -1
  239. package/dist/lib/secrets/install-helper.js +44 -3
  240. package/dist/lib/session/active.d.ts +67 -0
  241. package/dist/lib/session/active.js +86 -2
  242. package/dist/lib/session/db.d.ts +11 -1
  243. package/dist/lib/session/db.js +142 -8
  244. package/dist/lib/session/discover.d.ts +23 -1
  245. package/dist/lib/session/discover.js +44 -10
  246. package/dist/lib/session/highlights.d.ts +49 -1
  247. package/dist/lib/session/highlights.js +95 -0
  248. package/dist/lib/session/host-link.d.ts +16 -2
  249. package/dist/lib/session/host-link.js +7 -1
  250. package/dist/lib/session/parse.js +33 -1
  251. package/dist/lib/session/prompt.d.ts +35 -0
  252. package/dist/lib/session/prompt.js +56 -0
  253. package/dist/lib/session/remote/remote-list.d.ts +9 -0
  254. package/dist/lib/session/remote/remote-list.js +19 -2
  255. package/dist/lib/session/remote/watch.d.ts +1 -1
  256. package/dist/lib/session/remote/watch.js +17 -3
  257. package/dist/lib/session/render.js +7 -3
  258. package/dist/lib/session/session-cache.d.ts +53 -0
  259. package/dist/lib/session/session-cache.js +93 -1
  260. package/dist/lib/session/state.js +1 -1
  261. package/dist/lib/session/tool-calls.d.ts +2 -0
  262. package/dist/lib/session/tool-calls.js +1 -1
  263. package/dist/lib/session/trajectory-compare.d.ts +47 -0
  264. package/dist/lib/session/trajectory-compare.js +116 -0
  265. package/dist/lib/session/trajectory-html.d.ts +19 -0
  266. package/dist/lib/session/trajectory-html.js +770 -0
  267. package/dist/lib/session/trajectory-lineage.d.ts +92 -0
  268. package/dist/lib/session/trajectory-lineage.js +195 -0
  269. package/dist/lib/session/trajectory-text.d.ts +37 -0
  270. package/dist/lib/session/trajectory-text.js +284 -0
  271. package/dist/lib/session/trajectory.d.ts +102 -0
  272. package/dist/lib/session/trajectory.js +384 -0
  273. package/dist/lib/session/types.d.ts +15 -0
  274. package/dist/lib/sha256-asset.d.ts +26 -0
  275. package/dist/lib/sha256-asset.js +41 -0
  276. package/dist/lib/share/backend.d.ts +95 -0
  277. package/dist/lib/share/backend.js +139 -0
  278. package/dist/lib/share/delete.d.ts +6 -2
  279. package/dist/lib/share/delete.js +8 -9
  280. package/dist/lib/share/html.d.ts +22 -0
  281. package/dist/lib/share/html.js +88 -0
  282. package/dist/lib/share/provision.d.ts +18 -4
  283. package/dist/lib/share/provision.js +38 -4
  284. package/dist/lib/share/publish.d.ts +39 -15
  285. package/dist/lib/share/publish.js +55 -26
  286. package/dist/lib/share/worker-template.js +171 -11
  287. package/dist/lib/sqlite.js +16 -3
  288. package/dist/lib/ssh-exec.d.ts +65 -0
  289. package/dist/lib/ssh-exec.js +120 -4
  290. package/dist/lib/startup/command-registry.d.ts +3 -1
  291. package/dist/lib/startup/command-registry.js +6 -3
  292. package/dist/lib/startup/dev-build.js +3 -3
  293. package/dist/lib/state.d.ts +2 -2
  294. package/dist/lib/state.js +11 -5
  295. package/dist/lib/subagents-registry.js +1 -1
  296. package/dist/lib/teams/agents.js +9 -0
  297. package/dist/lib/terminal/index.d.ts +1 -1
  298. package/dist/lib/terminal/inject.d.ts +19 -0
  299. package/dist/lib/terminal/inject.js +19 -9
  300. package/dist/lib/terminal/types.d.ts +1 -1
  301. package/dist/lib/terminal/types.js +1 -1
  302. package/dist/lib/tmux/orphan-reap.js +1 -1
  303. package/dist/lib/tmux/session.d.ts +46 -13
  304. package/dist/lib/tmux/session.js +204 -27
  305. package/dist/lib/traces/backend.d.ts +29 -0
  306. package/dist/lib/traces/backend.js +46 -0
  307. package/dist/lib/traces/classify.d.ts +52 -0
  308. package/dist/lib/traces/classify.js +72 -0
  309. package/dist/lib/traces/config.d.ts +10 -0
  310. package/dist/lib/traces/config.js +2 -0
  311. package/dist/lib/traces/provision.d.ts +19 -0
  312. package/dist/lib/traces/provision.js +30 -0
  313. package/dist/lib/traces/sync.d.ts +150 -0
  314. package/dist/lib/traces/sync.js +447 -0
  315. package/dist/lib/traces/worker-template.d.ts +4 -0
  316. package/dist/lib/traces/worker-template.js +159 -0
  317. package/dist/lib/types.d.ts +34 -19
  318. package/dist/lib/types.js +14 -0
  319. package/dist/lib/usage-backoff.d.ts +2 -1
  320. package/dist/lib/usage-backoff.js +60 -19
  321. package/dist/lib/usage-refresh.d.ts +18 -4
  322. package/dist/lib/usage-refresh.js +68 -19
  323. package/dist/lib/watchdog/log.d.ts +1 -1
  324. package/dist/lib/watchdog/log.js +1 -1
  325. package/dist/lib/watchdog/runner.d.ts +36 -45
  326. package/dist/lib/watchdog/runner.js +150 -119
  327. package/dist/lib/watchdog/service.d.ts +0 -1
  328. package/dist/lib/watchdog/service.js +0 -1
  329. package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
  330. package/dist/lib/watchdog/watchdog-agent.js +59 -0
  331. package/dist/lib/watchdog/watchdog.d.ts +11 -2
  332. package/dist/lib/watchdog/watchdog.js +37 -104
  333. package/dist/lib/workflows.d.ts +1 -1
  334. package/package.json +5 -7
  335. package/dist/commands/usage.d.ts +0 -27
  336. package/dist/commands/usage.js +0 -96
  337. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  338. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
  339. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  340. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  341. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
  342. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  343. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -24
  344. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  345. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -123
  346. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  347. package/dist/lib/usage-fleet.d.ts +0 -32
  348. package/dist/lib/usage-fleet.js +0 -125
@@ -26,6 +26,7 @@ import { getCacheDir } from '../state.js';
26
26
  import { mapBounded } from '../concurrency.js';
27
27
  import { atomicWriteFileSync, ensureLockTarget, withFileLock } from '../fs-atomic.js';
28
28
  import { withRefreshLease } from '../refresh-coordinator.js';
29
+ import { padToWidth } from '../session/width.js';
29
30
  const execFileAsync = promisify(execFile);
30
31
  const CLAUDE_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
31
32
  const CLAUDE_TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
@@ -118,6 +119,57 @@ export function usageUnreachableError(agent, cause) {
118
119
  ? `${agent} usage read failed: ${detail}`
119
120
  : `${agent} usage read failed.`;
120
121
  }
122
+ /**
123
+ * Marker for a log-based (`network: false`) provider — Codex, Grok — that has
124
+ * simply never recorded a rate-limit event on this machine yet: no session
125
+ * log exists, or no session in it carries usage data. Distinct on purpose from
126
+ * `usageUnreachableError`: that one means the local log COULDN'T be read (a
127
+ * real failure worth surfacing distinctly); this one means there is nothing to
128
+ * read because the account has not run here, which is expected for a fresh
129
+ * install and should render as a benign state, not an error (RUSH-3040).
130
+ */
131
+ export const USAGE_NO_RECENT_USAGE_MARKER = 'no usage recorded yet';
132
+ export const USAGE_BENIGN_STATE = Symbol('usageBenignState');
133
+ /**
134
+ * Shared error-classification + 429 backoff for a networked usage fetch whose
135
+ * only signal is an HTTP status (or none at all, on a network failure) —
136
+ * Antigravity's :retrieveUserQuota and Muse's Meta Model API probe are both
137
+ * this shape. They were added to `USAGE_SOURCES` after the four original
138
+ * `usageXError` constructors and did not get their own scheme (usage.ts's
139
+ * error handling was written for "four networked providers"; RUSH-3040).
140
+ * Route every no-snapshot outcome for either through this one function so a
141
+ * future entry cannot be added second-class again — it owns noting the 429
142
+ * backoff, so callers must NOT also call {@link noteUsageRateLimited} for the
143
+ * same response.
144
+ */
145
+ export function classifyUsageFetchFailure(agent, agentId, status, retryAfterHeader, usageScope) {
146
+ if (status === 429) {
147
+ noteUsageRateLimited(agentId, retryAfterHeader ?? null, { account: usageScope });
148
+ return usageRejectedError(agent, 429);
149
+ }
150
+ if (status !== null)
151
+ return usageRejectedError(agent, status);
152
+ return usageUnreachableError(agent);
153
+ }
154
+ /** Classify a `UsageInfo.error` string into its {@link UsageErrorKind}, or null when there is no error. */
155
+ export function classifyUsageErrorKind(error) {
156
+ if (!error)
157
+ return null;
158
+ if (isUsageHeadlessScopeError(error))
159
+ return 'headless-scope';
160
+ if (error.startsWith('No readable '))
161
+ return 'no-credential';
162
+ if (error.includes('credential expired'))
163
+ return 'expired-credential';
164
+ if (error.includes('rate-limited this machine') || error.includes('is rate-limiting the usage endpoint')) {
165
+ return 'rate-limited';
166
+ }
167
+ if (error.includes('rejected the usage read'))
168
+ return 'rejected';
169
+ if (error.includes('usage read failed'))
170
+ return 'unreachable';
171
+ return 'rejected';
172
+ }
121
173
  /**
122
174
  * True when a Claude OAuth access token is within the refresh leeway of expiry
123
175
  * (or already expired) — i.e. it "would need a refresh" before the next use.
@@ -165,6 +217,15 @@ const COMPACT_BAR_LEN = 5;
165
217
  const USAGE_BAR_LEN = 10;
166
218
  const FULL = '\u2588';
167
219
  const EMPTY = '\u2591';
220
+ const PARTIAL_BLOCKS = ['', '\u258F', '\u258E', '\u258D', '\u258C', '\u258B', '\u258A', '\u2589'];
221
+ /** Construct the benign no-local-log result without overloading `error`. */
222
+ export function usageNoRecentUsageInfo() {
223
+ return { snapshot: null, error: null, [USAGE_BENIGN_STATE]: 'no-recent-usage' };
224
+ }
225
+ /** Read a benign state for the human renderer; symbols are omitted by JSON serialization. */
226
+ export function getUsageBenignState(info) {
227
+ return info[USAGE_BENIGN_STATE] ?? null;
228
+ }
168
229
  /** The single registry of agent usage sources and their transport. */
169
230
  const USAGE_SOURCES = {
170
231
  claude: { fetch: getClaudeUsageInfo, network: true },
@@ -333,6 +394,9 @@ async function fetchLiveUsageDeduped(input, usageKey, cached, fileOnly) {
333
394
  home: input.home,
334
395
  cliVersion: input.cliVersion,
335
396
  organizationId: input.info.organizationId,
397
+ // Scope this fetch's 429 backoff to the account being fetched, so one
398
+ // throttled account cannot park the whole provider (RUSH-3036).
399
+ usageScope: usageKey,
336
400
  fileOnly,
337
401
  });
338
402
  if (usage.snapshot) {
@@ -391,6 +455,26 @@ export function pickCompactUsageWindows(windows, maxWindows) {
391
455
  }
392
456
  return chosen;
393
457
  }
458
+ /** Human label for a classified usage error, for the no-bars branch of {@link formatUsageSummary}. */
459
+ function formatUsageErrorKindLabel(kind, detail) {
460
+ switch (kind) {
461
+ case 'no-credential':
462
+ return 'sign in / provision token';
463
+ case 'expired-credential':
464
+ return 're-auth for usage';
465
+ case 'rate-limited': {
466
+ const retryHint = detail?.match(/not retrying for (.+)\.$/)?.[1] ?? null;
467
+ return retryHint ? `rate-limited (retry ~${retryHint})` : 'rate-limited';
468
+ }
469
+ case 'rejected':
470
+ case 'unreachable':
471
+ case 'headless-scope':
472
+ case null:
473
+ case undefined:
474
+ default:
475
+ return 'usage unavailable';
476
+ }
477
+ }
394
478
  /** Format a one-line usage summary with compact bars for inline display. */
395
479
  export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
396
480
  const parts = [];
@@ -416,11 +500,20 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
416
500
  // multi-meter agent cannot force the whole table to wrap.
417
501
  const selected = pickCompactUsageWindows(snapshot.windows, opts?.maxWindows);
418
502
  const hidden = Math.max(0, snapshot.windows.filter((w) => w.key !== 'sonnet_week').length - selected.length);
419
- const windowParts = selected.map((window) => {
503
+ const expected = opts?.expectedWindows;
504
+ const windowsToRender = expected
505
+ ? expected.map(({ key, shortLabel }) => ({ window: selected.find((item) => item.key === key), shortLabel }))
506
+ : selected.map((window) => ({ window, shortLabel: window.shortLabel }));
507
+ const windowParts = windowsToRender.map(({ window, shortLabel }, index) => {
508
+ if (!window) {
509
+ const missing = chalk.red(`${shortLabel}: ${FULL.repeat(COMPACT_BAR_LEN)} unavailable`);
510
+ return index < windowsToRender.length - 1 ? padToWidth(missing, 20) : missing;
511
+ }
420
512
  const bar = renderCompactUsageBar(window.usedPercent);
421
513
  const pct = colorUsage(`${Math.round(window.usedPercent)}%`, window.usedPercent);
422
514
  const reset = window.resetsAt ? chalk.dim(` (${formatResetHint(window.resetsAt)})`) : '';
423
- return `${chalk.gray(`${window.shortLabel}:`)} ${bar} ${pct}${reset}`;
515
+ const rendered = `${chalk.gray(`${shortLabel}:`)} ${bar} ${pct}${reset}`;
516
+ return index < windowsToRender.length - 1 ? padToWidth(rendered, 20) : rendered;
424
517
  });
425
518
  if (hidden > 0) {
426
519
  windowParts.push(chalk.dim(`+${hidden}`));
@@ -428,6 +521,9 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
428
521
  if (windowParts.length > 0) {
429
522
  parts.push(windowParts.join(' '));
430
523
  }
524
+ else if (snapshot.refreshHint) {
525
+ parts.push(chalk.dim(snapshot.refreshHint));
526
+ }
431
527
  // The bars came from the cache and the live read that should have confirmed
432
528
  // them failed, so they are the last thing we saw — not the current state.
433
529
  // Drawing them unmarked is what let a 26h-old "48% used" read as fact.
@@ -445,11 +541,16 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
445
541
  // mistaken for a missing setup-token or seeding failure (RUSH-2392).
446
542
  parts.push(chalk.dim(USAGE_HEADLESS_SCOPE_MARKER));
447
543
  }
544
+ else if (opts?.benignState === 'no-recent-usage') {
545
+ parts.push(chalk.dim(opts.noRecentUsageLabel || USAGE_NO_RECENT_USAGE_MARKER));
546
+ }
448
547
  else if (opts?.unavailable) {
449
548
  // Signed-in account we could NOT fetch usage for (no live token in a reachable
450
549
  // home / org mismatch / fetch error). Say so explicitly instead of drawing a
451
- // blank gauge that reads like "0% used".
452
- parts.push(chalk.dim('usage unavailable'));
550
+ // blank gauge that reads like "0% used" — and name the SPECIFIC cause when
551
+ // the caller passed one, rather than the generic bucket that used to cover
552
+ // ~6 different failures (RUSH-3040).
553
+ parts.push(chalk.dim(formatUsageErrorKindLabel(opts.errorKind, opts.errorDetail)));
453
554
  }
454
555
  return parts.join(' ');
455
556
  }
@@ -598,11 +699,17 @@ async function getCodexUsageInfo(options) {
598
699
  return { snapshot: null, error: null };
599
700
  }
600
701
  const files = collectCodexSessionFiles(options?.home, sinceMs);
702
+ const now = new Date();
601
703
  for (const filePath of files) {
602
704
  const match = await readLatestCodexRateLimits(filePath);
603
705
  if (!match)
604
706
  continue;
605
- const windows = normalizeCodexWindows(match.rateLimits);
707
+ // Same freshness filter Grok already applies (RUSH-3040): a window whose
708
+ // reset time or windowMinutes-derived expiry has passed is a STALE read,
709
+ // not a current one — rendering it as-is is how a codex bar kept showing
710
+ // "100% used" past its own reset. Try the next-older session file rather
711
+ // than surfacing a stale bar.
712
+ const windows = normalizeCodexWindows(match.rateLimits).filter((window) => isCachedUsageWindowFresh(window, match.capturedAt, now));
606
713
  if (windows.length === 0)
607
714
  continue;
608
715
  return {
@@ -615,10 +722,14 @@ async function getCodexUsageInfo(options) {
615
722
  error: null,
616
723
  };
617
724
  }
618
- return { snapshot: null, error: null };
725
+ // No session ever recorded a rate-limit event on this machine (or none of
726
+ // the ones found were still fresh) — a benign "nothing to show yet", not a
727
+ // failure (RUSH-3040). Distinct from the outer catch below, which is a
728
+ // genuine read/parse failure.
729
+ return usageNoRecentUsageInfo();
619
730
  }
620
- catch {
621
- return { snapshot: null, error: null };
731
+ catch (err) {
732
+ return { snapshot: null, error: usageUnreachableError('Codex', err) };
622
733
  }
623
734
  }
624
735
  /**
@@ -671,7 +782,7 @@ async function getClaudeUsageInfo(options) {
671
782
  }
672
783
  // Honour a live Retry-After rather than re-arming the penalty (see
673
784
  // usage-backoff.ts). No request at all while the window is open.
674
- const throttledUntil = usageRateLimitedUntil('claude');
785
+ const throttledUntil = usageRateLimitedUntil('claude', Date.now(), options?.usageScope);
675
786
  if (throttledUntil) {
676
787
  return { snapshot: null, error: usageThrottledError('Claude', throttledUntil) };
677
788
  }
@@ -687,7 +798,7 @@ async function getClaudeUsageInfo(options) {
687
798
  });
688
799
  if (!response.ok) {
689
800
  if (response.status === 429) {
690
- noteUsageRateLimited('claude', response.headers.get('retry-after'));
801
+ noteUsageRateLimited('claude', response.headers.get('retry-after'), { account: options?.usageScope });
691
802
  }
692
803
  // Setup-token is user:inference only; usage needs user:profile → 403
693
804
  // with a scope-requirement body. Distinct from a real rejection so the
@@ -780,7 +891,7 @@ async function getKimiUsageInfo(options) {
780
891
  }
781
892
  // Honour a live Retry-After rather than re-arming the penalty (see
782
893
  // usage-backoff.ts). No request at all while the window is open.
783
- const throttledUntil = usageRateLimitedUntil('kimi');
894
+ const throttledUntil = usageRateLimitedUntil('kimi', Date.now(), options?.usageScope);
784
895
  if (throttledUntil) {
785
896
  return { snapshot: null, error: usageThrottledError('Kimi', throttledUntil) };
786
897
  }
@@ -796,7 +907,7 @@ async function getKimiUsageInfo(options) {
796
907
  // way there are no bars to draw, and the status is what tells them apart.
797
908
  if (!response.ok) {
798
909
  if (response.status === 429) {
799
- noteUsageRateLimited('kimi', response.headers.get('retry-after'));
910
+ noteUsageRateLimited('kimi', response.headers.get('retry-after'), { account: options?.usageScope });
800
911
  }
801
912
  return { snapshot: null, error: usageRejectedError('Kimi', response.status) };
802
913
  }
@@ -914,7 +1025,7 @@ async function getDroidUsageInfo(options) {
914
1025
  }
915
1026
  // Honour a live Retry-After rather than re-arming the penalty (see
916
1027
  // usage-backoff.ts). No request at all while the window is open.
917
- const throttledUntil = usageRateLimitedUntil('droid');
1028
+ const throttledUntil = usageRateLimitedUntil('droid', Date.now(), options?.usageScope);
918
1029
  if (throttledUntil) {
919
1030
  return { snapshot: null, error: usageThrottledError('Droid', throttledUntil) };
920
1031
  }
@@ -929,7 +1040,7 @@ async function getDroidUsageInfo(options) {
929
1040
  // 401 => revoked/expired token. No bars to draw, and the status says why.
930
1041
  if (!response.ok) {
931
1042
  if (response.status === 429) {
932
- noteUsageRateLimited('droid', response.headers.get('retry-after'));
1043
+ noteUsageRateLimited('droid', response.headers.get('retry-after'), { account: options?.usageScope });
933
1044
  }
934
1045
  return { snapshot: null, error: usageRejectedError('Droid', response.status) };
935
1046
  }
@@ -956,7 +1067,7 @@ async function getDroidUsageInfo(options) {
956
1067
  }
957
1068
  }
958
1069
  /** Probe Claude's OAuth token against the usage endpoint. Never refreshes — reports `expired` for a near-expiry token; see the comment below (RUSH-1822). */
959
- export async function probeClaudeStatus(home, cliVersion) {
1070
+ export async function probeClaudeStatus(home, cliVersion, usageScope) {
960
1071
  // accessTokenCache: the daemon warms this probe every ~3 min per account, so it
961
1072
  // reads ONLY the file-based setup-token and never the interactive login —
962
1073
  // transmitting that ACL-bound token to the usage API from a background loop is
@@ -981,7 +1092,7 @@ export async function probeClaudeStatus(home, cliVersion) {
981
1092
  // window is open, report the throttle from the recorded state instead of
982
1093
  // firing again and re-arming it (usage-backoff.ts). This 3-min-cadence
983
1094
  // probe is what created the loop it now respects.
984
- if (usageRateLimitedUntil('claude'))
1095
+ if (usageRateLimitedUntil('claude', Date.now(), usageScope))
985
1096
  return { status: 429, token: 'present' };
986
1097
  try {
987
1098
  const response = await fetch(CLAUDE_USAGE_URL, {
@@ -995,7 +1106,7 @@ export async function probeClaudeStatus(home, cliVersion) {
995
1106
  signal: AbortSignal.timeout(8000),
996
1107
  });
997
1108
  if (response.status === 429) {
998
- noteUsageRateLimited('claude', response.headers.get('retry-after'));
1109
+ noteUsageRateLimited('claude', response.headers.get('retry-after'), { account: usageScope });
999
1110
  }
1000
1111
  // Setup-token is user:inference only; usage needs user:profile → 403.
1001
1112
  // That is NOT a revocation — the account still runs (RUSH-2392).
@@ -1023,7 +1134,7 @@ export async function probeClaudeStatus(home, cliVersion) {
1023
1134
  }
1024
1135
  }
1025
1136
  /** Probe Kimi's OAuth token against the /usages endpoint. Never refreshes (single-use rotation — see getKimiUsageInfo). */
1026
- export async function probeKimiStatus(home) {
1137
+ export async function probeKimiStatus(home, usageScope) {
1027
1138
  const credPath = resolveKimiCredentialPath(home);
1028
1139
  if (!credPath)
1029
1140
  return { status: null, token: 'missing' };
@@ -1047,7 +1158,7 @@ export async function probeKimiStatus(home) {
1047
1158
  // is what created the loop it now respects. It sits AFTER the local
1048
1159
  // missing/expired checks — as in probeClaudeStatus and probeDroidStatus — so
1049
1160
  // a genuinely broken credential is never misreported as merely throttled.
1050
- if (usageRateLimitedUntil('kimi'))
1161
+ if (usageRateLimitedUntil('kimi', Date.now(), usageScope))
1051
1162
  return { status: 429, token: 'present' };
1052
1163
  try {
1053
1164
  const response = await fetch(KIMI_USAGES_URL, {
@@ -1056,7 +1167,7 @@ export async function probeKimiStatus(home) {
1056
1167
  signal: AbortSignal.timeout(8000),
1057
1168
  });
1058
1169
  if (response.status === 429) {
1059
- noteUsageRateLimited('kimi', response.headers.get('retry-after'));
1170
+ noteUsageRateLimited('kimi', response.headers.get('retry-after'), { account: usageScope });
1060
1171
  }
1061
1172
  return { status: response.status, token: 'present' };
1062
1173
  }
@@ -1065,7 +1176,7 @@ export async function probeKimiStatus(home) {
1065
1176
  }
1066
1177
  }
1067
1178
  /** Probe Droid's WorkOS token against the billing-limits endpoint. Never refreshes (single-use rotation — see getDroidUsageInfo). */
1068
- export async function probeDroidStatus(home) {
1179
+ export async function probeDroidStatus(home, usageScope) {
1069
1180
  const cred = decryptDroidAuthPayload(home || os.homedir());
1070
1181
  const accessToken = cred?.access_token;
1071
1182
  if (typeof accessToken !== 'string' || !accessToken)
@@ -1077,7 +1188,7 @@ export async function probeDroidStatus(home) {
1077
1188
  // window is open, report the throttle from the recorded state instead of
1078
1189
  // firing again and re-arming it (usage-backoff.ts). This 3-min-cadence
1079
1190
  // probe is what created the loop it now respects.
1080
- if (usageRateLimitedUntil('droid'))
1191
+ if (usageRateLimitedUntil('droid', Date.now(), usageScope))
1081
1192
  return { status: 429, token: 'present' };
1082
1193
  try {
1083
1194
  const response = await fetch(DROID_USAGE_URL, {
@@ -1086,7 +1197,7 @@ export async function probeDroidStatus(home) {
1086
1197
  signal: AbortSignal.timeout(8000),
1087
1198
  });
1088
1199
  if (response.status === 429) {
1089
- noteUsageRateLimited('droid', response.headers.get('retry-after'));
1200
+ noteUsageRateLimited('droid', response.headers.get('retry-after'), { account: usageScope });
1090
1201
  }
1091
1202
  return { status: response.status, token: 'present' };
1092
1203
  }
@@ -1313,7 +1424,7 @@ function deleteCachedClaudeOauth(service) {
1313
1424
  * a file-based setup-token and, when none is provisioned, gets `null` — it never
1314
1425
  * reads Claude Code's interactive login (transmitting that ACL-bound OAuth token
1315
1426
  * to Anthropic's API is what gets it revoked; see the branch body and
1316
- * docs/credential-management.md). It is OFF by default so full-credential
1427
+ * docs/secrets.md). It is OFF by default so full-credential
1317
1428
  * callers that refresh (`isClaudeAuthValid` -> `getClaudeAccessToken`) still
1318
1429
  * read the interactive login. Rush Cloud dispatch does not call this helper
1319
1430
  * at all (SING-1b: the account manifest is email-only; RUSH-2359 removed the
@@ -1346,7 +1457,7 @@ export async function loadClaudeOauth(home, opts) {
1346
1457
  // interactive credential used programmatically, which Anthropic flags and
1347
1458
  // revokes (the fleet-wide-logout class, RUSH-1822), and which violates the
1348
1459
  // invariant that the interactive/rotating login is untouchable
1349
- // (docs/credential-management.md). Report unprovisioned (-> probe
1460
+ // (docs/secrets.md). Report unprovisioned (-> probe
1350
1461
  // token 'missing' -> auth-health 'unconfigured', benign for rotation); seed a
1351
1462
  // setup-token via the mint-auth path to restore usage/probe for the account.
1352
1463
  return null;
@@ -1507,6 +1618,32 @@ export function writeClaudeUsageCache(usageKey, snapshot, cachePath = getClaudeU
1507
1618
  /* best-effort cache write — lock busy or disk full */
1508
1619
  }
1509
1620
  }
1621
+ /** Atomically merge partial native Claude windows into the current fresh row. */
1622
+ export function mergeClaudeUsageCacheWindows(usageKey, snapshot, cachePath = getClaudeUsageCachePath()) {
1623
+ try {
1624
+ ensureLockTarget(cachePath, '{}');
1625
+ withFileLock(cachePath, () => {
1626
+ const cache = readClaudeUsageCacheFile(cachePath);
1627
+ const prior = cache[usageKey];
1628
+ const priorSnapshot = prior
1629
+ ? deserializeClaudeUsageSnapshot(prior, snapshot.capturedAt ?? new Date())
1630
+ : null;
1631
+ const windows = new Map(priorSnapshot?.windows.map((window) => [window.key, window]) ?? []);
1632
+ for (const window of snapshot.windows)
1633
+ windows.set(window.key, window);
1634
+ cache[usageKey] = serializeClaudeUsageSnapshot({
1635
+ ...snapshot,
1636
+ windows: [...windows.values()],
1637
+ plan: snapshot.plan ?? priorSnapshot?.plan ?? null,
1638
+ unavailable: carryForwardUnavailable(prior?.unavailable, snapshot.unavailable),
1639
+ });
1640
+ atomicWriteFileSync(cachePath, JSON.stringify(cache, null, 2), 'utf-8');
1641
+ });
1642
+ }
1643
+ catch {
1644
+ /* best-effort cache write — lock busy or disk full */
1645
+ }
1646
+ }
1510
1647
  /** Read the entire usage cache file from disk. */
1511
1648
  function readClaudeUsageCacheFile(cachePath) {
1512
1649
  if (!fs.existsSync(cachePath)) {
@@ -1535,6 +1672,7 @@ function serializeClaudeUsageSnapshot(snapshot) {
1535
1672
  return {
1536
1673
  capturedAt: snapshot.capturedAt?.toISOString() || null,
1537
1674
  plan: snapshot.plan ?? null,
1675
+ refreshHint: snapshot.refreshHint ?? null,
1538
1676
  unavailable: snapshot.unavailable
1539
1677
  ? {
1540
1678
  reason: snapshot.unavailable.reason,
@@ -1561,6 +1699,10 @@ function serializeClaudeUsageSnapshot(snapshot) {
1561
1699
  * candidate (RUSH-2858). Dropping mirrors the Grok collector, and an all-expired
1562
1700
  * snapshot deserializes to null so `readClaudeUsageCache` deletes the entry and
1563
1701
  * callers surface "usage unavailable" plus the recorded throttle reason.
1702
+ *
1703
+ * A row that carries a plan survives even with no fresh windows: the plan is a
1704
+ * truthful reading in its own right, and losing it is what made the cached view
1705
+ * contradict the refreshed one for meterless harnesses. See the guard below.
1564
1706
  */
1565
1707
  function deserializeClaudeUsageSnapshot(snapshot, now) {
1566
1708
  const capturedAt = parseDateValue(snapshot.capturedAt);
@@ -1575,7 +1717,17 @@ function deserializeClaudeUsageSnapshot(snapshot, now) {
1575
1717
  }))
1576
1718
  .filter((window) => isCachedUsageWindowFresh(window, capturedAt, now));
1577
1719
  const unavailable = deserializeUnavailable(snapshot.unavailable, now);
1578
- if (windows.length === 0 && !unavailable) {
1720
+ // A windowless row is not automatically worthless. Grok's collector reports
1721
+ // the subscription tier and no meters at all, so treating "no fresh windows"
1722
+ // as "nothing cached" deleted the only truthful thing we knew about the
1723
+ // account: `--refresh` wrote {plan: 'SuperGrok Heavy', windows: []}, the very
1724
+ // next plain `agents view` deserialized it to null, `readClaudeUsageCache`
1725
+ // pruned the row, and the row rendered "usage unavailable" one read after a
1726
+ // successful refresh. Keep a plan-bearing row — it renders as the plan alone,
1727
+ // and `deriveUsageStatusFromSnapshot` still returns null for zero windows, so
1728
+ // it can never read as a 0% bar or an "available" badge (the RUSH-2858
1729
+ // property that made expired windows drop in the first place).
1730
+ if (windows.length === 0 && !unavailable && !snapshot.plan && !snapshot.refreshHint) {
1579
1731
  return null;
1580
1732
  }
1581
1733
  return {
@@ -1584,6 +1736,7 @@ function deserializeClaudeUsageSnapshot(snapshot, now) {
1584
1736
  capturedAt,
1585
1737
  windows,
1586
1738
  plan: snapshot.plan ?? null,
1739
+ refreshHint: snapshot.refreshHint ?? null,
1587
1740
  unavailable,
1588
1741
  };
1589
1742
  }
@@ -1862,14 +2015,17 @@ function renderUsageBar(usedPercent) {
1862
2015
  }
1863
2016
  /** Render a compact usage bar for inline summaries. */
1864
2017
  function renderCompactUsageBar(usedPercent) {
1865
- return renderBar(usedPercent, COMPACT_BAR_LEN, usedPercent > 0 ? 1 : 0);
1866
- }
1867
- /** Render a colored block-character progress bar. */
1868
- export function renderBar(usedPercent, length, minimumVisible = 0) {
1869
- const rounded = Math.round((usedPercent / 100) * length);
1870
- const filled = Math.max(minimumVisible, Math.max(0, Math.min(length, rounded)));
2018
+ return renderBar(usedPercent, COMPACT_BAR_LEN);
2019
+ }
2020
+ /** Render a proportional colored progress bar at one-eighth-cell resolution. */
2021
+ export function renderBar(usedPercent, length) {
2022
+ const clamped = Math.max(0, Math.min(100, usedPercent));
2023
+ const eighths = Math.round((clamped / 100) * length * 8);
2024
+ const filled = Math.floor(eighths / 8);
2025
+ const partial = eighths % 8;
1871
2026
  const color = getUsageColor(usedPercent);
1872
- return color(FULL.repeat(filled)) + chalk.dim(EMPTY.repeat(length - filled));
2027
+ const gauge = FULL.repeat(filled) + PARTIAL_BLOCKS[partial];
2028
+ return color(gauge) + chalk.dim(EMPTY.repeat(length - filled - (partial > 0 ? 1 : 0)));
1873
2029
  }
1874
2030
  /** Apply the appropriate color to a text string based on usage percentage. */
1875
2031
  function colorUsage(text, usedPercent) {
@@ -1955,11 +2111,12 @@ async function getGrokUsageInfo(options) {
1955
2111
  try {
1956
2112
  const base = options?.home || os.homedir();
1957
2113
  const logPath = path.join(base, '.grok', 'logs', 'unified.jsonl');
2114
+ // No log yet: a benign "nothing recorded here", not a failure (RUSH-3040).
1958
2115
  if (!fs.existsSync(logPath))
1959
- return { snapshot: null, error: null };
2116
+ return usageNoRecentUsageInfo();
1960
2117
  const match = await readLatestGrokBilling(logPath);
1961
2118
  if (!match)
1962
- return { snapshot: null, error: null };
2119
+ return usageNoRecentUsageInfo();
1963
2120
  // Grok has no live usage API (`network: false`) — bars are last-seen from
1964
2121
  // this machine's unified.jsonl only. Drop windows whose billing period has
1965
2122
  // already ended so a stale 100% does not paint "rate-limited" after reset,
@@ -1968,6 +2125,10 @@ async function getGrokUsageInfo(options) {
1968
2125
  // (see readLatestGrokBilling).
1969
2126
  const now = new Date();
1970
2127
  const windows = match.windows.filter((window) => isCachedUsageWindowFresh(window, match.capturedAt, now));
2128
+ const version = options?.cliVersion;
2129
+ const refreshHint = windows.length === 0
2130
+ ? `run grok${version ? `@${version}` : ''} once to refresh usage`
2131
+ : null;
1971
2132
  return {
1972
2133
  snapshot: {
1973
2134
  source: 'last_seen',
@@ -1975,12 +2136,13 @@ async function getGrokUsageInfo(options) {
1975
2136
  capturedAt: match.capturedAt,
1976
2137
  windows,
1977
2138
  plan: match.subscriptionTier,
2139
+ refreshHint,
1978
2140
  },
1979
2141
  error: null,
1980
2142
  };
1981
2143
  }
1982
- catch {
1983
- return { snapshot: null, error: null };
2144
+ catch (err) {
2145
+ return { snapshot: null, error: usageUnreachableError('Grok', err) };
1984
2146
  }
1985
2147
  }
1986
2148
  /**
@@ -1994,16 +2156,40 @@ async function getGrokUsageInfo(options) {
1994
2156
  async function getMuseUsageInfo(options) {
1995
2157
  try {
1996
2158
  const base = options?.home || os.homedir();
1997
- const live = await probeMuseRateLimits(base);
1998
- if (live)
1999
- return { snapshot: live, error: null };
2159
+ // Honour a live Retry-After rather than re-arming the penalty (see
2160
+ // usage-backoff.ts). The local log fallback still works while the live
2161
+ // probe is throttled only report the throttle when there is truly
2162
+ // nothing else to show.
2163
+ const throttledUntil = usageRateLimitedUntil('muse', Date.now(), options?.usageScope);
2164
+ if (throttledUntil) {
2165
+ const local = await readMuseLocalSessionUsage(base);
2166
+ if (local)
2167
+ return { snapshot: local, error: null };
2168
+ return { snapshot: null, error: usageThrottledError('Muse', throttledUntil) };
2169
+ }
2170
+ const probe = await probeMuseRateLimits(base);
2171
+ if (probe.snapshot)
2172
+ return { snapshot: probe.snapshot, error: null };
2000
2173
  const local = await readMuseLocalSessionUsage(base);
2001
- if (!local)
2002
- return { snapshot: null, error: null };
2003
- return { snapshot: local, error: null };
2174
+ if (local)
2175
+ return { snapshot: local, error: null };
2176
+ // No live snapshot and no local log every source came up empty. Only
2177
+ // now does the probe's own outcome become the reported error, mirroring
2178
+ // Cursor's "surface the last resort's failure" pattern above.
2179
+ if (!probe.hasKey)
2180
+ return { snapshot: null, error: usageNoCredentialError('Muse') };
2181
+ if (probe.noHeaders)
2182
+ return usageNoRecentUsageInfo();
2183
+ return {
2184
+ snapshot: null,
2185
+ error: classifyUsageFetchFailure('Muse', 'muse', probe.status, probe.retryAfter, options?.usageScope),
2186
+ };
2004
2187
  }
2005
- catch {
2006
- return { snapshot: null, error: null };
2188
+ catch (err) {
2189
+ // A thrown request (timeout, DNS, TLS, a malformed payload) is a failed
2190
+ // read like any other — staying silent here would hand the caller a stale
2191
+ // snapshot to render as confirmed (RUSH-3040).
2192
+ return { snapshot: null, error: usageUnreachableError('Muse', err) };
2007
2193
  }
2008
2194
  }
2009
2195
  /** Resolve a Muse API key from env or auth.json without logging the value. */
@@ -2037,25 +2223,28 @@ function resolveMuseApiKey(base) {
2037
2223
  }
2038
2224
  /**
2039
2225
  * Probe Meta Model API for rate-limit headers. Uses GET /v1/models (no token
2040
- * spend). Returns null when unauthenticated or the headers are absent.
2226
+ * spend). The 429 backoff is noted by the CALLER via
2227
+ * {@link classifyUsageFetchFailure}, not here, so a throttled read is recorded
2228
+ * exactly once regardless of which branch of `getMuseUsageInfo` observes it.
2041
2229
  */
2042
2230
  async function probeMuseRateLimits(base) {
2043
- if (usageRateLimitedUntil('muse'))
2044
- return null;
2045
2231
  const key = resolveMuseApiKey(base);
2046
2232
  if (!key)
2047
- return null;
2233
+ return { snapshot: null, hasKey: false, status: null, retryAfter: null, noHeaders: false };
2048
2234
  try {
2049
2235
  const response = await fetch('https://api.meta.ai/v1/models', {
2050
2236
  headers: { Authorization: `Bearer ${key}` },
2051
2237
  signal: AbortSignal.timeout(8_000),
2052
2238
  });
2053
- if (response.status === 429) {
2054
- noteUsageRateLimited('muse', response.headers.get('retry-after'));
2055
- return null;
2239
+ if (!response.ok) {
2240
+ return {
2241
+ snapshot: null,
2242
+ hasKey: true,
2243
+ status: response.status,
2244
+ retryAfter: response.headers.get('retry-after'),
2245
+ noHeaders: false,
2246
+ };
2056
2247
  }
2057
- if (!response.ok)
2058
- return null;
2059
2248
  const limitTokens = headerNumber(response.headers, 'x-ratelimit-limit-tokens');
2060
2249
  const remainingTokens = headerNumber(response.headers, 'x-ratelimit-remaining-tokens');
2061
2250
  const limitRequests = headerNumber(response.headers, 'x-ratelimit-limit-requests');
@@ -2083,18 +2272,25 @@ async function probeMuseRateLimits(base) {
2083
2272
  windowMinutes: 1,
2084
2273
  });
2085
2274
  }
2086
- if (windows.length === 0)
2087
- return null;
2275
+ if (windows.length === 0) {
2276
+ return { snapshot: null, hasKey: true, status: response.status, retryAfter: null, noHeaders: true };
2277
+ }
2088
2278
  return {
2089
- source: 'live',
2090
- sourceLabel: 'Meta Model API rate limits',
2091
- capturedAt: new Date(),
2092
- windows,
2093
- plan: 'Meta Model API',
2279
+ snapshot: {
2280
+ source: 'live',
2281
+ sourceLabel: 'Meta Model API rate limits',
2282
+ capturedAt: new Date(),
2283
+ windows,
2284
+ plan: 'Meta Model API',
2285
+ },
2286
+ hasKey: true,
2287
+ status: response.status,
2288
+ retryAfter: null,
2289
+ noHeaders: false,
2094
2290
  };
2095
2291
  }
2096
2292
  catch {
2097
- return null;
2293
+ return { snapshot: null, hasKey: true, status: null, retryAfter: null, noHeaders: false };
2098
2294
  }
2099
2295
  }
2100
2296
  function headerNumber(headers, name) {
@@ -2333,7 +2529,7 @@ export function normalizeCursorUsageSummary(data) {
2333
2529
  function readCursorCredentials(base) {
2334
2530
  try {
2335
2531
  const cfgPath = path.join(base, '.cursor', 'cli-config.json');
2336
- const authPath = path.join(base, '.config', 'cursor', 'auth.json');
2532
+ const authPath = path.join(base, '.cursor', 'auth.json');
2337
2533
  if (!fs.existsSync(cfgPath) || !fs.existsSync(authPath))
2338
2534
  return null;
2339
2535
  const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
@@ -2660,10 +2856,14 @@ async function refreshAntigravityAccessToken(refreshToken) {
2660
2856
  }
2661
2857
  /**
2662
2858
  * POST :retrieveUserQuota against the Code Assist endpoints in order, returning
2663
- * the first successful bucket list. null when every endpoint rejects (expired
2664
- * token, no quota API for the account) or the network fails.
2859
+ * the first successful bucket list. `buckets: null` when every endpoint rejects
2860
+ * (expired token, no quota API for the account) or the network fails — `status`
2861
+ * carries the LAST rejection's HTTP status (or null when every attempt threw)
2862
+ * so the caller can classify the failure instead of it reading as silence.
2665
2863
  */
2666
2864
  async function fetchAntigravityQuota(accessToken) {
2865
+ let lastStatus = null;
2866
+ let lastRetryAfter = null;
2667
2867
  for (const url of ANTIGRAVITY_QUOTA_URLS) {
2668
2868
  try {
2669
2869
  const response = await fetch(url, {
@@ -2676,16 +2876,19 @@ async function fetchAntigravityQuota(accessToken) {
2676
2876
  body: '{}',
2677
2877
  signal: AbortSignal.timeout(5000),
2678
2878
  });
2679
- if (!response.ok)
2879
+ if (!response.ok) {
2880
+ lastStatus = response.status;
2881
+ lastRetryAfter = response.headers.get('retry-after');
2680
2882
  continue;
2883
+ }
2681
2884
  const data = (await response.json());
2682
- return Array.isArray(data?.buckets) ? data.buckets : [];
2885
+ return { buckets: Array.isArray(data?.buckets) ? data.buckets : [], status: response.status, retryAfter: null };
2683
2886
  }
2684
2887
  catch {
2685
2888
  continue;
2686
2889
  }
2687
2890
  }
2688
- return null;
2891
+ return { buckets: null, status: lastStatus, retryAfter: lastRetryAfter };
2689
2892
  }
2690
2893
  /** Compact model tag for the inline bar — 'gemini-2.5-flash-lite' => '2.5FL'. */
2691
2894
  export function antigravityModelShortLabel(modelId) {
@@ -2747,16 +2950,28 @@ async function getAntigravityUsageInfo(options) {
2747
2950
  try {
2748
2951
  const token = await loadAntigravityOauth(options?.home);
2749
2952
  if (!token)
2750
- return { snapshot: null, error: null };
2953
+ return { snapshot: null, error: usageNoCredentialError('Antigravity') };
2751
2954
  let accessToken = normalizeString(token.access_token);
2752
2955
  if ((!accessToken || antigravityTokenNeedsRefresh(token.expiry)) && token.refresh_token) {
2753
2956
  accessToken = await refreshAntigravityAccessToken(token.refresh_token);
2754
2957
  }
2755
2958
  if (!accessToken)
2756
- return { snapshot: null, error: null };
2757
- const buckets = await fetchAntigravityQuota(accessToken);
2758
- if (!buckets)
2759
- return { snapshot: null, error: null };
2959
+ return { snapshot: null, error: usageExpiredCredentialError('Antigravity') };
2960
+ // Honour a live Retry-After rather than re-arming the penalty (see
2961
+ // usage-backoff.ts). No request at all while the window is open. Antigravity
2962
+ // previously had NO rate-limit backoff at all (RUSH-3040) — every refresh
2963
+ // re-hit a throttled endpoint.
2964
+ const throttledUntil = usageRateLimitedUntil('antigravity', Date.now(), options?.usageScope);
2965
+ if (throttledUntil) {
2966
+ return { snapshot: null, error: usageThrottledError('Antigravity', throttledUntil) };
2967
+ }
2968
+ const { buckets, status, retryAfter } = await fetchAntigravityQuota(accessToken);
2969
+ if (!buckets) {
2970
+ return {
2971
+ snapshot: null,
2972
+ error: classifyUsageFetchFailure('Antigravity', 'antigravity', status, retryAfter, options?.usageScope),
2973
+ };
2974
+ }
2760
2975
  const windows = normalizeAntigravityWindows(buckets);
2761
2976
  if (windows.length === 0)
2762
2977
  return { snapshot: null, error: null };
@@ -2770,7 +2985,10 @@ async function getAntigravityUsageInfo(options) {
2770
2985
  error: null,
2771
2986
  };
2772
2987
  }
2773
- catch {
2774
- return { snapshot: null, error: null };
2988
+ catch (err) {
2989
+ // A thrown request (timeout, DNS, TLS, a malformed payload) is a failed
2990
+ // read like any other — staying silent here would hand the caller a stale
2991
+ // snapshot to render as confirmed (RUSH-3040).
2992
+ return { snapshot: null, error: usageUnreachableError('Antigravity', err) };
2775
2993
  }
2776
2994
  }