# Délais d'attente des outils d'agent : des plans de récupération qui évitent les répétitions

Un délai d'attente est une observation sur le client, pas un verdict sur le travail. L'appelant a cessé d'attendre. C'est tout ce qu'il sait. Lorsqu'un agent transforme cette observation en « échec » et relance un appel qui modifie l'état, il peut créer un deuxième paiement, un deuxième déploiement, un deuxième ticket d'assistance ou une commande distante exécutée deux fois sur une machine déjà sous tension.

Les délais d'attente des outils d'agent exigent un plan de récupération, car les agents agissent plus vite que les personnes qui les supervisent et ont tendance à traiter la sortie des outils comme une vérité absolue. Une réponse manquante n'est pas une vérité absolue. Le chemin de récupération doit déterminer s'il faut relancer, attendre, rechercher des preuves ou s'arrêter pour demander une décision humaine. Concevez ce chemin avant d'autoriser un agent à effectuer des appels importants.

## Un délai d'attente laisse trois scénarios plausibles

Après un délai d'attente côté client, la requête correspond à l'un de trois grands scénarios : le service ne l'a jamais reçue, il l'a reçue mais ne l'a pas encore terminée, ou il l'a terminée sans que le client reçoive le résultat. Une panne réseau peut survenir avant l'ouverture de la connexion, pendant le transport du corps de la requête, pendant le traitement par le service ou pendant le retour de la réponse. Le même type d'exception peut recouvrir ces quatre situations.

Cette distinction change l'action suivante. Si la recherche DNS a échoué avant toute connexion, une nouvelle tentative peut être raisonnable. Si le service a accepté une requête de suppression d'une ressource et que la réponse a disparu, la relancer peut répéter une opération destructive. Si le service a placé un travail asynchrone en file d'attente, une deuxième soumission peut créer un travail concurrent alors que le premier est encore en cours.

HTTP ne fournit pas de bit magique indiquant au client quel scénario s'est produit. La RFC 9110 décrit les méthodes de requête et la signification des réponses, notamment la distinction entre méthodes sûres et idempotentes. Elle ne promet pas qu'un client puisse déduire l'exécution côté serveur à partir d'une réponse perdue. Cette limite est physique, et non due à une option manquante du SDK.

Les équipes confondent souvent deux questions distinctes :

- Cette requête peut-elle être renvoyée sans modifier l'état final voulu ?
- La tentative initiale a-t-elle réellement atteint le service et eu un effet sur lui ?

L'idempotence répond à la première question. La réconciliation répond à la seconde. Un système a besoin des deux. Un `PUT` idempotent peut être répété sans danger, mais un délai d'attente peut tout de même vous empêcher de savoir si le travail déclenché en aval par cette requête est terminé. Une consultation d'état peut établir le résultat, mais elle ne vous protège pas contre le travail en double si le service accepte deux créations impossibles à distinguer.

Les agents ont besoin de cette distinction dans leurs contrats d'outils. Une erreur en texte brut comme `request timed out` pousse à improviser. Un résultat structuré indiquant `outcome: unknown` signale à l'agent qu'il doit quitter la branche de nouvelle tentative et passer à la branche de recherche de preuves.

## Cartographiez le chemin de la requête avant de choisir une nouvelle tentative

Un plan de récupération utile nomme les limites où des preuves peuvent exister. Commencez par le processus de l'agent, puis examinez l'enveloppe de l'outil, le pool de connexions, la passerelle ou le proxy s'il y en a un, l'entrée du service, l'application, le stockage durable et les éventuels travailleurs qui traitent les tâches asynchrones. Un délai d'attente à une limite ne dit rien de fiable sur la limite suivante.

Prenons l'appel de création d'un déploiement. L'agent envoie une requête par l'intermédiaire d'un outil. Le client écrit le corps complet de la requête, le serveur valide un enregistrement de déploiement, puis la connexion se rompt avant que la réponse n'atteigne le client. L'outil signale un délai d'attente. L'agent relance la requête avec de nouvelles données. Le serveur possède alors deux enregistrements de déploiement, chacun valide de son propre point de vue.

Modifions maintenant un seul détail : le client expire pendant l'envoi du corps et le serveur rejette le corps partiel avant l'exécution du code applicatif. Le même outil peut tout de même renvoyer `timeout`. Dans ce cas, une nouvelle tentative peut créer exactement un déploiement. L'appelant ne peut pas distinguer ces deux situations à partir du seul délai d'attente.

Notez les preuves que chaque composant peut produire. Pour une action HTTP classique, elles comprennent :

