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

  1. Rebuild expectedScope from the now-current limits (new offerRevision) and start a fresh attempt.
  2. Debounce/disable the reset button while a rate-limit refresh is in flight so the captured offer cannot go stale.
  3. 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

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.