@kaitranntt/ccs 8.6.1 → 8.7.0-dev.2

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 (187) hide show
  1. package/dist/auth/profile-detector.d.ts.map +1 -1
  2. package/dist/auth/profile-detector.js +7 -0
  3. package/dist/auth/profile-detector.js.map +1 -1
  4. package/dist/cliproxy/config/provider-route.d.ts +3 -0
  5. package/dist/cliproxy/config/provider-route.d.ts.map +1 -1
  6. package/dist/cliproxy/config/provider-route.js +35 -1
  7. package/dist/cliproxy/config/provider-route.js.map +1 -1
  8. package/dist/cliproxy/executor/claude-launcher.d.ts.map +1 -1
  9. package/dist/cliproxy/executor/claude-launcher.js +38 -15
  10. package/dist/cliproxy/executor/claude-launcher.js.map +1 -1
  11. package/dist/cliproxy/executor/index.js +1 -1
  12. package/dist/cliproxy/executor/index.js.map +1 -1
  13. package/dist/cliproxy/executor/launch-settings.d.ts +48 -0
  14. package/dist/cliproxy/executor/launch-settings.d.ts.map +1 -0
  15. package/dist/cliproxy/executor/launch-settings.js +154 -0
  16. package/dist/cliproxy/executor/launch-settings.js.map +1 -0
  17. package/dist/commands/bar/bar-paths.d.ts +1 -1
  18. package/dist/commands/bar/bar-paths.d.ts.map +1 -1
  19. package/dist/commands/bar/bar-server-probe.d.ts +4 -6
  20. package/dist/commands/bar/bar-server-probe.d.ts.map +1 -1
  21. package/dist/commands/bar/bar-server-probe.js +21 -15
  22. package/dist/commands/bar/bar-server-probe.js.map +1 -1
  23. package/dist/commands/bar/install-subcommand.d.ts.map +1 -1
  24. package/dist/commands/bar/install-subcommand.js +2 -7
  25. package/dist/commands/bar/install-subcommand.js.map +1 -1
  26. package/dist/commands/bar/launch-descriptor.d.ts +20 -0
  27. package/dist/commands/bar/launch-descriptor.d.ts.map +1 -0
  28. package/dist/commands/bar/launch-descriptor.js +104 -0
  29. package/dist/commands/bar/launch-descriptor.js.map +1 -0
  30. package/dist/commands/bar/launch-subcommand.d.ts +3 -0
  31. package/dist/commands/bar/launch-subcommand.d.ts.map +1 -1
  32. package/dist/commands/bar/launch-subcommand.js +27 -15
  33. package/dist/commands/bar/launch-subcommand.js.map +1 -1
  34. package/dist/commands/cliproxy/help-subcommand.js +1 -1
  35. package/dist/commands/cliproxy/help-subcommand.js.map +1 -1
  36. package/dist/commands/cliproxy/proxy-lifecycle-subcommand.d.ts +5 -4
  37. package/dist/commands/cliproxy/proxy-lifecycle-subcommand.d.ts.map +1 -1
  38. package/dist/commands/cliproxy/proxy-lifecycle-subcommand.js +7 -5
  39. package/dist/commands/cliproxy/proxy-lifecycle-subcommand.js.map +1 -1
  40. package/dist/commands/cursor-command.d.ts.map +1 -1
  41. package/dist/commands/cursor-command.js +2 -0
  42. package/dist/commands/cursor-command.js.map +1 -1
  43. package/dist/config/loader/config-getters.d.ts +5 -0
  44. package/dist/config/loader/config-getters.d.ts.map +1 -1
  45. package/dist/config/loader/config-getters.js +7 -0
  46. package/dist/config/loader/config-getters.js.map +1 -1
  47. package/dist/config/loader/defaults-merger.d.ts.map +1 -1
  48. package/dist/config/loader/defaults-merger.js +5 -0
  49. package/dist/config/loader/defaults-merger.js.map +1 -1
  50. package/dist/config/loader/yaml-serializer.d.ts.map +1 -1
  51. package/dist/config/loader/yaml-serializer.js +3 -2
  52. package/dist/config/loader/yaml-serializer.js.map +1 -1
  53. package/dist/config/schemas/index.d.ts +1 -1
  54. package/dist/config/schemas/index.d.ts.map +1 -1
  55. package/dist/config/schemas/index.js.map +1 -1
  56. package/dist/config/schemas/unified-config.d.ts.map +1 -1
  57. package/dist/config/schemas/unified-config.js +5 -0
  58. package/dist/config/schemas/unified-config.js.map +1 -1
  59. package/dist/config/schemas/websearch.d.ts +20 -2
  60. package/dist/config/schemas/websearch.d.ts.map +1 -1
  61. package/dist/config/schemas/websearch.js +1 -1
  62. package/dist/cursor/cursor-daemon-entry.d.ts.map +1 -1
  63. package/dist/cursor/cursor-daemon-entry.js +32 -13
  64. package/dist/cursor/cursor-daemon-entry.js.map +1 -1
  65. package/dist/cursor/cursor-daemon.d.ts +4 -1
  66. package/dist/cursor/cursor-daemon.d.ts.map +1 -1
  67. package/dist/cursor/cursor-daemon.js +14 -8
  68. package/dist/cursor/cursor-daemon.js.map +1 -1
  69. package/dist/cursor/cursor-runtime-probe.d.ts.map +1 -1
  70. package/dist/cursor/cursor-runtime-probe.js +2 -1
  71. package/dist/cursor/cursor-runtime-probe.js.map +1 -1
  72. package/dist/glmt/pipeline/response-builder.d.ts +5 -0
  73. package/dist/glmt/pipeline/response-builder.d.ts.map +1 -1
  74. package/dist/glmt/pipeline/response-builder.js +6 -7
  75. package/dist/glmt/pipeline/response-builder.js.map +1 -1
  76. package/dist/glmt/pipeline/types.d.ts +2 -7
  77. package/dist/glmt/pipeline/types.d.ts.map +1 -1
  78. package/dist/services/logging/log-redaction.d.ts.map +1 -1
  79. package/dist/services/logging/log-redaction.js +12 -2
  80. package/dist/services/logging/log-redaction.js.map +1 -1
  81. package/dist/shared/provider-preset-catalog.d.ts +1 -1
  82. package/dist/shared/provider-preset-catalog.d.ts.map +1 -1
  83. package/dist/shared/provider-preset-catalog.js +14 -0
  84. package/dist/shared/provider-preset-catalog.js.map +1 -1
  85. package/dist/targets/codex-adapter.d.ts.map +1 -1
  86. package/dist/targets/codex-adapter.js +8 -7
  87. package/dist/targets/codex-adapter.js.map +1 -1
  88. package/dist/targets/codex-cliproxy-provider-config.d.ts.map +1 -1
  89. package/dist/targets/codex-cliproxy-provider-config.js +12 -1
  90. package/dist/targets/codex-cliproxy-provider-config.js.map +1 -1
  91. package/dist/ui/assets/{Trans-BpNkQ7ek.js → Trans-By7poS8F.js} +1 -1
  92. package/dist/ui/assets/{accounts-BjBpKHAo.js → accounts-CT4cNpXE.js} +1 -1
  93. package/dist/ui/assets/{alert-dialog-pDQKbmfk.js → alert-dialog-D-4eNWT-.js} +1 -1
  94. package/dist/ui/assets/{api-Bz4gk1bk.js → api-BOsmB6JC.js} +1 -1
  95. package/dist/ui/assets/{auth-section-DPJ_tnXs.js → auth-section-BNu9bqxZ.js} +1 -1
  96. package/dist/ui/assets/{backups-section-DGeCRWP2.js → backups-section-QEZ5MeIb.js} +1 -1
  97. package/dist/ui/assets/{channels-BZHQSZ2O.js → channels-C8lOwrre.js} +1 -1
  98. package/dist/ui/assets/{checkbox-H7Yhg_4C.js → checkbox-DxCvOyao.js} +1 -1
  99. package/dist/ui/assets/{claude-extension-BFpic0ox.js → claude-extension-CvbfmbdL.js} +1 -1
  100. package/dist/ui/assets/{cliproxy-ai-providers-E8zkuN9-.js → cliproxy-ai-providers-jeMd_Enz.js} +1 -1
  101. package/dist/ui/assets/{cliproxy-control-panel-CXij15eo.js → cliproxy-control-panel-Ylrw7Vt0.js} +1 -1
  102. package/dist/ui/assets/{cliproxy-DQizRbbC.js → cliproxy-daECx_cg.js} +1 -1
  103. package/dist/ui/assets/{codex-Clsf_Ll9.js → codex-7d3v_F3-.js} +1 -1
  104. package/dist/ui/assets/{confirm-dialog-D-AtmCDF.js → confirm-dialog-BGbC3827.js} +1 -1
  105. package/dist/ui/assets/{copilot-D0Z8CZJb.js → copilot-DGVMqIvj.js} +1 -1
  106. package/dist/ui/assets/{cursor-BXf4OXxE.js → cursor-D94mZoOn.js} +1 -1
  107. package/dist/ui/assets/{droid-DUaaFFGf.js → droid-CijT6OtA.js} +1 -1
  108. package/dist/ui/assets/{globalenv-section-Bi6g0RHv.js → globalenv-section-Cro0fjBM.js} +1 -1
  109. package/dist/ui/assets/{health-qWLTnC0s.js → health-DFG40_kD.js} +1 -1
  110. package/dist/ui/assets/{index-BTfHfkkh.js → index-BTJNmedC.js} +1 -1
  111. package/dist/ui/assets/{index-2aTPFpCV.js → index-Bf0--BiU.js} +1 -1
  112. package/dist/ui/assets/{index-B8BRpC7W.js → index-Bf5DDwJP.js} +1 -1
  113. package/dist/ui/assets/{index-BBdOx52b.js → index-BfbJlKvM.js} +1 -1
  114. package/dist/ui/assets/{index-D4nxVNz2.js → index-D77S3tlM.js} +1 -1
  115. package/dist/ui/assets/{index-bTno3t7S.js → index-DLRoa85N.js} +1 -1
  116. package/dist/ui/assets/{index-Dx2jv0Ov.js → index-K_KarWwQ.js} +3 -3
  117. package/dist/ui/assets/{logs-dRi0gOyI.js → logs-Bc1-UurC.js} +1 -1
  118. package/dist/ui/assets/{masked-input-DtbQSbOn.js → masked-input-uuW_Ngh8.js} +1 -1
  119. package/dist/ui/assets/{proxy-status-widget-DxwDomuX.js → proxy-status-widget-BFrQZqYz.js} +1 -1
  120. package/dist/ui/assets/{raw-json-settings-editor-panel-CDlRVxaa.js → raw-json-settings-editor-panel-DEHwL2oY.js} +1 -1
  121. package/dist/ui/assets/{searchable-select-DNOKIELZ.js → searchable-select-pEfkrlZT.js} +1 -1
  122. package/dist/ui/assets/{separator-DlmzFyxp.js → separator-BTcnUF3t.js} +1 -1
  123. package/dist/ui/assets/{shared-D1fHDQQG.js → shared-DWBCHrPI.js} +1 -1
  124. package/dist/ui/assets/{table-B0nIYCuj.js → table-DdMU9lLS.js} +1 -1
  125. package/dist/ui/assets/{updates-BqVdDTJj.js → updates-DfYyQw34.js} +1 -1
  126. package/dist/ui/assets/{use-accounts-BkjSf38E.js → use-accounts-DnMcRpOC.js} +1 -1
  127. package/dist/ui/index.html +1 -1
  128. package/dist/utils/bar-auth-token.d.ts +5 -0
  129. package/dist/utils/bar-auth-token.d.ts.map +1 -1
  130. package/dist/utils/bar-auth-token.js +23 -1
  131. package/dist/utils/bar-auth-token.js.map +1 -1
  132. package/dist/utils/openai-compat-launch-settings.d.ts.map +1 -1
  133. package/dist/utils/openai-compat-launch-settings.js +6 -0
  134. package/dist/utils/openai-compat-launch-settings.js.map +1 -1
  135. package/dist/utils/shell-executor.d.ts +2 -0
  136. package/dist/utils/shell-executor.d.ts.map +1 -1
  137. package/dist/utils/shell-executor.js +11 -7
  138. package/dist/utils/shell-executor.js.map +1 -1
  139. package/dist/utils/websearch/agy.d.ts +28 -0
  140. package/dist/utils/websearch/agy.d.ts.map +1 -0
  141. package/dist/utils/websearch/agy.js +83 -0
  142. package/dist/utils/websearch/agy.js.map +1 -0
  143. package/dist/utils/websearch/hook-config.d.ts.map +1 -1
  144. package/dist/utils/websearch/hook-config.js +3 -0
  145. package/dist/utils/websearch/hook-config.js.map +1 -1
  146. package/dist/utils/websearch/hook-env.d.ts.map +1 -1
  147. package/dist/utils/websearch/hook-env.js +13 -1
  148. package/dist/utils/websearch/hook-env.js.map +1 -1
  149. package/dist/utils/websearch/index.d.ts +2 -1
  150. package/dist/utils/websearch/index.d.ts.map +1 -1
  151. package/dist/utils/websearch/index.js +7 -2
  152. package/dist/utils/websearch/index.js.map +1 -1
  153. package/dist/utils/websearch/mcp-installer.d.ts +5 -4
  154. package/dist/utils/websearch/mcp-installer.d.ts.map +1 -1
  155. package/dist/utils/websearch/mcp-installer.js +7 -13
  156. package/dist/utils/websearch/mcp-installer.js.map +1 -1
  157. package/dist/utils/websearch/status.d.ts.map +1 -1
  158. package/dist/utils/websearch/status.js +25 -5
  159. package/dist/utils/websearch/status.js.map +1 -1
  160. package/dist/utils/websearch/types.d.ts +7 -1
  161. package/dist/utils/websearch/types.d.ts.map +1 -1
  162. package/dist/utils/websearch/types.js.map +1 -1
  163. package/dist/utils/websearch-manager.d.ts +2 -1
  164. package/dist/utils/websearch-manager.d.ts.map +1 -1
  165. package/dist/utils/websearch-manager.js +6 -1
  166. package/dist/utils/websearch-manager.js.map +1 -1
  167. package/dist/web-server/routes/bar-routes.d.ts +16 -0
  168. package/dist/web-server/routes/bar-routes.d.ts.map +1 -1
  169. package/dist/web-server/routes/bar-routes.js.map +1 -1
  170. package/dist/web-server/routes/cursor-routes.d.ts.map +1 -1
  171. package/dist/web-server/routes/cursor-routes.js +2 -0
  172. package/dist/web-server/routes/cursor-routes.js.map +1 -1
  173. package/dist/web-server/routes/index.d.ts.map +1 -1
  174. package/dist/web-server/routes/index.js +7 -5
  175. package/dist/web-server/routes/index.js.map +1 -1
  176. package/dist/web-server/routes/route-helpers.d.ts.map +1 -1
  177. package/dist/web-server/routes/route-helpers.js +8 -5
  178. package/dist/web-server/routes/route-helpers.js.map +1 -1
  179. package/dist/web-server/routes/websearch-routes.d.ts.map +1 -1
  180. package/dist/web-server/routes/websearch-routes.js +7 -0
  181. package/dist/web-server/routes/websearch-routes.js.map +1 -1
  182. package/dist/web-server/usage/native-quota-collector.d.ts +50 -8
  183. package/dist/web-server/usage/native-quota-collector.d.ts.map +1 -1
  184. package/dist/web-server/usage/native-quota-collector.js +907 -71
  185. package/dist/web-server/usage/native-quota-collector.js.map +1 -1
  186. package/lib/hooks/websearch-transformer.cjs +73 -4
  187. package/package.json +1 -1
