{"record":{"id":"0dfca9ac8e8114b6","repo":"stablyai/orca","slug":"offerchanged","errorCode":"offerChanged","errorMessage":"The Codex reset-credit offer changed before reset.","messagePattern":"The Codex reset-credit offer changed before reset\\.","errorType":"exception","errorClass":"CodexResetCreditScopeRejection","httpStatus":null,"severity":"warning","filePath":"src/main/codex-accounts/service.ts","lineNumber":602,"sourceCode":"      target: rateLimitState.codexTarget,\n      account: this.toSummary(account),\n      limits: rateLimitState.codex\n    })\n    // Why: a same-key replay resolves an already-started provider mutation;\n    // its credit snapshot may have refreshed, but its account/runtime identity may not change.\n    if (requireCurrentOffer && !currentScope) {\n      throw new CodexResetCreditScopeRejection(\n        'offerUnavailable',\n        rateLimitState,\n        'The Codex reset-credit offer is no longer available.'\n      )\n    }\n    if (\n      requireCurrentOffer &&\n      currentScope &&\n      resetScopeKey(expectedScope) !== resetScopeKey(currentScope)\n    ) {\n      throw new CodexResetCreditScopeRejection(\n        'offerChanged',\n        rateLimitState,\n        'The Codex reset-credit offer changed before reset.'\n      )\n    }\n\n    return { managedHomePath: account.managedHomePath, rateLimits: rateLimitState }\n  }\n\n  private hydrateResetCreditAttempts(): void {\n    try {\n      const ledger = this.store.getCodexResetCreditAttemptLedger()\n      this.durableResetLedger = ledger\n      for (const durable of ledger.attempts) {\n        const scopeKey = resetScopeKey(durable.expectedScope)\n        const accountScopeKey = resetAccountScopeKey(durable.expectedScope)\n        this.resetAttemptsByKey.set(durable.idempotencyKey, {\n          expectedScope: durable.expectedScope,","sourceCodeStart":584,"sourceCodeEnd":620,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/src/main/codex-accounts/service.ts#L584-L620","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: attempt built against a stale offerRevision\nservice.consumeRateLimitResetCredit(key, scope) // rejects offerChanged\n\n// after: rebuild scope from refreshed limits\nconst fresh = buildCodexResetCreditExpectedScope({ target, account, limits: rateLimits.getState().codex })!\nawait service.consumeRateLimitResetCredit(crypto.randomUUID(), fresh)","handlingStrategy":"validation","validationCode":"// Recompute the scope from the latest limits immediately before consuming.\nconst fresh = buildCodexResetCreditExpectedScope({ target, account, limits: rateLimits.getState().codex })!\nif (resetScopeKey(fresh) !== resetScopeKey(scope)) {\n  // offer rotated; use `fresh` with a new idempotency key\n}\nawait service.consumeRateLimitResetCredit(crypto.randomUUID(), fresh)","typeGuard":null,"tryCatchPattern":"try {\n  await service.consumeRateLimitResetCredit(key, scope)\n} catch (error) {\n  if (error instanceof CodexResetCreditScopeRejection && error.reason === 'offerChanged') {\n    // rebuild from current limits and retry with a fresh key\n  } else throw error\n}","preventionTips":["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."],"tags":["codex-accounts","rate-limit-reset","scope-rejection","offer","stale-state"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}