# Cómo registrar acciones canceladas del agente sin mentir

Un registro de auditoría que etiqueta toda acción cancelada del agente como «fallida» está mintiendo. Puede que el agente haya dejado de esperar, pero la fila de la base de datos quizá ya exista, la implementación quizá ya esté en marcha o el comando remoto quizá siga ejecutándose después de que desapareciera la conexión.

La solución no es añadir una lista más larga de códigos de error. Hace falta un modelo de resultados que separe lo que observó la puerta de enlace de lo que ocurrió en el destino. Los investigadores deben poder distinguir entre una acción que nunca salió de la máquina, una que el destino rechazó, una que terminó con un comprobante y una cuyo efecto sigue siendo desconocido.

## El resultado de quien hace la llamada no es el resultado de la acción

Quien hace una llamada solo ve una parte limitada de la acción: envía el trabajo y espera una respuesta. La acción atraviesa varios sistemas que no comparten reloj, ciclo de vida del proceso ni una vía de retorno confiable. Cuando quien hizo la llamada cancela, se desconecta o llega a su límite de tiempo, aprende algo sobre su propia espera. No necesariamente aprende nada definitivo sobre el efecto remoto.

Esta diferencia importa sobre todo en los trabajos que cambian el estado. Crear un ticket, emitir un reembolso, aplicar cambios de infraestructura, eliminar un objeto, rotar un token de acceso y ejecutar un comando remoto tienen consecuencias que pueden sobrevivir a una respuesta perdida. Si tu diario escribe `failed` porque el proceso del agente terminó antes, un reintento posterior puede crear un segundo ticket, emitir un segundo reembolso o ejecutar dos veces el comando destructivo.

Las operaciones de lectura también necesitan esta honestidad, aunque el riesgo sea distinto. Una consulta cancelada puede devolver una vista incompleta y hacer que un agente tome una mala decisión posterior. Por sí sola, normalmente no cambia el mundo. Un POST, PATCH, DELETE o comando de shell remoto sí puede hacerlo.

Mantén separadas estas tres cosas en cada registro:

- **Disposición de quien hizo la llamada:** completada, cancelada, desconectada o agotada por tiempo de espera.
- **Evidencia de envío:** nunca iniciada, iniciada localmente, bytes entregados al transporte o comprobante confirmado por el lado remoto.
- **Resultado del efecto:** sin efecto, completado, rechazado, parcialmente completado o efecto desconocido.

Los equipos suelen fusionar el primer y el tercer campo porque una sola columna de estado resulta cómoda. Esa comodidad dura hasta la revisión de un incidente. Entonces alguien tiene que explicar por qué «solicitud cancelada» aparece junto a un objeto que claramente existe en producción.

La regla práctica es directa: escribe **sin efecto** solo cuando la evidencia descarte la ejecución. Escribe **efecto desconocido** cuando la ejecución siga siendo posible y no tengas un resultado confiable. «Desconocido» no es una carencia vergonzosa del registro. Es el resultado exacto de una acción distribuida con una ruta de observación interrumpida.

## Registra el límite de la evidencia, no una historia inventada

Cada acción necesita un límite explícito después del cual la puerta de enlace ya no puede prometer honestamente que no ocurrió nada. Llámalo límite de envío. En HTTP, puede ser el momento en que la solicitud se confirma en una conexión y se entrega al sistema operativo, o cuando un servicio ascendente confirma que la aceptó. En SSH, puede ser el momento en que el auxiliar envía la solicitud de comando por un canal autenticado.

No finjas que un único booleano `sent=true` resuelve el asunto. Las escrituras locales pueden almacenarse en búfer. Una biblioteca de transporte puede informar de una escritura antes de que la aplicación del otro extremo lea los datos. El otro extremo puede recibir una solicitud, aplicar el cambio y perder la respuesta durante el regreso. El registro debe describir la evidencia más sólida disponible, no convertir un detalle de implementación en una prueba.

Un registro de acción útil tiene identificadores inmutables y una secuencia de observaciones. Esta estructura compacta funciona tanto para una llamada a una API como para la ejecución de un comando:

```json
{
  "action_id": "act_01JQ7M4V6K",
  "session_id": "ses_01JQ7M2Y8A",
  "channel": "http",
  "intent": {
    "method": "POST",
    "target": "api.example.internal/v1/releases",
    "request_fingerprint": "sha256:...",
    "idempotency_token": "release_01JQ7M4V6K"
  },
  "observations": [
    {"at": "2026-07-22T16:40:01Z", "kind": "authorized"},
    {"at": "2026-07-22T16:40:02Z", "kind": "dispatch_started"},
    {"at": "2026-07-22T16:40:03Z", "kind": "transport_write_completed"},
    {"at": "2026-07-22T16:40:33Z", "kind": "caller_deadline_exceeded"}
  ],
  "caller_disposition": "timed_out",
  "effect_outcome": "unknown_effect",
  "outcome_basis": "response_not_observed_after_dispatch"
}
```

