Par Juste Thales Gnimavo (CEO, ZeroSuite) et Claude Opus 5.5 (Claude Code)
Entre le 27 et le 30 octobre 2026, nous avons trouvé dans Déblo Duo trois bugs qui avaient un point commun. Aucun ne levait d'erreur. L'app semblait fonctionner, le serveur aussi, les logs étaient propres. À chaque fois, c'est un chiffre qui nous a alertés : un appel de zéro seconde, un nombre de messages qui aurait dû être six et valait zéro, un délai de 14 secondes là où il ne devait y en avoir aucun.
Déblo Duo est notre interprète en temps réel : chacun parle sa langue et entend l'autre dans la sienne. (Le contexte est dans Pourquoi Déblo Duo existe.) Un appel réunit plusieurs participants dans une salle de notre couche audio temps réel : l'hôte, l'invité et un worker côté serveur qui relaie leur audio vers le modèle de traduction vocale en temps réel de Google. L'essentiel passe par l'audio. Quelques informations passent par de petits messages de données : en mode un seul téléphone, « je tiens le bouton, c'est à moi de parler » ; dans tous les modes, « je raccroche ».
27 octobre : un appel qui raccrochait à zéro seconde
Le premier est venu de Juste, qui testait Duo 1.0.9 sur son téléphone. Son signalement : « le premier appui sur le gros micro lance un appel, le deuxième marche. »
C'est le genre de signalement qu'on écarte facilement comme un mauvais appui. Nous avons plutôt lu la base de données. Dans la table des sessions vocales, il y avait des sessions en mode un seul téléphone d'exactement 0 seconde à 05:37 et 05:39 UTC, chacune suivie d'une tentative qui tenait. Le bug était réel, et reproductible.
La cause était un état périmé. La fin d'un appel laissait l'état partagé de l'appel dans sa phase « terminé ». L'écran un seul téléphone est le seul écran d'appel qui démarre directement à l'étape de l'appel ; les autres commencent par une invitation ou un écran de chargement. À son premier rendu, l'effet de fin d'appel de l'écran lisait la phase restée du précédent appel avant que la réinitialisation ne s'exécute, concluait que l'appel était fini, et raccrochait.
Le correctif a consisté à réinitialiser l'état de l'appel avant de naviguer vers l'écran un seul téléphone, depuis son unique point d'entrée. Duo 1.0.10 est sorti avec. Juste a confirmé qu'aucun appel ne raccrochait plus à zéro, et la base ne montrait aucune session de 0 seconde depuis l'installation. Nous avons aussi noté la réserve : la séquence exacte (un appel, puis un appel en mode un seul téléphone dans la même session de l'app) n'était pas visible dans la base, car une seule session avait été enregistrée. Cette partie repose sur son observation.
29 octobre : un droit que personne n'avait accordé
Deux jours plus tard, en préparant une journée complète de tests, nous avons regardé comment le backend émet les jetons d'accès d'un appel. Chaque participant rejoint la salle avec un jeton qui liste ce qu'il a le droit de faire : publier de l'audio, s'abonner, publier des données.
Les jetons de l'hôte et de l'invité ne contenaient pas le droit de publier des données. Ils ne l'avaient jamais eu, depuis les premiers briefs de l'interprète. Pourtant, les clients envoient deux choses sous forme de données : le signal d'appui pour parler et le message de raccrochage.
La lecture du code nous disait ce qui devait se passer. Nous voulions savoir ce qui se passait vraiment. Nous avons donc écrit une sonde : trois participants dans une salle du service audio de production, sans worker de traduction et sans facturation. Un émetteur sans droit sur les données, un avec, et un récepteur à l'écoute.
La sortie tenait en deux lignes. L'émetteur sans le droit : aucune erreur de publication. Le récepteur : un message, venu de l'émetteur qui avait le droit. L'autre message n'est tout simplement pas arrivé, et rien ne l'a signalé à son émetteur.
Cela voulait dire que deux fonctions n'avaient jamais marché en production. En mode un seul téléphone, le bouton appuyer pour parler n'atteignait jamais le worker. Le message « Raccrocher » non plus. Le mode un seul téléphone avait été marqué livré lors d'une session précédente. Son banc de test passait. Le banc fabriquait ses propres jetons, droit sur les données inclus, et ne pouvait donc pas voir ce que le backend émettait réellement. Un banc qui fabrique ses propres identifiants ne teste pas les vrais.
Avant d'ouvrir le droit, nous avons vérifié l'autre sens : un invité pourrait-il désormais envoyer un faux message de contrôle ? Les clients web et mobile ignorent tout paquet de données qui ne vient pas de l'agent. Le worker vérifie le rôle de l'émetteur : le raccrochage exige un participant connu, le signal d'appui pour parler n'est accepté que de l'hôte. Les auditeurs d'une présentation ne reçoivent aucun droit sur les données.
Le correctif tenait en un seul changement dans le backend : accorder le droit sur les données aux jetons de l'hôte et de l'invité. Nous avons rejoué la sonde avec un jeton exactement de la nouvelle forme, micro plus données, à 21:38 UTC. Le récepteur a reçu les deux messages. Aucune recompilation de l'app n'était nécessaire, puisque les jetons sont émis à neuf pour chaque appel. Mais la sonde de transport ne prouvait que le transport. Restait à savoir si le correctif marchait dans un vrai appel Duo, et l'observation attendue était écrite à côté : des événements d'appui pour parler et de raccrochage dans le log du worker de production pendant un vrai appel.
Le soir même : six appels, zéro raccrochage
Juste a testé depuis le salon le soir même, le 29 octobre, entre 23:22 et 23:38 UTC. Le mode un seul téléphone a marché : 1 minute 20 secondes. Le log de production du worker montrait six événements d'appui pour parler. Ce correctif était prouvé.
Le deuxième test consistait à faire raccrocher l'invité en premier. Ça « marchait ». L'écran de l'hôte a fini par afficher « Terminé ». Au bout de 14 secondes.
Quatorze secondes, c'est assez long pour qu'un hôte croie l'app figée, et assez court pour passer inaperçu dans une démo rapide. Nous avons compté. Sur six appels, le worker avait reçu zéro message de raccrochage. Les messages d'appui pour parler, envoyés par le même chemin et par les mêmes clients, arrivaient à chaque fois.
La différence tenait à ce qui venait juste après. Le signal d'appui pour parler est envoyé et la connexion reste ouverte. Le raccrochage est envoyé, puis le client se déconnecte aussitôt. Le message se perdait à la fermeture de la connexion. Le « Terminé » de l'hôte n'était pas l'arrivée du raccrochage ; c'était le worker qui finissait par abandonner un participant devenu muet, via un délai d'expiration qui n'avait jamais été prévu pour porter un raccrochage.
Nous avons cessé de compter sur le message. Le service audio indique déjà au worker pourquoi un participant est parti, et il distingue un départ initié par le client d'une coupure réseau. Le worker traite désormais un départ initié par le client comme un raccrochage et termine l'appel après un court délai de grâce. Une coupure réseau garde le délai plus long, pour qu'un invité qui traverse une zone blanche ne se fasse pas raccrocher au nez.
La preuve est arrivée le lendemain matin, le 30 octobre. À 07:49:06 UTC, le worker a enregistré le départ de l'invité avec un motif initié par le client. L'hôte est parti à 07:49:10. L'appel s'est terminé à 07:49:15 avec le motif « guest left ». Environ quatre secondes, là où il y en avait quatorze.
Le client envoie toujours le message de raccrochage. Il ne sert plus à rien. Nous le retirerons dans une future version de l'app.
Ce que les trois ont en commun
Chacun de ces bugs a survécu parce que quelque chose avait l'air de marcher.
L'appel de zéro seconde était suivi d'un appel qui tenait, si bien que le deuxième appui de l'utilisateur le « réparait ». Le bouton appuyer pour parler changeait d'apparence quand on le pressait ; le serveur ne l'entendait jamais. Raccrocher terminait l'appel, au bout d'un moment, via un délai d'expiration qui n'avait jamais été conçu pour ça.
Aucun n'a produit une ligne de log disant que quelque chose n'allait pas. Le service audio écarte un paquet venu d'un émetteur sans le droit et ne dit rien à l'émetteur. Un message envoyé juste avant une déconnexion peut disparaître avec la connexion. Un état périmé n'est pas une exception.
Ce qui les a trouvés, c'est la lecture de quelque chose qu'on ne peut pas impressionner : la colonne de durée dans la base, une sonde qui affichait ce que le récepteur recevait, un décompte d'événements dans le log du worker. Six événements d'appui pour parler et zéro raccrochage, sur les mêmes appels, c'est un diagnostic. « Ça marche sur mon téléphone » n'en est pas un.
Nous avions déjà une règle pour ça, héritée d'autres projets : un élément n'est pas corrigé tant que sa preuve sur une cible réelle n'a pas été produite. Ces quatre jours nous ont montré contre quoi la règle protège. Sans elle, appuyer pour parler serait resté « livré » jusqu'à la démo investisseurs, et le raccrochage à 14 secondes serait passé pour une bizarrerie.
Ce que ça a changé, et ce qui reste ouvert
Trois choses ont changé dans notre façon de travailler sur Duo.
Un banc de test doit désormais demander ses jetons au backend au lieu de les fabriquer lui-même. Tant que ce n'est pas fait, le banc n'est fiable sur rien de ce qui touche aux permissions. C'est sur la liste de la prochaine vérification groupée.
Chaque correctif porte désormais l'observation qui le prouvera, écrite avant le test : quel événement, dans quel log, sur quelle cible. Le 29 octobre, cette phrase était prête avant que Juste ne prenne son téléphone, et la session de test a été une lecture, pas un débat.
Une leçon plus modeste est sortie de la même soirée. Le marqueur de version que le worker affiche au démarrage était bloqué sur une ancienne valeur depuis une semaine ; il ne prouvait donc rien sur ce qui tournait. Nous l'avons mis à jour ce soir-là. Il a de nouveau dérivé depuis, et tant qu'il ne sera pas lié automatiquement au commit, nous lisons les déploiements depuis le conteneur en cours d'exécution, pas depuis le marqueur.
Le droit sur les données, appuyer pour parler et le raccrochage sont prouvés en production. Une salle de présentation avec deux auditeurs dans deux langues restait à prouver à la fin de cette journée de tests : la seule présentation lancée par Juste a duré 15 secondes, sans personne à l'écoute. Ce test est venu plus tard, et il a trouvé quelque chose que nous n'attendions pas.