mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-05 10:13:12 +00:00
fix: cap native quota profile refresh fan-out (#1627)
* fix: cap native quota profile refresh fan-out * fix: preserve native quota live cache state
This commit is contained in:
1 parent
f2cd076c08
commit
0f9a87619c
2 files changed
+168
-78
No files matched your search
@@ -710,12 +710,72 @@ function codexProfileMatchesPausedAccount(
|
||||
/**
|
||||
* Tag a row with whether it is the surface's default profile (drives the
|
||||
* "active" badge only). The row's own `paused` flag is authoritative for
|
||||
* dimming: it is already set by the collector to reflect LIVENESS — parked
|
||||
* (no on-disk creds / unsupported) rows arrive with paused:true; profiles with
|
||||
* a usable token arrive with paused:false regardless of default status. We must
|
||||
* NOT override `paused` from `isDefault`, or a valid non-default subscription
|
||||
* (e.g. an isolated `ccsx` profile) would render dimmed despite live quota.
|
||||
* dimming: live/default rows arrive with paused:false, while non-default
|
||||
* profiles are served cache-only and dimmed to avoid live-refresh fan-out.
|
||||
*/
|
||||
|
||||
function buildParkedClaudeProfileRow(profile: string, now: number): BarSummaryRow {
|
||||
return {
|
||||
account_id: `${SURFACE_CLAUDE}:${profile}`,
|
||||
provider: CLAUDE_NATIVE_PROVIDER,
|
||||
surface: SURFACE_CLAUDE,
|
||||
profile,
|
||||
is_subscription: true,
|
||||
displayName: profile,
|
||||
tier: null,
|
||||
paused: true,
|
||||
quota_percentage: null,
|
||||
quotaStatus: 'unsupported',
|
||||
next_reset: null,
|
||||
is_default: false,
|
||||
last_activity_at: null,
|
||||
today_cost: null,
|
||||
health: 'ok',
|
||||
cached: false,
|
||||
fetchedAt: new Date(now).toISOString(),
|
||||
needsReauth: true,
|
||||
};
|
||||
}
|
||||
|
||||
function buildParkedCodexProfileRow(profile: string, now: number): BarSummaryRow {
|
||||
return {
|
||||
account_id: `${SURFACE_CODEX}:${profile}`,
|
||||
provider: CODEX_NATIVE_PROVIDER,
|
||||
surface: SURFACE_CODEX,
|
||||
profile,
|
||||
is_subscription: true,
|
||||
displayName: profile,
|
||||
tier: null,
|
||||
paused: true,
|
||||
quota_percentage: null,
|
||||
quotaStatus: 'unsupported',
|
||||
next_reset: null,
|
||||
is_default: false,
|
||||
last_activity_at: null,
|
||||
today_cost: null,
|
||||
health: 'ok',
|
||||
cached: false,
|
||||
fetchedAt: new Date(now).toISOString(),
|
||||
needsReauth: true,
|
||||
};
|
||||
}
|
||||
|
||||
function collectCachedOrParkedProfileRow(
|
||||
map: Map<string, ProviderState>,
|
||||
profile: string,
|
||||
now: number,
|
||||
buildParkedRow: (profile: string, now: number) => BarSummaryRow
|
||||
): BarSummaryRow {
|
||||
const state = getState(map, profile);
|
||||
if (state.cachedRow) {
|
||||
return { ...state.cachedRow, cached: true, paused: true };
|
||||
}
|
||||
const parkedRow = buildParkedRow(profile, now);
|
||||
state.cachedRow = parkedRow;
|
||||
state.cachedAt = now;
|
||||
return parkedRow;
|
||||
}
|
||||
|
||||
function markDefault(row: BarSummaryRow, isDefault: boolean): BarSummaryRow {
|
||||
return { ...row, is_default: isDefault };
|
||||
}
|
||||
@@ -734,10 +794,11 @@ function markDefaultAndSyncCache(
|
||||
isDefault: boolean
|
||||
): BarSummaryRow {
|
||||
const state = map.get(profile);
|
||||
const marked = markDefault(row, isDefault);
|
||||
if (state?.cachedRow) {
|
||||
state.cachedRow = { ...state.cachedRow, is_default: isDefault };
|
||||
}
|
||||
return markDefault(row, isDefault);
|
||||
return marked;
|
||||
}
|
||||
|
||||
async function collectClaudeRowForProfile(
|
||||
@@ -789,26 +850,7 @@ async function collectClaudeRowForProfile(
|
||||
// user has not logged in via 'ccs auth' for this machine or the credentials
|
||||
// are stored only in keychain (which we deliberately do not access here).
|
||||
if (!creds) {
|
||||
const parkedRow: BarSummaryRow = {
|
||||
account_id: `${SURFACE_CLAUDE}:${profile}`,
|
||||
provider: CLAUDE_NATIVE_PROVIDER,
|
||||
surface: SURFACE_CLAUDE,
|
||||
profile,
|
||||
is_subscription: true,
|
||||
displayName: profile,
|
||||
tier: null,
|
||||
paused: true,
|
||||
quota_percentage: null,
|
||||
quotaStatus: 'unsupported',
|
||||
next_reset: null,
|
||||
is_default: false,
|
||||
last_activity_at: null,
|
||||
today_cost: null,
|
||||
health: 'ok',
|
||||
cached: false,
|
||||
fetchedAt: new Date(now).toISOString(),
|
||||
needsReauth: true,
|
||||
};
|
||||
const parkedRow = buildParkedClaudeProfileRow(profile, now);
|
||||
// Cache the parked row so repeated calls don't re-stat the fs.
|
||||
state.cachedRow = parkedRow;
|
||||
state.cachedAt = now;
|
||||
@@ -1062,26 +1104,7 @@ async function collectCodexRowForProfile(
|
||||
// profile.
|
||||
const stale = serveCached(state);
|
||||
if (stale) return stale;
|
||||
const parkedRow: BarSummaryRow = {
|
||||
account_id: `${SURFACE_CODEX}:${profile}`,
|
||||
provider: CODEX_NATIVE_PROVIDER,
|
||||
surface: SURFACE_CODEX,
|
||||
profile,
|
||||
is_subscription: true,
|
||||
displayName: profile,
|
||||
tier: null,
|
||||
paused: true,
|
||||
quota_percentage: null,
|
||||
quotaStatus: 'unsupported',
|
||||
next_reset: null,
|
||||
is_default: false,
|
||||
last_activity_at: null,
|
||||
today_cost: null,
|
||||
health: 'ok',
|
||||
cached: false,
|
||||
fetchedAt: new Date(now).toISOString(),
|
||||
needsReauth: true,
|
||||
};
|
||||
const parkedRow = buildParkedCodexProfileRow(profile, now);
|
||||
state.cachedRow = parkedRow;
|
||||
state.cachedAt = now;
|
||||
return parkedRow;
|
||||
@@ -1545,30 +1568,62 @@ async function getNativeAccountRowsMultiProfile(
|
||||
})();
|
||||
|
||||
const tasks: Promise<BarSummaryRow | null>[] = [];
|
||||
const results: (BarSummaryRow | null)[] = [];
|
||||
const now = (deps.now ?? Date.now)();
|
||||
|
||||
// Forced refresh applies to every profile: parked profiles (no creds) short-
|
||||
// circuit to a parked row with zero network, so forcing them is free, while
|
||||
// every profile that has a usable token gets live quota — not just the
|
||||
// default. Per-profile TTL + breaker still protect each account.
|
||||
// Preserve the safety budget: only the active/default profile for each surface
|
||||
// may perform a live refresh. Non-default profiles are cache-only (or parked)
|
||||
// so one /summary request can trigger at most one Claude and one Codex live
|
||||
// upstream call regardless of configured profile count.
|
||||
for (const p of claudeProfiles) {
|
||||
const isDefault = p === claudeDefault;
|
||||
if (!isDefault) {
|
||||
results.push(
|
||||
markDefaultAndSyncCache(
|
||||
claudeProfileStates,
|
||||
p,
|
||||
collectCachedOrParkedProfileRow(claudeProfileStates, p, now, buildParkedClaudeProfileRow),
|
||||
false
|
||||
)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
tasks.push(
|
||||
collectClaudeRowForProfile(p, deps, force)
|
||||
.then((r) => (r ? markDefaultAndSyncCache(claudeProfileStates, p, r, isDefault) : null))
|
||||
collectClaudeRowForProfile(
|
||||
p,
|
||||
deps,
|
||||
force || claudeProfileStates.get(p)?.cachedRow?.quotaStatus === 'unsupported'
|
||||
)
|
||||
.then((r) => (r ? markDefaultAndSyncCache(claudeProfileStates, p, r, true) : null))
|
||||
.catch(() => null)
|
||||
);
|
||||
}
|
||||
|
||||
for (const p of codexProfiles) {
|
||||
const isDefault = p === codexDefault;
|
||||
if (!isDefault) {
|
||||
results.push(
|
||||
markDefaultAndSyncCache(
|
||||
codexProfileStates,
|
||||
p,
|
||||
collectCachedOrParkedProfileRow(codexProfileStates, p, now, buildParkedCodexProfileRow),
|
||||
false
|
||||
)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
tasks.push(
|
||||
collectCodexRowForProfile(p, deps, force)
|
||||
.then((r) => (r ? markDefaultAndSyncCache(codexProfileStates, p, r, isDefault) : null))
|
||||
collectCodexRowForProfile(
|
||||
p,
|
||||
deps,
|
||||
force || codexProfileStates.get(p)?.cachedRow?.quotaStatus === 'unsupported'
|
||||
)
|
||||
.then((r) => (r ? markDefaultAndSyncCache(codexProfileStates, p, r, true) : null))
|
||||
.catch(() => null)
|
||||
);
|
||||
}
|
||||
|
||||
const results = await Promise.all(tasks);
|
||||
results.push(...(await Promise.all(tasks)));
|
||||
const rows = results.filter((r): r is BarSummaryRow => r !== null);
|
||||
|
||||
// Sort by (surface, profile) for stable ordering.
|
||||
|
||||
@@ -989,7 +989,7 @@ describe('getCachedNativeAccountRows (instant, no-fetch fallback)', () => {
|
||||
*
|
||||
* - claudeProfiles: profile names for the Claude surface (ccs)
|
||||
* - codexProfiles: profile names for the Codex surface (ccsx)
|
||||
* - claudeDefault / codexDefault: the active profile per surface (paused:false)
|
||||
* - claudeDefault / codexDefault: the only live-polled profile per surface (paused:false)
|
||||
* - credsForProfile: map from profile name to credentials (null = parked)
|
||||
* - claudeFetch: network fetcher for Claude (all profiles share one implementation)
|
||||
* - codexNativeAuth: map from profile name to {accessToken, accountId}
|
||||
@@ -1113,13 +1113,13 @@ describe('multi-profile: account_id and wire fields', () => {
|
||||
expect(ck?.is_subscription).toBe(true);
|
||||
});
|
||||
|
||||
it('paused reflects liveness (creds present), NOT default-ness; is_default marks the default independently', async () => {
|
||||
it('only default profiles are live-polled; is_default marks the default independently', async () => {
|
||||
const clock = { now: 1_000_000 };
|
||||
const deps = makeMultiProfileDeps({
|
||||
clock,
|
||||
// Claude: work = default + creds (live); ck = non-default + NO creds (parked).
|
||||
claudeProfiles: ['work', 'ck'],
|
||||
// Codex: personal = default + creds (live); ck = NON-default + creds (live).
|
||||
// Codex: personal = default + creds (live); ck = NON-default + creds (cache-only).
|
||||
codexProfiles: ['personal', 'ck'],
|
||||
claudeDefault: 'work',
|
||||
codexDefault: 'personal',
|
||||
@@ -1146,11 +1146,11 @@ describe('multi-profile: account_id and wire fields', () => {
|
||||
expect(codexPersonal?.paused).toBe(false);
|
||||
expect(codexPersonal?.is_default).toBe(true);
|
||||
|
||||
// Codex ck: NON-default but HAS creds -> LIVE, NOT dimmed. This is the key
|
||||
// correctness guarantee: a valid isolated subscription is never dimmed just
|
||||
// because it is not the surface default.
|
||||
// Codex ck: NON-default with creds is cache-only/parked. Only the default
|
||||
// profile is live-polled so a dashboard request cannot fan out to every
|
||||
// configured subscription account.
|
||||
const codexCk = rows.find((r) => r.surface === 'ccsx' && r.profile === 'ck');
|
||||
expect(codexCk?.paused).toBe(false);
|
||||
expect(codexCk?.paused).toBe(true);
|
||||
expect(codexCk?.is_default).toBe(false);
|
||||
});
|
||||
|
||||
@@ -1170,6 +1170,8 @@ describe('multi-profile: account_id and wire fields', () => {
|
||||
|
||||
const rows = await getNativeAccountRows(deps);
|
||||
expect(rows.length).toBe(claudeProfiles.length + codexProfiles.length);
|
||||
expect(deps.claudeFetchCount()).toBe(1);
|
||||
expect(deps.codexNetworkCount()).toBe(1);
|
||||
});
|
||||
|
||||
it('rows are sorted by (surface, profile)', async () => {
|
||||
@@ -1264,7 +1266,7 @@ describe('multi-profile: per-profile circuit breaker isolation', () => {
|
||||
claudeDefault: 'work',
|
||||
credsForProfile: () => maxCreds(),
|
||||
claudeFetch: async (_token, accountId) => {
|
||||
// 'work' (ccs:work) always 429s; 'ck' always succeeds
|
||||
// 'work' (ccs:work) always 429s; 'ck' remains cache-only
|
||||
if (accountId?.includes('work') && workFails) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -1294,8 +1296,8 @@ describe('multi-profile: per-profile circuit breaker isolation', () => {
|
||||
});
|
||||
}
|
||||
|
||||
// After the three 429s on 'work', check that 'ck' still succeeds.
|
||||
// We reset state to have a clean run where 'ck' has no prior breaker history.
|
||||
// After the three 429s on 'work', check that 'ck' remains cache-only.
|
||||
// We reset state to have a clean run with no prior breaker history.
|
||||
resetNativeQuotaState();
|
||||
clock.now += MAX_COOLDOWN_JUMP_MP;
|
||||
workFails = false;
|
||||
@@ -1304,8 +1306,8 @@ describe('multi-profile: per-profile circuit breaker isolation', () => {
|
||||
const ckRow = rows.find((r) => r.profile === 'ck');
|
||||
const workRow = rows.find((r) => r.profile === 'work');
|
||||
|
||||
// 'ck' should succeed — its breaker was never tripped.
|
||||
expect(ckRow?.quotaStatus).toBe('ok');
|
||||
// 'ck' stays cache-only, independent of work's breaker.
|
||||
expect(ckRow?.quotaStatus).toBe('unsupported');
|
||||
// 'work' is also fine after reset (no breaker state).
|
||||
expect(workRow?.quotaStatus).toBe('ok');
|
||||
});
|
||||
@@ -1339,10 +1341,10 @@ describe('multi-profile: per-profile circuit breaker isolation', () => {
|
||||
},
|
||||
});
|
||||
|
||||
// First call: 'work' gets a 429, 'ck' succeeds.
|
||||
// First call: 'work' gets a 429, 'ck' remains cache-only.
|
||||
const rows1 = await getNativeAccountRows(deps);
|
||||
const ck1 = rows1.find((r) => r.profile === 'ck');
|
||||
expect(ck1?.quotaStatus).toBe('ok');
|
||||
expect(ck1?.quotaStatus).toBe('unsupported');
|
||||
expect(workCall429Count).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Skip past cooldown for 'work' only; 'ck' is within TTL.
|
||||
@@ -1351,8 +1353,8 @@ describe('multi-profile: per-profile circuit breaker isolation', () => {
|
||||
// Second call past 'work' cooldown: work tries again (429 again); ck cached.
|
||||
const rows2 = await getNativeAccountRows(deps);
|
||||
const ck2 = rows2.find((r) => r.profile === 'ck');
|
||||
// 'ck' still has a good cached row.
|
||||
expect(ck2?.quotaStatus).toBe('ok');
|
||||
// 'ck' remains cache-only and is not affected by work's breaker.
|
||||
expect(ck2?.quotaStatus).toBe('unsupported');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1449,11 +1451,11 @@ describe('review focus areas: reauth caching + codex local fallback', () => {
|
||||
const r1 = first.find((r) => r.profile === 'ck');
|
||||
expect(r1?.needsReauth).toBe(true);
|
||||
expect(r1?.paused).toBe(true);
|
||||
expect(deps.codexNetworkCount()).toBe(1);
|
||||
expect(deps.codexNetworkCount()).toBe(0);
|
||||
|
||||
const second = await getNativeAccountRows(deps, { force: true });
|
||||
expect(second.find((r) => r.profile === 'ck')?.cached).toBe(true);
|
||||
expect(deps.codexNetworkCount()).toBe(1);
|
||||
expect(deps.codexNetworkCount()).toBe(0);
|
||||
});
|
||||
|
||||
it('Codex named profile without on-disk auth is parked, never filled from global local data', async () => {
|
||||
@@ -1563,7 +1565,7 @@ describe('review focus areas: reauth caching + codex local fallback', () => {
|
||||
expect(deps.claudeFetchCount()).toBe(1); // re-checked -> fetched
|
||||
});
|
||||
|
||||
it('Codex named profile with valid auth but sparse payload stays active, not parked', async () => {
|
||||
it('Codex named profile with valid auth but sparse payload stays parked when not default', async () => {
|
||||
resetNativeQuotaState();
|
||||
const clock = { now: 7_000_000 };
|
||||
const deps = makeMultiProfileDeps({
|
||||
@@ -1579,9 +1581,42 @@ describe('review focus areas: reauth caching + codex local fallback', () => {
|
||||
const rows = await getNativeAccountRows(deps);
|
||||
const ck = rows.find((r) => r.profile === 'ck');
|
||||
expect(ck).toBeDefined();
|
||||
expect(ck?.paused).toBe(false); // active subscription, not parked
|
||||
expect(ck?.needsReauth).toBe(false);
|
||||
expect(ck?.quotaStatus).toBe('ok');
|
||||
expect(ck?.quota_percentage).toBeNull(); // no windows, but still active
|
||||
expect(ck?.paused).toBe(true); // cache-only, parked until selected as default
|
||||
expect(ck?.needsReauth).toBe(true);
|
||||
expect(ck?.quotaStatus).toBe('unsupported');
|
||||
expect(ck?.quota_percentage).toBeNull(); // no windows while parked
|
||||
});
|
||||
|
||||
it('non-default cache-only rows do not overwrite the canonical live cache', async () => {
|
||||
resetNativeQuotaState();
|
||||
const clock = { now: 8_000_000 };
|
||||
let codexDefault = 'ck';
|
||||
const deps = makeMultiProfileDeps({
|
||||
clock,
|
||||
claudeProfiles: [],
|
||||
codexProfiles: ['default', 'ck'],
|
||||
codexDefault,
|
||||
codexNativeAuth: (p) => ({ accessToken: `t-${p}`, accountId: `id-${p}` }),
|
||||
codexNetworkFetch: async () => codexSuccessQuota(),
|
||||
});
|
||||
deps.defaultCodexProfile = () => codexDefault;
|
||||
|
||||
const first = await getNativeAccountRows(deps);
|
||||
expect(first.find((r) => r.profile === 'ck')?.paused).toBe(false);
|
||||
expect(deps.codexNetworkCount()).toBe(1);
|
||||
|
||||
codexDefault = 'default';
|
||||
const second = await getNativeAccountRows(deps);
|
||||
const ckAsNonDefault = second.find((r) => r.profile === 'ck');
|
||||
expect(ckAsNonDefault?.cached).toBe(true);
|
||||
expect(ckAsNonDefault?.paused).toBe(true);
|
||||
expect(deps.codexNetworkCount()).toBe(2);
|
||||
|
||||
codexDefault = 'ck';
|
||||
const third = await getNativeAccountRows(deps);
|
||||
const ckDefaultAgain = third.find((r) => r.profile === 'ck');
|
||||
expect(ckDefaultAgain?.cached).toBe(true);
|
||||
expect(ckDefaultAgain?.paused).toBe(false);
|
||||
expect(deps.codexNetworkCount()).toBe(2);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user