La huella de la solicitud identifica lo que se intentó sin colocar en el diario una credencial bearer, el cuerpo sin procesar de la solicitud ni un argumento secreto de comando. El identificador debe mantenerse estable entre reintentos y conciliaciones posteriores. Si un operador no puede relacionar la solicitud original, su reintento y el objeto remoto final, el rastro de auditoría no responde a la pregunta importante.

Hay una diferencia importante entre `dispatch_started` y `transport_write_completed`. El primero dice que la puerta de enlace comenzó la operación. El segundo dice que su transporte local aceptó los datos salientes. Ninguno indica que la aplicación remota los ejecutara. Si tu implementación no puede distinguirlos, registra el hecho más débil y explícalo en la base del resultado.

## Una cancelación antes del envío puede significar que no hubo efecto

Una cancelación puede respaldar la conclusión de que no hubo efecto, pero solo antes de que la puerta de enlace confirme la acción en un canal externo. Este es el caso claro: el agente revoca la solicitud mientras todavía espera una autorización local, antes de desbloquear la bóveda, antes de iniciar una solicitud HTTP o antes de entregar un comando SSH al auxiliar de transporte.

La entrada de auditoría debe mostrar por qué la conclusión es segura. «Cancelada» por sí sola no dice al investigador dónde ocurrió la cancelación. Registra una etapa y un punto de prueba local.

```json
{
  "action_id": "act_01JQ7P1N2R",
  "caller_disposition": "canceled",
  "effect_outcome": "no_effect",
  "outcome_basis": "cancellation_observed_before_dispatch",
  "last_observed_stage": "awaiting_authorization"
}
```

Este resultado también es apropiado cuando el control local deniega la acción antes de que pueda comenzar cualquier solicitud externa. Una aprobación ausente, una bóveda bloqueada, una sesión revocada o una solicitud local no válida pueden producir «sin efecto», siempre que la puerta de enlace nunca haya enviado el trabajo. El diario debe distinguir una solicitud denegada de una solicitud cancelada porque cuentan historias diferentes sobre el control humano y el comportamiento del agente, pero ambas pueden afirmar con seguridad que el destino no vio nada.

No apliques esa etiqueta después de crear una conexión y comenzar a escribir solo porque la llamada de transporte devuelve un error de cancelación. Muchas bibliotecas usan el mismo valor de error para varias rutas: cancelación mientras estaba en cola, durante una escritura, mientras esperaba los encabezados de respuesta o mientras leía el cuerpo. No son situaciones equivalentes.

La propagación de la cancelación tiene una finalidad útil, pero no es una máquina del tiempo. Las directrices de cancelación de gRPC indican que una cancelación del cliente avisa de que ya no necesita el resultado de la RPC y recomiendan que los servidores detengan el trabajo y propaguen la cancelación a las tareas posteriores. Es una buena práctica para cuidar los recursos. No demuestra que un efecto secundario anterior se haya deshecho ni puede deshacer una escritura confirmada de forma independiente.

Si el servicio receptor ofrece un endpoint de cancelación explícito vinculado a un identificador de operación, regístralo como una segunda acción. Su resultado solo puede cambiar el resultado del efecto original si el servicio proporciona una declaración confiable sobre la operación original. Una solicitud de cancelación de mejor esfuerzo que devuelve una respuesta después de una interrupción de red crea otro efecto desconocido. No limpia mágicamente el primero.

## Un tiempo de espera después del envío tiene un efecto desconocido

Un límite de tiempo es un límite local para esperar. No es un veredicto sobre la ejecución remota. Una vez que la acción cruza el límite de envío, un tiempo de espera debe dar como resultado «efecto desconocido», salvo que un comprobante del protocolo o una comprobación posterior demuestre algo más.

Es fácil equivocarse con HTTP porque las etiquetas de estado parecen definitivas. RFC 9110 dice que un 408 significa que el servidor no recibió un mensaje de solicitud completo dentro del tiempo que estaba dispuesto a esperar. Un 504 significa que una puerta de enlace no recibió a tiempo una respuesta de un servidor ascendente. Ambas afirmaciones describen a un observador concreto y un intercambio concreto. Ninguna demuestra que otro sistema no actuara sobre datos que ya había recibido.

