<?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%2F7c20ddc7-a082-4223-9d2b-248d53654f56.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>No-Show Desk: TabPFN spots the motel guests likely to cancel, and Gemma asks them first</title>
      <dc:creator>Dhruv</dc:creator>
      <pubDate>Sun, 18 Oct 2026 00:06:15 +0000</pubDate>
      <link>https://dev.to/mycmdhub/no-show-desk-tabpfn-spots-the-motel-guests-likely-to-cancel-and-gemma-asks-them-first-4kl1</link>
      <guid>https://dev.to/mycmdhub/no-show-desk-tabpfn-spots-the-motel-guests-likely-to-cancel-and-gemma-asks-them-first-4kl1</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;A small motel loses a night's room every time a guest silently doesn't turn up and when no deposit was taken, nothing comes back. &lt;strong&gt;No-Show Desk&lt;/strong&gt; is a morning assistant for small motels and B&amp;amp;Bs. It picks the arriving guests most likely to cancel then says why in plain words and drafts a short "are you still coming?" message that the owner sends or skips. Scored on 30 real past mornings, learning only from earlier bookings, its picks caught 111 of 172 cancellations, against about 64 for random picks of the same size.&lt;/p&gt;

&lt;p&gt;Here is the demo morning (real 2017 bookings; guest names are made up). At 8 a.m. a Telegram message lands on the owner's phone:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Good morning. 7 guests arriving in the next 3 days might not show: $1,599.20 of bookings. Start with Chloe Nguyen (74%), Daniel Okafor (67%) and Ana Silva (61%), plus 4 more.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They tap through to one page. Chloe's row opens: room only, booked 72 days ahead, and &lt;em&gt;"34% of past room-only bookings cancelled, against 26% of all past bookings without a deposit."&lt;/em&gt; They press &lt;strong&gt;Draft a message&lt;/strong&gt;. A few seconds later there's a short note asking Chloe to confirm she's still coming or to say if her plans changed. Under it, in green: &lt;em&gt;Dates, nights and total match the booking.&lt;/em&gt; They press &lt;strong&gt;Send by email&lt;/strong&gt;. The owner decides everything; nothing is sent on its own.&lt;/p&gt;

