# Evita las escrituras duplicadas en una API cuando los agentes de IA reintentan solicitudes

Los agentes de IA cometen errores de reintento más rápido que las personas. Una persona que ve un tiempo de espera puede detenerse, revisar la cola de tickets y decidir qué ocurrió. Un agente suele ver una excepción, seguir la instrucción de reintentar y enviar una segunda escritura antes de que la primera solicitud haya terminado en algún punto más allá de los límites de la red.

Ese comportamiento crea tickets duplicados, intentos de pago repetidos, invitaciones de usuario duplicadas y dos despliegues del mismo cambio. La solución no consiste en decirle al agente que tenga cuidado. Necesitas un contrato de API que conserve la identidad de una acción prevista durante los reintentos, detecte los cambios ocultos detrás de un identificador reutilizado y solicite una nueva confirmación humana cuando la consecuencia lo merezca.

## Los reintentos son normales, los efectos duplicados son opcionales

Un tiempo de espera no significa que el servidor no haya hecho nada. Puede haber creado el ticket y haber perdido la respuesta durante el regreso. También puede seguir procesando la solicitud. Un balanceador de carga puede haber aceptado la conexión mientras el servicio ascendente nunca recibió los datos. El cliente no puede deducir el resultado a partir de un error del socket.

Por eso la instrucción habitual para un agente, «reintenta ante errores de red», es incompleta. Trata todos los resultados inciertos como acciones fallidas. En los endpoints de escritura, un resultado incierto tiene tres estados posibles:

- el servidor no recibió la solicitud
- el servidor aceptó la solicitud y completó el trabajo
- el servidor aceptó la solicitud, pero todavía no lo ha completado

El mismo reintento debe ser seguro en los tres estados. Si crea un segundo efecto cuando la operación ya se completó, el endpoint tiene un límite de reintento inseguro.

HTTP no resuelve esto por ti. RFC 9110 define los métodos idempotentes como aquellos cuyo efecto previsto permanece igual después de una o varias solicitudes idénticas. Menciona PUT, DELETE y los métodos seguros. La RFC también indica que un cliente puede reintentar una solicitud idempotente después de un fallo de comunicación. Es útil, pero no convierte en seguro cualquier endpoint que use una ruta PUT. Un servidor puede asociar el envío de correo electrónico, la emisión de crédito o el inicio de un despliegue a un controlador PUT y repetir ese efecto secundario si no lo diseña para evitarlo.

POST necesita un acuerdo explícito. Muchas API usan POST para acciones porque el servidor asigna los identificadores de los recursos o porque la solicitud significa «realiza esta operación empresarial». Un agente solo puede reintentar una solicitud así cuando la API indica cómo identifica una misma operación entre varios intentos.

Separa los reintentos de transporte de los reintentos empresariales. Un reintento de transporte vuelve a enviar la misma operación porque el resultado sigue siendo desconocido. Un reintento empresarial inicia otra operación porque la primera terminó con un fallo conocido y definitivo. Confundirlos produce el informe de incidente clásico: el agente reintentó correctamente, dos veces.

## Un token de idempotencia identifica una acción prevista

Un token de idempotencia es un identificador opaco generado por el cliente que significa «todas las solicitudes que llevan este valor son intentos de realizar esta acción». El agente lo crea antes de la primera solicitud, lo guarda junto con el estado de la tarea y envía el mismo valor en cada reintento.

El token debe pertenecer a la acción lógica, no a un intento HTTP. Si un agente crea un ticket de soporte, pierde la respuesta y realiza otra solicitud con un token nuevo, la API no tiene forma de reconocer que se trata de un reintento. Debe crear un segundo ticket porque el cliente le indicó que era una segunda operación.

Usa un valor aleatorio con mucha entropía. UUID es una opción habitual, pero sirve cualquier formato siempre que los clientes no puedan adivinar los valores y el servidor trate el token como opaco. Colócalo en una cabecera `Idempotency-Key` o en un campo de solicitud documentado. Una cabecera mantiene separada la identidad de la operación de la carga empresarial y facilita que el middleware la conserve en los registros y el rastreo.