- Les horodatages du client, la cible sélectionnée, l'empreinte du corps de la requête et un identifiant d'opération généré par l'appelant.
- Les journaux d'accès du service, qui indiquent si l'entrée a accepté la requête.
- Un enregistrement applicatif qui conserve l'identifiant d'opération avec un résultat validé.
- Les enregistrements des travailleurs ou des files d'attente pour les actions qui se poursuivent après la requête synchrone.
- Un point de terminaison de lecture qui renvoie l'état actuel ou l'état de l'opération.

Ne faites pas des journaux réseau votre seule source de vérité. Le journal d'un répartiteur peut montrer que des octets sont arrivés, mais il ne peut pas prouver que la transaction de base de données a été validée. Un enregistrement de base de données peut prouver une validation, mais pas nécessairement qu'un fournisseur externe a reçu l'effet secondaire suivant. L'enregistrement de référence doit correspondre à l'action que vous cherchez à prouver.

Pour l'envoi d'un e-mail, l'identifiant de message accepté par le fournisseur constitue une preuve plus solide que le journal de votre application indiquant « envoi imminent ». Pour une migration de base de données, une table de migration ou un enregistrement de transaction vaut mieux qu'un code de sortie de processus shell que l'appelant n'a jamais reçu. Pour la création d'une ressource cloud, une URL d'opération ou une étiquette de ressource contenant un identifiant généré par l'appelant est préférable à une nouvelle requête de création.

## L'idempotence doit appartenir à l'opération, pas à la tentative

Un mécanisme de nouvelle tentative ne fonctionne que si chaque tentative d'une même action voulue porte le même identifiant durable. Générez cet identifiant avant le premier appel réseau. Conservez-le avec la description de l'action. Réutilisez-le après un redémarrage du processus ou de l'outil, ou après un transfert à un opérateur humain.

Ne générez pas un nouvel UUID dans une boucle de nouvelles tentatives. Ce schéma paraît rigoureux lors d'une revue de code, mais il annule tout l'objectif recherché. Le serveur voit chaque tentative comme une nouvelle requête, ce qui produit précisément les opérations en double.

Une requête peut transporter une valeur d'idempotence dans un en-tête ou dans un champ du corps, selon l'API. Le détail du transport compte moins que la règle appliquée par le serveur. Le serveur doit associer atomiquement cette valeur à l'opération et à son résultat. Si deux requêtes identiques arrivent en même temps, il doit les sérialiser ou faire en sorte que l'une observe l'autre. Un cache qui expire avant la fin des nouvelles tentatives retardées n'offre pas une protection fiable contre les doublons.

Une requête HTTP concrète pourrait ressembler à ceci :

```http
POST /deployments HTTP/1.1
Content-Type: application/json
Idempotency-Key: op_7d5d4d8e4e5a
X-Correlation-ID: run_42_task_9

{"repository":"api","revision":"a1b2c3d4","environment":"staging"}
```

Le serveur doit conserver la valeur d'idempotence avec une empreinte des champs significatifs de la requête et l'identifiant du déploiement ou de l'opération obtenu. Si la même valeur arrive avec une autre révision ou un autre environnement, il faut la rejeter. Renvoyer le premier résultat pour une charge utile différente appliquerait silencieusement la mauvaise intention.

Pour une opération asynchrone, renvoyez une référence d'opération durable dès que le serveur accepte le travail :

```json
{
  "operation_id": "dep_1842",
  "state": "accepted",
  "status_url": "/operations/dep_1842"
}
```

Après un délai d'attente, l'agent interroge `op_7d5d4d8e4e5a` ou `dep_1842` avant d'envisager une nouvelle soumission. Si l'API ne prend en charge ni valeur d'idempotence ni recherche par référence externe, considérez par conception l'écriture comme ambiguë. Cela peut convenir à une ressource de test jetable. C'est un mauvais choix pour une action autonome qui crée des coûts ou modifie l'état de production.

Ne considérez pas une méthode comme sûre simplement parce qu'elle utilise `POST` avec une bibliothèque de nouvelles tentatives. Les noms des méthodes HTTP donnent une indication sur la sémantique attendue, mais ne protègent pas contre une implémentation serveur qui duplique le travail. Consultez la documentation précise de l'API, puis testez vous-même le comportement en cas de doublon.

## Donnez aux agents un état explicite pour les issues inconnues

Un agent ne doit pas recevoir uniquement `success` ou `error` d'un outil capable d'agir sur le monde extérieur. Il lui faut un troisième résultat : `unknown`. Cet état empêche le comportement le plus dangereux d'un modèle, qui consiste à traiter un échange incomplet comme une autorisation d'essayer une version légèrement différente de la même commande.

