fix: detect org-access-denied errors, suppress chat output, and rotate Claude tokens
When a Claude account is suspended, the API returns "Your organization does not have access to Claude" on the success path or "Failed to authenticate. API Error: 403 terminated" on the error path. Both are now classified as 'org-access-denied', suppressed from Discord output, and trigger automatic token rotation. If all tokens are exhausted, enters cooldown without Kimi fallback (same as usage-exhausted and auth-expired). Changes: - agent-error-detection: add isClaudeOrgAccessDeniedMessage(), expand classifyClaudeAuthError() and shouldRotateClaudeToken() - streamed-output-evaluator: detect org-access-denied in success-path chain - provider-fallback: add NO_FALLBACK_COOLDOWN_REASONS set and isPrimaryNoFallbackCooldownActive() helper - provider-retry: handle org-access-denied in rotation loop - message-agent-executor / task-scheduler: use generalized no-fallback cooldown check - Tests: +8 test cases across 5 test files (415 total passing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -54,13 +54,26 @@ export function isClaudeAuthExpiredMessage(text: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function isClaudeOrgAccessDeniedMessage(text: string): boolean {
|
||||
const normalized = text.trim().toLowerCase().replace(/\s+/g, ' ');
|
||||
const hasOrgAccessDeniedMarker = normalized.includes(
|
||||
'does not have access to claude',
|
||||
);
|
||||
const hasRecoveryHint =
|
||||
normalized.includes('please login again') ||
|
||||
normalized.includes('contact your administrator');
|
||||
|
||||
return hasOrgAccessDeniedMarker && hasRecoveryHint;
|
||||
}
|
||||
|
||||
// ── Rotation decision ───────────────────────────────────────────
|
||||
|
||||
export function shouldRotateClaudeToken(reason: string): boolean {
|
||||
return (
|
||||
reason === '429' ||
|
||||
reason === 'usage-exhausted' ||
|
||||
reason === 'auth-expired'
|
||||
reason === 'auth-expired' ||
|
||||
reason === 'org-access-denied'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,13 +82,14 @@ export function shouldRotateClaudeToken(reason: string): boolean {
|
||||
export type ErrorCategory =
|
||||
| 'rate-limit'
|
||||
| 'auth-expired'
|
||||
| 'org-access-denied'
|
||||
| 'overloaded'
|
||||
| 'network-error'
|
||||
| 'none';
|
||||
|
||||
export interface AgentErrorClassification {
|
||||
category: ErrorCategory;
|
||||
reason: string; // '429' | 'auth-expired' | 'overloaded' | 'network-error' | ''
|
||||
reason: string; // '429' | 'auth-expired' | 'org-access-denied' | 'overloaded' | 'network-error' | ''
|
||||
retryAfterMs?: number;
|
||||
}
|
||||
|
||||
@@ -142,6 +156,19 @@ export function classifyClaudeAuthError(
|
||||
if (!error) return NONE;
|
||||
const lower = error.toLowerCase();
|
||||
|
||||
const hasOrgAccessDeniedMarker =
|
||||
lower.includes('your organization does not have access to claude') ||
|
||||
(lower.includes('does not have access to claude') &&
|
||||
lower.includes('contact your administrator'));
|
||||
const hasTerminated403AuthFailure =
|
||||
lower.includes('failed to authenticate') &&
|
||||
lower.includes('403') &&
|
||||
lower.includes('terminated');
|
||||
|
||||
if (hasOrgAccessDeniedMarker || hasTerminated403AuthFailure) {
|
||||
return { category: 'org-access-denied', reason: 'org-access-denied' };
|
||||
}
|
||||
|
||||
if (
|
||||
(lower.includes('failed to authenticate') ||
|
||||
lower.includes('authentication_error')) &&
|
||||
|
||||
Reference in New Issue
Block a user