Una solicitud práctica sería esta:

```bash
curl -X POST https://api.example.test/v1/tickets \\
  -H 'Authorization: Bearer $TOKEN' \\
  -H 'Content-Type: application/json' \\
  -H 'Idempotency-Key: 81b59b1a-9e75-4de7-a53b-1bb50969c83c' \\
  -d '{"project":"ops","title":"Rotate staging certificate","priority":"high"}'
```

En la primera llamada aceptada, el servidor registra el token, una huella canónica de la solicitud, el estado de la operación y, finalmente, la respuesta que reproducirá. Si una solicitud posterior lleva el mismo token y la misma huella, el servidor devuelve el resultado anterior en lugar de crear otro ticket.

El cliente necesita un lugar duradero donde guardar el token. Un agente que lo almacena solo en el prompt actual o en la memoria del proceso pierde la identidad de la operación después de reiniciarse. Guárdalo junto al registro de la tarea, del trabajo o del punto de control del flujo. Si una persona pide al agente que cree un segundo ticket deliberadamente separado con el mismo texto, el agente debe generar un token nuevo porque la persona ha expresado una intención nueva.

No hagas que el token sea igual al nombre mutable de una tarea, a una marca de tiempo o a una solicitud en lenguaje natural. Esos valores pueden colisionar, cambiar entre reintentos o revelar información en los registros. Los identificadores opacos son aburridos. Precisamente por eso funcionan.

## Una huella detecta los reintentos modificados

El token responde si el cliente afirma que dos solicitudes pertenecen a una misma operación. La huella de solicitud responde si esas solicitudes realmente significan lo mismo. Necesitas ambas cosas.

Imagina que un agente pide primero a una API de despliegue que envíe el commit `a1b2c3` a staging. Se agota el tiempo de espera, lee una nota de tarea más reciente y reintenta con el mismo token, pero con el commit `d4e5f6`. Si el servidor reproduce ciegamente la primera respuesta, oculta un error del agente. Si ejecuta el segundo cuerpo, permite que un solo identificador de operación autorice dos despliegues distintos.

Normaliza las partes relevantes de la solicitud y calcula un hash del resultado. La mayoría de las API incluyen el método HTTP, una ruta normalizada, la cuenta o el tenant autenticado y el cuerpo JSON canónico. Algunas incluyen determinadas cabeceras cuando cambian el efecto empresarial. Excluye las cabeceras de rastreo variables, los metadatos de conexión y la propia cabecera de idempotencia.

JSON exige cuidado. Los hashes de bytes sin procesar fallan cuando un JSON equivalente usa un orden de propiedades o espacios diferentes. Una representación canónica ordena las propiedades de los objetos, conserva el orden de las matrices, utiliza un formato numérico definido y omite los campos asignados por el servidor. Mejor aún, calcula la huella sobre el objeto de comando validado, después de que la API aplique los valores predeterminados y rechace los campos desconocidos. Así coincide con la operación que ejecutará el servidor, no con una codificación de entrada arbitraria.

Por ejemplo, este pseudocódigo registra un resumen después de la validación:

```text
command = validate_create_ticket(request.body)
canonical = canonical_json({
  "method": "POST",
  "route": "/v1/tickets",
  "account_id": authenticated_account.id,
  "command": command
})
fingerprint = sha256(canonical)
```

Cuando ya existe un token, compara las huellas antes de devolver el resultado anterior o esperar por él. Si son diferentes, rechaza la solicitud con una respuesta de conflicto. Incluye el identificador y el estado de la operación almacenados, pero no repitas datos protegidos de la solicitud ante un cliente no autorizado.

Una huella no detecta duplicados por sí sola. Dos usuarios pueden presentar legítimamente dos tickets idénticos. Un servicio de nóminas puede emitir legítimamente pagos iguales a dos empleados. Aplicar un hash a una carga y deduplicar todas las coincidencias elimina silenciosamente trabajo válido. Limita la deduplicación al token de idempotencia y aplica reglas de unicidad específicas del negocio solo donde el dominio las necesite.

