<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dhruv</title>
    <description>The latest articles on DEV Community by Dhruv (@mycmdhub).</description>
    <link>https://dev.to/mycmdhub</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3740582%2F3e011681-a820-432e-8515-e5471cbefae7.jpeg</url>
      <title>DEV Community: Dhruv</title>
      <link>https://dev.to/mycmdhub</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mycmdhub"/>
    <language>en</language>
    <item>
      <title>Four words of Python killed every sentence after first</title>
      <dc:creator>Dhruv</dc:creator>
      <pubDate>Fri, 21 Aug 2026 01:54:55 +0000</pubDate>
      <link>https://dev.to/mycmdhub/four-words-of-python-killed-every-sentence-after-first-18pd</link>
      <guid>https://dev.to/mycmdhub/four-words-of-python-killed-every-sentence-after-first-18pd</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The bug report was: "it says hello, then it goes deaf."&lt;/p&gt;

&lt;p&gt;My AI receptionist answers a real landline. Caller dials, agent greets them properly, caller says "hi, I'd like to check my booking" and then nothing. Forever ♾️. The call stays connected. No error, no exception, no warning. The WebSocket to our speech provider sits there wide open, healthy, receiving audio, and the agent has simply stopped existing.&lt;/p&gt;

&lt;p&gt;Turn one worked. Turn two never happened. Every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wrong week
&lt;/h2&gt;

&lt;p&gt;I spent it on the network, because that is where the evidence pointed. A speech socket goes quiet, check the socket. I logged every frame in and out where frames were flowing. I checked for silent disconnects, proxy timeouts, Heroku's 55s idle timeout, mu-law framing, buffer starvation. I added heartbeats.&lt;/p&gt;

&lt;p&gt;Then I added reconnect logic, which "fixed" it in the sense that the agent came back four seconds later having forgotten the conversation.&lt;/p&gt;

&lt;p&gt;That reconnect was the tell and I ignored it. Something was closing a connection nobody had asked to close. I read it as "the network is flaky" because that was the story I had already decided to believe.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four words
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;receive_audio_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_connected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it the way the author did. This generator drains the speech socket for the lifetime of the call. When it finishes, the call is over, so mark the bridge disconnected. Clean, defensive, obviously right.&lt;/p&gt;

&lt;p&gt;Now read how the caller uses it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;bridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receive_audio_events&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EndOfTurn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;          &lt;span class="c1"&gt;# got the turn end, go run the model
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;break&lt;/code&gt; on an async generator runs its &lt;code&gt;finally&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not when the socket dies. Not when the call ends. Every single turn — the moment the caller stopped talking and we broke out to go think — Python unwound that generator, ran the cleanup, and set &lt;code&gt;is_connected = False&lt;/code&gt; on a socket in perfect health.&lt;/p&gt;

&lt;p&gt;Turn two arrives. The code checks &lt;code&gt;is_connected&lt;/code&gt;, sees &lt;code&gt;False&lt;/code&gt;, and politely declines to listen. The socket is fine. The audio is arriving. The flag is a lie, and the flag is what we trusted.&lt;/p&gt;

&lt;p&gt;Four words. One &lt;code&gt;finally:&lt;/code&gt; clause that was correct for the loop I had in mind, and fatal for the loop that actually existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nothing caught it
&lt;/h2&gt;

&lt;p&gt;This is the part I keep thinking about.&lt;/p&gt;

&lt;p&gt;There was no exception to catch, because nothing failed. No error to log, because nothing errored. No alert to fire, because every health check we had asked a component whether it was &lt;em&gt;up&lt;/em&gt;, and every component was up. The socket was open, the process was fine, memory was flat. The agent had quietly agreed with itself to stop.&lt;/p&gt;

&lt;p&gt;The tests were green too. They tested one turn.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, and the better fix
&lt;/h2&gt;

&lt;p&gt;The fix is to move the cleanup to where the connection genuinely ends, rather than where a consumer got bored:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;receive_audio_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# No `finally` here. This generator is broken out of every turn, and
&lt;/span&gt;    &lt;span class="c1"&gt;# `break` runs `finally`. Connection lifetime is closed by close().
&lt;/span&gt;    &lt;span class="c1"&gt;# A consumer that stops iterating is not a closed connection.
&lt;/span&gt;    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The better fix was structural, and it is the one I would actually recommend to anyone debugging a live provider integration: &lt;strong&gt;I stopped using a human as the regression suite.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For weeks the loop had been — someone calls the number, describes what they heard, I dig through logs and guess. That is a debugging cycle measured in hours per iteration with a person as the test harness, and it is why one &lt;code&gt;finally&lt;/code&gt; clause survived a week.&lt;/p&gt;

