diff --git a/src/index.ts b/src/index.ts index 4d9265a..2f07a82 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,6 +66,7 @@ import { normalizeStoredSeqCursor } from './message-cursor.js'; import { initCodexTokenRotation } from './codex-token-rotation.js'; import { initTokenRotation } from './token-rotation.js'; import { + shouldStartTokenRefreshLoop, startTokenRefreshLoop, stopTokenRefreshLoop, } from './token-refresh.js'; @@ -300,8 +301,16 @@ async function main(): Promise { initTokenRotation(); initCodexTokenRotation(); - // Start OAuth token auto-refresh (before subsystems that spawn agents) - startTokenRefreshLoop(); + // Only the Claude service owns Claude OAuth refresh to avoid + // cross-service refresh-token races on shared credentials. + if (shouldStartTokenRefreshLoop(SERVICE_AGENT_TYPE)) { + startTokenRefreshLoop(); + } else { + logger.info( + { serviceAgentType: SERVICE_AGENT_TYPE }, + 'Skipping Claude OAuth token auto-refresh for non-Claude service', + ); + } loadState(); diff --git a/src/token-refresh.test.ts b/src/token-refresh.test.ts new file mode 100644 index 0000000..8a11308 --- /dev/null +++ b/src/token-refresh.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest'; + +import { shouldStartTokenRefreshLoop } from './token-refresh.js'; + +describe('shouldStartTokenRefreshLoop', () => { + it('starts refresh for the Claude service', () => { + expect(shouldStartTokenRefreshLoop('claude-code')).toBe(true); + }); + + it('skips refresh for the Codex service', () => { + expect(shouldStartTokenRefreshLoop('codex')).toBe(false); + }); +}); diff --git a/src/token-refresh.ts b/src/token-refresh.ts index 6606440..14cf51d 100644 --- a/src/token-refresh.ts +++ b/src/token-refresh.ts @@ -19,6 +19,7 @@ import path from 'path'; import { logger } from './logger.js'; import { DATA_DIR } from './config.js'; import { getAllTokens, updateTokenValue } from './token-rotation.js'; +import type { AgentType } from './types.js'; const TOKEN_URL = 'https://api.anthropic.com/v1/oauth/token'; const CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e'; @@ -34,6 +35,12 @@ const CHECK_INTERVAL_MS = 5 * 60 * 1000; const REFRESH_BEFORE_EXPIRY_MS = 30 * 60 * 1000; const REQUEST_TIMEOUT_MS = 15_000; +export function shouldStartTokenRefreshLoop( + serviceAgentType: AgentType, +): boolean { + return serviceAgentType === 'claude-code'; +} + interface OAuthCredentials { accessToken: string; refreshToken: string;