Un clic sur Regenerate, et 107 messages avaient disparu


La dernière réponse n’était pas tout à fait juste, alors vous cliquez sur Regenerate. L’interface se rafraîchit, tout semble normal — jusqu’à ce que vous remontiez et constatiez que la conversation d’il y a quatre heures a disparu, avec tout ce qui s’est passé entre-temps. Vous n’avez rien supprimé. Vous n’avez jamais cliqué sur quoi que ce soit portant la mention « effacer ».

Ce cas a vraiment été signalé : un seul regenerate a transformé une session de 430 messages en 323. 107 messages actifs ont été archivés en silence, y compris tout ce qui suivait un turn de quatre heures auparavant.

430 est devenu 323

Le bug tient dans la question « vers quelle ligne ce regenerate fait-il un rewind ? ».

Dans Desktop, regenerate et edit établissent d’abord un plan de troncature : la ligne à refaire, et tout ce qui suit compté comme historique. Le serveur validait ce plan à l’envoi, mais ne rejetait que trois cas :

  • ancres périmées (erreur 4018) ;
  • dérive d’ordinal (4030) ;
  • coupes sur tout le transcript (4028).

Ce qui passait entre les trois : une ancre row-id valide qui pointait vers une ligne très ancienne. Et le chemin regenerate du client faisait confiance à son propre parentId sans rien demander. Ensemble, un regenerate cliqué sur le dernier message pouvait légitimement rejouer le turn à l’ordinal 31 et archiver la centaine de messages qui suivait.

Reproduit sur main, une session de 430 messages a été réduite à 310 à l’ordinal 31 sans que rien ne s’y oppose. Il faut le dire clairement : la cause racine côté client n’a pas été reproduite, et aucun commit coupable unique n’a été identifié. Autrement dit, plusieurs chemins peuvent déclencher ce type de déclenchement intempestif — d’où un correctif placé à la sortie plutôt qu’à une entrée précise.

Désormais, il demande d’abord

L’approche n’est pas « deviner plus finement de quel turn vous parliez ». C’est vous montrer la conséquence avant que l’action ne s’exécute.

  • Regenerate (planConfirmedReload) et edit (planConfirmedEdit) partagent désormais une seule vérification de coupe profonde (confirmIfDeepCut) ;
  • Tout rewind qui ferait disparaître un turn utilisateur visible ultérieur ouvre la boîte de confirmation destructive existante (deepCutConfirmRequest, en réutilisant le libellé de restore, donc aucune nouvelle chaîne d’interface n’a été ajoutée) ;
  • Ce qui reste ininterrompu est aussi défini explicitement : un regenerate de fin de conversation, un edit du dernier turn et un edit d’un turn en échec ne demandent rien ;
  • Si vous refusez, rien n’est envoyé. Restore garde sa propre boîte de dialogue distincte.

Une boîte de dialogue peut rester ouverte longtemps — vous êtes allé chercher un café —, alors les changements survenus pendant cette fenêtre sont gérés :

  • Regenerate abandonne le plan si la session est passée en busy ou si son transcript a changé ;
  • Edit repointe vers le transcript actuel avec revalidateEditPlan, de sorte qu’une réponse en streaming qui s’allonge soit prise en compte ; il s’arrête avec un avis si le turn édité a bougé ou si un nouveau turn utilisateur ultérieur est arrivé ;
  • Les deux abandonnent l’action si vous avez changé de session.

Le serveur compte aussi

L’invite côté client n’est qu’une couche. Le serveur compte désormais réellement ce qu’une coupe ferait disparaître : _archived_user_turns mesure la coupe en lignes utilisateur durables, et chaque détail penche du côté conservateur :

  • Un carrier réparé compte chaque ligne de son run user;user fusionné, lu à partir des lignes physiques ;
  • Les marqueurs d’affichage absorbés ne comptent pas ;
  • Les ids inconnus et le turn propre d’un carrier non estampillé comptent, eux — fail closed plutôt que de sous-compter ;
  • Lors d’une coupe sur un carrier durable, les lignes antérieures à la cible survivent.

Si le client et le serveur comptent les turns différemment, le serveur répond 4033. Quand le client voit 4033 :

  • Il annule immédiatement l’état optimiste, puisque l’envoi refusé n’a rien écrit ;
  • Puis il vous demande. Si vous acceptez, regenerate ou edit se relancent depuis le début via les fonctions privées runReload / runEdit, en relisant la session et en la re-vérifiant ;
  • La coupe profonde forcée ne couvre que le transcript pour lequel vous avez répondu. Tout changement pendant celle-ci redemande confirmation.

La nouvelle tentative existante d’edit en cas de cible périmée (4018) a également été resserrée : elle porte la confirmation profonde uniquement lorsque le transcript rafraîchi compte le même nombre de turns utilisateur ultérieurs.

Cette version se contente d’avertir

Une chose doit être claire : cette version ne bloque pas encore les coupes profondes. Elle les enregistre.

Quand le serveur voit une coupe non confirmée de plus d’un turn utilisateur, il journalise ALLOWED (warn-only release) et l’exécute quand même. L’interrupteur d’application est DEEP_TRUNCATE_ENFORCED ; dès que la prochaine version l’activera, la même coupe sera refusée avec 4033 et le client prendra le chemin « annuler, demander, relancer » décrit plus haut.

Le séquencement est délibéré : une version de données réelles sur la fréquence de ces déclenchements intempestifs, puis le verrou dur — plutôt que de livrer l’application immédiatement et de bloquer des rewinds profonds légitimes.

Statut et mise à niveau

Ce correctif (PR #133748, qui ferme l’issue #133716, étiquetée P0) a été fusionné dans main le 2026-10-11. Le tag actuel, v0.21.6 du 8 octobre, ne le contient pas ; la prochaine version le fera.

Après la mise à niveau, vous remarquerez deux choses : bien moins de coupes accidentelles, et une confirmation supplémentaire de temps à autre. Cette boîte de dialogue est tout l’intérêt du changement — quand elle apparaît, cela signifie que vous étiez sur le point de perdre une conversation que vous n’aviez pas encore lue.

Pour conclure

Regenerate est le bouton que les gens traitent comme « annuler la dernière étape », alors qu’il signifie en réalité « refaire à partir d’une certaine ligne, et tout ce qui suit devient de l’historique ». Confondre les deux permet à un seul clic égaré d’engloutir toute une tranche de travail. La nouvelle règle est sans détour : si de la conversation ultérieure doit disparaître, demander d’abord. Le coût de protéger une session est d’un clic ; le coût de perdre 107 messages, c’est que vous ne saurez jamais ce qu’ils disaient.

Lectures associées : reprise et continuation de session traite de la reprise d’une conversation, le guide des limites de session explique ce qui compte comme limite utilisateur, et l’isolation de l’embed Desktop porte sur ce qui se passe dans l’interface et sur ce qu’on vous en dit.