## TL;DR
Duplicates happen when the send succeeds but the acknowledgment is lost: the agent retries, and the retry sends again. An idempotency key fixes this: attach a unique key per intended reply, and have the send path dedupe on it. Same key twice means one reply, no matter how many retries.

## The query

```text
auto-reply agent sent duplicate responses: idempotency key
```

## Use this when

- Customers receive the same auto-reply twice
- Duplicates cluster after timeouts or deploys
- You're designing an agent's send path


## Not for

- Human agents double-sending
- Intentional follow-up messages
- Duplicate ticket creation (different fix)


## Steps

### 1. Generate one key per intended reply

When the agent decides to reply, create a unique key for that reply: conversation id plus a reply counter works. The key represents intent, not attempt: retries of the same reply reuse the same key.

Expected output: every intended reply carrying a stable unique key.

### 2. Dedupe sends on the key

Before sending, check a sent-keys store: if the key was already sent, skip. Write the key to the store atomically with the send, so a crash between send and record can't double-send.

Expected output: a sent-keys store checked before every send.

### 3. Make the whole send path retry-safe

Timeouts, deploys, and restarts will retry. That's fine now: retries with the same key are no-ops. Test by killing the agent mid-send and confirming one reply.

Expected output: a chaos test proving retries never double-send.

### 4. Expire old keys on a schedule

The sent-keys store grows forever otherwise. Expire keys after a window longer than any plausible retry (days, not minutes). Document the window.

Expected output: a bounded store with a documented expiry policy.

## Variant phrasings

### ai agent duplicate replies

Steps 1 and 2: key per reply, dedupe on send.

### chatbot sent same message twice

Step 3's retry-safety test proves the fix.

## Why it happens

At-least-once delivery is the norm for agent infrastructure: timeouts trigger retries, deploys interrupt sends, queues redeliver. Without idempotency, every retry is a new send. The duplicate isn't a bug in the reply logic; it's the infrastructure doing exactly what it's designed to do, unguarded.

## Edge cases

- The key must survive restarts: keep the store in a database, not in memory.
- Different reply intents need different keys. One key per conversation under-dedupes legitimate follow-ups.
- Some channels dedupe natively. Don't rely on it; own the guarantee yourself.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_kwg7iXPC2E6rlz_PZcCYRQ
