Retour à la liste

Blog

Confier l'historique de session au serveur : concevoir la reprise de conversation pour les agents IA sur mobile

Pourquoi Tactic Remote 1.8.2 a déplacé l'historique de conversation du téléphone vers le serveur, et comment cette décision a éliminé à la racine toute une catégorie de problèmes de désordre et de perte d'historique.

Publié le Tags: engineering / architecture / ios / reliability / websocket
Blog

Suivez les mises à jour produit et ingénierie de cette catégorie.

Voir la catégorie

Cet article porte sur une décision d'architecture : pourquoi Tactic Remote 1.8.2 a déplacé l'historique de conversation du téléphone vers le serveur, et comment cette décision a fait disparaître à la racine toute une série de problèmes que nous rapiécions sans cesse.

Une approche vouée à l'échec

Avant la version 1.8.2, l'historique de conversation était entièrement reconstitué en temps réel côté client. Le serveur envoyait au téléphone les événements de flux de l'agent ; le téléphone les rendait séquentiellement sous forme de bulles, formant la conversation que vous voyiez à l'écran.

Sur ordinateur, cette approche fonctionnait à peu près, la connexion étant stable. Mais sur mobile, elle souffre d'un défaut fondamental : l'historique n'existe que dans la connexion actuelle, dans la session en cours. Les problèmes s'enchaînaient.

  • Après une reconnexion, la nouvelle connexion ne reçoit pas les anciens événements : l'historique est vide ou incomplet.
  • Tuer puis relancer l'application efface la conversation en mémoire ; impossible de reprendre là où on en était.
  • Quand plusieurs appareils se connectent à la même session, chacun ne voit que les événements survenus depuis sa propre connexion.

Un problème plus subtil se posait également, lié à l'ordre. Lors de l'envoi d'un message, le client affichait d'abord de façon optimiste la bulle utilisateur, tandis que la première réponse de l'agent n'arrivait qu'après le démarrage effectif de son processus. La moindre gigue temporelle — un message en file d'attente qui n'arrivait qu'après une reconnexion — pouvait faire glisser la bulle utilisateur sous la réponse. Ce problème de « bulle utilisateur tombée en bas » a été corrigé plusieurs fois, mais toujours en traitant les symptômes. La cause profonde était structurelle : aucune partie du système n'était responsable de l'ordre réel de la conversation.

S'inspirer de tmux

Le mode terminal de Tactic Remote a toujours été stable, grâce à tmux. Le modèle de tmux est simple et robuste : le tampon de terminal est l'unique source de vérité. Se connecter (attach), c'est rejouer ce tampon. Déconnexion, changement d'appareil, redémarrage — tout cela n'est qu'une nouvelle connexion au même tampon ; l'historique est naturellement préservé.

C'est exactement ce qui manquait au mode conversation. La conception de la version 1.8.2 a donc transposé cette idée :

Confier au serveur un enregistrement de référence de la conversation, et le restituer intégralement au client lors de la connexion.

Dès lors que le serveur devient la source de vérité pour l'ordre des événements, les problèmes précédents ne sont plus des bugs à corriger un par un — ils deviennent structurellement impossibles.

Comment ça fonctionne concrètement

Côté serveur : un tampon circulaire par session

Le serveur maintient un enregistrement de conversation pour chaque session de chat. Chaque événement diffusé est ajouté séquentiellement dans un tampon circulaire. Deux limites s'appliquent : un nombre maximal d'événements et une taille totale en octets ; les événements les plus anciens sont élagués en premier. L'événement d'initialisation qui ouvre la session — point d'ancrage de la restauration — n'est jamais supprimé. En cas d'élagage, le tampon est marqué « tronqué », de sorte qu'à la restauration le client affiche honnêtement « messages antérieurs omis » plutôt que de feindre un historique complet.

Un point crucial : les messages envoyés par l'utilisateur génèrent désormais un événement ordonné côté serveur, inscrit dans cet enregistrement. Auparavant, ces messages n'existaient que dans le rendu optimiste côté client ; le serveur n'en savait rien, et les messages intercalés en cours de session (steer) n'étaient même pas enregistrés. Maintenant que le serveur fait autorité sur l'ordre, chaque tour de parole de l'utilisateur est ancré à sa position réelle. Le problème de la « bulle utilisateur tombée en bas » disparaît structurellement.

Restauration : une enveloppe plutôt que la rediffusion des événements bruts

La première idée était de rediffuser les événements bruts lors de la connexion. Elle a vite été écartée. Les anciens clients ignorent les événements qu'ils ne reconnaissent pas ; or les événements rediffusés sont précisément des événements reconnus, qui seraient traités comme de nouveaux messages en temps réel et ajouteraient l'historique en double.

