# Como registrar ações canceladas de agentes sem mentir

Um log de auditoria que classifica toda ação cancelada do agente como «falhou» está mentindo. O agente pode ter parado de esperar, mas a linha no banco de dados talvez já exista, a implantação pode já estar em andamento ou o comando remoto ainda pode estar sendo executado depois que a conexão desapareceu.

A solução não é criar uma lista maior de códigos de falha. É adotar um modelo de resultados que separe o que o gateway observou do que aconteceu no destino. Os investigadores precisam distinguir entre uma ação que nunca saiu da máquina, uma que foi rejeitada pelo destino, uma que terminou com uma confirmação e uma cujo efeito continua desconhecido.

## O resultado do chamador não é o resultado da ação

O chamador vê apenas uma parte limitada da ação: envia o trabalho e espera uma resposta. A própria ação atravessa vários sistemas que não compartilham relógio, ciclo de vida do processo ou um caminho de retorno confiável. Quando o chamador cancela, se desconecta ou chega ao prazo final, ele aprende algo sobre a própria espera. Isso não significa que tenha aprendido algo definitivo sobre o efeito remoto.

Essa distinção é mais importante em trabalhos que alteram estado. Criar um ticket, emitir um reembolso, aplicar mudanças de infraestrutura, excluir um objeto, alternar um token de acesso e executar um comando remoto são ações cujas consequências podem sobreviver à ausência de uma resposta. Se o seu diário grava `failed` porque o processo do agente terminou primeiro, uma nova tentativa posterior pode criar um segundo ticket, emitir um segundo reembolso ou executar duas vezes o comando destrutivo.

Operações de leitura também exigem honestidade, embora o risco seja diferente. Uma busca cancelada pode devolver uma visão incompleta, e o agente pode tomar uma decisão de acompanhamento equivocada. Em geral, ela não altera o mundo por si só. Um POST, PATCH, DELETE ou comando de shell remoto pode alterar.

Mantenha três coisas separadas em cada registro:

- **Disposição do chamador:** concluída, cancelada, desconectada ou expirada.
- **Evidência de envio:** nunca iniciada, iniciada localmente, bytes entregues ao transporte ou confirmação recebida do lado remoto.
- **Resultado do efeito:** sem efeito, bem-sucedido, rejeitado, parcialmente concluído ou efeito desconhecido.

As equipes costumam juntar o primeiro e o terceiro campos porque uma única coluna de status é conveniente. Essa conveniência dura até a revisão de um incidente. Então alguém precisa explicar por que «solicitação cancelada» aparece ao lado de um objeto que existe claramente em produção.

A regra prática é direta: escreva **sem efeito** somente quando as evidências excluírem a execução. Escreva **efeito desconhecido** quando a execução continuar plausível e você não tiver um resultado confiável. Desconhecido não é uma lacuna constrangedora no log. É o resultado correto de uma ação distribuída com o caminho de observação interrompido.

## Registre o limite das evidências, não uma história inventada

Toda ação precisa de um limite explícito depois do qual o gateway já não pode prometer honestamente que nada aconteceu. Podemos chamá-lo de limite de envio. No HTTP, esse limite pode ser o momento em que a solicitação é confirmada em uma conexão e entregue ao sistema operacional, ou quando um serviço upstream confirma que a aceitou. No SSH, pode ser o momento em que o auxiliar envia a solicitação de comando por um canal autenticado.

Não finja que um único booleano `sent=true` resolve a questão. Escritas locais podem ser armazenadas em buffer. Uma biblioteca de transporte pode informar uma escrita antes que o aplicativo remoto leia os dados. Um peer pode receber uma solicitação, aplicar a mudança e perder a resposta no caminho de volta. O log deve descrever a evidência mais forte disponível, sem transformar um detalhe de implementação em prova.