Imagina un agente que envía `POST /v1/releases` con un límite de 30 segundos. La API valida la solicitud, inserta una fila de lanzamiento, pide a un controlador de implementaciones que comience y después se queda bloqueada mientras prepara la respuesta. A los 30 segundos, el agente ve un tiempo de espera. El lanzamiento existe. Un reintento sin token de idempotencia puede crear otro lanzamiento, aunque la primera llamada aparezca como «fallida» en la transcripción del agente.

El registro veraz sería este:

```json
{
  "caller_disposition": "timed_out",
  "effect_outcome": "unknown_effect",
  "outcome_basis": "deadline_after_transport_write_no_remote_receipt",
  "recovery_required": "lookup_by_idempotency_token"
}
```

No uses `failed` como atajo para «desconocido». Reserva los resultados de fallo para hechos que puedas establecer: un servicio remoto devolvió un error de validación, un comando devolvió un estado de salida distinto de cero, no se pudo establecer una conexión antes de que saliera ninguna solicitud de la puerta de enlace o una decisión de autorización local denegó la ejecución. Un tiempo de espera no cumple ese estándar cuando el envío saliente pudo haber ocurrido.

El completado parcial merece su propio resultado cuando el lado remoto ofrece pruebas. Las API por lotes y los scripts suelen hacer parte del trabajo antes de fallar. Si un servicio devuelve una lista de identificadores de objetos completados seguida de un error, registra `partial_effect`, conserva los identificadores cuando la política lo permita y captura el motivo indicado por el servicio. Llamarlo simplemente «fallido» oculta el trabajo exacto de limpieza que debe realizar un operador.

## Los acuses de recibo perdidos necesitan un registro separado

Un acuse de recibo perdido ocurre después de que el receptor quizá haya actuado, pero antes de que la puerta de enlace reciba una confirmación final. Es algo bastante habitual como para merecer una observación con nombre propio, no un error de red genérico.

La secuencia suele parecer normal hasta el último momento:

1. La puerta de enlace autoriza y envía una acción.
2. El servicio remoto la acepta y realiza, o pone en cola, el trabajo solicitado.
3. La respuesta se retrasa, la conexión se interrumpe o el proceso local termina.
4. La puerta de enlace no tiene una prueba duradera de que se completó, aunque el sistema remoto quizá sí la tenga.

El primer error consiste en sobrescribir la entrada original cuando una consulta posterior tiene éxito. Eso hace que el diario parezca indicar que la puerta de enlace conocía el resultado en ese momento. El investigador necesita ambos hechos: la llamada inicial terminó sin acuse de recibo y una conciliación posterior encontró un resultado remoto coincidente.

Añade otra observación:

```json
{
  "action_id": "act_01JQ7M4V6K",
  "reconciliation": {
    "at": "2026-07-22T16:43:10Z",
    "method": "GET /v1/operations/release_01JQ7M4V6K",
    "remote_reference": "op_8f2c",
    "result": "succeeded"
  },
  "effect_outcome": "succeeded",
  "outcome_basis": "remote_operation_lookup"
}
```

La disposición original de quien hizo la llamada sigue siendo `timed_out`. No la reescribas como `completed`. Quien hizo la llamada sí agotó el tiempo de espera. El sistema supo más tarde que la acción remota había tenido éxito. Ambos hechos coexisten sin contradicción.

Un comprobante remoto solo es confiable en la medida en que lo sea su correlación. Coincidir por nombre de objeto, hora actual o lenguaje natural proporcionado por el agente ofrece pruebas débiles. Es preferible usar un token de idempotencia aceptado por el destino, un identificador de operación devuelto antes de que comience un trabajo prolongado o un identificador de solicitud del proveedor que el destino garantice como único para esa solicitud. Si el destino no ofrece nada de esto, usa una consulta de lectura específica y registra por qué basta o por qué sigue siendo ambigua.

Por ejemplo, descubrir un usuario nuevo llamado `build-bot` no demuestra qué solicitud de creación lo produjo. Descubrir un objeto con un token de solicitud almacenado igual al token de acción original es mucho mejor. Esa diferencia determina si puedes reintentar de forma segura.

## La idempotencia convierte la recuperación en una comprobación, no en una apuesta

La idempotencia no es un permiso para reintentar que se añade después de los hechos. Es un contrato establecido antes del envío. El cliente proporciona un token estable y el servicio garantiza que las solicitudes repetidas con ese token identifican la misma operación lógica, en lugar de crear efectos nuevos.