Utilisez un contrat de résultat qui indique la phase ayant échoué, tout en reconnaissant que cette phase peut rester incertaine. Par exemple :

```json
{
  "outcome": "unknown",
  "operation_id": "op_7d5d4d8e4e5a",
  "correlation_id": "run_42_task_9",
  "transport_observation": "response deadline exceeded after request write",
  "retry_allowed": false,
  "reconcile": {
    "method": "GET",
    "path": "/operations/by-id/op_7d5d4d8e4e5a"
  }
}
```

Le champ `retry_allowed` doit provenir de l'outil ou de la définition de l'action, et non d'une supposition de l'agent sur le sens des verbes. Un agent ne peut pas déduire sans risque que `create_release` est inoffensif simplement parce que la cible est un environnement de staging. Un déploiement de staging peut tout de même envoyer des notifications, consommer un quota partagé ou modifier un canal de publication.

Faites suivre à l'agent une séquence de récupération limitée :

1. Conserver dans l'enregistrement d'exécution l'action prévue, l'identifiant d'opération, la cible et l'observation liée au délai d'attente.
2. Interroger la source d'état de référence avec le même identifiant d'opération ou une référence fournie par le service.
3. Poursuivre uniquement après confirmation d'un résultat terminal. Relancer seulement si la définition de l'action l'autorise et si la source d'état ne montre aucune opération acceptée.
4. S'arrêter et présenter les preuves lorsque le service ne peut pas établir le résultat dans le délai de récupération prévu pour l'action.

La condition d'arrêt est importante. Un agent qui interroge indéfiniment monopolise l'attention et peut maintenir une tâche active longtemps après la disparition de son objectif initial. Un agent qui essaie cinq variantes d'une requête d'écriture peut créer un chantier de nettoyage pour quelqu'un d'autre. Donnez à chaque opération une échéance de récupération, distincte de l'échéance de la requête.

Une approbation humaine ne résout pas à elle seule une issue inconnue. L'approbation répond à « cet appelant peut-il tenter cette action ? ». Elle ne répond pas à « la tentative précédente a-t-elle réussi ? ». Conservez séparément les preuves d'autorisation et les preuves d'exécution, dans l'interface comme dans les journaux.

## Les délais d'attente des lectures ne se traitent pas comme ceux des écritures

Une lecture qui expire présente généralement moins de risques de doublon, mais elle peut tout de même conduire l'agent à prendre une mauvaise décision. Il peut expirer pendant l'énumération des ressources, recevoir ailleurs une réponse incomplète ou obsolète depuis un cache et conclure qu'une ressource n'existe pas. Il tente alors une création qui entre en collision avec la réalité.

Classez les lectures selon la décision qu'elles doivent permettre. Un simple rafraîchissement de tableau de bord peut être relancé avec un délai progressif limité. Une lecture utilisée pour décider d'une écriture doit suivre une règle de cohérence explicite. Si le service propose un ETag, un numéro de version, un numéro de génération ou un point de terminaison indiquant l'état après écriture, utilisez-le. S'il ne propose qu'une cohérence éventuelle, rendez visibles pour l'agent la durée d'attente et la condition d'échec.

Évitez un nombre universel de tentatives. Une courte recherche de métadonnées peut tolérer deux tentatives rapides. Une requête de rapport qui charge un entrepôt peut nécessiter une longue échéance et aucune répétition immédiate. Un appel qui renvoie `429 Too Many Requests` ou une indication explicite de nouvelle tentative du service doit être traité autrement qu'un délai d'attente de socket. Traiter chaque erreur comme un problème réseau temporaire est une façon pour un agent de transformer une panne partielle en charge évitable.

Le délai progressif aide à protéger les services, mais il ne résout pas l'ambiguïté. Il espace davantage les requêtes en double. Associez-le à une valeur d'idempotence ou à une vérification d'état pour chaque écriture importante.

Utilisez des écritures conditionnelles lorsque l'API les prend en charge. Une requête `If-Match` avec un ETag connu peut empêcher un agent d'écraser une ressource modifiée depuis sa lecture. Une création qui accepte un identifiant de ressource choisi par le client peut faire converger les tentatives vers un seul objet. Ces mécanismes protègent les transitions d'état, mais ne remplacent pas l'enregistrement des effets secondaires produits en dehors de cette ressource.

## SSH dissimule l'exécution distante derrière un flux interrompu