Los hashes criptográficos hacen que las colisiones accidentales sean poco prácticas cuando usas una función moderna como SHA-256. No demuestran la intención del cliente. El token transmite la intención y la huella impone la coherencia. Los equipos que los tratan como intercambiables suelen acabar con una regla de deduplicación que no pueden explicar cuando rechaza una solicitud legítima.

## El servidor debe reclamar el token antes de actuar

Una tabla de idempotencia que registra los resultados solo después de completar el efecto secundario sigue teniendo una condición de carrera. Dos reintentos simultáneos pueden consultar la tabla, no encontrar nada, crear dos tickets y competir después para guardar el resultado. He visto este problema disfrazado de fallo intermitente del agente, cuando el defecto real era la ausencia de una restricción de unicidad.

El servidor debe reclamar el token de forma atómica antes de realizar un trabajo irreversible. Añade una restricción única sobre el ámbito y el token, normalmente un identificador de cuenta junto con el token de idempotencia. En una sola transacción, intenta insertar una fila con la huella y el estado `in_progress`. La solicitud que gana controla la ejecución. Las demás leen la fila existente.

Una tabla simplificada podría contener estos campos:

```sql
create table idempotency_operations (
  account_id text not null,
  token text not null,
  fingerprint text not null,
  state text not null,
  response_status integer,
  response_body jsonb,
  created_at timestamptz not null,
  primary key (account_id, token)
);
```

La clave primaria hace aquí un trabajo real. El código de la aplicación que comprueba primero e inserta después deja una brecha suficientemente grande para que pasen trabajadores simultáneos, redeliveries de la cola y reintentos impacientes.

Después de reclamar el token, el controlador realiza la acción empresarial y escribe la respuesta final en la fila de la operación. Las solicitudes posteriores que coincidan reciben ese estado y cuerpo guardados. Así los clientes obtienen una respuesta estable, incluso cuando el controlador original ya terminó correctamente, pero la conexión murió antes de responder.

El caso incómodo es una solicitud que posee una fila y muere a mitad del trabajo. No borres la fila solo porque un trabajador agotó el tiempo de espera. Otro trabajador podría seguir terminando la tarea o el proveedor externo podría haber aceptado ya la operación. Marca la operación como pendiente o desconocida, registra datos suficientes para investigarla y permite que los clientes consulten su estado. Un trabajo de reparación puede resolver registros antiguos solo cuando comprende el estado del sistema externo.

Para el trabajo que cruza una base de datos y una API externa, usa el patrón outbox o un token de idempotencia del proveedor. Una transacción de base de datos no puede deshacer un correo, un pago o un despliegue en la nube después de que salga de tu proceso. Escribe la intención y un evento outbox en una sola transacción local y haz que un trabajador envíe el evento con un identificador de operación estable para el sistema externo. Así el código de recuperación tiene algo concreto que reproducir sin inventar una segunda acción.

## La confirmación debe vincularse a la operación exacta

La confirmación humana evita otro fallo: un agente puede tener permiso para actuar, pero la acción propuesta puede resultar inesperada, demasiado amplia o repetirse después de que cambie el contexto. Un botón genérico de «permitir despliegue» no resuelve el problema. Permite que un agente sustituya un despliegue por otro bajo la misma aprobación.

Una confirmación útil indica el objetivo, la operación, la consecuencia y el identificador de operación. Para un despliegue en producción, muestra el entorno, el artefacto o referencia del commit, el servicio afectado y si la acción puede revertirse. Para un pago, muestra el beneficiario, el importe, la divisa y la referencia de la factura. Para un ticket, muestra el proyecto de destino y el título.

El registro de confirmación debe vincularse a la huella de la solicitud y caducar cuando la propuesta deje de estar vigente. Si el agente cambia el cuerpo después de que una persona lo apruebe, la huella cambia y el sistema debe solicitar una nueva confirmación. Reutilizar una aprobación después de cambiar la solicitud es una forma silenciosa de escalada de privilegios, aunque nadie lo haya pretendido.

No obligues a una persona a aprobar cada reintento de bajo riesgo. Eso convierte un diseño correcto de idempotencia en fatiga de aprobaciones. La primera aprobación puede autorizar la operación identificada por una huella concreta, y los reintentos coincidentes pueden usarla porque no pueden cambiar su significado. Una carga modificada necesita otra decisión.

