Una prenotazione, due registri, una risposta che non arriva
Un agente AI deve prenotare un ricambio per una macchina. Scrive nel proprio registro che la richiesta è stata gestita, poi chiama il magazzino. Tra i due passaggi il processo si interrompe. Al riavvio trova «fatto» e non riprova: il ricambio non è mai stato prenotato. Invertiamo l’ordine: il magazzino prenota, ma la risposta si perde prima che l’agente aggiorni il registro. Questa volta riprovare può prenotare due pezzi. Lo stesso messaggio mancante conduce a due errori opposti.
Abstract. La domanda è come separare intenzione registrata, effetto remoto e conferma osservata. Costruiamo un modello eseguibile con due database SQLite indipendenti, tre protocolli e cinque punti di interruzione. Mostriamo perché una outbox protegge l’intenzione, ma non basta da sola a impedire duplicati; serve anche un contratto del destinatario. Il risultato riguarda quindici casi deterministici, non una misura di affidabilità industriale. Non eseguiamo ordini, prenotazioni o chiamate a servizi esterni.
Il confine che un testo convincente non può eliminare
Una transazione locale rende indivisibile un insieme di modifiche nello stesso sistema: o vengono confermate insieme, oppure non risultano applicate. Non rende automaticamente indivisibile una scrittura in un altro servizio. Il modello linguistico può scegliere correttamente il ricambio e generare JSON perfetto, ma questo non estende la transazione oltre il suo confine. Il problema nasce dal protocollo di esecuzione, non dalla qualità della frase «operazione completata».
Nel nostro esempio chiamiamo I l’intenzione persistente e N il numero degli effetti nel magazzino simulato. L’obiettivo, per una richiesta accettata che può essere completata, è N=1. N=0 indica un effetto mancante; N=2 un duplicato. Sono conteggi senza unità fisica, non probabilità. Lo stato locale «done» descrive ciò che il protocollo ha registrato: finché non definiamo come viene prodotto, non dimostra da solo nessuno di quei valori.
La outbox conserva qualcosa da fare, non una prova di esecuzione
La prima modifica consiste nel registrare insieme la decisione applicativa e un messaggio ancora da consegnare, nella stessa transazione locale. Questa coda persistente è la outbox. Un processo di inoltro, o relay, legge solo intenzioni confermate e le invia al destinatario. Se il processo cade dopo la conferma locale, al riavvio la riga pending è ancora presente. La domanda «ricordiamo che cosa dobbiamo tentare?» ha finalmente una risposta indipendente dalla memoria volatile dell’agente.
Questo meccanismo non rende atomici i due database. Dopo che il magazzino applica l’effetto, il relay potrebbe interrompersi prima di segnare la consegna: la riga resta pending e sarà inviata di nuovo. Nell’implementazione didattica il contatore remoto aumenta una seconda volta se ogni richiesta viene trattata come nuova. La outbox elimina uno specifico vuoto tra decisione e messaggio, ma lascia aperta l’ambiguità della risposta persa.
L’identità appartiene all’intenzione, non al tentativo
Assegniamo alla prenotazione una chiave stabile, tenantA:reserve42. Ogni tentativo della stessa intenzione usa quella chiave. Il destinatario conserva una ricevuta che associa chiave, parametri e risultato. Se la ricevuta esiste e i parametri coincidono, restituisce il risultato precedente senza applicare un secondo effetto. Se i parametri differiscono, deve rifiutare il riuso ambiguo: prenotare due pezzi non è lo stesso comando che prenotarne uno.
L’ultima riga è decisiva. Se prima scrivessimo la ricevuta e poi applicassimo l’effetto in una transazione diversa, ricreeremmo il difetto iniziale nel destinatario. Se facessimo il contrario, avremmo ancora un intervallo in cui l’effetto esiste ma la ricevuta no. Nel modello entrambi vengono confermati insieme. In un servizio concorrente servono anche isolamento e vincoli di unicità correttamente gestiti: due worker non devono entrambi osservare «chiave assente» e applicare l’effetto.
Quindici prove: che cosa interrompiamo davvero
Il pacchetto Python crea due file SQLite nuovi per ciascuna prova: uno rappresenta l’orchestratore, l’altro il magazzino. I file restano nella cartella runs per rendere ispezionabile lo stato. Il magazzino contiene soltanto un contatore, non scorte reali. Un’eccezione interrompe il controllo in un punto prestabilito; il percorso di ripresa usa nuove connessioni. Non spegniamo il computer e non misuriamo la resistenza del disco a una caduta di alimentazione.
I punti sono: 0 prima della registrazione locale; 1 dopo quella registrazione ma prima dell’invio; 2 dopo l’effetto remoto, con risposta non osservata; 3 dopo la risposta ma prima della conferma locale finale; 4 dopo tale conferma. Nel caso 0 assumiamo che il chiamante ripresenti la stessa intenzione. Senza questa ipotesi, neppure una outbox può ricordare una richiesta mai registrata. Negli altri casi il relay riprende il lavoro pendente; una riga done non viene riprocessata.
| Protocollo / punto | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| mark-first | 1 | 0 | 1 | 1 | 1 |
| outbox-no-dedup | 1 | 1 | 2 | 2 | 1 |
| outbox-dedup | 1 | 1 | 1 | 1 | 1 |
Ogni cella conta gli effetti dopo la ripresa. Nel protocollo mark-first, che scrive done prima dell’invio, il punto 1 produce zero effetti: il registro locale impedisce proprio il tentativo necessario. Nella outbox senza deduplica, i punti 2 e 3 producono due effetti, perché il destinatario ha già agito e il relay non lo ha ancora registrato. La combinazione outbox più ricevuta atomica mantiene un effetto in tutti e cinque i casi modellati.