La récupération après un délai d'attente SSH exige davantage de prudence que dans le cas de HTTP. Une session SSH perdue peut survenir après le démarrage d'une commande sur l'hôte distant, pendant le transfert de la sortie, après la fin de la commande ou alors qu'un processus enfant continue après la déconnexion de son parent. Un message du shell local ne permet pas de savoir lequel de ces scénarios s'est produit.

Le schéma dangereux est celui d'une longue commande composée :

```sh
ssh deploy@host 'download-release \u0026\u0026 migrate-db \u0026\u0026 restart-service'
```

Si la connexion tombe après `migrate-db`, relancer la commande complète peut exécuter deux fois les migrations ou redémarrer un service dont la nouvelle version n'a jamais fini d'être téléchargée. La transcription du terminal a réduit plusieurs transitions d'état à un résultat opaque.

Découpez le travail distant en opérations dotées de marqueurs durables et consultables. Un déploiement peut enregistrer un identifiant de version avant de commencer, conserver les versions de migration dans la base de données et exposer la révision active par une commande d'état locale. Un outil de récupération se reconnecte et demande ces marqueurs avant toute autre action.

Par exemple, un agent peut utiliser une commande d'état distante dont la sortie est conçue pour les machines plutôt que pour les humains :

```sh
ssh deploy@host '/usr/local/bin/release-status --json'
```

```json
{
  "release_id": "rel_202",
  "phase": "migrated",
  "active_revision": "9f24c1",
  "migration_version": "20250308_02"
}
```

L'action de récupération dispose alors d'une base pour décider. Si `phase` vaut `migrated`, ne relancez pas les migrations. Si l'hôte ne renvoie aucun `release_id`, l'agent ne peut démarrer l'opération que si la commande distante garantit que cette absence signifie qu'aucune exécution précédente n'a eu lieu. Si SSH ne peut pas se reconnecter, le résultat reste inconnu. Ne remplacez pas cette incertitude par une nouvelle tentative optimiste lorsque l'action modifie un hôte de production.

Utilisez les verrous distants avec prudence. Un verrou peut empêcher les exécutions simultanées, mais un verrou obsolète après une panne de l'hôte peut bloquer la récupération. Placez l'identifiant d'opération et une règle d'expiration dans l'enregistrement du verrou, et rendez son inspection possible sans le supprimer aveuglément. Une commande de nettoyage qui supprime tous les anciens verrous est elle aussi une opération qui modifie l'état et nécessite ses propres preuves.

Sallyport peut garder les identifiants SSH hors du processus de l'agent pendant que son utilitaire `sp-ssh` intégré effectue la connexion, mais l'isolation des identifiants ne rend pas sûre la relecture d'une session interrompue. La commande distante a toujours besoin d'un identifiant d'opération, de marqueurs durables et d'un moyen de réconciliation.

## Les journaux d'audit aident à enquêter, mais ne prouvent pas l'achèvement

Un journal d'action doit conserver suffisamment de détails pour reconstituer l'intention et la récupération sans stocker de secrets. Enregistrez la session de l'appelant, l'heure, l'identité de la cible, l'identifiant d'opération, l'empreinte de la requête ou le modèle de commande, l'événement d'autorisation, le résultat du transport et le résultat final vérifié. N'enregistrez pas de jetons porteurs, de clés privées ni de corps de requête bruts susceptibles de contenir des identifiants ou des données personnelles.

Séparez une action tentée d'une action terminée. Une ligne indiquant `POST /deployments timeout` est un enregistrement de tentative. Une requête ultérieure renvoyant une opération à l'état `succeeded` est une preuve d'achèvement. Conservez les deux. Remplacer le premier enregistrement par une réussite finale efface la partie la plus utile de l'incident : la période pendant laquelle l'appelant ne savait pas.

La détection des falsifications compte lorsqu'une exécution d'agent donne lieu à un litige. Vous devez pouvoir répondre à ces questions : quel processus a effectué l'appel, quelle action était-il autorisé à effectuer, a-t-il reçu un résultat et comment l'équipe a-t-elle établi l'état final ? Une table d'activité modifiable est facile à interroger, mais constitue une preuve faible si un processus compromis peut réécrire l'historique.

Sallyport enregistre les sessions d'agent et les appels individuels dans un journal d'audit chiffré, insensible à l'écriture et chaîné par hachage, et `sp audit verify` peut vérifier la chaîne hors ligne sur le texte chiffré. Cet enregistrement peut montrer l'action de la passerelle et l'historique de l'appelant, tandis que le service distant ou l'hôte reste la source de référence pour déterminer si le travail voulu est terminé.