Para cada integración HTTP que cambie el estado, haz al responsable del servicio estas cuatro preguntas directas:

- ¿Acepta un token de idempotencia proporcionado por quien llama?
- ¿Qué campos de la solicitud deben mantenerse idénticos cuando se repite ese token?
- ¿Durante cuánto tiempo conserva la relación entre el token y el resultado?
- ¿Puede quien llama recuperar el resultado original después de perderse la respuesta?

Si las respuestas son vagas, no anuncies que el reintento automático es seguro. «Normalmente deduplicamos» no es un contrato. La expulsión de una caché, una conmutación por error regional o un cambio en el analizador de solicitudes pueden convertir esa suposición en trabajo duplicado.

Cuando el destino no tiene compatibilidad con la idempotencia, divide la operación de riesgo cuando sea posible. Crea un borrador duradero con una referencia externa única, verifica ese borrador y después emite el comando irreversible contra el identificador que devuelve. Esto no vuelve segura cualquier operación, pero crea un punto de conciliación antes de la parte costosa o destructiva.

Un flujo de implementación muestra la diferencia. Un endpoint de una sola operación que crea e inicia un lanzamiento deja poco margen para recuperarse de un acuse de recibo perdido. Un flujo de dos llamadas puede crear un lanzamiento usando el ID de acción como referencia externa, consultar esa referencia después de un tiempo de espera e iniciarlo solo cuando quien llama tenga un ID de lanzamiento conocido. La llamada adicional suele costar menos que explicar un cambio inesperado en producción.

Nunca generes un token de idempotencia nuevo para un reintento automático de la misma acción. Un token nuevo declara que el reintento es una solicitud lógica nueva. Eso puede ser correcto cuando una persona repite el trabajo de forma intencionada, pero anula la deduplicación durante la recuperación. Conserva el token original en el registro de la acción y haz que el reintento cite el ID de la acción principal.

Un contrato de idempotencia también ayuda a responder a los incidentes. Los investigadores pueden hacer una pregunta concreta: «¿Qué decidió el destino para el token X?». Sin ese contrato, tienen que inferir la intención a partir de horas, registros y nombres. Es lento, propenso a errores y a menudo imposible una vez que pasan los periodos de retención.

## El cierre de SSH y la finalización del comando son hechos distintos

SSH invita a cometer un error parecido porque una sesión cerrada parece indicar que el comando se detuvo. No es así. Una interrupción de red puede cortar el cliente mientras el proceso remoto continúa bajo su shell padre, supervisor o gestor de servicios. A la inversa, un proceso puede terminar mientras el cliente pierde el informe de salida.

RFC 4254 trata el cierre del canal, el fin de archivo y el estado de salida como eventos de protocolo distintos. Recomienda devolver un estado de salida cuando termina un comando remoto, pero no convierte el cierre del canal en una prueba de que lo recibiste. La especificación también dice que los pares intercambian mensajes de cierre antes de que cada lado considere cerrado el canal. Eso informa sobre el estado del canal SSH, no sobre si el comando remoto produjo un cambio en el sistema de archivos o en un servicio antes de que el canal muriera.

Registra la evidencia de SSH como observaciones separadas:

```json
{
  "channel": "ssh",
  "observations": [
    {"kind": "command_request_sent"},
    {"kind": "stdout_received", "bytes": 1840},
    {"kind": "connection_lost"}
  ],
  "caller_disposition": "disconnected",
  "effect_outcome": "unknown_effect",
  "outcome_basis": "no_exit_status_or_remote_process_identity"
}
```

Si recibes un estado de salida válido y un cierre completo del canal después de la salida del comando, tienes pruebas sólidas sobre el proceso del comando, aunque no una prueba absoluta de cada efecto externo que haya desencadenado. Un script puede enviar correctamente trabajo asíncrono y terminar con código cero antes de que ese trabajo finalice. Registra el resultado del comando como completado y modela el trabajo externo como una operación propia si el sistema remoto te proporciona un ID.

Los comandos necesitan un plan de recuperación antes de que un agente los ejecute. Prefiere comandos que impriman o escriban un identificador de operación duradero. Para reiniciar un servicio, consulta el gestor de servicios usando un nombre de unidad conocido y un estado anterior y posterior. Para una migración de base de datos, revisa el registro de migraciones en lugar de confiar en la salida del terminal. Para una operación de archivos, comprueba un hash de contenido y un marcador de versión o generación, no solo si existe una ruta.