&lt;p&gt;I'd been pitching motels a voice agent that answers the phone when nobody's at the desk. When this week's theme asked for something a small business would actually pay for, motels were the business I knew, so I built something smaller: one list and one message each morning, simple enough for a few owners to try.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TabPFN&lt;/strong&gt;, a model already trained to learn from small tables, scores every upcoming booking from the motel's own past bookings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4&lt;/strong&gt;, Google's open-weight model, writes the words on the owner's own computer, right inside the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plain code&lt;/strong&gt; works out every date, night count and total, and checks every draft before the owner can send it.&lt;/li&gt;
&lt;/ul&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%2Fahpiukfpceyjunue0udv.jpg" 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%2Fahpiukfpceyjunue0udv.jpg" alt="The No-Show Desk owner page with seven guests who might not show and a drafted message to Chloe that the checker has confirmed" width="800" height="727"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;On real past bookings, outcome hidden until scoring&lt;/th&gt;
&lt;th&gt;Measured&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cancellations the desk caught ahead of time, of all that happened, over 30 test mornings&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;111 of 172&lt;/strong&gt; (random picks of the same size: about 64)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drafts with a wrong date, night count or total&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;13 of 100&lt;/strong&gt; when Gemma worked them out, &lt;strong&gt;0 of 100&lt;/strong&gt; when code handed them over&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seconds per message, Gemma in the browser against Ollama (a program that runs models locally) on the same Mac&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;3.25&lt;/strong&gt; against 31.4 (median of 100; two builds of the same small model)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rule I built everything around: &lt;strong&gt;code computes, the model phrases.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who pays.&lt;/strong&gt; The owner of a small motel or B&amp;amp;B, who pays for a booking system but has no data team. On the demo morning, 4 of the 7 flagged guests did cancel, holding $776.60 of bookings, and a single night there is $67 to $107. The value is hearing sooner, while the room can still be resold, so the desk has to cost less than one resold night a month. Gemma costs nothing per message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it live:&lt;/strong&gt; &lt;a href="https://no-show-desk.onrender.com" rel="noopener noreferrer"&gt;no-show-desk.onrender.com&lt;/a&gt;. Every visitor gets their own copy of the demo morning. On a computer, the first &lt;strong&gt;Draft&lt;/strong&gt; asks before downloading Gemma (2 GB), then writes each message in a few seconds (tested in Chrome). Phones get the pre‑written message from Gemma, checked the same way. The free server can take up to a minute to wake. The 08:00 Telegram message comes from the version that runs on my computer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Today&lt;/strong&gt;: the guests who might not show, riskiest first. A row opens into the booking, the reasons, &lt;strong&gt;Draft&lt;/strong&gt; and &lt;strong&gt;Leave&lt;/strong&gt; (no message, with a few seconds to undo). Edit a draft and get a fact wrong, and the desk flags it as you type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay&lt;/strong&gt;: pick any of 30 real past mornings, see who the desk would have messaged, then press &lt;strong&gt;Reveal what happened&lt;/strong&gt;. On the demo morning, Chloe (74%) really did cancel, and so did 3 of the other 6 flagged guests; Tara, at 26%, was below the 30% line the desk uses to decide who to flag and cancelled anyway. Drag the line or press &lt;strong&gt;Replay all 30 mornings&lt;/strong&gt; to add it up against random picks.&lt;/li&gt;
&lt;/ul&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%2Fsz02imf28acg8abu6f07.jpg" 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%2Fsz02imf28acg8abu6f07.jpg" alt="Replay of the demo morning after Reveal what happened: caught 4 of 5 cancellations, and over 30 mornings 111 of 172 cancellations caught against about 64 for random picks, with one bar per morning" width="800" height="889"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The demo lives on a fixed day in early 2017 because the data is real bookings that end in 2017. Guest names and contacts are made up. Prices are the hotels' own numbers and the data doesn't name the currency (the hotels are Portuguese, so probably euros) plus the page shows them with a $ sign. &lt;strong&gt;Send&lt;/strong&gt; writes to a file and says &lt;em&gt;demo: not delivered&lt;/em&gt;.&lt;/p&gt;

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


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/My-CMDhub" rel="noopener noreferrer"&gt;
        My-CMDhub
      &lt;/a&gt; / &lt;a href="https://github.com/My-CMDhub/no-show-desk" rel="noopener noreferrer"&gt;
        no-show-desk
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Find out which guests won't turn up, days before they don't. A morning no-show desk for small motels: TabPFN flags risky bookings, local Gemma drafts a reconfirm message, code checks every fact.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/My-CMDhub/no-show-desk/docs/logo/no-show-desk-lockup.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FMy-CMDhub%2Fno-show-desk%2FHEAD%2Fdocs%2Flogo%2Fno-show-desk-lockup.png" alt="No-Show Desk" width="520"&gt;&lt;/a&gt;
&lt;p&gt;&lt;strong&gt;Know who won't turn up, before they don't.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A morning assistant for small motels and B&amp;amp;Bs. It flags the guests most likely to cancel or not show,&lt;br&gt;
says why in plain words, and drafts a short "are you still coming?" message for the owner to send.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/My-CMDhub/no-show-desk/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b6e5c025e092b280ca7f23a57285764f18cbbc8947e38c4afdd02ce6626933cf/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d313531393143" alt="License: MIT"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/022fd39895899c79a30c02a5a55775ac66f8d4fb88baaef5ef37ff35f78c12f8/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f707974686f6e2d332e31322d313531393143"&gt;&lt;img src="https://camo.githubusercontent.com/022fd39895899c79a30c02a5a55775ac66f8d4fb88baaef5ef37ff35f78c12f8/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f707974686f6e2d332e31322d313531393143" alt="Python 3.12"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/826219468ea8d1f1528b9dcb1feba77af9fc4b6dbed7ece42b7bea35f9e0323d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7269736b2d54616250464e2d463242373035"&gt;&lt;img src="https://camo.githubusercontent.com/826219468ea8d1f1528b9dcb1feba77af9fc4b6dbed7ece42b7bea35f9e0323d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7269736b2d54616250464e2d463242373035" alt="Risk: TabPFN"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/ab15ac44073eb2dc1e284d0e6792166d07d82ab87dbd9e8afa49d2fde49df8cd/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6472616674732d47656d6d61253230342532304532422532432532306c6f63616c2d463242373035"&gt;&lt;img src="https://camo.githubusercontent.com/ab15ac44073eb2dc1e284d0e6792166d07d82ab87dbd9e8afa49d2fde49df8cd/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6472616674732d47656d6d61253230342532304532422532432532306c6f63616c2d463242373035" alt="Drafts: Gemma 4 E2B, local"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://no-show-desk.onrender.com" rel="nofollow noopener noreferrer"&gt;Try the live demo&lt;/a&gt;&lt;/strong&gt; · your own copy of the demo morning · Gemma writes in your browser on a computer&lt;/p&gt;
&lt;a rel="noopener noreferrer" href="https://github.com/My-CMDhub/no-show-desk/docs/images/desk-desktop.jpg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FMy-CMDhub%2Fno-show-desk%2FHEAD%2Fdocs%2Fimages%2Fdesk-desktop.jpg" alt="The owner page: seven guests might not show, with Chloe's drafted message open and checked" width="760"&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;A small motel loses a night's income every time a guest silently doesn't arrive. Many of those cancellations can be heard about earlier, if someone asks. No-Show Desk picks who to ask each morning, so the owner spends a minute on it, and a room that would have stood empty can be offered to someone else.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How a morning works&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td width="62%"&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;08:00, a Telegram message.&lt;/strong&gt; &lt;em&gt;"7 guests arriving in the next 3 days might not show: $1,599.20 of bookings. Start with Chloe Nguyen&lt;/em&gt;…&lt;/li&gt;
&lt;/ol&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/My-CMDhub/no-show-desk" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;noshow/&lt;/code&gt;: the app. Risk, reasons, facts, drafting, the checker, the agent, the page and the Telegram bot. &lt;code&gt;tests/&lt;/code&gt;: 61 tests for the logic.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scripts/&lt;/code&gt; and &lt;code&gt;results/&lt;/code&gt;: every measured result in this post comes from a script in the first and is written to the second, including every stored draft. The few things I only observed are said as such.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;render.yaml&lt;/code&gt; and &lt;code&gt;results/demo_snapshot.json&lt;/code&gt;: the live demo.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The data, and the columns that give the answer away
&lt;/h3&gt;

