mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-06 08:13:49 +00:00
fix(bar): short-TTL parked rows + keep valid codex subs active
Addresses two review focus areas: - Stale Parked Rows: profiles with no on-disk credentials cached their parked row for the full quota TTL, so a fresh login stayed dimmed for up to 10 min. Parked rows (quotaStatus 'unsupported') now use a 30s TTL so a new login is picked up within seconds. - Codex Fallback: a named codex profile whose token authenticated but whose response lacked core windows was downgraded to a parked/needsAuth row, hiding a real subscription. It now emits an active (quota-less) row; only the bare default still falls back to global local session data. Also fixes a latent coalescing bug the short TTL exposed: a synchronous return inside the pending IIFE cleared state.pending during its own assignment, leaving a stale resolved promise that the next call reused. The IIFE now yields once so the assignment lands before the finally clears it.
This commit is contained in:
1 parent
7b610f0e82
commit
b514986ddd
2 files changed
+94
-8
No files matched your search
@@ -64,6 +64,11 @@ const NATIVE_QUOTA_TTL_MS = 600_000; // 10 minutes
|
||||
// an expired account is shown dimmed and re-checked at most this often instead
|
||||
// of being re-polled (and re-401'd) on every /summary refresh.
|
||||
const REAUTH_COOLDOWN_MS = NATIVE_QUOTA_TTL_MS; // 10 minutes
|
||||
// Parked rows (no on-disk creds, quotaStatus 'unsupported') re-check on a short
|
||||
// TTL — re-statting credentials is cheap, and this lets a profile that the user
|
||||
// just logged into appear within seconds instead of staying dimmed for the full
|
||||
// quota TTL. (Reauth/error rows keep the full TTL + cooldown to avoid re-401s.)
|
||||
const PARKED_TTL_MS = 30_000; // 30 seconds
|
||||
|
||||
/** Exponential backoff base; delay = min(base * 2^n, MAX) + jitter. */
|
||||
const RETRY_BASE_MS = 1_000;
|
||||
@@ -697,9 +702,12 @@ async function collectClaudeRowForProfile(
|
||||
const now = (deps.now ?? Date.now)();
|
||||
const state = getState(claudeProfileStates, profile);
|
||||
|
||||
// Serve from cache while within TTL — force bypasses TTL short-circuit.
|
||||
if (!force && state.cachedRow && now - state.cachedAt < NATIVE_QUOTA_TTL_MS) {
|
||||
return serveCached(state);
|
||||
// Serve from cache while within TTL — force bypasses the short-circuit. Parked
|
||||
// rows (no creds -> quotaStatus 'unsupported') use a short TTL so a fresh login
|
||||
// is picked up within seconds instead of staying dimmed for the full quota TTL.
|
||||
if (!force && state.cachedRow) {
|
||||
const ttl = state.cachedRow.quotaStatus === 'unsupported' ? PARKED_TTL_MS : NATIVE_QUOTA_TTL_MS;
|
||||
if (now - state.cachedAt < ttl) return serveCached(state);
|
||||
}
|
||||
|
||||
// Breaker open or cooldown active -> zero network, serve stale (may be null).
|
||||
@@ -722,6 +730,12 @@ async function collectClaudeRowForProfile(
|
||||
|
||||
state.pending = (async (): Promise<BarSummaryRow | null> => {
|
||||
try {
|
||||
// Yield once so the `state.pending = (...)()` assignment above completes
|
||||
// before any synchronous return below runs the finally that nulls it.
|
||||
// Without this, a sync return (e.g. the no-creds parked path) clears
|
||||
// pending DURING assignment, leaving a stale resolved promise that the
|
||||
// next call's coalescing check would return instead of re-evaluating.
|
||||
await Promise.resolve();
|
||||
const creds = readCreds(profile);
|
||||
|
||||
// No credentials file found -> emit parked row (needs auth, file absent).
|
||||
@@ -854,9 +868,12 @@ async function collectCodexRowForProfile(
|
||||
const now = (deps.now ?? Date.now)();
|
||||
const state = getState(codexProfileStates, profile);
|
||||
|
||||
// Serve from cache while within TTL — force bypasses TTL short-circuit.
|
||||
if (!force && state.cachedRow && now - state.cachedAt < NATIVE_QUOTA_TTL_MS) {
|
||||
return serveCached(state);
|
||||
// Serve from cache while within TTL — force bypasses the short-circuit. Parked
|
||||
// rows (no auth -> quotaStatus 'unsupported') use a short TTL so a fresh login
|
||||
// is picked up within seconds instead of staying dimmed for the full quota TTL.
|
||||
if (!force && state.cachedRow) {
|
||||
const ttl = state.cachedRow.quotaStatus === 'unsupported' ? PARKED_TTL_MS : NATIVE_QUOTA_TTL_MS;
|
||||
if (now - state.cachedAt < ttl) return serveCached(state);
|
||||
}
|
||||
|
||||
// Breaker open or cooldown active -> skip network, go to LOCAL fallback.
|
||||
@@ -881,6 +898,10 @@ async function collectCodexRowForProfile(
|
||||
|
||||
state.pending = (async (): Promise<BarSummaryRow | null> => {
|
||||
try {
|
||||
// Yield once so the `state.pending = (...)()` assignment completes before
|
||||
// any synchronous return below runs the finally that nulls it (otherwise a
|
||||
// sync return leaves a stale resolved promise the next call would reuse).
|
||||
await Promise.resolve();
|
||||
// ----------------------------------------------------------------
|
||||
// PRIMARY: live network fetch (skipped when breaker/cooldown active)
|
||||
// ----------------------------------------------------------------
|
||||
@@ -898,14 +919,24 @@ async function collectCodexRowForProfile(
|
||||
state.breakerOpenUntil = 0;
|
||||
state.cooldownUntil = 0;
|
||||
state.backoffAttempt = 0;
|
||||
// Only usable when at least one core window (5h/weekly) resolved.
|
||||
// At least one core window (5h/weekly) resolved -> full quota row.
|
||||
if (quota.coreUsage?.fiveHour || quota.coreUsage?.weekly) {
|
||||
const row = buildCodexNetworkRow(quota, now, SURFACE_CODEX, profile);
|
||||
state.cachedRow = row;
|
||||
state.cachedAt = now;
|
||||
return { ...row, cached: false };
|
||||
}
|
||||
// else: fall through to LOCAL fallback below.
|
||||
// Success but no core window. The token authenticated, so this is a
|
||||
// VALID active subscription with a sparse payload — emit an active
|
||||
// (quota-less) row instead of parking it. Only the bare default keeps
|
||||
// falling through to the global local session data below.
|
||||
if (profile !== DEFAULT_PROFILE) {
|
||||
const row = buildCodexNetworkRow(quota, now, SURFACE_CODEX, profile);
|
||||
state.cachedRow = row;
|
||||
state.cachedAt = now;
|
||||
return { ...row, cached: false };
|
||||
}
|
||||
// else (default): fall through to LOCAL fallback below.
|
||||
} else if (quota.needsReauth) {
|
||||
// Token expired -> dimmed reauth row. Cache it and cool down so the
|
||||
// expired account is NOT re-polled (and re-401'd) every refresh; it
|
||||
@@ -1201,6 +1232,10 @@ async function collectCodexRow(
|
||||
|
||||
state.pending = (async (): Promise<BarSummaryRow | null> => {
|
||||
try {
|
||||
// Yield once so the `state.pending = (...)()` assignment completes before
|
||||
// any synchronous return below runs the finally that nulls it (otherwise a
|
||||
// sync return leaves a stale resolved promise the next call would reuse).
|
||||
await Promise.resolve();
|
||||
// ----------------------------------------------------------------
|
||||
// PRIMARY: live network fetch (skipped when breaker/cooldown active)
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
@@ -1334,4 +1334,55 @@ describe('review focus areas: reauth caching + codex local fallback', () => {
|
||||
expect(cached.find((r) => r.surface === 'ccs' && r.profile === 'ck')?.is_default).toBe(false);
|
||||
expect(cached.find((r) => r.surface === 'ccsx' && r.profile === 'ck')?.is_default).toBe(false);
|
||||
});
|
||||
|
||||
it('parked (no-creds) rows use a short TTL so a fresh login is detected quickly', async () => {
|
||||
resetNativeQuotaState();
|
||||
const clock = { now: 7_000_000 };
|
||||
let hasCreds = false;
|
||||
const deps = makeMultiProfileDeps({
|
||||
clock,
|
||||
claudeProfiles: ['work'],
|
||||
codexProfiles: [],
|
||||
claudeDefault: 'work',
|
||||
credsForProfile: () => (hasCreds ? maxCreds() : null),
|
||||
claudeFetch: async () => successQuota(),
|
||||
});
|
||||
|
||||
// First poll: no creds -> parked, no network.
|
||||
const first = await getNativeAccountRows(deps);
|
||||
expect(first.find((r) => r.profile === 'work')?.paused).toBe(true);
|
||||
expect(deps.claudeFetchCount()).toBe(0);
|
||||
|
||||
// User logs in; advance past the short parked TTL (30s) but far within the
|
||||
// full quota TTL (10min). The parked row must NOT be served stale.
|
||||
hasCreds = true;
|
||||
clock.now += 31_000;
|
||||
const second = await getNativeAccountRows(deps);
|
||||
const work = second.find((r) => r.profile === 'work');
|
||||
expect(work?.paused).toBe(false); // now live, not dimmed
|
||||
expect(work?.quotaStatus).toBe('ok');
|
||||
expect(deps.claudeFetchCount()).toBe(1); // re-checked -> fetched
|
||||
});
|
||||
|
||||
it('Codex named profile with valid auth but sparse payload stays active, not parked', async () => {
|
||||
resetNativeQuotaState();
|
||||
const clock = { now: 7_000_000 };
|
||||
const deps = makeMultiProfileDeps({
|
||||
clock,
|
||||
claudeProfiles: [],
|
||||
codexProfiles: ['ck'],
|
||||
codexDefault: 'default',
|
||||
codexNativeAuth: (p) => ({ accessToken: `t-${p}`, accountId: `id-${p}` }),
|
||||
// Successful response but NO core windows (sparse / changed payload).
|
||||
codexNetworkFetch: async () => ({ success: true }) as CodexQuotaResult,
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user