Chat : écrire pendant que l'IA répond, et deux fils ne fusionnent plus

Repris d'AJEAN 0.14.0, adapté.

AJOUT EN COURS DE RÉPONSE
Il fallait arrêter la génération pour glisser une précision (le serveur
répondait 409). Un message envoyé pendant une réponse part désormais EN
FILE : runChat l'injecte à la prochaine frontière d'étape (après un appel
d'outil) — le modèle en tient compte dans la SUITE de sa réponse — ou, si
le tour se termine avant, il devient le tour suivant, dans l'ordre. Le
bouton envoyer réapparaît à côté de stop dès qu'il y a du texte ; le
message s'affiche « en attente » au-dessus de la carte jusqu'à ce que le
flux le confirme. Stop abandonne la file (queue_dropped, le client retire
ses pastilles).

Écarts avec l'amont :
- Les messages en attente sont posés AU-DESSUS de la carte, pas dans le
  fil qui s'écrit encore (ils s'y seraient intercalés entre deux bulles).
- Dédoublonnage par identifiant d'envoi (cid) : l'UI réessaie un envoi dont
  la réponse s'est perdue sur le tunnel. Le 409 « déjà en cours » faisait
  office de garde ; sans lui, le réessai mettait le message deux fois en
  file.
- Une tâche planifiée qui occupe le modèle garde le refus 409 (sa fin ne
  dépile rien).
- Pas d'injection après un stop : la boucle repassait en tête une fois
  l'outil interrompu et journalisait le message en file (vu en test).
- runChat prend l'injecteur en paramètre variadique : les appels hors chat
  (tâches, vérification du mode Code, tests) ne changent pas.

DEUX FILS FUSIONNÉS
Un appareil déconnecté pendant qu'un autre changeait de discussion se
réabonnait avec un `from` hérité de l'ancienne : les Seq n'étant pas
globaux, la fin de la nouvelle discussion se greffait sur le début de
l'ancienne restée à l'écran. caught_up et reset portent maintenant l'id de
la discussion ; le client le renvoie (conv_id) et, s'il ne correspond plus,
le serveur ordonne un reset avant de rejouer. La garde « from au-delà du
dernier Seq » émet elle aussi ce reset (elle rejouait par-dessus l'écran
sans le vider).

Vérifié dans le navigateur avec un faux moteur : précision injectée après
l'outil dans le même tour, message en file devenu tour suivant, stop qui
abandonne la file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5.5 committed 2026-10-02 23:11:40 +02:00
1 parent 58c2da1302
commit 48b8193dca
11 files changed
+459 -41

No files matched your search

+9
View File
@@ -1349,6 +1349,15 @@ type chatReq struct {
// chargement ; 0 = tout. nil = ancien client (app relais…), qui ne sait pas
// afficher le bouton « échanges précédents » : il reçoit toujours tout.
Tail *int `json:"tail"`
// ClientID = identifiant unique d'un ENVOI (/api/chat/send), stable d'un
// réessai à l'autre : le serveur ne met pas deux fois le même message en
// file (voir Conversation.recentCIDs).
ClientID string `json:"cid"`
// ConvID = id de la discussion AFFICHÉE par le client (reçu au dernier
// caught_up/reset du flux). Renvoyé à chaque (re)abonnement pour que le
// serveur détecte qu'un AUTRE appareil a changé de discussion entre-temps
// et ordonne un reset avant de rejouer — sinon deux fils fusionnaient.
ConvID string `json:"conv_id"`
// Files = chemins relatifs des fichiers déposés juste avant par
// /api/chat/upload ("uploads/rapport.pdf"). Ils sont annoncés au modèle en
// tête du message (voir attachNote) ; le contenu, lui, reste sur le disque et