A guest messages on arrival day. Over twenty messages we sort out a late check-in, a parking spot, and a broken coffee machine. Then they ask one more thing — and the bot answers a question from message three as if it were still live, re-confirms the parking they'd already cancelled, and ignores the coffee machine entirely.
To the guest, it looked like nobody was listening. In a way, nobody was.
I saw that failure over and over while running short-term rentals, and it taught me something that shaped everything we built afterward: most AI support bots get dumber the longer a conversation runs. Not because the model is weak — because of how the conversation is fed to it.
This is the architecture we built to fix that. It now runs roughly 3,300 support cases for one operator — about two-thirds messaging, one-third voice. No model rewriting its own weights, no autonomous agent loose in production. Just a few small systems that feed each other, with a human in the loop where it counts.
~3,300 support cases · one operator · ~90 days
Roughly 2 in 3 messaging, 1 in 3 voice
Why the obvious approach breaks
The default way to build an AI support bot is to paste the entire conversation transcript into the prompt on every new message.
Five messages in, fine. Thirty messages in — my coffee-machine guest — it falls apart. The bot re-reads its own old replies and treats them as gospel. It repeats answers. It misses the newest, most important thing the guest just said.
The instinct is to reach for a bigger context window. It feels like “more memory.” It isn't — it just makes it cheaper to keep doing the wrong thing. The real gap is that there's nowhere to put working memory: a clean, current understanding of the conversation, separate from the raw log of everything ever said.
So we built one.
1Memory: a structured snapshot, refreshed every turn
After every bot reply, a cheap, fast model compresses the recent thread into a small structured snapshot. Four fields — not freeform text:
guest_intent— what they actually wantestablished_facts— what's already settledopen_questions— what's still unresolvedsummary_text— three to five sentences, plain language
For the coffee-machine guest, that snapshot reads:
intent: settle in for the stay
facts: checked in late (ok); parking cancelled; coffee machine broken
open: replacement or refund for the coffee machine?
summary: Guest arrived late and is settled. Parking no longer
needed. Coffee machine is broken; they want it resolved.Next turn, the bot reads that plus the last couple of raw messages — not forty. It can't re-confirm the cancelled parking, because the snapshot says it's cancelled. The newest open question sits at the top.
The per-turn memory loop
Guest sends a message
Bot reads snapshot + last ~6 messages
not the whole transcript
Reply sent to guest
Refresh the 4-field snapshot
intent · facts · open questions · summary
↻ next turn reads the updated snapshot
Two decisions made it safe on real guests:
Fire-and-forget. The refresh runs after the reply is already in the guest's inbox. The guest never waits for the summarizer. Added latency: zero.
Graceful degradation. No snapshot yet? Use the full transcript. Summarizer fails or returns garbage? Keep the previous snapshot. The memory layer can improve a reply — it can never break one.
2Escalation: when unsure, it stops
The worst early moments weren't wrong facts. They were confident wrong answers to questions the bot had no business touching — a deposit dispute, a refund, an angry guest. A human would have paused. The bot just answered.
So now, when the routing bot can't safely handle a message, it doesn't guess. It opens a support case and goes silent while a human takes over. A guard on every incoming message keeps the bot out of the way while a human is handling it — no bot and operator talking over each other.
This is the workhorse. Those ~3,300 cases are this mechanism, routing work to the right place across messaging and voice.
And the memory pays off twice here: that same snapshot becomes the operator's brief. Whoever picks up the coffee-machine case reads three sentences instead of scrolling forty messages. We built the memory for the bot; it turned out to be just as useful for the human handoff.
Escalation & flagging
Routing bot evaluates a message
Open a support case
bot goes silent while a human handles it
Operator picks it up
reads 3 sentences, not 40 messages
One-click flag (+ optional note)
the training signal — a human says 'this was wrong'
3Flagging: humans mark the misses, one click
People expect this part to be clever. It isn't, on purpose.
The bot sends a guest the wrong wifi password — unit 4B's code to a guest in 4A. An operator spots it and flags that reply: one click, optional note (“wrong wifi — that's 4B's”). That's the whole mechanism. No sentiment classifier, no intent pipeline, no labelling team.
This is where most “self-improving” claims overreach, so I'll be exact: we don't score sentiment or intent per turn. The signal is a human saying “that one was wrong.” Cheaper and more trustworthy than a model grading itself. Operators already know a bad reply when they see one — we just gave them a button.
4The loop: built, and deliberately on a leash
Here's the part the title implies, and here's exactly how far it actually goes — because this is where I see the most hand-waving in the industry.
The mechanism is wired end-to-end: a scheduled job pulls recent conversations, surfaces the flagged ones, and hands them to an LLM with the bot's current instructions. It proposes a rewrite. The wifi mistake doesn't just get fixed in one chat — it becomes a proposed instruction change: match the wifi code to the exact unit before sending it.
But the proposal does not ship itself. It lands in an admin UI as a diff, flagged examples highlighted. A human approves it or bins it. Only on approval does it go live.
I'll be straight about maturity: the escalation backbone is mature — thousands of cases, every day. The memory layer and the flagging signal are newer, in early real use. The loop is built and deliberately gated — and I'm in no rush to take the leash off. I am not going to let a prompt rewrite itself unsupervised and then explain to a guest why it went sideways. The human gate isn't a temporary limitation. It's the design.
The improvement loop — built, and gated
Scheduled job pulls recent conversations
flagged ones surfaced first
LLM proposes a rewrite
specific changes, with rationale
Lands as a diff in the admin UI
flagged examples highlighted
Approved version goes live
on the very next message
What I'd tell another builder
Skip the generic lessons. Three specific things earned their keep:
- A structured snapshot beats a bigger window. Four fixed fields, refreshed each turn, read instead of the transcript. That single move fixed the long-thread problem outright.
- Do the expensive work after the reply ships. Summarization, logging, analysis — none of it should sit between the guest and an answer.
- Gate the self-improvement with a human, and don't apologize for it. The credibility of an “improving” system comes from the fact that nothing changes without someone signing the diff.
The takeaway
“Self-improving AI” sounds like a black box you have to trust.
What we built is the opposite: structured memory, an escalation that knows when to stop, and an improvement loop a human keeps on a leash. It came straight out of being the operator answering the messages — and being embarrassed enough, often enough, to fix it properly.
The unglamorous version is the one you can actually put in front of customers.