April 19, 2026 • Versión: v2.4.x

Rotación de Auth Basada en Timeout Activa Prematuramente Provider Fallback

Los timeouts de solicitud genéricos son incorrectamente tratados como señales de rate-limit, causando enfriamiento y rotación agresiva de perfil de auth que se propaga a provider/model fallback incluso cuando el provider está temporalmente lento.

🔍 Síntomas

Mensajes de error principales

Cuando se produce un timeout de solicitud en un proveedor que soporta auth.profiles, el runner embebido emite mensajes de error en cascada:

Profile openai-codex:default timed out (possible rate limit). Trying next account...
No available auth profile for openai-codex (all in cooldown or unavailable).
... provider=openai model=gpt-5.2 ...   # fallback triggered

Comportamiento observable

  • Agotamiento prematuro de perfiles: Un solo timeout en un perfil causa rotación inmediata al siguiente perfil disponible
  • Acumulación de estados de cooldown: Cada timeout escribe una entrada de cooldown con backoff exponencial (~1m → 5m → 25m → 1h cap)
  • Fallback de modelo innecesario: Cuando todos los perfiles entran en cooldown, el sistema procede a los fallbacks de modelo configurados incluso si el proveedor original está operativo
  • Ruido en logs: Mensajes repetidos de `timed out (possible rate limit)` crean confusión sobre el estado real de rate-limiting

Escenario de reproducción

bash

Activador: Una única solicitud supera el umbral de timeoutSeconds

openclaw run –agent ./my-agent.ts –timeout-seconds 30

Observado: Rotación inmediata de perfil de autenticación sin reintento

Esperado: Al menos un reintento con backoff antes de la rotación

Componentes afectados

ComponenteRuta del archivoPunto de falla
Embedded Runnersrc/agents/pi-embedded-runner/run.tsTimeout → markAuthProfileFailure()advanceAuthProfile()
Auth Profilessrc/agents/auth-profiles/usage.tsProgramación uniforme de cooldown para timeout y razones de rate-limit

🧠 Causa raíz

Análisis arquitectónico

El bucle de failover de auth-profile en el runner embebido confunde dos modos de falla distintos:

  1. Señales fuertes de rate-limit: HTTP 429, códigos de error específicos del proveedor (ej., error.code === "rate_limit_exceeded")
  2. Señales transitorias débiles: Timeouts genéricos de solicitudes (latencia de red temporal, streaming lento, pico de latencia del SDK)

Desglose de la ruta del código

Archivo: src/agents/pi-embedded-runner/run.ts

El handler de timeout se ejecuta sin una compuerta de reintento:

typescript // Flujo simplificado (números de línea aproximados) async function executeWithAuthProfile(provider, profile, request) { try { const result = await executeRequest(request, { timeout: timeoutMs }); return result; } catch (error) { if (isTimeout(error)) { // ❌ Sin compuerta de reintento - marcado de falla inmediato markAuthProfileFailure(profile, { reason: “timeout” }); advanceAuthProfile(provider); // ← Dispara rotación throw new NoAvailableAuthProfileError(provider); }

if (isRateLimit(error)) {
  // ✓ Correcto: señal fuerte justifica cooldown inmediato
  markAuthProfileFailure(profile, { reason: "rate_limit" });
  advanceAuthProfile(provider);
  throw new NoAvailableAuthProfileError(provider);
}

} }

Archivo: src/agents/auth-profiles/usage.ts

El cálculo de cooldown aplica una programación exponencial idéntica para todas las razones de falla:

