claude-failover/VERSION.md
Ubuntu 7c5f8384fa fix(quota): add cooldown + 2-poll confirmation to prevent swap ping-pong
Anthropic HTTP 500 errors surface in the TUI with payloads containing
"rate limit" text, which the monitor was matching against quotaPatterns
and treating as a real 429 quota hit. With no cooldown and no
confirmation, a burst of 500s produced sub-minute ping-pong swaps that
tore down user sessions.

Two-layer fix:
- quota.reactivate_cooldown (already in config, 5m) now gates the
  monitor too — not just the dispatcher. A completed swap suppresses
  further detection for the cooldown window.
- A hit with no parseable reset time is treated as suspected only on
  the first poll; a second consecutive poll is required before
  emitting SwapRequested. Legitimate 429s with "resets in ..." still
  swap instantly on the first detection.

Adds state.RecordSwap / LastSwapInfo for the cooldown, and a
forensic log line on every detection: trigger_session, matched
pattern, 120-char pane snippet.

Tests cover: instant swap with reset, 2-poll confirmation without
reset, and suspected-state reset on recovery.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:18:27 +00:00

4.3 KiB

Version actuelle : 0.2.2

[0.2.2] - 2026-04-15

Type: Patch — Confirmation requise pour les faux positifs (root cause)

Corrigé

  • Cause racine du ping-pong : les erreurs HTTP 500 transitoires d'Anthropic contiennent le texte "rate limit" dans leur payload ({"type":"api_error",...} avec des traces mentionnant "rate limit"). Le monitor les confondait avec de vrais 429 quota hits. La v0.2.1 cassait la boucle via cooldown, mais un swap par salve de 500s pouvait encore tuer les sessions dédiées.
  • Le nouveau log forensique v0.2.1 a révélé exactement ça (snippet capturé : API Error: 500 {"type":"error","error":{"type":"api_error",...}).

Ajouté

  • Confirmation 2-polls pour les hits sans reset time : si extractResetTime ne trouve rien (= pas un vrai 429), le monitor marque l'état suspectedHitAt et attend le poll suivant. Le swap n'est émis que si la détection persiste. Un hit isolé (= erreur 500 transitoire) est absorbé sans swap.
  • Un vrai 429 (avec resets in 45 minutes ou resets at 8pm) continue à déclencher un swap instantané.
  • Monitor.suspectedHitAt (not locked — only touched from poll goroutine).
  • 3 nouveaux tests : TestPollTriggersSwitchOnTwoBlockedPoolWithReset, TestPollRequiresConfirmationWhenNoResetTime, TestPollSuspectedHitClearedOnRecovery.

Tests effectués

  • go test ./... — full suite passe
  • Service redémarré, état consistant

Fichiers modifiés

  • internal/quota/monitor.go
  • internal/quota/monitor_test.go

[0.2.1] - 2026-04-15

Type: Patch — Fix boucle de swaps infinis (ping-pong)

Corrigé

  • Boucle infinie de swaps : le monitor pouvait émettre des SwapRequested toutes les 30s, créant un ping-pong entre comptes quand du texte de pane (anciens errors Anthropic 500 / TUI banners) matchait quotaPatterns avec reset="". En prod, interval observé descendant jusqu'à 1 min.
  • Cause racine : aucun cooldown global entre swaps dans la boucle de détection. La config quota.reactivate_cooldown (5m) existait mais n'était utilisée que par le dispatcher, pas par le monitor.

Ajouté

  • state.QuotaState.LastSwapAt/LastSwapFrom/LastSwapTo + RecordSwap() + LastSwapInfo() pour tracker le dernier swap.
  • monitor.poll() vérifie quota.reactivate_cooldown avant de déclencher un swap. Log explicite quand le cooldown bloque : [quota] swap cooldown active.
  • Log forensique détaillé lors d'un SwapRequested : session déclencheuse, pattern matché, snippet du pane (120 chars). Ex : trigger_session="ccl-1-conformvault" pattern="rate limit" snippet="...".
  • switcher.executeSwitch appelle state.RecordSwap() après SetActiveAccount.

Tests effectués

  • go build ./cmd/claude-failover OK
  • go test ./internal/quota/... ./internal/state/... ./internal/switcher/... OK
  • Binaire installé dans /usr/local/bin/claude-failover
  • Service redémarré — pas de swap intempestif depuis

Fichiers modifiés

  • internal/state/state.go
  • internal/quota/monitor.go
  • internal/switcher/account_switcher.go

[0.2.0] - 2026-04-14

Type: Minor — Implémentation des goroutines Phase 2

Ajouté

  • Phase 2.1 : internal/watcher — SessionWatcher (détection fin de tâche, timeout, signal file)
  • Phase 2.5 : internal/notify — Notifier Telegram + Resend email
  • Phase 2.2 : internal/dispatcher — Dispatcher fsnotify + launchAgent
  • Phase 2.3 : internal/quota — QuotaMonitor (scraping pane tmux)
  • Phase 2.4 : internal/switcher — AccountSwitcher (state machine flip symlink)
  • Phase 2.6 : internal/janitor — Janitor (housekeeping agent-queue)
  • Phase 2.7 : cmd/claude-failover/main.go — Intégration complète toutes goroutines
  • Nouveaux champs config : watcher, dispatcher, janitor, notifications
  • state : ForEachWorking, SetStalled, SetActiveAccount, ActiveAccount
  • config.example.yaml : sections complètes pour tous les composants
  • scripts/claude-failover.service : unité systemd

Tests effectués

  • go test ./... -race (toutes phases)

[0.1.0] - 2026-04-14

Type: Initial — Daemon skeleton

Ajouté

  • Entry point, signal handling, config YAML loader
  • tmux.Client interface + ExecClient
  • State struct (JSON flush, sessions)
  • HTTP /health + /status
  • SessionLifecycleManager (reconcile 15s)