Algunos equipos dependen de un mensaje de chat como «¿Continuar?» y consideran la respuesta una aprobación. Bajo presión, esto falla porque el registro suele carecer de los parámetros exactos y el agente puede interpretar una respuesta posterior como consentimiento para una solicitud anterior. Incluye el identificador de operación en el registro de confirmación y exige que el ejecutor lo verifique antes de enviar la escritura.

Un payload de aprobación sencillo hace visible el vínculo:

```json
{
  "operation_id": "op_3f8c",
  "idempotency_token": "81b59b1a-9e75-4de7-a53b-1bb50969c83c",
  "fingerprint": "e5c7...",
  "expires_at": "2025-06-14T15:30:00Z",
  "approved_by": "user_42"
}
```

Trata la confirmación como autorización para un comando concreto, no como permiso para improvisar alrededor de una categoría de comandos. Esa diferencia mantiene seguro un reintento sin dar al agente una aprobación general que pueda reutilizar después.

## Los sistemas de tickets también necesitan una comprobación de duplicados a nivel empresarial

Los tokens de idempotencia detienen los intentos de transporte duplicados, pero los sistemas de tickets tienen otra fuente de duplicación: los agentes pueden iniciar operaciones separadas que describen el mismo problema. Una alerta de monitorización llega dos veces, dos ejecuciones del agente leen el mismo canal de incidentes o un programador se despierta después de un fallo y reproduce una tarea sin su estado original.

No lo resuelvas deduplicando por el texto del título. Los títulos de los tickets varían lo suficiente como para no detectar duplicados, y dos títulos idénticos pueden referirse a incidentes distintos. Decide qué significa identidad en el dominio de los tickets. Puede ser un identificador de evento de alerta, un identificador de incidente, una referencia a un problema del repositorio o un valor compuesto como servicio, huella de alerta y periodo del incidente.

Haz explícito ese identificador empresarial en la API:

```json
{
  "source_event_id": "alert-7c91",
  "project": "operations",
  "title": "Certificate expiry alert",
  "description": "Alert event alert-7c91 crossed its threshold."
}
```

El servicio de tickets puede imponer la unicidad de `source_event_id` dentro del ámbito previsto. Una segunda ejecución del agente recibe entonces el identificador del ticket existente en lugar de añadir otro elemento a la cola. Esto es independiente de la idempotencia. Las dos llamadas pueden tener tokens de idempotencia distintos porque proceden de dos procesos de agente diferentes, pero representar el mismo evento ascendente.

Los agentes solo deberían buscar antes de crear cuando el resultado de la búsqueda tenga una identidad estable en la que puedan confiar. Los flujos de búsqueda por título resultan tentadores porque no requieren cambios en la API. Fallan en cuanto el índice se retrasa, cambia la clasificación de resultados o un agente reformula el título. Coloca la regla de unicidad donde se realiza la escritura y devuelve una respuesta clara que indique si la API creó o reutilizó un ticket.

Ten cuidado con los comentarios automáticos y los cambios de estado. Una operación que encuentra un ticket existente podría añadir aun así un comentario duplicado o volver a abrir un incidente resuelto. Da a cada subacción relevante su propio identificador o haz que el comando de escritura exprese todo el estado deseado. Los endpoints vagos de «actualiza este ticket» son difíciles de reintentar de forma segura porque nadie puede saber qué parte de la actualización ya se ejecutó.

## Las escrituras de pagos necesitan consultar el resultado, no confiar en el optimismo

Las acciones de pago requieren un estándar más estricto porque un cargo duplicado perjudica al cliente aunque después lo reembolses. La aplicación debe enviar un token de idempotencia estable al proveedor de pagos y conservar la referencia de la transacción del proveedor junto al registro de la operación local.

Cuando el cliente agota el tiempo de espera, debe tratar el pago como desconocido. Debe consultar por la referencia del proveedor, la referencia del comercio o el token de idempotencia si el proveedor ofrece esa búsqueda. No debe iniciar otro intento de pago porque el agente no recibió una respuesta de éxito.

