Tacnode™
Back to Blog
AI Engineering

One Customer, Multiple AI Agents: Voice, Chat and App at Once

One customer, multiple AI agents: when voice, chat and app agents act on one account at once, refunds double up and offers collide. How to coordinate them.

Boyd Stowe
Boyd Stowe
Solutions Engineering
9 min read
One customer account in the center with arrows from a voice agent, a chat agent and an app agent converging on it, and two refund arrows colliding at the account.

TL;DR: One customer, multiple AI agents: when a customer reaches you on voice, chat and in-app at the same time, three agents can act on the same account inside the same few minutes. Shared conversation history doesn’t stop them from each issuing a refund or each making a retention offer, because history records what was said, not what was committed. Give every channel one commitments record per account, make each remedy a conditional write against it, and have every agent read it before it speaks.

A customer’s order is three days late. They open the in-app chat and complain. The chat agent apologizes and starts looking into it. Two minutes later, still annoyed, they call. Your voice agent picks up, sees a late order and a frustrated customer, and issues a refund. While it’s talking, the chat agent finishes its own look and issues a refund too.

Both agents followed policy. The policy says one refund per incident. The customer got two.

This is a different problem from the one most omnichannel work solves. Teams put a lot of effort into making the customer’s context follow them across channels, so the voice agent knows what was said in chat. That’s conversation memory. What broke here is that two agents committed actions against one account at the same time, and neither saw the other’s commitment before making its own.

What Happens When Multiple AI Agents Serve One Customer?

When multiple AI agents serve one customer at once, such as a voice agent, a chat agent and an in-app agent, each can read the same account state and act on it inside the same window. Without coordination, they duplicate refunds, stack retention offers or act on details another channel just changed. Each agent is correct alone; together they break the account’s rules.

The key word is “at once.” A customer who chats on Monday and calls on Thursday is a handoff. The voice agent needs the chat history, and memory solves that. A customer who is in two channels in the same few minutes is contention: two writers, one account, one validity window. That needs coordination.

Shared Context Isn’t Shared Commitments

Omnichannel agent platforms address the memory part. ElevenLabs, for example, describes running one agent’s logic across voice and text: “On text-only channels, the speech stages simply switch off and the same logic layer answers in writing.” On shared context, the same post says: “A conversation that a customer begins on WhatsApp can transition to the in-app chat without starting from scratch.”

That addresses the handoff case. It doesn’t cover two live channels issuing remedies in parallel. A transcript tells the voice agent that the chat agent said “let me look into a refund.” It doesn’t tell it whether the refund was issued, is about to be, or was decided against. Agents need a record of what has been committed, and they need to check it at the moment they commit, not when they loaded the conversation.

Three Collisions, One Account

The scenario below is illustrative, with constructed timings.

text
t = 0:00   Customer opens in-app chat: order #8812 is three days late
t = 0:40   Chat agent reads order #8812: late, no remedy issued
t = 2:05   Customer calls; voice agent reads order #8812: late, no remedy issued
t = 2:50   Customer, in the app, changes delivery address
t = 3:10   Voice agent issues refund for #8812 and schedules redelivery
           to the address it read at 2:05
t = 3:25   Chat agent issues refund for #8812
t = 4:00   Customer says "I want to cancel" in both channels;
           voice offers three months at half price, chat offers a free month

Refund issued twice. Both agents read “no remedy issued” before either wrote one. This is the check-then-act shape from Race Conditions Between AI Agents, applied to a remedy instead of a cap.

Two retention offers. Each agent made the offer its playbook allows. The customer now has two in writing and will reasonably expect both.

Address changed mid-call. The voice agent scheduled redelivery against an address it read before the customer changed it in the app. Nothing collided at the database level; the redelivery just went to the wrong place.

Two timelines, one for the chat agent and one for the voice agent. Each agent reads that no remedy has been issued, and both reads happen before either agent issues its refund, so the account receives two refunds.

The Payment Processor Won’t Catch It

It’s tempting to assume the payment system will reject the second refund. Usually it won’t. Stripe’s documentation is explicit: “You can issue more than one refund against a charge, but you can’t refund a total greater than the original charge amount” (Stripe Docs).

That’s the processor enforcing its own invariant: refunds can’t exceed the charge. Your invariant, one goodwill remedy per incident, is a business rule the processor knows nothing about. Two partial refunds that together stay under the charge amount both succeed. The same is true of your CRM, your offer engine and your order system: each enforces its own rules, not the cross-channel rule you care about.

Is This Contention or Just Tenancy?

Before building anything, check whether you have the problem. Plenty of large support deployments don’t.

Tenancy is many customers, each on their own state. A million customers each talking to one agent at a time is partitioning. Each account has one writer. You need capacity, not coordination.

Contention is several writers on one customer’s state in one window. It shows up when customers escalate across channels at once, when a proactive outbound agent and an inbound agent reach the same customer, when an AI agent and a human agent both work a case, or when an in-app agent changes account details while a voice agent is acting on them. That’s multi-writer contention.

The litmus test: when two channels act on this account at the same moment, what resolves the conflict today? If the answer is “the second agent usually sees the first one’s note,” you have a race.

Measure the Overlap Before You Build

You don’t have to guess how often this happens. If your channels log the actions they take, a self-join over the last month tells you:

sql
-- Pairs of actions on the same order, from different channels, within 10 minutes
SELECT a.order_id,
       a.channel  AS first_channel,  a.kind AS first_action,
       b.channel  AS second_channel, b.kind AS second_action,
       b.created_at - a.created_at AS gap
FROM   agent_actions a
JOIN   agent_actions b
  ON   b.order_id   = a.order_id
 AND   b.channel   <> a.channel
 AND   b.created_at >  a.created_at
 AND   b.created_at <= a.created_at + interval '10 minutes'
WHERE  a.created_at > now() - interval '30 days';

Count the pairs per week, then count the ones where both actions were remedies or offers. That second number is your exposure. If it rounds to zero, conversation memory and ordinary handoffs are enough, and you can stop here. If it doesn’t, it also tells you which channel pairs and which actions to coordinate first.

Use the window that matches your business. Ten minutes catches a customer escalating mid-conversation; a day catches an outbound campaign landing on top of an open case.

Coordinating the Account

Keep one commitments record per account. Every remedy, offer and account-changing action any channel takes is written to one place as a row with a status: pending, issued, declined. Agents write there before they call the refund API or confirm an offer to the customer, not after.

Make “one per incident” a constraint, not a prompt. A partial unique index turns the business rule into something the store enforces for every channel:

sql
CREATE UNIQUE INDEX one_remedy_per_incident
  ON remedies (order_id, incident_type)
  WHERE status IN ('pending', 'issued');

INSERT INTO remedies (remedy_id, order_id, incident_type, channel, kind, status)
VALUES ('rm-41', 8812, 'late_delivery', 'voice', 'refund', 'pending')
ON CONFLICT (order_id, incident_type) WHERE status IN ('pending', 'issued')
DO NOTHING
RETURNING remedy_id;
-- no row returned: another channel already has a remedy in flight for this incident

In PostgreSQL, a unique index makes attempts to insert a duplicate entry fail (docs), and a WHERE clause makes it a partial unique index that only constrains rows matching the condition, so a declined remedy doesn’t block a new one. With ON CONFLICT DO NOTHING, the second channel’s insert simply returns no row. If the voice and chat inserts race, only one remedy row lands. The losing agent gets a clear answer: a remedy is already in progress, so tell the customer what the other channel did.

Use the same shape for offers. One active retention offer per customer per window is the same constraint on a different table. The second channel learns that an offer exists and can repeat it instead of inventing a new one.

Check versions on details another channel can change. For the address, the redelivery tool should take the address version the agent read and refuse if it changed. Where the address lives in your order system, that check has to happen in the order system or immediately before the call, because a separate store can’t block writes it doesn’t own.

Serialize the commit, not the conversation. Routing every action for one account through a single queue works, but a voice agent can’t hold a customer in silence behind a chat agent’s model call. Let both agents talk freely and serialize only the short step where a remedy is recorded.

Read commitments before speaking. Coordination at write time stops the second refund. It doesn’t stop the voice agent from promising one first. Have each agent read open commitments for the account right before it offers anything, so it says “I see a refund is already being processed” instead of making a promise the write will reject.

The voice agent and the chat agent each try to insert a remedy for the same incident. A one-per-incident unique index lets one insert commit; the other insert returns no row.

Then Execute

Once the remedy row exists, the agent calls the processor with the remedy id as the idempotency key, so a retried call can’t refund twice, then marks the row issued. If the call fails, it marks the row declined, which frees the incident for another attempt. The remedy record coordinates your agents; the processor still executes the refund under its own rules.

A timeout is not a failure. The riskiest moment is the call that never answers. The voice agent sends the refund, the connection drops, and it doesn’t know whether the refund exists. If it treats that as a failure and marks the row declined, the chat agent is free to refund again. Keep the row pending, and retry with the same key. Stripe’s idempotent requests docs describe the contract: “if a connection error occurs, you can safely repeat the request without risk of creating a second object or performing the update twice.” Mark the row declined only when a retry with the same key returns a definite failure.

Where Tacnode Fits

The pattern above needs one store that every channel’s agent writes commitments to, with real constraints and transactions, and one read that shows each agent, in a single snapshot, the orders, prior remedies, open offers and what the other channels just committed.

The Tacnode Context Lake™ holds that decision context. The commitments record, remedies, offers and agent actions are state Tacnode holds, so recording a remedy or an offer runs as an ACID transaction, and serializable isolation is the default. Two channels recording a remedy for the same incident at the same moment can’t both commit one.

Orders, payments and addresses usually live in your own order system, processor and CRM, and arrive by change data capture. Tacnode enforces nothing inside those systems. It gives each channel’s agent an internally coherent view: the order, the remedies other channels just wrote and derived values such as remedies issued to this customer this quarter, all computed against the same set of ingested events. Each agent’s commitment is written back and becomes context for the next channel’s check, so every channel works from the same shared context.

If your channels never overlap on the same customer within minutes, you don’t need this; conversation memory and an ordinary handoff are enough.

FAQ

AI AgentsCustomer ExperienceVoice AIConcurrencyOmnichannel
Boyd Stowe

Written by Boyd Stowe

Former Couchbase and IBM. Two decades helping enterprises adopt new database paradigms.

Fresh Context Newsletter

The argument behind the posts — what breaks when AI acts on stale data, from the team building the Context Lake. About twice a month.

Subscribe on LinkedIn

Ready to see Tacnode Context Lake™ in action?

Book a demo and discover how Tacnode can power your AI-native applications.

Book a Demo