@@ -15,11 +15,48 @@
15
15
  * - circuit breaker stops calling after repeated 429s for a cooldown
16
16
  * - serve-stale-on-failure; only omit a row when there is genuinely no data
17
17
  *
18
- * Claude path: reads native credentials + polls api.anthropic.com/api/oauth/usage.
18
+ * Claude path: reads per-profile .credentials.json (file-only, NO keychain)
19
+ * and polls api.anthropic.com/api/oauth/usage. If the file is absent the
20
+ * profile is emitted as a parked row (paused:true) — never a keychain call.
21
+ *
19
22
  * Codex path: PRIMARY = live network (chatgpt.com/backend-api/wham/usage, via
20
23
  * fetchCodexQuota), FALLBACK = local session logs (getCodexLocalQuota), mirroring
21
24
  * the same safety pattern as the Claude path.
25
+ *
26
+ * Multi-profile: each Claude or Codex profile gets its own ProviderState so a
27
+ * 429 on one profile never trips the breaker of another. The active/default
28
+ * profile for each surface is live-polled (paused:false); all other profiles are
29
+ * cache-only (paused:true, force=false hardcoded) so the 2.5s /summary deadline
30
+ * is maintained — at most 2 live upstream calls per /summary regardless of
31
+ * profile count.
32
+ *
33
+ * NO macOS Keychain access anywhere in this module. The old global-default
34
+ * Claude reader (readClaudeCredentials) is kept for back-compat but is no longer
35
+ * used by the multi-profile path.
22
36
  */
37
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
38
+ if (k2 === undefined) k2 = k;
39
+ var desc = Object.getOwnPropertyDescriptor(m, k);
40
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
41
+ desc = { enumerable: true, get: function() { return m[k]; } };
42
+ }
43
+ Object.defineProperty(o, k2, desc);
44
+ }) : (function(o, m, k, k2) {
45
+ if (k2 === undefined) k2 = k;
46
+ o[k2] = m[k];
47
+ }));
48
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
49
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
50
+ }) : function(o, v) {
51
+ o["default"] = v;
52
+ });
53
+ var __importStar = (this && this.__importStar) || function (mod) {
54
+ if (mod && mod.__esModule) return mod;
55
+ var result = {};
56
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
57
+ __setModuleDefault(result, mod);
58
+ return result;
59
+ };
23
60
  Object.defineProperty(exports, "__esModule", { value: true });
24
61
  exports.getCachedNativeAccountRows = exports.getNativeAccountRows = exports.resetNativeQuotaState = void 0;
25
62
  const claude_native_credentials_1 = require("./claude-native-credentials");
