refactor: introduce AgentTriggerReason type union as SSOT for error reason strings
Replace scattered reason string literals with a centralized type hierarchy: - AgentTriggerReason: all possible trigger reasons - ClaudeRotationReason, CodexRotationReason, NoFallbackCooldownReason: derived subtypes for specific contexts - AgentErrorClassification, FallbackTriggerResult, CodexRotationTriggerResult: discriminated unions for compile-time narrowing Remove dead export isUsageExhausted() (superseded by isPrimaryNoFallbackCooldownActive). Replace NO_FALLBACK_COOLDOWN_REASONS Set with isNoFallbackCooldownReason() type guard. Add tests for agent-error-detection and provider-retry (423 total passing). Known limitation: 6 `as CodexRotationReason` casts in Codex rotation paths where streamed trigger reason is AgentTriggerReason but runtime value is always CodexRotationReason-compatible. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -68,7 +68,35 @@ export function isClaudeOrgAccessDeniedMessage(text: string): boolean {
|
||||
|
||||
// ── Rotation decision ───────────────────────────────────────────
|
||||
|
||||
export function shouldRotateClaudeToken(reason: string): boolean {
|
||||
export type AgentTriggerReason =
|
||||
| '429'
|
||||
| 'usage-exhausted'
|
||||
| 'auth-expired'
|
||||
| 'org-access-denied'
|
||||
| 'overloaded'
|
||||
| 'network-error'
|
||||
| 'success-null-result';
|
||||
|
||||
export type FallbackTriggerReason = Exclude<
|
||||
AgentTriggerReason,
|
||||
'usage-exhausted' | 'success-null-result'
|
||||
>;
|
||||
|
||||
export type ClaudeRotationReason = Extract<
|
||||
AgentTriggerReason,
|
||||
'429' | 'usage-exhausted' | 'auth-expired' | 'org-access-denied'
|
||||
>;
|
||||
|
||||
export type CodexRotationReason = FallbackTriggerReason;
|
||||
|
||||
export type NoFallbackCooldownReason = Extract<
|
||||
AgentTriggerReason,
|
||||
'usage-exhausted' | 'auth-expired' | 'org-access-denied'
|
||||
>;
|
||||
|
||||
export function shouldRotateClaudeToken(
|
||||
reason: AgentTriggerReason,
|
||||
): reason is ClaudeRotationReason {
|
||||
return (
|
||||
reason === '429' ||
|
||||
reason === 'usage-exhausted' ||
|
||||
@@ -77,6 +105,16 @@ export function shouldRotateClaudeToken(reason: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function isNoFallbackCooldownReason(
|
||||
reason: AgentTriggerReason,
|
||||
): reason is NoFallbackCooldownReason {
|
||||
return (
|
||||
reason === 'usage-exhausted' ||
|
||||
reason === 'auth-expired' ||
|
||||
reason === 'org-access-denied'
|
||||
);
|
||||
}
|
||||
|
||||
// ── Unified error classification ────────────────────────────────
|
||||
|
||||
export type ErrorCategory =
|
||||
@@ -87,11 +125,37 @@ export type ErrorCategory =
|
||||
| 'network-error'
|
||||
| 'none';
|
||||
|
||||
export interface AgentErrorClassification {
|
||||
category: ErrorCategory;
|
||||
reason: string; // '429' | 'auth-expired' | 'org-access-denied' | 'overloaded' | 'network-error' | ''
|
||||
retryAfterMs?: number;
|
||||
}
|
||||
export type AgentErrorClassification =
|
||||
| {
|
||||
category: 'none';
|
||||
reason: '';
|
||||
retryAfterMs?: undefined;
|
||||
}
|
||||
| {
|
||||
category: 'rate-limit';
|
||||
reason: '429';
|
||||
retryAfterMs?: number;
|
||||
}
|
||||
| {
|
||||
category: 'auth-expired';
|
||||
reason: 'auth-expired';
|
||||
retryAfterMs?: undefined;
|
||||
}
|
||||
| {
|
||||
category: 'org-access-denied';
|
||||
reason: 'org-access-denied';
|
||||
retryAfterMs?: undefined;
|
||||
}
|
||||
| {
|
||||
category: 'overloaded';
|
||||
reason: 'overloaded';
|
||||
retryAfterMs?: undefined;
|
||||
}
|
||||
| {
|
||||
category: 'network-error';
|
||||
reason: 'network-error';
|
||||
retryAfterMs?: undefined;
|
||||
};
|
||||
|
||||
const NONE: AgentErrorClassification = {
|
||||
category: 'none',
|
||||
|
||||
Reference in New Issue
Block a user