Skip to content

fix(forecast): ignore stale time-bounded runtime overlay reasons (#507) - #508

Merged
ndycode merged 3 commits into
mainfrom
fix/507-stale-runtime-overlay
Jun 3, 2026
Merged

fix(forecast): ignore stale time-bounded runtime overlay reasons (#507)#508
ndycode merged 3 commits into
mainfrom
fix/507-stale-runtime-overlay

Conversation

@ndycode

@ndycode ndycode commented Jun 3, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #507. forecast --live reported both accounts as unavailable (runtime skip: rate-limited) while Codex kept executing prompts successfully.

Root cause: the runtime overlay in runtime-observability.json persists 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 kept lib/forecast.ts marking the account unavailable. The proxy was unaffected because it rebuilds skip reasons fresh per request (getAccountRuntimeSkipReason -> isRateLimitedForFamily), which is why Codex still worked. doctor's forecast-runtime-alignment warning surfaced the same stale state through the shared evaluateForecastAccounts call.

Fix

In the overlay-application block of evaluateForecastAccount, validate time-bounded overlay reasons against the time-aware disk state before applying them:

  • rate-limited is ignored when getRateLimitResetTimeForFamily(account, now, "codex") returns null (no active reset on disk, including model-scoped keys like codex:5h).
  • cooling-down:... is ignored when coolingDownUntil is 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 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 parametrized runtime-skip test into non-time-bounded reasons (still applied unconditionally).
  • Added: stale rate-limited overlay with expired disk entry -> account ready, reason dropped.
  • Added: active rate-limited overlay (codex and codex:5h future reset) -> account unavailable.
  • Added: stale cooling-down overlay (elapsed coolingDownUntil) -> reason dropped.
  • Added: active cooling-down overlay -> 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

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.json long after the underlying rate-limit window or cooldown expires, causing forecast --live to incorrectly report working accounts as unavailable.

  • lib/forecast.ts: adds isStaleOverlayReason computed from the already-available rateLimitResetAt and coolingDownActive values; ignores rate-limited overlay when getRateLimitResetTimeForFamily returns null, and cooling-down:… overlay when coolingDownUntil is 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 (accountSkipReasons vs lastPoolExhaustionSkipReasons), active boundaries, and model-scoped rate-limit keys (codex:5h); the it.each is 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

Filename Overview
lib/forecast.ts adds isStaleOverlayReason guard that cross-references live disk state before applying time-bounded overlay reasons; logic is correct and tightly scoped
test/forecast.test.ts comprehensive new staleness tests covering both overlay sources, both time-bounded reasons, absent disk state, active/expired boundaries, and model-scoped keys; previous thread gap addressed

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: ...]
Loading

Comments Outside Diff (1)

  1. test/forecast.test.ts, line 208-321 (link)

    P2 missing coverage: stale overlay via accountSkipReasons source

    overlayReason is resolved from accountSkipReasons first, then lastPoolExhaustionSkipReasons. all five new staleness tests exclusively use lastPoolExhaustionSkipReasons. if accountSkipReasons ever 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
    This is a comment left during a code review.
    Path: test/forecast.test.ts
    Line: 208-321
    
    Comment:
    **missing coverage: stale overlay via `accountSkipReasons` source**
    
    `overlayReason` is resolved from `accountSkipReasons` first, then `lastPoolExhaustionSkipReasons`. all five new staleness tests exclusively use `lastPoolExhaustionSkipReasons`. if `accountSkipReasons` ever 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.
    
    How can I resolve this? If you propose a fix, please make it concise.

    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!

    Fix in Codex

Reviews (3): Last reviewed commit: "test(forecast): cover accountSkipReasons..." | Re-trigger Greptile

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.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

Comment thread test/forecast.test.ts
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

runtime overlay skip reasons for rate-limited and cooling-down:* are now treated as stale when their stored reset or cooldown timestamps have expired. evaluateForecastAccount avoids applying expired overlays so accounts are not falsely marked unavailable. tests cover both stale and active overlay cases.

Changes

Stale Runtime Overlay Handling

Layer / File(s) Summary
stale overlay detection in forecast evaluation
lib/forecast.ts:261-290
evaluateForecastAccount now computes whether an overlay reason is time-stale: "rate-limited" is stale when rateLimitResetAt is null/expired; "cooling-down:..." is stale when account.coolingDownUntil has elapsed. expired overlays are not used to mark availability unavailable. policyBlockedIndexes unchanged.
time-bounded overlay test coverage
test/forecast.test.ts:185-186, test/forecast.test.ts:208-364
parameterized tests updated so non-time-bounded reasons are circuit-open, token-exhausted, and workspace-disabled (:185-186). added tests (:208-364) asserting stale rate-limited and stale cooling-down:server-error overlays are ignored when disk timestamps are past or absent, and active overlays still mark unavailable and include the proper runtime skip: reason (including model-scoped rateLimitResetTimes).

notes:

  • tests reference lib/forecast.ts:261-290 and cover new behavior in test/forecast.test.ts:208-364.
  • missing regression test: there is no explicit test for concurrent updates to rateLimitResetAt race conditions when forecast runs during an on-disk update. add a concurrency/regression test exercising race between disk write and forecast evaluation.
  • windows edge cases: no windows-specific path or timezone edge-case tests found for timestamp parsing; verify timestamp handling on windows/timezone variations.
  • concurrency risk: evaluateForecastAccount reads timestamps to decide availability. concurrent writes to disk/state could create short-lived false positives; consider atomic read/update or stronger synchronization.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • ndycode/codex-multi-auth#480: introduced runtime overlay skip reasons used by forecast evaluation; this PR adjusts staleness handling for time-bounded overlays.

Suggested labels

bug

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed title follows conventional commits format (fix type, lowercase imperative, 71 chars) and clearly summarizes the fix: ignoring stale time-bounded runtime overlay reasons.
Linked Issues check ✅ Passed changes directly address #507: evaluateForecastAccount validates time-bounded overlay reasons (rate-limited, cooling-down) against disk state before applying them, fixing false-positive unavailability while codex requests succeed.
Out of Scope Changes check ✅ Passed all changes are scoped to forecast overlay staleness validation. lib/forecast.ts:+26/-1 adds isStaleOverlayReason guard; test/forecast.test.ts:+159/-2 covers stale vs active time-bounded overlays. no unrelated modifications present.
Description check ✅ Passed PR description is comprehensive and well-structured with all required sections completed: clear summary of the issue, root cause analysis, fix details, test coverage explanation, and verification results.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plan used: #507

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/507-stale-runtime-overlay
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/507-stale-runtime-overlay

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 807679e and ff9c9f4.

📒 Files selected for processing (2)
  • lib/forecast.ts
  • test/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
Use removeWithRetry for Windows filesystem cleanup instead of bare fs.rm to handle EBUSY/EPERM/ENOTEMPTY backoff
Use source files in tests, not compiled dist/ 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 in eslint.config.js

Files:

  • test/forecast.test.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Do not use as any, @ts-ignore, or @ts-expect-error type assertions

Files:

  • test/forecast.test.ts
  • lib/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.ts
  • lib/forecast.ts
test/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Windows filesystem operations must include retry handling for transient EBUSY, EPERM, and ENOTEMPTY errors 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 through lib/index.ts or documented package subpaths
Never import from dist/ 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 win

time-bounded overlay staleness coverage is already in place

  • lib/forecast.ts documents and enforces that only "rate-limited" and "cooling-down:*" are time-bounded (lib/forecast.ts:261-243), and isStaleOverlayReason checks rateLimitResetAt / account.coolingDownUntil accordingly (lib/forecast.ts:276-281); overlay skip is applied only when !isStaleOverlayReason (lib/forecast.ts:287-290).
  • test/forecast.test.ts has 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!

Comment thread test/forecast.test.ts
Comment thread test/forecast.test.ts
…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
@ndycode

ndycode commented Jun 3, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between ff9c9f4 and 65db170.

📒 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
Use removeWithRetry for Windows filesystem cleanup instead of bare fs.rm to handle EBUSY/EPERM/ENOTEMPTY backoff
Use source files in tests, not compiled dist/ 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 in eslint.config.js

Files:

  • test/forecast.test.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Do not use as any, @ts-ignore, or @ts-expect-error type 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, and ENOTEMPTY errors 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

Comment thread 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
@ndycode

ndycode commented Jun 3, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ndycode
ndycode merged commit 12d70fe into main Jun 3, 2026
2 checks passed
@ndycode
ndycode deleted the fix/507-stale-runtime-overlay branch June 3, 2026 14:29
ndycode added a commit that referenced this pull request Jun 3, 2026
Bump version to 2.2.2 and add release notes for the forecast --live
stale-overlay false positive fixed in #508 (#507).

No tag or publish yet: awaiting manual npm publish.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] forecast --live marks working accounts as unavailable

1 participant