When automation works well, human handoff looks like an interface detail: someone clicks “take over” and AI stops replying. That interpretation disappears in the first real concurrency case.
A message may already be queued. The model may be generating a response. A webhook may arrive twice. An operator may take over on another device while an old retry remains alive. If handoff is only a boolean checked at the beginning, two authorities can act on the same customer.
The primary problem is not prompt quality. It is state coordination.
Handoff is a transfer of authority
Before producing an effect, the system must answer: who has authority to speak for this conversation now?
That authority belongs neither to the model, queue, nor operator screen. It belongs to the conversation domain. A simple state machine clarifies the design:
- AI_ACTIVE: policy allows automation, but every reply still requires validation;
- HANDOFF_PENDING: a transfer is underway and no new automated work may begin;
- HUMAN_ACTIVE: only the current operator may authorize outgoing messages;
- AI_SUSPENDED: automation is blocked by policy, failure, or review, even without an active operator.
Names may vary. What matters is an atomic, versioned, observable transition. “Human active” cannot be only a visual attribute updated after old jobs were released.
A safe handoff does not merely stop the next generation. It revokes authority from every automated job born under an earlier conversation version.
The race the happy path does not show
Consider a short sequence:
- Customer message M42 arrives and creates job J18.
- J18 reads AI_ACTIVE and requests a model response.
- Before it returns, an operator takes over.
- The operator replies manually.
- J18 receives the model output and sends a second message.
No component “failed.” Each part did exactly what it knew to do. The defect lies in the contract between read and effect: J18 checked authority too early and assumed it would remain valid.
The same race appears in follow-ups, scheduling, and CRM updates. Canceling the model call saves cost but does not guarantee correctness. Cancellation may arrive late, a response may already be persisted, or a retry may reappear.
Separate proposal, policy, and effect
Treat a generated answer as a proposal, not an authorized effect. The safer path has three moments:
- Proposal: the model receives allowed context and produces a candidate.
- Policy: the service rereads the current conversation version and owner, validates restrictions, and decides whether the proposal may still leave the system.
- Effect: an authorized message receives an idempotency key, enters an outbox, and only then goes to the channel.
The second read is essential. The world can change between generation and send. Policy compares the authority version captured by the job with the current version. A mismatch makes the work obsolete, not an error to retry.
The Auto Whats technical case expresses the invariant directly: only the current conversation owner may authorize an outgoing response.
A version beats a boolean
A field such as aiEnabled expresses preference, not proof that a job still has permission. Add a monotonic authority version, such as authorityVersion.
At creation, the job stores:
- conversation identifier;
- observed authority version;
- originating message identifier;
- applied policy;
- correlation key.
When a person takes over, one transaction changes state, increments the version, and records the owner. Before sending, the job compares its captured version with the current one. Inequality invalidates the effect even when generation succeeded.
Returning control to AI creates a new conversation epoch. Old jobs remain invalid and cannot revive accidentally.
Idempotency must cross the channel
Webhooks and queues often provide at-least-once delivery. “I received the message” and “I processed it once” are different claims.
Deduplication may combine external message ID, account, and channel. Sending needs another key derived from cause and authorized version. Repeating a handler then cannot create a new message simply because confirmation was delayed.
A transactional outbox connects internal state and external effect:
- the authorized decision and outgoing record are stored together;
- a worker sends pending items;
- confirmation updates that same item;
- retries reuse the idempotency key;
- a revoked authority version blocks unsent items.
This does not create exactly-once delivery across the world. It creates an explicit contract that makes duplication detectable, recoverable, and bounded.
Cancellation is optimization; invalidation is correctness
Aborting a model request, removing a queued job, and stopping streaming are useful optimizations. They save time and reduce late effects. None replaces validation at the send boundary.
The system must remain correct if cancellation arrives late. Cancellation improves the common path; version and policy protect the invariant.
Obsolete work should also be distinct from failed work. A response discarded because a person took over is an expected coordination outcome and deserves a reason such as authority_revoked rather than an error alert.
Context without turning conversation into a warehouse
Handoff needs enough context for the person to continue, but that does not authorize sending every historical message to every model or retaining text indefinitely.
A responsible architecture separates:
- operational events required for consistency;
- excerpts allowed in model context;
- a useful operator summary;
- sensitive data requiring masking, short retention, or deletion;
- aggregate metrics that do not depend on complete conversation storage.
The operator should know why handoff occurred: low confidence, explicit request, commercial policy, integration failure, or manual decision. “AI gave up” is a poor diagnosis for both experience and improvement.
Observe the transition, not only volume
Counting automated messages reveals little about safety. Better signals track the boundary:
- jobs invalidated after authority changes;
- time from handoff request to visible confirmation;
- messages blocked by stale versions;
- duplicates intercepted by idempotency keys;
- human replies that correct automated proposals;
- AI resumptions after human closure;
- sessions where queue, database, and channel diverge too long.
Correlation, reason, version, latency, and outcome are often enough to investigate without turning observability into unrestricted collection.
Test dangerous interleavings
A happy test covers message, generation, and send. A concurrent system must test alternate orders:
- a person takes over before the job starts;
- takeover happens during generation;
- takeover happens after outbox creation but before send;
- duplicate webhooks reach two workers;
- channel confirmation expires and retry begins;
- the conversation returns to AI while an old job exists;
- two operators try to take over together;
- the process restarts between decision and effect.
A controllable clock, deterministic IDs, and fake adapters make these sequences reproducible. The goal is to prove the invariant at every interruption point.
Failing safely is also user experience
When authority is uncertain, prefer controlled silence to two competing voices. The interface can show pending work, suggest a reply to the operator, raise an alert, and support explicit recovery.
A safe failure must be legible. If automation is blocked, the operator should know the reason and possible action. If a message still awaits channel confirmation, the screen must not present it as delivered. System correctness and product clarity meet in the same state.
The right question comes before the model
Better models reduce poor replies. They do not solve authority, idempotency, redelivery, or job competition. Those are properties of the surrounding system.
Before choosing a prompt, provider, or context size, write the invariant: only the current authority may produce an external effect. Then design versions, policy, outbox, cancellation, reasons, and tests around it.
This architecture applies beyond WhatsApp. Agents that send email, update CRM, approve tasks, or execute tools face the same boundary. AI can propose quickly. The product must decide safely.
Also read AI with boundaries: why architecture comes before the prompt and use the technical briefing to navigate the other systems.