Non leggiamo il grafico come una percentuale di successo: abbiamo scelto cinque interruzioni, non campionato guasti da una distribuzione rappresentativa. È un esperimento per trovare controesempi e verificare una proprietà nel modello. Il codice controlla le tre sequenze di risultati con assert. Il frammento finale richiama lo stesso simulatore e stampa la tabella; il pacchetto contiene anche il codice che disegna le barre, così i numeri non dipendono da una figura costruita a mano.
La proprietà e le ipotesi che la sostengono
Per ogni chiave k, l’invariante desiderato è N(k)≤1: nessun secondo effetto per la stessa intenzione. Nel modello lo sostiene la transazione del destinatario: o non esistono né effetto né ricevuta, oppure esistono entrambi; una ricevuta conservata impedisce un nuovo incremento. Per ottenere anche N(k)≥1 occorre altro: un’intenzione conservata, tentativi che continuano, un destinatario che torna disponibile e una richiesta valida. La prima è una proprietà di sicurezza del protocollo; la seconda riguarda il progresso. Un sistema può evitare duplicati e restare bloccato per sempre.
Il linguaggio «exactly once» diventa quindi utile solo indicando il confine: un effetto logico per chiave, nelle condizioni dichiarate. Non significa un solo pacchetto di rete, un solo tentativo, né assenza di errori aziendali. Due intenzioni distinte con chiavi diverse possono prenotare lo stesso ricambio due volte, correttamente per il protocollo ma erroneamente per l’utente. Occorre perciò stabilire l’identità dell’azione accettata prima del ciclo di tentativi; chiedere al modello di rigenerare una chiave a ogni ripresa annulla la protezione.
Dove il modello didattico smette di bastare
La ricevuta ha una durata. Se viene cancellata prima dell’arrivo di un vecchio tentativo, quel messaggio può apparire nuovo. La finestra di conservazione deve quindi essere coerente con ritardi, code e politica dei tentativi; in alternativa un’epoca chiusa può rifiutare richieste ormai scadute. Questa politica non è implementata nell’esperimento. Inoltre il tenant fa parte dell’identità: una chiave uguale di due clienti non deve confondere due intenzioni indipendenti.
Un processo reale deve affrontare worker concorrenti, lease scadute, ordinamento di aggiornamenti sullo stesso oggetto, risposte parziali e richieste rifiutate definitivamente. Una prenotazione accettata dal sistema remoto potrebbe inoltre essere solo l’avvio di un lavoro asincrono: accettato e completato sono stati diversi. Se il destinatario non offre identità stabile e lettura dell’esito, dopo un timeout può restare necessario riconciliare lo stato anziché dichiarare successo. Nessun prompt risolve da solo l’assenza di queste informazioni.
Conclusione: che cosa può davvero dire l’agente
La registrazione locale dimostra che abbiamo conservato un’intenzione; una ricevuta remota verificata dimostra l’esito previsto dal contratto del destinatario. Nel modello, unirle mediante outbox, tentativi con identità stabile e deduplica atomica elimina sia la perdita del punto 1 sia i duplicati dei punti 2 e 3. È questo il percorso che giustifica «fatto», non la presenza della parola in un log. L’esperimento è didattico e non attesta una funzionalità disponibile o un risultato operativo di EL-AI.
Le fonti primarie consultate descrivono il pattern outbox e il contratto delle API idempotenti; abbiamo letto anche esempi implementativi e sezioni su richieste tardive e parametri diversi. Il simulatore, i controesempi e la tabella sono nostre elaborazioni eseguite, non benchmark degli autori. Ogni caso gestisce una sola intenzione e un numero costante di transizioni; il lavoro di controllo cresce linearmente con i casi, mentre costi del database, latenza di rete e gestione di code estese non sono stimati. Il prossimo passo sperimentale sarebbe l’iniezione di guasti nel servizio concreto, includendo concorrenza e scadenza delle ricevute: qui resta una proposta.
Fonti e riproducibilità
AWS Prescriptive Guidance — Transactional outbox pattern.
Malcolm Featonby — Making retries safe with idempotent APIs, Amazon Builders’ Library.
# Run beside experiment.py; all effects are local simulated counters.
from experiment import run
for protocol in ("mark-first", "outbox-no-dedup", "outbox-dedup"):
print(protocol, [run(protocol, cut)["effects"] for cut in range(5)])
Codice, dati e istruzioni · JSON. Calcoli didattici eseguiti con Python 3.14.0; figure con Matplotlib 3.11.2. Analisi con assistenza AI, senza dichiarare peer review o revisione umana. Copertina originale ImageGen, illustrativa: non documenta persone, sedi o installazioni EL-AI. Fonti consultate il 3 ottobre 2026.

