Démarrage, captures, postes distants : correctifs portés d'AJEAN

- Au démarrage, une lecture ratée de la base (verrou bbolt transitoire pendant
  le chevauchement des process au redémarrage du conteneur) était confondue
  avec une conversation absente. Pire que l'amont chez nous : getStr rend ""
  dans les deux cas, donc convEnsureActive forgeait un NOUVEL identifiant et
  l'écrasait — le fil en cours devenait orphelin, en silence. On sonde
  désormais la base avec son erreur AVANT toute écriture, on réessaie quatre
  fois, et un échec durable est journalisé sans que rien ne soit touché.

- Une capture d'écran disparaissait sans laisser de trace : stripImageParts
  aplatissait le message en ne gardant que sa légende. Le marqueur
  imageLostMarker rend la perte VISIBLE, pour que le modèle sache reprendre
  une capture au lieu de la redécrire de mémoire.

- maxLogEvents 20000 → 200000 : un seul tour à très long raisonnement
  tronquait déjà le journal de rejeu, et l'utilisateur perdait le début de sa
  conversation à l'écran.

- Keepalive WebSocket des postes distants (ping toutes les 25 s, des DEUX
  côtés). Sans lui, un poste au repos était coupé au bout de ~60-100 s par les
  intermédiaires qui ferment les canaux inactifs, puis reconnecté après
  backoff — les déconnexions à répétition sur tous les postes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5 committed 2026-09-07 22:33:55 +02:00
1 parent 2d4f5378c0
commit c45ba615fa
6 files changed
+308 -11

No files matched your search

+34
View File
@@ -25,6 +25,12 @@ const (
nodePairCodeTTL = 10 * time.Minute
nodeCallTimeout = 5 * time.Minute // un shell distant peut être long
nodeHelloWait = 15 * time.Second
// nodePingInterval : cadence du keepalive WebSocket poste↔serveur. 25 s tient
// sous les délais d'inactivité usuels des proxys et relais (~60-100 s).
nodePingInterval = 25 * time.Second
// nodePingTimeout : au-delà, le poste ne répond plus — on ferme au lieu de
// laisser une connexion morte occuper le registre.
nodePingTimeout = 10 * time.Second
)
// pairedNode est un poste appairé, tel que persisté dans bkState["nodes"].
@@ -279,6 +285,34 @@ func handleNodeWS(w http.ResponseWriter, r *http.Request) {
defer nodeUnregister(nc)
touchNodeSeen(pn.ID)
// Keepalive : un ping WebSocket périodique garde la connexion vivante à travers
// les intermédiaires qui coupent les canaux INACTIFS (relais, Caddy, Cloudflare :
// ~60-100 s sans trafic). Sans lui, un poste au repos — l'IA ne le sollicite pas
// en permanence — était déconnecté en boucle puis reconnecté après backoff.
// coder/websocket sérialise Ping avec les écritures de données en interne : pas
// de course avec nc.send. Un ping sans réponse ferme la connexion, ce qui
// débloque le Read de la boucle ci-dessous.
pingCtx, stopPing := context.WithCancel(ctx)
defer stopPing()
go func() {
t := time.NewTicker(nodePingInterval)
defer t.Stop()
for {
select {
case <-pingCtx.Done():
return
case <-t.C:
pc, cancel := context.WithTimeout(pingCtx, nodePingTimeout)
err := c.Ping(pc)
cancel()
if err != nil {
_ = c.Close(websocket.StatusGoingAway, "ping sans réponse")
return
}
}
}
}()
// Boucle de lecture : des « result » chiffrés qui débloquent l'appel en attente.
for {
m, err := nc.readMsg(ctx)