By Juste Thales Gnimavo (CEO, ZeroSuite) and Claude Opus 5.5 (Claude Code)
Between October 27 and October 30, 2026, we found three bugs in Déblo Duo that had one thing in common. None of them raised an error. The app looked fine, the server looked fine, the logs were clean. Each time, what told us something was wrong was a number: a call that lasted zero seconds, a count of messages that should have been six and was zero, a delay of 14 seconds where there should have been none.
Déblo Duo is our real-time interpreter: each person speaks their language and hears the other in theirs. (The background is in Why Déblo Duo exists.) A call has several participants in a room on our real-time audio layer: the host, the guest, and a server-side worker that bridges their audio to Google's real-time speech translation model. Most of what matters travels as audio. A few things travel as small data messages: in one-phone mode, "I am holding the button, it is my turn to speak"; in every mode, "I am hanging up".
October 27: a call that hung up at zero seconds
The first one came from Juste, testing Duo 1.0.9 on his phone. His report: "le premier appui sur le gros micro lance un appel, le deuxième marche." The first press on the big microphone starts a call, the second one works.
It is the kind of report that is easy to dismiss as a mis-tap. We read the database instead. In the voice sessions table there were one-phone sessions of exactly 0 seconds at 05:37 and 05:39 UTC, each followed by an attempt that held. The bug was real, and it was repeatable.
The cause was stale state. Ending a call left the shared call state in its "ended" phase. The one-phone screen is the only call screen that starts directly in the call step; the others start on an invitation or a loading screen. On its first render, the screen's end-of-call effect read the phase left over from the previous call before the reset ran, concluded the call was over, and hung up.
The fix was to reset the call state before navigating to the one-phone screen, from its single entry point. Duo 1.0.10 shipped with it. Juste confirmed no call hung up at zero any more, and the database showed no 0-second session since the install. We wrote down the reservation too: the exact sequence (one call, then a one-phone call in the same app session) was not visible in the database, because only one session had been recorded. That part rests on his observation.
October 29: a right that nobody had granted
Two days later, preparing a full day of tests, we looked at how the backend issues access tokens for a call. Each participant joins the room with a token that lists what they may do: publish audio, subscribe, publish data.
The host and guest tokens did not include the right to publish data. They never had, since the first interpreter briefs. Yet the clients send two things as data: the push-to-talk signal and the hang-up message.
Reading the code told us what should happen. We wanted to know what did happen. So we wrote a probe: three participants in a room on the production audio service, with no translation worker and no billing. One sender had no data right, one did, and a receiver listened.
The output was two lines. The sender without the right: no publish error. The receiver: one message, from the sender that had the right. The other message simply did not arrive, and nothing told its sender.
That meant two features had never worked in production. In one-phone mode, the push-to-talk button never reached the worker. The "Hang up" message never reached it either. One-phone mode had been marked delivered in an earlier session. Its test bench passed. The bench minted its own tokens, with the data right included, so it could not see what the backend actually issued. A bench that makes its own credentials does not test the real ones.
Before opening the right, we checked the other direction: could a guest now send a fake control message? The web and mobile clients ignore any data packet that does not come from the agent. The worker checks the sender's role: hang-up requires a known participant, the push-to-talk signal is accepted from the host alone. Listeners in a presentation keep no data right at all.
The fix was one change in the backend: grant the data right to the host and guest tokens. We replayed the probe with a token of the exact new shape, microphone plus data, at 21:38 UTC. The receiver got both messages. No app rebuild was needed, since tokens are issued fresh for every call. But the transport probe proved only the transport. Whether the fix worked in a real Duo call was left open, with the observation due written next to it: push-to-talk and hang-up events in the production worker's log during a real call.
That night: six calls, zero hang-ups
Juste tested from the living room that same night, October 29, between 23:22 and 23:38 UTC. One-phone mode worked: 1 minute 20 seconds. The worker's production log showed six push-to-talk events. That fix was proven.
The second test was the guest hanging up first. It "worked". The host's screen eventually showed "Terminé". After 14 seconds.
Fourteen seconds is long enough for a host to think the app has frozen, and short enough to pass in a quick demo. We counted. Across six calls, the worker had received zero hang-up messages. Push-to-talk messages, sent through the same path by the same clients, arrived every time.
The difference was what came right after. Push-to-talk is sent and the connection stays up. Hang-up is sent and then the client disconnects immediately. The message was lost on connection close. The host's "Terminé" was not the hang-up arriving; it was the worker eventually giving up on a participant who had gone silent, through a timeout that was never meant to carry a hang-up.
We stopped relying on the message. The audio service already tells the worker why a participant left, and it distinguishes a departure the client initiated from a network cut. The worker now treats a client-initiated departure as a hang-up and ends the call after a short grace. A network cut keeps the longer grace, so a guest walking through a dead zone is not hung up on.
The proof came the next morning, October 30. At 07:49:06 UTC the worker logged the guest leaving with a client-initiated reason. The host left at 07:49:10. The call ended at 07:49:15 with the reason "guest left". About four seconds where there had been fourteen.
The client still sends the hang-up message. It no longer does anything. We will remove it with a future app version.
What the three have in common
Each of these bugs survived because something looked like it worked.
The zero-second call was followed by a call that held, so the user's second tap "fixed" it. The push-to-talk button changed its look when pressed; the server never heard it. Hang-up ended the call, eventually, through a timeout that was never designed to carry it.
None of them produced a log line saying something was wrong. The audio service drops a packet from a sender without the right and says nothing to the sender. A message sent just before disconnecting can disappear with the connection. A stale state is not an exception.
What found them was a reading of something that cannot be impressed: the duration column in the database, a probe that printed what the receiver got, a count of events in the worker's log. Six push-to-talk events and zero hang-ups, on the same calls, is a diagnosis. "It works on my phone" is not.
We had a rule for this already, inherited from other projects: an item is not fixed until its proof on a real target has been produced. These four days showed us what the rule protects against. Without it, push-to-talk would have stayed "delivered" until the investor demo, and the 14-second hang-up would have been a quirk.
What it changed, and what is still open
Three things changed in how we work on Duo.
A test bench now has to ask the backend for its tokens instead of making its own. Until that is done, the bench cannot be trusted on anything that involves permissions. It is on the list for the next grouped verification.
Each fix now carries the observation that will prove it, written before the test: which event, in which log, on which target. On October 29 that sentence was ready before Juste picked up his phone, so the test session was a reading, not a debate.
One smaller lesson came out of the same night. The version marker the worker prints at boot had been stuck on an old value for a week, so it proved nothing about what was running. We updated it that night. It has drifted again since, and until it is tied to the commit automatically, we read deployments from the running container, not from the marker.
The data right, push-to-talk and hang-up are proven in production. A presentation room with two listeners in two languages was still unproven at the end of that test day: the one presentation Juste started lasted 15 seconds, with no one listening. That test came later, and it found something we did not expect.