Hay dos operaciones que las personas suelen mezclar: crear una intención de pago y capturar los fondos. Pueden tener comportamientos de reintento diferentes. Un servicio puede crear o recuperar de forma segura un mismo objeto de pago mediante un token y exigir después una acción explícita de captura cuando se hayan superado las comprobaciones. Modela abiertamente los estados empresariales en lugar de ocultarlos detrás de un único endpoint que lo intente todo en cada llamada.

Los importes necesitan un tratamiento canónico antes de calcular la huella. Convierte los valores a la unidad monetaria mínima admitida o a otra representación exacta antes de que la solicitud llegue a la capa de deduplicación. No calcules un hash sobre un valor de presentación en coma flotante esperando que las operaciones equivalentes se comparen de forma fiable. Una solicitud de pago también debería incluir una referencia de factura o pedido cuando el dominio tenga una, porque ofrece al personal una forma de identificar una intención duplicada más allá de los reintentos de red.

La función de idempotencia del proveedor no elimina la responsabilidad de tu propia API. Tu aplicación aún debe impedir que dos tareas del agente inicien dos solicitudes distintas al proveedor para el mismo pedido. Añade una restricción de unicidad al estado pagable del pedido, usa un registro de operación local y haz que el agente consulte ese registro después de una incertidumbre.

Los reembolsos merecen el mismo cuidado. «Reintentar el reembolso» puede significar repetir la misma solicitud de reembolso o iniciar otro reembolso parcial. Conserva un identificador estable para cada instrucción de reembolso y registra el importe solicitado hasta el momento. Si el agente necesita emitir un segundo reembolso, conviértelo en una instrucción nueva y autorizada de forma explícita, con un identificador nuevo.

## Los despliegues necesitan referencias inmutables y un bloqueo de versión

Un reintento de despliegue solo es seguro si nombra la misma versión. Los nombres de rama como `main` y las etiquetas mutables como `latest` no cumplen ese estándar. Un reintento después de un tiempo de espera puede resolver el mismo nombre en un código diferente y parecer correcto mientras despliega algo que la persona aprobadora nunca revisó.

Usa un resumen inmutable del artefacto, un identificador de commit o una versión que el sistema de lanzamientos garantice que no cambiará. Inclúyelo en la huella de la solicitud y en la confirmación. Si un agente envía el mismo token de idempotencia con una referencia de artefacto modificada, recházalo como conflicto en lugar de tratar la segunda solicitud como una actualización del reintento.

También necesitas una regla de concurrencia para el entorno. Dos operaciones distintas pueden llevar legítimamente tokens diferentes y aun así entrar en conflicto porque ambas se dirigen a producción. Un bloqueo de lanzamiento, una comprobación optimista de versión o una cola de despliegue pueden serializar esos cambios. La idempotencia no decide cuál de dos despliegues distintos debe ganar. Solo impide que un despliegue se ejecute dos veces.

Considera esta secuencia de fallo. El agente inicia el despliegue `dep-118` para el commit `a1b2c3` y el controlador de despliegues lo acepta. El agente pierde la respuesta, supone que falló e inicia `dep-119` con el commit `d4e5f6` porque apareció un commit más reciente. Ahora ambos trabajos modifican el mismo entorno. Un token solo habría detenido un reintento real de `dep-118`; el bloqueo de lanzamiento o la comprobación de la versión esperada del entorno detiene el segundo plan conflictivo.

La API de despliegues debe exponer un recurso de estado de la operación que indique queued, running, succeeded, failed, canceled o unknown. Los agentes deben consultar ese estado después de un tiempo de espera. No deben inferir que el despliegue terminó a partir de una respuesta ausente o de una línea de registro que no incluya el identificador de operación.

La reversión necesita su propio identificador de operación y su propia aprobación. Tratar una reversión como un reintento del despliegue oculta un cambio importante de intención. Puede ejecutarse automáticamente bajo una regla de seguridad documentada, pero debe dejar un registro distinto del lanzamiento original.

## Las herramientas del agente deben conservar la identidad de la operación a través de la frontera

