claude-failover/VERSION.md
Ubuntu eb6b74c547 feat(pool): add start_index so manual and auto pools can coexist
Production had two disjoint tmux pools named alike but for different
purposes:
  ccl-0..ccl-9           — manual/interactive sessions (operator)
  ccl-auto-11..ccl-auto-20 — autonomous dispatcher pool

Until now the daemon's loops iterated prefix + 0..Max, so with the
deployed config ("prefix: ccl-auto", min=2, max=10) the dispatcher
looked for sessions "ccl-auto0..ccl-auto9" that never existed, while
the real auto pool ccl-auto-11..20 was invisible. Net effect: no task
was ever dispatched, and killAllPoolSessions fabricated phantom
"ccl-auto0/1" sessions on each swap.

- AutonomousConfig gains StartIndex (yaml start_index, default 0).
  Behaviour is unchanged when StartIndex is 0.
- Monitor, switcher (kill + recreate), dispatcher (findFreeSession),
  and lifecycle (EnsureAll + reconcile) all iterate
  [StartIndex, StartIndex+Max) so the daemon only touches its own
  range and leaves ccl-0..ccl-9 alone.
- Production config updated to prefix: "ccl-auto-", start_index: 11,
  min: 10, max: 10 — covering the 10 real ccl-auto-11..20 sessions.

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

205 lines
9.1 KiB
Markdown

# Version actuelle : 0.3.1
## [0.3.1] - 2026-04-15
**Type:** Patch — `start_index` pour faire coexister pool manuel et pool auto
### Corrigé
- **Bug prod non détecté depuis longtemps** : le pool autonome daemon ignorait
le pool réel et vice-versa. Conséquence : **aucune tâche automatique n'était
dispatchée** (ex. `installer-d2-cli` dans secuaas-hosting/inbox depuis 18:39).
- Cause : le config avait `prefix: "ccl-auto"` (sans tiret), donc les loops
généraient `ccl-auto0..9` alors que `setup-tmux.sh` crée `ccl-auto-11..20`.
Le daemon créait en plus 2 sessions fantômes `ccl-auto0/1` au swap.
### Ajouté
- `config.AutonomousConfig.StartIndex` (YAML `start_index`, défaut 0).
Les loops du daemon (monitor, dispatcher, switcher kill/recreate, lifecycle
ensure/reconcile) itèrent désormais `start..start+Max-1` au lieu de `0..Max-1`.
- Permet au pool autonome `ccl-auto-11..20` de coexister avec le pool manuel
`ccl-0..9` (réservé opérateur). Le daemon ne touche que ce qu'il gère.
### Modifié
- `/etc/claude-failover/config.yaml` :
```yaml
autonomous:
prefix: "ccl-auto-" # + tiret
start_index: 11 # NEW
min: 10
max: 10
```
### Tests effectués
- ✅ `go test ./...` full suite
- ✅ Sessions `ccl-auto0/1` fantômes kill manuellement, pool `ccl-auto-11..20`
intact, pool manuel `ccl-0..9` intact
- ✅ Daemon redémarré, `config loaded: ... pool min=10 max=10`
### Fichiers modifiés
- `internal/config/config.go` — champ `StartIndex`
- `internal/quota/monitor.go` — loop avec StartIndex
- `internal/switcher/account_switcher.go` — kill + recreate avec StartIndex
- `internal/dispatcher/dispatcher.go` — findFreeSession avec StartIndex
- `internal/lifecycle/manager.go` — ensure + reconcile avec StartIndex
- `/etc/claude-failover/config.yaml` — prefix fixé + start_index
## [0.3.0] - 2026-04-15
**Type:** Minor — Auto-resume des sessions dédiées après un swap légitime
### Corrigé
- **Les sessions dédiées (ccl-1-conformvault, ccl-2-scanyze) étaient tuées puis
recréées au bash prompt lors d'un swap légitime** (vrai 429 quota hit),
interrompant le travail interactif en cours. L'opérateur devait relancer
manuellement `claude --resume <uuid>` avec le bon `CLAUDE_CONFIG_DIR` après
chaque swap.
- La couverture de `saveAllSessions()` ne captait que les sessions tracked en
`state="working"`. Les sessions dédiées user-driven étaient ignorées, donc
leur UUID de resume était perdu au kill.
### Ajouté
- `switcher.saveDedicatedUUIDs()` : capture le UUID de chaque session dédiée
configurée, peu importe son tracked state. Appelé juste avant `killAll`.
- `switcher.relaunchDedicatedSessions(targetHome)` : après recréation,
envoie `CLAUDE_CONFIG_DIR=<targetHome> claude --dangerously-skip-permissions
--resume <uuid>` dans chaque session dédiée. Si l'UUID manque, la session
reste au shell (pas de tentative aveugle).
- `isValidResumeUUID()` défense contre un fichier resume-id corrompu (check
longueur 36 + regex hex/dash).
### Tests
- ✅ `TestDedicatedRelaunchAfterSwap` vérifie : capture UUID → write file →
relaunch avec la bonne commande → `CLAUDE_CONFIG_DIR` pointant sur le home
du compte cible.
- ✅ `go test ./...` full suite
### Fichiers modifiés
- `internal/switcher/account_switcher.go`
- `internal/switcher/account_switcher_test.go`
## [0.2.3] - 2026-04-15
**Type:** Patch — Veto 5xx pour écarter les faux positifs persistants
### Corrigé
- **Les 500/503 d'Anthropic restent visibles dans l'historique de conversation
Claude Code** (pas juste en flash). Donc `tmux capture-pane -S -3` les voyait
à chaque poll, et la confirmation 2-polls v0.2.2 finissait par les confirmer
→ swap sur faux positif persistant.
- **Racine du faux positif** : le pattern `"rate limit"` (substring lâche)
matchait dans le contenu textuel d'un 500 rendu par Claude TUI.
### Modifié
- `quotaPatterns` retravaillés pour privilégier les signatures spécifiques :
- Retiré : `"rate limit"` (trop générique, matche les transcripts)
- Ajouté : `"rate_limit_error"` (type d'erreur Anthropic pour les vrais 429)
- Ajouté : `"5-hour limit"` (phrasing Claude Code)
- **Veto 5xx** : `serverErrorPatterns` = [`"api_error"`, `"overloaded_error"`,
`"internal server error"`, `"api error: 5"`]. Si l'un est présent, même si un
`quotaPattern` matche, `isQuotaExhausted` retourne `false`. Un 500/503
n'est pas un quota.
### Ajouté
- `hasServerError()` helper + tests exhaustifs :
- `api_error_500_veto`, `overloaded_error_veto`, `internal_server_error_veto`
- `real_rate_limit_error_wins` (sanity : vrai 429 passe toujours)
### Tests effectués
- ✅ 14 sous-tests `TestIsQuotaExhausted` passent
- ✅ `go test ./...` complet OK
- ✅ Service redémarré
### Fichiers modifiés
- `internal/quota/monitor.go`
- `internal/quota/monitor_test.go`
## [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)