You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug]: account-switch warning diverges from routing for short-window timestamp units and freshness #5045
The manual account-switch warning added by #4982 can report an exhausted account as usable even while the server router rejects it. Two current dev boundaries cause the divergence:
gui/src/codex-quota-utils.ts::computeCodexUsageScore compares shortResetAt directly with Date.now(). Stored quota accepts Unix seconds as well as milliseconds, while server routing normalizes the value through resetAtToMs. A seconds-form future reset therefore looks expired only in the GUI.
The reset-less terminal-short-window rule requires shortObservedAt, but the main-account DTO and Free/Go projection do not consistently carry that stored observation timestamp. Routing can use fresh terminal evidence while the modal receives no freshness evidence and returns null.
Expected behaviour
The Dashboard warning and server selection must use the same effective usage score for the same account/quota snapshot. Normalize reset units identically and either project shortObservedAt through every relevant DTO or return a server-computed score.
Required regressions
active seconds-form and milliseconds-form reset timestamps produce the same warning;
main and pool DTOs preserve fresh reset-less terminal evidence;
Free/Go projection does not discard freshness needed for its short window;
stale evidence remains ignored; and
the test compares the GUI decision with the server routing helper for the same fixtures.
Regression origin
Merged in #4982 at 302c4c37ed; the exact reviewed head still contains the direct timestamp comparison.
이 이슈는 #4982가 넣은 Dashboard 수동 계정 전환 경고가, 서버 라우터가 같은 계정을 거절하는 순간과 점수 계산이 어긋난다는 이야기입니다. 지금 dev를 보면 두 구현이 실제로 갈라져 있습니다.
서버 쪽은 src/codex/routing/cooldown-math.ts의 computeCodexUsageScore / isTerminalShortWindow입니다. short reset을 비교할 때 resetAtToMs(resetAt) > now를 씁니다. 주석에도 “초·밀리초가 둘 다 저장에 온다”고 적혀 있고, 변환은 src/codex/quota.ts의 resetAtToMs 한곳에 모읍니다. reset이 없으면 shortObservedAt 신선도로 terminal short를 판정합니다(#3425).
GUI 쪽은 gui/src/codex-quota-utils.ts의 별도 computeCodexUsageScore입니다. 여기 short reset 비교는 shortReset > now로 Date.now()와 직접 합니다. 초 단위로 저장된 미래 reset(예: 1_700_000_000초)은 밀리초 now보다 작아 보여서, GUI만 “이미 만료”로 읽고 경고를 약하게 줄 수 있습니다. 또한 reset-less terminal 규칙은 shortObservedAt이 필요한데, 이슈 말대로 main-account DTO / Free·Go projection이 그 관측 시각을 빠뜨리면 모달은 freshness 증거 없이 null을 돌리고, 서버는 같은 스냅샷으로 거절할 수 있습니다.
기대는 “같은 quota 스냅샷이면 Dashboard 경고와 서버 selection 점수가 같아야 한다”입니다. 단위 정규화를 맞추고, shortObservedAt을 관련 DTO에 실어 주거나, 서버가 계산한 점수를 그대로 내려주는 쪽이 맞습니다. 회귀도 초/ms 동일 경고, main·pool DTO freshness 보존, Free/Go가 short freshness를 버리지 않음, stale 무시는 유지, 같은 fixture로 GUI 결정 vs 서버 helper 비교까지 적어 두었습니다. #4982 후속 구멍이지 types/config 분할과 무관합니다.
라인 86-95 (gui/src/codex-quota-utils.ts) - shortReset > now 직접 비교. 서버의 resetAtToMs 경로와 단위가 어긋날 수 있다.
라인 157-172 (src/codex/routing/cooldown-math.ts) - 서버는 resetAtToMs + shortObservedAt freshness. 권위 구현은 이쪽이다.
경로 gui/src/components/codex-account-switch-modal.tsx - 경고 UI가 GUI helper만 부른다. 서버 점수와 공유하지 않는다.
경로 #4982 (302c4c3 언급) - 계정 전환 경고 도입 시점 후속. tip #5040과 무관한 account-pool/gui 버그.
메인테이너의 판단이 필요한 지점
GUI에 resetAtToMs를 복제할지, 공유 패키지/생성 DTO로 서버 점수를 내려줄지. 복제는 드리프트 위험이 다시 생기고, 서버 점수 하향은 계약이 커진다.
Free/Go projection에서 shortObservedAt을 빼는 이유가 의도(민감/크기)인지 누락인지.
gui와 src/codex에 동명 computeCodexUsageScore가 둘인 상태 자체를 이번 기회에 하나로 모을지, 이번 PR은 단위·DTO만 맞출지.
너의 추천
인정. 최소 수정은 GUI short-reset 비교를 resetAtToMs와 동일한 규칙으로 맞추고, main/pool/Free·Go DTO에 shortObservedAt을 빠짐없이 실으며, 같은 fixture로 GUI vs cooldown-math 점수를 단언하는 테스트 추가. 가능하면 동명 함수 이중 구현을 줄이는 follow-up을 이슈에 남긴다.
isTerminalShortWindow and resetAtToMs now live on src/codex/quota-types, the dependency-free leaf whose own comments already say the dashboard shares values from it because it cannot import the routing or disk-cache owners. Routing and the account-switch warning call the same function, so the two cannot answer differently for one snapshot.
Both boundaries you named are covered, and a third turned up beside them. The units problem was the direct Date.now() comparison. The projection problem was providerQuotaFromCodexQuota dropping shortObservedAt, so the reset-less terminal rule arrived with no freshness evidence. The third is precedence: the dashboard accepted a fresh observation even when an ELAPSED reset was present, where routing treats a reset as authoritative once it exists — the same class of wrong answer in the other direction.
One correction to the issue's third required regression, recorded because I implemented it the wrong way first. The Free/Go projection that needed fixing is the server DTO, not normalizeQuotaForPlan. That GUI normalizer deliberately strips non-monthly windows for 30-day plans — tests/gui/rate-limit-reset-credits.test.ts pins it and says #1791's burst-window carve-out lives on the server DTO — and it is not on the warning's path at all, since the modal scores confirm.quota directly. My first attempt extended it and that test failed, correctly.
tests/gui/gui-codex-usage-score-parity.test.ts compares the two implementations on shared fixtures rather than asserting either against a literal, which is the shape this defect needed: each side was self-consistent and only their disagreement was wrong.
Activity
lidge-jun commentedon Sep 18, 2026
리뷰 · 우선순위 70 / 80
이 이슈는 #4982가 넣은 Dashboard 수동 계정 전환 경고가, 서버 라우터가 같은 계정을 거절하는 순간과 점수 계산이 어긋난다는 이야기입니다. 지금
dev를 보면 두 구현이 실제로 갈라져 있습니다.서버 쪽은
src/codex/routing/cooldown-math.ts의computeCodexUsageScore/isTerminalShortWindow입니다. short reset을 비교할 때resetAtToMs(resetAt) > now를 씁니다. 주석에도 “초·밀리초가 둘 다 저장에 온다”고 적혀 있고, 변환은src/codex/quota.ts의resetAtToMs한곳에 모읍니다. reset이 없으면shortObservedAt신선도로 terminal short를 판정합니다(#3425).GUI 쪽은
gui/src/codex-quota-utils.ts의 별도computeCodexUsageScore입니다. 여기 short reset 비교는shortReset > now로 Date.now()와 직접 합니다. 초 단위로 저장된 미래 reset(예: 1_700_000_000초)은 밀리초 now보다 작아 보여서, GUI만 “이미 만료”로 읽고 경고를 약하게 줄 수 있습니다. 또한 reset-less terminal 규칙은shortObservedAt이 필요한데, 이슈 말대로 main-account DTO / Free·Go projection이 그 관측 시각을 빠뜨리면 모달은 freshness 증거 없이null을 돌리고, 서버는 같은 스냅샷으로 거절할 수 있습니다.기대는 “같은 quota 스냅샷이면 Dashboard 경고와 서버 selection 점수가 같아야 한다”입니다. 단위 정규화를 맞추고,
shortObservedAt을 관련 DTO에 실어 주거나, 서버가 계산한 점수를 그대로 내려주는 쪽이 맞습니다. 회귀도 초/ms 동일 경고, main·pool DTO freshness 보존, Free/Go가 short freshness를 버리지 않음, stale 무시는 유지, 같은 fixture로 GUI 결정 vs 서버 helper 비교까지 적어 두었습니다. #4982 후속 구멍이지 types/config 분할과 무관합니다.라인 86-95 (gui/src/codex-quota-utils.ts) -
shortReset > now직접 비교. 서버의resetAtToMs경로와 단위가 어긋날 수 있다.라인 157-172 (src/codex/routing/cooldown-math.ts) - 서버는
resetAtToMs+shortObservedAtfreshness. 권위 구현은 이쪽이다.경로 gui/src/components/codex-account-switch-modal.tsx - 경고 UI가 GUI helper만 부른다. 서버 점수와 공유하지 않는다.
경로 #4982 (302c4c3 언급) - 계정 전환 경고 도입 시점 후속. tip #5040과 무관한 account-pool/gui 버그.
메인테이너의 판단이 필요한 지점
resetAtToMs를 복제할지, 공유 패키지/생성 DTO로 서버 점수를 내려줄지. 복제는 드리프트 위험이 다시 생기고, 서버 점수 하향은 계약이 커진다.shortObservedAt을 빼는 이유가 의도(민감/크기)인지 누락인지.gui와src/codex에 동명computeCodexUsageScore가 둘인 상태 자체를 이번 기회에 하나로 모을지, 이번 PR은 단위·DTO만 맞출지.너의 추천
인정. 최소 수정은 GUI short-reset 비교를
resetAtToMs와 동일한 규칙으로 맞추고, main/pool/Free·Go DTO에shortObservedAt을 빠짐없이 실으며, 같은 fixture로 GUI vscooldown-math점수를 단언하는 테스트 추가. 가능하면 동명 함수 이중 구현을 줄이는 follow-up을 이슈에 남긴다.이 댓글은 grok-bot이 작성했습니다
fix(quota): make the account-switch warning agree with routing
fix(quota): make the account-switch warning agree with routing (#5055)
lidge-jun commentedon Sep 18, 2026
Fixed on
devby #5055.isTerminalShortWindowandresetAtToMsnow live onsrc/codex/quota-types, the dependency-free leaf whose own comments already say the dashboard shares values from it because it cannot import the routing or disk-cache owners. Routing and the account-switch warning call the same function, so the two cannot answer differently for one snapshot.Both boundaries you named are covered, and a third turned up beside them. The units problem was the direct
Date.now()comparison. The projection problem wasproviderQuotaFromCodexQuotadroppingshortObservedAt, so the reset-less terminal rule arrived with no freshness evidence. The third is precedence: the dashboard accepted a fresh observation even when an ELAPSED reset was present, where routing treats a reset as authoritative once it exists — the same class of wrong answer in the other direction.One correction to the issue's third required regression, recorded because I implemented it the wrong way first. The Free/Go projection that needed fixing is the server DTO, not
normalizeQuotaForPlan. That GUI normalizer deliberately strips non-monthly windows for 30-day plans —tests/gui/rate-limit-reset-credits.test.tspins it and says #1791's burst-window carve-out lives on the server DTO — and it is not on the warning's path at all, since the modal scoresconfirm.quotadirectly. My first attempt extended it and that test failed, correctly.tests/gui/gui-codex-usage-score-parity.test.tscompares the two implementations on shared fixtures rather than asserting either against a literal, which is the shape this defect needed: each side was self-consistent and only their disagreement was wrong.