La solution retenue consiste à emballer tout l'enregistrement dans un événement inédit : structured_replay, qui contient un numéro de séquence, un indicateur de dernière tranche, un indicateur de troncature, l'état d'exécution courant, et la liste des événements. Les anciens clients ne reconnaissent pas ce nouvel événement et l'ignorent sans problème ; les nouveaux clients déballent l'enveloppe et injectent les événements dans le même pipeline de traitement pour reconstituer la conversation.

Comme la taille maximale d'un frame WebSocket sur iOS est d'environ 1 MiB, la restauration est découpée en tranches de 256 Ko, numérotées dans l'ordre. Les approbations d'outils déjà traitées sont marquées « autorisées / refusées » lors de la restauration, afin qu'une carte d'approbation historique ne soit pas rendue à tort comme une carte en attente d'action.

Côté client : reconstruire à la connexion, bloquer les effets de bord

Quand le client reçoit la première tranche de restauration (numéro de séquence 0), il efface la conversation locale, puis injecte les événements dans le même gestionnaire de traitement pour reconstruire l'historique. La restauration emprunte donc exactement le même chemin de rendu que le temps réel — aucune logique supplémentaire n'est nécessaire.

Un détail crucial doit être géré : pendant la restauration, les effets de bord qui ne doivent se déclencher que dans un contexte temps réel doivent être bloqués — envoi automatique de la file, chien de garde des délais, basculement d'état d'exécution, mise à disposition automatique des approbations. Un indicateur les neutralise jusqu'à l'arrivée de la dernière tranche, moment où l'état d'exécution de référence contenu dans l'enveloppe est adopté.

Pour la bulle optimiste envoyée lors d'un message, nous ne l'avons pas supprimée. Le démarrage du processus agent prend une à deux secondes, et attendre purement la restauration créerait un blanc perceptible. Notre solution : la bulle devient un espace réservé « en attente de confirmation ». Dès que l'accusé de réception du serveur arrive, cet espace est retiré et la bulle est réinsérée à sa position réelle dans l'enregistrement de référence. Le désordre devient structurellement impossible à maintenir.

Compatibilité : pourquoi « ignorer les événements inconnus » est une décision de conception

La restauration a pu être déployée en toute sécurité grâce à un contrat établi de longue date : le client ignore systématiquement les événements qu'il ne reconnaît pas. C'est précisément ce qui permet au serveur d'introduire des événements inédits comme structured_replay sans perturber les anciens clients. Ils ne voient pas la fonctionnalité de restauration, mais ils ne rencontrent aucune erreur non plus.

Dans un système où l'application iOS, les serveurs Mac / Linux / Windows et le CLI npm sont publiés indépendamment avec des versions souvent décalées, cette discipline protocolaire — n'ajouter qu'en ajout, ne jamais modifier, ignorer l'inconnu — est ce qui rend possible le déploiement serveur en avance sur les clients.

Limites et arbitrages honnêtes

Cette conception a ses limites, et nous préférons les énoncer clairement.

  • Le tampon serveur est borné. Pour les très longues sessions, les événements les plus anciens seront élagués ; la restauration l'indique explicitement par « messages antérieurs omis », sans prétendre à un historique complet.
  • Une session récupérée repart de zéro. Après une longue période sans abonné, le serveur libère la session ; une session reprise via claude --resume repart d'un tampon vide, sans rejouer l'historique antérieur. C'est un arbitrage délibéré : un historique limité en échange d'une empreinte mémoire maîtrisée.
  • La persistance locale côté client est une amélioration future. Pour l'instant, l'enregistrement de référence n'existe que pendant la durée de vie du serveur. Stocker l'historique localement sur le client est une évolution prévue pour une prochaine version.

Plutôt que de vous laisser découvrir un jour qu'un pan d'historique a disparu et d'en chercher vous-même la cause, nous préférons formuler ces limites dès maintenant.


Envie de voir la restauration à la connexion en action ? L'application iOS est disponible gratuitement sur l'App Store ; l'application compagnon et le CLI sont également gratuits. Si l'autre gros chantier de cette version vous intéresse — ce blocage d'interface qui ne venait pas du tout du code métier — lisez « Traquer un gel d'interface SwiftUI ».

Essayez Tactic Remote

Pilotez vos agents de programmation depuis votre téléphone

Connectez-vous à Claude Code, Codex et aux autres agents sur votre Mac, PC Windows ou Linux. Consultez leur progression et envoyez la prochaine instruction depuis votre iPhone ou iPad.