April 19, 2026 • Version: v2.4.x

Timeout-gesteuerte Auth-Rotation löst vorzeitig Provider-Fallback aus

Allgemeine Request-Timeouts werden fälschlicherweise als Rate-Limit-Signale behandelt, was zu aggressivem Auth-Profile-Cooldown und -Rotation führt, die in Provider/Model-Fallback kaskadieren, selbst wenn der Provider vorübergehend langsam ist.

🔍 Symptome

Primäre Fehlermeldungen

Wenn bei einem Anbieter, der auth.profiles unterstützt, eine Anfrage-Zeitüberschreitung auftritt, gibt der eingebettete Runner kaskadierende Fehlermeldungen aus:

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

Beobachtbares Verhalten

  • Vorzeitige Erschöpfung der Profile: Eine einzelne Zeitüberschreitung bei einem Profil führt zu einer sofortigen Rotation zum nächsten verfügbaren Profil
  • Akkumulation des Cooldown-Zustands: Jede Zeitüberschreitung schreibt einen Cooldown-Eintrag mit exponentieller Rückkehr (~1m → 5m → 25m → 1h Deckelung)
  • Unnötiger Modell-Fallback: Wenn alle Profile in den Cooldown-Modus wechseln, verwendet das System die konfigurierten Modell-Fallbacks, auch wenn der ursprüngliche Anbieter funktionsfähig ist
  • Log-Rauschen: Wiederholte `timed out (possible rate limit)`-Meldungen erzeugen Verwirrung über den tatsächlichen Rate-Limiting-Status

Reproduktionsszenario

bash

Trigger: Eine einzelne Anfrage überschreitet den timeoutSeconds-Schwellenwert

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

Beobachtet: Sofortige Auth-Profile-Rotation ohne Wiederholung

Erwartet: Mindestens ein Wiederholungsversuch mit Backoff vor der Rotation

Betroffene Komponenten

KomponenteDateipfadFehlerstelle
Embedded Runnersrc/agents/pi-embedded-runner/run.tsTimeout → markAuthProfileFailure()advanceAuthProfile()
Auth Profilessrc/agents/auth-profiles/usage.tsEinheitlicher Cooldown-Zeitplan für Timeout- und Rate-Limit-Gründe

🧠 Ursache

Architekturanalyse

Die Auth-Profile-Failover-Schleife im eingebetteten Runner vermischt zwei unterschiedliche Fehlermodi:

  1. Starke Rate-Limit-Signale: HTTP 429, anbieterspezifische Fehlercodes (z.B. error.code === "rate_limit_exceeded")
  2. Schwache transiente Signale: Generische Anfrage-Zeitüberschreitungen (Netzwerkaussetzer, langsames Streaming, SDK-Latenzspitzen)

Code-Pfad-Aufschlüsselung

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

Der Timeout-Handler führt ohne Wiederholungs-Gate aus:

typescript // Vereinfachter Ablauf (Zeilennummern approximativ) async function executeWithAuthProfile(provider, profile, request) { try { const result = await executeRequest(request, { timeout: timeoutMs }); return result; } catch (error) { if (isTimeout(error)) { // ❌ Kein Retry-Gate - sofortige Fehlermarkierung markAuthProfileFailure(profile, { reason: “timeout” }); advanceAuthProfile(provider); // ← Löst Rotation aus throw new NoAvailableAuthProfileError(provider); }

if (isRateLimit(error)) {
  // ✓ Korrekt: starkes Signal rechtfertigt sofortigen Cooldown
  markAuthProfileFailure(profile, { reason: "rate_limit" });
  advanceAuthProfile(provider);
  throw new NoAvailableAuthProfileError(provider);
}

} }

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

Die Cooldown-Berechnung wendet identische exponentielle Zeitpläne für alle Fehlergründe an:

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

// Identisch aufgerufen für “timeout” und “rate_limit” Gründe

Fehlerkaskaden-Sequenz

  1. Anfrage-Zeitüberschreitung tritt auf
  2. markAuthProfileFailure(reason: “timeout”) schreibt Cooldown-Eintrag
  3. advanceAuthProfile() rotiert zum nächsten Profil
  4. Wenn alle Profile nicht verfügbar: a. NoAvailableAuthProfileError wird ausgelöst b. Überprüfe agents.defaults.model.fallbacks c. Fortfahren zum Fallback-Modell/Anbieter ← Verfrüht!
  5. Wenn keine Fallbacks: Anfrage schlägt vollständig fehl

