Back to all posts

Inside the engine behind AIVA's two-way SMS AI agent

Voice and chat live inside a session. SMS doesn't. Here's how our two-way SMS engine picks up exactly where it left off, hours later, with nothing to hold onto.

NI
Nisha Iyer
Engineering

Every other channel AIVA runs on has a session. A phone call is a continuous connection — you hang up, it's over. A web widget conversation lives inside a browser tab. SMS has neither. A customer can text a question, put their phone away, and reply six hours later from a different location, on a different network, having completely forgotten they were mid-conversation. AIVA has to remember for them.

That's the actual engineering problem behind SMS. Not natural-language understanding — we already had that from voice and chat. The problem is state: how do you hold a conversation with no connection to hold it on.

Why SMS breaks the session model

Voice and web chat are both built around the idea of a live session — a socket or a call leg that stays open while the conversation happens. The conversation engine can keep working memory in that session for as long as it's alive, and when it closes, the conversation is done.

SMS, running on Twilio underneath — more on that integration here — doesn't give us that. Every message is a separate webhook. There's no connection between the text a customer sends at 2:14 PM and the reply they send at 8:30 PM — as far as the transport layer is concerned, they could be two different conversations from two different people. Treating them that way is how you end up with a bot that asks "sorry, what appointment are you referring to?" six hours into a conversation the customer thought was already halfway done.

Treating every message as a resume, not a start

We stopped thinking about SMS as a live exchange and started treating every inbound message as a request to resume a thread. Each phone number, per business, maps to a persistent conversation record — not a session with a timeout, a durable object that outlives the gap between messages, however long that gap is.

When a message comes in, the engine doesn't start from nothing. It loads the full thread: what was asked, what was answered, what booking or request is in progress, and what state that request is in. Only then does it generate a reply. A customer who asked "do you have anything tomorrow morning?" at lunchtime and comes back after dinner with "actually let's do afternoon instead" gets a reply that knows exactly what "instead" refers to, without re-asking anything they already told us.

The 6-hour gap in the title isn't a limit — it's just a realistic example. We've seen threads pick back up correctly after multi-day gaps. The design doesn't put a clock on context. If the thread exists, the context exists.

What actually lives in the thread record

"Remembers the conversation" undersells what has to be stored and kept current. The thread record isn't just a transcript — a raw log of every message sent and received would tell you what was said but not what to do with it. Alongside the message history, the record carries a working state: what the customer is trying to accomplish (book, reschedule, cancel, ask a question), which fields of that request are already filled in and which are still open, and whether the last thing AIVA said was a question still waiting on an answer.

That last piece matters more than it sounds like it should. If AIVA asked "what time works for you?" and the next inbound message is just "3pm," the engine needs to know that's an answer to a pending question, not a new, context-free request that happens to contain a time. Without tracking what's still open, a two-character reply like "yes" or a bare number is nearly meaningless on its own — it only means something in light of what was asked immediately before it, however long ago that was.

The edge cases that actually show up

A design that works cleanly in the happy path — customer asks, AIVA answers, customer replies later, AIVA continues — still has to hold up against the messier ways people actually text a business.

A new topic lands inside an old thread. A customer who booked an appointment last week might text again today about something unrelated — a different question, not a follow-up. The engine still loads the existing thread, but it has to recognize that the new message doesn't reference the old request and treat it as its own thing, rather than forcing it to connect to whatever was last discussed. Getting this wrong in one direction means bolting an unrelated question onto a finished booking; getting it wrong in the other means failing to notice when a customer genuinely is following up on something from days earlier.

Messages that arrive out of the order they were sent. Carrier delivery isn't instantaneous or perfectly ordered, and a webhook can occasionally arrive later than you'd expect from when the customer actually sent it. Keying every message to the same persistent thread, rather than trusting arrival order to reflect conversation order, is what keeps a delayed message from getting treated as a reply to the wrong question.