Ne confondez pas un événement d'audit de la passerelle avec une transaction applicative. Si la passerelle a enregistré une requête sortante, celle-ci peut avoir échoué avant la validation par le service. Si le service l'a validée, la passerelle peut ne jamais avoir reçu la réponse. L'enquête fonctionne lorsque les enregistrements des deux côtés partagent un identifiant d'opération ou de corrélation.

## Testez volontairement l'ambiguïté avant qu'une panne ne le fasse à votre place

Un plan de gestion des délais d'attente qui n'a jamais été confronté à une réponse perdue repose sur une hypothèse. Testez précisément le cas où le service termine l'opération mais où le client perd le résultat. C'est le cas que la plupart des équipes oublient, car les doubles de test ordinaires ne savent pas l'exprimer.

Construisez un point de terminaison de test ou un proxy qui accepte une requête, valide son enregistrement durable, puis retarde ou supprime la réponse. Envoyez deux fois le même identifiant d'opération. Vérifiez que le service renvoie une seule opération logique, que l'agent consulte l'état après le délai d'attente et que la piste d'audit conserve les deux tentatives ainsi que le résultat de la réconciliation.

Testez ensuite le cas inverse : interrompez la requête avant que le service ne l'accepte. Vérifiez que la récupération ne peut relancer la requête qu'après avoir constaté l'absence d'enregistrement d'opération. Les deux tests peuvent produire la même exception côté client. Le contrat de votre outil doit conduire à des actions différentes, car les preuves côté service diffèrent.

Testez également les situations suivantes :

- Le service accepte l'opération, mais son travailleur reste en attente au-delà de l'échéance de récupération de l'agent.
- Deux processus d'agent soumettent le même identifiant d'opération presque au même moment.
- Le point de terminaison d'état est indisponible alors que le point de terminaison d'écriture principal fonctionne.
- Une commande SSH démarre un processus enfant, puis la connexion se termine avant la sortie finale.
- Un humain reprend une exécution suspendue après qu'un autre opérateur a déjà réconcilié l'action.

Le dernier cas révèle un problème courant en exploitation : l'état de récupération doit vivre en dehors de la mémoire conversationnelle de l'agent. Conservez l'identifiant d'opération et le résultat actuel dans un enregistrement d'exécution durable. Un agent redémarré doit lire cet enregistrement et poursuivre la réconciliation, plutôt que d'inventer une nouvelle action parce qu'il ne voit plus sa transcription précédente.

Déclenchez des alertes pour les issues inconnues qui dépassent leur délai de récupération. N'alertez pas à chaque premier délai d'attente si des nouvelles tentatives ordinaires résolvent les lectures sûres. Alertez lorsqu'une écriture importante n'a pas d'état terminal confirmé, lorsque des identifiants d'opération en double portent des charges utiles différentes ou lorsque les marqueurs distants contredisent la progression attendue. Ce sont les cas qui exigent l'intervention d'une personne avant que l'agent ne poursuive.

## Un plan de récupération doit rendre le refus ordinaire

Le comportement le plus solide après un délai d'attente est souvent le refus : « Je ne peux pas confirmer que la demande de déploiement est terminée, je ne vais donc pas en soumettre une autre. » Ce n'est pas une défaillance de l'outil. C'est la bonne réponse lorsque des preuves manquent autour d'une action irréversible ou coûteuse.

Rendez cette réponse utile. Affichez l'identifiant d'opération, la cible, le dernier état confirmé, les horodatages et la requête d'état exacte ou l'inspection distante qui permettrait de trancher. Si aucune requête de référence n'existe, dites-le clairement et transmettez la décision à une personne qui comprend les conséquences d'un doublon.

Les équipes résistent à cette approche parce qu'une nouvelle tentative semble productive et qu'une pause paraît lente. Après suffisamment d'écritures en double et de déploiements inachevés, le compromis devient évident. Une minute consacrée à la réconciliation coûte moins cher que la découverte de deux systèmes convaincus d'avoir chacun exécuté l'unique action demandée à l'agent.

Commencez par les écritures qui déclenchent des mouvements d'argent, des messages externes, des mises en production, des changements d'accès et des suppressions de données. Pour chacune, exigez trois réponses du responsable de l'API : quel identifiant lie les nouvelles tentatives à une seule opération, où l'appelant peut-il consulter le résultat et quelle preuve existe lorsque la connexion meurt ? Si une réponse manque, faites renvoyer `unknown` par l'outil et exigez une décision humaine délibérée au lieu d'apprendre à l'agent de deviner.