typescript function calculateAuthProfileCooldownMs(errorCount: number): number { // ~1m → 5m → 25m → 1h cap const baseMs = 60_000; const cooldown = baseMs * Math.pow(5, Math.min(errorCount - 1, 3)); return Math.min(cooldown, 3_600_000); // 1-hour cap }

// Llamado de forma idéntica para razones “timeout” y “rate_limit”

Secuencia de cascada de fallas

  1. Ocurre timeout de solicitud
  2. markAuthProfileFailure(reason: “timeout”) escribe entrada de cooldown
  3. advanceAuthProfile() rota al siguiente perfil
  4. Si todos los perfiles no están disponibles: a. Se lanza NoAvailableAuthProfileError b. Se verifican agents.defaults.model.fallbacks c. Se procede al fallback de modelo/proveedor ← ¡Prematuro!
  5. Si no hay fallbacks: La solicitud falla completamente

Por qué esto es incorrecto

Tipo de señalConfiabilidadRespuesta apropiada
HTTP 429AltaCooldown inmediato + rotar
Código de error del proveedorAltaCooldown inmediato + rotar
Timeout genéricoBaja (transitoria)Reintentar con backoff antes del cooldown

Los timeouts genéricos son indistinguibles de:

  • Picos temporales de latencia de red
  • Inicio lento de respuesta de streaming
  • Sobrecarga de conexión del SDK
  • Carga temporal del lado del proveedor

Brecha de configuración

No existe configuración para controlar el comportamiento de reintento por razón:

typescript // Actual: No existe config retrySameProfileOnTimeout agents: { defaults: { timeoutSeconds: 30, modelFailover: { // Faltante: retrySameProfileOnTimeout, retryBackoffMs } } }

🛠️ Solución paso a paso

Recomendado: Adición de compuerta de reintento mínima

Esta solución agrega una compuerta de reintento por razón para las fallas de timeout antes de activar el cooldown y la rotación.

Paso 1: Extender el esquema de configuración

Archivo: src/config/schema.ts

Agregar nuevos campos a la configuración de failover de modelo:

typescript // Antes interface ModelFailoverConfig { fallbacks: string[]; }

// Después interface ModelFailoverConfig { fallbacks: string[]; retrySameProfileOnTimeout: number; // Por defecto: 1 retryBackoffMs: [number, number]; // Por defecto: [300, 1200] ms (mín, máx jitter) }

Paso 2: Implementar compuerta de reintento en el Runner Embebido

Archivo: src/agents/pi-embedded-runner/run.ts

Modificar el manejo de timeout para incluir lógica de reintento:

typescript // Antes async function executeWithAuthProfile(provider, profile, request) { try { return await executeRequest(request, { timeout: timeoutMs }); } catch (error) { if (isTimeout(error)) { markAuthProfileFailure(profile, { reason: “timeout” }); advanceAuthProfile(provider); throw new NoAvailableAuthProfileError(provider); } // … manejo de rate limit } }

// Después async function executeWithAuthProfile(provider, profile, request, options = {}) { const config = getConfig(); const { retrySameProfileOnTimeout = 1, retryBackoffMs = [300, 1200] } = config.agents?.defaults?.modelFailover ?? {};

// Seguimiento de reintentos por perfil por sesión const retryState = getOrCreateRetryState(profile.id);

try { return await executeRequest(request, { timeout: timeoutMs }); } catch (error) { if (isTimeout(error)) { const maxRetries = retrySameProfileOnTimeout; const currentRetries = retryState.consecutiveTimeouts;

  if (currentRetries < maxRetries) {
    // Reintentar en el mismo perfil con backoff con jitter
    const [minDelay, maxDelay] = retryBackoffMs;
    const delay = minDelay + Math.random() * (maxDelay - minDelay);
    
    console.log(
      `Profile ${profile.id} timed out. ` +
      `Retry ${currentRetries + 1}/${maxRetries} in ${Math.round(delay)}ms...`
    );
    
    retryState.consecutiveTimeouts++;
    await sleep(delay);
    
    // Re-ejecutar en el mismo perfil (sin escribir cooldown)
    return await executeWithAuthProfile(
      provider, profile, request, 
      { ...options, isRetry: true }
    );
  }
  
  // Reintentos agotados: aplicar cooldown + rotar
  console.log(
    `Profile ${profile.id} timed out (${maxRetries} retries exhausted). ` +
    `Trying next account...`
  );
  
  markAuthProfileFailure(profile, { reason: "timeout" });
  clearRetryState(profile.id);  // Reiniciar contador de reintentos
  advanceAuthProfile(provider);
  throw new NoAvailableAuthProfileError(provider);
}

// Manejo de rate-limit sin cambios (cooldown inmediato)
if (isRateLimit(error)) {
  markAuthProfileFailure(profile, { reason: "rate_limit" });
  clearRetryState(profile.id);
  advanceAuthProfile(provider);
  throw new NoAvailableAuthProfileError(provider);
}

throw error;

} }

Paso 3: Agregar gestión de estado de reintento

Archivo: src/agents/auth-profiles/retry-state.ts (nuevo archivo)

typescript interface RetryState { consecutiveTimeouts: number; lastRetryTimestamp: number; }

const retryStateMap = new Map<string, RetryState>();

export function getOrCreateRetryState(profileId: string): RetryState { if (!retryStateMap.has(profileId)) { retryStateMap.set(profileId, { consecutiveTimeouts: 0, lastRetryTimestamp: 0 }); } return retryStateMap.get(profileId)!; }

export function clearRetryState(profileId: string): void { retryStateMap.delete

Evidencia y fuentes

Esta guía de solución de problemas fue sintetizada automáticamente por la tubería de inteligencia de FixClaw a partir de las discusiones de la comunidad.