Warum dies falsch ist

SignaltypZuverlässigkeitAngemessene Reaktion
HTTP 429HochSofortiger Cooldown + Rotation
Anbieter-FehlercodeHochSofortiger Cooldown + Rotation
Generischer TimeoutNiedrig (transient)Wiederholung mit Backoff vor Cooldown

Generische Zeitüberschreitungen sind nicht unterscheidbar von:

  • Temporären Netzwerk-Latenzspitzen
  • Langsamer Streaming-Antwort-Initiation
  • SDK-Verbindungsoverhead
  • Temporärer anbieterspezifischer Last

Konfigurationslücke

Es existiert keine Konfiguration zur Steuerung des Wiederholungsverhaltens pro Grund:

typescript // Aktuell: Kein retrySameProfileOnTimeout Config vorhanden agents: { defaults: { timeoutSeconds: 30, modelFailover: { // Fehlend: retrySameProfileOnTimeout, retryBackoffMs } } }

🛠️ Schritt-für-Schritt-Lösung

Empfohlen: Minimale Retry-Gate-Ergänzung

Diese Lösung fügt ein pro-Grund-Retry-Gate für Timeout-Fehler hinzu, bevor Cooldown und Rotation ausgelöst werden.

Schritt 1: Konfigurationsschema erweitern

Datei: src/config/schema.ts

Neue Felder zur Modell-Failover-Konfiguration hinzufügen:

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

// Nachher interface ModelFailoverConfig { fallbacks: string[]; retrySameProfileOnTimeout: number; // Standard: 1 retryBackoffMs: [number, number]; // Standard: [300, 1200] ms (min, max jitter) }

Schritt 2: Retry-Gate im Embedded Runner implementieren

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

Timeout-Handling mit Wiederholungslogik modifizieren:

typescript // Vorher 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); } // … Rate-Limit-Handling } }

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

// Wiederholungen pro Profil pro Sitzung verfolgen 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) {
    // Dasselbe Profil mit gejittertem Backoff wiederholen
    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);
    
    // Erneut auf demselben Profil ausführen (kein Cooldown geschrieben)
    return await executeWithAuthProfile(
      provider, profile, request, 
      { ...options, isRetry: true }
    );
  }
  
  // Wiederholungen erschöpft: Cooldown anwenden + rotieren
  console.log(
    `Profile ${profile.id} timed out (${maxRetries} retries exhausted). ` +
    `Trying next account...`
  );
  
  markAuthProfileFailure(profile, { reason: "timeout" });
  clearRetryState(profile.id);  // Retry-Zähler zurücksetzen
  advanceAuthProfile(provider);
  throw new NoAvailableAuthProfileError(provider);
}

// Rate-Limit-Handling unverändert (sofortiger Cooldown)
if (isRateLimit(error)) {
  markAuthProfileFailure(profile, { reason: "rate_limit" });
  clearRetryState(profile.id);
  advanceAuthProfile(provider);
  throw new NoAvailableAuthProfileError(provider);
}

throw error;

} }

Schritt 3: Retry-Zustandsverwaltung hinzufügen

Datei: src/agents/auth-profiles/retry-state.ts (neue Datei)

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(profileId); }

export function clearAllRetryStates(): void { retryStateMap.clear(); }

Schritt 4: Standardkonfiguration aktualisieren

Datei: src/config/defaults.ts

typescript // Vorher export const defaultAgentsConfig = { defaults: { timeoutSeconds: 30, modelFailover: { fallbacks: [] } } };

// Nachher export const defaultAgentsConfig = { defaults: { timeoutSeconds: 30, modelFailover: { fallbacks: [], retrySameProfileOnTimeout: 1, retryBackoffMs: [300, 1200] } } };

Konfiguration nach der Lösung

