From 5cad53ac7a4e80cdfd85e96f3d2fdbb2a28e8936 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 15 Apr 2026 19:51:15 +0000 Subject: [PATCH] docs: add WORK_IN_PROGRESS.md and document false-positive protection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WORK_IN_PROGRESS.md captures the v0.2.1→v0.2.3 incident, root cause, and the optional follow-ups (preserve dedicated sessions during swap, Telegram alert on SwapRequested, /quota/status endpoint). - architecture.md §2.2.1 describes the four-layer defense: strict patterns, 5xx veto, two-poll confirmation, post-swap cooldown. Co-Authored-By: Claude Opus 4.6 (1M context) --- WORK_IN_PROGRESS.md | 69 ++++++++++++++++++++++++++++++++++++++++++++ docs/architecture.md | 36 +++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 WORK_IN_PROGRESS.md diff --git a/WORK_IN_PROGRESS.md b/WORK_IN_PROGRESS.md new file mode 100644 index 0000000..1529d27 --- /dev/null +++ b/WORK_IN_PROGRESS.md @@ -0,0 +1,69 @@ +# Travaux en Cours - claude-failover + +## Dernière mise à jour +2026-04-15 19:30:00 + +## Version Actuelle +0.2.3 + +## Demande Actuelle +Aucune — v0.2.3 shippée, service stable. + +## Étapes Complétées +- [x] v0.2.1 — Cooldown post-swap + log forensique (trigger_session, pattern, snippet) +- [x] v0.2.2 — Confirmation 2-polls pour les hits sans reset time +- [x] v0.2.3 — Veto 5xx (`api_error` / `overloaded_error` / `internal server error`) + et retrait du pattern générique `"rate limit"` (remplacé par `rate_limit_error`) +- [x] Documentation : `docs/architecture.md` §2.2.1 "False-positive protection" +- [x] Tests unitaires exhaustifs (14 cas pour `isQuotaExhausted` dont les 3 veto 5xx) +- [x] Déploiement prod : `/usr/local/bin/claude-failover` + service redémarré +- [x] Push sur Forgejo `origin/main` (commits `7c5f838` et `62e98cb`) + +## Prochaines Étapes +- [ ] **Optionnel** : préserver les sessions dédiées (ccl-1-conformvault, + ccl-2-scanyze) lors d'un swap légitime — actuellement `killAllPoolSessions` + les tue aussi. Interruption désagréable pour le travail interactif. + Options : skip dedicated dans le kill, OU auto-relaunch avec + `--resume ` après kill. Non-bloquant tant que les vrais quota hits + sont rares. +- [ ] **Optionnel** : telegram alert quand `SwapRequested` est émis pour + que l'opérateur soit au courant sans lire les logs. Le `notifier.Telegram` + existe déjà — il suffit de câbler. +- [ ] **Optionnel** : exposer `/quota/status` via `internal/api` avec les champs + `last_swap_at`, `suspected_hit_at`, `cooldown_remaining` pour le dashboard. + +## Contexte Important +- **Symptôme en prod (2026-04-15)** : daemon faisait des swaps compte1↔compte2 + en boucle (intervalle descendant à 1 min), tuant les sessions interactives + ccl-1 et ccl-2 en permanence. `reset=""` sur tous les events. +- **Cause racine identifiée via le log forensique v0.2.1** : les 500 d'Anthropic + contenaient le texte "rate limit" dans leur payload. Snippet capturé : + `API Error: 500 {"type":"error","error":{"type":"api_error",...}}`. + Le monitor les confondait avec de vrais 429. +- **Config `reactivate_cooldown: "5m"`** existait déjà dans config.yaml mais + n'était consommée que par le dispatcher — pas par le monitor. v0.2.1 a + branché le monitor dessus. +- **Comptes disponibles** : `compte1` (Claude Max), `compte2` (Claude Team). + Symlink actuel : `~/.claude → .claude-compte2`. +- **Sessions tmux gérées** : pool autonome `ccl-auto-*` (min=2, max=10) + + dédiées `ccl-1-conformvault`, `ccl-2-scanyze`. + +## Fichiers Modifiés (cette série de fixes) +- `internal/quota/monitor.go` — quotaPatterns, serverErrorPatterns, suspectedHitAt, cooldown +- `internal/quota/monitor_test.go` — 14 sous-tests isQuotaExhausted + 3 tests poll +- `internal/state/state.go` — LastSwapAt/From/To + RecordSwap + LastSwapInfo +- `internal/switcher/account_switcher.go` — appel `state.RecordSwap()` après swap +- `docs/architecture.md` — §2.2.1 False-positive protection +- `VERSION.md` — changelog 0.2.1 → 0.2.3 + +## Bugs Connus +- **Sessions dédiées tuées lors d'un swap légitime** : comportement documenté + et voulu (respawn sur le nouveau compte), mais coupe brutalement le travail + interactif en cours. Voir Prochaines Étapes. + +## Historique des Demandes (Récentes) +| Date | Version | Demande | Status | +|------|---------|---------|--------| +| 2026-04-15 | 0.2.1 | Casser le ping-pong + logs forensiques | Terminé | +| 2026-04-15 | 0.2.2 | Confirmation 2-polls pour absorber les flashes | Terminé | +| 2026-04-15 | 0.2.3 | Veto 5xx + patterns stricts (root cause) | Terminé | diff --git a/docs/architecture.md b/docs/architecture.md index 5d0b944..e78368e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -75,6 +75,42 @@ re-implements dispatch selection natively in Go. - Emits `QuotaWarning` and `SwapRequested` events when thresholds in the config are crossed. +#### 2.2.1 False-positive protection (v0.2.3+) + +Three layers prevent transient upstream errors from being mistaken for +quota exhaustion and triggering useless — and destructive — swaps: + +1. **Strict pattern matching** (`isQuotaExhausted`). `quotaPatterns` + keys on specific phrases that only a real 429 surfaces: + `"you've hit your limit"`, `"rate_limit_error"` (Anthropic typed + error), `"quota exceeded"`, `"usage limit reached"`, + `"claude pro usage"`, `"too many requests"`, `"5-hour limit"`. + The generic substring `"rate limit"` is **not** a pattern — it + matches inside the bodies of unrelated error transcripts. + +2. **Server-error veto** (`hasServerError`). If the same pane also + contains `"api_error"`, `"overloaded_error"`, `"internal server + error"`, or `"api error: 5"`, the quota match is vetoed. An + Anthropic 500/503 response is surfaced in the Claude Code + conversation transcript and stays visible until the user scrolls; + without this veto it would be re-matched on every poll. + +3. **Two-poll confirmation** (`Monitor.suspectedHitAt`). A hit with no + parseable reset time (real 429s always include one, see + `extractResetTime`) is treated as *suspected* on the first poll and + only emits `SwapRequested` if a second consecutive poll also + detects the same condition. A single-poll flash is absorbed. + +4. **Post-swap cooldown** (`state.QuotaState.LastSwapAt` + + `quota.reactivate_cooldown`, default 5m). After a swap, the monitor + suppresses all detection for the cooldown window, breaking the + ping-pong failure mode where both accounts appear exhausted in + alternation. + +Forensic logging on every `SwapRequested` includes the triggering +session name, the matched pattern, and a 120-char snippet of the pane +so production incidents can be diagnosed from `journalctl` alone. + ### 2.3 session-watcher - Maintains an in-memory table keyed by tmux session name (`ccl-*`).