&lt;p&gt;There's no public motel data, so I used the &lt;em&gt;Hotel booking demand&lt;/em&gt; dataset (Antonio, Almeida &amp;amp; Nunes, 2019): 119,390 real bookings from two Portuguese hotels, 2015 to 2017. All 1,207 no-shows are marked cancelled, so the desk predicts both. Every test works on a motel-sized slice: about 3,000 past bookings and 20 arrivals a morning.&lt;/p&gt;

&lt;p&gt;Some columns are only filled in &lt;em&gt;after&lt;/em&gt; the guest arrives, so they give the answer away: bookings with a parking space recorded cancelled &lt;strong&gt;0.0%&lt;/strong&gt; of the time because the space is recorded when the car turns up. I dropped six such columns and every test learns only from bookings that arrived &lt;em&gt;before&lt;/em&gt; a date and is scored on bookings after it, the way the desk is used.&lt;/p&gt;

&lt;p&gt;The desk only scores bookings &lt;strong&gt;without a deposit&lt;/strong&gt;: a guest who prepaid and then cancels costs the owner nothing (non-refundable bookings in this data "cancel" 99.4% of the time). All the percentages below are among bookings without a deposit.&lt;/p&gt;

&lt;h3&gt;
  
  
  TabPFN at motel scale
&lt;/h3&gt;

&lt;p&gt;A 20-room motel has a few thousand past bookings at most. TabPFN was trained in advance on a huge number of synthetic tables, so it learns a new one in a single step, with no training run of my own. I gave it, gradient boosting (the usual tool for tables) and a plain logistic regression 300, 1,000 or 3,000 past bookings, three histories each and scored the same 5,000 bookings from 2017. The score is ROC-AUC: how often a guest who cancelled is ranked above one who didn't.&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%2Fmpb0qbqurhjvh3fowqsk.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%2Fmpb0qbqurhjvh3fowqsk.png" alt="Line chart of ROC-AUC by number of past bookings learned from. TabPFN 0.721 at 300, 0.766 at 1,000 and 0.778 at 3,000. Gradient boosting 0.682, 0.702 and 0.747" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;TabPFN ranked best in all 9 runs, with gradient boosting (default settings) furthest behind. The plain regression came much closer than I expected (0.699, 0.741 and 0.749), so the honest summary: TabPFN wins at every size, by 0.02 to 0.03 over the regression.&lt;/p&gt;

&lt;h3&gt;
  
  
  Checking the cut-off on a year it never saw
&lt;/h3&gt;

&lt;p&gt;My first version flagged anyone over 50%. On my first demo morning it flagged 1 guest of 20, who didn't cancel, and missed all 4 who did: with only about 3 in 10 guests cancelling, few scores ever reach 50%. So I picked &lt;strong&gt;30%&lt;/strong&gt; on 30 mornings from 2017 then, because choosing and reporting on the same mornings is peeking, checked it on 30 mornings from &lt;strong&gt;2016&lt;/strong&gt; that the choice never saw. Two measures: cancellations caught, of all that happened, and how many flagged guests really cancelled.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag when&lt;/th&gt;
&lt;th&gt;2017 (picked here): cancellations caught&lt;/th&gt;
&lt;th&gt;flagged guests who did cancel&lt;/th&gt;
&lt;th&gt;2016 (never seen): cancellations caught&lt;/th&gt;
&lt;th&gt;flagged guests who did cancel&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5 riskiest&lt;/td&gt;
&lt;td&gt;84 of 172&lt;/td&gt;
&lt;td&gt;56%&lt;/td&gt;
&lt;td&gt;81 of 134&lt;/td&gt;
&lt;td&gt;54%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25% or more&lt;/td&gt;
&lt;td&gt;129 of 172&lt;/td&gt;
&lt;td&gt;49%&lt;/td&gt;
&lt;td&gt;105 of 134&lt;/td&gt;
&lt;td&gt;52%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;30% or more&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;111 of 172&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;53%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;92 of 134&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;56%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50% or more&lt;/td&gt;
&lt;td&gt;70 of 172&lt;/td&gt;
&lt;td&gt;65%&lt;/td&gt;
&lt;td&gt;66 of 134&lt;/td&gt;
&lt;td&gt;77%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On 2016, which the choice never saw, 30% still caught more than the 5-riskiest rule or the 50% line, and 50% left 2 mornings with nobody flagged. 25% catches more, but fewer of its flagged guests cancel. &lt;strong&gt;Replay&lt;/strong&gt; in the demo recomputes these rows in browser and they match the results files exactly.&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%2Fa5cbn3b7c0txjjq8cs7w.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%2Fa5cbn3b7c0txjjq8cs7w.png" alt="Bar chart. Over 30 test mornings in 2017 the desk's flags found 111 of 172 cancellations. Picking the same number of guests at random would have found 64" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Is it better than a rule I could follow by hand?
&lt;/h3&gt;