Um registro de ação útil tem identificadores imutáveis e uma sequência de observações. Este formato compacto funciona tanto para uma chamada de API quanto para a execução de um 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"
}
```

A impressão digital da solicitação identifica o que foi tentado sem colocar no diário uma credencial bearer, o corpo bruto da solicitação ou um argumento de comando secreto. O identificador deve permanecer estável entre novas tentativas e reconciliações posteriores. Se um operador não conseguir ligar a solicitação original, sua nova tentativa e o objeto remoto final, a trilha de auditoria não responderá à pergunta que importa.

Há uma diferença importante entre `dispatch_started` e `transport_write_completed`. O primeiro diz que o gateway iniciou a operação. O segundo diz que o transporte local aceitou os dados de saída. Nenhum dos dois diz que o aplicativo remoto os executou. Se sua implementação não conseguir distingui-los, registre o fato mais fraco e deixe isso claro na base do resultado.

## O cancelamento antes do envio pode significar sem efeito

Um cancelamento pode sustentar a conclusão de que não houve efeito, mas somente antes de o gateway confirmar a ação em um canal externo. Este é o caso simples: o agente revoga a solicitação enquanto ainda espera uma autorização local, antes de o cofre ser desbloqueado, antes de uma solicitação HTTP começar ou antes de um comando SSH ser entregue ao auxiliar de transporte.

A entrada de auditoria deve mostrar por que a conclusão é segura. «Cancelada», sozinha, não informa ao investigador em que ponto ocorreu o cancelamento. Registre uma etapa e um ponto de prova local.

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

Esse resultado também é apropriado quando uma barreira local nega a ação antes que qualquer solicitação externa possa começar. Uma aprovação ausente, um cofre bloqueado, uma sessão revogada ou uma solicitação local inválida podem produzir sem efeito, desde que o gateway nunca tenha enviado o trabalho. O diário deve distinguir uma solicitação negada de uma solicitação cancelada, pois elas contam histórias diferentes sobre controle humano e comportamento do agente, mas ambas podem afirmar com segurança que o destino não viu nada.

Não aplique esse rótulo depois de criar uma conexão e começar a escrever apenas porque a chamada de transporte retorna um erro de cancelamento. Muitas bibliotecas usam o mesmo valor de erro para vários caminhos: cancelamento enquanto estava na fila, durante uma escrita, enquanto aguardava os cabeçalhos da resposta ou durante a leitura do corpo. Esses casos não são equivalentes.

A propagação do cancelamento tem uma finalidade importante, mas não é uma máquina do tempo. As orientações de cancelamento do gRPC dizem que o cancelamento pelo cliente sinaliza que ele já não precisa do resultado da RPC e recomendam que os servidores interrompam o trabalho e propaguem o cancelamento para as tarefas downstream. Isso é uma boa prática de uso de recursos. Não prova que um efeito anterior foi desfeito e não pode desfazer uma escrita confirmada de forma independente.

Se o serviço receptor oferecer um endpoint explícito de cancelamento vinculado a um identificador de operação, registre-o como uma segunda ação. O resultado dessa ação só pode alterar o resultado do efeito original se o serviço fornecer uma declaração confiável sobre a operação original. Uma solicitação de cancelamento de melhor esforço que retorna depois de uma interrupção de rede cria outro efeito desconhecido. Ela não limpa a primeira ação por mágica.

## Um timeout depois do envio tem efeito desconhecido

Um prazo final é um limite local para a espera. Não é um veredito sobre a execução remota. Depois que uma ação atravessa o limite de envio, um timeout deve resultar por padrão em efeito desconhecido, a menos que uma confirmação do protocolo ou uma verificação posterior prove algo mais.

É fácil errar com HTTP porque os rótulos de status parecem definitivos. A RFC 9110 diz que 408 significa que o servidor não recebeu uma mensagem de solicitação completa dentro do tempo que estava disposto a esperar. 504 significa que um gateway não recebeu uma resposta oportuna de um servidor upstream. As duas afirmações descrevem um observador específico e uma troca específica. Nenhuma prova que outro sistema não tenha agido sobre dados já recebidos.

Considere um agente que envia `POST /v1/releases` com um prazo de 30 segundos. A API valida a solicitação, insere uma linha de release, pede ao controlador de implantação que inicie o processo e então trava ao formatar a resposta. Aos 30 segundos, o agente vê um timeout. A release existe. Uma nova tentativa sem um token de idempotência pode criar outra release, mesmo que a primeira chamada esteja marcada como «falhou» na transcrição do agente.

O registro honesto seria:

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

Não use `failed` como atalho para desconhecido. Reserve os resultados de falha para fatos que você consegue estabelecer: um serviço remoto devolveu um erro de validação, um comando retornou um status de saída diferente de zero, não foi possível estabelecer uma conexão antes de qualquer solicitação sair do gateway ou uma decisão de autorização local negou a execução. Depois que o envio de saída pode ter ocorrido, um timeout não atende a esse padrão.

A conclusão parcial merece seu próprio resultado quando o lado remoto fornece evidências. APIs em lote e scripts frequentemente fazem parte do trabalho antes de falhar. Se um serviço retornar uma lista de IDs de objetos concluídos seguida de um erro, registre `partial_effect`, preserve os IDs dos objetos quando a política permitir e capture o motivo informado pelo serviço. Chamar isso simplesmente de «falhou» esconde exatamente o trabalho de limpeza que o operador precisa fazer.

## Confirmações perdidas precisam de um registro separado

Uma confirmação perdida ocorre depois que o receptor pode ter agido, mas antes que o gateway receba uma confirmação final. Isso acontece com frequência suficiente para merecer uma observação nomeada, em vez de um erro de rede genérico.

A sequência costuma parecer normal até o último momento:

1. O gateway autoriza e envia uma ação.
2. O serviço remoto a aceita e executa ou coloca em fila o trabalho solicitado.
3. A resposta atrasa, a conexão cai ou o processo local termina.
4. O gateway não tem uma prova durável da conclusão, embora o sistema remoto possa ter essa prova.

O primeiro erro é sobrescrever a entrada original quando uma consulta posterior tem sucesso. Isso faz o diário parecer dizer que o gateway conhecia o resultado naquele momento. O investigador precisa dos dois fatos: a chamada inicial terminou sem confirmação e uma reconciliação posterior encontrou um resultado remoto correspondente.

Acrescente uma observação:

```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"
}
```

A disposição original do chamador continua sendo `timed_out`. Não a reescreva como `completed`. O chamador realmente atingiu o timeout. O sistema descobriu depois que a ação remota teve sucesso. Esses fatos coexistem sem contradição.

Uma confirmação remota só é tão confiável quanto sua correlação. Fazer a correspondência pelo nome de um objeto, pelo horário atual ou pela linguagem natural fornecida pelo agente é fraco. Prefira um token de idempotência aceito pelo destino, um ID de operação retornado antes do início de um trabalho demorado ou um ID de solicitação do provedor que o destino garanta ser exclusivo para aquela solicitação. Se o destino não oferecer nada disso, use uma consulta de leitura restrita e registre por que ela é suficiente ou por que continua ambígua.

Por exemplo, descobrir um novo usuário chamado `build-bot` não prova qual solicitação de criação o produziu. Encontrar um objeto com um token de solicitação armazenado igual ao token original da ação é muito melhor. Essa diferença determina se é possível repetir a ação com segurança.

## A idempotência transforma a recuperação em uma verificação, não em uma aposta

Idempotência não é uma permissão para repetir anexada depois do fato. É um contrato estabelecido antes do envio. O cliente fornece um token estável, e o serviço garante que solicitações repetidas com esse token identifiquem a mesma operação lógica, em vez de criar novos efeitos.

Para toda integração HTTP que altera estado, faça ao responsável pelo serviço quatro perguntas diretas:

- Ele aceita um token de idempotência fornecido pelo chamador?
- Quais campos da solicitação precisam permanecer iguais quando o token se repete?
- Por quanto tempo ele mantém o vínculo entre o token e o resultado?
- O chamador pode recuperar o resultado original depois de perder a resposta?

Se as respostas forem vagas, não apresente novas tentativas automáticas como seguras. «Nós normalmente deduplicamos» não é um contrato. Uma expulsão do cache, um failover regional ou uma mudança no analisador de solicitações pode transformar essa suposição em trabalho duplicado.

Quando o destino não tiver suporte à idempotência, divida uma operação arriscada sempre que possível. Crie um rascunho durável com uma referência externa exclusiva, verifique esse rascunho e então emita o comando irreversível usando o identificador retornado. Isso não torna toda operação segura, mas cria um ponto de reconciliação antes da parte cara ou destrutiva.

Um fluxo de implantação ilustra a diferença. Um endpoint único que cria e inicia uma release deixa pouco espaço para recuperação diante de uma confirmação perdida. Um fluxo com duas chamadas pode criar uma release usando o ID da ação como referência externa, consultar essa referência depois de um timeout e iniciá-la somente quando o chamador tiver um ID de release conhecido. A chamada extra costuma custar menos do que explicar uma mudança inesperada em produção.

Nunca gere um novo token de idempotência para uma nova tentativa automática da mesma ação. Um token novo declara que a tentativa é uma nova solicitação lógica. Isso pode estar correto quando uma pessoa repete o trabalho intencionalmente, mas destrói a deduplicação durante a recuperação. Mantenha o token original no registro da ação e faça a nova tentativa citar o ID da ação pai.

Um contrato de idempotência também ajuda na resposta a incidentes. Os investigadores podem fazer uma pergunta concreta: «O que o destino decidiu para o token X?» Sem esse contrato, precisam inferir a intenção a partir de horários, logs e nomes. Isso é lento, sujeito a erros e muitas vezes impossível depois que os períodos de retenção terminam.

## O fechamento do SSH e a conclusão do comando são fatos diferentes

O SSH incentiva um erro semelhante porque uma sessão fechada parece indicar que o comando parou. Não indica. Uma falha de rede pode interromper o cliente enquanto o processo remoto continua sob o shell pai, um supervisor ou um gerenciador de serviços. Da mesma forma, um processo pode terminar enquanto o cliente perde o relatório de saída.

A RFC 4254 trata o fechamento do canal, o fim do arquivo e o status de saída como eventos de protocolo distintos. Ela recomenda retornar um status de saída quando um comando remoto termina, mas não transforma o fechamento do canal em prova de que você recebeu esse status. A especificação também diz que os peers trocam mensagens de fechamento antes de cada lado considerar o canal fechado. Isso informa o estado do canal SSH, não se o comando remoto alterou o sistema de arquivos ou um serviço antes de o canal morrer.

Registre as evidências do SSH como observações 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"
}
```

