After re-login with newer Claude Code (v2.1.75+), CCM refresh requests
returned persistent 429s. Root cause: CCM omitted the `scope` parameter
that the server now requires for tokens with `user:file_upload` scope.
Changes to fully match Claude Code's OAuth behavior:
- Add `scope` parameter to token refresh request body
- Parse `scope` from refresh response and store back
- Add `subscriptionType`/`rateLimitTier` to credential struct to
preserve Claude Code's profile state on write-back
- Change credential file write to read-modify-write, preserving
other top-level JSON keys (matches Claude Code's BP6 pattern)
- Same for macOS keychain write path
- Increase token expiry buffer from 1 min to 5 min (matching CC's
isOAuthTokenExpired with 300s buffer)
- Add cross-process mkdir-based file lock compatible with Claude
Code's proper-lockfile protocol (~/.claude.lock)
- Add post-failure recovery: re-read credentials from disk after
refresh failure in case another process succeeded
- Add 401/403 "OAuth token has been revoked" recovery in proxy
handler: reload credentials and retry once
Replace bytes.Replace-based UUID injection with proper JSON
unmarshal/re-marshal through map[string]json.RawMessage — the old
approach silently failed when the body used non-canonical JSON escaping.
Return 500 when metadata.user_id is present but in an unrecognized
format, instead of silently passing through with an empty session ID.
Claude Code v2.1.78 changed metadata.user_id from a template literal
(`user_${id}_account_${uuid}_session_${sid}`) to a JSON-encoded object
(`JSON.stringify({device_id, account_uuid, session_id})`), breaking
session ID extraction via `_session_` substring match.
- Fix extractCCMSessionID to try JSON parse first, fallback to legacy
- Remove subscriptionType/rateLimitTier/isMax from oauthCredentials
(profile state does not belong in auth credentials)
- Add state_path option for persisting profile state across restarts
- Parse account.uuid from /api/oauth/profile response
- Inject account_uuid into forwarded requests when client sends it empty
(happens when using ANTHROPIC_AUTH_TOKEN instead of Claude AI OAuth)
External credentials returning 400 are marked unavailable for pollInterval
duration; status stream/poll success clears the rejection early. Default
credentials trigger a stale poll to let the usage API detect account issues
without causing 429 storms.
When the access token expires and refreshToken() gets 429, getAccessToken()
returned the error but left credentials unchanged with no cooldown. Every
subsequent request re-attempted the refresh, creating a burst that overwhelmed
the token endpoint.
- refreshToken() now returns Retry-After duration from 429 response headers
(-1 when no header present, meaning permanently blocked)
- getAccessToken() caches the 429 and blocks further refresh attempts until
Retry-After expires (or permanently if no header)
- reloadCredentials() clears the block when new credentials are loaded from file
- Remove go pollUsage() on upstream errors (unrelated to usage state)
pollUsage(ctx) accepted caller context, and service_status.go passed
r.Context() which gets canceled on client disconnect or service shutdown.
This caused incrementPollFailures → interruptConnections on transient
cancellations. Each implementation now uses its own persistent context:
defaultCredential uses serviceContext, externalCredential uses
getReverseContext().
- Add fiveHourReset/weeklyReset to statusPayload and aggregatedStatus
with weight-averaged reset time aggregation across credential pools
- Rewrite response headers (utilization + reset times) for all users,
not just external credential users
- Rewrite WebSocket rate_limits events for all users with aggregated values
- Add proactive WebSocket status push: synthetic codex.rate_limits events
sent on connection start and on status changes via statusObserver
- Remove one-shot stream forward compatibility (statusStreamHeader,
restoreLastUpdatedIfUnchanged, oneShot detection)
- Remove unused onBecameUnusable field from CCM credential structs
(OCM wires it for WebSocket interruption; CCM has no equivalent)
- Replace time.After with time.NewTimer in doHTTPWithRetry and
connectorLoop to avoid timer leaks on context cancellation
- Pass already-resolved provider to rewriteResponseHeadersForExternalUser
instead of re-resolving via credentialForUser
- Hoist reverseYamuxConfig to package-level var (immutable, no need to
allocate on every call)