A thread that's already resolved gets reopened. Once a booking is confirmed, the thread doesn't get deleted or archived out of reach — a customer texting three days later to change that same booking needs the engine to recognize the request is about something already on the books, not start a fresh, duplicate booking conversation next to it.

The cost of remembering everything

None of this is free. Loading a full thread on every inbound message is more work than treating each text as a stateless request, and the honest trade-off is between context and overhead — the richer the history AIVA carries into every reply, the more there is to process before it can respond. For most conversations this isn't a meaningful cost; a booking thread is a handful of exchanges, not hundreds. But it's part of why the record tracks structured state — what's booked, what's pending, what's still open — rather than leaning on the raw message log alone to reconstruct context every single time. A well-kept summary of "what's actually going on" is cheaper to work from than re-reading an entire conversation from scratch on every reply, and it's also more reliable once a thread runs long.

Two-way, not two separate flows

The other piece is that SMS conversations aren't one-directional. A business-initiated message — a confirmation or a reminder — lands in the same thread as anything the customer asks on their own. That matters because customers don't treat them as separate: they reply to a reminder text the same way they'd reply to any other message, often with a question or a change ("can we push this to 5?") rather than a simple yes or no. The engine has to handle that reply as a continuation of the same conversation, not a dead-end notification that happened to get a response. It's the same logic that lets a customer text the shorthand commands — STATUS, RESCHEDULE, REFUND — and get an instant, correct answer regardless of how long the thread has been sitting quiet.

This is also why SMS works on phones a chat widget never reaches — feature phones, spotty data connections, customers who never open a browser tab for a business they're not actively shopping with. No app, no login, no persistent connection required. Just a text thread that's still there whenever the customer comes back to it. We've written more about why that reach matters on its own.

What this shares with the rest of AIVA

The thread-loading approach is SMS-specific, but the underlying understanding — what was asked, what's booked, what's still open — comes from the same conversation engine that runs voice and web chat. A booking made on a call shows up correctly if the customer later texts asking about it, because there's one system underneath every channel, not three keeping separate notes.

For SMS specifically, the lesson was narrower: the hard part was never getting the model to understand a text message. It was building a system willing to wait, indefinitely, for the other half of a conversation — and pick it up exactly where it left off when it finally comes. If you want to see it on your own number, SMS runs on the same pay-as-you-go setup as the rest of AIVA, and it's worth trying with a real gap in the middle.

Share
NI
Written by
Nisha Iyer
Engineering

FAQ

Common questions.

Each phone number, per business, is tied to a persistent conversation thread rather than a session with a timeout. When a new message comes in, AIVA loads that full thread — what was asked, what's booked, what's still open — before generating a reply.

No fixed cutoff. We've seen threads pick back up correctly after multi-day gaps, not just a few hours. The design doesn't put a clock on context — if the thread exists, the context exists.

AIVA still loads the existing thread but recognizes the new message doesn't reference the earlier request, so it's handled as its own topic instead of being forced onto whatever was last discussed.

No. Voice, SMS, and the web widget all run on the same underlying conversation engine — SMS just adds a layer for persisting and resuming state across the gaps between messages.

That reply lands in the same thread as everything else. A customer texting 'can we push this to 5?' after a reminder is treated as a continuation of the booking conversation, not a disconnected message.

Yes. Because SMS, voice, and web chat share one conversation engine, a booking made on a call shows up correctly if the same customer texts about it later, and vice versa.

Twilio handles message delivery to and from carrier networks. AIVA's own engine handles everything about the conversation itself — understanding the message, tracking thread state, and deciding what to reply.

Every message is keyed to the same persistent thread rather than relied on to arrive in perfect order, which is what keeps an occasionally delayed message from being read as a reply to the wrong question.

Like this? Get more.

One email a month. Engineering deep-dives, product launches, customer stories. No fluff.

4,200+ subscribers. Unsubscribe anytime.