Se você receber um status de saída válido e um fechamento completo do canal depois da saída do comando, terá uma evidência forte sobre o processo do comando, embora não uma prova absoluta de todos os efeitos externos que ele desencadeou. Um script pode enviar trabalho assíncrono com sucesso e terminar com código zero antes de esse trabalho acabar. Registre o resultado do comando como bem-sucedido e modele o trabalho externo como sua própria operação se o sistema remoto fornecer um ID.

Os comandos precisam de um plano de recuperação antes de o agente executá-los. Prefira comandos que exibam ou gravem um identificador de operação durável. Para reiniciar um serviço, consulte o gerenciador de serviços usando um nome de unidade conhecido e um estado antes e depois. Para uma migração de banco de dados, consulte o registro de migrações em vez de confiar na saída do terminal. Para uma operação de arquivo, verifique um hash do conteúdo e um marcador de versão ou geração, não apenas se o caminho existe.

Evite tratar um sinal enviado a um auxiliar local como prova de que o processo remoto parou. O sinal pode chegar antes do envio remoto, depois de o comando remoto terminar ou depois de a conexão cair. Registre o sinal como um evento do chamador ou do gateway. Só altere o resultado do efeito quando o endpoint remoto fornecer evidências.

## A investigação começa pela cronologia, não por um rótulo final

