stablyai/orca · warning · CodexResetCreditScopeRejection
offerChanged
offerChanged
Error message
The Codex reset-credit offer changed before reset.
What it means
A CodexResetCreditScopeRejection with reason 'offerChanged'. requireCurrentOffer is true, a current scope exists, but resetScopeKey(expectedScope) !== resetScopeKey(currentScope) — specifically the offerRevision (and thus the credit snapshot) changed since the attempt was captured. A fresh attempt must consume exactly the offer it was authorized against; a drifted offer is refused to prevent charging the wrong credit grant.
Source
Thrown at src/main/codex-accounts/service.ts:602
target: rateLimitState.codexTarget,
account: this.toSummary(account),
limits: rateLimitState.codex
})
// Why: a same-key replay resolves an already-started provider mutation;
// its credit snapshot may have refreshed, but its account/runtime identity may not change.
if (requireCurrentOffer && !currentScope) {
throw new CodexResetCreditScopeRejection(
'offerUnavailable',
rateLimitState,
'The Codex reset-credit offer is no longer available.'
)
}
if (
requireCurrentOffer &&
currentScope &&
resetScopeKey(expectedScope) !== resetScopeKey(currentScope)
) {
throw new CodexResetCreditScopeRejection(
'offerChanged',
rateLimitState,
'The Codex reset-credit offer changed before reset.'
)
}
return { managedHomePath: account.managedHomePath, rateLimits: rateLimitState }
}
private hydrateResetCreditAttempts(): void {
try {
const ledger = this.store.getCodexResetCreditAttemptLedger()
this.durableResetLedger = ledger
for (const durable of ledger.attempts) {
const scopeKey = resetScopeKey(durable.expectedScope)
const accountScopeKey = resetAccountScopeKey(durable.expectedScope)
this.resetAttemptsByKey.set(durable.idempotencyKey, {
expectedScope: durable.expectedScope,View on GitHub (pinned to 1136503c6a)
Solutions
- Rebuild expectedScope from the now-current limits (new offerRevision) and start a fresh attempt.
- Debounce/disable the reset button while a rate-limit refresh is in flight so the captured offer cannot go stale.
- Consume the rejectedBeforeProvider result and re-trigger after the refresh settles.
Example fix
// before: attempt built against a stale offerRevision
service.consumeRateLimitResetCredit(key, scope) // rejects offerChanged
// after: rebuild scope from refreshed limits
const fresh = buildCodexResetCreditExpectedScope({ target, account, limits: rateLimits.getState().codex })!
await service.consumeRateLimitResetCredit(crypto.randomUUID(), fresh) Defensive patterns
Strategy: validation
Validate before calling
// Recompute the scope from the latest limits immediately before consuming.
const fresh = buildCodexResetCreditExpectedScope({ target, account, limits: rateLimits.getState().codex })!
if (resetScopeKey(fresh) !== resetScopeKey(scope)) {
// offer rotated; use `fresh` with a new idempotency key
}
await service.consumeRateLimitResetCredit(crypto.randomUUID(), fresh) Try / catch
try {
await service.consumeRateLimitResetCredit(key, scope)
} catch (error) {
if (error instanceof CodexResetCreditScopeRejection && error.reason === 'offerChanged') {
// rebuild from current limits and retry with a fresh key
} else throw error
} Prevention
- Debounce the reset button during rate-limit refreshes so the captured offer cannot go stale.
- Capture the scope synchronously right before the consume call, not on render.
When it happens
Trigger: Between capturing expectedScope and validateResetCreditScope running, the rate-limit limits refreshed so the current offer's offerRevision differs. The account/target/revision still match, but the credit offer itself rotated.
Common situations: A background quota refresh lands while the user's reset click is queued behind the mutation queue; the offer the user saw is no longer the live offer.
Related errors
AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12).
Data as JSON: /api/errors/0dfca9ac8e8114b6.
Report an issue: GitHub.