Multi-line task bodies arrived in Claude Code as "[Pasted text #N +M lines]" and sat in the input buffer forever — the trailing Enter that SendKeys appends to the paste is consumed as a newline inside the paste, not as a submit. Observed live on ccl-auto-11 (secumon) and ccl-auto-12 (secuops): prompt visible, agent idle. - tmux.Client grows a SendEnter(session) method. ExecClient runs `tmux send-keys -t <sess> Enter` (no preceding text), which Claude's TUI accepts as the explicit submit action after a paste. - Dispatcher: after SendKeys(msg), sleep 500ms for the paste to register, then SendEnter. Same sequence a human would perform. - Five mockTmux implementations updated (quota, dispatcher, switcher, lifecycle, watcher tests). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
10 KiB
10 KiB
Version actuelle : 0.3.2
[0.3.2] - 2026-04-15
Type: Patch — Double-Enter pour soumettre les prompts multi-lignes
Corrigé
- Les tâches dispatchées restaient coincées dans le buffer d'entrée Claude.
Le message s'affichait comme
[Pasted text #N +M lines]et Claude attendait indéfiniment un Enter explicite. Constaté surccl-auto-11(tâche secumon) etccl-auto-12(tâche secuops) : le prompt était bien envoyé mais jamais soumis, l'agent restait au prompt vide. - Cause :
SendKeysenvoietmux send-keys -t <sess> <text> Entermais quandtextcontient des\n, Claude Code détecte un paste et absorbe le Enter final comme nouvelle ligne du paste. Aucun submit.
Ajouté
tmux.Client.SendEnter(session): envoie un Enter isolé. ImplémentationExecClient.SendEnter=tmux send-keys -t <sess> Enter.- Dispatcher : après
SendKeys(msg),time.Sleep(500ms)puisSendEnter()pour soumettre le paste. - Mocks mis à jour dans 5 fichiers de test (quota, dispatcher, switcher, lifecycle, watcher).
Tests effectués
- ✅
go test ./...full suite (incluant dispatcher) - ✅ Sessions ccl-auto-11 et ccl-auto-12 débloquées manuellement après Enter, travail en cours depuis
Fichiers modifiés
internal/tmux/client.go— interface + ExecClient.SendEnterinternal/dispatcher/dispatcher.go— submit après paste- 5 fichiers
*_test.go— mocks étendus
[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-clidans secuaas-hosting/inbox depuis 18:39). - Cause : le config avait
prefix: "ccl-auto"(sans tiret), donc les loops généraientccl-auto0..9alors quesetup-tmux.shcréeccl-auto-11..20. Le daemon créait en plus 2 sessions fantômesccl-auto0/1au swap.
Ajouté
config.AutonomousConfig.StartIndex(YAMLstart_index, défaut 0). Les loops du daemon (monitor, dispatcher, switcher kill/recreate, lifecycle ensure/reconcile) itèrent désormaisstart..start+Max-1au lieu de0..Max-1.- Permet au pool autonome
ccl-auto-11..20de coexister avec le pool manuelccl-0..9(réservé opérateur). Le daemon ne touche que ce qu'il gère.
Modifié
/etc/claude-failover/config.yaml:autonomous: prefix: "ccl-auto-" # + tiret start_index: 11 # NEW min: 10 max: 10
Tests effectués
- ✅
go test ./...full suite - ✅ Sessions
ccl-auto0/1fantômes kill manuellement, poolccl-auto-11..20intact, pool manuelccl-0..9intact - ✅ Daemon redémarré,
config loaded: ... pool min=10 max=10
Fichiers modifiés
internal/config/config.go— champStartIndexinternal/quota/monitor.go— loop avec StartIndexinternal/switcher/account_switcher.go— kill + recreate avec StartIndexinternal/dispatcher/dispatcher.go— findFreeSession avec StartIndexinternal/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 bonCLAUDE_CONFIG_DIRaprès chaque swap. - La couverture de
saveAllSessions()ne captait que les sessions tracked enstate="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 avantkillAll.switcher.relaunchDedicatedSessions(targetHome): après recréation, envoieCLAUDE_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
- ✅
TestDedicatedRelaunchAfterSwapvérifie : capture UUID → write file → relaunch avec la bonne commande →CLAUDE_CONFIG_DIRpointant sur le home du compte cible. - ✅
go test ./...full suite
Fichiers modifiés
internal/switcher/account_switcher.gointernal/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 -3les 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é
quotaPatternsretravaillé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)
- Retiré :
- Veto 5xx :
serverErrorPatterns= ["api_error","overloaded_error","internal server error","api error: 5"]. Si l'un est présent, même si unquotaPatternmatche,isQuotaExhaustedretournefalse. Un 500/503 n'est pas un quota.
Ajouté
hasServerError()helper + tests exhaustifs :api_error_500_veto,overloaded_error_veto,internal_server_error_vetoreal_rate_limit_error_wins(sanity : vrai 429 passe toujours)
Tests effectués
- ✅ 14 sous-tests
TestIsQuotaExhaustedpassent - ✅
go test ./...complet OK - ✅ Service redémarré
Fichiers modifiés
internal/quota/monitor.gointernal/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
extractResetTimene trouve rien (= pas un vrai 429), le monitor marque l'étatsuspectedHitAtet 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 minutesouresets 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.gointernal/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
SwapRequestedtoutes les 30s, créant un ping-pong entre comptes quand du texte de pane (anciens errors Anthropic 500 / TUI banners) matchaitquotaPatternsavecreset="". 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érifiequota.reactivate_cooldownavant 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.executeSwitchappellestate.RecordSwap()aprèsSetActiveAccount.
Tests effectués
- ✅
go build ./cmd/claude-failoverOK - ✅
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.gointernal/quota/monitor.gointernal/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, ActiveAccountconfig.example.yaml: sections complètes pour tous les composantsscripts/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)