La interfaz de herramientas de un agente debe hacer que el comportamiento seguro sea más fácil que el inseguro. Dale al agente una acción que acepte un identificador de operación estable, un payload y un modo de reintento declarado. Devuelve un resultado que indique si el servicio creó trabajo, reprodujo un resultado anterior, encontró una operación en curso o rechazó un reintento modificado.

Evita las herramientas que generan silenciosamente un token de idempotencia nuevo en cada invocación. Parecen cómodas en una demostración y fallan ante el primer tiempo de espera real. Si la herramienta se encarga de generar el token, debe devolverlo de inmediato y guardarlo donde una invocación posterior pueda recuperarlo. En la mayoría de los sistemas, la capa de flujo debería controlar el token porque entiende qué llamadas pertenecen a una misma acción solicitada por el usuario.

Sallyport puede mantener las credenciales de la API fuera del agente mientras este envía la acción HTTP prevista a través de su conexión MCP. Esa separación ayuda a evitar la exposición de credenciales, pero la API descendente sigue necesitando un comportamiento idempotente. Una credencial protegida no convierte un POST ambiguo en un reintento seguro.

Haz explícita la regla de reintento del agente en el contrato de la herramienta:

```text
if response is a known success:
    record operation complete
if response is a timeout or connection failure:
    query operation status using the same token
    retry only with the same token if the API permits it
if response says fingerprint conflict:
    stop and request a new operation or human review
if response is a known business failure:
    do not retry until the task changes
```

No permitas que el agente use la espera exponencial como sustituto del estado. La espera exponencial reduce la presión sobre un servicio, lo cual importa, pero no responde si la última escritura tuvo éxito. El agente debe conservar el identificador de la operación antes de esperar.

## Los registros deben demostrar qué ocurrió después de una escritura discutida

Cuando un cliente dice que se le cobró dos veces o un ingeniero encuentra dos tickets, necesitas responder cuatro preguntas: qué ejecución del agente emitió cada solicitud, qué token utilizó, qué huella calculó el servidor y qué resultado devolvió el servicio descendente. Los registros generales de solicitudes suelen omitir al menos una de ellas.

Registra una operación en el límite donde la API acepta la acción. Incluye la identidad autenticada, el token, la huella, la ruta de solicitud, las transiciones de estado de la operación, la referencia de respuesta y la referencia del proveedor ascendente cuando exista. Mantén los secretos y los cuerpos sensibles completos fuera de los registros habituales. Una huella permite comparar solicitudes sin guardar todos los campos privados en cada sistema de registros.

Un registro de auditoría de solo anexado ayuda cuando un agente tiene autoridad para realizar escrituras externas. El registro debe distinguir entre intentada, aprobada, enviada, aceptada, completada y reproducida. No son estados equivalentes. Un reintento que recibe una respuesta anterior almacenada debe indicar `replayed`, no `created`, o los operadores lo contabilizarán como una segunda acción.

Sallyport registra las sesiones de los agentes y las acciones individuales en diarios derivados de un registro de auditoría cifrado y encadenado mediante hashes, y `sp audit verify` puede verificar la cadena sin conexión. Esto puede demostrar qué pasó por la pasarela de acciones. Combina esas pruebas con los registros de idempotencia del servicio receptor, porque el servicio receptor es quien determina si ejecutó la acción empresarial.

Prueba el flujo de escritura discutida antes de confiar en él. Obliga al servidor a completar una solicitud y descarta la respuesta. Envía copias simultáneas con un mismo token. Reinicia el agente entre intentos. Reutiliza un token con un payload modificado. Mata un trabajador después de que reclame un token y antes de registrar la finalización. Un diseño que solo sobrevive a respuestas de éxito limpias no ha resuelto las escrituras duplicadas.

Empieza por el endpoint de escritura que más daño causaría si se repitiera. Añade un token estable, reclámalo de forma atómica antes de cualquier efecto secundario, vincúlalo a una huella y ofrece a los clientes una consulta de estado para los resultados desconocidos. Después, haz que el agente conserve ese identificador hasta que pueda demostrar que la operación llegó a un estado terminal.
