fix: OAuth tokens expire after inactivity despite /api/oauth/usage polling
Problem
OAuth tokens expire after periods of inactivity (typically 1-2 hours), causing "Token refresh failed: 400" errors when users return to OpenCode. This has been reported in multiple issues:
- #6559: Claude subscription token expires after a period of time
- #4992: Getting "Unauthorized: token expired" during conversation
Root Cause Analysis
The existing token refresh logic in opencode-anthropic-auth plugin only triggers during API calls. If the app is idle or closed:
- No API calls are made
- The access token expires (~1 hour)
- Eventually the refresh token also becomes invalid
- On next use: "Token refresh failed: 400"
The previous fix attempt (PR #9112) using /api/oauth/usage endpoint did not work because:
- That endpoint only returns usage statistics
- It does not refresh the OAuth token
- It does not extend the session lifetime
Solution
Implement a proactive token keepalive that:
-
Refreshes tokens before they expire - Uses Anthropic's OAuth token endpoint (
POST /v1/oauth/token) withgrant_type: refresh_token - Runs every 30 minutes - More frequent to catch expiring tokens
- Refreshes 10 minutes before expiry - Proactive refresh buffer
- Pings with Messages API - After refresh, sends minimal request to maintain session activity
- Updates stored tokens - Saves refreshed tokens to auth.json
Implementation Details
See PR #9122 for the implementation:
New file: packages/opencode/src/auth/keepalive.ts
// Key functions:
- refreshAnthropicToken() - Calls Anthropic OAuth token endpoint
- updateStoredToken() - Persists refreshed token to auth.json
- keepAliveAccount() - Checks expiry, refreshes if needed, then pings
- pingAllAnthropicAccounts() - Processes all OAuth accounts
Token Refresh Flow
Every 30 minutes:
For each Anthropic OAuth account:
1. Check if token expires within 10 minutes
2. If yes: POST /v1/oauth/token (refresh_token grant)
3. Update stored token
4. POST /v1/messages (minimal ping)
Modified: packages/opencode/src/project/bootstrap.ts
- Initializes
AuthKeepAliveon app startup
Testing
- Start OpenCode with Anthropic OAuth authentication
- Leave idle for several hours or overnight
- Check logs for
auth.keepaliveservice:INFO auth.keepalive: starting oauth keepalive {"intervalMs": 1800000} INFO auth.keepalive: token expired or expiring soon, refreshing {"recordId": "...", "expiresIn": 300} INFO auth.keepalive: token refresh successful {"recordId": "..."} INFO auth.keepalive: keepalive ping successful {"recordId": "..."} - Resume usage - no token expiration errors
Important Notes
- Uses the same
client_idasopencode-anthropic-authplugin - App must be running for keepalive to work (cannot prevent expiration if app is closed)
- Token consumption: ~10 tokens per ping (negligible)
This issue might be a duplicate of existing issues. Please check:
- #9111: OAuth token expires after inactivity causing 'Token refresh failed: 400' - Same root cause analysis and similar proposed solution
- #6559: Claude subscription token expires after a period of time - Reports the same token expiration after inactivity
- #4992: Getting "Unauthorized: token expired" during conversation - Similar token expiration errors during usage
Feel free to ignore if none of these address your specific case.
Closing this issue - the proposed solution (proactive token refresh via keepalive) doesn't solve the problem. Users still get 'Token refresh failed: 400' errors after overnight inactivity, even with the keepalive running.
The issue appears to be that Anthropic's OAuth refresh tokens themselves expire or become invalid after extended periods, which cannot be prevented by periodic pinging or token refresh.
This may be a limitation of Anthropic's OAuth implementation for consumer accounts (Claude Max/Pro).