Um investigador deve conseguir reconstruir uma ação indeterminada sem adivinhar qual linha do log veio primeiro. Isso exige um ID de ação estável, um ID de sessão, ordenação monotônica dos eventos dentro do gateway e horários dos eventos observados. O horário do relógio ajuda na correlação, mas pode variar entre sistemas. Não baseie toda a conclusão no fato de dois relógios concordarem até o milissegundo.

Uma boa investigação faz estas perguntas nesta ordem:

1. Quem autorizou a ação e qual processo do agente fez a solicitação?
2. O gateway atravessou seu limite de envio?
3. Quais observações de transporte ocorreram depois do envio?
4. Alguma confirmação remota confiável chegou?
5. Se não, qual consulta de reconciliação pode identificar a operação lógica original?

Essa sequência evita um hábito ruim: procurar primeiro nos logs remotos, encontrar um evento parecido e declarar que ele é a resposta. Comece pela ação pretendida e pelo material de correlação. Depois avalie se as evidências remotas correspondem exatamente àquela operação.

O registro de auditoria precisa resistir a alterações silenciosas. Se um operador puder mudar `unknown_effect` para `succeeded` sem preservar o estado anterior e a base da atualização, o registro se torna uma afirmação, não uma evidência. Registros somente para acréscimo, encadeamento por hash e verificação offline tornam mais difícil ocultar edições posteriores ao incidente. Eles não provam que todo sistema remoto disse a verdade, mas preservam o que o gateway observou e quando descobriu algo novo.

