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:
@@ -5,6 +5,7 @@ import type { AgentOutput } from './agent-runner.js';
|
||||
// ── Mocks ──────────────────────────────────────────────────────
|
||||
vi.mock('./agent-error-detection.js', () => ({
|
||||
isClaudeUsageExhaustedMessage: vi.fn(() => false),
|
||||
isClaudeOrgAccessDeniedMessage: vi.fn(() => false),
|
||||
isClaudeAuthExpiredMessage: vi.fn(() => false),
|
||||
isClaudeAuthError: vi.fn(() => false),
|
||||
}));
|
||||
@@ -22,6 +23,7 @@ vi.mock('./codex-token-rotation.js', () => ({
|
||||
|
||||
import {
|
||||
isClaudeUsageExhaustedMessage,
|
||||
isClaudeOrgAccessDeniedMessage,
|
||||
isClaudeAuthExpiredMessage,
|
||||
isClaudeAuthError,
|
||||
} from './agent-error-detection.js';
|
||||
@@ -60,6 +62,7 @@ function errorOutput(error: string): AgentOutput {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(isClaudeUsageExhaustedMessage).mockReturnValue(false);
|
||||
vi.mocked(isClaudeOrgAccessDeniedMessage).mockReturnValue(false);
|
||||
vi.mocked(isClaudeAuthExpiredMessage).mockReturnValue(false);
|
||||
vi.mocked(isClaudeAuthError).mockReturnValue(false);
|
||||
vi.mocked(detectFallbackTrigger).mockReturnValue({
|
||||
@@ -164,6 +167,25 @@ describe('evaluateStreamedOutput', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Claude org-access-denied banner', () => {
|
||||
it('suppresses output and returns newTrigger', () => {
|
||||
vi.mocked(isClaudeOrgAccessDeniedMessage).mockReturnValue(true);
|
||||
|
||||
const result = evaluateStreamedOutput(
|
||||
successOutput(
|
||||
'Your organization does not have access to Claude. Please login again or contact your administrator.',
|
||||
),
|
||||
freshState(),
|
||||
claudeOpts,
|
||||
);
|
||||
expect(result.shouldForwardOutput).toBe(false);
|
||||
expect(result.newTrigger).toEqual({ reason: 'org-access-denied' });
|
||||
expect(result.state.streamedTriggerReason).toEqual({
|
||||
reason: 'org-access-denied',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('duplicate trigger suppression', () => {
|
||||
it('suppresses output but returns no newTrigger when already triggered', () => {
|
||||
vi.mocked(isClaudeUsageExhaustedMessage).mockReturnValue(true);
|
||||
|
||||
Reference in New Issue
Block a user