&lt;p&gt;All seven guests flagged on the demo day booked through the same travel agent (#9; the data numbers agents), so could an owner just follow a rule? Each rule got the same number of messages per morning as the desk:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Same number of messages each morning&lt;/th&gt;
&lt;th&gt;Cancellations caught, of 172&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TabPFN, 30% line&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;111&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logistic regression, fitted on each morning's history&lt;/td&gt;
&lt;td&gt;106&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Online travel agent first, then longest lead time&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Travel agent #9 first, then longest lead time&lt;/td&gt;
&lt;td&gt;94&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Longest lead time first&lt;/td&gt;
&lt;td&gt;81&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The best rule I could think of finds 99. TabPFN finds 111 by weighing everything together, 5 more than the regression.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reasons a person can read
&lt;/h3&gt;

&lt;p&gt;TabPFN gives a probability, not a reason, so the reasons come from plain counting. A guest's group (lead time, travel agent, channel, meal plan) is named only if it cancelled at least 5 percentage points more often than all bookings without a deposit, over at least 30 of them. The reasons describe the group; the ranking is TabPFN's.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code computes, the model phrases
&lt;/h3&gt;

&lt;p&gt;I gave Gemma the same 50 bookings twice, two drafts each. Once as the raw record: &lt;em&gt;arrival 2017-08-23, 2 weekend nights, 5 week nights, average daily rate 160.2&lt;/em&gt;. Once with every fact worked out by my code: &lt;em&gt;arrive Wednesday 23 August, leave Wednesday 30 August, 7 nights, $1,121.40&lt;/em&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fapj14yuksiaftkgsrhzt.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%2Fapj14yuksiaftkgsrhzt.png" alt="Bar chart of drafts whose date, night count or amount doesn't match the booking, out of 100. Gemma working out dates and totals itself 13. Code handing Gemma the facts 0 through Ollama and 0 in the browser" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From that very record, Gemma wrote to Grace: &lt;em&gt;"your booking is confirmed for August 23 to August 25, 2017. That is 2 nights for a total of $320.40."&lt;/em&gt; It's 7 nights and $1,121.40. Another offered &lt;em&gt;"cancel for free until April 0th."&lt;/em&gt; All of them were fluent and friendly.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;plain-Python checker&lt;/strong&gt; reads every draft's dates, night counts and dollar amounts against the booking. If a draft is wrong, the owner sees exactly what's wrong before deciding. Three times, reading the drafts myself caught something no count showed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;My checker was wrong first.&lt;/strong&gt; Its first count said 25 bad drafts from the raw record against 1 from code's facts. I read all 26 flagged drafts, and 13 were correct drafts in formats it couldn't read yet ("2/13", "Feb 23", a total without "$"). I taught it those formats, test first, and rescored the &lt;em&gt;same&lt;/em&gt; stored drafts: 13 against 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The desk offered dates that had passed.&lt;/strong&gt; The demo motel's policy (my choice; the data has none) ends free cancellation 2 days before arrival, so for almost every guest it messages, that date was gone. Code now leaves a passed date out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My own instruction leaked into a message.&lt;/strong&gt; I had told Gemma to mention the free-cancel date "if you are given one". It wrote &lt;em&gt;"You can cancel for free until [Insert Date Here]"&lt;/em&gt; once in 6 test drafts. Every number was right, so the checker passed it. The fix was the same principle: code decides whether that sentence exists at all and the checker now catches placeholders.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  An agent that waits for a person
&lt;/h3&gt;

&lt;p&gt;Each guest is one run of a small LangGraph graph, a library for agents that can stop and wait for a human. It &lt;strong&gt;pauses twice for the owner&lt;/strong&gt;: Draft or Leave, then Send or Reject once the draft is checked. Leave and Reject are stored as different outcomes, because "no message needed" and "this draft was wrong" mean different things.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemma inside the browser
&lt;/h3&gt;

&lt;p&gt;On my 8 GB Mac, Gemma through Ollama takes about 30 seconds a draft. Google also publishes a 2 GB build of the same small Gemma 4 that runs inside a web page, on the computer's graphics chip (through WebGPU). So, I gave it the same 100 drafts and the same checker.&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%2Fm09k01ejoz033rkua3m8.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%2Fm09k01ejoz033rkua3m8.png" alt="Bar chart of median seconds per message. 3.25 in the browser on WebGPU against 31.4 through Ollama on the same 8 GB Mac" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;None of its 100 drafts had a wrong fact, at a median of 3.25 seconds against 31.4 (the two builds aren't identical files). Reading all found something the checker isn't built for: a few drafts &lt;em&gt;leave a fact out&lt;/em&gt;, like the night count on a one-night stay, 6 in the browser and 5 through Ollama. None stated anything false.&lt;/p&gt;