&lt;p&gt;What replaced it was unglamorous: a standalone script that opens the provider's WebSocket directly, sends the &lt;strong&gt;real&lt;/strong&gt; prompt and the &lt;strong&gt;real&lt;/strong&gt; tool definitions, and prints every single message that comes back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;websockets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;DEEPGRAM_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;subprotocols&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DEEPGRAM_API_KEY&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings_msg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# exactly what the handler sends
&lt;/span&gt;    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="c1"&gt;# exactly what it says back
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;logger.info(message)&lt;/code&gt; — no filtering, no parsing, no &lt;code&gt;if&lt;/code&gt;/&lt;code&gt;elif&lt;/code&gt;. That last line is the entire trick, and it is the line the production code was missing.&lt;/p&gt;

&lt;p&gt;Because the moment I stopped interpreting the provider's messages and just printed them, the thing our handler had been silently discarding for a week was sitting there in the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UNPARSABLE_CLIENT_MESSAGE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unknown field `processors`, expected `thresholds`"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A hundred and fifty lines, most of it the payload. I should have written it in week one, and the reason I didn't is that building the harness always feels like the thing you do &lt;em&gt;instead of&lt;/em&gt; fixing the bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;p&gt;I stopped trusting flags.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;is_connected&lt;/code&gt; was a variable claiming to know something about the world, and for a week it lied to me while every socket, every process and every health check was fine. A flag can only ever tell you what some earlier code believed.&lt;/p&gt;

&lt;p&gt;Now I measure the thing itself. Did audio come back? How long did it take?&lt;/p&gt;

&lt;p&gt;It took four words to break it and a week to find them.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
    </item>
    <item>
      <title>Six silent bugs in a voice AI phone line</title>
      <dc:creator>Dhruv</dc:creator>
      <pubDate>Fri, 21 Aug 2026 01:53:57 +0000</pubDate>
      <link>https://dev.to/mycmdhub/six-silent-bugs-in-a-voice-ai-phone-line-392</link>
      <guid>https://dev.to/mycmdhub/six-silent-bugs-in-a-voice-ai-phone-line-392</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The agent said goodbye and then stayed on the line for 566 seconds. That was the loudest of six bugs that all returned successfully, logged an OK, and kept running, six PRs, and the traces for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;Ovela answers the phone for a motel in regional Victoria. A guest dials a landline, and five things have to happen before the agent can say a word:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the phone network hands me the caller's audio (&lt;strong&gt;Twilio&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;something decides when the caller has actually stopped talking, as opposed to pausing (&lt;strong&gt;Deepgram&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;a model decides what to say, and which of 12 tools to call (&lt;strong&gt;OpenAI&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;something turns that text into speech (&lt;strong&gt;Cartesia&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;it goes back down the phone line&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of it inside &lt;strong&gt;800 ms&lt;/strong&gt;. Past about a second, people say "hello?" and start talking over the agent. There is no spinner on a phone call, and no way to say "still thinking".&lt;/p&gt;

&lt;p&gt;Two words you will need. A &lt;strong&gt;turn&lt;/strong&gt; is one round trip - caller speaks, agent answers and it is the unit everything here is measured in. &lt;strong&gt;Barge-in&lt;/strong&gt; is the caller talking over the agent, which has to cut the agent off mid-word, the way a human would stop talking if you interrupted them.&lt;/p&gt;

&lt;p&gt;That deadline is why these bugs were expensive, and the deadline is also why none of them looked like bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;Start with the one that would not let a caller off the phone.&lt;/p&gt;

&lt;p&gt;The agent has a tool for ending a call. It returned this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hangup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Thanks for calling, goodbye!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing read &lt;code&gt;action&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When a model calls a tool, you run the function and hand the return value back to the model as text, and it carries on from there. That is the tool loop, and mine passed this whole dict straight through. The model found &lt;code&gt;message&lt;/code&gt;, said goodbye — warmly, in the right tone, at the right moment. Then the line stayed open.&lt;/p&gt;

&lt;p&gt;Here is one call, laid out from Twilio's record and Sentry's turn timeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;01:06:15   call answered
01:07:46   1:31  caller says goodbye, agent says goodbye back
              …  475 seconds of an open line. Three turns fire in that
                 stretch, all on room noise, none of them a conversation.
01:15:41   9:26  the caller gives up and hangs up the phone themselves
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1gy422o01yn15iams1tf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1gy422o01yn15iams1tf.png" alt="Twilio's own record: 9 min 26 sec on a call the agent had already said goodbye on." width="800" height="92"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sentry has the hole. Each row is one turn; the last column is the start time in seconds, so the spacing is the conversation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr4zos7o69vmeiruj8728.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr4zos7o69vmeiruj8728.png" alt="Thirteen turns, one per row. The last column is the start time in seconds — 12, 11, 8, 12, 12, 7, 13, 7, 3… then 313." width="800" height="373"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;b82b0208   1.48s    1,786,756,066.3304   ← the goodbye
aa847306   1.48s    1,786,756,379.7518   ← the next thing that fired
                    ─────────────────────
                              313 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rows above those two are spaced 12, 11, 8, 12, 12, 7, 13, 7 and 3 seconds apart. Then 313.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;566 seconds, and the agent ended none of it.&lt;/strong&gt; &lt;code&gt;transfer_to_staff&lt;/code&gt; had the identical defect — it promised a transfer that never dialled.&lt;/p&gt;

