@kaitranntt/ccs 8.6.1 → 8.7.0-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/account-context.d.ts.map +1 -1
- package/dist/auth/account-context.js +3 -1
- package/dist/auth/account-context.js.map +1 -1
- package/dist/auth/profile-detector.d.ts.map +1 -1
- package/dist/auth/profile-detector.js +7 -0
- package/dist/auth/profile-detector.js.map +1 -1
- package/dist/cliproxy/config/extended-context-config.d.ts.map +1 -1
- package/dist/cliproxy/config/extended-context-config.js +24 -5
- package/dist/cliproxy/config/extended-context-config.js.map +1 -1
- package/dist/cliproxy/config/provider-route.d.ts +3 -0
- package/dist/cliproxy/config/provider-route.d.ts.map +1 -1
- package/dist/cliproxy/config/provider-route.js +35 -1
- package/dist/cliproxy/config/provider-route.js.map +1 -1
- package/dist/cliproxy/executor/claude-launcher.d.ts.map +1 -1
- package/dist/cliproxy/executor/claude-launcher.js +38 -15
- package/dist/cliproxy/executor/claude-launcher.js.map +1 -1
- package/dist/cliproxy/executor/index.js +1 -1
- package/dist/cliproxy/executor/index.js.map +1 -1
- package/dist/cliproxy/executor/launch-settings.d.ts +48 -0
- package/dist/cliproxy/executor/launch-settings.d.ts.map +1 -0
- package/dist/cliproxy/executor/launch-settings.js +154 -0
- package/dist/cliproxy/executor/launch-settings.js.map +1 -0
- package/dist/cliproxy/model-catalog.d.ts.map +1 -1
- package/dist/cliproxy/model-catalog.js +51 -1
- package/dist/cliproxy/model-catalog.js.map +1 -1
- package/dist/codex-auth/codex-profile-plugin-cache.d.ts +2 -0
- package/dist/codex-auth/codex-profile-plugin-cache.d.ts.map +1 -0
- package/dist/codex-auth/codex-profile-plugin-cache.js +204 -0
- package/dist/codex-auth/codex-profile-plugin-cache.js.map +1 -0
- package/dist/codex-auth/codex-profile-resources.d.ts.map +1 -1
- package/dist/codex-auth/codex-profile-resources.js +2 -0
- package/dist/codex-auth/codex-profile-resources.js.map +1 -1
- package/dist/commands/bar/bar-paths.d.ts +1 -1
- package/dist/commands/bar/bar-paths.d.ts.map +1 -1
- package/dist/commands/bar/bar-server-probe.d.ts +4 -6
- package/dist/commands/bar/bar-server-probe.d.ts.map +1 -1
- package/dist/commands/bar/bar-server-probe.js +21 -15
- package/dist/commands/bar/bar-server-probe.js.map +1 -1
- package/dist/commands/bar/install-subcommand.d.ts.map +1 -1
- package/dist/commands/bar/install-subcommand.js +2 -7
- package/dist/commands/bar/install-subcommand.js.map +1 -1
- package/dist/commands/bar/launch-descriptor.d.ts +20 -0
- package/dist/commands/bar/launch-descriptor.d.ts.map +1 -0
- package/dist/commands/bar/launch-descriptor.js +104 -0
- package/dist/commands/bar/launch-descriptor.js.map +1 -0
- package/dist/commands/bar/launch-subcommand.d.ts +3 -0
- package/dist/commands/bar/launch-subcommand.d.ts.map +1 -1
- package/dist/commands/bar/launch-subcommand.js +27 -15
- package/dist/commands/bar/launch-subcommand.js.map +1 -1
- package/dist/commands/cliproxy/help-subcommand.js +1 -1
- package/dist/commands/cliproxy/help-subcommand.js.map +1 -1
- package/dist/commands/cliproxy/proxy-lifecycle-subcommand.d.ts +5 -4
- package/dist/commands/cliproxy/proxy-lifecycle-subcommand.d.ts.map +1 -1
- package/dist/commands/cliproxy/proxy-lifecycle-subcommand.js +7 -5
- package/dist/commands/cliproxy/proxy-lifecycle-subcommand.js.map +1 -1
- package/dist/commands/cursor-command.d.ts.map +1 -1
- package/dist/commands/cursor-command.js +2 -0
- package/dist/commands/cursor-command.js.map +1 -1
- package/dist/config/loader/config-getters.d.ts +5 -0
- package/dist/config/loader/config-getters.d.ts.map +1 -1
- package/dist/config/loader/config-getters.js +7 -0
- package/dist/config/loader/config-getters.js.map +1 -1
- package/dist/config/loader/defaults-merger.d.ts.map +1 -1
- package/dist/config/loader/defaults-merger.js +5 -0
- package/dist/config/loader/defaults-merger.js.map +1 -1
- package/dist/config/loader/yaml-serializer.d.ts.map +1 -1
- package/dist/config/loader/yaml-serializer.js +3 -2
- package/dist/config/loader/yaml-serializer.js.map +1 -1
- package/dist/config/schemas/index.d.ts +1 -1
- package/dist/config/schemas/index.d.ts.map +1 -1
- package/dist/config/schemas/index.js.map +1 -1
- package/dist/config/schemas/unified-config.d.ts.map +1 -1
- package/dist/config/schemas/unified-config.js +5 -0
- package/dist/config/schemas/unified-config.js.map +1 -1
- package/dist/config/schemas/websearch.d.ts +20 -2
- package/dist/config/schemas/websearch.d.ts.map +1 -1
- package/dist/config/schemas/websearch.js +1 -1
- package/dist/cursor/cursor-daemon-entry.d.ts.map +1 -1
- package/dist/cursor/cursor-daemon-entry.js +32 -13
- package/dist/cursor/cursor-daemon-entry.js.map +1 -1
- package/dist/cursor/cursor-daemon.d.ts +4 -1
- package/dist/cursor/cursor-daemon.d.ts.map +1 -1
- package/dist/cursor/cursor-daemon.js +14 -8
- package/dist/cursor/cursor-daemon.js.map +1 -1
- package/dist/cursor/cursor-runtime-probe.d.ts.map +1 -1
- package/dist/cursor/cursor-runtime-probe.js +2 -1
- package/dist/cursor/cursor-runtime-probe.js.map +1 -1
- package/dist/glmt/pipeline/response-builder.d.ts +5 -0
- package/dist/glmt/pipeline/response-builder.d.ts.map +1 -1
- package/dist/glmt/pipeline/response-builder.js +6 -7
- package/dist/glmt/pipeline/response-builder.js.map +1 -1
- package/dist/glmt/pipeline/types.d.ts +2 -7
- package/dist/glmt/pipeline/types.d.ts.map +1 -1
- package/dist/services/logging/log-redaction.d.ts.map +1 -1
- package/dist/services/logging/log-redaction.js +12 -2
- package/dist/services/logging/log-redaction.js.map +1 -1
- package/dist/shared/provider-preset-catalog.d.ts +1 -1
- package/dist/shared/provider-preset-catalog.d.ts.map +1 -1
- package/dist/shared/provider-preset-catalog.js +14 -0
- package/dist/shared/provider-preset-catalog.js.map +1 -1
- package/dist/shared/toml-object.d.ts +5 -0
- package/dist/shared/toml-object.d.ts.map +1 -1
- package/dist/shared/toml-object.js +111 -1
- package/dist/shared/toml-object.js.map +1 -1
- package/dist/targets/codex-adapter.d.ts.map +1 -1
- package/dist/targets/codex-adapter.js +8 -7
- package/dist/targets/codex-adapter.js.map +1 -1
- package/dist/targets/codex-cliproxy-provider-config.d.ts.map +1 -1
- package/dist/targets/codex-cliproxy-provider-config.js +12 -1
- package/dist/targets/codex-cliproxy-provider-config.js.map +1 -1
- package/dist/ui/assets/{Trans-BpNkQ7ek.js → Trans-udTKDAiY.js} +1 -1
- package/dist/ui/assets/{accounts-BjBpKHAo.js → accounts-DM0qGQmb.js} +1 -1
- package/dist/ui/assets/{alert-dialog-pDQKbmfk.js → alert-dialog-NSrT05Q2.js} +1 -1
- package/dist/ui/assets/{api-Bz4gk1bk.js → api-eMMnfsak.js} +1 -1
- package/dist/ui/assets/{auth-section-DPJ_tnXs.js → auth-section-BxSRH9zw.js} +1 -1
- package/dist/ui/assets/{backups-section-DGeCRWP2.js → backups-section-CWXr1_mw.js} +1 -1
- package/dist/ui/assets/{channels-BZHQSZ2O.js → channels-BBGb1vBB.js} +1 -1
- package/dist/ui/assets/{checkbox-H7Yhg_4C.js → checkbox-B3K9bT5D.js} +1 -1
- package/dist/ui/assets/{claude-extension-BFpic0ox.js → claude-extension-D9BCl9x-.js} +1 -1
- package/dist/ui/assets/cliproxy-CMqWk7m9.js +4 -0
- package/dist/ui/assets/{cliproxy-ai-providers-E8zkuN9-.js → cliproxy-ai-providers-BXsW2OhA.js} +1 -1
- package/dist/ui/assets/{cliproxy-control-panel-CXij15eo.js → cliproxy-control-panel-DPQKQS-6.js} +1 -1
- package/dist/ui/assets/codex-CxZKAvy2.js +34 -0
- package/dist/ui/assets/{confirm-dialog-D-AtmCDF.js → confirm-dialog-Bd7QQmh1.js} +1 -1
- package/dist/ui/assets/{copilot-D0Z8CZJb.js → copilot-LGR9P5Tm.js} +1 -1
- package/dist/ui/assets/{cursor-BXf4OXxE.js → cursor-pst9GU6_.js} +1 -1
- package/dist/ui/assets/{droid-DUaaFFGf.js → droid-BOZUprl3.js} +1 -1
- package/dist/ui/assets/{globalenv-section-Bi6g0RHv.js → globalenv-section-DJ5UkjeK.js} +1 -1
- package/dist/ui/assets/{health-qWLTnC0s.js → health-DUsZU-Ya.js} +1 -1
- package/dist/ui/assets/{index-D4nxVNz2.js → index-5mv-MYj7.js} +1 -1
- package/dist/ui/assets/{index-BBdOx52b.js → index-B-_IztpU.js} +1 -1
- package/dist/ui/assets/{index-2aTPFpCV.js → index-B_oqRqAI.js} +1 -1
- package/dist/ui/assets/{index-B8BRpC7W.js → index-Cr5j2J4e.js} +1 -1
- package/dist/ui/assets/{index-Dx2jv0Ov.js → index-DXkMsg40.js} +4 -4
- package/dist/ui/assets/{index-BTfHfkkh.js → index-hoCKrABm.js} +1 -1
- package/dist/ui/assets/{index-bTno3t7S.js → index-i73j7jKY.js} +1 -1
- package/dist/ui/assets/{logs-dRi0gOyI.js → logs-DYd4j4Qq.js} +1 -1
- package/dist/ui/assets/{masked-input-DtbQSbOn.js → masked-input-BDvcDWuM.js} +1 -1
- package/dist/ui/assets/{proxy-status-widget-DxwDomuX.js → proxy-status-widget-B_PozEsq.js} +1 -1
- package/dist/ui/assets/{raw-json-settings-editor-panel-CDlRVxaa.js → raw-json-settings-editor-panel-3ZJcfAeS.js} +1 -1
- package/dist/ui/assets/searchable-select-CTvbVJUs.js +1 -0
- package/dist/ui/assets/{separator-DlmzFyxp.js → separator-DBbORiOt.js} +1 -1
- package/dist/ui/assets/{shared-D1fHDQQG.js → shared-BDaNzpmF.js} +1 -1
- package/dist/ui/assets/{table-B0nIYCuj.js → table-C_CmIT9v.js} +1 -1
- package/dist/ui/assets/{updates-BqVdDTJj.js → updates-Dc0B4lfE.js} +1 -1
- package/dist/ui/assets/{use-accounts-BkjSf38E.js → use-accounts-BO-oKLTi.js} +1 -1
- package/dist/ui/index.html +1 -1
- package/dist/utils/bar-auth-token.d.ts +5 -0
- package/dist/utils/bar-auth-token.d.ts.map +1 -1
- package/dist/utils/bar-auth-token.js +23 -1
- package/dist/utils/bar-auth-token.js.map +1 -1
- package/dist/utils/openai-compat-launch-settings.d.ts.map +1 -1
- package/dist/utils/openai-compat-launch-settings.js +6 -0
- package/dist/utils/openai-compat-launch-settings.js.map +1 -1
- package/dist/utils/shell-executor.d.ts +2 -0
- package/dist/utils/shell-executor.d.ts.map +1 -1
- package/dist/utils/shell-executor.js +11 -7
- package/dist/utils/shell-executor.js.map +1 -1
- package/dist/utils/websearch/agy.d.ts +28 -0
- package/dist/utils/websearch/agy.d.ts.map +1 -0
- package/dist/utils/websearch/agy.js +83 -0
- package/dist/utils/websearch/agy.js.map +1 -0
- package/dist/utils/websearch/hook-config.d.ts.map +1 -1
- package/dist/utils/websearch/hook-config.js +3 -0
- package/dist/utils/websearch/hook-config.js.map +1 -1
- package/dist/utils/websearch/hook-env.d.ts.map +1 -1
- package/dist/utils/websearch/hook-env.js +13 -1
- package/dist/utils/websearch/hook-env.js.map +1 -1
- package/dist/utils/websearch/index.d.ts +2 -1
- package/dist/utils/websearch/index.d.ts.map +1 -1
- package/dist/utils/websearch/index.js +7 -2
- package/dist/utils/websearch/index.js.map +1 -1
- package/dist/utils/websearch/mcp-installer.d.ts +5 -4
- package/dist/utils/websearch/mcp-installer.d.ts.map +1 -1
- package/dist/utils/websearch/mcp-installer.js +7 -13
- package/dist/utils/websearch/mcp-installer.js.map +1 -1
- package/dist/utils/websearch/status.d.ts.map +1 -1
- package/dist/utils/websearch/status.js +25 -5
- package/dist/utils/websearch/status.js.map +1 -1
- package/dist/utils/websearch/types.d.ts +7 -1
- package/dist/utils/websearch/types.d.ts.map +1 -1
- package/dist/utils/websearch/types.js.map +1 -1
- package/dist/utils/websearch-manager.d.ts +2 -1
- package/dist/utils/websearch-manager.d.ts.map +1 -1
- package/dist/utils/websearch-manager.js +6 -1
- package/dist/utils/websearch-manager.js.map +1 -1
- package/dist/web-server/model-pricing.d.ts.map +1 -1
- package/dist/web-server/model-pricing.js +15 -0
- package/dist/web-server/model-pricing.js.map +1 -1
- package/dist/web-server/routes/bar-routes.d.ts +16 -0
- package/dist/web-server/routes/bar-routes.d.ts.map +1 -1
- package/dist/web-server/routes/bar-routes.js.map +1 -1
- package/dist/web-server/routes/cursor-routes.d.ts.map +1 -1
- package/dist/web-server/routes/cursor-routes.js +2 -0
- package/dist/web-server/routes/cursor-routes.js.map +1 -1
- package/dist/web-server/routes/index.d.ts.map +1 -1
- package/dist/web-server/routes/index.js +7 -5
- package/dist/web-server/routes/index.js.map +1 -1
- package/dist/web-server/routes/route-helpers.d.ts.map +1 -1
- package/dist/web-server/routes/route-helpers.js +35 -10
- package/dist/web-server/routes/route-helpers.js.map +1 -1
- package/dist/web-server/routes/websearch-routes.d.ts.map +1 -1
- package/dist/web-server/routes/websearch-routes.js +7 -0
- package/dist/web-server/routes/websearch-routes.js.map +1 -1
- package/dist/web-server/usage/aggregator.d.ts.map +1 -1
- package/dist/web-server/usage/aggregator.js +10 -7
- package/dist/web-server/usage/aggregator.js.map +1 -1
- package/dist/web-server/usage/native-quota-collector.d.ts +50 -8
- package/dist/web-server/usage/native-quota-collector.d.ts.map +1 -1
- package/dist/web-server/usage/native-quota-collector.js +910 -71
- package/dist/web-server/usage/native-quota-collector.js.map +1 -1
- package/lib/hooks/websearch-transformer.cjs +73 -4
- package/package.json +1 -1
- package/dist/ui/assets/cliproxy-DQizRbbC.js +0 -4
- package/dist/ui/assets/codex-Clsf_Ll9.js +0 -30
- package/dist/ui/assets/searchable-select-DNOKIELZ.js +0 -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
|
|
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
|
-
|
|
47
|
-
const
|
|
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
|
-
|
|
60
|
-
const
|
|
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
|
-
|
|
64
|
-
|
|
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:
|
|
181
|
-
provider:
|
|
182
|
-
|
|
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:
|
|
215
|
-
provider:
|
|
216
|
-
|
|
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:
|
|
280
|
-
provider:
|
|
281
|
-
|
|
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,599 @@ function serveCached(state) {
|
|
|
302
381
|
return { ...state.cachedRow, cached: true };
|
|
303
382
|
}
|
|
304
383
|
// ============================================================================
|
|
305
|
-
// Claude
|
|
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, readDefaultCredentials = claude_native_credentials_1.readClaudeCredentials) {
|
|
394
|
+
try {
|
|
395
|
+
const instanceDir = path.join((0, config_manager_1.getCcsDir)(), 'instances', profile);
|
|
396
|
+
const credFile = path.join(instanceDir, '.credentials.json');
|
|
397
|
+
if (fs.existsSync(credFile)) {
|
|
398
|
+
const raw = fs.readFileSync(credFile, 'utf8');
|
|
399
|
+
const parsed = JSON.parse(raw);
|
|
400
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
401
|
+
return parsed;
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
// The bare `ccs` default login uses the standard global credential lookup:
|
|
406
|
+
// ~/.claude/.credentials.json, falling back to the single global
|
|
407
|
+
// "Claude Code-credentials" Keychain item that Claude Code itself maintains.
|
|
408
|
+
// This is the ONE pre-existing global read the shipped Bar already performs --
|
|
409
|
+
// NOT a per-profile Keychain scan. Isolated `ccs auth` profiles stay
|
|
410
|
+
// file-only and never touch the Keychain; a real instance directory named
|
|
411
|
+
// "default" is therefore parked when its file is absent.
|
|
412
|
+
if (profile === DEFAULT_PROFILE && !fs.existsSync(instanceDir)) {
|
|
413
|
+
return readDefaultCredentials();
|
|
414
|
+
}
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// ============================================================================
|
|
422
|
+
// Profile enumeration helpers (production implementations, DI-overridable)
|
|
306
423
|
// ============================================================================
|
|
424
|
+
/**
|
|
425
|
+
* Read Codex native auth from the profile's on-disk auth.json.
|
|
426
|
+
* 'personal' reads ~/.codex/auth.json; other names read codex-instances/<name>/auth.json.
|
|
427
|
+
* Returns null when absent or unparseable.
|
|
428
|
+
*/
|
|
429
|
+
function readCodexNativeAuthFromDisk(profile) {
|
|
430
|
+
try {
|
|
431
|
+
let authPath;
|
|
432
|
+
if (profile === DEFAULT_PROFILE) {
|
|
433
|
+
authPath = path.join(os.homedir(), '.codex', 'auth.json');
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
// resolveCodexProfileDir would validate, but we do it inline to avoid the
|
|
437
|
+
// import coupling and to handle invalid names gracefully (return null).
|
|
438
|
+
const instancesDir = path.join((0, config_manager_1.getCcsDir)(), 'codex-instances');
|
|
439
|
+
authPath = path.join(instancesDir, profile, 'auth.json');
|
|
440
|
+
}
|
|
441
|
+
if (!fs.existsSync(authPath))
|
|
442
|
+
return null;
|
|
443
|
+
const raw = fs.readFileSync(authPath, 'utf8');
|
|
444
|
+
const parsed = JSON.parse(raw);
|
|
445
|
+
const tokens = parsed.tokens;
|
|
446
|
+
if (!tokens)
|
|
447
|
+
return null;
|
|
448
|
+
const accessToken = tokens.access_token;
|
|
449
|
+
const accountId = tokens.account_id;
|
|
450
|
+
if (typeof accessToken !== 'string' || !accessToken)
|
|
451
|
+
return null;
|
|
452
|
+
return {
|
|
453
|
+
accessToken,
|
|
454
|
+
accountId: typeof accountId === 'string' ? accountId : '',
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* List all Claude profiles from the profile registry (merged legacy + unified).
|
|
463
|
+
* Returns [] on any read error so the collector degrades gracefully.
|
|
464
|
+
*/
|
|
465
|
+
function listClaudeProfilesFromDisk() {
|
|
466
|
+
try {
|
|
467
|
+
// Import lazily inside function to avoid circular dep and DI override in tests
|
|
468
|
+
const { ProfileRegistry } = require('../../auth/profile-registry');
|
|
469
|
+
const registry = new ProfileRegistry();
|
|
470
|
+
const profiles = Object.keys(registry.getAllProfilesMerged());
|
|
471
|
+
// Add the bare `ccs` default login (DEFAULT_PROFILE) when ~/.claude exists --
|
|
472
|
+
// the default way of running `ccs`, distinct from named `ccs <profile>`
|
|
473
|
+
// profiles. unshift so it leads before alphabetical sorting downstream.
|
|
474
|
+
if (fs.existsSync(path.join(os.homedir(), '.claude'))) {
|
|
475
|
+
if (!profiles.includes(DEFAULT_PROFILE))
|
|
476
|
+
profiles.unshift(DEFAULT_PROFILE);
|
|
477
|
+
}
|
|
478
|
+
return profiles;
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
return [];
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Resolve the default Claude profile. A registry-designated default wins;
|
|
486
|
+
* otherwise the bare ~/.claude login (DEFAULT_PROFILE) is the default `ccs`.
|
|
487
|
+
*/
|
|
488
|
+
function getDefaultClaudeProfileFromDisk() {
|
|
489
|
+
try {
|
|
490
|
+
const { ProfileRegistry } = require('../../auth/profile-registry');
|
|
491
|
+
const registry = new ProfileRegistry();
|
|
492
|
+
const reg = registry.getDefaultResolved();
|
|
493
|
+
if (reg)
|
|
494
|
+
return reg;
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
// fall through to the bare default below
|
|
498
|
+
}
|
|
499
|
+
if (fs.existsSync(path.join(os.homedir(), '.claude')))
|
|
500
|
+
return DEFAULT_PROFILE;
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* List all Codex profiles from the registry, plus DEFAULT_PROFILE when the bare
|
|
505
|
+
* ~/.codex/auth.json exists. Returns [] on any read error.
|
|
506
|
+
*/
|
|
507
|
+
function listCodexProfilesFromDisk() {
|
|
508
|
+
try {
|
|
509
|
+
const { CodexProfileRegistry } = require('../../codex-auth/codex-profile-registry');
|
|
510
|
+
const registry = new CodexProfileRegistry();
|
|
511
|
+
const pausedIdentities = getPausedCodexAccountIdentities();
|
|
512
|
+
const profiles = registry
|
|
513
|
+
.listProfiles()
|
|
514
|
+
.filter((name) => !codexProfileMatchesPausedAccount(name, registry.getProfile(name), pausedIdentities));
|
|
515
|
+
// Add the bare ~/.codex/auth.json (the default `ccsx` invocation) as the
|
|
516
|
+
// DEFAULT_PROFILE account when it exists. It is the "default way of running",
|
|
517
|
+
// distinct from named `ccsx <profile>` profiles.
|
|
518
|
+
if (fs.existsSync(path.join(os.homedir(), '.codex', 'auth.json'))) {
|
|
519
|
+
if (!profiles.includes(DEFAULT_PROFILE))
|
|
520
|
+
profiles.push(DEFAULT_PROFILE);
|
|
521
|
+
}
|
|
522
|
+
return profiles;
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
return [];
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Resolve the default Codex profile. Falls back to DEFAULT_PROFILE when the bare
|
|
530
|
+
* ~/.codex/auth.json exists and no registry default is set.
|
|
531
|
+
*/
|
|
532
|
+
function getDefaultCodexProfileFromDisk() {
|
|
533
|
+
try {
|
|
534
|
+
const { CodexProfileRegistry } = require('../../codex-auth/codex-profile-registry');
|
|
535
|
+
const registry = new CodexProfileRegistry();
|
|
536
|
+
const pausedIdentities = getPausedCodexAccountIdentities();
|
|
537
|
+
const def = registry.getDefault();
|
|
538
|
+
if (def && !codexProfileMatchesPausedAccount(def, registry.getProfile(def), pausedIdentities)) {
|
|
539
|
+
return def;
|
|
540
|
+
}
|
|
541
|
+
const fallback = registry
|
|
542
|
+
.listProfiles()
|
|
543
|
+
.find((name) => !codexProfileMatchesPausedAccount(name, registry.getProfile(name), pausedIdentities));
|
|
544
|
+
if (fallback)
|
|
545
|
+
return fallback;
|
|
546
|
+
// Fall back to the bare ~/.codex account (DEFAULT_PROFILE) when no registry
|
|
547
|
+
// default is set — it is the default `ccsx` invocation.
|
|
548
|
+
if (fs.existsSync(path.join(os.homedir(), '.codex', 'auth.json')))
|
|
549
|
+
return DEFAULT_PROFILE;
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function getPausedCodexAccountIdentities() {
|
|
557
|
+
try {
|
|
558
|
+
const accounts = (0, query_1.getProviderAccounts)('codex');
|
|
559
|
+
if (accounts.length === 0)
|
|
560
|
+
return null;
|
|
561
|
+
const identities = new Set();
|
|
562
|
+
for (const account of accounts) {
|
|
563
|
+
if (!account.paused)
|
|
564
|
+
continue;
|
|
565
|
+
identities.add(account.id);
|
|
566
|
+
if (account.email)
|
|
567
|
+
identities.add(account.email);
|
|
568
|
+
}
|
|
569
|
+
return identities;
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function codexProfileMatchesPausedAccount(profileName, profile, pausedIdentities) {
|
|
576
|
+
if (!pausedIdentities || pausedIdentities.size === 0)
|
|
577
|
+
return false;
|
|
578
|
+
return [profileName, profile.email, profile.account_id].some((identity) => typeof identity === 'string' && pausedIdentities.has(identity));
|
|
579
|
+
}
|
|
580
|
+
// ============================================================================
|
|
581
|
+
// Per-profile collectors with full safety controls
|
|
582
|
+
// ============================================================================
|
|
583
|
+
/**
|
|
584
|
+
* Tag a row with whether it is the surface's default profile (drives the
|
|
585
|
+
* "active" badge only). The row's own `paused` flag is authoritative for
|
|
586
|
+
* dimming: live/default rows arrive with paused:false, while non-default
|
|
587
|
+
* profiles are served cache-only and dimmed to avoid live-refresh fan-out.
|
|
588
|
+
*/
|
|
589
|
+
function buildParkedClaudeProfileRow(profile, now) {
|
|
590
|
+
return {
|
|
591
|
+
account_id: `${SURFACE_CLAUDE}:${profile}`,
|
|
592
|
+
provider: CLAUDE_NATIVE_PROVIDER,
|
|
593
|
+
surface: SURFACE_CLAUDE,
|
|
594
|
+
profile,
|
|
595
|
+
is_subscription: true,
|
|
596
|
+
displayName: profile,
|
|
597
|
+
tier: null,
|
|
598
|
+
paused: true,
|
|
599
|
+
quota_percentage: null,
|
|
600
|
+
quotaStatus: 'unsupported',
|
|
601
|
+
next_reset: null,
|
|
602
|
+
is_default: false,
|
|
603
|
+
last_activity_at: null,
|
|
604
|
+
today_cost: null,
|
|
605
|
+
health: 'ok',
|
|
606
|
+
cached: false,
|
|
607
|
+
fetchedAt: new Date(now).toISOString(),
|
|
608
|
+
needsReauth: true,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
function buildParkedCodexProfileRow(profile, now) {
|
|
612
|
+
return {
|
|
613
|
+
account_id: `${SURFACE_CODEX}:${profile}`,
|
|
614
|
+
provider: CODEX_NATIVE_PROVIDER,
|
|
615
|
+
surface: SURFACE_CODEX,
|
|
616
|
+
profile,
|
|
617
|
+
is_subscription: true,
|
|
618
|
+
displayName: profile,
|
|
619
|
+
tier: null,
|
|
620
|
+
paused: true,
|
|
621
|
+
quota_percentage: null,
|
|
622
|
+
quotaStatus: 'unsupported',
|
|
623
|
+
next_reset: null,
|
|
624
|
+
is_default: false,
|
|
625
|
+
last_activity_at: null,
|
|
626
|
+
today_cost: null,
|
|
627
|
+
health: 'ok',
|
|
628
|
+
cached: false,
|
|
629
|
+
fetchedAt: new Date(now).toISOString(),
|
|
630
|
+
needsReauth: true,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function collectCachedOrParkedProfileRow(map, profile, now, buildParkedRow) {
|
|
634
|
+
const state = getState(map, profile);
|
|
635
|
+
if (state.cachedRow) {
|
|
636
|
+
return { ...state.cachedRow, cached: true, paused: true };
|
|
637
|
+
}
|
|
638
|
+
const parkedRow = buildParkedRow(profile, now);
|
|
639
|
+
state.cachedRow = parkedRow;
|
|
640
|
+
state.cachedAt = now;
|
|
641
|
+
return parkedRow;
|
|
642
|
+
}
|
|
643
|
+
function markDefault(row, isDefault) {
|
|
644
|
+
return { ...row, is_default: isDefault };
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Tag the row with is_default AND write the flag back onto the cached copy. The
|
|
648
|
+
* collector caches a row before the default profile is known (the default is
|
|
649
|
+
* resolved in the enumerator), so without this the cache-fallback path
|
|
650
|
+
* (getCachedNativeAccountRows) would serve the default account with
|
|
651
|
+
* is_default:false and the UI would stop ordering/tagging it as the default.
|
|
652
|
+
*/
|
|
653
|
+
function markDefaultAndSyncCache(map, profile, row, isDefault) {
|
|
654
|
+
const state = map.get(profile);
|
|
655
|
+
const marked = markDefault(row, isDefault);
|
|
656
|
+
if (state?.cachedRow) {
|
|
657
|
+
state.cachedRow = { ...state.cachedRow, is_default: isDefault };
|
|
658
|
+
}
|
|
659
|
+
return marked;
|
|
660
|
+
}
|
|
661
|
+
async function collectClaudeRowForProfile(profile, deps, force = false) {
|
|
662
|
+
const now = (deps.now ?? Date.now)();
|
|
663
|
+
const state = getState(claudeProfileStates, profile);
|
|
664
|
+
// Serve from cache while within TTL — force bypasses the short-circuit. Parked
|
|
665
|
+
// rows (no creds -> quotaStatus 'unsupported') use a short TTL so a fresh login
|
|
666
|
+
// is picked up within seconds instead of staying dimmed for the full quota TTL.
|
|
667
|
+
if (!force && state.cachedRow) {
|
|
668
|
+
const ttl = state.cachedRow.quotaStatus === 'unsupported' ? PARKED_TTL_MS : NATIVE_QUOTA_TTL_MS;
|
|
669
|
+
if (now - state.cachedAt < ttl)
|
|
670
|
+
return serveCached(state);
|
|
671
|
+
}
|
|
672
|
+
// Breaker open or cooldown active -> zero network, serve stale (may be null).
|
|
673
|
+
// Force does NOT bypass the breaker — it protects the account.
|
|
674
|
+
if (now < state.breakerOpenUntil || now < state.cooldownUntil) {
|
|
675
|
+
return serveCached(state);
|
|
676
|
+
}
|
|
677
|
+
// Coalesce: concurrent callers past TTL share one in-flight fetch.
|
|
678
|
+
if (state.pending) {
|
|
679
|
+
return state.pending;
|
|
680
|
+
}
|
|
681
|
+
// For per-profile reads: use the injected seam (file-only, no keychain).
|
|
682
|
+
const readDefaultCredentials = deps.readCredentials ?? claude_native_credentials_1.readClaudeCredentials;
|
|
683
|
+
const readCreds = deps.readClaudeCredentialsForProfile ??
|
|
684
|
+
((p) => readClaudeCredentialsForProfileFromDisk(p, readDefaultCredentials));
|
|
685
|
+
const fetchQuota = deps.fetchClaudeQuota ?? quota_fetcher_claude_1.fetchClaudeQuotaWithToken;
|
|
686
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
687
|
+
state.pending = (async () => {
|
|
688
|
+
try {
|
|
689
|
+
// Yield once so the `state.pending = (...)()` assignment above completes
|
|
690
|
+
// before any synchronous return below runs the finally that nulls it.
|
|
691
|
+
// Without this, a sync return (e.g. the no-creds parked path) clears
|
|
692
|
+
// pending DURING assignment, leaving a stale resolved promise that the
|
|
693
|
+
// next call's coalescing check would return instead of re-evaluating.
|
|
694
|
+
await Promise.resolve();
|
|
695
|
+
const creds = readCreds(profile);
|
|
696
|
+
// No credentials file found -> emit parked row (needs auth, file absent).
|
|
697
|
+
// This is the expected case when the profile exists in the registry but the
|
|
698
|
+
// user has not logged in via 'ccs auth' for this machine or the credentials
|
|
699
|
+
// are stored only in keychain (which we deliberately do not access here).
|
|
700
|
+
if (!creds) {
|
|
701
|
+
const parkedRow = buildParkedClaudeProfileRow(profile, now);
|
|
702
|
+
// Cache the parked row so repeated calls don't re-stat the fs.
|
|
703
|
+
state.cachedRow = parkedRow;
|
|
704
|
+
state.cachedAt = now;
|
|
705
|
+
return parkedRow;
|
|
706
|
+
}
|
|
707
|
+
// No token / unsupported subscription -> never spend a call, omit the row.
|
|
708
|
+
if (!(0, claude_native_credentials_1.hasSupportedSubscription)(creds)) {
|
|
709
|
+
return serveCached(state);
|
|
710
|
+
}
|
|
711
|
+
const token = (0, claude_native_credentials_1.getAccessToken)(creds);
|
|
712
|
+
if (!token) {
|
|
713
|
+
return serveCached(state);
|
|
714
|
+
}
|
|
715
|
+
const tier = (0, claude_native_credentials_1.getSubscriptionTier)(creds);
|
|
716
|
+
const quota = await fetchQuota(token, `${SURFACE_CLAUDE}:${profile}`);
|
|
717
|
+
if (quota.success) {
|
|
718
|
+
// Success closes the breaker and clears backoff.
|
|
719
|
+
state.consecutive429 = 0;
|
|
720
|
+
state.breakerOpenUntil = 0;
|
|
721
|
+
state.cooldownUntil = 0;
|
|
722
|
+
state.backoffAttempt = 0;
|
|
723
|
+
const row = buildClaudeRow(quota, tier, now, SURFACE_CLAUDE, profile);
|
|
724
|
+
state.cachedRow = row;
|
|
725
|
+
state.cachedAt = now;
|
|
726
|
+
return { ...row, cached: false };
|
|
727
|
+
}
|
|
728
|
+
// 401 -> token expired. Emit a dimmed reauth row so the bar can prompt.
|
|
729
|
+
// Cache it and open a cooldown so the expired account is NOT re-polled
|
|
730
|
+
// (and re-401'd) on every refresh; it re-checks after REAUTH_COOLDOWN_MS,
|
|
731
|
+
// picking up a successful re-auth.
|
|
732
|
+
if (quota.needsReauth) {
|
|
733
|
+
const row = {
|
|
734
|
+
account_id: `${SURFACE_CLAUDE}:${profile}`,
|
|
735
|
+
provider: CLAUDE_NATIVE_PROVIDER,
|
|
736
|
+
surface: SURFACE_CLAUDE,
|
|
737
|
+
profile,
|
|
738
|
+
is_subscription: true,
|
|
739
|
+
displayName: profile,
|
|
740
|
+
tier,
|
|
741
|
+
paused: true,
|
|
742
|
+
quota_percentage: null,
|
|
743
|
+
quotaStatus: 'error',
|
|
744
|
+
next_reset: null,
|
|
745
|
+
is_default: false,
|
|
746
|
+
last_activity_at: null,
|
|
747
|
+
today_cost: null,
|
|
748
|
+
health: 'error',
|
|
749
|
+
cached: false,
|
|
750
|
+
fetchedAt: new Date(now).toISOString(),
|
|
751
|
+
needsReauth: true,
|
|
752
|
+
};
|
|
753
|
+
state.cachedRow = row;
|
|
754
|
+
state.cachedAt = now;
|
|
755
|
+
state.cooldownUntil = now + REAUTH_COOLDOWN_MS;
|
|
756
|
+
return { ...row, cached: false };
|
|
757
|
+
}
|
|
758
|
+
// 429 / 5xx / transient. Apply backoff + breaker, then serve stale.
|
|
759
|
+
const is429 = quota.httpStatus === 429;
|
|
760
|
+
if (is429) {
|
|
761
|
+
state.consecutive429 += 1;
|
|
762
|
+
if (state.consecutive429 >= CB_TRIP_THRESHOLD) {
|
|
763
|
+
state.breakerOpenUntil = now + CB_COOLDOWN_MS;
|
|
764
|
+
}
|
|
765
|
+
const retryAfter = parseRetryAfterMs(quota.errorDetail, now);
|
|
766
|
+
const backoff = retryAfter ?? computeBackoffMs(state.backoffAttempt);
|
|
767
|
+
state.cooldownUntil = now + backoff;
|
|
768
|
+
state.backoffAttempt += 1;
|
|
769
|
+
// We do NOT sleep-then-retry inside the request path (that would burn
|
|
770
|
+
// the request budget). The cooldown gates the NEXT call instead.
|
|
771
|
+
void sleep; // retained as an injectable seam for future inline retry
|
|
772
|
+
}
|
|
773
|
+
else if (quota.retryable) {
|
|
774
|
+
const backoff = computeBackoffMs(state.backoffAttempt);
|
|
775
|
+
state.cooldownUntil = now + backoff;
|
|
776
|
+
state.backoffAttempt += 1;
|
|
777
|
+
}
|
|
778
|
+
// Serve last good row on failure; omit if we never succeeded.
|
|
779
|
+
return serveCached(state);
|
|
780
|
+
}
|
|
781
|
+
catch {
|
|
782
|
+
// Network/parse rejection -> treat as transient, serve stale.
|
|
783
|
+
const backoff = computeBackoffMs(state.backoffAttempt);
|
|
784
|
+
state.cooldownUntil = now + backoff;
|
|
785
|
+
state.backoffAttempt += 1;
|
|
786
|
+
return serveCached(state);
|
|
787
|
+
}
|
|
788
|
+
finally {
|
|
789
|
+
state.pending = null;
|
|
790
|
+
}
|
|
791
|
+
})();
|
|
792
|
+
return state.pending;
|
|
793
|
+
}
|
|
794
|
+
async function collectCodexRowForProfile(profile, deps, force = false) {
|
|
795
|
+
const now = (deps.now ?? Date.now)();
|
|
796
|
+
const state = getState(codexProfileStates, profile);
|
|
797
|
+
// Serve from cache while within TTL — force bypasses the short-circuit. Parked
|
|
798
|
+
// rows (no auth -> quotaStatus 'unsupported') use a short TTL so a fresh login
|
|
799
|
+
// is picked up within seconds instead of staying dimmed for the full quota TTL.
|
|
800
|
+
if (!force && state.cachedRow) {
|
|
801
|
+
const ttl = state.cachedRow.quotaStatus === 'unsupported' ? PARKED_TTL_MS : NATIVE_QUOTA_TTL_MS;
|
|
802
|
+
if (now - state.cachedAt < ttl)
|
|
803
|
+
return serveCached(state);
|
|
804
|
+
}
|
|
805
|
+
// Breaker open or cooldown active -> skip network, go to LOCAL fallback.
|
|
806
|
+
// Force does NOT bypass the breaker — it protects the account.
|
|
807
|
+
const breakerOrCooldownActive = now < state.breakerOpenUntil || now < state.cooldownUntil;
|
|
808
|
+
// Coalesce: concurrent callers past TTL share one in-flight resolution.
|
|
809
|
+
if (state.pending) {
|
|
810
|
+
return state.pending;
|
|
811
|
+
}
|
|
812
|
+
// Resolve the native auth for this profile to get a network accountId.
|
|
813
|
+
const readNativeAuth = deps.readCodexNativeAuth ?? ((p) => readCodexNativeAuthFromDisk(p));
|
|
814
|
+
// For the network fallback: the legacy getDefaultCodexAccountId is the
|
|
815
|
+
// CLIProxy-registry path; for native profiles we use the on-disk auth directly.
|
|
816
|
+
const fetchNetwork = deps.fetchCodexNetworkQuota ?? ((accountId) => (0, quota_fetcher_codex_1.fetchCodexQuota)(accountId));
|
|
817
|
+
const getCodex = deps.getCodexQuota ?? codex_local_quota_collector_1.getCodexLocalQuota;
|
|
818
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
819
|
+
state.pending = (async () => {
|
|
820
|
+
try {
|
|
821
|
+
// Yield once so the `state.pending = (...)()` assignment completes before
|
|
822
|
+
// any synchronous return below runs the finally that nulls it (otherwise a
|
|
823
|
+
// sync return leaves a stale resolved promise the next call would reuse).
|
|
824
|
+
await Promise.resolve();
|
|
825
|
+
// ----------------------------------------------------------------
|
|
826
|
+
// PRIMARY: live network fetch (skipped when breaker/cooldown active)
|
|
827
|
+
// ----------------------------------------------------------------
|
|
828
|
+
if (!breakerOrCooldownActive) {
|
|
829
|
+
const nativeAuth = readNativeAuth(profile);
|
|
830
|
+
// Use the on-disk accountId for the network call; fall through to local
|
|
831
|
+
// when the auth file is absent (parked profile).
|
|
832
|
+
if (nativeAuth) {
|
|
833
|
+
const quota = await fetchNetwork(nativeAuth.accountId || profile);
|
|
834
|
+
if (quota.success) {
|
|
835
|
+
// A healthy response closes the breaker and clears backoff,
|
|
836
|
+
// regardless of content.
|
|
837
|
+
state.consecutive429 = 0;
|
|
838
|
+
state.breakerOpenUntil = 0;
|
|
839
|
+
state.cooldownUntil = 0;
|
|
840
|
+
state.backoffAttempt = 0;
|
|
841
|
+
// At least one core window (5h/weekly) resolved -> full quota row.
|
|
842
|
+
if (quota.coreUsage?.fiveHour || quota.coreUsage?.weekly) {
|
|
843
|
+
const row = buildCodexNetworkRow(quota, now, SURFACE_CODEX, profile);
|
|
844
|
+
state.cachedRow = row;
|
|
845
|
+
state.cachedAt = now;
|
|
846
|
+
return { ...row, cached: false };
|
|
847
|
+
}
|
|
848
|
+
// Success but no core window. The token authenticated, so this is a
|
|
849
|
+
// VALID active subscription with a sparse payload — emit an active
|
|
850
|
+
// (quota-less) row instead of parking it. Only the bare default keeps
|
|
851
|
+
// falling through to the global local session data below.
|
|
852
|
+
if (profile !== DEFAULT_PROFILE) {
|
|
853
|
+
const row = buildCodexNetworkRow(quota, now, SURFACE_CODEX, profile);
|
|
854
|
+
state.cachedRow = row;
|
|
855
|
+
state.cachedAt = now;
|
|
856
|
+
return { ...row, cached: false };
|
|
857
|
+
}
|
|
858
|
+
// else (default): fall through to LOCAL fallback below.
|
|
859
|
+
}
|
|
860
|
+
else if (quota.needsReauth) {
|
|
861
|
+
// Token expired -> dimmed reauth row. Cache it and cool down so the
|
|
862
|
+
// expired account is NOT re-polled (and re-401'd) every refresh; it
|
|
863
|
+
// re-checks after REAUTH_COOLDOWN_MS to pick up a re-auth.
|
|
864
|
+
const reauthRow = {
|
|
865
|
+
account_id: `${SURFACE_CODEX}:${profile}`,
|
|
866
|
+
provider: CODEX_NATIVE_PROVIDER,
|
|
867
|
+
surface: SURFACE_CODEX,
|
|
868
|
+
profile,
|
|
869
|
+
is_subscription: true,
|
|
870
|
+
displayName: profile,
|
|
871
|
+
tier: null,
|
|
872
|
+
paused: true,
|
|
873
|
+
quota_percentage: null,
|
|
874
|
+
quotaStatus: 'error',
|
|
875
|
+
next_reset: null,
|
|
876
|
+
is_default: false,
|
|
877
|
+
last_activity_at: null,
|
|
878
|
+
today_cost: null,
|
|
879
|
+
health: 'error',
|
|
880
|
+
cached: false,
|
|
881
|
+
fetchedAt: new Date(now).toISOString(),
|
|
882
|
+
needsReauth: true,
|
|
883
|
+
};
|
|
884
|
+
state.cachedRow = reauthRow;
|
|
885
|
+
state.cachedAt = now;
|
|
886
|
+
state.cooldownUntil = now + REAUTH_COOLDOWN_MS;
|
|
887
|
+
return { ...reauthRow, cached: false };
|
|
888
|
+
}
|
|
889
|
+
else if (quota.httpStatus === 429) {
|
|
890
|
+
// 429: apply breaker + backoff, then fall through to local.
|
|
891
|
+
state.consecutive429 += 1;
|
|
892
|
+
if (state.consecutive429 >= CB_TRIP_THRESHOLD) {
|
|
893
|
+
state.breakerOpenUntil = now + CB_COOLDOWN_MS;
|
|
894
|
+
}
|
|
895
|
+
const retryAfter = parseRetryAfterMs(quota.errorDetail, now);
|
|
896
|
+
const backoff = retryAfter ?? computeBackoffMs(state.backoffAttempt);
|
|
897
|
+
state.cooldownUntil = now + backoff;
|
|
898
|
+
state.backoffAttempt += 1;
|
|
899
|
+
void sleep; // retained as an injectable seam for future inline retry
|
|
900
|
+
}
|
|
901
|
+
else if (quota.retryable) {
|
|
902
|
+
// Other transient failure: set cooldown, fall through to local.
|
|
903
|
+
const backoff = computeBackoffMs(state.backoffAttempt);
|
|
904
|
+
state.cooldownUntil = now + backoff;
|
|
905
|
+
state.backoffAttempt += 1;
|
|
906
|
+
}
|
|
907
|
+
else {
|
|
908
|
+
// Terminal non-retryable failure (e.g. 403/404): back off so we
|
|
909
|
+
// don't re-hit a dead endpoint every poll when no local data caches
|
|
910
|
+
// a row to engage the TTL short-circuit. Then fall through to local.
|
|
911
|
+
const backoff = computeBackoffMs(state.backoffAttempt);
|
|
912
|
+
state.cooldownUntil = now + backoff;
|
|
913
|
+
state.backoffAttempt += 1;
|
|
914
|
+
}
|
|
915
|
+
// Fall through to LOCAL fallback below.
|
|
916
|
+
}
|
|
917
|
+
// No on-disk auth for this profile -> fall through to the fallback below.
|
|
918
|
+
}
|
|
919
|
+
// ----------------------------------------------------------------
|
|
920
|
+
// LOCAL FALLBACK: session-log read (zero network). The session logs live
|
|
921
|
+
// in the GLOBAL ~/.codex, so they represent ONLY the bare default account,
|
|
922
|
+
// never a named profile. Using them for a named profile would misattribute
|
|
923
|
+
// the default's usage to the profile, so only the default falls back to
|
|
924
|
+
// local; named profiles park instead.
|
|
925
|
+
// ----------------------------------------------------------------
|
|
926
|
+
if (profile === DEFAULT_PROFILE) {
|
|
927
|
+
const localQuota = await getCodex();
|
|
928
|
+
if (localQuota) {
|
|
929
|
+
const row = buildCodexRow(localQuota, now, SURFACE_CODEX, profile);
|
|
930
|
+
state.cachedRow = row;
|
|
931
|
+
state.cachedAt = now;
|
|
932
|
+
return { ...row, cached: false };
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
// Named profile (or default with no local data): serve the last-known row
|
|
936
|
+
// if any, else a dimmed parked row -- never global local data for a named
|
|
937
|
+
// profile.
|
|
938
|
+
const stale = serveCached(state);
|
|
939
|
+
if (stale)
|
|
940
|
+
return stale;
|
|
941
|
+
const parkedRow = buildParkedCodexProfileRow(profile, now);
|
|
942
|
+
state.cachedRow = parkedRow;
|
|
943
|
+
state.cachedAt = now;
|
|
944
|
+
return parkedRow;
|
|
945
|
+
}
|
|
946
|
+
catch {
|
|
947
|
+
// Network/parse rejection -> treat as transient, serve stale.
|
|
948
|
+
const backoff = computeBackoffMs(state.backoffAttempt);
|
|
949
|
+
state.cooldownUntil = now + backoff;
|
|
950
|
+
state.backoffAttempt += 1;
|
|
951
|
+
return serveCached(state);
|
|
952
|
+
}
|
|
953
|
+
finally {
|
|
954
|
+
state.pending = null;
|
|
955
|
+
}
|
|
956
|
+
})();
|
|
957
|
+
return state.pending;
|
|
958
|
+
}
|
|
959
|
+
// ============================================================================
|
|
960
|
+
// Legacy single-profile collectors (unchanged; used by old tests + back-compat)
|
|
961
|
+
// ============================================================================
|
|
962
|
+
/**
|
|
963
|
+
* @deprecated Use collectClaudeRowForProfile with the 'default' or appropriate
|
|
964
|
+
* profile name. Kept for backward compatibility with existing tests that stub
|
|
965
|
+
* readCredentials/getDefaultCodexAccountId directly.
|
|
966
|
+
*/
|
|
307
967
|
async function collectClaudeRow(deps, force = false) {
|
|
308
968
|
const now = (deps.now ?? Date.now)();
|
|
309
|
-
|
|
969
|
+
// Use the legacy single-state approach via profile key '__legacy__' to avoid
|
|
970
|
+
// breaking state isolation with the per-profile maps.
|
|
971
|
+
const state = getState(claudeProfileStates, '__legacy__');
|
|
310
972
|
// Serve from cache while within TTL — force bypasses TTL short-circuit.
|
|
311
973
|
if (!force && state.cachedRow && now - state.cachedAt < NATIVE_QUOTA_TTL_MS) {
|
|
312
974
|
return serveCached(state);
|
|
313
975
|
}
|
|
314
976
|
// Breaker open or cooldown active -> zero network, serve stale (may be null).
|
|
315
|
-
// Force does NOT bypass the breaker — it protects the account.
|
|
316
977
|
if (now < state.breakerOpenUntil || now < state.cooldownUntil) {
|
|
317
978
|
return serveCached(state);
|
|
318
979
|
}
|
|
@@ -320,12 +981,12 @@ async function collectClaudeRow(deps, force = false) {
|
|
|
320
981
|
if (state.pending) {
|
|
321
982
|
return state.pending;
|
|
322
983
|
}
|
|
323
|
-
const
|
|
984
|
+
const readCredentialsFn = deps.readCredentials ?? claude_native_credentials_1.readClaudeCredentials;
|
|
324
985
|
const fetchQuota = deps.fetchClaudeQuota ?? quota_fetcher_claude_1.fetchClaudeQuotaWithToken;
|
|
325
986
|
const sleep = deps.sleep ?? defaultSleep;
|
|
326
987
|
state.pending = (async () => {
|
|
327
988
|
try {
|
|
328
|
-
const creds =
|
|
989
|
+
const creds = readCredentialsFn();
|
|
329
990
|
// No token / unsupported subscription -> never spend a call, omit the row.
|
|
330
991
|
if (!creds || !(0, claude_native_credentials_1.hasSupportedSubscription)(creds)) {
|
|
331
992
|
return serveCached(state);
|
|
@@ -342,13 +1003,12 @@ async function collectClaudeRow(deps, force = false) {
|
|
|
342
1003
|
state.breakerOpenUntil = 0;
|
|
343
1004
|
state.cooldownUntil = 0;
|
|
344
1005
|
state.backoffAttempt = 0;
|
|
345
|
-
const row =
|
|
1006
|
+
const row = buildClaudeRowLegacy(quota, tier, now);
|
|
346
1007
|
state.cachedRow = row;
|
|
347
1008
|
state.cachedAt = now;
|
|
348
1009
|
return { ...row, cached: false };
|
|
349
1010
|
}
|
|
350
|
-
// 401 -> token expired.
|
|
351
|
-
// a real, actionable state distinct from a transient failure.
|
|
1011
|
+
// 401 -> token expired.
|
|
352
1012
|
if (quota.needsReauth) {
|
|
353
1013
|
const row = {
|
|
354
1014
|
account_id: CLAUDE_PROVIDER,
|
|
@@ -367,11 +1027,9 @@ async function collectClaudeRow(deps, force = false) {
|
|
|
367
1027
|
fetchedAt: new Date(now).toISOString(),
|
|
368
1028
|
needsReauth: true,
|
|
369
1029
|
};
|
|
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
1030
|
return row;
|
|
373
1031
|
}
|
|
374
|
-
// 429 / 5xx / transient.
|
|
1032
|
+
// 429 / 5xx / transient.
|
|
375
1033
|
const is429 = quota.httpStatus === 429;
|
|
376
1034
|
if (is429) {
|
|
377
1035
|
state.consecutive429 += 1;
|
|
@@ -382,20 +1040,16 @@ async function collectClaudeRow(deps, force = false) {
|
|
|
382
1040
|
const backoff = retryAfter ?? computeBackoffMs(state.backoffAttempt);
|
|
383
1041
|
state.cooldownUntil = now + backoff;
|
|
384
1042
|
state.backoffAttempt += 1;
|
|
385
|
-
|
|
386
|
-
// the request budget). The cooldown gates the NEXT call instead.
|
|
387
|
-
void sleep; // retained as an injectable seam for future inline retry
|
|
1043
|
+
void sleep;
|
|
388
1044
|
}
|
|
389
1045
|
else if (quota.retryable) {
|
|
390
1046
|
const backoff = computeBackoffMs(state.backoffAttempt);
|
|
391
1047
|
state.cooldownUntil = now + backoff;
|
|
392
1048
|
state.backoffAttempt += 1;
|
|
393
1049
|
}
|
|
394
|
-
// Serve last good row on failure; omit if we never succeeded.
|
|
395
1050
|
return serveCached(state);
|
|
396
1051
|
}
|
|
397
1052
|
catch {
|
|
398
|
-
// Network/parse rejection -> treat as transient, serve stale.
|
|
399
1053
|
const backoff = computeBackoffMs(state.backoffAttempt);
|
|
400
1054
|
state.cooldownUntil = now + backoff;
|
|
401
1055
|
state.backoffAttempt += 1;
|
|
@@ -407,18 +1061,40 @@ async function collectClaudeRow(deps, force = false) {
|
|
|
407
1061
|
})();
|
|
408
1062
|
return state.pending;
|
|
409
1063
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
1064
|
+
/** Legacy row builder — no surface/profile/is_subscription fields. */
|
|
1065
|
+
function buildClaudeRowLegacy(quota, tier, now) {
|
|
1066
|
+
const quotaWindows = buildClaudeQuotaWindows(quota);
|
|
1067
|
+
return {
|
|
1068
|
+
account_id: CLAUDE_PROVIDER,
|
|
1069
|
+
provider: CLAUDE_PROVIDER,
|
|
1070
|
+
displayName: 'Claude Code',
|
|
1071
|
+
tier,
|
|
1072
|
+
paused: false,
|
|
1073
|
+
quota_percentage: deriveClaudeQuotaPercentage(quota),
|
|
1074
|
+
quotaStatus: 'ok',
|
|
1075
|
+
next_reset: deriveClaudeNextReset(quota),
|
|
1076
|
+
is_default: false,
|
|
1077
|
+
last_activity_at: null,
|
|
1078
|
+
today_cost: null,
|
|
1079
|
+
health: 'ok',
|
|
1080
|
+
cached: false,
|
|
1081
|
+
fetchedAt: new Date(now).toISOString(),
|
|
1082
|
+
needsReauth: false,
|
|
1083
|
+
...(quotaWindows.length > 0 ? { quotaWindows } : {}),
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Legacy Codex collector — uses the old getDefaultCodexAccountId dep.
|
|
1088
|
+
* Kept for backward compatibility with existing tests.
|
|
1089
|
+
*/
|
|
413
1090
|
async function collectCodexRow(deps, force = false) {
|
|
414
1091
|
const now = (deps.now ?? Date.now)();
|
|
415
|
-
const state =
|
|
1092
|
+
const state = getState(codexProfileStates, '__legacy__');
|
|
416
1093
|
// Serve from cache while within TTL — force bypasses TTL short-circuit.
|
|
417
1094
|
if (!force && state.cachedRow && now - state.cachedAt < NATIVE_QUOTA_TTL_MS) {
|
|
418
1095
|
return serveCached(state);
|
|
419
1096
|
}
|
|
420
1097
|
// Breaker open or cooldown active -> skip network, go to LOCAL fallback.
|
|
421
|
-
// Force does NOT bypass the breaker — it protects the account.
|
|
422
1098
|
const breakerOrCooldownActive = now < state.breakerOpenUntil || now < state.cooldownUntil;
|
|
423
1099
|
// Coalesce: concurrent callers past TTL share one in-flight resolution.
|
|
424
1100
|
if (state.pending) {
|
|
@@ -430,6 +1106,10 @@ async function collectCodexRow(deps, force = false) {
|
|
|
430
1106
|
const sleep = deps.sleep ?? defaultSleep;
|
|
431
1107
|
state.pending = (async () => {
|
|
432
1108
|
try {
|
|
1109
|
+
// Yield once so the `state.pending = (...)()` assignment completes before
|
|
1110
|
+
// any synchronous return below runs the finally that nulls it (otherwise a
|
|
1111
|
+
// sync return leaves a stale resolved promise the next call would reuse).
|
|
1112
|
+
await Promise.resolve();
|
|
433
1113
|
// ----------------------------------------------------------------
|
|
434
1114
|
// PRIMARY: live network fetch (skipped when breaker/cooldown active)
|
|
435
1115
|
// ----------------------------------------------------------------
|
|
@@ -438,19 +1118,12 @@ async function collectCodexRow(deps, force = false) {
|
|
|
438
1118
|
if (accountId) {
|
|
439
1119
|
const quota = await fetchNetwork(accountId);
|
|
440
1120
|
if (quota.success) {
|
|
441
|
-
// A healthy response closes the breaker and clears backoff,
|
|
442
|
-
// regardless of content.
|
|
443
1121
|
state.consecutive429 = 0;
|
|
444
1122
|
state.breakerOpenUntil = 0;
|
|
445
1123
|
state.cooldownUntil = 0;
|
|
446
1124
|
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
1125
|
if (quota.coreUsage?.fiveHour || quota.coreUsage?.weekly) {
|
|
453
|
-
const row =
|
|
1126
|
+
const row = buildCodexNetworkRowLegacy(quota, now);
|
|
454
1127
|
state.cachedRow = row;
|
|
455
1128
|
state.cachedAt = now;
|
|
456
1129
|
return { ...row, cached: false };
|
|
@@ -458,7 +1131,6 @@ async function collectCodexRow(deps, force = false) {
|
|
|
458
1131
|
// else: fall through to LOCAL fallback below.
|
|
459
1132
|
}
|
|
460
1133
|
else if (quota.needsReauth) {
|
|
461
|
-
// Token expired -> reauth row; do NOT cache as a good value.
|
|
462
1134
|
return {
|
|
463
1135
|
account_id: CODEX_PROVIDER,
|
|
464
1136
|
provider: CODEX_PROVIDER,
|
|
@@ -478,7 +1150,6 @@ async function collectCodexRow(deps, force = false) {
|
|
|
478
1150
|
};
|
|
479
1151
|
}
|
|
480
1152
|
else if (quota.httpStatus === 429) {
|
|
481
|
-
// 429: apply breaker + backoff, then fall through to local.
|
|
482
1153
|
state.consecutive429 += 1;
|
|
483
1154
|
if (state.consecutive429 >= CB_TRIP_THRESHOLD) {
|
|
484
1155
|
state.breakerOpenUntil = now + CB_COOLDOWN_MS;
|
|
@@ -487,42 +1158,34 @@ async function collectCodexRow(deps, force = false) {
|
|
|
487
1158
|
const backoff = retryAfter ?? computeBackoffMs(state.backoffAttempt);
|
|
488
1159
|
state.cooldownUntil = now + backoff;
|
|
489
1160
|
state.backoffAttempt += 1;
|
|
490
|
-
void sleep;
|
|
1161
|
+
void sleep;
|
|
491
1162
|
}
|
|
492
1163
|
else if (quota.retryable) {
|
|
493
|
-
// Other transient failure: set cooldown, fall through to local.
|
|
494
1164
|
const backoff = computeBackoffMs(state.backoffAttempt);
|
|
495
1165
|
state.cooldownUntil = now + backoff;
|
|
496
1166
|
state.backoffAttempt += 1;
|
|
497
1167
|
}
|
|
498
1168
|
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
1169
|
const backoff = computeBackoffMs(state.backoffAttempt);
|
|
503
1170
|
state.cooldownUntil = now + backoff;
|
|
504
1171
|
state.backoffAttempt += 1;
|
|
505
1172
|
}
|
|
506
1173
|
// Fall through to LOCAL fallback below.
|
|
507
1174
|
}
|
|
508
|
-
// No configured accountId -> fall through to local fallback.
|
|
509
1175
|
}
|
|
510
1176
|
// ----------------------------------------------------------------
|
|
511
|
-
// LOCAL FALLBACK
|
|
512
|
-
// when network is unavailable / no accountId / breaker active)
|
|
1177
|
+
// LOCAL FALLBACK
|
|
513
1178
|
// ----------------------------------------------------------------
|
|
514
1179
|
const localQuota = await getCodex();
|
|
515
1180
|
if (localQuota) {
|
|
516
|
-
const row =
|
|
1181
|
+
const row = buildCodexRowLegacy(localQuota, now);
|
|
517
1182
|
state.cachedRow = row;
|
|
518
1183
|
state.cachedAt = now;
|
|
519
1184
|
return { ...row, cached: false };
|
|
520
1185
|
}
|
|
521
|
-
// No local data either: serve stale (may be null).
|
|
522
1186
|
return serveCached(state);
|
|
523
1187
|
}
|
|
524
1188
|
catch {
|
|
525
|
-
// Network/parse rejection -> treat as transient, serve stale.
|
|
526
1189
|
const backoff = computeBackoffMs(state.backoffAttempt);
|
|
527
1190
|
state.cooldownUntil = now + backoff;
|
|
528
1191
|
state.backoffAttempt += 1;
|
|
@@ -534,21 +1197,120 @@ async function collectCodexRow(deps, force = false) {
|
|
|
534
1197
|
})();
|
|
535
1198
|
return state.pending;
|
|
536
1199
|
}
|
|
1200
|
+
/** Legacy Codex row builder (local quota). */
|
|
1201
|
+
function buildCodexRowLegacy(quota, now) {
|
|
1202
|
+
const quotaWindows = buildCodexQuotaWindows(quota);
|
|
1203
|
+
return {
|
|
1204
|
+
account_id: CODEX_PROVIDER,
|
|
1205
|
+
provider: CODEX_PROVIDER,
|
|
1206
|
+
displayName: 'Codex',
|
|
1207
|
+
tier: quota.tier,
|
|
1208
|
+
paused: false,
|
|
1209
|
+
quota_percentage: quota.quotaPercentage,
|
|
1210
|
+
quotaStatus: 'ok',
|
|
1211
|
+
next_reset: quota.nextReset,
|
|
1212
|
+
is_default: false,
|
|
1213
|
+
last_activity_at: null,
|
|
1214
|
+
today_cost: null,
|
|
1215
|
+
health: quota.stale ? 'warning' : 'ok',
|
|
1216
|
+
cached: false,
|
|
1217
|
+
fetchedAt: new Date(now).toISOString(),
|
|
1218
|
+
needsReauth: false,
|
|
1219
|
+
...(quotaWindows.length > 0 ? { quotaWindows } : {}),
|
|
1220
|
+
...(quota.staleAsOf ? { staleAsOf: quota.staleAsOf } : {}),
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
/** Legacy Codex network row builder. */
|
|
1224
|
+
function buildCodexNetworkRowLegacy(quota, now) {
|
|
1225
|
+
const windows = [];
|
|
1226
|
+
const fiveHour = quota.coreUsage?.fiveHour;
|
|
1227
|
+
if (fiveHour) {
|
|
1228
|
+
windows.push({
|
|
1229
|
+
key: 'five_hour',
|
|
1230
|
+
label: '5h',
|
|
1231
|
+
usedPercent: 100 - fiveHour.remainingPercent,
|
|
1232
|
+
remainingPercent: fiveHour.remainingPercent,
|
|
1233
|
+
resetAt: fiveHour.resetAt,
|
|
1234
|
+
windowMinutes: FIVE_HOUR_MINUTES,
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
const weekly = quota.coreUsage?.weekly;
|
|
1238
|
+
if (weekly) {
|
|
1239
|
+
windows.push({
|
|
1240
|
+
key: 'seven_day',
|
|
1241
|
+
label: 'week',
|
|
1242
|
+
usedPercent: 100 - weekly.remainingPercent,
|
|
1243
|
+
remainingPercent: weekly.remainingPercent,
|
|
1244
|
+
resetAt: weekly.resetAt,
|
|
1245
|
+
windowMinutes: SEVEN_DAY_MINUTES,
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
const coreWindows = [fiveHour, weekly].filter((w) => !!w);
|
|
1249
|
+
const quotaPercentage = coreWindows.length > 0 ? Math.min(...coreWindows.map((w) => w.remainingPercent)) : null;
|
|
1250
|
+
const resets = coreWindows
|
|
1251
|
+
.map((w) => w.resetAt)
|
|
1252
|
+
.filter((r) => typeof r === 'string')
|
|
1253
|
+
.map((r) => ({ iso: r, ms: new Date(r).getTime() }))
|
|
1254
|
+
.filter((r) => Number.isFinite(r.ms))
|
|
1255
|
+
.sort((a, b) => a.ms - b.ms);
|
|
1256
|
+
const nextReset = resets.length > 0 ? resets[0].iso : null;
|
|
1257
|
+
return {
|
|
1258
|
+
account_id: CODEX_PROVIDER,
|
|
1259
|
+
provider: CODEX_PROVIDER,
|
|
1260
|
+
displayName: 'Codex',
|
|
1261
|
+
tier: quota.planType ?? null,
|
|
1262
|
+
paused: false,
|
|
1263
|
+
quota_percentage: quotaPercentage,
|
|
1264
|
+
quotaStatus: 'ok',
|
|
1265
|
+
next_reset: nextReset,
|
|
1266
|
+
is_default: false,
|
|
1267
|
+
last_activity_at: null,
|
|
1268
|
+
today_cost: null,
|
|
1269
|
+
health: 'ok',
|
|
1270
|
+
cached: false,
|
|
1271
|
+
fetchedAt: new Date(now).toISOString(),
|
|
1272
|
+
needsReauth: false,
|
|
1273
|
+
...(windows.length > 0 ? { quotaWindows: windows } : {}),
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
537
1276
|
// ============================================================================
|
|
538
1277
|
// Public entry point
|
|
539
1278
|
// ============================================================================
|
|
540
1279
|
/**
|
|
541
|
-
* Build the native subscription rows
|
|
1280
|
+
* Build the native subscription rows for /summary.
|
|
1281
|
+
*
|
|
1282
|
+
* When profile-enumeration deps are injected (listClaudeProfiles / listCodexProfiles
|
|
1283
|
+
* etc.), all profiles are enumerated and the active/default profile is live-polled
|
|
1284
|
+
* while non-default profiles are cache-only (parked). This keeps the 2.5s deadline:
|
|
1285
|
+
* at most 2 live upstream calls per /summary regardless of profile count.
|
|
542
1286
|
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
1287
|
+
* When no enumeration deps are injected (legacy mode / old tests that stub only
|
|
1288
|
+
* readCredentials + getDefaultCodexAccountId), the old single-profile collectors
|
|
1289
|
+
* are used for backward compatibility.
|
|
545
1290
|
*
|
|
546
|
-
* `opts.force` bypasses the TTL short-circuit on
|
|
547
|
-
*
|
|
548
|
-
* respected regardless of force (account protection).
|
|
1291
|
+
* `opts.force` bypasses the TTL short-circuit on the ACTIVE profile. The circuit
|
|
1292
|
+
* breaker is always respected regardless of force (account protection).
|
|
549
1293
|
*/
|
|
550
1294
|
async function getNativeAccountRows(deps = {}, opts) {
|
|
551
1295
|
const force = opts?.force ?? false;
|
|
1296
|
+
// Multi-profile enumeration is the DEFAULT (production) behavior. The legacy
|
|
1297
|
+
// single-profile path is retained ONLY for backward-compat with old tests that
|
|
1298
|
+
// inject readCredentials / getDefaultCodexAccountId and assert the original
|
|
1299
|
+
// single-row output. Those harnesses never inject the enumeration seams; the
|
|
1300
|
+
// new multi-profile tests pair both seams, and production injects neither — so
|
|
1301
|
+
// everything except the legacy harness takes the multi-profile path below.
|
|
1302
|
+
const hasProfileEnumeration = deps.listClaudeProfiles !== undefined ||
|
|
1303
|
+
deps.listCodexProfiles !== undefined ||
|
|
1304
|
+
deps.defaultClaudeProfile !== undefined ||
|
|
1305
|
+
deps.defaultCodexProfile !== undefined;
|
|
1306
|
+
const isLegacyTestHarness = !hasProfileEnumeration &&
|
|
1307
|
+
(deps.readCredentials !== undefined || deps.getDefaultCodexAccountId !== undefined);
|
|
1308
|
+
if (!isLegacyTestHarness) {
|
|
1309
|
+
return getNativeAccountRowsMultiProfile(deps, force);
|
|
1310
|
+
}
|
|
1311
|
+
// Legacy path: backward-compatible with old tests that only inject
|
|
1312
|
+
// readCredentials / getDefaultCodexAccountId. Produces the old single-row
|
|
1313
|
+
// output (account_id='claude-code'/'codex', no surface/profile).
|
|
552
1314
|
const [claude, codex] = await Promise.all([
|
|
553
1315
|
collectClaudeRow(deps, force).catch(() => null),
|
|
554
1316
|
collectCodexRow(deps, force).catch(() => null),
|
|
@@ -561,6 +1323,79 @@ async function getNativeAccountRows(deps = {}, opts) {
|
|
|
561
1323
|
return rows;
|
|
562
1324
|
}
|
|
563
1325
|
exports.getNativeAccountRows = getNativeAccountRows;
|
|
1326
|
+
async function getNativeAccountRowsMultiProfile(deps, force) {
|
|
1327
|
+
const listClaude = deps.listClaudeProfiles ?? listClaudeProfilesFromDisk;
|
|
1328
|
+
const listCodex = deps.listCodexProfiles ?? listCodexProfilesFromDisk;
|
|
1329
|
+
const defaultClaude = deps.defaultClaudeProfile ?? getDefaultClaudeProfileFromDisk;
|
|
1330
|
+
const defaultCodex = deps.defaultCodexProfile ?? getDefaultCodexProfileFromDisk;
|
|
1331
|
+
const claudeProfiles = (() => {
|
|
1332
|
+
try {
|
|
1333
|
+
return listClaude();
|
|
1334
|
+
}
|
|
1335
|
+
catch {
|
|
1336
|
+
return [];
|
|
1337
|
+
}
|
|
1338
|
+
})();
|
|
1339
|
+
const codexProfiles = (() => {
|
|
1340
|
+
try {
|
|
1341
|
+
return listCodex();
|
|
1342
|
+
}
|
|
1343
|
+
catch {
|
|
1344
|
+
return [];
|
|
1345
|
+
}
|
|
1346
|
+
})();
|
|
1347
|
+
const claudeDefault = (() => {
|
|
1348
|
+
try {
|
|
1349
|
+
return defaultClaude();
|
|
1350
|
+
}
|
|
1351
|
+
catch {
|
|
1352
|
+
return null;
|
|
1353
|
+
}
|
|
1354
|
+
})();
|
|
1355
|
+
const codexDefault = (() => {
|
|
1356
|
+
try {
|
|
1357
|
+
return defaultCodex();
|
|
1358
|
+
}
|
|
1359
|
+
catch {
|
|
1360
|
+
return null;
|
|
1361
|
+
}
|
|
1362
|
+
})();
|
|
1363
|
+
const tasks = [];
|
|
1364
|
+
const results = [];
|
|
1365
|
+
const now = (deps.now ?? Date.now)();
|
|
1366
|
+
// Preserve the safety budget: only the active/default profile for each surface
|
|
1367
|
+
// may perform a live refresh. Non-default profiles are cache-only (or parked)
|
|
1368
|
+
// so one /summary request can trigger at most one Claude and one Codex live
|
|
1369
|
+
// upstream call regardless of configured profile count.
|
|
1370
|
+
for (const p of claudeProfiles) {
|
|
1371
|
+
const isDefault = p === claudeDefault;
|
|
1372
|
+
if (!isDefault) {
|
|
1373
|
+
results.push(markDefaultAndSyncCache(claudeProfileStates, p, collectCachedOrParkedProfileRow(claudeProfileStates, p, now, buildParkedClaudeProfileRow), false));
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
tasks.push(collectClaudeRowForProfile(p, deps, force || claudeProfileStates.get(p)?.cachedRow?.quotaStatus === 'unsupported')
|
|
1377
|
+
.then((r) => (r ? markDefaultAndSyncCache(claudeProfileStates, p, r, true) : null))
|
|
1378
|
+
.catch(() => null));
|
|
1379
|
+
}
|
|
1380
|
+
for (const p of codexProfiles) {
|
|
1381
|
+
const isDefault = p === codexDefault;
|
|
1382
|
+
if (!isDefault) {
|
|
1383
|
+
results.push(markDefaultAndSyncCache(codexProfileStates, p, collectCachedOrParkedProfileRow(codexProfileStates, p, now, buildParkedCodexProfileRow), false));
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
tasks.push(collectCodexRowForProfile(p, deps, force || codexProfileStates.get(p)?.cachedRow?.quotaStatus === 'unsupported')
|
|
1387
|
+
.then((r) => (r ? markDefaultAndSyncCache(codexProfileStates, p, r, true) : null))
|
|
1388
|
+
.catch(() => null));
|
|
1389
|
+
}
|
|
1390
|
+
results.push(...(await Promise.all(tasks)));
|
|
1391
|
+
const rows = results.filter((r) => r !== null);
|
|
1392
|
+
// Sort by (surface, profile) for stable ordering.
|
|
1393
|
+
return rows.sort((a, b) => {
|
|
1394
|
+
const sa = (a.surface ?? '') + ':' + (a.profile ?? '');
|
|
1395
|
+
const sb = (b.surface ?? '') + ':' + (b.profile ?? '');
|
|
1396
|
+
return sa.localeCompare(sb);
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
564
1399
|
/**
|
|
565
1400
|
* Last-known native rows from cache, WITHOUT any fetch (instant, no network).
|
|
566
1401
|
*
|
|
@@ -571,10 +1406,14 @@ exports.getNativeAccountRows = getNativeAccountRows;
|
|
|
571
1406
|
*/
|
|
572
1407
|
function getCachedNativeAccountRows() {
|
|
573
1408
|
const rows = [];
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
1409
|
+
for (const state of claudeProfileStates.values()) {
|
|
1410
|
+
if (state.cachedRow)
|
|
1411
|
+
rows.push({ ...state.cachedRow, cached: true });
|
|
1412
|
+
}
|
|
1413
|
+
for (const state of codexProfileStates.values()) {
|
|
1414
|
+
if (state.cachedRow)
|
|
1415
|
+
rows.push({ ...state.cachedRow, cached: true });
|
|
1416
|
+
}
|
|
578
1417
|
return rows;
|
|
579
1418
|
}
|
|
580
1419
|
exports.getCachedNativeAccountRows = getCachedNativeAccountRows;
|