json5 { “agents”: { “defaults”: { “timeoutSeconds”: 30, “modelFailover”: { “fallbacks”: [“gpt-4-turbo”, “claude-3-opus”], “retrySameProfileOnTimeout”: 1, // Wiederholungen vor Cooldown (0 = deaktiviert) “retryBackoffMs”: [300, 1200] // [min, max] gejitterte Verzögerung in ms } } } }

Optional: Pro-Grund-Cooldown-Zeitpläne

Für eine ausgereiftere Lösung, Cooldown-Zeitpläne nach Grund differenzieren:

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

typescript const COOLDOWN_SCHEDULES = { timeout: { baseMs: 10_000, // 10 Sekunden (vs 60s für Rate-Limit) multiplier: 2, // 10s → 20s → 40s → 80s capMs: 300_000 // 5 Minuten Deckelung (vs 1 Stunde) }, rate_limit: { baseMs: 60_000, multiplier: 5, // 60s → 5m → 25m → 1h capMs: 3_600_000 // 1 Stunde Deckelung } };

export function calculateAuthProfileCooldownMs( errorCount: number, reason: ’timeout’ | ‘rate_limit’ ): number { const schedule = COOLDOWN_SCHEDULES[reason]; const cooldown = schedule.baseMs * Math.pow(schedule.multiplier, Math.min(errorCount - 1, 3)); return Math.min(cooldown, schedule.capMs); }

🧪 Verifizierung

Unit-Test: Einzelne Zeitüberschreitung wiederholt dasselbe Profil

Datei: src/agents/pi-embedded-runner/__tests__/timeout-retry.test.ts

typescript describe(‘Timeout retry behavior’, () => { const mockProfile = { id: ’test-profile’, provider: ‘openai-codex’ };

beforeEach(() => { clearAllRetryStates(); });

test(‘single timeout retries same profile without cooldown’, async () => { const executeRequest = jest.fn() .mockRejectedValueOnce(new TimeoutError()) .mockResolvedValueOnce({ data: ‘success’ });

const markAuthProfileFailure = jest.fn();
const advanceAuthProfile = jest.fn();

await executeWithAuthProfile('openai-codex', mockProfile, mockRequest, {
  executeRequest,
  markAuthProfileFailure,
  advanceAuthProfile,
  config: { retrySameProfileOnTimeout: 1, retryBackoffMs: [0, 10] }
});

// Verify retry occurred
expect(executeRequest).toHaveBeenCalledTimes(2);

// Verify NO cooldown was written
expect(markAuthProfileFailure).not.toHaveBeenCalled();

// Verify NO rotation occurred
expect(advanceAuthProfile).not.toHaveBeenCalled();

});

test(’exhausted retries triggers cooldown and rotation’, async () => { const executeRequest = jest.fn() .mockRejectedValue(new TimeoutError());

const markAuthProfileFailure = jest.fn();
const advanceAuthProfile = jest.fn();

await expect(
  executeWithAuthProfile('openai-codex', mockProfile, mockRequest, {
    executeRequest,
    markAuthProfileFailure,
    advanceAuthProfile,
    config: { retrySameProfileOnTimeout: 1, retryBackoffMs: [0, 10] }
  })
).rejects.toThrow(NoAvailableAuthProfileError);

// Verify retry exhausted
expect(executeRequest).toHaveBeenCalledTimes(2);

// Verify cooldown WAS written
expect(markAuthProfileFailure).toHaveBeenCalledWith(
  mockProfile, 
  { reason: 'timeout' }
);

// Verify rotation occurred
expect(advanceAuthProfile).toHaveBeenCalledWith('openai-codex');

});

test(‘rate-limit triggers immediate cooldown (no retry)’, async () => { const executeRequest = jest.fn().mockRejectedValue({ status: 429, code: ‘rate_limit_exceeded’ });

const markAuthProfileFailure = jest.fn();
const advanceAuthProfile = jest.fn();

await expect(
  executeWithAuthProfile('openai-codex', mockProfile, mockRequest, {
    executeRequest,
    markAuthProfileFailure,
    advanceAuthProfile,
    config: { retrySameProfileOnTimeout: 1, retryBackoffMs: [0, 10] }
  })
).rejects.toThrow(NoAvailableAuthProfileError);

// Verify NO retry for rate-limit
expect(executeRequest).toHaveBeenCalledTimes(1);
expect(markAuthProfileFailure).toHaveBeenCalledWith(
  mockProfile, 
  { reason: 'rate_limit' }
);

}); });

Integrationstest: Mehrere Profile + Intermittierende Zeitüberschreitungen

typescript test(‘intermittent timeouts do not exhaust all profiles’, async () => { const profiles = [ { id: ‘profile-1’, provider: ‘openai-codex’ }, { id: ‘profile-2’, provider: ‘openai-codex’ }, { id: ‘profile-3’, provider: ‘openai-codex’ } ];

// Profile 1: timeout → retry → success // Profile 2: timeout → retry → timeout → cooldown // Profile 3: success const executeRequest = jest.fn() .mockImplementation(({ profile }) => { if (profile.id === ‘profile-1’) return Promise.resolve({ data: ‘ok’ }); if (profile.id === ‘profile-2’) return Promise.reject(new TimeoutError()); if (profile.id === ‘profile-3’) return Promise.resolve({ data: ‘ok’ }); });

const result = await runWithAuthProfiles(profiles, mockRequest, { executeRequest, config: { retrySameProfileOnTimeout: 1, retryBackoffMs: [0, 10] } });

// Should succeed using profile-1 or profile-3 expect(result).toBeDefined();

// profile-2 cooldown should be recorded expect(getProfileCooldown(‘profile-2’)).toBeDefined(); expect(getProfileCooldown(‘profile-3’)).toBeUndefined(); });

Manuelle Verifizierungsschritte

bash

1. Debug-Logging aktivieren

export OPENCLAW_LOG_LEVEL=debug

2. Agent mit bekanntem timeout-anfälligen Szenario ausführen

openclaw run –agent ./test-agent.ts –timeout-seconds 5

3. Erwartete Log-Ausgabe mit Lösung:

[DEBUG] Profile openai-codex:default timed out. Retry 1/1 in 847ms…

[DEBUG] Request succeeded on retry

NICHT: “Trying next account…” bei erster Zeitüberschreitung

4. Nach Lösung, wenn Wiederholungen erschöpft:

[INFO] Profile openai-codex:default timed out (1 retries exhausted). Trying next account…

[INFO] Cooldown applied: 10000ms for timeout reason

Verifizierungs-Checkliste

KriteriumTestmethodeErwartetes Ergebnis
Einzelne Zeitüberschreitung wiederholt dasselbe ProfilUnit-Test2 executeRequest-Aufrufe, 0 Cooldown-Schreibvorgänge
Erschöpfte Wiederholungen → CooldownUnit-TestmarkAuthProfileFailure aufgerufen mit reason: “timeout”
Rate-Limit umgeht WiederholungUnit-Test1 executeRequest-Aufruf, sofortiger Cooldown
Korrekte Log-AusgabeManuelle PrüfungRetry-Zähler + Verzögerung vor Cooldown angezeigt
Verhinderung der ProfilerschöpfungIntegrationstest3 intermittierende Zeitüberschreitungen verwenden maximal 2 Profile

⚠️ Häufige Fehler

Randfälle und umgebungsspezifische Fallen

  • Jitter-Bereich zu eng: Wenn retryBackoffMs zu klein ist (z.B. [1, 10]), können Wiederholungen sofort auf dasselbe transiente Problem treffen. Empfohlenes Minimum: [300, 1200]
  • Risiko endloser Wiederholungsschleife: Wenn retrySameProfileOnTimeout sehr hoch gesetzt ist ohne globales Timeout, können Anfragen unbegrenzt hängen. Immer mit timeoutSeconds kombinieren
  • Retry-Zustands-Leckage zwischen Sitzungen: Sicherstellen, dass clearRetryState() bei erfolgreicher Profilrotation aufgerufen wird, um veraltete Retry-Zähler zu verhindern
  • Speicherdruck bei langlebigen Prozessen: Die Retry-Zustandsmap sollte WeakMap oder explizite Bereinigung für Profilobjekte verwenden

macOS-spezifische Überlegungen

bash

Die Simulation der Netzwerklatenz kann abweichen

Testen mit: sudo scutil –set InitialTSR 5000

Docker-spezifische Überlegungen

bash

Container-Netzwerk-Timeouts können je nach Ressourcenbeschränkungen variieren

Sicherstellen, dass der Container ausreichende Ressourcen für das Timeout-Handling hat:

docker run –memory=512m –cpus=1 …

Windows-spezifische Überlegungen

powershell

PowerShell-Schlafpräzision unterscheidet sich von Unix

Sicherstellen, dass die Sleep-Implementierung eine Monotonic-Clock verwendet:

[System.Diagnostics.Stopwatch]::GetTimestamp()

Konfigurationsfallen

typescript // ❌ FALSCH: retryBackoffMs umgekehrt (min > max) { retryBackoffMs: [1200, 300] }

// ✅ KORREKT: [min, max] { retryBackoffMs: [300, 1200] }

// ❌ FALSCH: retrySameProfileOnTimeout = 0 deaktiviert das gesamte Timeout-Handling // (sollte “Wiederholung bei Timeout deaktiviert” sein, nicht “unendliche Wiederholungen”) { retrySameProfileOnTimeout: 0 }

// ✅ KORREKT: Zum Deaktivieren einen großen Backoff oder separate Konfiguration verwenden { retrySameProfileOnTimeout: 0, timeoutRetriesEnabled: false }

Wechselwirkung mit bestehendem Fallback-Verhalten

Wenn agents.defaults.model.fallbacks konfiguriert ist, gilt das Wiederholungsverhalten pro Anbieter:

json5 { “agents”: { “defaults”: { “modelFailover”: { “fallbacks”: [“gpt-4”, “claude-3”], “retrySameProfileOnTimeout”: 1, “retryBackoffMs”: [500, 2000] } } } }

Sequenz mit Lösung:

  1. Anfrage an gpt-4-turbo mit openai-codex:profile-1 führt zu Zeitüberschreitung
  2. Dasselbe profile-1 wiederholen (kein Cooldown geschrieben)
  3. Wiederholung schlägt fehl → Cooldown + Rotation zu profile-2
  4. Wenn profile-2 ebenfalls erschöpft → Fallback auf gpt-4 (frische Profile)

🔗 Zugehörige Fehler

Kontextuell verbundene Fehlercodes und historische Probleme

Fehler / ProblemBeschreibungBeziehung
NoAvailableAuthProfileErrorWird ausgelöst, wenn alle Profile im Cooldown sindPrimäres Symptom des aggressiven Timeout-Handlings
Profile ${id} timed out (possible rate limit)Irreführende Log-MeldungImpliziert Rate-Limit, obwohl nur Timeout aufgetreten ist
MARK_AUTH_PROFILE_FAILUREAuth-Profil-FehlerverfolgungKernmechanismus, der ein Retry-Gate benötigt
HTTP 429Explizites Rate-Limit-SignalKorrekter Auslöser für Cooldown (sollte unverändert bleiben)
error.code === “insufficient_quota”Anbieterspezifischer QuotenfehlerStarkes Signal, sollte Wiederholung umgehen

Zugehörige Konfigurationsparameter

ParameterAktuelles VerhaltenProblem
agents.defaults.timeoutSecondsLöst Profilrotation ausZu aggressiv für transiente Zeitüberschreitungen
agents.defaults.modelFailover.fallbacksWird ausgelöst, wenn alle Profile erschöpft sindUnnötigerweise durch einzelne Zeitüberschreitung ausgelöst
agents.defaults.maxConcurrentRequestsKann Timeout-Probleme verschlimmernHohe Parallelität + Timeouts = schnellere Profilerschöpfung

Historischer Kontext

Dieses Problem manifestiert sich je nach Konfiguration unterschiedlich:

  • High-Traffic-Deployments: Mehrere gleichzeitige Zeitüberschreitungen können alle Profile schnell erschöpfen
  • Low-Traffic-Deployments: Einzelne Zeitüberschreitung kann das einzige Signal sein, verursacht aber trotzdem Fallback
  • Gemeinsame Infrastruktur: Timeouts eines Teams beeinflussen die Profilverfügbarkeit anderer Teams

Verweise auf zugehörige OpenClaw-Komponenten

  • src/agents/pi-embedded-runner/run.ts - Embedded-Runner-Auth-Schleife
  • src/agents/auth-profiles/usage.ts - Cooldown-Berechnung
  • src/agents/auth-profiles/cooldown-store.ts - Persistenter Cooldown-Zustand
  • src/config/schema.ts - Konfigurationstyp-Definitionen
  • src/errors/auth-profile-errors.ts - Fehlerklassendefinitionen

Belege & Quellen

Diese Troubleshooting-Anleitung wurde automatisch von der FixClaw Intelligence Pipeline aus Community-Diskussionen synthetisiert.