O Sallyport mantém as execuções dos agentes e as ações individuais em diários separados, projetados a partir de um único log de auditoria criptografado e encadeado por hash, e `sp audit verify` pode verificar a cadeia offline sobre o texto cifrado. Esse design é útil aqui porque uma reconciliação posterior pode ser registrada como um novo fato sem apagar o timeout ou a desconexão originais.

Não coloque corpos de solicitações que contenham segredos em uma trilha de auditoria apenas para melhorar a resposta a incidentes. Armazene destinos sanitizados, impressões digitais das solicitações, identificadores de operação aprovados, classificações de resposta e as referências remotas mínimas necessárias para a reconciliação. Um log que resolve uma investigação ao vazar credenciais cria o próximo incidente.

## Os nomes dos resultados devem orientar um comportamento seguro do agente

Uma taxonomia de resultados só merece existir quando o ambiente de execução do agente reage de forma diferente a cada resultado. Se todo resultado que não é sucesso levar a uma nova tentativa imediata, os rótulos detalhados de auditoria serão apenas decoração.

Use estas regras operacionais:

- Repita automaticamente depois de `no_effect` somente quando a intenção original ainda estiver autorizada e atual.
- Repita depois de `rejected` somente quando o agente alterar a entrada inválida ou uma pessoa resolver o conflito informado.
- Faça a reconciliação de `unknown_effect` antes de repetir qualquer ação que altere estado.
- Trate `partial_effect` como uma tarefa de limpeza ou continuação, não como uma folha em branco.
- Escale o caso quando a reconciliação não conseguir identificar uma única operação remota correspondente.

A última regra é onde as equipes ficam impacientes. Elas querem que o agente continue avançando. Esse impulso é razoável para uma solicitação de leitura, mas é imprudente em ações que gastam dinheiro, alteram acessos, mudam a produção ou excluem dados. Uma ação não resolvida deve continuar visível até que uma pessoa ou uma consulta remota confiável feche a lacuna nas evidências.

Seu fluxo de aprovação também não deve esconder essa distinção. Uma pessoa que aprovou uma ação aprovou uma tentativa, não novas tentativas ilimitadas depois de um resultado desconhecido. Se a próxima tentativa puder criar um segundo efeito, mostre que ela é uma nova tentativa de uma ação indeterminada e exija uma decisão nova quando o risco justificar.

Construa este modelo antes de adicionar mais canais ou mais comportamento autônomo. O primeiro timeout depois de o agente alterar algo importante é um péssimo momento para descobrir que seu log de auditoria tem apenas dois resultados: sucesso e aquilo que o cliente por acaso conseguiu ver.
