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:
Eyejoker
2026-03-25 17:24:10 +09:00
parent 3e1e032346
commit 60a9fe86ec
11 changed files with 372 additions and 16 deletions

View File

@@ -37,7 +37,7 @@ export type RotationOutcome =
| { type: 'success' }
| { type: 'error'; message?: string }
| { type: 'needs-fallback'; trigger: TriggerInfo }
| { type: 'no-fallback'; trigger: TriggerInfo }; // usage-exhausted/auth-expired
| { type: 'no-fallback'; trigger: TriggerInfo }; // usage-exhausted/auth-expired/org-access-denied
// ── Shared rotation loop ─────────────────────────────────────────
@@ -63,7 +63,7 @@ export async function runClaudeRotationLoop(
) {
logger.info(
{ ...logContext, reason: trigger.reason },
'Claude rate-limited, retrying with rotated account',
'Claude account unavailable, retrying with rotated account',
);
const attempt = await runAttempt();
@@ -162,10 +162,11 @@ export async function runClaudeRotationLoop(
// ── All tokens exhausted ──
// Usage exhausted or auth-expired: don't fall back to Kimi
// Usage/auth/org access failures: don't fall back to Kimi
if (
trigger.reason === 'usage-exhausted' ||
trigger.reason === 'auth-expired'
trigger.reason === 'auth-expired' ||
trigger.reason === 'org-access-denied'
) {
markPrimaryCooldown(trigger.reason, trigger.retryAfterMs);
logger.info(