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:
Eyejoker
2026-03-25 17:46:43 +09:00
parent b86fd1ad89
commit 94eeaa5e3e
10 changed files with 290 additions and 53 deletions

View File

@@ -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',