@@ -27,12 +64,25 @@ const quota_fetcher_claude_1 = require("../../cliproxy/quota/quota-fetcher-claud
27
64
  const quota_fetcher_codex_1 = require("../../cliproxy/quota/quota-fetcher-codex");
28
65
  const query_1 = require("../../cliproxy/accounts/query");
29
66
  const codex_local_quota_collector_1 = require("./codex-local-quota-collector");
67
+ const fs = __importStar(require("node:fs"));
68
+ const path = __importStar(require("node:path"));
69
+ const os = __importStar(require("node:os"));
70
+ const config_manager_1 = require("../../utils/config-manager");
30
71
  // ============================================================================
31
72
  // Safety constants (concrete, named, module-level)
32
73
  // ============================================================================
33
74
  /** On-demand cache TTL. Floor is 5 min; we use 10 min because the bar polls
34
75
  * /summary far more often than a hook fires. */
35
76
  const NATIVE_QUOTA_TTL_MS = 600000; // 10 minutes
77
+ // After a 401/expired result, cache the reauth row and cool the profile down so
78
+ // an expired account is shown dimmed and re-checked at most this often instead
79
+ // of being re-polled (and re-401'd) on every /summary refresh.
80
+ const REAUTH_COOLDOWN_MS = NATIVE_QUOTA_TTL_MS; // 10 minutes
81
+ // Parked rows (no on-disk creds, quotaStatus 'unsupported') re-check on a short
82
+ // TTL — re-statting credentials is cheap, and this lets a profile that the user
83
+ // just logged into appear within seconds instead of staying dimmed for the full
84
+ // quota TTL. (Reauth/error rows keep the full TTL + cooldown to avoid re-401s.)
85
+ const PARKED_TTL_MS = 30000; // 30 seconds
36
86
  /** Exponential backoff base; delay = min(base * 2^n, MAX) + jitter. */
37
87
  const RETRY_BASE_MS = 1000;
38
88
  /** Ceiling for any single backoff / Retry-After cooldown derived from one call. */
@@ -43,8 +93,19 @@ const JITTER_MAX_MS = 500;
43
93
  const CB_TRIP_THRESHOLD = 3;
44
94
  /** How long the breaker stays open (zero network) once tripped. */
45
95
  const CB_COOLDOWN_MS = 900000; // 15 minutes
46
- const CLAUDE_PROVIDER = 'claude-code';
47
- const CODEX_PROVIDER = 'codex';
96
+ // Surface identifiers
97
+ const SURFACE_CLAUDE = 'ccs';
98
+ const SURFACE_CODEX = 'ccsx';
99
+ // The "default way of running" a surface — the bare login (e.g. ~/.codex for
100
+ // ccsx), as opposed to a named `ccsx <profile>` profile. Rendered in the Bar as
101
+ // the base command ("ccsx") with a "default" badge, not as a named profile.
102
+ const DEFAULT_PROFILE = 'default';
103
+ // Provider values on the wire (unchanged from before)
104
+ const CLAUDE_NATIVE_PROVIDER = 'claude-code';
105
+ const CODEX_NATIVE_PROVIDER = 'codex';
106
+ // Keep old names as aliases to avoid breaking the existing global collector path
107
+ const CLAUDE_PROVIDER = CLAUDE_NATIVE_PROVIDER;
108
+ const CODEX_PROVIDER = CODEX_NATIVE_PROVIDER;
48
109
  function freshProviderState() {
49
110
  return {
50
111
  cachedRow: null,
@@ -56,12 +117,21 @@ function freshProviderState() {
56
117
  backoffAttempt: 0,
57
118
  };
58
119
  }
59
- const claudeState = freshProviderState();
60
- const codexState = freshProviderState();
120
+ // Per-profile state maps (key = profile name)
121
+ const claudeProfileStates = new Map();
122
+ const codexProfileStates = new Map();
123
+ function getState(map, key) {
124
+ let s = map.get(key);
125
+ if (!s) {
126
+ s = freshProviderState();
127
+ map.set(key, s);
128
+ }
129
+ return s;
130
+ }
61
131
  /** Reset all module state. Tests call this to avoid cross-test pollution. */
62
132
  function resetNativeQuotaState() {
63
- Object.assign(claudeState, freshProviderState());
64
- Object.assign(codexState, freshProviderState());
133
+ claudeProfileStates.clear();
134
+ codexProfileStates.clear();
65
135
  }
66
136
  exports.resetNativeQuotaState = resetNativeQuotaState;
67
137
  // ============================================================================
@@ -174,12 +244,15 @@ function buildClaudeQuotaWindows(quota) {
174
244
  }
175
245
  return windows;
176
246
  }
177
- function buildClaudeRow(quota, tier, now) {
247
+ function buildClaudeRow(quota, tier, now, surface, profile) {
178
248
  const quotaWindows = buildClaudeQuotaWindows(quota);
179
249
  return {
180
- account_id: CLAUDE_PROVIDER,
181
- provider: CLAUDE_PROVIDER,
182
- displayName: 'Claude Code',
250
+ account_id: `${surface}:${profile}`,
251
+ provider: CLAUDE_NATIVE_PROVIDER,
252
+ surface,
253
+ profile,
254
+ is_subscription: true,
255
+ displayName: profile,
183
256
  tier,
184
257
  paused: false,
185
258
  quota_percentage: deriveClaudeQuotaPercentage(quota),
@@ -208,12 +281,15 @@ function buildCodexQuotaWindows(quota) {
208
281
  windowMinutes: w.windowMinutes,
209
282
  }));
210
283
  }
211
- function buildCodexRow(quota, now) {
284
+ function buildCodexRow(quota, now, surface, profile) {
212
285
  const quotaWindows = buildCodexQuotaWindows(quota);
213
286
  return {
214
- account_id: CODEX_PROVIDER,
215
- provider: CODEX_PROVIDER,
216
- displayName: 'Codex',
287
+ account_id: `${surface}:${profile}`,
288
+ provider: CODEX_NATIVE_PROVIDER,
289
+ surface,
290
+ profile,
291
+ is_subscription: true,
292
+ displayName: profile,
217
293
  tier: quota.tier,
218
294
  paused: false,
219
295
  quota_percentage: quota.quotaPercentage,
@@ -240,7 +316,7 @@ function buildCodexRow(quota, now) {
240
316
  * stable keys as the Claude path. quota_percentage = min remaining across present
241
317
  * core windows. next_reset = soonest core resetAt. No staleAsOf on a live result.
242
318
  */
243
- function buildCodexNetworkRow(quota, now) {
319
+ function buildCodexNetworkRow(quota, now, surface, profile) {
244
320
  const windows = [];
245
321
  const fiveHour = quota.coreUsage?.fiveHour;
246
322
  if (fiveHour) {
@@ -276,9 +352,12 @@ function buildCodexNetworkRow(quota, now) {
276
352
  .sort((a, b) => a.ms - b.ms);
277
353
  const nextReset = resets.length > 0 ? resets[0].iso : null;
278
354
  return {
279
- account_id: CODEX_PROVIDER,
280
- provider: CODEX_PROVIDER,
281
- displayName: 'Codex',
355
+ account_id: `${surface}:${profile}`,
356
+ provider: CODEX_NATIVE_PROVIDER,
357
+ surface,
358
+ profile,
359
+ is_subscription: true,
360
+ displayName: profile,
282
361
  tier: quota.planType ?? null,
283
362
  paused: false,
284
363
  quota_percentage: quotaPercentage,
@@ -302,17 +381,596 @@ function serveCached(state) {
302
381
  return { ...state.cachedRow, cached: true };
303
382
  }
304
383
  // ============================================================================
305
- // Claude path with full safety controls
384
+ // File-only Claude credentials reader for per-profile paths (NO keychain)
385
+ // ============================================================================
386
+ /**
387
+ * Read credentials for a specific Claude Code profile (file-only, no keychain).
388
+ *
389
+ * Looks for .credentials.json in the profile's instance directory. If the file
390
+ * is absent or unparseable, returns null — the caller emits a parked row.
391
+ * Never calls security/Keychain — zero new keychain access from this feature.
392
+ */
393
+ function readClaudeCredentialsForProfileFromDisk(profile) {
394
+ // The bare `ccs` default login uses the standard global credential lookup:
395
+ // ~/.claude/.credentials.json, falling back to the single global
396
+ // "Claude Code-credentials" Keychain item that Claude Code itself maintains.
397
+ // This is the ONE pre-existing global read the shipped Bar already performs --
398
+ // NOT a per-profile Keychain scan. Isolated `ccs auth` profiles below stay
399
+ // file-only and never touch the Keychain.
400
+ if (profile === DEFAULT_PROFILE) {
401
+ return (0, claude_native_credentials_1.readClaudeCredentials)();
402
+ }
403
+ try {
404
+ const instanceDir = path.join((0, config_manager_1.getCcsDir)(), 'instances', profile);
405
+ const credFile = path.join(instanceDir, '.credentials.json');
406
+ if (!fs.existsSync(credFile))
407
+ return null;
408
+ const raw = fs.readFileSync(credFile, 'utf8');
409
+ const parsed = JSON.parse(raw);
410
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
411
+ return parsed;
412
+ }
413
+ return null;
414
+ }
415
+ catch {
416
+ return null;
417
+ }
418
+ }
419
+ // ============================================================================
420
+ // Profile enumeration helpers (production implementations, DI-overridable)
421
+ // ============================================================================
422
+ /**
423
+ * Read Codex native auth from the profile's on-disk auth.json.
424
+ * 'personal' reads ~/.codex/auth.json; other names read codex-instances/<name>/auth.json.
425
+ * Returns null when absent or unparseable.
426
+ */
427
+ function readCodexNativeAuthFromDisk(profile) {
428
+ try {
429
+ let authPath;
430
+ if (profile === DEFAULT_PROFILE) {
431
+ authPath = path.join(os.homedir(), '.codex', 'auth.json');
432
+ }
433
+ else {
434
+ // resolveCodexProfileDir would validate, but we do it inline to avoid the
435
+ // import coupling and to handle invalid names gracefully (return null).
436
+ const instancesDir = path.join((0, config_manager_1.getCcsDir)(), 'codex-instances');
437
+ authPath = path.join(instancesDir, profile, 'auth.json');
438
+ }
439
+ if (!fs.existsSync(authPath))
440
+ return null;
441
+ const raw = fs.readFileSync(authPath, 'utf8');
442
+ const parsed = JSON.parse(raw);
443
+ const tokens = parsed.tokens;
444
+ if (!tokens)
445
+ return null;
446
+ const accessToken = tokens.access_token;
447
+ const accountId = tokens.account_id;
448
+ if (typeof accessToken !== 'string' || !accessToken)
449
+ return null;
450
+ return {
451
+ accessToken,
452
+ accountId: typeof accountId === 'string' ? accountId : '',
453
+ };
454
+ }
455
+ catch {
456
+ return null;
457
+ }
458
+ }
459
+ /**
460
+ * List all Claude profiles from the profile registry (merged legacy + unified).
461
+ * Returns [] on any read error so the collector degrades gracefully.
462
+ */
463
+ function listClaudeProfilesFromDisk() {
464
+ try {
465
+ // Import lazily inside function to avoid circular dep and DI override in tests
466
+ const { ProfileRegistry } = require('../../auth/profile-registry');
467
+ const registry = new ProfileRegistry();
468
+ const profiles = Object.keys(registry.getAllProfilesMerged());
469
+ // Add the bare `ccs` default login (DEFAULT_PROFILE) when ~/.claude exists --
470
+ // the default way of running `ccs`, distinct from named `ccs <profile>`
471
+ // profiles. unshift so it leads before alphabetical sorting downstream.
472
+ if (fs.existsSync(path.join(os.homedir(), '.claude'))) {
473
+ if (!profiles.includes(DEFAULT_PROFILE))
474
+ profiles.unshift(DEFAULT_PROFILE);
475
+ }
476
+ return profiles;
477
+ }
478
+ catch {
479
+ return [];
480
+ }
481
+ }
482
+ /**
483
+ * Resolve the default Claude profile. A registry-designated default wins;
484
+ * otherwise the bare ~/.claude login (DEFAULT_PROFILE) is the default `ccs`.
485
+ */
486
+ function getDefaultClaudeProfileFromDisk() {
487
+ try {
488
+ const { ProfileRegistry } = require('../../auth/profile-registry');
489
+ const registry = new ProfileRegistry();
490
+ const reg = registry.getDefaultResolved();
491
+ if (reg)
492
+ return reg;
493
+ }
494
+ catch {
495
+ // fall through to the bare default below
496
+ }
497
+ if (fs.existsSync(path.join(os.homedir(), '.claude')))
498
+ return DEFAULT_PROFILE;
499
+ return null;
500
+ }
501
+ /**
502
+ * List all Codex profiles from the registry, plus DEFAULT_PROFILE when the bare
503
+ * ~/.codex/auth.json exists. Returns [] on any read error.
504
+ */
505
+ function listCodexProfilesFromDisk() {
506
+ try {
507
+ const { CodexProfileRegistry } = require('../../codex-auth/codex-profile-registry');
508
+ const registry = new CodexProfileRegistry();
509
+ const pausedIdentities = getPausedCodexAccountIdentities();
510
+ const profiles = registry
511
+ .listProfiles()
512
+ .filter((name) => !codexProfileMatchesPausedAccount(name, registry.getProfile(name), pausedIdentities));
513
+ // Add the bare ~/.codex/auth.json (the default `ccsx` invocation) as the
514
+ // DEFAULT_PROFILE account when it exists. It is the "default way of running",
515
+ // distinct from named `ccsx <profile>` profiles.
516
+ if (fs.existsSync(path.join(os.homedir(), '.codex', 'auth.json'))) {
517
+ if (!profiles.includes(DEFAULT_PROFILE))
518
+ profiles.push(DEFAULT_PROFILE);
519
+ }
520
+ return profiles;
521
+ }
522
+ catch {
523
+ return [];
524
+ }
525
+ }
526
+ /**
527
+ * Resolve the default Codex profile. Falls back to DEFAULT_PROFILE when the bare
528
+ * ~/.codex/auth.json exists and no registry default is set.
529
+ */
530
+ function getDefaultCodexProfileFromDisk() {
531
+ try {
532
+ const { CodexProfileRegistry } = require('../../codex-auth/codex-profile-registry');
533
+ const registry = new CodexProfileRegistry();
534
+ const pausedIdentities = getPausedCodexAccountIdentities();
535
+ const def = registry.getDefault();
536
+ if (def && !codexProfileMatchesPausedAccount(def, registry.getProfile(def), pausedIdentities)) {
537
+ return def;
538
+ }
539
+ const fallback = registry
540
+ .listProfiles()
541
+ .find((name) => !codexProfileMatchesPausedAccount(name, registry.getProfile(name), pausedIdentities));
542
+ if (fallback)
543
+ return fallback;
544
+ // Fall back to the bare ~/.codex account (DEFAULT_PROFILE) when no registry
545
+ // default is set — it is the default `ccsx` invocation.
546
+ if (fs.existsSync(path.join(os.homedir(), '.codex', 'auth.json')))
547
+ return DEFAULT_PROFILE;
548
+ return null;
549
+ }
550
+ catch {
551
+ return null;
552
+ }
553
+ }
554
+ function getPausedCodexAccountIdentities() {
555
+ try {
556
+ const accounts = (0, query_1.getProviderAccounts)('codex');
557
+ if (accounts.length === 0)
558
+ return null;
559
+ const identities = new Set();
560
+ for (const account of accounts) {
561
+ if (!account.paused)
562
+ continue;
563
+ identities.add(account.id);
564
+ if (account.email)
565
+ identities.add(account.email);
566
+ }
567
+ return identities;
568
+ }
569
+ catch {
570
+ return null;
571
+ }
572
+ }
573
+ function codexProfileMatchesPausedAccount(profileName, profile, pausedIdentities) {
574
+ if (!pausedIdentities || pausedIdentities.size === 0)
575
+ return false;
576
+ return [profileName, profile.email, profile.account_id].some((identity) => typeof identity === 'string' && pausedIdentities.has(identity));
577
+ }
578
+ // ============================================================================
579
+ // Per-profile collectors with full safety controls
580
+ // ============================================================================
581
+ /**
582
+ * Tag a row with whether it is the surface's default profile (drives the
583
+ * "active" badge only). The row's own `paused` flag is authoritative for
584
+ * dimming: live/default rows arrive with paused:false, while non-default
585
+ * profiles are served cache-only and dimmed to avoid live-refresh fan-out.
586
+ */
587
+ function buildParkedClaudeProfileRow(profile, now) {
588
+ return {
589
+ account_id: `${SURFACE_CLAUDE}:${profile}`,
590
+ provider: CLAUDE_NATIVE_PROVIDER,
591
+ surface: SURFACE_CLAUDE,
592
+ profile,
593
+ is_subscription: true,
594
+ displayName: profile,
595
+ tier: null,
596
+ paused: true,
597
+ quota_percentage: null,
598
+ quotaStatus: 'unsupported',
599
+ next_reset: null,
600
+ is_default: false,
601
+ last_activity_at: null,
602
+ today_cost: null,
603
+ health: 'ok',
604
+ cached: false,
605
+ fetchedAt: new Date(now).toISOString(),
606
+ needsReauth: true,
607
+ };
608
+ }
609
+ function buildParkedCodexProfileRow(profile, now) {
610
+ return {
611
+ account_id: `${SURFACE_CODEX}:${profile}`,
612
+ provider: CODEX_NATIVE_PROVIDER,
613
+ surface: SURFACE_CODEX,
614
+ profile,
615
+ is_subscription: true,
616
+ displayName: profile,
617
+ tier: null,
618
+ paused: true,
619
+ quota_percentage: null,
620
+ quotaStatus: 'unsupported',
621
+ next_reset: null,
622
+ is_default: false,
623
+ last_activity_at: null,
624
+ today_cost: null,
625
+ health: 'ok',
626
+ cached: false,
627
+ fetchedAt: new Date(now).toISOString(),
628
+ needsReauth: true,
629
+ };
630
+ }
631
+ function collectCachedOrParkedProfileRow(map, profile, now, buildParkedRow) {
632
+ const state = getState(map, profile);
633
+ if (state.cachedRow) {
634
+ return { ...state.cachedRow, cached: true, paused: true };
635
+ }
636
+ const parkedRow = buildParkedRow(profile, now);
637
+ state.cachedRow = parkedRow;
638
+ state.cachedAt = now;
639
+ return parkedRow;
640
+ }
641
+ function markDefault(row, isDefault) {
642
+ return { ...row, is_default: isDefault };
643
+ }
644
+ /**
645
+ * Tag the row with is_default AND write the flag back onto the cached copy. The
646
+ * collector caches a row before the default profile is known (the default is
647
+ * resolved in the enumerator), so without this the cache-fallback path
648
+ * (getCachedNativeAccountRows) would serve the default account with
649
+ * is_default:false and the UI would stop ordering/tagging it as the default.
650
+ */
651
+ function markDefaultAndSyncCache(map, profile, row, isDefault) {
652
+ const state = map.get(profile);
653
+ const marked = markDefault(row, isDefault);
654
+ if (state?.cachedRow) {
655
+ state.cachedRow = { ...state.cachedRow, is_default: isDefault };
656
+ }
657
+ return marked;
658
+ }
659
+ async function collectClaudeRowForProfile(profile, deps, force = false) {
660
+ const now = (deps.now ?? Date.now)();
661
+ const state = getState(claudeProfileStates, profile);
662
+ // Serve from cache while within TTL — force bypasses the short-circuit. Parked
663
+ // rows (no creds -> quotaStatus 'unsupported') use a short TTL so a fresh login
664
+ // is picked up within seconds instead of staying dimmed for the full quota TTL.
665
+ if (!force && state.cachedRow) {
666
+ const ttl = state.cachedRow.quotaStatus === 'unsupported' ? PARKED_TTL_MS : NATIVE_QUOTA_TTL_MS;
667
+ if (now - state.cachedAt < ttl)
668
+ return serveCached(state);
669
+ }
670
+ // Breaker open or cooldown active -> zero network, serve stale (may be null).
671
+ // Force does NOT bypass the breaker — it protects the account.
672
+ if (now < state.breakerOpenUntil || now < state.cooldownUntil) {
673
+ return serveCached(state);
674
+ }
675
+ // Coalesce: concurrent callers past TTL share one in-flight fetch.
676
+ if (state.pending) {
677
+ return state.pending;
678
+ }
679
+ // For per-profile reads: use the injected seam (file-only, no keychain).
680
+ const readCreds = deps.readClaudeCredentialsForProfile ??
681
+ ((p) => readClaudeCredentialsForProfileFromDisk(p));
682
+ const fetchQuota = deps.fetchClaudeQuota ?? quota_fetcher_claude_1.fetchClaudeQuotaWithToken;
683
+ const sleep = deps.sleep ?? defaultSleep;
684
+ state.pending = (async () => {
685
+ try {
686
+ // Yield once so the `state.pending = (...)()` assignment above completes
687
+ // before any synchronous return below runs the finally that nulls it.
688
+ // Without this, a sync return (e.g. the no-creds parked path) clears
689
+ // pending DURING assignment, leaving a stale resolved promise that the
690
+ // next call's coalescing check would return instead of re-evaluating.
691
+ await Promise.resolve();
692
+ const creds = readCreds(profile);
693
+ // No credentials file found -> emit parked row (needs auth, file absent).
694
+ // This is the expected case when the profile exists in the registry but the
695
+ // user has not logged in via 'ccs auth' for this machine or the credentials
696
+ // are stored only in keychain (which we deliberately do not access here).
697
+ if (!creds) {
698
+ const parkedRow = buildParkedClaudeProfileRow(profile, now);
699
+ // Cache the parked row so repeated calls don't re-stat the fs.
700
+ state.cachedRow = parkedRow;
701
+ state.cachedAt = now;
702
+ return parkedRow;
703
+ }
704
+ // No token / unsupported subscription -> never spend a call, omit the row.
705
+ if (!(0, claude_native_credentials_1.hasSupportedSubscription)(creds)) {
706
+ return serveCached(state);
707
+ }
708
+ const token = (0, claude_native_credentials_1.getAccessToken)(creds);
709
+ if (!token) {
710
+ return serveCached(state);
711
+ }
712
+ const tier = (0, claude_native_credentials_1.getSubscriptionTier)(creds);
713
+ const quota = await fetchQuota(token, `${SURFACE_CLAUDE}:${profile}`);
714
+ if (quota.success) {
715
+ // Success closes the breaker and clears backoff.
716
+ state.consecutive429 = 0;
717
+ state.breakerOpenUntil = 0;
718
+ state.cooldownUntil = 0;
719
+ state.backoffAttempt = 0;
720
+ const row = buildClaudeRow(quota, tier, now, SURFACE_CLAUDE, profile);
721
+ state.cachedRow = row;
722
+ state.cachedAt = now;
723
+ return { ...row, cached: false };
724
+ }
725
+ // 401 -> token expired. Emit a dimmed reauth row so the bar can prompt.
726
+ // Cache it and open a cooldown so the expired account is NOT re-polled
727
+ // (and re-401'd) on every refresh; it re-checks after REAUTH_COOLDOWN_MS,
728
+ // picking up a successful re-auth.
729
+ if (quota.needsReauth) {
730
+ const row = {
731
+ account_id: `${SURFACE_CLAUDE}:${profile}`,
732
+ provider: CLAUDE_NATIVE_PROVIDER,
733
+ surface: SURFACE_CLAUDE,
734
+ profile,
735
+ is_subscription: true,
736
+ displayName: profile,
737
+ tier,
738
+ paused: true,
739
+ quota_percentage: null,
740
+ quotaStatus: 'error',
741
+ next_reset: null,
742
+ is_default: false,
743
+ last_activity_at: null,
744
+ today_cost: null,
745
+ health: 'error',
746
+ cached: false,
747
+ fetchedAt: new Date(now).toISOString(),
748
+ needsReauth: true,
749
+ };
750
+ state.cachedRow = row;
751
+ state.cachedAt = now;
752
+ state.cooldownUntil = now + REAUTH_COOLDOWN_MS;
753
+ return { ...row, cached: false };
754
+ }
755
+ // 429 / 5xx / transient. Apply backoff + breaker, then serve stale.
756
+ const is429 = quota.httpStatus === 429;
757
+ if (is429) {
758
+ state.consecutive429 += 1;
759
+ if (state.consecutive429 >= CB_TRIP_THRESHOLD) {
760
+ state.breakerOpenUntil = now + CB_COOLDOWN_MS;
761
+ }
762
+ const retryAfter = parseRetryAfterMs(quota.errorDetail, now);
763
+ const backoff = retryAfter ?? computeBackoffMs(state.backoffAttempt);
764
+ state.cooldownUntil = now + backoff;
765
+ state.backoffAttempt += 1;
766
+ // We do NOT sleep-then-retry inside the request path (that would burn
767
+ // the request budget). The cooldown gates the NEXT call instead.
768
+ void sleep; // retained as an injectable seam for future inline retry
769
+ }
770
+ else if (quota.retryable) {
771
+ const backoff = computeBackoffMs(state.backoffAttempt);
772
+ state.cooldownUntil = now + backoff;
773
+ state.backoffAttempt += 1;
774
+ }
775
+ // Serve last good row on failure; omit if we never succeeded.
776
+ return serveCached(state);
777
+ }
778
+ catch {
779
+ // Network/parse rejection -> treat as transient, serve stale.
780
+ const backoff = computeBackoffMs(state.backoffAttempt);
781
+ state.cooldownUntil = now + backoff;
782
+ state.backoffAttempt += 1;
783
+ return serveCached(state);
784
+ }
785
+ finally {
786
+ state.pending = null;
787
+ }
788
+ })();
789
+ return state.pending;
790
+ }
791
+ async function collectCodexRowForProfile(profile, deps, force = false) {
792
+ const now = (deps.now ?? Date.now)();
793
+ const state = getState(codexProfileStates, profile);
794
+ // Serve from cache while within TTL — force bypasses the short-circuit. Parked
795
+ // rows (no auth -> quotaStatus 'unsupported') use a short TTL so a fresh login
796
+ // is picked up within seconds instead of staying dimmed for the full quota TTL.
797
+ if (!force && state.cachedRow) {
798
+ const ttl = state.cachedRow.quotaStatus === 'unsupported' ? PARKED_TTL_MS : NATIVE_QUOTA_TTL_MS;
799
+ if (now - state.cachedAt < ttl)
800
+ return serveCached(state);
801
+ }
802
+ // Breaker open or cooldown active -> skip network, go to LOCAL fallback.
803
+ // Force does NOT bypass the breaker — it protects the account.
804
+ const breakerOrCooldownActive = now < state.breakerOpenUntil || now < state.cooldownUntil;
805
+ // Coalesce: concurrent callers past TTL share one in-flight resolution.
806
+ if (state.pending) {
807
+ return state.pending;
808
+ }
809
+ // Resolve the native auth for this profile to get a network accountId.
810
+ const readNativeAuth = deps.readCodexNativeAuth ?? ((p) => readCodexNativeAuthFromDisk(p));
811
+ // For the network fallback: the legacy getDefaultCodexAccountId is the
812
+ // CLIProxy-registry path; for native profiles we use the on-disk auth directly.
813
+ const fetchNetwork = deps.fetchCodexNetworkQuota ?? ((accountId) => (0, quota_fetcher_codex_1.fetchCodexQuota)(accountId));
814
+ const getCodex = deps.getCodexQuota ?? codex_local_quota_collector_1.getCodexLocalQuota;
815
+ const sleep = deps.sleep ?? defaultSleep;
816
+ state.pending = (async () => {
817
+ try {
818
+ // Yield once so the `state.pending = (...)()` assignment completes before
819
+ // any synchronous return below runs the finally that nulls it (otherwise a
820
+ // sync return leaves a stale resolved promise the next call would reuse).
821
+ await Promise.resolve();
822
+ // ----------------------------------------------------------------
823
+ // PRIMARY: live network fetch (skipped when breaker/cooldown active)
824
+ // ----------------------------------------------------------------
825
+ if (!breakerOrCooldownActive) {
826
+ const nativeAuth = readNativeAuth(profile);
827
+ // Use the on-disk accountId for the network call; fall through to local
828
+ // when the auth file is absent (parked profile).
829
+ if (nativeAuth) {
830
+ const quota = await fetchNetwork(nativeAuth.accountId || profile);
831
+ if (quota.success) {
832
+ // A healthy response closes the breaker and clears backoff,
833
+ // regardless of content.
834
+ state.consecutive429 = 0;
835
+ state.breakerOpenUntil = 0;
836
+ state.cooldownUntil = 0;
837
+ state.backoffAttempt = 0;
838
+ // At least one core window (5h/weekly) resolved -> full quota row.
839
+ if (quota.coreUsage?.fiveHour || quota.coreUsage?.weekly) {
840
+ const row = buildCodexNetworkRow(quota, now, SURFACE_CODEX, profile);
841
+ state.cachedRow = row;
842
+ state.cachedAt = now;
843
+ return { ...row, cached: false };
844
+ }
845
+ // Success but no core window. The token authenticated, so this is a
846
+ // VALID active subscription with a sparse payload — emit an active
847
+ // (quota-less) row instead of parking it. Only the bare default keeps
848
+ // falling through to the global local session data below.
849
+ if (profile !== DEFAULT_PROFILE) {
850
+ const row = buildCodexNetworkRow(quota, now, SURFACE_CODEX, profile);
851
+ state.cachedRow = row;
852
+ state.cachedAt = now;
853
+ return { ...row, cached: false };
854
+ }
855
+ // else (default): fall through to LOCAL fallback below.
856
+ }
857
+ else if (quota.needsReauth) {
858
+ // Token expired -> dimmed reauth row. Cache it and cool down so the
859
+ // expired account is NOT re-polled (and re-401'd) every refresh; it
860
+ // re-checks after REAUTH_COOLDOWN_MS to pick up a re-auth.
861
+ const reauthRow = {
862
+ account_id: `${SURFACE_CODEX}:${profile}`,
863
+ provider: CODEX_NATIVE_PROVIDER,
864
+ surface: SURFACE_CODEX,
865
+ profile,
866
+ is_subscription: true,
867
+ displayName: profile,
868
+ tier: null,
869
+ paused: true,
870
+ quota_percentage: null,
871
+ quotaStatus: 'error',
872
+ next_reset: null,
873
+ is_default: false,
874
+ last_activity_at: null,
875
+ today_cost: null,
876
+ health: 'error',
877
+ cached: false,
878
+ fetchedAt: new Date(now).toISOString(),
879
+ needsReauth: true,
880
+ };
881
+ state.cachedRow = reauthRow;
882
+ state.cachedAt = now;
883
+ state.cooldownUntil = now + REAUTH_COOLDOWN_MS;
884
+ return { ...reauthRow, cached: false };
885
+ }
886
+ else if (quota.httpStatus === 429) {
887
+ // 429: apply breaker + backoff, then fall through to local.
888
+ state.consecutive429 += 1;
889
+ if (state.consecutive429 >= CB_TRIP_THRESHOLD) {
890
+ state.breakerOpenUntil = now + CB_COOLDOWN_MS;
891
+ }
892
+ const retryAfter = parseRetryAfterMs(quota.errorDetail, now);
893
+ const backoff = retryAfter ?? computeBackoffMs(state.backoffAttempt);
894
+ state.cooldownUntil = now + backoff;
895
+ state.backoffAttempt += 1;
896
+ void sleep; // retained as an injectable seam for future inline retry
897
+ }
898
+ else if (quota.retryable) {
899
+ // Other transient failure: set cooldown, fall through to local.
900
+ const backoff = computeBackoffMs(state.backoffAttempt);
901
+ state.cooldownUntil = now + backoff;
902
+ state.backoffAttempt += 1;
903
+ }
904
+ else {
905
+ // Terminal non-retryable failure (e.g. 403/404): back off so we
906
+ // don't re-hit a dead endpoint every poll when no local data caches
907
+ // a row to engage the TTL short-circuit. Then fall through to local.
908
+ const backoff = computeBackoffMs(state.backoffAttempt);
909
+ state.cooldownUntil = now + backoff;
910
+ state.backoffAttempt += 1;
911
+ }
912
+ // Fall through to LOCAL fallback below.
913
+ }
914
+ // No on-disk auth for this profile -> fall through to the fallback below.
915
+ }
916
+ // ----------------------------------------------------------------
917
+ // LOCAL FALLBACK: session-log read (zero network). The session logs live
918
+ // in the GLOBAL ~/.codex, so they represent ONLY the bare default account,
919
+ // never a named profile. Using them for a named profile would misattribute
920
+ // the default's usage to the profile, so only the default falls back to
921
+ // local; named profiles park instead.
922
+ // ----------------------------------------------------------------
923
+ if (profile === DEFAULT_PROFILE) {
924
+ const localQuota = await getCodex();
925
+ if (localQuota) {
926
+ const row = buildCodexRow(localQuota, now, SURFACE_CODEX, profile);
927
+ state.cachedRow = row;
928
+ state.cachedAt = now;
929
+ return { ...row, cached: false };
930
+ }
931
+ }
932
+ // Named profile (or default with no local data): serve the last-known row
933
+ // if any, else a dimmed parked row -- never global local data for a named
934
+ // profile.
935
+ const stale = serveCached(state);
936
+ if (stale)
937
+ return stale;
938
+ const parkedRow = buildParkedCodexProfileRow(profile, now);
939
+ state.cachedRow = parkedRow;
940
+ state.cachedAt = now;
941
+ return parkedRow;
942
+ }
943
+ catch {
944
+ // Network/parse rejection -> treat as transient, serve stale.
945
+ const backoff = computeBackoffMs(state.backoffAttempt);
946
+ state.cooldownUntil = now + backoff;
947
+ state.backoffAttempt += 1;
948
+ return serveCached(state);
949
+ }
950
+ finally {
951
+ state.pending = null;
952
+ }
953
+ })();
954
+ return state.pending;
955
+ }
956
+ // ============================================================================
957
+ // Legacy single-profile collectors (unchanged; used by old tests + back-compat)
306
958
  // ============================================================================
959
+ /**
960
+ * @deprecated Use collectClaudeRowForProfile with the 'default' or appropriate
961
+ * profile name. Kept for backward compatibility with existing tests that stub
962
+ * readCredentials/getDefaultCodexAccountId directly.
963
+ */
307
964
  async function collectClaudeRow(deps, force = false) {
308
965
  const now = (deps.now ?? Date.now)();
309
- const state = claudeState;
966
+ // Use the legacy single-state approach via profile key '__legacy__' to avoid
967
+ // breaking state isolation with the per-profile maps.
968
+ const state = getState(claudeProfileStates, '__legacy__');
310
969
  // Serve from cache while within TTL — force bypasses TTL short-circuit.
311
970
  if (!force && state.cachedRow && now - state.cachedAt < NATIVE_QUOTA_TTL_MS) {
312
971
  return serveCached(state);
313
972
  }
314
973
  // Breaker open or cooldown active -> zero network, serve stale (may be null).
315
- // Force does NOT bypass the breaker — it protects the account.
316
974
  if (now < state.breakerOpenUntil || now < state.cooldownUntil) {
317
975
  return serveCached(state);
318
976
  }
@@ -320,12 +978,12 @@ async function collectClaudeRow(deps, force = false) {
320
978
  if (state.pending) {
321
979
  return state.pending;
322
980
  }
323
- const readCredentials = deps.readCredentials ?? claude_native_credentials_1.readClaudeCredentials;
981
+ const readCredentialsFn = deps.readCredentials ?? claude_native_credentials_1.readClaudeCredentials;
324
982
  const fetchQuota = deps.fetchClaudeQuota ?? quota_fetcher_claude_1.fetchClaudeQuotaWithToken;
325
983
  const sleep = deps.sleep ?? defaultSleep;
326
984
  state.pending = (async () => {
327
985
  try {
328
- const creds = readCredentials();
986
+ const creds = readCredentialsFn();
329
987
  // No token / unsupported subscription -> never spend a call, omit the row.
330
988
  if (!creds || !(0, claude_native_credentials_1.hasSupportedSubscription)(creds)) {
331
989
  return serveCached(state);
@@ -342,13 +1000,12 @@ async function collectClaudeRow(deps, force = false) {
342
1000
  state.breakerOpenUntil = 0;
343
1001
  state.cooldownUntil = 0;
344
1002
  state.backoffAttempt = 0;
345
- const row = buildClaudeRow(quota, tier, now);
1003
+ const row = buildClaudeRowLegacy(quota, tier, now);
346
1004
  state.cachedRow = row;
347
1005
  state.cachedAt = now;
348
1006
  return { ...row, cached: false };
349
1007
  }
350
- // 401 -> token expired. Emit a reauth row so the bar can prompt; this is
351
- // a real, actionable state distinct from a transient failure.
1008
+ // 401 -> token expired.
352
1009
  if (quota.needsReauth) {
353
1010
  const row = {
354
1011
  account_id: CLAUDE_PROVIDER,
@@ -367,11 +1024,9 @@ async function collectClaudeRow(deps, force = false) {
367
1024
  fetchedAt: new Date(now).toISOString(),
368
1025
  needsReauth: true,
369
1026
  };
370
- // Do not cache the reauth row as a good value; it should re-evaluate
371
- // once the user re-auths. But return it now.
372
1027
  return row;
373
1028
  }
374
- // 429 / 5xx / transient. Apply backoff + breaker, then serve stale.
1029
+ // 429 / 5xx / transient.
375
1030
  const is429 = quota.httpStatus === 429;
376
1031
  if (is429) {
377
1032
  state.consecutive429 += 1;
@@ -382,20 +1037,16 @@ async function collectClaudeRow(deps, force = false) {
382
1037
  const backoff = retryAfter ?? computeBackoffMs(state.backoffAttempt);
383
1038
  state.cooldownUntil = now + backoff;
384
1039
  state.backoffAttempt += 1;
385
- // We do NOT sleep-then-retry inside the request path (that would burn
386
- // the request budget). The cooldown gates the NEXT call instead.
387
- void sleep; // retained as an injectable seam for future inline retry
1040
+ void sleep;
388
1041
  }
389
1042
  else if (quota.retryable) {
390
1043
  const backoff = computeBackoffMs(state.backoffAttempt);
391
1044
  state.cooldownUntil = now + backoff;
392
1045
  state.backoffAttempt += 1;
393
1046
  }
394
- // Serve last good row on failure; omit if we never succeeded.
395
1047
  return serveCached(state);
396
1048
  }
397
1049
  catch {
398
- // Network/parse rejection -> treat as transient, serve stale.
399
1050
  const backoff = computeBackoffMs(state.backoffAttempt);
400
1051
  state.cooldownUntil = now + backoff;
401
1052
  state.backoffAttempt += 1;
@@ -407,18 +1058,40 @@ async function collectClaudeRow(deps, force = false) {
407
1058
  })();
408
1059
  return state.pending;
409
1060
  }
410
- // ============================================================================
411
- // Codex path with live network as PRIMARY, local logs as FALLBACK
412
- // ============================================================================
1061
+ /** Legacy row builder — no surface/profile/is_subscription fields. */
1062
+ function buildClaudeRowLegacy(quota, tier, now) {
1063
+ const quotaWindows = buildClaudeQuotaWindows(quota);
1064
+ return {
1065
+ account_id: CLAUDE_PROVIDER,
1066
+ provider: CLAUDE_PROVIDER,
1067
+ displayName: 'Claude Code',
1068
+ tier,
1069
+ paused: false,
1070
+ quota_percentage: deriveClaudeQuotaPercentage(quota),
1071
+ quotaStatus: 'ok',
1072
+ next_reset: deriveClaudeNextReset(quota),
1073
+ is_default: false,
1074
+ last_activity_at: null,
1075
+ today_cost: null,
1076
+ health: 'ok',
1077
+ cached: false,
1078
+ fetchedAt: new Date(now).toISOString(),
1079
+ needsReauth: false,
1080
+ ...(quotaWindows.length > 0 ? { quotaWindows } : {}),
1081
+ };
1082
+ }
1083
+ /**
1084
+ * Legacy Codex collector — uses the old getDefaultCodexAccountId dep.
1085
+ * Kept for backward compatibility with existing tests.
1086
+ */
413
1087
  async function collectCodexRow(deps, force = false) {
414
1088
  const now = (deps.now ?? Date.now)();
415
- const state = codexState;
1089
+ const state = getState(codexProfileStates, '__legacy__');
416
1090
  // Serve from cache while within TTL — force bypasses TTL short-circuit.
417
1091
  if (!force && state.cachedRow && now - state.cachedAt < NATIVE_QUOTA_TTL_MS) {
418
1092
  return serveCached(state);
419
1093
  }
420
1094
  // Breaker open or cooldown active -> skip network, go to LOCAL fallback.
421
- // Force does NOT bypass the breaker — it protects the account.
422
1095
  const breakerOrCooldownActive = now < state.breakerOpenUntil || now < state.cooldownUntil;
423
1096
  // Coalesce: concurrent callers past TTL share one in-flight resolution.
424
1097
  if (state.pending) {
@@ -430,6 +1103,10 @@ async function collectCodexRow(deps, force = false) {
430
1103
  const sleep = deps.sleep ?? defaultSleep;
431
1104
  state.pending = (async () => {
432
1105
  try {
1106
+ // Yield once so the `state.pending = (...)()` assignment completes before
1107
+ // any synchronous return below runs the finally that nulls it (otherwise a
1108
+ // sync return leaves a stale resolved promise the next call would reuse).
1109
+ await Promise.resolve();
433
1110
  // ----------------------------------------------------------------
434
1111
  // PRIMARY: live network fetch (skipped when breaker/cooldown active)
435
1112
  // ----------------------------------------------------------------
@@ -438,19 +1115,12 @@ async function collectCodexRow(deps, force = false) {
438
1115
  if (accountId) {
439
1116
  const quota = await fetchNetwork(accountId);
440
1117
  if (quota.success) {
441
- // A healthy response closes the breaker and clears backoff,
442
- // regardless of content.
443
1118
  state.consecutive429 = 0;
444
1119
  state.breakerOpenUntil = 0;
445
1120
  state.cooldownUntil = 0;
446
1121
  state.backoffAttempt = 0;
447
- // Only usable when at least one core window (5h/weekly) resolved. A
448
- // success with empty coreUsage (only code-review/additional windows,
449
- // or a changed payload) carries no glanceable signal — do NOT cache
450
- // a contentless "ok" row or clobber a good cache; fall through to
451
- // the local fallback so the bar shows real data instead.
452
1122
  if (quota.coreUsage?.fiveHour || quota.coreUsage?.weekly) {
453
- const row = buildCodexNetworkRow(quota, now);
1123
+ const row = buildCodexNetworkRowLegacy(quota, now);
454
1124
  state.cachedRow = row;
455
1125
  state.cachedAt = now;
456
1126
  return { ...row, cached: false };
@@ -458,7 +1128,6 @@ async function collectCodexRow(deps, force = false) {
458
1128
  // else: fall through to LOCAL fallback below.
459
1129
  }
460
1130
  else if (quota.needsReauth) {
461
- // Token expired -> reauth row; do NOT cache as a good value.
462
1131
  return {
463
1132
  account_id: CODEX_PROVIDER,
464
1133
  provider: CODEX_PROVIDER,
@@ -478,7 +1147,6 @@ async function collectCodexRow(deps, force = false) {
478
1147
  };
479
1148
  }
480
1149
  else if (quota.httpStatus === 429) {
481
- // 429: apply breaker + backoff, then fall through to local.
482
1150
  state.consecutive429 += 1;
483
1151
  if (state.consecutive429 >= CB_TRIP_THRESHOLD) {
484
1152
  state.breakerOpenUntil = now + CB_COOLDOWN_MS;
@@ -487,42 +1155,34 @@ async function collectCodexRow(deps, force = false) {
487
1155
  const backoff = retryAfter ?? computeBackoffMs(state.backoffAttempt);
488
1156
  state.cooldownUntil = now + backoff;
489
1157
  state.backoffAttempt += 1;
490
- void sleep; // retained as an injectable seam for future inline retry
1158
+ void sleep;
491
1159
  }
492
1160
  else if (quota.retryable) {
493
- // Other transient failure: set cooldown, fall through to local.
494
1161
  const backoff = computeBackoffMs(state.backoffAttempt);
495
1162
  state.cooldownUntil = now + backoff;
496
1163
  state.backoffAttempt += 1;
497
1164
  }
498
1165
  else {
499
- // Terminal non-retryable failure (e.g. 403/404): back off so we
500
- // don't re-hit a dead endpoint every poll when no local data caches
501
- // a row to engage the TTL short-circuit. Then fall through to local.
502
1166
  const backoff = computeBackoffMs(state.backoffAttempt);
503
1167
  state.cooldownUntil = now + backoff;
504
1168
  state.backoffAttempt += 1;
505
1169
  }
506
1170
  // Fall through to LOCAL fallback below.
507
1171
  }
508
- // No configured accountId -> fall through to local fallback.
509
1172
  }
510
1173
  // ----------------------------------------------------------------
511
- // LOCAL FALLBACK: session log read (zero network, always attempted
512
- // when network is unavailable / no accountId / breaker active)
1174
+ // LOCAL FALLBACK
513
1175
  // ----------------------------------------------------------------
514
1176
  const localQuota = await getCodex();
515
1177
  if (localQuota) {
516
- const row = buildCodexRow(localQuota, now);
1178
+ const row = buildCodexRowLegacy(localQuota, now);
517
1179
  state.cachedRow = row;
518
1180
  state.cachedAt = now;
519
1181
  return { ...row, cached: false };
520
1182
  }
521
- // No local data either: serve stale (may be null).
522
1183
  return serveCached(state);
523
1184
  }
524
1185
  catch {
525
- // Network/parse rejection -> treat as transient, serve stale.
526
1186
  const backoff = computeBackoffMs(state.backoffAttempt);
527
1187
  state.cooldownUntil = now + backoff;
528
1188
  state.backoffAttempt += 1;
@@ -534,21 +1194,120 @@ async function collectCodexRow(deps, force = false) {
534
1194
  })();
535
1195
  return state.pending;
536
1196
  }
1197
+ /** Legacy Codex row builder (local quota). */
1198
+ function buildCodexRowLegacy(quota, now) {
1199
+ const quotaWindows = buildCodexQuotaWindows(quota);
1200
+ return {
1201
+ account_id: CODEX_PROVIDER,
1202
+ provider: CODEX_PROVIDER,
1203
+ displayName: 'Codex',
1204
+ tier: quota.tier,
1205
+ paused: false,
1206
+ quota_percentage: quota.quotaPercentage,
1207
+ quotaStatus: 'ok',
1208
+ next_reset: quota.nextReset,
1209
+ is_default: false,
1210
+ last_activity_at: null,
1211
+ today_cost: null,
1212
+ health: quota.stale ? 'warning' : 'ok',
1213
+ cached: false,
1214
+ fetchedAt: new Date(now).toISOString(),
1215
+ needsReauth: false,
1216
+ ...(quotaWindows.length > 0 ? { quotaWindows } : {}),
1217
+ ...(quota.staleAsOf ? { staleAsOf: quota.staleAsOf } : {}),
1218
+ };
1219
+ }
1220
+ /** Legacy Codex network row builder. */
1221
+ function buildCodexNetworkRowLegacy(quota, now) {
1222
+ const windows = [];
1223
+ const fiveHour = quota.coreUsage?.fiveHour;
1224
+ if (fiveHour) {
1225
+ windows.push({
1226
+ key: 'five_hour',
1227
+ label: '5h',
1228
+ usedPercent: 100 - fiveHour.remainingPercent,
1229
+ remainingPercent: fiveHour.remainingPercent,
1230
+ resetAt: fiveHour.resetAt,
1231
+ windowMinutes: FIVE_HOUR_MINUTES,
1232
+ });
1233
+ }
1234
+ const weekly = quota.coreUsage?.weekly;
1235
+ if (weekly) {
1236
+ windows.push({
1237
+ key: 'seven_day',
1238
+ label: 'week',
1239
+ usedPercent: 100 - weekly.remainingPercent,
1240
+ remainingPercent: weekly.remainingPercent,
1241
+ resetAt: weekly.resetAt,
1242
+ windowMinutes: SEVEN_DAY_MINUTES,
1243
+ });
1244
+ }
1245
+ const coreWindows = [fiveHour, weekly].filter((w) => !!w);
1246
+ const quotaPercentage = coreWindows.length > 0 ? Math.min(...coreWindows.map((w) => w.remainingPercent)) : null;
1247
+ const resets = coreWindows
1248
+ .map((w) => w.resetAt)
1249
+ .filter((r) => typeof r === 'string')
1250
+ .map((r) => ({ iso: r, ms: new Date(r).getTime() }))
1251
+ .filter((r) => Number.isFinite(r.ms))
1252
+ .sort((a, b) => a.ms - b.ms);
1253
+ const nextReset = resets.length > 0 ? resets[0].iso : null;
1254
+ return {
1255
+ account_id: CODEX_PROVIDER,
1256
+ provider: CODEX_PROVIDER,
1257
+ displayName: 'Codex',
1258
+ tier: quota.planType ?? null,
1259
+ paused: false,
1260
+ quota_percentage: quotaPercentage,
1261
+ quotaStatus: 'ok',
1262
+ next_reset: nextReset,
1263
+ is_default: false,
1264
+ last_activity_at: null,
1265
+ today_cost: null,
1266
+ health: 'ok',
1267
+ cached: false,
1268
+ fetchedAt: new Date(now).toISOString(),
1269
+ needsReauth: false,
1270
+ ...(windows.length > 0 ? { quotaWindows: windows } : {}),
1271
+ };
1272
+ }
537
1273
  // ============================================================================
538
1274
  // Public entry point
539
1275
  // ============================================================================
540
1276
  /**
541
- * Build the native subscription rows (Claude Code + Codex) for /summary.
1277
+ * Build the native subscription rows for /summary.
1278
+ *
1279
+ * When profile-enumeration deps are injected (listClaudeProfiles / listCodexProfiles
1280
+ * etc.), all profiles are enumerated and the active/default profile is live-polled
1281
+ * while non-default profiles are cache-only (parked). This keeps the 2.5s deadline:
1282
+ * at most 2 live upstream calls per /summary regardless of profile count.
542
1283
  *
543
- * Each path is independently try/caught so one failing source never blocks the
544
- * other or the response. Returns only rows that represent real data.
1284
+ * When no enumeration deps are injected (legacy mode / old tests that stub only
1285
+ * readCredentials + getDefaultCodexAccountId), the old single-profile collectors
1286
+ * are used for backward compatibility.
545
1287
  *
546
- * `opts.force` bypasses the TTL short-circuit on both paths so a debounce-
547
- * passing refresh re-pulls native rows live. The circuit breaker is always
548
- * respected regardless of force (account protection).
1288
+ * `opts.force` bypasses the TTL short-circuit on the ACTIVE profile. The circuit
1289
+ * breaker is always respected regardless of force (account protection).
549
1290
  */
550
1291
  async function getNativeAccountRows(deps = {}, opts) {
551
1292
  const force = opts?.force ?? false;
1293
+ // Multi-profile enumeration is the DEFAULT (production) behavior. The legacy
1294
+ // single-profile path is retained ONLY for backward-compat with old tests that
1295
+ // inject readCredentials / getDefaultCodexAccountId and assert the original
1296
+ // single-row output. Those harnesses never inject the enumeration seams; the
1297
+ // new multi-profile tests pair both seams, and production injects neither — so
1298
+ // everything except the legacy harness takes the multi-profile path below.
1299
+ const hasProfileEnumeration = deps.listClaudeProfiles !== undefined ||
1300
+ deps.listCodexProfiles !== undefined ||
1301
+ deps.defaultClaudeProfile !== undefined ||
1302
+ deps.defaultCodexProfile !== undefined;
1303
+ const isLegacyTestHarness = !hasProfileEnumeration &&
1304
+ (deps.readCredentials !== undefined || deps.getDefaultCodexAccountId !== undefined);
1305
+ if (!isLegacyTestHarness) {
1306
+ return getNativeAccountRowsMultiProfile(deps, force);
1307
+ }
1308
+ // Legacy path: backward-compatible with old tests that only inject
1309
+ // readCredentials / getDefaultCodexAccountId. Produces the old single-row
1310
+ // output (account_id='claude-code'/'codex', no surface/profile).
552
1311
  const [claude, codex] = await Promise.all([
553
1312
  collectClaudeRow(deps, force).catch(() => null),
554
1313
  collectCodexRow(deps, force).catch(() => null),
@@ -561,6 +1320,79 @@ async function getNativeAccountRows(deps = {}, opts) {
561
1320
  return rows;
562
1321
  }
563
1322
  exports.getNativeAccountRows = getNativeAccountRows;
1323
+ async function getNativeAccountRowsMultiProfile(deps, force) {
1324
+ const listClaude = deps.listClaudeProfiles ?? listClaudeProfilesFromDisk;
1325
+ const listCodex = deps.listCodexProfiles ?? listCodexProfilesFromDisk;
1326
+ const defaultClaude = deps.defaultClaudeProfile ?? getDefaultClaudeProfileFromDisk;
1327
+ const defaultCodex = deps.defaultCodexProfile ?? getDefaultCodexProfileFromDisk;
1328
+ const claudeProfiles = (() => {
1329
+ try {
1330
+ return listClaude();
1331
+ }
1332
+ catch {
1333
+ return [];
1334
+ }
1335
+ })();
1336
+ const codexProfiles = (() => {
1337
+ try {
1338
+ return listCodex();
1339
+ }
1340
+ catch {
1341
+ return [];
1342
+ }
1343
+ })();
1344
+ const claudeDefault = (() => {
1345
+ try {
1346
+ return defaultClaude();
1347
+ }
1348
+ catch {
1349
+ return null;
1350
+ }
1351
+ })();
1352
+ const codexDefault = (() => {
1353
+ try {
1354
+ return defaultCodex();
1355
+ }
1356
+ catch {
1357
+ return null;
1358
+ }
1359
+ })();
1360
+ const tasks = [];
1361
+ const results = [];
1362
+ const now = (deps.now ?? Date.now)();
1363
+ // Preserve the safety budget: only the active/default profile for each surface
1364
+ // may perform a live refresh. Non-default profiles are cache-only (or parked)
1365
+ // so one /summary request can trigger at most one Claude and one Codex live
1366
+ // upstream call regardless of configured profile count.
1367
+ for (const p of claudeProfiles) {
1368
+ const isDefault = p === claudeDefault;
1369
+ if (!isDefault) {
1370
+ results.push(markDefaultAndSyncCache(claudeProfileStates, p, collectCachedOrParkedProfileRow(claudeProfileStates, p, now, buildParkedClaudeProfileRow), false));
1371
+ continue;
1372
+ }
1373
+ tasks.push(collectClaudeRowForProfile(p, deps, force || claudeProfileStates.get(p)?.cachedRow?.quotaStatus === 'unsupported')
1374
+ .then((r) => (r ? markDefaultAndSyncCache(claudeProfileStates, p, r, true) : null))
1375
+ .catch(() => null));
1376
+ }
1377
+ for (const p of codexProfiles) {
1378
+ const isDefault = p === codexDefault;
1379
+ if (!isDefault) {
1380
+ results.push(markDefaultAndSyncCache(codexProfileStates, p, collectCachedOrParkedProfileRow(codexProfileStates, p, now, buildParkedCodexProfileRow), false));
1381
+ continue;
1382
+ }
1383
+ tasks.push(collectCodexRowForProfile(p, deps, force || codexProfileStates.get(p)?.cachedRow?.quotaStatus === 'unsupported')
1384
+ .then((r) => (r ? markDefaultAndSyncCache(codexProfileStates, p, r, true) : null))
1385
+ .catch(() => null));
1386
+ }
1387
+ results.push(...(await Promise.all(tasks)));
1388
+ const rows = results.filter((r) => r !== null);
1389
+ // Sort by (surface, profile) for stable ordering.
1390
+ return rows.sort((a, b) => {
1391
+ const sa = (a.surface ?? '') + ':' + (a.profile ?? '');
1392
+ const sb = (b.surface ?? '') + ':' + (b.profile ?? '');
1393
+ return sa.localeCompare(sb);
1394
+ });
1395
+ }
564
1396
  /**
565
1397
  * Last-known native rows from cache, WITHOUT any fetch (instant, no network).
566
1398
  *
@@ -571,10 +1403,14 @@ exports.getNativeAccountRows = getNativeAccountRows;
571
1403
  */
572
1404
  function getCachedNativeAccountRows() {
573
1405
  const rows = [];
574
- if (claudeState.cachedRow)
575
- rows.push({ ...claudeState.cachedRow, cached: true });
576
- if (codexState.cachedRow)
577
- rows.push({ ...codexState.cachedRow, cached: true });
1406
+ for (const state of claudeProfileStates.values()) {
1407
+ if (state.cachedRow)
1408
+ rows.push({ ...state.cachedRow, cached: true });
1409
+ }
1410
+ for (const state of codexProfileStates.values()) {
1411
+ if (state.cachedRow)
1412
+ rows.push({ ...state.cachedRow, cached: true });
1413
+ }
578
1414
  return rows;
579
1415
  }
580
1416
  exports.getCachedNativeAccountRows = getCachedNativeAccountRows;