&lt;p&gt;Read the transcript and the call is perfect. Read the logs and everything succeeded. The only places it looks wrong are the carrier's billing record and a five-minute hole in a column of timestamps.&lt;/p&gt;

&lt;p&gt;Reconstructing that call took a Twilio record and thirteen unrelated Sentry traces, because in August each turn was its own trace with nothing tying them together. That is exactly what &lt;a href="https://github.com/My-CMDhub/Ovela-AI/pull/11" rel="noopener noreferrer"&gt;PR #11&lt;/a&gt; fixes: the same call today arrives as one conversation, in one view.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That is the shape of every serious bug in this system.&lt;/strong&gt; They return, they log an OK, they keep running. Here are four more of the same family, all from the provider boundary:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the provider told me&lt;/th&gt;
&lt;th&gt;What I did with it&lt;/th&gt;
&lt;th&gt;What the caller heard&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UNPARSABLE_CLIENT_MESSAGE: unknown field 'processors', expected 'thresholds'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;dropped&lt;/td&gt;
&lt;td&gt;25 seconds of silence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;400 unsupported encoding for raw: mulaw&lt;/code&gt; (correct: &lt;code&gt;pcm_mulaw&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;dropped&lt;/td&gt;
&lt;td&gt;only the cached greeting was ever real speech&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;sonic-english&lt;/code&gt; sunsetted → a second &lt;code&gt;400&lt;/code&gt; behind the first&lt;/td&gt;
&lt;td&gt;dropped&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;TurnInfo&lt;/code&gt; carries its state in &lt;code&gt;event&lt;/code&gt;; I read &lt;code&gt;type&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;matched nothing&lt;/td&gt;
&lt;td&gt;barge-in never fired, and neither did the early nudge to the model that starts it thinking before the caller finishes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each of those arrived on a WebSocket as an ordinary message and hit an &lt;code&gt;if&lt;/code&gt;/&lt;code&gt;elif&lt;/code&gt; chain with &lt;strong&gt;no&lt;/strong&gt; &lt;code&gt;else&lt;/code&gt;. Parsed, matched nothing, discarded. The socket stayed open. Audio kept flowing in. Nothing came back, and every health check in the system stayed green, because every component genuinely was up.&lt;/p&gt;

&lt;p&gt;Two more, once the telemetry was honest enough to find them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tenant lookup 404'd on every single call.&lt;/strong&gt; It queried by document ID; the rows are keyed by slug. It failed, fell through to the correct query, succeeded, and logged success — while spending &lt;strong&gt;4,316 ms&lt;/strong&gt; on the critical path of the first turn. A slow path that is also a working one is the hardest kind to notice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One booking was looked up eight times in a single call&lt;/strong&gt;, because nothing remembered the answer to a question the caller had not changed. One database round trip is about 250 ms; a single &lt;code&gt;lookup_booking&lt;/code&gt; makes up to three of them in sequence includes phone, then name, then a hundred-row scan to redo the name match case insensitively - so a miss costs 1.1 to 1.6 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;Six PRs, stacked, each one reviewable on its own:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PR&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/My-CMDhub/Ovela-AI/pull/7" rel="noopener noreferrer"&gt;#7 Make the provider bridges speak the APIs the providers actually run&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;the four dropped errors, and a &lt;code&gt;finally&lt;/code&gt; that ran on every &lt;code&gt;break&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/My-CMDhub/Ovela-AI/pull/8" rel="noopener noreferrer"&gt;#8 Stop the agent interrupting itself&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;barge-in armed before any audio existed; chunks from an abandoned turn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/My-CMDhub/Ovela-AI/pull/9" rel="noopener noreferrer"&gt;#9 Drive the turn from the orchestrator&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;a design change, not a fix&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/My-CMDhub/Ovela-AI/pull/10" rel="noopener noreferrer"&gt;#10 Execute control-flow tool actions, kill the cold start&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;the 566-second call; 4,316 ms → 12 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/My-CMDhub/Ovela-AI/pull/11" rel="noopener noreferrer"&gt;#11 Repair the telemetry, group the call as one conversation&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;a span measuring 0.01 ms; a span hiding 84% of a turn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/My-CMDhub/Ovela-AI/pull/12" rel="noopener noreferrer"&gt;#12 Stop paying for work already done&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;prompt cache missed every turn; eight identical lookups&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;#9 is the odd one out, and its commit message says so in the first line: it is not a bug fix.&lt;/strong&gt; It moves the live conversation off a multi-agent graph and into a direct streaming call. The graph routes work between a Manager and two workers — worth its latency for background jobs, not for someone waiting mid-sentence. Under the real payload it cost 2,810 ms to first token against 1,040 ms direct.&lt;/p&gt;

&lt;p&gt;Everything else in that table is the same bug wearing different clothes: &lt;strong&gt;a message arrived, nothing was listening, and the system reported success.&lt;/strong&gt; A dropped provider error, an &lt;code&gt;action&lt;/code&gt; field with no reader, a span opened and closed on the same line, an &lt;code&gt;environment&lt;/code&gt; tag never set. Same shape, six times.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hanging up without cutting off the goodbye.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The obvious fix is to terminate the call the moment the tool fires — and then the caller never hears the farewell, because the line drops mid-word. So the intent is recorded, the audio finishes draining, and only then does the call end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hangup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pending_hangup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;interrupted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;ConversationState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AGENT_SPEAKING&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pending_hangup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pending_hangup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;interrupted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🛑 Hangup aborted — user spoke during farewell&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_hangup_call&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the caller starts speaking during the goodbye, they have changed their mind, and hanging up on them would be worse than the original bug. The termination is also idempotent — because a tool with no observable effect gets retried by the model, which is how the bug announced itself in the first place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caching the task, not the result.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The repeated booking lookup got a per-call memo. The detail that matters is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Cache the task, not the result: the prefetch and the first tool call
&lt;/span&gt;    &lt;span class="c1"&gt;# race for the same row, and the loser must join the in-flight request
&lt;/span&gt;    &lt;span class="c1"&gt;# instead of issuing a second one.
&lt;/span&gt;    &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ensure_future&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lookup_motel_reservation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you cache the finished value, both racers find an empty cache and both hit the database. Caching the unfinished task makes the second one wait on the first. Invalidation uses a read-only allowlist, so a tool added next month that writes a booking will be slow before it is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two lines at the front of a prompt.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The other half of that PR is smaller and I nearly left it out. A volatile header — the current date and time — sat at the very start of a ~9,000-token system prompt. The prefix therefore changed on every single turn, so none of it could ever be reused. Moving it to the end was a two-line diff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- return f"""{context_header}You're the AI receptionist for Coal Creek Motel...
&lt;/span&gt;&lt;span class="gi"&gt;+ return f"""You're the AI receptionist for Coal Creek Motel...
+ {context_header}"""
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cached prompt tokens are billed at a discount and, more to the point here, skip re-processing — so this is both cheaper and quicker to first token. Sentry reads the result straight off the model call: &lt;strong&gt;178 new tokens, 8.9K served from cache, 32 out&lt;/strong&gt; — 98% of the prompt reused on a turn budgeted at 800 ms end to end.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcfcfafqxheik4dkey24z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcfcfafqxheik4dkey24z.png" alt="One model call after the prompt fix: 178 new tokens in, 8.9K served from cache." width="799" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the memo did, stated precisely.&lt;/strong&gt; Two live calls, one either side of the fix, paired trace by trace:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;before (8 calls)&lt;/th&gt;
&lt;th&gt;after (7 calls)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Queries that actually ran&lt;/td&gt;
&lt;td&gt;8 of 8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4 of 7&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resolved from an in-flight task&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;3&lt;/strong&gt;, at ~0.1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median of the queries that still ran&lt;/td&gt;
&lt;td&gt;1,263 ms&lt;/td&gt;
&lt;td&gt;1,136 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total query wait per call&lt;/td&gt;
&lt;td&gt;11,049 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4,496 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Look at the third row, because it says what this fix is not. A memo cannot make a database query faster, and it didn't. The 59% saving is entirely the three queries that stopped happening.&lt;/p&gt;

&lt;p&gt;Elsewhere: cold start &lt;strong&gt;4,316 ms → 12 ms&lt;/strong&gt; — that pair came from Heroku startup logs at the time, which have since rotated, so it is the one number here with no artifact behind it. Conversational turns land at &lt;strong&gt;400–890 ms&lt;/strong&gt;, median &lt;strong&gt;790 ms&lt;/strong&gt; across 28 consecutive turns. There is no before-figure for that median, and I am not going to manufacture one — before these fixes the pipeline never reached a second turn, so there was nothing to time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Use of Sentry
&lt;/h2&gt;

&lt;p&gt;Sentry found the latency bugs, and one I shipped myself halfway through. It is not the place I reported them from afterwards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The span that was lying to me.&lt;/strong&gt; A span is a stopwatch around one stage of a request; nest them and a turn arrives as a waterfall instead of a single number. Mine covered "user speech ended → tool executed", which turned out to be far too much ground for one stopwatch. Across the 25 pre-fix turns still inside Sentry's retention window it held a median &lt;strong&gt;84%&lt;/strong&gt; of turn time. Here is one of them: 5.29 seconds of a 5.52 second turn in a single bar, with the only other span reading &lt;strong&gt;0.01 ms&lt;/strong&gt;, because it was opened and closed on the same line.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr7sj1zxcqrrgopmyh7ge.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr7sj1zxcqrrgopmyh7ge.png" alt="Before: one bar holding 5.29 s of a 5.52 s turn, and a span measuring 0.01 ms because it opened and closed on the same line." width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I read that 84% and concluded the model was slow, because the model was what I already suspected. Splitting it into &lt;code&gt;llm.stream&lt;/code&gt; per model round and &lt;code&gt;gen_ai.execute_tool&lt;/code&gt; per tool inverted the answer immediately:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0kcbbhpqqvl7ama738xu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0kcbbhpqqvl7ama738xu.png" alt="After: the same stage split into a model round, a tool call, a second model round and speech synthesis. Different turn, different length — the shape is the point." width="799" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A 2.02 s turn: &lt;strong&gt;538 ms&lt;/strong&gt; model round, &lt;strong&gt;506 ms&lt;/strong&gt; tool call, &lt;strong&gt;407 ms&lt;/strong&gt; second model round, &lt;strong&gt;268 ms&lt;/strong&gt; of speech synthesis. (Different turn, different length — compare the shape, not the totals.) I had spent a day blaming the language model. It was a database round trip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One phone call, one conversation.&lt;/strong&gt; The pipeline emits Sentry's &lt;code&gt;gen_ai.&lt;/code&gt;* conventions with the Twilio CallSid as the conversation id:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk.ai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;set_conversation_id&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call_sid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;set_conversation_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call_sid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line, and the dashboard stopped being a latency chart. This is a real call - the caller's words, the tool the agent reached for, the arguments it passed, and the cost of each:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqfq1po266208ygv99o41.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqfq1po266208ygv99o41.png" alt="One phone call as one Sentry conversation. The same lookup_booking runs at 1.72 s, then resolves from the memo at 0.08 ms and 0.09 ms." width="800" height="473"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lookup_booking  guest_name: "Drew Patel"    1.72 s     ← the real query
lookup_booking  guest_name: "Drew Patel"    0.08 ms    ← memo
lookup_booking  guest_name: "Drew Patel"    0.09 ms    ← memo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;That screenshot also contains a bug I have not fixed.&lt;/strong&gt; Look at the turn where the caller spells their name — &lt;code&gt;B h r u v p a t e l&lt;/code&gt; — and the very next lookup still goes out as &lt;code&gt;"Drew Patel"&lt;/code&gt;. The memo did its job: same question in, cached answer back. The model never updated the argument after being corrected. A cache is only ever as right as the question it is asked, and that one is next on my list as UX work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sentry also caught me.&lt;/strong&gt; While adding those tool spans I shipped this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_child&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen_ai.execute_tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{...})&lt;/span&gt;
&lt;span class="c1"&gt;# TypeError: Span.__init__() got an unexpected keyword argument 'attributes'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;start_child()&lt;/code&gt; takes no &lt;code&gt;attributes&lt;/code&gt;. My unit test passed because it used a &lt;code&gt;MagicMock&lt;/code&gt;, and a MagicMock accepts every keyword argument you invent. Someone called the number ten minutes later and every tool call on that call failed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F75wqfue84qd02vrmi7my.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F75wqfue84qd02vrmi7my.png" alt="The bug I shipped mid-challenge, caught in Sentry before the call it broke had ended." width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I found it in the issue before the call had ended, and rewrote the test against a real &lt;code&gt;Transaction&lt;/code&gt; subclass — then proved the new test fails against the broken version. &lt;strong&gt;A mock at a provider boundary tests your assumption. The provider's contract is somewhere else, and it does not care what your mock agreed to.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also in Sentry, also mine:&lt;/strong&gt; &lt;code&gt;sentry_sdk.init()&lt;/code&gt; never passed an &lt;code&gt;environment&lt;/code&gt;, so every event arrived tagged &lt;code&gt;production&lt;/code&gt; — including the ones my own test suite raised on every run. PR #7 sets it and blanks the DSN in &lt;code&gt;conftest.py&lt;/code&gt;. That is why every screenshot here reads &lt;code&gt;environment: demo&lt;/code&gt;: it is the live service, labelled correctly for the first time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I could not run Seer Autofix.&lt;/strong&gt; I asked Sentry support, including about the &lt;code&gt;bugsmash26&lt;/code&gt; credits but needs a paid plan and the credits do not unlock it on a free-tier org. So there is no RCA screenshot here. The conversation view above is what I used instead, and it is the artifact that showed me the tool firing three times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Use of Google AI
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Gemini 2.5 Flash reads the traces after every call, without being asked.&lt;/strong&gt; When Twilio posts a &lt;code&gt;completed&lt;/code&gt; callback, &lt;a href="https://github.com/My-CMDhub/Ovela-AI/blob/bugsmash/submission/backend/services/latency_watchdog.py" rel="noopener noreferrer"&gt;a watchdog&lt;/a&gt; waits two minutes for spans to land, &lt;a href="https://github.com/My-CMDhub/Ovela-AI/blob/bugsmash/submission/backend/scripts/analyze_trace.py" rel="noopener noreferrer"&gt;pulls them from Sentry's API&lt;/a&gt;, aggregates them per span name, and hands the numbers to Gemini with the pipeline architecture as context. It replies in strict JSON.&lt;/p&gt;

&lt;p&gt;Here is a real run: &lt;a href="https://github.com/My-CMDhub/Ovela-AI/blob/bugsmash/submission/benchmarks/trace_analysis.json" rel="noopener noreferrer"&gt;the whole file is in the repo&lt;/a&gt;, including the two root causes I trimmed for space:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bottleneck_span"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Span 1: User Speech Ended -&amp;gt; First Token Yielded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bottleneck_share_pct"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"root_causes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Excessive latency in specific tool executions: The `perform_live_search`
     tool has an extremely high median latency of 2126.73 ms, and other tools
     like `check_availability` and `lookup_booking` also add ~500 ms when
     invoked."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The pipeline currently does not meet the sub-800ms TTFA target,
              with a median `user_voice_turn_transaction` of 952.18 ms."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the honest shape of this system, stated once: &lt;strong&gt;a turn with no tool call lands at ~790 ms median across 28 turns. A turn that hits the database lands around 950 ms.&lt;/strong&gt; The 800 ms budget is met on conversation and missed on lookups — which is exactly what the analyser flags, unprompted, after every call, because that window mixes both kinds of turn together.&lt;/p&gt;

&lt;p&gt;It also flagged &lt;code&gt;perform_live_search&lt;/code&gt; at 2,126 ms — on two samples, so a median of two points is just the slower one. The analyser did not mention that, and I will.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Gemini does not do here is fix files, and it has not yet found a root cause I did not already have.&lt;/strong&gt; It reads production telemetry and says where to look, unprompted, after every call. That step used to cost me a day per bug. A span that regresses more than 40% against the previous run raises a Sentry message; nobody has to open a chart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google ADK still runs the cold path, and that is exactly why PR #9 exists.&lt;/strong&gt; The graph was driving the live conversation, and it was never built for that: it routes work between a Manager, a BookingWorker and an InfoWorker, which is worth its latency in the background and not worth it to someone waiting mid-sentence. Measured under the payload actually shipped a ~9,500-token prompt with 12 tool definitions — the direct streaming call reached first token in &lt;strong&gt;1,040 ms against 2,810 ms&lt;/strong&gt;. On a bare "hello" the ranking reverses, which is precisely why the benchmark that counts is the real payload, and why a day of my optimisation went the wrong way before I understood that. &lt;/p&gt;

&lt;p&gt;So Gemini does the analysis and the background reasoning, and does not drive the conversation, because the measurement said not to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every number above was read out of Sentry after a call placed over the phone network📞.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;here's what the latency sounds like:&lt;/em&gt; &lt;a href="https://drive.google.com/file/d/1wYBWHZL0o3vQ8x-mbOJP0yv9JBffuFsd/view?usp=sharing" rel="noopener noreferrer"&gt;click to hear&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
    </item>
    <item>
      <title>From Voice Demo to Operational Voice Assistant: Reviving Ovela AI</title>
      <dc:creator>Dhruv</dc:creator>
      <pubDate>Wed, 03 Jun 2026 15:17:28 +0000</pubDate>
      <link>https://dev.to/mycmdhub/from-voice-demo-to-operational-voice-assistant-reviving-ovela-ai-2nlm</link>
      <guid>https://dev.to/mycmdhub/from-voice-demo-to-operational-voice-assistant-reviving-ovela-ai-2nlm</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-05-21"&gt;GitHub Finish-Up-A-Thon Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Ovela AI started as a side project driven by a question that kept pulling me back:&lt;/p&gt;

&lt;p&gt;What would it take for a business to genuinely trust a voice AI system?&lt;/p&gt;

&lt;p&gt;At first, I thought the answer was simple: make conversations sound natural.&lt;/p&gt;

&lt;p&gt;The original prototype could answer calls, respond to questions, and carry a conversation reasonably well. From a technical perspective, it looked impressive.&lt;/p&gt;

&lt;p&gt;But after speaking with accommodation providers and small business owners, I realized I was focused on the wrong problem.&lt;/p&gt;

&lt;p&gt;Businesses don't trust a system because it sounds human.&lt;/p&gt;

&lt;p&gt;They trust it because it behaves reliably.&lt;/p&gt;

&lt;p&gt;Can it check availability correctly?&lt;/p&gt;

&lt;p&gt;Can it update reservations safely?&lt;/p&gt;

&lt;p&gt;Can it collect payments?&lt;/p&gt;

&lt;p&gt;Can it transfer a call when confidence is low?&lt;/p&gt;

&lt;p&gt;Can staff see exactly what happened afterward?&lt;/p&gt;

&lt;p&gt;That realization changed the direction of the project completely.&lt;/p&gt;

&lt;p&gt;Ovela AI evolved from a voice demo into an operational voice assistant designed to help businesses handle real customer interactions while keeping humans in control of important decisions.&lt;/p&gt;

&lt;p&gt;Today, Ovela can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handle inbound phone calls&lt;/li&gt;
&lt;li&gt;Check room availability&lt;/li&gt;
&lt;li&gt;Create reservations&lt;/li&gt;
&lt;li&gt;Process payments through Stripe&lt;/li&gt;
&lt;li&gt;Answer property and local information questions&lt;/li&gt;
&lt;li&gt;Transfer calls when needed&lt;/li&gt;
&lt;li&gt;Keep staff synchronized through a management dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More importantly, every improvement is guided by a simple principle:&lt;/p&gt;

&lt;p&gt;AI should support human operations, not blindly replace them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;📞 Live Demo (Australia)&lt;/p&gt;

&lt;p&gt;Phone: +61 3 4823 6219&lt;/p&gt;

&lt;p&gt;Due to abuse protection and testing limits, availability may occasionally be restricted.&lt;/p&gt;

&lt;p&gt;Try asking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Do you have any rooms available this weekend?"&lt;/li&gt;
&lt;li&gt;"Can I make a reservation?"&lt;/li&gt;
&lt;li&gt;"What attractions are nearby?"&lt;/li&gt;
&lt;li&gt;"What's the weather like today?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🌐 Website: &lt;a href="https://ovela.dev" rel="noopener noreferrer"&gt;https://ovela.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🐙 GitHub Repository:&lt;a href="https://github.com/My-CMDhub/Ovela-AI" rel="noopener noreferrer"&gt;https://github.com/My-CMDhub/Ovela-AI&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Comeback Story
&lt;/h2&gt;

&lt;p&gt;Like many side projects, Ovela reached a point where the prototype worked well enough to demonstrate the idea.&lt;/p&gt;

&lt;p&gt;Then it sat untouched not because the project failed but because other priorities took over.&lt;/p&gt;

&lt;p&gt;Months later, after more conversations with business owners and more exposure to real operational challenges, I came back to the project with a very different perspective.&lt;/p&gt;

&lt;p&gt;The biggest lesson was surprisingly non-technical.&lt;/p&gt;

&lt;p&gt;The challenge isn't making AI speak.&lt;/p&gt;

&lt;p&gt;The challenge is making AI behave appropriately within human workflows.&lt;/p&gt;

&lt;p&gt;A real receptionist doesn't simply answer questions.&lt;/p&gt;

&lt;p&gt;They:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Recognize interruptions&lt;/li&gt;
&lt;li&gt;Acknowledge requests before acting&lt;/li&gt;
&lt;li&gt;Handle uncertainty&lt;/li&gt;
&lt;li&gt;Understand when information is missing&lt;/li&gt;
&lt;li&gt;Escalate sensitive situations&lt;/li&gt;
&lt;li&gt;Maintain context across an entire conversation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most voice demos don't fail because speech recognition is poor.&lt;/p&gt;

&lt;p&gt;They fail because the operational behavior doesn't match what people expect from a trusted assistant.&lt;/p&gt;

&lt;p&gt;That became the focus of the revival.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Changed
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Multi-Agent Architecture
&lt;/h4&gt;

&lt;p&gt;The original system relied on a much simpler flow.&lt;/p&gt;

&lt;p&gt;The new version uses a multi-agent architecture built around Google's Agent Development Kit (ADK), allowing different agents to handle reservations, business operations, and information requests independently.&lt;/p&gt;

&lt;h4&gt;
  
  
  Lower Latency Conversations
&lt;/h4&gt;

&lt;p&gt;Voice interactions are highly sensitive to delays.&lt;/p&gt;

&lt;p&gt;Several architectural bottlenecks were removed to improve response times and reduce awkward pauses during calls.&lt;/p&gt;

&lt;h4&gt;
  
  
  Stronger Context Awareness
&lt;/h4&gt;

&lt;p&gt;One of the most interesting challenges was interruption handling.&lt;/p&gt;

&lt;p&gt;People interrupt constantly during real conversations.&lt;/p&gt;

&lt;p&gt;The system now maintains awareness of what information has already been spoken, allowing it to continue naturally instead of restarting or losing context.&lt;/p&gt;

&lt;h4&gt;
  
  
  Operational Reliability
&lt;/h4&gt;

&lt;p&gt;Reservation workflows, payment handling, availability checks, and dashboard synchronization were rebuilt to behave more like real business processes rather than isolated AI actions.&lt;/p&gt;

&lt;h4&gt;
  
  
  Abuse Protection
&lt;/h4&gt;

&lt;p&gt;Real phone systems attract misuse.&lt;/p&gt;

&lt;p&gt;Rate limits, call protections, and operational safeguards were added to prevent abuse while keeping legitimate usage frictionless.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Experience with GitHub Copilot
&lt;/h2&gt;

&lt;p&gt;Returning to a codebase that has sat inactive for months is often harder than starting a brand new one. You inherit your own past decisions without fully remembering &lt;em&gt;why&lt;/em&gt; you made them. &lt;/p&gt;

&lt;p&gt;For the revival of Ovela AI, I didn't use GitHub Copilot as a simple autocomplete tool to write boilerplate code. Instead, I used it as a high-level engineering partner and data auditor to manage complex architectural shifts and harden my system’s reliability. &lt;/p&gt;

&lt;p&gt;Here are the two major ways Copilot helped me cross the finish line, backed by real-world interaction during my workflow development:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Translating Complex Systems into Architecture Diagrams
&lt;/h3&gt;

&lt;p&gt;As Ovela AI transitioned to a multi-agent setup, mapping out component connections, telephony triggers, and dashboard synchronization endpoints became a major cognitive bottleneck. I leveraged Copilot within my workspace as a principal solutions architect. By feeding it my core file dependencies, it mapped out a clean, production-ready system workflow directly in &lt;strong&gt;Mermaid.js&lt;/strong&gt; syntax for the repository documentation. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe86amleofdqd1e5x9e0s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe86amleofdqd1e5x9e0s.png" alt="Architecture Diagrams Breakdown" width="799" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Eval Hardening (The Supreme Judge)
&lt;/h3&gt;

&lt;p&gt;Building a reliable voice AI requires robust testing. I simulate conversations between two LLMs and dump the evaluation telemetry into local &lt;code&gt;.json&lt;/code&gt; log files. However, default automated grading scripts are notoriously prone to false positives (e.g., grading a hallucinated response highly simply because it sounded polite). &lt;/p&gt;

&lt;p&gt;I utilized Copilot as a &lt;strong&gt;Supreme AI Evaluation Auditor&lt;/strong&gt;. I passed it raw JSON conversation objects, prompting it to critically audit the automated scores, spot misleading feedback, and generate an adjusted "Supreme Score" with a bulleted logical justification. This drastically reduced noise in my evaluation pipeline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fayr6o4bxcb4adbc3nhsc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fayr6o4bxcb4adbc3nhsc.png" alt="Eval score finalisation" width="800" height="485"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx66o9v9sn2of5417sogq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx66o9v9sn2of5417sogq.png" alt="Solidifying eval score of simulation test" width="799" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Operational Code Polish &amp;amp; Balancing
&lt;/h3&gt;

&lt;p&gt;Beyond these two core pillars, Copilot served as an excellent "cleanup crew" throughout this journey even after hitting rate limits ✋. It assisted in tracking down legacy typing issues, reviewing asynchronous edge cases, and generating clean inline documentation. &lt;/p&gt;

&lt;p&gt;Ultimately, the biggest value Copilot provided wasn't just writing lines of code faster but it was accelerating complex architectural decisions and data validation when reviving a stale codebase.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;The most valuable lesson wasn't technical.&lt;/p&gt;

&lt;p&gt;It was understanding the difference between a convincing demo and a useful product.&lt;/p&gt;

&lt;p&gt;A demo succeeds when the AI says the right thing.&lt;/p&gt;

&lt;p&gt;A business system succeeds when the right thing actually happens afterward.&lt;/p&gt;

&lt;p&gt;That distinction changed how I think about voice AI.&lt;/p&gt;

&lt;p&gt;Natural conversation matters.&lt;/p&gt;

&lt;p&gt;Latency matters.&lt;/p&gt;

&lt;p&gt;Speech quality matters.&lt;/p&gt;

&lt;p&gt;But trust matters more.&lt;/p&gt;

&lt;p&gt;Trust comes from reliability, transparency, and knowing when humans should remain part of the process.&lt;/p&gt;

&lt;p&gt;I don't believe current voice AI systems perfectly replicate human interaction, and that's not really the goal.&lt;/p&gt;

&lt;p&gt;What interests me is the space between humans and AI:&lt;/p&gt;

&lt;p&gt;How can AI handle repetitive operational work while humans remain responsible for judgment, relationships, and important decisions?&lt;/p&gt;

&lt;p&gt;Reviving Ovela helped me explore that question far more deeply than when I first started the project.&lt;/p&gt;

&lt;p&gt;And honestly, that's what made finishing it worthwhile.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
      <category>agents</category>
      <category>github</category>
    </item>
  </channel>
</rss>