&lt;p&gt;So the page drafts in the browser and it asks before the 2 GB download, says what the file is and where it comes from, and shows real progress. In a browser on my Mac the download took about 3 minutes and loading about 10 seconds. Real devices changed the page twice. My &lt;strong&gt;iPhone 15 Pro&lt;/strong&gt; downloaded the whole model, then Safari reloaded the page, most likely out of memory, so phones now get the recorded message instead. On my &lt;strong&gt;8 GB Mac&lt;/strong&gt; the browser froze for a while just after Gemma loaded but only on one test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The message stays with the motel.&lt;/strong&gt; A no-show message holds a guest's name, dates and money. Gemma writes it on the owner's own computer, so none of that leaves your disk to be phrased.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There's no bill per message.&lt;/strong&gt; The desk flagged about 7 guests a morning on the test days; with a paid writing API, that cost grows every morning. Here the model is a file on the owner's disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small businesses have small data.&lt;/strong&gt; TabPFN learns from a few thousand rows with no training run and no data scientist, which is the size of a real motel's history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I could read and change every step that writes or checks a message.&lt;/strong&gt; When Gemma leaked "[Insert Date Here]", I could see the exact instruction, change one line, and rerun 100 drafts.&lt;/p&gt;

&lt;p&gt;One step isn't local yet: TabPFN scoring runs through the API of Prior Labs, so booking details (never names; this data has none) leave the computer for that one call each morning. TabPFN's v2 weights can run on computer instead and &lt;a href="https://huggingface.co/Prior-Labs/TabPFN-v2-clf" rel="noopener noreferrer"&gt;their licence&lt;/a&gt; allows commercial use with attribution. I checked: on my 8 GB Mac, at about 23 seconds a morning, v2 caught the same 111 of 172 on the 30 test mornings, flagging 205 guests instead of 210 (ROC-AUC 0.787 against the API's 0.792). That's the path for a paying version.&lt;/p&gt;

&lt;h2&gt;
  
  
  My First User
&lt;/h2&gt;

&lt;p&gt;Not yet. I told a motel owner about the desk: someone I'd earlier pitched a voice agent to. He said he'd use it if he had more cancellations, and that it's handier, saves money and is easier to set up than the voice agent I'd pitched him. He doesn't have many cancellations right now, so he hasn't tried it and I can't tell you what an owner would pay. A second owner, introduced through a friend, is next. The first question I'll ask is whether a reply ever arrived early enough to resell the room.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TabPFN.&lt;/strong&gt; The risk step inside an agent. TabPFN scores each morning's bookings from only earlier bookings and a LangGraph run per guest acts on the score, pausing for the owner. It was tested the way it's used: on 30 later mornings it caught 111 of 172 cancellations, against about 64 at random and the 30% line held on a 2016 year it was never chosen on (92 of 134). It beat gradient boosting at every history size, a plain regression by 0.02 to 0.03 ROC-AUC and the best hand rule by 12 cancellations. Run on an 8 GB Mac with TabPFN's v2 weights, it caught the same 111, in about 23 seconds a morning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma.&lt;/strong&gt; Gemma 4 E2B, the smallest of Gemma 4's five sizes, writes every message, in the visitor's browser or through Ollama on the owner's Mac. On the same bookings, wrong dates or totals fell from 13 to 0 of 100 when code handed Gemma the facts. The browser build drafted the same 100 in a median 3.25 seconds against 31.4 through Ollama on the same Mac, with no wrong fact. A checker reads every draft and the owner's own edits before send.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render.&lt;/strong&gt; Render runs the agent's front end from a &lt;code&gt;render.yaml&lt;/code&gt; blueprint. Every visitor gets their own desk, where each guest is an agent run on the server that waits for Draft or Leave, then Send or Reject. The TabPFN scores were worked out earlier on my Mac and saved, and a test fails if the saved morning drifts from what the desk works out live.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's real, and what isn't
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real:&lt;/strong&gt; the bookings, TabPFN's scores, the reasons, Gemma's drafts and every check. The demo day is the earliest of the 30 test mornings that flagged 7 guests, the average.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not measured:&lt;/strong&gt; whether a message gets a guest to reply or a room resold. That's the part only a real motel can test and it's what I'd build next: the desk running every morning on a live booking system, with the replies counted.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Changing what my phone agent was shown beat changing the model</title>
      <dc:creator>Dhruv</dc:creator>
      <pubDate>Wed, 07 Oct 2026 00:19:31 +0000</pubDate>
      <link>https://dev.to/mycmdhub/changing-what-my-phone-agent-was-shown-beat-changing-the-model-94a</link>
      <guid>https://dev.to/mycmdhub/changing-what-my-phone-agent-was-shown-beat-changing-the-model-94a</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/kaggle-2026-09-23"&gt;Kaggle Benchmarking Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;On 18 September 2026, two turns after its own lookup had returned a booking's real email, my AI phone receptionist told the caller no email was on file. Then it read one back anyway: "Dhruv Patel at gmail dot com". That's my name, at an address that exists nowhere.&lt;/p&gt;

&lt;p&gt;I didn't change the model. I changed its notes, the facts my code hands it on every turn of the call. Across six models on Kaggle, invented emails went from 17 of 216 runs to none.&lt;/p&gt;

&lt;p&gt;The same move cut two more mistakes from real calls, and a fourth I found by probing. On one of them, the weakest model after the fix (14 of 18 runs right) beat the strongest model before it (4 of 18).&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%2Ftstthl6x99t0zxatxyhz.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%2Ftstthl6x99t0zxatxyhz.png" alt="runs where my agent did the wrong thing, before vs after each fix, all six models pooled. Pushed the caller toward reception: 97 of 108 before, 5 of 108 after. Said the guest's name to a caller who hadn't said who they were: 14 of 216 before, 0 of 216 after. Used an email address that isn't on the booking: 17 of 216 before, 0 of 215 after." width="800" height="570"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The three mistakes from real calls. The fourth, found by probing, is in Finding 4. A "run" is one model answering one scripted call once.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The pattern, in three rules:&lt;/strong&gt; withhold what must never be said, carry what will otherwise be guessed, compute what it can't work out.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My agent answers the phone for a motel. It finds bookings, resends confirmations and puts callers through to reception. Its worst mistakes never crash anything. They sound polite and fluent, and they are wrong. So my itch was simple: when it gets something wrong, is that the model, or what I showed the model?&lt;/p&gt;

&lt;p&gt;Two words I'll use throughout. A &lt;strong&gt;tool&lt;/strong&gt; is a function the model can call, like "look up a booking"; whatever the tool returns, the model reads. &lt;strong&gt;Notes&lt;/strong&gt; are facts my agent's code adds to its instructions during a call, such as the booking it has already found.&lt;/p&gt;

&lt;p&gt;How I built it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A copy of my agent's real instructions (about 7,000 tokens) and its 12 tools, set in a fictional motel.&lt;/li&gt;
&lt;li&gt;Each mistake became a pair of tasks: the same calls, the same models, one change, before and after the fix.&lt;/li&gt;
&lt;li&gt;The tools give fixed, scripted replies, so the only thing that varies is the model's choices.&lt;/li&gt;
&lt;li&gt;Control tasks where the caller asks directly ("put me through", "bye", "here's my new email"), to check a fix didn't just make the agent passive.&lt;/li&gt;
&lt;li&gt;Scoring in plain Python, no AI judge. Every call runs three times per model, and I read every reply the scorer flagged.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fixes for the three from real calls are public (&lt;a href="https://github.com/My-CMDhub/Ovela-AI/commit/7190cad" rel="noopener noreferrer"&gt;here's the email one&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Models Tested
&lt;/h2&gt;

&lt;p&gt;Six models from Kaggle's hosted list: GPT-5.6 Luna, GPT-5.4 mini, Gemini 3.7 Flash, Gemma 4 31B, Claude Haiku 4.5 and Claude Sonnet 5.&lt;/p&gt;

&lt;p&gt;Why these: three model makers (OpenAI, Google, Anthropic), including Google's open-weights Gemma, and a mix of small, fast models and larger ones. A phone call can't wait long for an answer, so fast models matter here. These runs are not my agent itself. They answer a different question: does the design change work whichever model reads it?&lt;/p&gt;

&lt;p&gt;Gemma ran with its provider's default reasoning, because it rejects "off". The other five ran with reasoning off.&lt;/p&gt;

&lt;p&gt;This is not a model ranking. Each model is compared with itself, before and after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Findings
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. A tool's reply is a prompt
&lt;/h3&gt;

&lt;p&gt;The caller's booking can't be found, for the third time. My agent's lookup tool used to answer: "I couldn't find a booking linked to your number or the details I have here - want me to put you through to reception?"&lt;/p&gt;

&lt;p&gt;The models took the hint. In 97 of 108 runs the agent pushed the caller toward reception instead of asking them to spell their name. A typical reply, from Gemma 4 31B:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I'm still not seeing it. Want me to put you through to reception?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then I deleted only the question. That dropped it to 17 of 108. Today's wording (spell the surname, ask for the booking reference, "Do not escalate to a human yet") brought it to 5 of 108.&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%2Flxt9jcpg0hw2kuxhwae8.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%2Flxt9jcpg0hw2kuxhwae8.png" alt="what the lookup tool said after the third miss, and how often the agent then pushed the caller toward reception. Old reply ending " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Model choice mattered far less than that one tool reply. Before the fix, no model got more than 4 of 18 runs right. After it, the worst got 14 of 18.&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%2Fxgrd7ad0l288de4k9nvd.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%2Fxgrd7ad0l288de4k9nvd.png" alt="" width="800" height="620"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Prompt-injection benchmarks such as &lt;a href="https://arxiv.org/abs/2406.13352" rel="noopener noreferrer"&gt;AgentDojo&lt;/a&gt; plant hostile text in tool results. Nothing here was hostile: my own tool gave honest, well-meant advice, and it still steered every model.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A rule next to a secret isn't a lock
&lt;/h3&gt;

&lt;p&gt;The caller has only said hello. Their number matches a booking, so before the fix the agent's notes held the guest's name, with the rule "Do not say the name, the reference, or any of these details until the caller has told you who they are…"&lt;/p&gt;

&lt;p&gt;Five models kept the secret. Claude Haiku 4.5 said it in 14 of 36 runs, spread over 9 of the 12 calls:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I have a booking here for Linh Tran, checking in 30 October, checking out 1 November—is that you?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The fix keeps the name out of the notes in code; the lookup returns it only once the caller gives it. After that: 0 of 216 runs said a name or booking reference.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Leaving a fact out makes models invent it
&lt;/h3&gt;

&lt;p&gt;This is the mistake from the opening. The caller has said who they are and their booking was found earlier. They ask which email it's under. Before the fix, the notes carried the booking but not its email, on the theory that the agent could look it up again.&lt;/p&gt;

&lt;p&gt;Some didn't look. 17 of 216 runs said or used an address that isn't on the booking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I have &lt;code&gt;priya.nair@gmail.com&lt;/code&gt; on file — is that correct?" (Claude Haiku 4.5)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The booking says &lt;code&gt;priya.nair@example.com&lt;/code&gt;. Fifteen of the 17 invented addresses were the guest's own name at gmail.com, the same shape as "Dhruv Patel at gmail dot com". With the email carried in the notes: 0 of 215 (one run was lost to a provider error).&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Having every input isn't working it out
&lt;/h3&gt;

&lt;p&gt;This one came from probing my agent, not from a real call. A caller asks to move a paid booking whose stay has already ended. Today's date is in the agent's instructions; the stay dates are on the booking.&lt;/p&gt;

&lt;p&gt;In two separate benchmark passes, 0 of 108 replies said the stay was over. Most sent the caller to reception because the booking was paid, which is safe but for the wrong reason:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Your payment's already processed. I'll need to put you through to reception to modify the booking. Shall I connect you?" (GPT-5.6 Luna)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ten replies in the second pass (fifteen in the first), all Claude Haiku 4.5, offered to move a stay that had already happened.&lt;/p&gt;

&lt;p&gt;My proposed fix (my agent doesn't do this yet) has code work out two facts and place them next to the booking: "this stay ended on Sunday 4 October 2026. It is over and can't be moved or changed", plus the payment rule. Offers to move a finished stay went from 10–15 to 0, and 50 of 108 replies now said the stay was over. Gemini 3.7 Flash did it 18 of 18 times; Haiku, never. The unpaid control stayed at 108 of 108, so the facts didn't make agents refuse changes they're allowed to make.&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%2Fwelk8p1xe039m590513i.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%2Fwelk8p1xe039m590513i.png" alt="caller asks to move a paid booking whose stay has already ended, 108 replies per design. Today's design, rule in the instructions: 10 offered to move it, 98 sent to reception without saying it was over. Proposed fix, two facts worked out in code: 58 sent to reception without saying it was over, 50 said the stay was over, 0 offered to move it." width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Would a model swap have done the same? For the name leak and the offers to move a finished stay, yes: only Haiku made those. For invented emails, mostly: two models never made one. For the push toward reception, no: every model did it in at least 14 of 18 runs. Changing what the model was shown cut the harmful outcome of all four, on every model that made it.&lt;/p&gt;

&lt;p&gt;Across all of this, the fixes didn't make agents passive. On the controls (put me through, goodbye, here's my name, here's my new email): 144 of 144 runs right before the fixes, 143 of 144 after, and every "put me through" call was put through both times.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I got wrong
&lt;/h3&gt;

&lt;p&gt;Before I saw the first pass's results, I predicted at least one small model would dial a transfer unprompted in one run in five. No model did, in any of the 324 booking-not-found runs. They pushed in words and waited. So I switched to scoring what they said, any push toward reception, and every result in this post uses that rule.&lt;/p&gt;

&lt;p&gt;I predicted inventing an email would get worse when the question came five exchanges after the lookup. It didn't: 11 inventions came straight after the lookup, 6 later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limits
&lt;/h3&gt;

&lt;p&gt;Six models, three repeats per call, provider-default temperature, so counts shift by a few runs between passes; the direction never did. I changed scorers after reading replies: the transfer score after the first pass, and the move-a-booking scorer twice. The proposed move-a-booking fix ran in one pass only. Kaggle's own score for the "Move a booking" proposed-fix task sits slightly below my final scorer's count, because its built-in version missed six replies that said the stay was over in other words.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it changed
&lt;/h3&gt;

&lt;p&gt;I used to treat a wrong answer as a model problem and reach for a bigger model. Now I check what the model was shown, and three rules come first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Withhold what must never be said.&lt;/strong&gt; Keep it out of the context in code; a written rule is a strong default, not a lock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Carry what will otherwise be guessed.&lt;/strong&gt; A missing fact doesn't get fetched. It gets made up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute what it can't work out.&lt;/strong&gt; Today's date plus a stay's dates isn't enough. "This stay is over" is.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What I'd measure next
&lt;/h3&gt;

&lt;p&gt;Ship the stay-timing fix in my agent and re-run it, then aim at the 58 replies that still answered by the payment rule with "this stay ended" two lines away. I'd also audit all 12 tool replies for sentences that steer, and run these pairs through real speech-recognition errors on longer calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Benchmark
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.kaggle.com/benchmarks/dpquote/ovela-phone-agent-before-and-after-the-fix" rel="noopener noreferrer"&gt;Ovela Phone Agent: Before and After the Fix&lt;/a&gt;&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3cs2z714es3js94555gy.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%2F3cs2z714es3js94555gy.png" alt="The benchmark leaderboard on Kaggle: task rows by six model columns; each cell is the share of runs that went right. Showing the first 8 of 11 rows" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Read it in pairs. Each cell is the share of runs where the agent did the right thing; 22.2% means 4 of 18 runs. The number under each model's name averages every task, before and after together, so it isn't a ranking. On "Move a booking" the score also counts the upcoming and unpaid calls, so 67% for today's design (four models; Gemma 65%, Haiku 39%) is the ceiling for an agent that never says a finished stay is over: every finished-stay reply scored a miss.&lt;/p&gt;

&lt;p&gt;The pairs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Booking not found: &lt;a href="https://www.kaggle.com/benchmarks/tasks/dpquote/booking-not-found-old-tool-wording" rel="noopener noreferrer"&gt;before&lt;/a&gt;, &lt;a href="https://www.kaggle.com/benchmarks/tasks/dpquote/booking-not-found-question-removed" rel="noopener noreferrer"&gt;question removed&lt;/a&gt;, &lt;a href="https://www.kaggle.com/benchmarks/tasks/dpquote/booking-not-found-current-tool-wording" rel="noopener noreferrer"&gt;after&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Unknown caller: &lt;a href="https://www.kaggle.com/benchmarks/tasks/dpquote/unknown-caller-name-visible-to-agent" rel="noopener noreferrer"&gt;before&lt;/a&gt;, &lt;a href="https://www.kaggle.com/benchmarks/tasks/dpquote/unknown-caller-name-withheld-in-code" rel="noopener noreferrer"&gt;after&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Email question: &lt;a href="https://www.kaggle.com/benchmarks/tasks/dpquote/email-question-email-missing-from-notes" rel="noopener noreferrer"&gt;before&lt;/a&gt;, &lt;a href="https://www.kaggle.com/benchmarks/tasks/dpquote/email-question-email-in-notes" rel="noopener noreferrer"&gt;after&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Controls: &lt;a href="https://www.kaggle.com/benchmarks/tasks/dpquote/controls-caller-asks-directly" rel="noopener noreferrer"&gt;before&lt;/a&gt;, &lt;a href="https://www.kaggle.com/benchmarks/tasks/dpquote/controls-caller-asks-current-design" rel="noopener noreferrer"&gt;after&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Move a booking: &lt;a href="https://www.kaggle.com/benchmarks/tasks/dpquote/probe-move-a-paid-booking" rel="noopener noreferrer"&gt;today's design&lt;/a&gt;, &lt;a href="https://www.kaggle.com/benchmarks/tasks/dpquote/probe-move-a-booking-facts-in-notes" rel="noopener noreferrer"&gt;proposed fix&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>kagglechallenge</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
    <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 real phone line. 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;

&lt;p&gt;*&lt;em&gt;Small update: several numbers here have since been re-measured on the server that actually answers the calls&lt;br&gt;
*&lt;/em&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>