No trates una señal enviada a un auxiliar local como prueba de que el proceso remoto se detuvo. La señal puede llegar antes del envío remoto, después de que termine el comando remoto o después de que se rompa la conexión. Registra la señal como un evento del agente o de la puerta de enlace. Cambia el resultado del efecto solo cuando el endpoint remoto proporcione pruebas.

## La investigación comienza con la cronología, no con una etiqueta final

Un investigador debe poder reconstruir una acción indeterminada sin adivinar qué línea del registro apareció primero. Para ello necesita un ID de acción estable, un ID de sesión, un ordenamiento monotónico de eventos dentro de la puerta de enlace y marcas de tiempo para los eventos observados. La hora del reloj ayuda a correlacionar, pero puede variar entre sistemas. No construyas toda la conclusión sobre dos relojes que coinciden al milisegundo.

Una buena investigación plantea estas preguntas en orden:

1. ¿Quién autorizó la acción y qué proceso del agente hizo la solicitud?
2. ¿La puerta de enlace cruzó su límite de envío?
3. ¿Qué observaciones del transporte ocurrieron después del envío?
4. ¿Llegó algún comprobante remoto confiable?
5. Si no llegó, ¿qué consulta de conciliación puede identificar la operación lógica original?

Esta secuencia evita un mal hábito común: buscar primero en los registros remotos, encontrar un evento parecido y declararlo como la respuesta. Empieza por la acción prevista y sus datos de correlación. Después evalúa si la evidencia remota coincide con esa operación exacta.

El registro de auditoría debe resistir las modificaciones silenciosas. Si un operador puede cambiar `unknown_effect` por `succeeded` sin conservar el estado anterior y la base de la actualización, el registro se convierte en una afirmación y deja de ser una prueba. Los registros de solo adición, el encadenamiento mediante hash y la verificación sin conexión hacen más difícil ocultar las modificaciones posteriores a un incidente. No demuestran que todos los sistemas remotos hayan dicho la verdad, pero conservan lo que la puerta de enlace observó y cuándo obtuvo nueva información.

Sallyport conserva las ejecuciones de los agentes y las acciones individuales en diarios separados proyectados desde un único registro de auditoría cifrado y encadenado mediante hash. `sp audit verify` puede verificar la cadena sin conexión sobre el texto cifrado. Este diseño resulta útil porque una conciliación posterior puede registrarse como un hecho nuevo sin borrar el tiempo de espera o la desconexión originales.

No introduzcas cuerpos de solicitud con secretos en un registro de auditoría solo para mejorar la respuesta a incidentes. Guarda destinos saneados, huellas de solicitudes, identificadores de operación aprobados, clasificaciones de respuestas y las referencias remotas mínimas necesarias para la conciliación. Un registro que resuelve una investigación filtrando credenciales provoca por sí mismo el siguiente incidente.

## Los nombres de los resultados deben guiar el comportamiento seguro del agente

Una taxonomía de resultados solo merece su lugar cuando el entorno de ejecución del agente responde de forma distinta a cada resultado. Si todo resultado que no sea un éxito provoca un reintento inmediato, las etiquetas detalladas de auditoría son puro adorno.

Aplica estas reglas operativas:

- Reintenta automáticamente después de `no_effect` solo cuando la intención original siga autorizada y vigente.
- Reintenta después de `rejected` solo cuando el agente haya cambiado la entrada no válida o una persona haya resuelto el conflicto indicado.
- Concilia `unknown_effect` antes de reintentar cualquier acción que cambie el estado.
- Trata `partial_effect` como una tarea de limpieza o continuación, no como un punto de partida vacío.
- Escala el caso cuando la conciliación no pueda identificar una única operación remota coincidente.

La última regla es la que impacienta a los equipos. Quieren que el agente siga avanzando. Ese instinto es razonable para una solicitud de lectura, pero resulta imprudente en acciones que gastan dinero, modifican accesos, cambian producción o eliminan datos. Una acción sin resolver debe seguir visible hasta que una persona o una consulta remota confiable cierre la brecha de evidencia.

El flujo de aprobación tampoco debe ocultar esta diferencia. La persona que aprobó una acción aprobó un intento, no reintentos ilimitados después de un resultado desconocido. Si el siguiente intento puede crear un segundo efecto, muestra que es un reintento de una acción indeterminada y exige una decisión nueva cuando el riesgo lo requiera.

Construye este modelo antes de añadir más canales o más comportamiento autónomo. El primer tiempo de espera después de que un agente cambie algo importante es un mal momento para descubrir que tu registro de auditoría solo tiene dos resultados: éxito y lo que el cliente haya alcanzado a ver.
