fix(forecast): ignore stale time-bounded runtime overlay reasons (#507) - #508
Conversation
forecast --live marked working accounts as unavailable because the
runtime overlay in runtime-observability.json persists a skip reason
("rate-limited", "cooling-down:...") on pool exhaustion and only clears
it on an explicit runtime reset, never on a subsequent successful
request. After the underlying window expired, the stale reason kept the
forecast reporting the account as unavailable even though the proxy
(which rebuilds skip reasons fresh per request) still routed to it
successfully. doctor's forecast-runtime-alignment warning surfaced the
same stale state via the shared forecast evaluation.
Cross-reference time-bounded overlay reasons against the time-aware disk
state before applying them:
- "rate-limited" is dropped when getRateLimitResetTimeForFamily returns
null (no active reset on disk for codex or a model-scoped key)
- "cooling-down:..." is dropped when coolingDownUntil is absent or has
elapsed
Each reason validates only against its own backing disk state, so the
fix never substitutes a misleading reason string. Non-time-bounded
reasons ("circuit-open", "token-exhausted", "policy-blocked") have no
disk expiry and remain unconditional. The check reuses the rateLimitResetAt
value and the single now timestamp already computed in the function, so
no extra disk read or clock skew is introduced.
Tests: split the runtime-skip parametrized test into non-time-bounded
reasons (still unconditional) and add coverage for stale vs active
rate-limited and cooling-down overlays, including a model-scoped
(codex:5h) active rate limit.
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
📝 WalkthroughWalkthroughruntime overlay skip reasons for ChangesStale Runtime Overlay Handling
notes:
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 📋 Issue PlannerBuilt with CodeRabbit's Coding Plans for faster development and fewer bugs. View plan used: ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/forecast.test.ts`:
- Around line 274-295: Add a regression test for evaluateForecastAccount that
covers the case where account.coolingDownUntil is missing (undefined) but
runtimeOverlay still contains a "cooling-down:..." skip reason; replicate the
existing test setup (use now, isCurrent: false, refreshToken, addedAt/lastUsed
values) but omit the coolingDownUntil field on the account, and assert
availability === "ready" and that result.reasons does not contain "runtime skip:
cooling-down:server-error" so the stale overlay is ignored; place the new it
block alongside the existing tests in test/forecast.test.ts referring to
evaluateForecastAccount to ensure staleness detection handles absent
coolingDownUntil.
- Around line 208-230: Add a regression test that mirrors the existing "ignores
a stale rate-limited overlay when no rate limit is active on disk" case but uses
an account object with no rateLimitResetTimes property (i.e., undefined) to
ensure evaluateForecastAccount treats the overlay as stale; specifically, create
a test that calls evaluateForecastAccount with
runtimeOverlay.lastPoolExhaustionSkipReasons containing "0": "rate-limited", an
account object that omits rateLimitResetTimes entirely (use same timestamps for
addedAt/lastUsed as the existing test), assert result.availability === "ready"
and that result.reasons does not contain "runtime skip: rate-limited", verifying
the logic in getRateLimitResetTimeForFamily / evaluateForecastAccount handles
missing rateLimitResetTimes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: c1af516d-a3ad-469a-9171-c3781a2a5a5c
📒 Files selected for processing (2)
lib/forecast.tstest/forecast.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (7)
test/**/*.test.ts
📄 CodeRabbit inference engine (test/AGENTS.md)
test/**/*.test.ts: Vitest globals (describe,it,expect) are enabled and should be used without explicit imports
Maintain 80% coverage threshold across statements, branches, functions, and lines
UseremoveWithRetryfor Windows filesystem cleanup instead of barefs.rmto handle EBUSY/EPERM/ENOTEMPTY backoff
Use source files in tests, not compileddist/files; test the source directly
Do not skip tests without justification; include rationale if a test must be skipped
Relax ESLint rules for test files as specified ineslint.config.js
Files:
test/forecast.test.ts
**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Do not use
as any,@ts-ignore, or@ts-expect-errortype assertions
Files:
test/forecast.test.tslib/forecast.ts
**/*.{ts,js}
📄 CodeRabbit inference engine (AGENTS.md)
Use ESM module syntax exclusively; the project is ESM-only with
"type": "module"
Files:
test/forecast.test.tslib/forecast.ts
test/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Windows filesystem operations must include retry handling for transient
EBUSY,EPERM, andENOTEMPTYerrors where tests cover Windows locks
Files:
test/forecast.test.ts
test/**
⚙️ CodeRabbit configuration file
tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.
Files:
test/forecast.test.ts
lib/**/*.ts
📄 CodeRabbit inference engine (lib/AGENTS.md)
lib/**/*.ts: All public exports should flow throughlib/index.tsor documented package subpaths
Never import fromdist/in source tests or library code
Never suppress type errors
Files:
lib/forecast.ts
lib/**
⚙️ CodeRabbit configuration file
focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.
Files:
lib/forecast.ts
🔇 Additional comments (6)
lib/forecast.ts (2)
276-281: LGTM!
286-290: ⚡ Quick wintime-bounded overlay staleness coverage is already in place
lib/forecast.tsdocuments and enforces that only"rate-limited"and"cooling-down:*"are time-bounded (lib/forecast.ts:261-243), andisStaleOverlayReasonchecksrateLimitResetAt/account.coolingDownUntilaccordingly (lib/forecast.ts:276-281); overlay skip is applied only when!isStaleOverlayReason(lib/forecast.ts:287-290).test/forecast.test.tshas regression coverage for both stale/active"rate-limited"and stale/active"cooling-down:..."overlays (test/forecast.test.ts:208-253,test/forecast.test.ts:274-315).- non-time-bounded overlay reasons like
"circuit-open"and"token-exhausted"are applied without staleness checks by design (test/forecast.test.ts:176-212,test/forecast.test.ts:851).test/forecast.test.ts (4)
185-206: LGTM!
253-272: LGTM!
232-251: LGTM!
297-316: LGTM!
…lay staleness Add regression cases for the absent-field branch of the stale-overlay guard, raised in review: rateLimitResetTimes / coolingDownUntil entirely undefined (limit cleared by runtime reset or successful request after the overlay was written), not just an expired entry. - "rate-limited" overlay with no rateLimitResetTimes -> account ready, reason dropped (getRateLimitResetTimeForFamily returns null on !times) - "cooling-down:..." overlay with no coolingDownUntil -> account ready, reason dropped
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/forecast.test.ts`:
- Around line 232-253: The tests cover stale-overlay behavior for
runtimeOverlay.lastPoolExhaustionSkipReasons but miss the precedence path where
evaluateForecastAccount consults accountSkipReasons first; add analogous
deterministic Vitest cases that set account.accountSkipReasons to contain stale
"rate-limited" and "cooling-down:*" entries (with no corresponding
rateLimitResetTimes or cooling window fields) and assert
evaluateForecastAccount(...) returns availability "ready" and that
result.reasons does not include the stale "account skip" messages; reference
evaluateForecastAccount, accountSkipReasons and lastPoolExhaustionSkipReasons to
mirror existing tests so the resolver precedence branch is exercised.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 813f0143-d8ff-44d0-83ce-405e64f0a622
📒 Files selected for processing (1)
test/forecast.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (5)
test/**/*.test.ts
📄 CodeRabbit inference engine (test/AGENTS.md)
test/**/*.test.ts: Vitest globals (describe,it,expect) are enabled and should be used without explicit imports
Maintain 80% coverage threshold across statements, branches, functions, and lines
UseremoveWithRetryfor Windows filesystem cleanup instead of barefs.rmto handle EBUSY/EPERM/ENOTEMPTY backoff
Use source files in tests, not compileddist/files; test the source directly
Do not skip tests without justification; include rationale if a test must be skipped
Relax ESLint rules for test files as specified ineslint.config.js
Files:
test/forecast.test.ts
**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Do not use
as any,@ts-ignore, or@ts-expect-errortype assertions
Files:
test/forecast.test.ts
**/*.{ts,js}
📄 CodeRabbit inference engine (AGENTS.md)
Use ESM module syntax exclusively; the project is ESM-only with
"type": "module"
Files:
test/forecast.test.ts
test/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Windows filesystem operations must include retry handling for transient
EBUSY,EPERM, andENOTEMPTYerrors where tests cover Windows locks
Files:
test/forecast.test.ts
test/**
⚙️ CodeRabbit configuration file
tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.
Files:
test/forecast.test.ts
Raised in re-review: the resolver reads accountSkipReasons before lastPoolExhaustionSkipReasons (forecast.ts ?? chain), but existing tests only exercised the latter key. - stale reason via accountSkipReasons (no disk-backed limit/cooldown) -> account ready, reason dropped on the precedence path - active reason via accountSkipReasons (future rateLimitResetTimes) -> account unavailable, reason applied
|
@coderabbitai review |
✅ Action performedReview finished.
|
Summary
Fixes #507.
forecast --livereported both accounts asunavailable(runtime skip: rate-limited) while Codex kept executing prompts successfully.Root cause: the runtime overlay in
runtime-observability.jsonpersists a per-account skip reason on pool exhaustion and is only cleared by an explicit runtime reset, never on a subsequent successful request. Once the underlying window expired, the stale reason keptlib/forecast.tsmarking the accountunavailable. The proxy was unaffected because it rebuilds skip reasons fresh per request (getAccountRuntimeSkipReason->isRateLimitedForFamily), which is why Codex still worked.doctor'sforecast-runtime-alignmentwarning surfaced the same stale state through the sharedevaluateForecastAccountscall.Fix
In the overlay-application block of
evaluateForecastAccount, validate time-bounded overlay reasons against the time-aware disk state before applying them:rate-limitedis ignored whengetRateLimitResetTimeForFamily(account, now, "codex")returnsnull(no active reset on disk, including model-scoped keys likecodex:5h).cooling-down:...is ignored whencoolingDownUntilis absent or<= now.Each reason validates only against its own backing disk state, so the displayed reason string is never substituted with a misleading one. Non-time-bounded reasons (
circuit-open,token-exhausted,policy-blocked) have no disk expiry and remain unconditional. The guard reuses therateLimitResetAtvalue and the singlenowtimestamp already computed in the function, so no extra disk read or clock skew is introduced.Tests
rate-limitedoverlay with expired disk entry -> account ready, reason dropped.rate-limitedoverlay (codexandcodex:5hfuture reset) -> account unavailable.cooling-downoverlay (elapsedcoolingDownUntil) -> reason dropped.cooling-downoverlay -> account unavailable.Verification
npx tsc --noEmit: clean.npx vitest run test/forecast.test.ts: 31 passed.npx vitest run(full suite): 4365 passed, 6 skipped, 0 failed.Notes / follow-up
token-exhaustedshares the same overlay-staleness class but has no disk-backed reset time to validate against, so it is intentionally left unconditional. Fully clearing it would require clearing the skip reason on a successful proxy request (CodeRabbit design-choice option 2); out of scope for this bug. Worth a tracking issue.note: greptile review for oc-chatgpt-multi-auth. cite files like
lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.Greptile Summary
fixes stale time-bounded runtime overlay reasons persisting in
runtime-observability.jsonlong after the underlying rate-limit window or cooldown expires, causingforecast --liveto incorrectly report working accounts asunavailable.lib/forecast.ts: addsisStaleOverlayReasoncomputed from the already-availablerateLimitResetAtandcoolingDownActivevalues; ignoresrate-limitedoverlay whengetRateLimitResetTimeForFamilyreturnsnull, andcooling-down:…overlay whencoolingDownUntilis absent or elapsed. non-time-bounded reasons (circuit-open,token-exhausted,policy-blocked) are applied unconditionally.test/forecast.test.ts: nine new vitest cases cover expired and absent disk entries, both overlay-source precedence paths (accountSkipReasonsvslastPoolExhaustionSkipReasons), active boundaries, and model-scoped rate-limit keys (codex:5h); theit.eachis narrowed to the three genuinely unconditional reasons.Confidence Score: 5/5
the staleness guard is tightly scoped, reuses already-computed values, and all nine new tests pass; no functional regressions introduced.
the fix reads no extra disk state and introduces no new async paths; the ternary correctly short-circuits for null overlayReason and the policyBlockedIndexes branch is unaffected. test coverage spans both staleness sources, both time-bounded reason types, active/expired/absent disk state, and model-scoped keys.
no files require special attention.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[evaluateForecastAccount] --> B[resolve overlayReason\naccountSkipReasons ?? lastPoolExhaustionSkipReasons] B --> C{overlayReason type?} C -->|rate-limited| D[rateLimitResetAt === null?] C -->|cooling-down:...| E[coolingDownUntil absent or <= now?] C -->|other non-time-bounded| F[isStaleOverlayReason = false] D -->|yes - stale| G[drop reason, account stays ready] D -->|no - active| H[apply: availability=unavailable] E -->|yes - stale| G E -->|no - active| H F --> H H --> I[reasons.push: runtime skip: ...]Comments Outside Diff (1)
test/forecast.test.ts, line 208-321 (link)accountSkipReasonssourceoverlayReasonis resolved fromaccountSkipReasonsfirst, thenlastPoolExhaustionSkipReasons. all five new staleness tests exclusively uselastPoolExhaustionSkipReasons. ifaccountSkipReasonsever had a stale"rate-limited"or"cooling-down:..."entry the guard would fire identically, but there's no test confirming that — so a refactor of the resolution order or short-circuit logic would silently pass vitest.Prompt To Fix With AI
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Reviews (3): Last reviewed commit: "test(forecast): cover accountSkipReasons..." | Re-trigger Greptile