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

@@ -5,7 +5,11 @@
* to eliminate the ~255-line structural duplication.
*/
import { shouldRotateClaudeToken } from './agent-error-detection.js';
import {
isNoFallbackCooldownReason,
shouldRotateClaudeToken,
type AgentTriggerReason,
} from './agent-error-detection.js';
import { logger } from './logger.js';
import { getErrorMessage } from './utils.js';
import {
@@ -21,7 +25,7 @@ import {
// ── Types ────────────────────────────────────────────────────────
export interface TriggerInfo {
reason: string;
reason: AgentTriggerReason;
retryAfterMs?: number;
}
@@ -163,11 +167,7 @@ export async function runClaudeRotationLoop(
// ── All tokens exhausted ──
// Usage/auth/org access failures: don't fall back to Kimi
if (
trigger.reason === 'usage-exhausted' ||
trigger.reason === 'auth-expired' ||
trigger.reason === 'org-access-denied'
) {
if (isNoFallbackCooldownReason(trigger.reason)) {
markPrimaryCooldown(trigger.reason, trigger.retryAfterMs);
logger.info(
{ ...logContext, reason: trigger.reason },