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>
This commit is contained in:
parent
75b5110748
commit
7c5f8384fa
5 changed files with 246 additions and 25 deletions
67
VERSION.md
67
VERSION.md
|
|
@ -1,4 +1,69 @@
|
|||
# Version actuelle : 0.2.0
|
||||
# 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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue