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

+37 -3
View File
@@ -139,7 +139,9 @@ func TestScreenshotSuitLEtatDeLaVision(t *testing.T) {
// Un base64 d'image persisté dans l'historique est rejoué à chaque tour : vu en
// production, 55 000 tokens de requête pour 32 768 de contexte — plus aucun
// tour ne passait. stripImageParts guérit les conversations existantes en
// retirant les parties image et en aplatissant le texte restant.
// retirant les parties image et en aplatissant le texte restant — en laissant
// imageLostMarker, pour que le modèle sache qu'une image A ÉTÉ montrée et qu'il
// doit en reprendre une plutôt que la redécrire de mémoire.
func TestStripImagePartsGueritLHistorique(t *testing.T) {
msgs := []Message{
{Role: "user", Content: "bonjour"}, // simple chaîne : intouchée
@@ -153,8 +155,40 @@ func TestStripImagePartsGueritLHistorique(t *testing.T) {
t.Fatalf("message texte modifié : %#v", out[0].Content)
}
got, ok := out[1].Content.(string)
if !ok || got != "Voici la capture demandée." {
t.Fatalf("le message multimodal doit être aplati en texte sans l'image, obtenu %#v", out[1].Content)
if !ok || got != "Voici la capture demandée."+imageLostMarker {
t.Fatalf("le message multimodal doit être aplati en texte, sans l'image mais avec le marqueur de perte, obtenu %#v", out[1].Content)
}
if strings.Contains(got, "base64") {
t.Fatalf("le base64 de l'image ne doit plus apparaître : %q", got)
}
}
// msgText est le point de passage de TOUTE la chaîne de compaction (estimation
// du contexte, transcript donné au résumeur). S'il n'extrait que les parties
// `text`, un message qui porte une capture ne pèse que sa légende et le résumeur
// ignore jusqu'à l'existence de l'image : elle disparaît sans laisser de trace.
func TestMsgTextSignaleLaPresenceDUneImage(t *testing.T) {
// Message VIVANT tel que construit par screenshotImageMessage ([]map[string]any).
live := Message{Role: "user", Content: []map[string]any{
{"type": "text", "text": "Capture demandée :"},
{"type": "image_url", "image_url": map[string]any{"url": "data:image/jpeg;base64,AAAA"}},
}}
// Même message RELU depuis le JSON persisté ([]any de map génériques).
reloaded := Message{Role: "user", Content: []any{
map[string]any{"type": "text", "text": "Capture demandée :"},
map[string]any{"type": "image_url", "image_url": map[string]any{"url": "data:image/jpeg;base64,AAAA"}},
}}
for name, m := range map[string]Message{"vivant": live, "relu": reloaded} {
got := msgText(m)
if !strings.Contains(got, "Capture demandée :") {
t.Fatalf("%s : le texte de la légende doit être conservé, obtenu %q", name, got)
}
if !strings.Contains(got, imageLostMarker) {
t.Fatalf("%s : la présence de l'image doit être signalée, obtenu %q", name, got)
}
if strings.Contains(got, "base64") {
t.Fatalf("%s : le base64 ne doit jamais entrer dans le transcript : %q", name, got)
}
}
}